2020-01-14

Arduino 解析 SPI 訊號控制電子紙模組

最近尋找一些能夠在沒有電源的情況下仍然可以顯示資料的顯示器
一般顯示器都需要接駁電源才能顯示,如果要外出仍能使用便需要帶著電池
在下發現電子紙這種電子裝置是一種只使用非常低電功耗便可以改變內容,在沒有供應電壓的情況下仍然可以維持最後變動的內容

電子紙的更新速度比傳統顯示器慢得多,平均只有 3 FPS 更新速度,比起傳統監視器 12 FPS 更新速度更慢
由於更新速度慢,比較適合不需要即時更新的內容,例如電子書及一些指示牌
電子紙應用比較出名就是 Amazon Kindle 電子書,是一種非常低電功耗的電子書,只需要在更新頁面資料才需要給予電源

在下的電子紙是使用 SPI技術 來控制顯示內容
SPI 全名是 Serial Peripheral Interface 中文是 序列周邊介面 來控制
SPI 與 I2C 相似,都是能夠減少引腳使用數量

SPI 訊號

引腳初始化訊號週期
位元位結束
延遲2微秒2微秒
SS
SCK
MOSI高/低
MISO高/低

訊號波紋時序圖
SSSCKMOSIMISO

Arduino 已經提供 SPI函式庫 ,在 Arduino 程式標頭加上 #include <SPI.h> 便可以使用 SPI函式庫
Arduino UNO Rev3 預設使用
Arduino UNO Rev3 引腳SPI 引腳
10SS
13SCK
11MOSI
12MISO
作為 SPI引腳,再接駁正確,便可以使用 Arduino 的 SPI函式庫

#include <SPI.h>

void setup() {
    SPI.begin();
    SPI.transfer(0x00);
}

void loop() {
}
以上是最簡單透過 Arduino 的 SPI函式庫,傳輸 0x00 資料到目標裝置

SPI 模式

SPI 有 4種模式
模式時脈閒置電壓資料採取電壓
0
1
2
3

SPI 次序

SPI 可以分
  • 由 最高有效位 至 最低有效位 ,即是資料由 第7位元 至 第0位元 傳送
  • 由 最低有效位 至 最高有效位 ,即是資料由 第0位元 至 第7位元 傳送

在 Arduino 可以使用
#include <SPI.h>

void setup() {
    SPI.begin();
    SPI.setBitOrder(MSBFIRST);
//    SPI.setBitOrder(LSBFIRST);
    SPI.setDataMode(SPI_MODE0);
//    SPI.setDataMode(SPI_MODE1);
//    SPI.setDataMode(SPI_MODE2);
//    SPI.setDataMode(SPI_MODE3);
    SPI.transfer(0x00);
}

void loop() {
}

同樣使用 SPI技術,但不同裝置的 模式 及 次序 會有分別,需要了解目標裝置的運作原理才能正確應用 SPI

由於使用 Arduino 的 SPI函式庫,限制必須使用指定的 SS, SCK, MOSI, MISO 4支引腳
而在下希望能隨意改動需要使用的引腳,不過發現網上的資源都是使用 Arduino 的 SPI函式庫
亦不見可以自訂引腳的 SPI函式庫,因此在下自行編寫一套函式庫

SPIModule.h
#ifndef SPIMODULE_H
#define SPIMODULE_H

#include <Arduino.h>

class SPIModule {
    private:
        byte ssPin;
        byte mosiPin;
        byte misoPin;
        byte sckPin;
        void declare(byte, byte, byte, byte);
    public:
        SPIModule();
        SPIModule(byte, byte, byte, byte);
        void initial();
        byte transfer(byte);
};

#endif

SPIModule.cpp
#include "SPIModule.h"

void SPIModule::declare(byte ssPin, byte mosiPin, byte misoPin, byte sckPin) {
    this->ssPin = ssPin;
    this->mosiPin = mosiPin;
    this->misoPin = misoPin;
    this->sckPin = sckPin;
}

