用 ESP32-S3 驱动 SSD1306:把「月薪猫」塞进 0.96 寸屏
用 ESP32-S3 驱动 SSD1306:把「月薪猫」塞进 0.96 寸屏
CoolJuvenlie手上一直有一块 0.96 寸的 SSD1306,买的时候想的是「随手点个灯就够了」,结果一直躺在元件盒里。最近把手上的 ESP32-S3 重新翻出来,索性给它安排点正事:让屏幕上的小猫动起来。
先算清楚这块屏能干什么
SSD1306 是单色屏,128×64 个像素,一个像素一位,整块缓冲区就是 128 × 64 / 8 = 1024 字节。也就是说,每刷新一帧,至少要往屏幕里写 1KB 的数据。这 1KB 就是后面所有性能问题的起点。
单色屏没什么灰度可言,所以做动画的思路很简单:把每一帧都压成 1bit 位图,然后一帧一帧地推过去。难点不在「能不能动」,而在「动得够不够顺」。
接线:ESP32-S3 的 I2C 可以随便挑引脚
ESP32-S3 有 GPIO 矩阵,I2C 的外设信号可以映射到大部分 GPIO 上,所以不必死守默认引脚。我用的是下面这组,接线短、走线也顺:
| 屏幕引脚 | ESP32-S3 | 说明 |
|---|---|---|
| VCC | 3V3 | 不要接 5V,模块虽然有稳压,但没必要冒险 |
| GND | GND | 共地 |
| SCL | GPIO9 | 时钟线 |
| SDA | GPIO8 | 数据线 |
地址一般是 0x3C,少数模块是 0x3D。上电先跑一个 I2C 扫描,确认地址再往下写,能省掉一半的玄学问题。
把 GIF 拆成 C 数组
素材是一段十几帧的小动画。用 Pillow 拆帧、缩放、二值化,再输出成 C 数组,脚本不到 40 行:
1 | from PIL import Image, ImageSequence |
注意二值化的阈值。阈值太高,线条会断;太低,整片糊成一团。单色屏没有中间调,这一步基本决定了成品好不好看,值得多试几组。
Arduino 端的播放逻辑
用 U8g2 的全缓冲模式,每帧把整个 1024 字节写进缓冲区再一次性 sendBuffer:
1 | #include <U8g2lib.h> |
U8g2 的 drawXBMP 会自动处理 PROGMEM 读取,所以数组放在 flash 里也不会读错。如果换成 Adafruit_SSD1306,就要自己用 pgm_read_byte,这点后面踩坑会讲到。
帧率到底能跑多快
以 400kHz 的 I2C 来算,一帧 1024 字节,加上地址字节和分页控制开销,实际传输大概在 21~25ms。理论上限差不多 30 帧左右,但那是「什么都不干只管发数据」的理想值。
加上清缓冲、绘制、解析的开销,实测稳定在 12~15 帧。想再快,只有两条路:
- 把 I2C 拉到 1MHz(部分 SSD1306 模块能撑住,但线长了容易出错);
- 只刷变化区域,把整屏刷新拆成局部刷新。
对一段十几帧的循环动画来说,12 帧已经完全够用了,所以我没有继续优化——够用就停,是我这几年学到的最省钱的一条经验。
踩过的坑
1. 屏幕其实不是 SSD1306
有一批 0.96 寸模块用的是 SH1106 驱动,它的显存是 132 列,比 SSD1306 多 4 列。用 SSD1306 的驱动去推,画面会整体偏移,右边还会缺一条。现象是「文字左边被切、右边有竖线」,遇到这个先怀疑驱动型号,别急着改代码。
2. PROGMEM 忘了 pgm_read_byte
Adafruit_SSD1306 的 drawBitmap 默认按 RAM 读取,把数组声明成 PROGMEM 之后必须自己解包,否则屏幕上会随机花屏——因为读到的是 flash 的地址空间,不是数据。这类问题不会报错,只会让你怀疑人生。
3. ESP32-S3 的默认引脚不能想当然
网上很多教程是 ESP32 的默认 SDA=21/SCL=22,S3 上这两个编号并不通用。老老实实 Wire.begin(8, 9) 显式指定,别抄。
小结
一块 3 块钱的屏幕 + 一块 ESP32-S3,就能做出一个放桌上的小动画。真正花时间的不是驱动,而是素材处理和那些「看起来像硬件坏了、其实是软件细节」的坑。
下一步想给它加个按键切换动画,再把桌面时钟接上去。等做完了再写一篇。
