【Nano Framework ESP32篇】使用 LCD 螢幕

东邪独孤發表於2024-04-28

在開始主題之前,先介紹一個刷韌體工具。這個工具在 idf 中是整合的,不過,樂鑫也單獨釋出了這個工具—— esptool。下載連結:Releases · espressif/esptool · GitHub。這貨是用 Python 寫的,只是封裝成了 exe,方便直接執行罷了。

在使用時,需要 -p 引數指定串列埠號,如 COM15,-b 指定波特率(可以省略)。下面咱們嘗試用 flash_id 指令來獲取 ESP32 的 Flash 資訊。

esptool -p com13 flash_id

輸出結果如下:

老周有很多塊 esp 開發板,如你所見,這塊板的 flash 是 16MB 的。請記住這個容量,待會刷 nanoCLR 時它會出事故。咱們再看看另一塊板的 flash 資訊。

這個是 8MB 的,注意在晶片名稱後有個 revision 引數(修訂號),因為找韌體時,要考慮這個引數,3 以上的才能選 rev3 的韌體,其他只能選 rev0。

有時候,韌體也分有 PSRAM 和 無 PSRAM 的,不過這個一般能通用。

接下來,老周先說明一下如何解決 bootloader 的 Hash 驗證導致的問題。咱們重現一下災難現場:

A、找一個韌體,解壓。

B、咱們用上面 16MB 那個來刷。

esptool -p COM9 -b 115200 write_flash -fs 16MB -fm dio -ff 40m 0x1000 "E:\demo\bootloader.bin" 0x8000 "E:\demo\partitions_16mb.bin" 0x10000 "E:\demo\nanoCLR.bin"

write_flash 指令就是刷韌體,把檔案寫入 Flash。它常用這些選項:

1、-fs:flash大小,如 8MB、16MB;

2、-fm:SPI 模式,如 dio、qio、dout;

3、-ff:通訊速率,如 40m(一般就這個值)。

選項之後就是檔案列表,列表按照 <偏移地址> <檔案路徑>的方式依次列出,比如上面的 0x1000 bootloader.bin 就是在0x1000處寫入 bootloader。在 0x8000 處寫入分割槽表。因為 flash 是 16MB 的,所以我就用 16MB 的分割槽表。

刷完後,開啟串列埠讀取一下資訊,看看有沒有正常啟動 CLR。咱們不需要安裝串列埠工具,在 VS Code 的擴充套件裡面就有,叫 Serial Monitor,屬於微軟大法的一種。安裝擴充套件後開啟終端皮膚,你會看到有個串列埠監視器,切換過去,選擇串列埠號,點選【開始監視】即可。

C、你會發現 ESP32 在無限重啟。停止串列埠監視後檢視錯誤。

不是 16MB 的嗎,怎麼變成 4MB 的。這裡它是認為 Flash 是 4MB的,刷入了 16MB 的分割槽表,自然就會認為超出容量,所以後面的分割槽找不到了。

如果你的命令視窗還沒關閉,你可以回去看看剛才執行 esptool 後輸出的警告:

Warning: Image file at 0x1000 is protected with a hash checksum, so not changing the flash size setting. Use the --flash_size=keep option instead of --flash_size=16MB in order to remove this warning, or use the --dont-append-digest option for the elf2image command in order to generate an image file without a hash checksum

bootloader 在生成的時候嵌入了 SHA256 的雜湊值,也就是說這個映象在編譯時是配置為 4MB 大小的。由於無法透過校驗,只能按 4M 的大小來刷,16 MB 的分割槽當然超出範圍了。樂鑫團隊稱將來的版本可能會取消這個校驗,但目前是需要校驗的。

解決方案:

方案1:刷韌體時,選 4MB 的分割槽表。這是最簡單的方案。