SPIModule::SPIModule() {
    this->declare(SS, MOSI, MISO, SCK);
}

SPIModule::SPIModule(byte ssPin, byte mosiPin, byte misoPin, byte sckPin) {
    this->declare(ssPin, mosiPin, misoPin, sckPin);
}

void SPIModule::initial() {
    pinMode(this->ssPin, OUTPUT);
    digitalWrite(this->ssPin, HIGH);
    pinMode(this->mosiPin, OUTPUT);
    pinMode(this->misoPin, INPUT);
    pinMode(this->sckPin, OUTPUT);
    digitalWrite(this->sckPin, HIGH);
}

byte SPIModule::transfer(byte data) {
    byte response = 0;
    digitalWrite(this->ssPin, LOW);
    for (byte i = 0x80; i > 0; i >>= 1) {
        digitalWrite(this->sckPin, LOW);
        digitalWrite(this->mosiPin, ((data & i) > 0) ? HIGH : LOW);
        digitalWrite(this->sckPin, HIGH);
        response |= ((digitalRead(this->misoPin) > LOW) ? i : 0);
    }
    digitalWrite(this->ssPin, HIGH);
    return response;
}

不知道閣下有沒有留意 SPIModule::transfer 在哪裡似層相識,就是在下編寫 PlayStation::sendCommand 功能
當時在下還未學習 I2C 及 SPI ,只是參巧其他 PlayStation 訊號分析文獻
但在下學習使用 電子紙 還需要使用 SPI 時發現 SPI 的 transfer 與 sendCommand 基本上是相同
因此在下估計 PlayStation 手掣的晶片都是其中一種使用 SPI 的晶片

電子紙

見下文
見下文
電子紙 正面

見下文
見下文
電子紙 背面
在下的 電子紙 為 4.2寸 ,解像度 闊 為 400高 為 300

見下文
電子紙 透過 柔性電路板 (Flexible Curcuit Board (FCB)) 連接到 電子紙模組

見下文
電子紙 使用 YF08E晶片 是其中一種 SPI晶片

YF08E引腳功能

引腳20引腳19引腳18引腳17引腳16引腳15引腳14引腳13引腳12引腳11
引腳1引腳2引腳3引腳4引腳5引腳6引腳7引腳8引腳9引腳10
描述
(缺口左邊)
B1VCC(B)B2B3B4B5B6B7B8GND
A1VCC(A)A2A3A4A5A6A7A8OE

引腳編號描述方向用途
1A1輸入輸出資料,第7位元;連接到目標裝置(A)引腳
2VCC(A)電源(A) ,接受 1.2V 至 3.6V , VCC(A) <= VCC(B)
3A2輸入輸出資料,第6位元;連接到目標裝置(A)引腳
4A3輸入輸出資料,第5位元;連接到目標裝置(A)引腳
5A4輸入輸出資料,第4位元;連接到目標裝置(A)引腳
6A5輸入輸出資料,第3位元;連接到目標裝置(A)引腳
7A6輸入輸出資料,第2位元;連接到目標裝置(A)引腳
8A7輸入輸出資料,第1位元;連接到目標裝置(A)引腳
9A8輸入輸出資料,第0位元;連接到目標裝置(A)引腳
10OE輸入
11GND接地
12B8輸入輸出資料,第0位元;連接到目標裝置(B)引腳
13B7輸入輸出資料,第1位元;連接到目標裝置(B)引腳
14B6輸入輸出資料,第2位元;連接到目標裝置(B)引腳
15B5輸入輸出資料,第3位元;連接到目標裝置(B)引腳
16B4輸入輸出資料,第4位元;連接到目標裝置(B)引腳
17B3輸入輸出資料,第5位元;連接到目標裝置(B)引腳
18B2輸入輸出資料,第6位元;連接到目標裝置(B)引腳
19VCC(B)電源(B),接受 1.65V 至 5.5V
20B1輸入輸出資料,第7位元;連接到目標裝置(B)引腳
由於主要是了解 電子紙 運作方式, YF08E晶片 在下不詳細描述

