
1. 从一个“小屏幕”说起为什么要在MicroPython上驱动OLED很多人第一次接触MicroPython都是因为一块几十块钱的开发板。它小巧、便宜、上手快插上电脑就能跑Python代码不用装一堆复杂的编译工具链。但玩了两天之后你会发现一个尴尬的问题板子跑起来了代码也执行了可你根本不知道它到底在干什么。没有屏幕所有的输出都只能靠串口打印一旦脱离电脑设备就成了一个“黑盒”。所以给开发板配一块OLED屏幕几乎是每个MicroPython玩家都会走的一步。它不贵通常十几块钱就能买到一块0.96寸的128×64分辨率小屏I2C接口两根线就能点亮。更重要的是它能让你实时看到传感器数据、程序状态、甚至显示简单的图像。这一步走通了后面想做视频播放、图像处理、甚至做一个自己的小终端才有了基础。这篇文章要聊的就是怎么在MicroPython环境下把一块OLED屏幕驱动起来并且让它显示图像。标题里提到了“看你喜欢看的视频”这其实是一个更大的目标——用MicroPython做视频播放。但视频的本质是什么是一帧一帧的图像快速切换。所以驱动OLED显示图像是通往视频播放的第一步也是最关键的一步。这篇文章是“上篇”我们先把图像显示这件事彻底搞明白。适合谁看如果你手里有一块支持MicroPython的开发板比如ESP32、ESP8266、RP2040之类的并且已经能跑基本的Python代码那这篇文章就是为你准备的。如果你还没入门也没关系我会把每一步都拆开讲尽量让第一次接触的人也能跟着做下来。2. 硬件选型与接线别小看这两根线2.1 OLED屏幕怎么选I2C还是SPI市面上常见的0.96寸OLED模块主要有两种接口I2C和SPI。I2C只需要两根信号线SDA、SCL接线简单速度够用适合大多数场景。SPI需要四根线以上速度快但占用的引脚多。对于显示静态图像或者低帧率视频来说I2C完全够用。我实测过128×64分辨率下I2C在400kHz速率下刷新一帧全屏图像大概需要几十毫秒对于入门来说完全可以接受。选屏的时候注意几个参数分辨率128×64是主流驱动芯片通常是SSD1306或SH1106。这两个芯片的初始化命令略有不同SSD1306更常见SH1106在某些廉价模块上会出现。如果你买到的屏幕显示有偏移或者花屏很可能就是驱动芯片不匹配。另外屏幕颜色有白色、蓝色、黄蓝双色几种双色屏通常是上半部分黄色、下半部分蓝色价格差不多看个人喜好。注意有些模块标称I2C地址是0x3C有些是0x3D。如果扫描不到设备先检查地址。用I2C扫描工具跑一下确认设备在线。2.2 接线两根线背后的门道I2C接线很简单VCC接3.3VGND接GNDSDA接开发板的I2C数据引脚SCL接时钟引脚。但这里有几个坑要注意。第一电压问题。大多数OLED模块支持3.3V和5V但有些模块只支持3.3V。如果你用5V供电可能会烧坏屏幕。保险起见先查一下模块的规格书或者直接用3.3V。第二上拉电阻。I2C总线需要上拉电阻通常模块上已经自带了4.7kΩ或10kΩ的上拉电阻。如果开发板的I2C引脚内部上拉不够强或者你接了很多设备可能需要外接上拉电阻。我遇到过一种情况单独接屏幕没问题但同时接了两个I2C设备后屏幕就不亮了。后来在SDA和SCL上各加了一个4.7kΩ电阻到3.3V问题解决。第三引脚选择。不同开发板的I2C引脚不一样。ESP32默认的I2C引脚是GPIO21SDA和GPIO22SCL但你可以通过代码重新映射到任意GPIO。RP2040的I2C引脚更多灵活性更高。接线之前先确认你的开发板手册别接错了。2.3 开发板选择够用就好理论上任何支持MicroPython并且有I2C接口的开发板都可以。ESP32是最常见的选择因为它便宜、性能强、社区资源多。ESP8266也可以但内存小一些跑图像显示可能会有点吃力。RP2040比如树莓派Pico性能很好双核RAM也够但MicroPython固件需要自己刷。如果你还没买板子我的建议是入门选ESP32性价比最高。如果已经有板子了那就直接用没必要为了这个项目专门换。3. 软件环境搭建从零到点亮屏幕3.1 刷写MicroPython固件拿到开发板的第一件事是刷MicroPython固件。不同板子的刷写方法不一样但基本流程是下载固件文件用烧录工具写入开发板。ESP32和ESP8266可以用esptoolRP2040可以直接拖拽uf2文件。刷完之后用串口工具连接开发板应该能看到MicroPython的REPL提示符。这时候你可以输入简单的Python代码比如print(hello)确认环境正常。提示刷固件之前先备份开发板上的原有数据。刷写会清空所有内容。3.2 安装驱动库ssd1306.pyMicroPython官方并没有内置OLED驱动库但社区里有一个非常成熟的ssd1306.py几乎成了标准。这个库很小只有几百行代码支持SSD1306和SH1106两种芯片。获取方式有两种一是从GitHub上下载二是直接通过MicroPython的包管理工具安装。如果你用的是Thonny或者uPyCraft这类IDE可以直接把文件上传到开发板。我习惯用mpremote工具命令行操作更方便。mpremote connect /dev/ttyUSB0 fs cp ssd1306.py :ssd1306.py上传之后在REPL里import ssd1306如果没有报错说明库已经就位。3.3 I2C扫描确认屏幕在线在写显示代码之前先确认屏幕能被扫描到。MicroPython的machine模块提供了I2C类可以扫描总线上的设备。from machine import I2C, Pin i2c I2C(0, sclPin(22), sdaPin(21), freq400000) devices i2c.scan() print(devices)如果输出类似[60]说明设备地址是0x3C60的十六进制。如果输出空列表说明接线有问题或者屏幕没供电。这时候检查VCC和GND确认SDA和SCL没有接反。注意有些开发板的I2C编号不是0可能是1或者其他。如果I2C(0)报错试试I2C(1)。4. 驱动OLED显示图像从像素到画面4.1 理解OLED的显存结构SSD1306的显存是一块128×64位的区域总共1024字节。它按页page组织每页8行共8页。每个字节代表一列中的8个像素低位在上高位在下。这种结构决定了你不能直接往屏幕上写“像素”而是要先在内存里构建一个缓冲区framebuffer然后一次性发送到屏幕。MicroPython的ssd1306库提供了framebuf模块它封装了基本的绘图操作比如画点、画线、画矩形、显示文字。但framebuf本身不支持直接显示图像文件你需要把图像转换成字节数组然后写入缓冲区。4.2 图像格式转换从PNG到字节数组这是整个流程中最关键的一步。你手头有一张图片可能是PNG、JPG或者BMP但OLED只认字节数组。怎么转换常用的工具是PC端的Python脚本配合PILPillow库。基本思路是打开图片缩放到128×64转换成黑白二值图像然后按SSD1306的显存格式打包成字节数组。from PIL import Image img Image.open(test.png).convert(1) # 转黑白 img img.resize((128, 64)) pixels img.load() buf bytearray(1024) for y in range(64): for x in range(128): if pixels[x, y]: buf[(y // 8) * 128 x] | 1 (y % 8) with open(test.bin, wb) as f: f.write(buf)这段代码的逻辑是遍历每个像素如果像素是亮的就把对应字节的对应位设为1。注意SSD1306的位顺序是低位在上所以1 (y % 8)。生成的test.bin文件只有1024字节非常小。你可以把它上传到开发板然后用framebuf的blit方法显示。4.3 在MicroPython中显示图像上传test.bin到开发板后用以下代码显示import ssd1306 from machine import I2C, Pin i2c I2C(0, sclPin(22), sdaPin(21)) oled ssd1306.SSD1306_I2C(128, 64, i2c) with open(test.bin, rb) as f: data f.read() oled.fill(0) oled.blit(framebuf.FrameBuffer(bytearray(data), 128, 64, framebuf.MONO_HLSB), 0, 0) oled.show()这里用到了framebuf.FrameBuffer它把字节数组包装成一个可绘制的对象然后blit到OLED的缓冲区。MONO_HLSB表示单色、水平字节、低位在前正好匹配SSD1306的格式。提示如果显示出来的图像是反的或者上下颠倒可能是位顺序或者扫描方向的问题。调整MONO_HLSB为MONO_HMSB试试或者在转换脚本里翻转Y轴。4.4 动态图像从静态到动画静态图像显示只是第一步。如果你想做动画比如播放视频就需要连续显示多帧图像。这时候有几个优化点第一减少I2C传输次数。每次oled.show()都会把整个缓冲区发送到屏幕耗时较长。如果帧率要求不高可以接受。但如果想提高帧率可以考虑只更新变化的部分或者降低分辨率。第二使用双缓冲。在内存里维护两个缓冲区一个用于绘制一个用于显示交替使用。这样可以避免绘制过程中的闪烁。第三压缩图像数据。1024字节一帧如果存100帧就是100KB对于ESP32来说不算大但对于ESP8266就有点紧张了。可以考虑用RLE游程编码压缩或者只存储关键帧。我实测过在ESP32上I2C速率400kHz全屏刷新一帧大约需要30-40毫秒也就是25-30帧每秒。这个帧率对于简单的动画来说已经够用了。但如果想播放真正的视频还需要进一步优化比如降低颜色深度、使用SPI接口、或者用DMA传输。5. 常见问题与排查技巧实录5.1 屏幕不亮从供电到地址的排查清单屏幕不亮是最常见的问题。排查顺序如下现象可能原因解决方法完全黑屏供电不足检查VCC是否接3.3V用万用表量一下完全黑屏接线错误确认SDA、SCL没有接反完全黑屏I2C地址不对运行扫描代码确认设备地址花屏驱动芯片不匹配尝试SH1106驱动显示偏移初始化参数不对调整列偏移量我遇到过一种情况屏幕在ESP32上不亮但在另一块板子上正常。后来发现是ESP32的I2C引脚默认上拉不够外接了两个4.7kΩ电阻就好了。5.2 图像显示异常位顺序与字节对齐图像显示异常通常表现为图像上下颠倒、左右镜像、或者出现条纹。这些问题大多和位顺序有关。SSD1306的显存是按页组织的每页8行。如果你的图像转换脚本没有正确处理页边界就会出现条纹。比如图像高度不是8的倍数时最后一页会多出一些空行。解决方法是在转换时补齐到8的倍数。另外framebuf的MONO_HLSB和MONO_HMSB分别对应低位在前和高位在前。SSD1306通常是低位在前但有些模块可能相反。如果图像看起来像“负片”可能是位顺序反了。5.3 内存不足ESP8266的优化策略ESP8266的RAM只有几十KB跑图像显示很容易内存不足。优化策略包括降低分辨率比如用64×32减少缓冲区数量只用一个使用gc.collect()手动回收内存把图像数据存在Flash里而不是RAM里我试过在ESP8266上显示128×64的全屏图像勉强能跑但帧率很低。如果要做动画建议换ESP32或者RP2040。5.4 刷新速度慢I2C速率与优化技巧I2C速率默认是100kHz可以提高到400kHz甚至1MHz。在MicroPython里初始化I2C时指定freq400000即可。但要注意不是所有模块都支持1MHz400kHz是比较稳妥的选择。另外oled.show()是阻塞操作会占用CPU时间。如果需要在显示的同时处理其他任务可以考虑用定时器或者异步任务。MicroPython支持uasyncio可以把显示操作放在协程里避免阻塞主循环。注意提高I2C速率可能会导致通信不稳定特别是接线较长的时候。如果出现随机花屏降低速率试试。6. 从图像到视频下一步的思路图像显示跑通之后下一步就是让它动起来。视频的本质是连续帧所以你需要解决三个问题帧数据的存储、帧率的控制、以及音频的同步如果需要声音。帧数据存储方面可以把多帧图像打包成一个文件每帧1024字节按顺序读取。如果帧数很多可以考虑压缩。简单的RLE压缩对于黑白图像效果不错特别是大面积纯色的帧。帧率控制方面MicroPython的time.ticks_ms()可以获取毫秒级时间戳用来计算帧间隔。如果一帧显示耗时40毫秒那理论帧率就是25fps。你可以通过调整I2C速率、降低分辨率、或者只更新变化区域来提高帧率。音频同步是更复杂的问题MicroPython对音频的支持有限通常需要外接DAC或者PWM输出。如果只是做无声动画那就不需要考虑音频。我在实际项目中的体会是MicroPython做视频播放瓶颈主要在I2C带宽和CPU处理能力。如果你真的想流畅播放视频SPI接口的屏幕会更好或者直接用硬件解码的方案。但作为学习和实验I2C OLED已经足够让你理解视频显示的基本原理了。最后分享一个小技巧如果你想让图像显示更流畅可以先把所有帧数据加载到内存里然后用一个循环快速切换。这样避免了频繁的文件读取操作帧率会明显提升。当然前提是你的开发板内存够大。