1. 给开发板加一块OLED屏,到底图什么
手里有块开发板,跑通了点灯、串口打印、蓝牙广播,然后呢?很多人卡在这个阶段——板子明明还能干更多事,但每次调试都得开着电脑看串口助手,设备一挪位置线就不够长,想做个脱机运行的小装置又不知道当前状态。给开发板挂一块OLED小屏,本质上解决的就是“让设备自己会说话”这件事。
我最早动这个念头,是做一批环境数据采集节点。设备部署在机柜里,每次读数据都要连电脑,烦得不行。后来给每块板子配了块0.96寸的OLED,上电就能看到温度、湿度、运行时长、网络状态,调试效率直接翻倍。再后来做低功耗设备,屏幕还能当“状态指示灯”用,比闪LED直观太多。
这篇内容围绕给NU40 DK这类开发板加装OLED显示屏展开。NU40 DK是一块基于nRF54L15芯片的开发板,支持蓝牙低功耗、Thread、Zigbee等多种无线协议,在物联网和可穿戴设备开发中用得很多。它本身没有板载屏幕,所有状态只能靠LED和串口判断,加一块OLED之后,设备就能独立显示信息了。
适合谁来读?如果你手上有NU40 DK或者类似的开发板,已经跑通过基础例程,想进一步做脱机可视化;或者你在做低功耗传感器节点、便携设备原型,需要一块小屏做状态显示;再或者你单纯想学I2C外设驱动,OLED是个非常好的练手项目——它协议简单、资料多、效果直观。这篇文章会从选屏、接线、驱动移植、显示优化到低功耗处理,把整个流程和踩过的坑都讲清楚。
需要提前说明的是,NU40 DK的排针布局和电压域有它的特殊性,不能照搬其他开发板的接线方式。下面会具体展开。
2. 选屏与硬件接线:别急着插线,先搞清楚这三件事
2.1 为什么0.96寸I2C OLED是首选
市面上给开发板配的小屏主要有几种:0.96寸和1.3寸的I2C OLED、SPI OLED、以及一些并口LCD。我推荐从0.96寸I2C OLED入手,原因很实际。
第一,接线少。I2C只需要两根信号线(SDA、SCL)加电源和地,一共四根线。SPI屏至少要六根线,接线复杂度翻倍,调试时排查接触不良的时间成本很高。第二,驱动成熟。SSD1306这颗驱动芯片的资料铺天盖地,各种平台都有现成库,移植工作量小。第三,功耗可控。OLED是自发光,显示黑色像素时不耗电,做低功耗设备时可以让屏幕大部分区域显示黑色,只在需要时点亮少量像素。
0.96寸的分辨率是128×64,虽然不大,但显示三四行文字、几个图标、简单的进度条完全够用。1.3寸的也是128×64,只是像素间距大一点,看起来更舒服,但功耗略高。如果设备外壳空间紧张,0.96寸更合适。
注意:买屏的时候一定要确认是I2C接口还是SPI接口。有些店铺标题写“I2C/SPI通用”,实际发货是SPI版本,需要自己改焊电阻。下单前问清楚,或者直接买标注“I2C 4针”的版本。
2.2 NU40 DK的引脚怎么选
NU40 DK的排针引出中,I2C接口有好几组可选。nRF54L15芯片内部有多个TWI(Two-Wire Interface,就是I2C)实例,理论上任意GPIO都可以配置成I2C功能,但实际选引脚时要考虑几个因素。
优先选芯片默认的TWI引脚,这样在设备树或配置文件中改动最小。其次要考虑引脚是否已经被板载外设占用——NU40 DK上有些引脚连了LED、按键、传感器,如果冲突了就得换。第三,如果同时要用其他外设(比如SPI屏幕、UART串口),要避免引脚功能冲突。
我一般选P1.02做SDA、P1.03做SCL,这两个引脚在NU40 DK上默认没有接其他外设,而且位置在排针边缘,接线方便。具体引脚编号以你手上的板子版本和原理图为准,不同批次的DK可能略有差异。
电源方面,OLED模块一般支持3.3V和5V供电,NU40 DK的排针上有3.3V输出,直接接就行。但要注意电流——SSD1306全亮时电流大约20mA,NU40 DK的3.3V稳压器带这个负载没问题,但如果同时挂多个外设,要算一下总电流。
2.3 接线表与上拉电阻的处理
把线接对是第一步。下面是我常用的接线对应关系:
| OLED引脚 | NU40 DK引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 供电正极 |
| GND | GND | 共地 |
| SDA | P1.02 | I2C数据线 |
| SCL | P1.03 | I2C时钟线 |
接线时有个细节容易被忽略:I2C总线需要上拉电阻。很多OLED模块板上已经自带了4.7kΩ或10kΩ的上拉电阻,这种情况下直接接就行。但如果你用的模块没有上拉,或者总线上挂了多个从设备导致上拉不足,就需要在SDA和SCL上各加一个4.7kΩ电阻拉到3.3V。
怎么判断模块有没有上拉?看模块背面,SDA和SCL走线附近如果有两个贴片电阻连到VCC,那就是自带的。没有的话,通信会不稳定,表现为偶尔能识别到设备、偶尔识别不到,或者读数据时随机出错。
提示:接线尽量短,杜邦线不要超过15厘米。I2C在长导线上容易受干扰,尤其是时钟频率拉到400kHz时。如果必须用长线,把速率降到100kHz试试。
3. 驱动移植:从零跑通SSD1306的完整链路
3.1 先确认I2C总线能扫到设备
在写显示代码之前,必须先用最基础的方式确认硬件连接没问题。NU40 DK的开发环境里,可以用I2C扫描的方式检测总线上有哪些设备。
SSD1306的默认I2C地址是0x3C(7位地址),有些模块是0x3D。扫描的时候如果两个地址都扫不到,说明接线或上拉有问题,先别往下走。
扫描的逻辑很简单:依次向0x00到0x7F的地址发送起始条件,看哪个地址有应答。有应答就说明该地址上有设备。代码大致长这样:
for (uint8_t addr = 0x00; addr < 0x80; addr++) { if (i2c_write(addr, NULL, 0) == 0) { printk("Found device at 0x%02X\n", addr); } }如果扫到了0x3C,恭喜你,硬件链路通了。扫不到的话,按这个顺序排查:先量VCC和GND之间是不是3.3V,再检查SDA和SCL有没有接反,然后确认上拉电阻,最后换一块OLED模块试试是不是屏本身坏了。
3.2 SSD1306初始化序列里每一步在干什么
SSD1306上电后不会自动工作,需要发送一串初始化命令。很多人直接抄一段初始化代码能用就行,但不知道每步的含义,出了问题就抓瞎。我把关键命令拆开讲。
第一步是关闭显示(0xAE),让屏幕在配置过程中保持黑屏,避免上电时闪花屏。第二步设置时钟分频和振荡频率(0xD5),这个参数影响刷新率和功耗,默认值0x80对应大约370kHz的振荡频率,刷新率约105Hz,够用。第三步设置多路复用比(0xA8),128×64的屏要设成0x3F,也就是63,表示64行全部启用。
接着设置显示偏移(0xD3)为0x00,不偏移。设置起始行(0x40)为0,从第0行开始扫描。然后设置电荷泵(0x8D),这个很关键——SSD1306内部需要升压电路把3.3V升到约7.5V来驱动OLED像素,命令0x14表示启用电荷泵,不启用的话屏幕完全不亮。
内存寻址模式(0x20)设成0x00表示水平寻址,写数据时列地址自动递增,写满一行自动换到下一行,这是最常用的模式。设置段重映射(0xA1)和公共输出扫描方向(0xC8)决定屏幕的显示方向,这两个命令配合可以旋转画面180度。
对比度设置(0x81)后面跟一个0x00到0xFF的值,我一般用0xCF,亮度适中,功耗也可以接受。最后开启显示(0xAF),屏幕就亮起来了。
整个初始化序列大概二十多条命令,顺序不能乱。尤其是电荷泵必须在开启显示之前启用,否则屏幕不亮。
3.3 显存管理与刷屏策略
SSD1306内部有一块128×64位的显存(GDDRAM),总共1024字节。MCU不能直接读这块显存,只能通过I2C写入。所以常规做法是在MCU内存里也开一块同样大小的缓冲区,所有绘图操作先改缓冲区,改完之后一次性把整个缓冲区刷到屏幕上。
这样做的好处是避免频繁的I2C通信。如果每画一个像素就发一次I2C,刷一屏要发几千次,速度慢且容易出错。用缓冲区的话,一次I2C传输发1024字节数据,在400kHz速率下大约25毫秒,可以接受。
缓冲区的组织方式是按页(page)来的。128×64的屏分成8页,每页8行,每页128列。缓冲区里第0到127字节对应第0页,第128到255字节对应第1页,以此类推。每个字节的8个位对应这一页中某一列的8个像素,最低位在最上面。
刷屏的时候,先设置页地址和列地址,然后连续写入128字节。循环8次把8页都写完。如果用了水平寻址模式,可以直接从第0页第0列开始连续写1024字节,芯片会自动换页,更省事。
注意:I2C传输1024字节时,要确认驱动层支持这么大的单次传输。有些平台的I2C驱动默认限制每次传输不超过255字节,需要改配置或者分多次发送。
3.4 字库与图形显示的基本处理
显示文字需要字库。最常用的是6×8和8×16两种点阵字库。6×8字库每个字符占6列8行,在128宽的屏幕上可以显示21个字符。8×16字库更大更清晰,但每行只能显示16个字符。
字库本质上是一个数组,每个字符对应若干个字节,每个字节表示一列的8个像素。比如字符“A”在6×8字库中占6个字节,依次写入屏幕的连续6列,就能显示出“A”。
我一般用取模软件生成自己需要的字库。如果只显示ASCII字符,直接用现成的标准字库就行。如果要显示中文,需要生成中文字库,每个汉字16×16点阵占32字节,字库文件会比较大,但NU40 DK的Flash空间足够。
图形显示方面,画点、画线、画矩形、画圆都是基于画点函数实现的。画点函数接收坐标和颜色(亮或灭),计算出该点在缓冲区中的字节位置和位偏移,然后修改对应位。画线用Bresenham算法,画圆用中点画圆算法,这些算法在嵌入式图形库中都有标准实现。
如果不想自己写图形库,可以移植u8g2或LVGL。u8g2轻量,适合单色屏,支持多种字体和图形;LVGL功能强,但资源占用大,NU40 DK跑LVGL需要仔细裁剪。我一般先用u8g2快速出效果,如果项目需要复杂UI再考虑LVGL。
4. 显示内容设计:让屏幕真正有用而不是摆设
4.1 状态信息的分层与刷新频率
屏幕装上了,显示什么?我见过很多项目,屏幕上永远显示一行“Hello World”或者一个静态Logo,这就浪费了。好的屏幕内容设计应该让操作者一眼就能获取关键信息。
我的做法是把显示内容分成三层。第一层是常驻信息,比如设备名称、固件版本、运行时长,这些不常变,放在屏幕顶部或底部固定区域。第二层是实时数据,比如传感器读数、连接状态、信号强度,这些需要定期刷新,放在屏幕中间主要区域。第三层是临时提示,比如按键反馈、错误代码、配网进度,这些只在特定事件时出现,显示几秒后自动消失或恢复。
刷新频率要控制好。常驻信息不需要频繁刷新,几秒更新一次就行。实时数据根据变化速度来定,温度每秒刷一次足够,信号强度可以每500毫秒刷一次。临时提示按事件触发刷新。
关键是避免全屏刷新。如果每次只改几个字符,可以只刷新变化的那一小块区域,减少I2C通信量。SSD1306支持局部刷新,设置好页地址和列地址范围,只写变化的部分。
4.2 低功耗场景下的屏幕策略
NU40 DK主打低功耗无线应用,屏幕是耗电大户,必须管好。SSD1306在正常显示时电流大约10到20mA,对于纽扣电池供电的设备来说完全不可接受。
低功耗策略有几个层次。最简单的是定时熄屏:设备空闲超过一定时间后,发送显示关闭命令(0xAE),屏幕进入休眠,电流降到微安级。有事件时再唤醒。这个策略适合大多数场景。
更激进的是动态刷新:屏幕只在数据变化时刷新,刷新完立即关闭显示。比如温度每10秒采集一次,采集完刷新屏幕,显示2秒后关闭。这样平均电流可以降到1mA以下。
还有一个技巧是减少亮像素数量。OLED的功耗和亮像素数量成正比,显示大面积白色背景比显示黑色背景耗电得多。所以低功耗设备的UI设计应该以黑色为底,只点亮必要的文字和图标。
如果设备有外壳,还可以考虑用深色亚克力面板,屏幕亮度调低一些,既省电又不会刺眼。
4.3 多页面切换与交互设计
如果NU40 DK上接了按键,可以用按键切换屏幕页面。比如第一页显示环境数据,第二页显示网络状态,第三页显示系统信息。每页内容不同,但共用同一块屏幕。
页面切换的逻辑用一个状态变量管理。按键中断触发时改变页面索引,主循环根据当前索引调用对应的绘制函数。绘制函数只负责往缓冲区写数据,刷屏统一在主循环里做。
按键消抖很重要。机械按键按下和释放时会有几十毫秒的抖动,如果不处理,一次按下可能触发多次页面切换。我一般用20毫秒的软件消抖:检测到按键变化后延时20毫秒再读一次,如果状态一致才认为是有效按键。
如果没有按键,也可以用定时自动轮播,每5秒切换一页。或者根据事件切换,比如配网时自动跳到配网进度页,配网完成后跳回数据页。
提示:页面切换时最好加一个过渡效果,比如先清屏再画新内容,避免两页内容重叠造成视觉混乱。清屏就是把缓冲区全部写0,然后刷屏,再画新页内容。
5. 踩坑实录:那些让我熬夜的OLED问题
5.1 屏幕亮但显示乱码或全白
这是最常见的问题。屏幕能亮说明供电和电荷泵正常,但显示内容不对,原因通常出在初始化序列或显存数据上。
先检查初始化序列是否完整发送。有些驱动库在发送命令时没有正确处理I2C的应答,导致部分命令丢失。用逻辑分析仪抓一下I2C波形,确认每条命令都收到了ACK。
再检查显存数据的组织方式。如果你用的字库是列行式(先列后行),但驱动按行列式解析,显示出来就是乱的。确认字库的取模方式和驱动的解析方式一致。
还有一种情况是全白。这通常是电荷泵电压过高或者对比度设置过大。把对比度命令(0x81)后面的值调低试试,从0xCF降到0x7F。
5.2 I2C通信随机失败
设备跑一段时间后,I2C通信偶尔失败,屏幕卡住不刷新。这个问题很隐蔽,因为不是每次都复现。
根本原因通常是I2C总线锁死。当主设备在传输过程中被中断打断,或者从设备在错误的时间拉低SDA,总线可能进入死锁状态,SCL被从设备一直拉低,主设备无法发起新的传输。
解决办法是在I2C驱动里加超时检测和总线恢复逻辑。检测到SCL被拉低超过一定时间(比如10毫秒),就手动切换SCL引脚为GPIO输出模式,发送9个时钟脉冲,让从设备释放总线,然后重新初始化I2C外设。
另一个原因是上拉电阻不足。杜邦线接触不良、线太长、总线上挂的设备太多,都会导致信号上升沿变缓,高速通信时出错。把速率降到100kHz,或者换短一点的线,通常能解决。
5.3 刷屏速度慢导致的视觉卡顿
用I2C刷一整屏1024字节,在100kHz速率下需要大约100毫秒,400kHz下大约25毫秒。如果主循环里还有其他耗时操作,屏幕刷新就会显得卡顿。
优化方向有几个。第一,提高I2C速率到400kHz甚至1MHz(如果从设备支持)。第二,只刷新变化区域,不要全屏刷新。第三,用DMA传输I2C数据,CPU在传输期间去处理其他任务。第四,降低刷新频率,不是所有数据都需要每秒刷新。
我做过一个项目,屏幕上显示一个实时变化的波形图。一开始全屏刷新,波形卡得像幻灯片。后来改成只刷新波形区域,其他区域不动,流畅度立刻上来了。
5.4 低功耗模式下屏幕唤醒异常
设备从休眠唤醒后,屏幕不亮或者显示异常。这是因为SSD1306在休眠期间如果断电了,重新上电需要完整的初始化序列。如果只是发送了显示开启命令(0xAF),但电荷泵没有重新启用,屏幕就不会亮。
正确的做法是:每次从休眠唤醒后,重新执行完整的初始化序列,或者至少确保电荷泵和显示开启命令都发送了。如果屏幕电源在休眠期间被完全切断,唤醒后必须重新初始化。
还有一个细节:如果用了屏幕的休眠命令(0xAE)而不是断电,唤醒时只需要发送0xAF就能恢复显示,显存内容还在。但如果断电了,显存内容丢失,唤醒后需要重新绘制。
6. 从能显示到好用:几个提升体验的细节
6.1 开机动画与品牌标识
设备上电后屏幕立刻显示内容会显得很生硬。加一个简单的开机动画,比如Logo渐亮、进度条、或者几帧简单的动画,能让设备看起来更精致。
开机动画的实现很简单:在初始化完成后,依次显示几帧预存的图像数据,每帧之间延时100到200毫秒。图像数据可以是一组位图数组,用取模软件生成。
如果Flash空间紧张,可以只做Logo淡入效果:从全黑开始,逐步提高对比度,直到Logo清晰显示。这个效果只需要改变对比度寄存器的值,不需要额外的图像数据。
6.2 屏幕方向与安装适配
OLED模块的安装方向取决于你的外壳设计。如果屏幕装反了,显示内容就是倒的。SSD1306支持通过命令旋转显示方向180度,不需要改代码里的坐标。
具体是修改段重映射命令(0xA1改成0xA0)和公共输出扫描方向命令(0xC8改成0xC0)。两个都改就是180度旋转,只改一个就是水平或垂直镜像。
如果屏幕需要旋转90度,SSD1306不支持。这种情况要么改外壳设计,要么在软件层做坐标变换,把绘制函数的x和y互换。坐标变换会增加一点CPU开销,但128×64的分辨率下影响很小。
6.3 多语言与特殊字符显示
如果产品要出口或者给不同地区的用户使用,屏幕上可能需要显示非ASCII字符。这时候标准字库就不够用了,需要生成包含目标语言字符的自定义字库。
生成自定义字库的流程是:先用字模软件选择需要的字符集,设置字体和大小,导出为C数组。然后把数组集成到工程里,修改显示函数从新字库中取模。
中文字库比较大,一个16×16的汉字占32字节,一千个常用汉字就是32KB。NU40 DK的Flash有1MB以上,放得下。但如果字库太大影响到了其他功能,可以考虑只放项目实际用到的字符,或者用外部Flash存储字库,需要时读取。
6.4 屏幕保护与寿命考虑
OLED像素长时间显示同一内容会老化,亮度下降,形成残影。虽然0.96寸小屏的寿命通常够用,但如果设备需要长期运行,还是要注意。
简单的做法是定时微调显示位置,比如每隔几小时把整个画面偏移一个像素,让像素点轮流工作。或者设置自动熄屏,无人操作时关闭显示。
如果屏幕上有固定不变的Logo或边框,可以定期反色显示几秒,平衡像素老化。这些策略在手机和电视上很常见,移植到小屏上同样有效。
注意:OLED的残影是不可逆的。如果发现屏幕上出现了淡淡的固定图案,说明像素已经开始老化,只能换屏。所以长期显示固定内容的场景,一定要做屏幕保护。
7. 把屏幕用起来之后,项目会变成什么样
给NU40 DK加OLED这件事,看起来只是多了一个显示外设,但实际用起来之后,整个项目的调试方式和交互逻辑都会变。
以前调试靠串口,设备一动就得重新接线。现在屏幕直接显示关键变量,设备放哪儿都能看到状态。以前做低功耗设备,不知道当前功耗状态,现在屏幕上显示运行模式和剩余电量估算,心里有底。以前做无线连接,配网成功没有全靠LED闪灯猜,现在屏幕上直接显示“已连接”和信号强度。
我最近做的一个便携环境监测设备,NU40 DK加OLED加温湿度传感器,装在巴掌大的盒子里。上电后屏幕显示温湿度、电池电压、蓝牙连接状态。按键切换页面可以看到历史最值。设备连续运行了两周,屏幕刷新策略是每5秒更新一次数据,无操作30秒后熄屏,按键唤醒。整体功耗比预期低不少。
如果你也在用NU40 DK做项目,强烈建议花半天时间把OLED加上。硬件成本不到二十块,软件工作量也不大,但带来的调试便利和产品完成度提升是实实在在的。从选屏、接线、驱动移植到显示设计,上面这些内容基本覆盖了全流程。剩下的就是动手试,遇到问题再回来翻对应的章节。