見下文
見下文
左端有一排 8支引腳 的 JST插口 (全名為 Japan Solderless Terminals)

見下文
電子紙公司會附送一條 JST線

見下文
JST線 另一端為 2.54mm引腳插口

見下文
通過 JST線 便可以接駁到 微控制器

見下文
右端有一排 8個通孔 ,需要自行焊接引腳
(不附送引腳)

見下文
焊接引腳則可以直接接駁到 麵包板

電子紙引腳功能

引腳編號描述方向用途
1VCC電源
2GND接地
3DIN輸入資料輸入
4CLK輸入時脈訊號
5CS輸入晶片選擇,低電壓選擇高電壓不選擇
6DC輸入模式切換,低電壓指令模式高電壓資料模式
7RST輸入重置所有設定
8BUSY輸出回應狀態,低電壓忙碌高電壓閒置

電子紙引腳對應SPI用途

電子紙 SPI 引腳名稱原 SPI 引腳名稱用途
CSSSSlaver 選擇
CLKSCK序列時脈
DINMOSIMaster 寫入資料到 Slaver
-MISOMaster 讀取 Slaver 資料

見下文
SPI 可以選擇 3線模式 或 4線模式
電子紙預設將一塊 SMD 0Ω 的電阻焊接在 0 的位置,即是使用 4線模式
SPI 可以只使用 3支引腳 即是 VCC, GND, MOSI 便能夠讓 SPI Master 控制 SPI Slaver
而完整功能亦只需要 4支引腳 除了基本 3支引腳 ,還有 MISO ,讓 SPI Master 接收 SPI Slaver 的訊號

電子紙暫存器

與 HD44780 相似,電子紙都有 暫存器 儲存著不同指示資料,而且資料數量比 HD44780 更多
DCRW第n位元 (* 不限制)用途
D7D6D5D4D3D2D1D0
0000000000板面設定
10########RES[1:0],REG,KW/R,UD,SHL,SHD_N,RST_N
0000000001電源設定
10------##VDS_EN,VDG_EN
10-----###VCOM_HV,VGHL_LV[1:0]
10--######VDH[5:0]
10--######VDL[5:0]
10--######VDHR[5:0]
0000000010關閉電源
0000000011關閉電源順序設定
10--##----T_VDS_OF
0000000100啟動電源
0000000101啟動電源測量
0000000110助推器軟啟動
10########BT_PHA[7:0]
10########BT_PHB[7:0]
10--######BT_PHC[5:0]
0000000111深度睡眠
1010100101Check code
0000010000顯示開始傳送(黑)
10########KPXL[1:8]
10---------
10########KPXL[n-1:n]
0000010001資料停止
11#-------KPXL[1:8]
0000010010顯示器重新整理
0000010011顯示開始傳送(紅)
10########KPXL[1:8]
10---------
10########KPXL[n-1:n]
0000100000VCOM LUT
0000100001W2W LUT
0000100010B2W LUT
0000100011W2B LUT
0000100100B2B LUT
0000110000鎖相迴路控制
10--######M[2:0],N[2:0]
0001000000溫度感應器校準
11########LM[10:3]/TSR[7:0]
11###-----LM[2:0]/-
0001000001溫度感應器選取
10#---####TSE,TO[3:0]
0001000010溫度感應器寫入
10########WATTR[7:0]
10########WMSB[7:0]
10########WLSB[7:0]
0001000011溫度感應器讀取
11########RMSB[7:0]
11########RLSB[7:0]
0001010000CDI
10########VBD[1:0],DDX[1:0],CDI[3:0]
0001010001低功耗檢測
11#-------LPD
0001100000TCON
10########S2G[3:0],G2S[3:0]
0001100001解像度設定
10-------#HRES[8:3]
10#####000HRES[8:3]
10-------#VRES[8:0]
10#####000VRES[8:0]
0001100101GSST
10-------#HRES[8:3]
10#####000HRES[8:3]
10-------#VRES[8:0]
10#####000VRES[8:0]
0001110001顯示狀態
11-#######PTL_FLAG,I C_BUSY,DATA_FLAG,PON,POF,BUSY
0010000000自動測量VCom
10--######AMVT[1:0],XON,AMVS,AMV,AMVE
0010000001讀取VCom
11--######VV[5:0]
0010000010VCM-DC
10--######VV[5:0]
0010010000部分視窗
10-------#HRST[8:3]
10#####000HRST[8:3]
10-------#HRED[8:3]
10#####111HRED[8:3]
10-------#VRST[8:0]
10########VRST[8:0]
10-------#VRED[8:0]
10########VRED[8:0]
10-------#PT_SCAN
0010010001部分輸入
0010010010部分輸出
0010100000編程模式
1010100101Check code
0010100001主動編程
0010100010讀取臨時密碼
11--------Read Dummy
11########Data of Address
11--------Read Dummy
11########Data of address
0011100011慳電模式
10########VCOM_W[3:0],SD_W[3:0]