方案2:自己編譯韌體。如果你有配置 IDF 環境(注意是 4xx 版本的,不是最新的),然後 clone 下 nf-interpreter 專案,可以自己把 SDK 配置為 16MB,編譯出來的 bootloader 就是匹配 16 MB 的。

方案3:這個好搞一些。直接編譯個 bootloader 替換掉原來的,就不用重新編譯 nanoCLR 了。

下面老周就演示一下如何進行方案3。bootloader 是通用的,不必考慮 IDF 版本。IDF 版本切換比較麻煩,可以用多個非安裝版 VS Code,來配置多個環境,或者用多個 WSL 來配置也行。反正你愛咋弄就咋弄。或者直接寫個指令碼來配置環境變數,然後啟動相關程式。

不過,這裡咱們只用到 bootloader,可以用最新的 IDF 去編譯,體積就大一點點,能刷進去的,不影響,畢竟在 0x1000 - 0x8000 的空間範圍是夠用的,不用也白浪費。

開啟 VS Code,點選側邊欄上的樂鑫圖示,選擇“New Project Wizard”。

然後等待兩年半,會開啟一個配置頁。

後面的串列埠號 ESP 晶片型別的可以不管,後面可以透過狀態列圖示修改。然後點 Choose Template。

到了這裡,要選一個專案模板。分組選 ESP-IDF,找到 get-start,用 sample_project 模板就行了,這個最簡潔,比 hello XXX 還簡潔。

選擇好後,點 Create project using template <你選的模板> 按鈕,然後它會提示你是否開啟建立的專案,如果 Yes,會用新視窗開啟。如果不想這麼反人類,可以選 No,然後在 VS Code 中手動開啟專案目錄即可。

這個專案咱們不用寫程式碼,在狀態列中找到 SDK 配置按鈕,點它,然後等待兩年半。

這個設定頁經常會無響應,如果等了三年半還沒開啟配置頁,可以點取消,然後重新點配置按鈕。

配置頁開啟後,找到“Serial flasher config” 節點,注意是頂層節點,不是 Bootloader config 下面那個。把 Flash size 改為你要的大小,比如這裡我要 16 MB。

設定好後點選頁面頂部的 【儲存】 按鈕,最後關閉頁面。

直接編譯專案即可。這裡老周是圖方便,畢竟用專案來編譯 bootloader 可以少很多麻煩。當然,bootloader 是可以單獨編譯的,在IDF的 components\bootloader\subproject 目錄下就是獨立的 bootloader 專案。不要直接在這裡操作,而是把 subproject 目錄中的內容複製到其他目錄再編譯,這可以保證 SDK 目錄不受破壞。這種方法配置起來特煩,而且容易報一堆錯,所以還是直接編譯專案來得爽。

專案編譯後會生成 bootloader.bin 檔案,把它複製並替換 nanoCLR 中的 bootloader(在 build\bootloader 目錄下找到 bootloader.bin 檔案)。接著,重新刷一下 nanoCLR 韌體。

esptool -c esp32 -p COM9 -b 115200 write_flash -fs 16MB -fm dio -ff 40m 0x1000 "E:\demo\bootloader.bin" 0x8000 "E:\demo\partitions_16mb.bin" 0x10000 "E:\demo\nanoCLR.bin"

刷寫完成後,再開啟串列埠監視器,你能看到你想要的東西(也可以用 flash_download_tool 來燒錄的)。

啟動 VS,開啟 Device explorer,點“Ping Device” 按鈕,如果看到 “XXX @ COM9 is active running nanoCLR.“ 的字樣就說明沒問題了。

----------------------------------------------------------------------------------------------------------------------------------

好了,正片現在開始。要在 .NET Nano 中使用 LCD 螢幕,必須使用帶有圖形驅動的韌體,否則是無法執行的。因為 .NET 類庫不帶驅動。開啟韌體下載頁:Cloudsmith - Repositories - .NET nanoFramework (net-nanoframework) - nanoframework-images (nanoframework-images) - Packages

點選 package groups,進入分組檢視,這樣找韌體方便。

