【Nano Framework ESP32篇】使用 LCD 屏幕

在開始主題之前,先介紹一個刷固件工具。這個工具在 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 老周有這個,黃色外殼,跟U盤差不多大。

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 給它輸出模擬電壓,也能達到調光的效果。

.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 一下,這樣我們在運行階段能看到分塊填充的效果。

 

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章