電子紙啟動次序

同樣與 HD44780 相似,列表中 DC 為 0 是 指示暫存器DC 為 1 是 資料暫存器
指示暫存器 跟後有時會有一組 資料暫存器,這組資料就是用作設定 指示 的 資料
雖然 指示暫存器 有眾多不同功能的 指示
但實際要啟動 電子紙 只需要執行 助推器軟啟動啟動電源

執行 助推器軟啟動 指示 後,還需要提交 0x17, 0x17, 0x17 3組 資料
及 啟動電源 ,不過 啟動電源 則不需要提交資料

#include "SPIModule.h"

SPIModule spiModule = SPIModule();
const byte DC_PIN = 9;
const byte RST_PIN = 8;
const byte BUSY_PIN = 7;

void setup() {
    spiModule.initial();
    pinMode(DC_PIN, OUTPUT);
    pinMode(RST_PIN, OUTPUT);
    digitalWrite(RST_PIN, HIGH);
    pinMode(BUSY_PIN, INPUT);
    reset();
    // 助推器軟啟動
    sendCommand(0x06);
    sendData(0x17);
    sendData(0x17);
    sendData(0x17);
    // 啟動電源
    sendCommand(0x04);
}

void loop() {
}

void reset() {
    digitalWrite(RST_PIN, LOW);
    digitalWrite(RST_PIN, HIGH);
}

void sendCommand(byte data) {
    digitalWrite(DC_PIN, LOW);
    spiModule.transfer(data);
}

void sendData(byte data) {
    digitalWrite(DC_PIN, HIGH);
    spiModule.transfer(data);
}

啟動電子紙後便可以將資料寫入到電子紙來改變顯示的內容
電子紙每個像素都以 1位元 表達,該像素需要 顯示顏色時為 0不顯示顏色為 1
但電子紙不是以 布林值 (boolean 或 bool) 來控制像素,而是以 位元組 (byte 或 unsigned char) 來控制
由於 1位元組 = 8位元 ,因此每次寫入資料都會以 1位元組 來控制 8格像素 ,亦所以電子紙 闊度 都是 8倍數 ,高度 則不限

見下文
像素是由電子紙左上角位置開始排列,並以 MSB 次序顯示
即是左上角的像素顯示顏色便要寫入 0x00 (主要是不顯示 0x80 的資料)
寫入資料後,便需要執行 顯示器重新整理 的 指示 ,即是 0x12
但 執行 顯示器重新整理 後,電子紙 會處於 忙碌狀態 ,需要等待回復到 閒置狀態 才能再 執行指示 或 寫入資料
當 BUSY引腳 是 低電壓 為 忙碌狀態,高電壓 為 閒置狀態
// 顯示開始傳送(黑)
sendCommand(0x10);
// 寫入資料
sendData(0x00);
// 顯示器重新整理
sendCommand(0x12);
while (digitalRead(BUSY_PIN) == LOW) {
    // 有需要可以顯示狀態變化
}