支援圖形驅動的有以下幾組:

a、ESP32_GenericDisplay_REV0:通用型,針對 revision < 3 的板子。

b、ESP32_PSRAM_BLE_GenericGraphic_REV3:通用版,支援 BLE,要求 Revision >= 3。

c、面向 M5Stack Core 或 Core 2 的韌體;

d、面向 M5StickC 和 M5StickCPlus 的韌體。

M5StickCPlus 2 是最新改進的,但 nanoCLR 沒有區配,不過,經過老周測試,M5StickC Plus 2 能正常使用。M5StuckC Plus 2 老周有這個,黃色外殼,跟隨身碟差不多大。

M5Stack 的東西,說實話,價格偏高,做工也一般般。唯一的好處是有外殼(雖然外殼也是歪的),做成品放到專案上用比較方便。當然,如果批次使用也可以找人設計外殼,再給工廠批次做,這比買 M5Stack 的價效比高。

不過,老周今天拿來演示的是另一款。這款是高仿 M5Stack Core 的,價格便宜了一半,唯一不同的是,比 M5Stack 少了功放晶片,不能用 i2S 輸出音訊,喇叭是直接連到 GPIO 25 的,用 DAC 來輸出。ESP32 的數模轉換隻有 8 位,所以音質嘛,也就是聽個響。

這個開發板只能刷通用韌體,即 ESP32_GenericDisplay_REV0。剛才老周已經用替換 bootloader 的方式刷了 16MB 的韌體,待會咱們可以直接程式設計。

LCD 螢幕的驅動晶片,見得多的是 St77xx 和 iLi93xx。如 St7789、iLi9341 等。這些晶片雖然多,不過用法差不多,99.997% 用 SPI 協議,所以咱們也不用關心時序的事了。但有個別引腳也要注意的,如區分命令(Command)和資料(Data)的資料線,復位線等。

說是SPI 協議,但這些玩意兒有 N 多種接線方式,有單線、雙、四、八、十六線通訊的接法。不過,以老周淺薄的經驗來看,單線和八線的見得多。

1、八線:即資料線有八根,D0 - D7,一根線發一個位,一起傳送一次可以發一個位元組。八根線統一由時鐘線來控制,時鐘快慢決定了發資料的速度。這種接線法太浪費 IO 口,ESP32 本身引腳不多,所以,ESP 系列開發板很少這種連線,倒是 K210 開發板較多。

2、單線:即一根資料線,由時鐘線控制。ESP 系列開發板一般是這種接法。

由於只有一根線(MOSI),沒有 MISO 連線,所以寫的時候方便,讀的時候就難搞。如果真要讀,就得重新初始化 SPI,把連線資料線的引腳調為 MISO,讀完後又重新初始化為寫(MOSI)。想法是這樣,但老周從未試過,畢竟這樣折騰比較影響效率。最重要的是,LCD 屏最主要的任務是顯示,咱們儘管向它寫資料就夠了,很少會讀資料。

這裡順便解釋一個容易被誤解的事。很多大夥伴(不管你用C語言,Arduino 或別的)在入門的時候都遇到過螢幕無法點亮的事。然後大夥就各種自我檢討,是我協議設定不對嗎?是我用的這個庫封裝有錯?是我的板子掛了?還是……人品問題。如果你沒做過什麼見不得人的事,那不用懷疑人品。其實是大夥在看原理圖時沒認真看。K210 開發板一般不會單獨接背光的線,所以你在 K210 開發上可以寫暫存器來調光。可是,許多 ESP32 開發板是有一根專門的背光線的。例如,請看下面這個原理圖。

這個圖告訴你,G7 控制 LCD 的背光。再看另一張圖。

這個比較複雜,背光開關 EN 接 G27,即 G27 是 LCD 背光控制線。SGM2578 控制電力分配(可能是帶電池的原因,電池和外部供電的均衡),WS4622 是控制LED的 RGB 通道調光用的,和 WS 2812 等是一類貨色。

