来源

有个朋友手搓了一个二氧化碳浓度计,我就在想自己是不是也能复刻一个。最好是能整一个屏幕,方便快速快速查看。最好要能上报数据,实现长时间的检测需求

硬件选择

  • esp32 作为核心,驱动传感器获取数据和驱动水墨屏展示结果。随便在网上买了一个,具体型号为 ESP32-WROOM-32E
    • 根据后来的研究,这个型号应该是比较老的型号了,架构比较旧。如果能买 esp32c3 这种新型号可能会好一点
  • scd41 作为二氧化碳传感器,也是随便买了一个
  • waveshare 的 2.9inch e-Paper 作为显示屏

折腾流程

点亮 esp32

esp32 的文档还是相对丰富,能查到不少的资料。但是为了给自己上点难度,也为了练习 rust ,我选择了 esp-rs 作为开发框架。它的文档也还不错,手把手教你配置环境。不过不知道为啥,在写这篇文章的时候,网站挂掉了

根据文档配置好环境后,运行示例程序,成功将程序在 esp32 上跑起来。为了证明程序跑起来了,我找来了一个从旧电脑上拆下来的电源灯插到了某一个数据接口上,通过控制接口的高低电平,成功控制了灯的开关。(翻了下手机相册,居然已经是两年以前的事情了,当时成功点亮了过后,就很长一段时间没管了)

