ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手把手用MicroPython驱动SSD1306 OLED:接线、中文渲染与滚动特效

手把手用MicroPython驱动SSD1306 OLED:接线、中文渲染与滚动特效 先交代个背景。之前有读者问我说用STM32 HAL库调OLED光初始化那几十个寄存器就够背一阵子后来换了MicroPython三行代码就能点亮一块0.96寸的SSD1306屏整个人都清爽了。这篇就讲一个非常实际的玩法用MicroPython驱动SSD1306/SSD1315覆盖接线、基础显示、中文渲染和滚动特效。标题虽然叫“手把手”其实就是一篇可以照着敲的实操记录适合刚入门MicroPython、或者想快速给板子加一块显示屏的朋友从零开始也能走通。这里选SSD1306/SSD1315而不是其他屏主要原因是这两个驱动芯片太常见了0.96寸128x64的OLED模块十块里有八块是SSD1306剩下还有一部分是SSD1315指令集基本兼容MicroPython的驱动可以直接通用。所以学一次能覆盖市面上大多数小屏模块性价比很高。1. 为什么选MicroPython而不是HAL库方案对比与整体思路我最早玩OLED是拿STM32标准库写的后来用HAL库说实话功能上没有任何区别但开发节奏完全不一样。HAL库点亮一块屏要先配I2C外设再写初始化序列然后把显存数组填好、调地址窗口、写入Page地址最后才看到画面。整个过程最少也要几十行初始化代码还不算字模数据和取模工具那一套。如果用DMA、中断去刷新代码量还会继续膨胀。MicroPython则是另一套思路。它已经把I2C、SPI这些底层协议封装成模块驱动库也是现成的开发者只需要关心业务逻辑画点什么、显示什么、什么时候刷新。对原型验证、课设演示、个人小工具来说省下的时间非常可观。我在项目中实际使用下来MicroPython方案有几个明显优势一是开发效率高。修改字模、调位置、改刷新方式直接改Python代码上传即生效不用编译、烧录、等下载器循环迭代速度很快。二是可读性好。代码量少逻辑清晰哪怕几个月后再翻出来看也能一眼看懂哪个部分是画图、哪个部分是滚动。三是生态成熟。官方有ssd1306.py驱动文件支持I2C和SPI两种接口拿到就能用。社区里的中文显示方案、动画方案也很多踩坑成本低。当然MicroPython不是万能的。如果你对刷新帧率有非常高的要求比如要做高速动态界面MicroPython逐像素打点的方式会明显拖慢速度。这时候要么用汇编级优化要么回归C语言。但做普通的状态显示、菜单界面、滚动字幕MicroPython完全够用而且开发体感好得多。整体思路是先用官方库点亮屏幕把自己变成“能用的人”再把中文显示和滚动特效加进去完成一个实用性强、可展示的小项目。后面所有代码都是在MicroPython固件下跑的开发板以ESP32为例其他板子只要改一下I2C引脚号即可。2. 硬件准备与最小系统接线硬件清单并不复杂属于那种“手头有就能搞”的级别。开发板一块我这边用的是ESP32 DevKitC你也可以用ESP8266、RP2040、STM32F407带MicroPython固件的板子0.96寸OLED模块一块驱动芯片是SSD1306或SSD1315I2C接口优先杜邦线或者排针排线若干电脑一台装有Thonny或uPyCraft用来上传代码接线是第一步也是最容易出错的地方。I2C接口的OLED一般有四个引脚VCC、GND、SCL、SDA。我常用的接线方式是OLED模块ESP32说明VCC3V3供电部分模块支持5V但3.3V更稳妥GNDGND共地SCLGPIO5I2C时钟线SDAGPIO4I2C数据线这里SPI接口的OLED不展开讲我们的代码全部走I2C。注意一个细节不同OLED模块丝印上SCL和SDA的标记可能不一致有的标成SCK/SDA有的标成SCL/SDI本质上都是I2C那两根线。还有个别模块会在VCC和GND之间接反保护二极管接反不会立刻烧但长期供电会导致异常。我的习惯是每次接线前都用万用表确认一下VCC和GND不要迷信丝印。关于I2C上拉电阻大多数开发板的I2C引脚内部已经有上拉但OLED模块和开发板之间如果用了很长的杜邦线信号质量会下降。实际测试中飞线长度超过20厘米后偶尔会出现花屏或第一帧显示不完整这时候可以在SDA和SCL上分别外接一个4.7kΩ上拉电阻到VCC问题基本能解决。硬件确认完毕下一步就是把MicroPython环境准备好。开发板烧录MicroPython固件这一步骤不同板子稍有差异这里不展开。假设你已经能在Thonny里看到MicroPython的REPL提示符我们直接进入代码部分。3. 点亮屏幕基础驱动代码与显示原理3.1 扫描I2C设备地址新手最容易卡的一步点亮屏幕之前强烈建议先扫描一下I2C总线确认屏幕地址。很多模块默认地址是0x3C但也有部分是0x3D这个地址不确认后面所有代码都会失败。在Thonny的Shell窗口输入下面代码运行后会输出总线上的所有设备地址from machine import Pin, I2C i2c I2C(0, sclPin(5), sdaPin(4), freq400000) print(i2c.scan())如果模块接线正常、供电正常会输出类似[60]的结果60是十进制表示换算成十六进制就是0x3C。如果输出[61]那你的模块地址是0x3D。如果输出空列表[]说明设备没有出现在总线上优先检查供电和接线再考虑换一根杜邦线试试。这个扫描代码还能用来确认I2C外设编号。ESP32上通常有多个I2C外设有些引脚组合属于I2C0有些属于I2C1不同板子差异较大。用I2C(0, ...)不行就换I2C(1, ...)这是排查“I2C设备找不到”最朴素也最有效的方法。3.2 初始化与基础绘图接口扫描到地址后把官方驱动文件ssd1306.py保存到开发板根目录或者通过Thonny的包管理器安装micropython-ssd1306库。这个驱动文件的源码在MicroPython官方仓库的drivers/display目录下是一个很精简的类封装基于MicroPython的frame buffer实现。然后写基础初始化代码from machine import Pin, I2C import ssd1306 import time i2c I2C(0, sclPin(5), sdaPin(4), freq400000) oled ssd1306.SSD1306_I2C(128, 64, i2c, addr0x3C) oled.fill(0) oled.text(Hello OLED, 20, 28, 1) oled.show()这段代码里的逻辑很直接先填充全屏为黑色然后在坐标(20, 28)处写一行ASCII文本最后调用show()把帧缓冲推送到屏幕。如果不调用show()屏幕上永远不会有变化——这是新手最容易忘记的一步因为fill()、text()都只是修改内存里的帧缓冲不会直接驱动屏幕刷新。SSD1306的128x64分辨率对应1024字节显存每8个像素点占1字节。OLED驱动内部用framebuf把这个区域抽象成一块画布pixel()、line()、rect()、text()这些方法都是在画布上操作show()时才打包整块内容通过I2C发给屏幕。这里有个性能特征值得了解show()每次传输1024字节数据在400kHz I2C下大约需要2到3毫秒在100kHz下大约需要10毫秒。所以画面更新频率有一个天花板不要和LCD的实时刷新率对比。做动画时如果发现帧率上不去优先怀疑I2C时钟频率再检查是否需要压缩刷新区域。简单的图形绘制可以这样做方便测试屏幕是否正常显示oled.fill(0) oled.rect(0, 0, 127, 63, 1) oled.line(0, 0, 127, 63, 1) oled.ellipse(64, 32, 20, 15, 1) oled.show()我用这种方式快速测试过很多模块只要能看到方框、直线、椭圆正常显示说明屏幕基本没问题后面就可以放心做中文字库和滚动特效。4. 中文显示从字模取模到逐字绘制4.1 为什么OLED显示不了中文MicroPython的oled.text()只支持ASCII字符因为官方的驱动里内置的字体是8x8的英文字母和数字字模不包含汉字。中文最少需要12x12或16x16的尺寸才能辨认清楚我一般用16x16也就是一个汉字占2个字节宽、2个字节高视觉上比较协调。所以要在OLED上显示中文本质上是自己准备“汉字字模”然后把每个汉字的像素数据按位置刷到帧缓冲里。绕开官方驱动里只支持8x8字体的局限。4.2 字模数据怎么来字模的获取方式有两种主流方法一种是自己画。用取模软件输入汉字生成对应的字节数组比如PCtoLCD2002、Image2Lcd这两个工具比较经典。操作步骤一般是新建一个16x16的点阵画布输入汉字调整字体大小然后选择“逐行式”、“高位在前”的取模方式导出成C语言或Python格式的字节数组。这里关键是要统一“逐行式、高位在前”的格式因为后面绘制的代码要按这个约定解析字节。另一种是直接用现成的字库文件。GitHub上有不少MicroPython中文字库项目比如某种GB2312的全角字库Python文件里面把几千个常用汉字按GB2312编码顺序存成数组。用的时候通过汉字的GB2312编码查表定位数据。这种方案的优点是省去取模过程缺点是文件体积大一个16x16全字库可能十几KB到几十KB不等对Flash小的板子有点压力还要注意版权和来源。我实际项目里用得比较多的是“有限字库”方案先把会出现的文字整理成列表比如“温度”“湿度”“时间”“状态”这些用取模工具一次性生成这些字的字模数据存到一个字典里。这样内存占用很小显示速度也更快适合做固定内容的状态屏。如果只是学习可以先用一个例程字库比如下面这样只放几个字的简易版本看懂了再扩展# 简易16x16中文点阵逐行式每行2字节高位在前 FONT { 你: [ 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x7F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x04, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x04, 0x04, 0x00, 0x00 ], 好: [ 0x00, 0x00, 0x7F, 0x08, 0x08, 0x08, 0x08, 0x3F, 0x08, 0x08, 0x08, 0x08, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x10, 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F, 0x10, 0x20, 0x00, 0x00, 0x00 ] }注意这些数据不一定来自真实的取模程序只是展示一种格式约定每个字有一行一行的像素数据每一行16个bit分成两个字节高位在前。4.3 绘制函数与完整示例拿到字模数据后绘制函数就很简单了。按行遍历检查对应位的值如果是1就在OLED上打一个点def draw_char(oled, char, x, y, font_data): for row in range(16): byte_high font_data[row * 2] byte_low font_data[row * 2 1] for col in range(8): if byte_high (0x80 col): oled.pixel(x col, y row, 1) for col in range(8): if byte_low (0x80 col): oled.pixel(x 8 col, y row, 1)调用时比如想显示“你好”先取到两个字的数据再绘制from machine import Pin, I2C import ssd1306 i2c I2C(0, sclPin(5), sdaPin(4), freq400000) oled ssd1306.SSD1306_I2C(128, 64, i2c, addr0x3C) oled.fill(0) draw_char(oled, 你, 0, 0, FONT[你]) draw_char(oled, 好, 16, 0, FONT[好]) oled.show()pixel()逐点打点的方法虽然直观但每画一个字要遍历256个点对于显示几个中文字的场景完全够用。如果显示大量中文且追求速度可以把逐点计算优化成按字节填充直接操作frame buffer内部的bytearray性能能提升不少但代码理解成本也高一些。我建议新手先用pixel()版本跑通以后再去优化。这里还有一个编码坑需要提一下。取模工具导出的字模数组和MicroPython文件里的字符串编码必须保持一致。MicroPython源码文件通常用UTF-8保存取模软件生成的汉字顺序默认是GB2312或GBK两者对同一个汉字的编码不同。如果直接在FONT字典里用中文做键文件保存时必须是UTF-8否则运行时会报KeyError。而取模工具导出的数组顺序如果写死在代码里就必须确保数组中第一个字对应字典里的第一个键一一对应。推荐的做法是给字典的键用中文数组数据来自取模软件时直接在代码里用中文键访问这样代码可读性最好。只要在文件第一行使用# -*- coding: utf-8 -*-并且Thonny保存文件时选择UTF-8编码就不会出现编码错乱。5. 滚动特效轮播、字幕与动画5.1 滚动实现原理屏幕上通常只能显示128x64的内容想展示一长串文字或者循环播放多页信息最简单的方法是做滚动效果。OLED的滚动主要有两种实现路径。一种是硬件滚动。SSD1306/SSD1315内部有硬件滚动命令芯片自己会周期性地把显存内容向左、向右或对角方向移动不需要主控反复刷新。这种方式的优点是省CPU、滚动平滑但有几个限制只能整屏滚动不能滚动局部区域速度档位固定而且滚动过程中不能正常写入其他内容一旦要更新显示内容必须先停止滚动。另一种是软件滚动也就是主控自己控制每一帧的偏移量把内容画到不同位置然后调用show()。优点是灵活可以控制滚动区域、速度、方向可以局部更新还能配合其他动画效果一起做。缺点是每帧都要刷新全屏对I2C占用多一些。我在实际展示项目中除了一整屏信息需要平滑移动时会考虑硬件滚动其他场景基本都是软件滚动。特别是做菜单、数字变化、字幕效果时软件滚动提供了最大可控性。5.2 循环滚动字幕实战循环滚动字幕是很多项目里都会用到的效果一段文字从屏幕右侧进入向左移动然后从左侧出去周而复始。实现思路是维护一个水平偏移量offset每帧把文字整体左移一个像素用oled.scroll(-1, 0)移动显存内容然后在新出现的最右侧一列位置绘制接下来的字模。简化版实现如下def draw_text_scrolling(oled, text, offset): # 区域限定在y24到y40之间模拟一条字幕带 oled.fill(0) total_width len(text) * 16 for i, ch in enumerate(text): x i * 16 - offset if -16 x 128: draw_char(oled, ch, x, 24, FONT[ch]) oled.show()循环里每帧更新offsettext 你好MicroPython欢迎使用OLED滚动字幕 offset 0 while True: draw_text_scrolling(oled, text, offset) offset 1 if offset len(text) * 16: offset 0 time.sleep_ms(10)这段代码的逻辑是把整个字符串按16像素一字排开屏幕相当于一个128像素宽的窗口窗口中心随offset移动只把落在窗口内的字模画出来。当offset超过整条文字的宽度就重新从0开始形成无缝循环。实际调试时如果滚动太快看不清把sleep_ms调到30或50。如果出现拖影或残影说明fill(0)没有覆盖整屏或者绘制坐标越界检查一下绘制坐标是否超过128宽度。5.3 其他滚动效果字幕滚动以外还有几种常见效果值得尝试。一种是垂直翻页效果。把多页内容分别画到帧缓冲的不同区域通过改变纵向偏移量实现上下滑动。用在环境监测站里很常见第一页显示温度第二页显示湿度第三页显示时间每2秒向上滚动一页看起来像翻页动画。另一种是单行逐字滚动常见于跑马灯。屏幕固定显示一个窗口文字在窗口内逐字移动比如只显示8个字的宽度超过部分滚动出来。实现方式和上面字幕类似不过要把窗口范围从整屏改成局部矩形可以用oled.fill_rect()先清理局部区域再绘制文字避免影响屏幕其他区域的内容。再一种是数字翻页效果比如时钟的分钟变化时的滚动切换动画。这个比较适合做小效果游戏面试或展示时也比较抓眼球。核心是先用fill_rect()清掉旧数字再把新数字从下方往上移动几个像素产生“滚入”的错觉。这些效果的代码都可以在字幕滚动的基础上改出来关键是把“偏移量”这个变量用好。理解了滚动原理自己就能组合出更多花活。6. 显示图片与扩展玩法OLED是1位色深屏幕每个像素只有亮和灭两种状态所以显示图片前必须先把彩色图片转换成128x64的1位BMP位图。转换工具可以用PCtoLCD2002也可以直接用Python的PIL库做二值化处理。我的常用做法是用PIL读取图片、缩放到128x64、转成灰度再按阈值二值化最后导出一个字节数组。MicroPython的frame buffer支持直接操作原始字节数据所以拿到字节数组后可以一次性写入显存比逐像素绘制快很多oled.blit_buffer(image_buffer, 0, 0, 128, 64) oled.show()blit_buffer()一次调用就能把整张图片推入帧缓冲性能远优于循环打点。适合做开机logo、状态图标或者把动态画面预先拆成几帧图片按顺序播放形成简单动画。扩展玩法上我试过把DHT11温湿度的读数显示在OLED上再把历史温度趋势通过滚动曲线展示也做过一个带菜单选择的小工具按按键切换不同页面菜单项高亮使用反色先画一个填充矩形再把白色文字绘制上去显示效果很清晰。OLED的局限性是屏幕尺寸小不适合展示复杂图表但用来做交互界面、状态提示、数据可视化小面板非常合适。7. 常见问题与排查实录这块是我实操里踩坑最多的地方写出来给大家参考。现象可能原因解决方法白屏完全没有反应供电没接好测VCC和GND电压确认是3.3V白屏但模块有点热VCC和GND接反立刻断电检查接线重新接扫描不到I2C设备接线错误、SDA/SCL接反交换SDA和SCL测试扫描不到I2C设备线太长上拉不足外接4.7k上拉电阻扫描地址不是0x3C模块默认地址就是0x3D代码里修改addr参数画面有残影或花屏I2C频率太高、干扰大降到100kHz缩短杜邦线第一帧显示正常第二帧白屏显存越界写入检查绘制坐标是否超过128x64中文变成乱码文件编码和字库编码不一致统一使用UTF-8编码中文显示重复第一个字字典键名没有匹配字模数据检查FONT里每个汉字的索引除了表格里的问题还有几个很隐蔽的坑。一个是我遇到过OLED模块标称支持I2C地址切换但实际模块上的地址选择电阻虚焊导致无论怎么改代码都识别不了。这种情况下用烙铁重焊一下地址选择焊盘或者干脆换一个模块测试。另一个是有的ESP32开发板I2C引脚默认被其他功能占用。比如某些板子GPIO4内部接了LEDI2C时钟信号会受干扰表现为偶发花屏。解决方法是换一组引脚尽量选空闲的GPIO。再有一个是MicroPython固件版本差异。不同版本对SSD1306_I2C的初始化参数支持略有不同老版本固件可能不支持freq400000参数。遇到异常时先升级到最新稳定版固件很多莫名其妙的问题就消失了。Proteus仿真的问题也有不少人问。如果没有硬件在Proteus里放一个SSD1306模型I2C地址同样要配置成0x3C或0x3D否则仿真里也白屏。不过仿真屏不支持硬件滚动命令软件滚动方式可以正常仿真。我一般建议先仿真把逻辑跑通再用真机调颜色和刷新率方面的细节。在实际编写过程中Thonny的“运行当前脚本”和“上传到开发板”要区分清楚。如果只是把代码粘贴到Shell临时运行断电后代码就没了要开机自启或者长期使用需要把主程序保存为main.py上传到开发板Flash里并确保ssd1306.py和字库文件也在板子上。这个细节很多人忽略等断电重启才发现屏幕不亮。写在最后的一点经验这套项目我从HAL库迁移到MicroPython前后折腾了大概一周。最大的体会是显示驱动的本质都是“往显存里填数据”任何花哨的效果最后都归结为对偏移量、坐标、刷新时机这三个变量的控制。把官方库跑通之后不建议急着去搜各种大而全的库先手动写几个小函数把中文、滚动、图片显示都过一遍原理搞懂了后面用什么库都顺手。如果后续想继续扩展可以考虑把字库切到文件系统里按需加载或者用blit_buffer做多帧动画再配合按键做交互菜单。屏幕虽然小能做的东西一点都不少。
返回列表