【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 给它输出模拟电压,也能达到调光的效果。提前是背光线刚好连接到支持 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 一下,这样我们在运行阶段能看到分块填充的效果。

 

好了,今天就水到这里了。

posted @ 2024-04-28 22:23  东邪独孤  阅读(1803)  评论(2编辑  收藏  举报