見下文
資料會由 左上角 至 右下角 順序寫入
當連續寫入資料,都會在第一列顯示
// 顯示開始傳送(黑)
sendCommand(0x10);
// 寫入資料
for (byte i = 0; i < 8; i++) {
    sendData(0x00);
}
// 顯示器重新整理
sendCommand(0x12);
while (digitalRead(BUSY_PIN) == LOW) {
    // 有需要可以顯示狀態變化
}

但如果只是變動電子紙一小部分位內容卻要將整個畫面更新是非常浪費資源及時間
因此 電子紙 的 指示暫存器 提供指定範圍
  1. 執行指示 部分輸入 0x91
  2. 執行指示 部分視窗 0x90
  3. 設定部分視窗的資料,共8位元組
    1. 設定資料 水平開始位置 (闊度 超過 248像素使用,只接受 第0位元,其餘位元忽視)
    2. 設定資料 水平開始位置 (第2, 1, 0位元 必定為 0 ,限制必定為 8n)
    3. 設定資料 水平結束位置 (闊度 超過 248像素使用,只接受 第0位元,其餘位元忽視)
    4. 設定資料 水平結束位置 (第2, 1, 0位元 必定為 1 ,限制必定為 8n + 7)
    5. 設定資料 垂直開始位置 (高度 超過 248像素使用,只接受 第0位元,其餘位元忽視)
    6. 設定資料 垂直開始位置
    7. 設定資料 垂直結束位置 (高度 超過 248像素使用,只接受 第0位元,其餘位元忽視)
    8. 設定資料 垂直結束位置
  4. 設定資料 結束部分視窗 0x01
  5. 執行指示 顯示開始傳送 (0x10 或 0x13)
  6. 寫入資料
  7. 執行指示 部分輸出 0x92

// 部分輸入
sendCommand(0x91);
// 部分視窗
sendCommand(0x90);
sendData(0x00);
sendData(0x00);
sendData(0x00);
sendData(0x3F);
sendData(0x00);
sendData(0x00);
sendData(0x00);
sendData(0x00);
sendData(0x01);
// 顯示開始傳送(黑)
sendCommand(0x10);
// 寫入資料
for (byte i = 0; i < 8; i++) {
    sendData(0x00);
}
// 部分輸出
sendCommand(0x92);
// 顯示器重新整理
sendCommand(0x12);
while (digitalRead(BUSY_PIN) == LOW) {
    // 有需要可以顯示狀態變化
}
x1 = 0x00 * 256 + 0x00 = 0
x2 = 0x00 * 256 + 0x3F = 63
y1 = 0x00 * 256 + 0x00 = 0
y2 = 0x00 * 256 + 0x00 = 0
表示資料會寫入至 (0,0) 至 (63,0) 之間 (與不使用 部分視窗 指示 相同)

見下文
將 部分視窗 指示 的 設定資料修改為
sendCommand(0x90);
sendData(0x00);
sendData(0x00);
sendData(0x00);
sendData(0x1F);
sendData(0x00);
sendData(0x00);
sendData(0x00);
sendData(0x01);
sendData(0x01);
x1 = 0x00 * 256 + 0x00 = 0
x2 = 0x00 * 256 + 0x1F = 31
y1 = 0x00 * 256 + 0x00 = 0
y2 = 0x00 * 256 + 0x01 = 1
表示資料會寫入至 (0,0) 至 (31,1) 之間

見下文
將 部分視窗 指示 的 設定資料修改為
sendCommand(0x90);
sendData(0x00);
sendData(0x00);
sendData(0x00);
sendData(0x0F);
sendData(0x00);
sendData(0x00);
sendData(0x00);
sendData(0x03);
sendData(0x01);
x1 = 0x00 * 256 + 0x00 = 0
x2 = 0x00 * 256 + 0x0F = 15
y1 = 0x00 * 256 + 0x00 = 0
y2 = 0x00 * 256 + 0x03 = 3
表示資料會寫入至 (0,0) 至 (15,3) 之間