然而,在后面的研究过程中,我发现我买的这个板子上 D2 数据口并联了一个自带一个 LED 灯(竟敢无视灯

点亮屏幕

点亮了板子过后就开始研究点亮屏幕。搜了一大堆资料,研究了一下,嵌入式行业有两个比较常见的通信标准,一个是 I2C ,另一个是 SPI

I2C 比较暴力,每个硬件有一个地址,主机连接多个从机,从机并联,只需要两根电源线(VCC/GND)+一个时钟线(CLK)+一根数据线(SDA)。主机向某个从机发送数据时,需要对具体的地址进行操作。这样的设计效率肯定非常低,但是更加简单,适合传感器这种反应慢,数据传输的也简单的组件。所以传感器 scd41 就用了 I2C 和 esp32 通信

SPI 比较复杂,且主从是一对一,有对应的复杂的协议,线也比较多,这里就不展开描述了。总之,按照顺序把线接到对应的接口,然后在代码里初始化的时候用对应的接口就行

但是只是在代码里初始化出来一个 SPI 接口,是没办法简单控制屏幕的。要想在屏幕上展示东西,还得按照屏幕的示例程序向屏幕驱动版里写数据

然而,屏幕驱动示例程序给出的是 cpp 的例子,因此需要手动将给出的程序翻译成 rust 。这个过程是比较耗时的,主要是从代码的角度来看,很多地方都只告诉了你要这样做,但是你不知道为什么要这样做,需要根据上面提到的屏幕的官方说明文档里的信息来研究

例如官方程序里的初始化函数:

void EPD_2IN9_V2_Init(void)
{
	EPD_2IN9_V2_Reset();
	DEV_Delay_ms(100);

	EPD_2IN9_V2_ReadBusy();
	EPD_2IN9_V2_SendCommand(0x12); // soft reset
	EPD_2IN9_V2_ReadBusy();

	EPD_2IN9_V2_SendCommand(0x01); //Driver output control
	EPD_2IN9_V2_SendData(0x27);
	EPD_2IN9_V2_SendData(0x01);
	EPD_2IN9_V2_SendData(0x00);

	EPD_2IN9_V2_SendCommand(0x11); //data entry mode
	EPD_2IN9_V2_SendData(0x03);

	EPD_2IN9_V2_SetWindows(0, 0, EPD_2IN9_V2_WIDTH-1, EPD_2IN9_V2_HEIGHT-1);

	EPD_2IN9_V2_SendCommand(0x21); //  Display update control
	EPD_2IN9_V2_SendData(0x00);
	EPD_2IN9_V2_SendData(0x80);

	EPD_2IN9_V2_SetCursor(0, 0);
	EPD_2IN9_V2_ReadBusy();

	EPD_2IN9_V2_LUT_by_host(WS_20_30);
}

可以看到虽然里面有一些命令是有一定注释的,但是本身还是比较难懂,而且还好各种没有加注释的数据

花了九牛二虎之力,终于能把屏幕写成全白或者全黑了,然后根据不同 bit 代表不同颜色的编码规则,做了个斑马条纹,效果如下:

显示图片

通过上面的折腾,终于基本熟悉了这个水墨屏驱动的数据格式,然后通过研究示例,找到了把图片展示到屏幕上的方法

其实就是先转换成灰度 bmp 图片,然后转换成驱动需要的按 bit 表示灰度的数据格式,然后发送给屏幕驱动

正常情况下,水墨屏只有黑白两种颜色,也就是 1 bit 。但是这块屏幕恰好支持 2 bit ,也就是四个灰度,可以展示更丰富的色彩(四种颜色丰富在哪?)

一开始,我直接找了一些将颜色映射成灰度四色的方案,但是效果非常差。经过一通调研,发现 色彩抖动 这项技术非常牛逼,能够让只能展示少数颜色的设备通过抖动展示出更多种颜色。从原理上来讲,其实就是人眼会将小范围内多个不同的颜色的像素融合成一个中间过渡颜色的像素,最简单的例子就是屏幕其实是红绿蓝三色,但是离远了看到的就是合成出来的白色,抖动算法要做的就是为了展示白色这个本来不能展示出来的颜色,把红绿蓝三种颜色分布到一个小范围的多个像素中,让人眼认为这是白色

然后在花费了九牛二虎之力后,将抖动出来的图片上传到屏幕驱动上,终于成功将四色灰度图展示到了这块 296x128 的小屏幕上

看出来是谁了吗,没错,就是 路人女主喵都

抖动灰阶图原图,可以看到里面有 白/浅灰/深灰/黑 这四种颜色

真・原图

到这里,算是完成了点亮屏幕的工作。但是这里是显示图像,只需要将像素展示到屏幕上。显示文字还需要一些其他工作

显示文字

其实在展示图像的时候,我已经做了一些布局相关的操作。由于屏幕是 296x128 ,但是图片是 2:1 的比例,所以缩放后应该是 256x128 ,宽度不够,因此需要做一个居中的操作。在这里,我做了一层抽象,实现了一个可绘制的 Canvas 对象,将图片取 offset ,放置到 canvas 中央,然后再上传到屏幕上

对于展示文字,其实也是一个大坑。要显示文字,其实就类似于将多个大小确定的图片展示到屏幕上,但是他们有一些特殊的规则,需要实现特定的接口,才能自动将文字转换成能展示到图中的文字,参考 bdf 项目

这种一眼就是大坑,本来我还打算展示中文,后来研究了一下,投入产出性价比太低,放弃了,直接使用了 embedded-graphics 里内置的英文字体

从 scd41 读取数据

连上传感器,按照一开始提到的 spec 表中的命令和数据格式,向传感器发送命令,然后接收并处理数据,转换成对应温湿度和二氧化碳浓度

或者直接从网上找一些实现,这个传感器应该是比较通用的那种,所以很容易就能找到。抄一下再改成自己想要的就行

组合起来

由于屏幕的大小是固定的,我直接了创建一个 Screen 来表示整个屏幕,这个类中会保存需要展示的字段,在绘制时将对应的要展示的字段格式化并放到固定的位置,只需要对外暴露设置这几个字段的接口,然后调用绘制接口即可展示结果到屏幕上。最后的效果如下

网络功能

上面组合出来的功能其实已经算是可用了,但是为了更加方便查看和收集历史数据,我准备给设备加上网络功能

esp32 本身就是支持 wifi 和蓝牙的。对于蓝牙我其实不太熟悉,所以暂时没有考虑,直接使用了 wifi 功能

又是一通研究下来,发现由于 esp32 作为单片机,是没有操作系统这一说的,也没有线程这一说,所以网上的库中提供的网络功能都是基于 poll 模型的,先将数据塞到缓冲区,然后通过不断调用一个函数来实现收发的逻辑。由于上面从 scd41 读取数据也需要 poll ,所以如果要直接对外暴露 tcp metric 接口,程序里就会有两个需要 poll 的地方,这种场景就需要多线程,或者协程。但是多线程和协程的生态都不太好,所以直接改用 udp 将数据发给一个服务端,然后服务端再暴露 metric 接口给 prometheus 。所以直接(让 AI )用 python 写了个简单的脚本实现了这个功能

总结

经过了一番折腾,一个开源拖拉机级别的二氧化碳浓度监测器就完成了。除了花费的时间太长和没有外壳以外,总体来说还是比较满意的。中间踩了挺多的坑,但是确实也学到了很多有意思的东西

之前没怎么玩过嵌入式相关的东西,这次做完发现嵌入式开发里这些操作都非常底层,发送数据需要手动控制针脚高低电平,而且整个芯片也是低频单核,不使用框架的情况下只能单线程顺序执行,没有并发的能力,导致网络功能都只能做成 poll 模型,没法和其他操作并行,这种情况以前从来没有思考过(没打过这么贫穷的仗)

整个项目到这里差不多就结束了,虽然遗留了一些问题,但是暂时不会投入更多的时间去解决了。该去做其他有趣的事情了

最后,源码在 这里

一些遗留问题

  • 不知为何,在连接上屏幕时,编译程序后, flash 程序到 esp32 的操作会失败,可能是有什么识别设备被占用的机制
  • 不知道是不是因为 scd41 存放太久,测量的温度和小米的温度计的结果始终有差距,尝试了校准功能也没有明显改善
  • 上面提到的中文展示,理论上是可以自己做一套 bdf 字体,然后在屏幕上展示中文的
  • 需要一个外壳。由于没有 3D 打印机啥的,打算看看能不能用纸板手搓一个

相关链接