這就是你點不亮螢幕的原因,背光線是獨立連線的,你寫驅動晶片的暫存器是不起作用的,你必須給背光線輸出高電平,LCD 屏才會亮起。當然了,你給它輸出 PWM 也行,還能調亮度呢,但可能會頻閃;不想頻閃的話可以用 DAC 給它輸出模擬電壓,也能達到調光的效果。提前是背光線剛好連線到支援 DAC 的引腳上,ESP32 有兩個固定的 IO 口—— G25、G26。

.NET Nano 封裝的 .NET API 在 Graphics 包中,所以,開啟 Nuget 包管理器,安裝 nanoFramework.Graphics 包,另外,咱們要操作 SPI 和 GPIO(GPIO是那根背光線,我們要讓螢幕亮起),還要安裝以下三個包:

nanoFramework.System.Device.Spi;

nanoFramework.System.Device.Gpio;

nanoFramework.Hardware.Esp32

老周這款高仿板用的是 iLi9342C,用 iLi9341 的驅動也通用。還得安裝一個 iLi9642 的專用包:nanoFramework.Graphics.Ili9342。如果你用的是其他晶片,可以安裝對應的包,如 St7735 等。

先宣告一下要用到的引腳,這個你要按照你的開發板來,找賣家要原理圖。如果賣家不給或給的圖是錯的,可以退貨。老周就因為這個原因退過兩次貨。

const int PIN_CLK = 18;   // 時鐘線
const int PIN_MOSI = 23;  // 資料線
const int PIN_MISO = 34;  // 用不上,但需要指定
const int LCD_DC = 27;    // 命令/資料切換線
const int LCD_RESET = 33; // 復位線
const int LCD_CS = 14;    // 片選
const int LCD_BL = 32;    // 背光線

時鐘線和資料線就不說了,和標準 SPI 的含義一樣。有一條 D/C 線,有的叫 W/S 線,它的作用時:D/C 低電平時表示傳送命令,D/C 高電平時表示發資料。復位線:高電平正常,低電平復位。在初始化時,先拉低復位線,然後進行各種初始化設定,完成後再把復位線拉高,復位完畢。

傳送命令的過程:D/C線拉低 ----> 寫入命令(通常就是一個位元組);

傳送資料的過程:D/C線拉高 ----> 寫入資料(可能是一個位元組,可能是多個,也可能是0個,如果沒有資料,這個過程直接忽略)。

這幾個驅動晶片用起來都差不多,就是寫暫存器,甚至連暫存器的編號都相同。

當然,咱們用封裝過的 iot 框架的目的,就是犧牲效能來換取開發應用的便捷,所以 .NET Nano 已經封裝好了,咱們不用去寫暫存器。使用 DisplayControl 類(nanoFramework.UI 名稱空間)就能往 LCD 屏裡寫入顏色。這個類公開的都是靜態成員,不用例項化。

1、初始化引腳功能。由於 ESP32 的引腳是複用的,所以對於 SPI 的時鐘線、資料線要設定。

Configuration.SetPinFunction(PIN_MOSI, DeviceFunction.SPI1_MOSI);
Configuration.SetPinFunction(PIN_CLK, DeviceFunction.SPI1_CLOCK);
Configuration.SetPinFunction(PIN_MISO, DeviceFunction.SPI1_MISO);

2、先給背光線來一波高電平,不然LCD不亮。

GpioController ctrl = new();
var pinbl = ctrl.OpenPin(LCD_BL);
pinbl.SetPinMode(PinMode.Output);
pinbl.Write(PinValue.High);

3、配置控制螢幕的 SPI 引數,型別是 SpiConfiguration, 也是在 UI 名稱空間下。

SpiConfiguration spicfg = new(
        spiBus: 1,
        chipselect: LCD_CS,
        dataCommand: LCD_DC,
        reset: LCD_RESET,
        backLight: -1        // 這裡不用指定背光線,要單獨控制才有效
    );