見下文
將 部分視窗 指示 的 設定資料修改為
sendCommand(0x90);
sendData(0x00);
sendData(0x00);
sendData(0x00);
sendData(0x0F);
sendData(0x00);
sendData(0x00);
sendData(0x00);
sendData(0x02);
sendData(0x01);
x1 = 0x00 * 256 + 0x00 = 0
x2 = 0x00 * 256 + 0x07 = 7
y1 = 0x00 * 256 + 0x00 = 0
y2 = 0x00 * 256 + 0x07 = 7
表示資料會寫入至 (0,0) 至 (7,7) 之間

見下文
透過 Arduino 使用 控制 電子紙 顯示圖案

見下文
在下使用的 電子紙 還能夠 接收 顯示開始傳送(紅) 指示 以 紅色電子墨水 顯示圖案

見下文
顯示圖案後可以不接駁電源仍能保留內容

見下文
電子紙 更新畫面的情況
以黑色電子墨水顯示,全畫面更新時間大約需要7秒
以紅色電子墨水顯示,全畫面更新時間大約需要13秒 (只要有1個像素使用紅字電子墨水便需要較長時間)

見下文
還可以同時顯示多種顏色
在下在電子紙上顯示 Linux Tux ,不可能手工逐點點印,在下是借用 ImageMagick 將圖案轉換成 可移植點陣圖 (Portable BitMap (PBM))
在 Terminal 輸入
sudo apt-get install imagemagick
按此安裝 ImageMagick
並將 PBM 內容,以每8格資料分割,最後將 8格資料(位元) 轉換成 十六進制資料 便完成
但留意 電子紙技術 0為黑色, 1為白色, PBM 卻是 0為白色(無色), 1為黑色(有色) ,轉換後需要將 0 與 1 互換才能正確顯示圖像的顏色
#!/bin/bash

file="image"
width=`convert "${file}" -print "%w" "/dev/null"`
if [ $(($width%8)) -eq "0" ]; then
    data=`convert "${file}" -flatten -monochrome -compress none pbm:- | tail +3 | tr -d " " | sed -r "s/(.{8})/\1, /g"`
    v="0"
    while [ "${v}" -le "255" ]; do
        b=`printf "%08d" $(echo "obase=2;${v}" | bc)`
        h=`printf "0x%02X" $((255-$v))`
        data=`echo "${data}" | sed -r "s/${b}/${h}/g"`
        v=$(($v+1))
    done
    echo "${data}"
else
    echo "Image width is not multiple of 8"
fi

見下文
不同型號的 Arduino 的記憶空間不同,如果資料太多便不能將資料寫入至 Arduino 記憶空間
例如在下在 電子紙 顯示 Linux Tux 的圖案的資料量超過在下使用的 Arduino 的記憶空間
因此在下將資料分開寫入,電子紙會將寫入的資料保存,並在接收 顯示器重新整理 指示後,才一次過將資料顯示

由於原始碼資料篇幅比較長,因此在下不在此長篇大論
閣下有興趣可以到 https://create.arduino.cc/editor/hkgoldenmra/fe3904fb-41bf-4f29-a999-e78e5657821d/preview
https://bitbucket.org/hkgoldenmra/epaperspimodule
或在 Terminal 輸入
git clone "https://bitbucket.org/hkgoldenmra/epaperspimodule.git" --depth=1

總結

電子紙雖然與 LCD熒幕操作相似,但操作上仍然有不同,而且操作上比 LCD熒幕更複雜
另外由於在下購買電子紙時沒有留意是連接 SPI模組,未能以原始方式操作電子紙
由於電子紙可以在沒有電源供應情況下保持圖案,其實很適合在一些只是展示、陳列、不常更新資訊的地方使用
而且 Society for Information Display 2019 來自世界各地的顯示器參展商,都展示不同類型的顯示器
展覽中有電子書參展商展示出使用全色電子墨水在電子紙上顯示彩色圖像 及 與 Android 整合的觸控平板電腦

參考資料

E-Ink Corporation

Good e-Reader

2 則留言 :