☰
手把手用MicroPython驱动SSD1306 OLED:接线、中文渲染与滚动特效
2026/9/29 11:49:18 网站建设 项目流程

先交代个背景。之前有读者问我,说用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或SSD1315,I2C接口优先
  • 杜邦线或者排针排线若干
  • 电脑一台,装有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, scl=Pin(5), sda=Pin(4), freq=400000) 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, scl=Pin(5), sda=Pin(4), freq=400000) oled = ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C) 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, scl=Pin(5), sda=Pin(4), freq=400000) oled = ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C) 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): # 区域限定在y=24到y=40之间,模拟一条字幕带 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()

循环里每帧更新offset:

text = "你好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内部接了LED,I2C时钟信号会受干扰,表现为偶发花屏。解决方法是换一组引脚,尽量选空闲的GPIO。

再有一个是MicroPython固件版本差异。不同版本对SSD1306_I2C的初始化参数支持略有不同,老版本固件可能不支持freq=400000参数。遇到异常时先升级到最新稳定版固件,很多莫名其妙的问题就消失了。

Proteus仿真的问题也有不少人问。如果没有硬件,在Proteus里放一个SSD1306模型,I2C地址同样要配置成0x3C或0x3D,否则仿真里也白屏。不过仿真屏不支持硬件滚动命令,软件滚动方式可以正常仿真。我一般建议先仿真把逻辑跑通,再用真机调颜色和刷新率方面的细节。

在实际编写过程中,Thonny的“运行当前脚本”和“上传到开发板”要区分清楚。如果只是把代码粘贴到Shell临时运行,断电后代码就没了;要开机自启或者长期使用,需要把主程序保存为main.py上传到开发板Flash里,并确保ssd1306.py和字库文件也在板子上。这个细节很多人忽略,等断电重启才发现屏幕不亮。

写在最后的一点经验

这套项目我从HAL库迁移到MicroPython,前后折腾了大概一周。最大的体会是:显示驱动的本质都是“往显存里填数据”,任何花哨的效果最后都归结为对偏移量、坐标、刷新时机这三个变量的控制。把官方库跑通之后,不建议急着去搜各种大而全的库,先手动写几个小函数,把中文、滚动、图片显示都过一遍,原理搞懂了,后面用什么库都顺手。

如果后续想继续扩展,可以考虑把字库切到文件系统里按需加载,或者用blit_buffer做多帧动画,再配合按键做交互菜单。屏幕虽然小,能做的东西一点都不少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询