注意不要在這裡指定 backLight 引數,點不亮的,因為許多板子,背光線不是整合在螢幕上,也就不會與螢幕直接連線,所以設定這個是無效的,我們剛剛單獨處理了。

4、用 ScreenConfiguration 類配置螢幕引數,如寬度、高度,還有x、y座標的偏移。這個偏移是需要的,因為不同的螢幕不一樣,有的要偏移 45,有的則要偏移 52。這個可以透過實驗不斷調校,調到合適的值就好。主要是因為顯示的內容不一定是從螢幕左上角開始的,經常會跑到螢幕外面。K210 的板子不用調整這個,但 ESP32 的板子需要調整,原因未知。

// 獲取驅動
var driver = Ili9342.GraphicDriver;

// 自定義初始化
driver.InitializationSequence = new byte[]
{
    (byte)GraphicDriverCommandType.Command, 1, 0x21,
    (byte)GraphicDriverCommandType.Command, 2, 0x3a, 0x55,
    (byte)GraphicDriverCommandType.Command, 5, 0x2a, 0x00, 0x00, 0x01, 0x3f,
    (byte)GraphicDriverCommandType.Command, 5, 0x2b, 0x00, 0x00, 0x00, 0xef,
    (byte)GraphicDriverCommandType.Command, 1, 0x11,
    (byte)GraphicDriverCommandType.Command, 1, 0x29
};

// 配置螢幕
ScreenConfiguration scrcfg = new(
        0,
        0,
        320,
        240,
        driver
    );

透過 Ili9342.GraphicDriver 靜態成員可以獲得相關的驅動。如果你的板子是 St7789,那就改為對應的類。注意上面程式碼中高亮的部分,即

driver.InitializationSequence = new byte[]
{
    (byte)GraphicDriverCommandType.Command, 1, 0x21,
    (byte)GraphicDriverCommandType.Command, 2, 0x3a, 0x55,
    (byte)GraphicDriverCommandType.Command, 5, 0x2a, 0x00, 0x00, 0x01, 0x3f,
    (byte)GraphicDriverCommandType.Command, 5, 0x2b, 0x00, 0x00, 0x00, 0xef,
    (byte)GraphicDriverCommandType.Command, 1, 0x11,
    (byte)GraphicDriverCommandType.Command, 1, 0x29
};

這裡是設定初始化指令,這個是因為老周這個板特別,iLi9342 預設上電是正色顯示的(即關閉反色),可是這塊鳥板正色時它顯示反色,反色時它卻顯示正色。所以,預設的初始化方式不適用,只能自己寫暫存器了。其實這些晶片上電時很多預設值都能用的,並不需要改太多的暫存器。

老周簡音介紹一下這個指令的格式。

1)這些指令就是 byte 陣列;

2)多條指令可以連線寫到一個資料組中;

3)每條指令的第一個位元組代表指令類別。1 表示一條正常傳送的指令(Command),0 表示 Sleep。你可別誤會,這個 Sleep 不是讓 LCD驅動晶片休眠,而是暫停一下(就像 Thread.Sleep 方法),SPI 不傳送罷了。

4)如果是第一個位元組是 Command,那麼第二個位元組是長度(SPI要發多少位元組),從第三個位元組起就是真正要傳送的。比如,0x01,0x03,0x22,0x15,0x8d。第一個 0x01 表明它是一條正常發出的指令,第二個是 03 表示後面有三個位元組,而 SPI 真正傳送的是 0x22,0x15,0x8d。這三個位元組中,0x22 表示驅動命令(暫存器),0x15和0x8d表示要寫入暫存器的資料。

5)如果第一個位元組是 Sleep,後面需要跟一個位元組,表示暫停時長,單位是 10ms。比如,0x00,0x02,第一個 0x00 表示暫停,0x02 表示暫停 2 * 10 = 20 毫秒。

好,弄懂這個,咱們回頭看看老周剛寫的初始化命令:

=> 傳送 0x21,單命令,沒有引數,所以只有一個位元組。0x21 本來是開啟反色顯示的,關閉反色顯示是 0x20 暫存器。由於老周這塊板不知怎麼回事,是反過來的。

=> 傳送 0x3a,設定畫素格式為 16 位,RGB565。0x55 上這麼來的:

第1-3位設定DBI,第5-7位設定DPI,請看下錶:

為了減少不必要的麻煩,通常咱們記憶體處理的畫素格式和螢幕顯示的一樣,所以左右兩邊都是 101,即5,合起來就是 0x55。

=> 傳送 0x2a,設定列的座標空間,即我們要寫入螢幕畫素的水平範圍。這個螢幕的長是 320,所以,命令引數有四個位元組。前兩個表示起點,即0;後兩個表示終點,0x01,0x3f 組合的16位整數是 319。座標從 0 起算,320就是319。

=> 傳送 0x3b 命令,表示行的座標空間,引數也是四個位元組,範圍 0- 239(240即239,要減1)。

=> 傳送 0x11 命令,表示讓 LCD 離開休眠模式,從而喚醒螢幕,上電時預設休眠。

=> 傳送 0x29 命令,開啟顯示模式,正常呈現畫面。

在例項化 ScreenConfiguration 物件時,提供這些引數:

a、螢幕左上角座標,我這裡設定為 0,0,剛剛好,沒有偏。如果你測試發現顯示的內容跑到螢幕外了,就要適當設定一下偏移座標,如x=45,y=52。

b、螢幕寬度和高度,這裡是 320 * 240。

c、驅動物件,就是剛從 Ili9342.GraphicDriver 返回的。

5、初始化 DisplayControl。

_ = DisplayControl.Initialize(spicfg, scrcfg, 10240);

最後的引數 10240 是預先分配的記憶體大小,不要弄太大,開發板的執行記憶體小到無語,分配太大了容易爆。Initialize 方法返回實際可分配的記憶體,如果記憶體不夠,返回的值可能比你指定的小。這裡我不理它,直接忽略。

6、如果能正常使用,這個時候已經可以向螢幕寫資料了,咱們把全螢幕填充為藍色。

 // 清空螢幕
 DisplayControl.Clear();
 ushort color = Color.Red.ToBgr565();
 // 寬高
 ushort dw = 80, dh = 80;
 ushort[] bf = new ushort[dw * dh];

 for(int i = 0; i < bf.Length; i++)
 {
     bf[i] = color;
 }

 while (true)
 {
     for(ushort x = 0; x < 320; x+=80 )
     {
         for(ushort y = 0; y < 239; y += 80)
         {
             DisplayControl.Write(x, y, dw, dh, bf);
             Thread.Sleep(300);
         }
     }
     Thread.Sleep(1000);
     DisplayControl.Clear();
 }

畫素格式是 16 位的,即,RGB 加起來16位,正好用一個 uint16 (ushort)可以表示。565表示 R 佔5位,G 佔6位,剩下5位留給 B。這裡明顯綠色多佔了一位,難道 LCD 屏看上去有些綠。

DisplayControl 類雖然封裝後呼叫方便,但這種封裝……反正老周有意見。原因有:1、只能在初始化時修改暫存器,顯示內容後無法改了;2、這東西耗記憶體。

所以,建立用來表示畫素的 ushort 陣列不能太大,否則會因為記憶體溢位而無法執行。320 * 240 個 ushort 值會報錯。這樣就不能一次性填充整個螢幕了,只能分塊來填,每塊 80 * 80,所以,橫著填四塊,堅著填三塊。在填充完一塊後,老周故意 Sleep 一下,這樣我們在執行階段能看到分塊填充的效果。

好了,今天就水到這裡了。

相關文章