ARTICLE DETAIL

资讯详情

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

STM32上基于SSD1306 OLED的实时调试面板实战:驱动、页面设计到坑点排查

STM32上基于SSD1306 OLED的实时调试面板实战:驱动、页面设计到坑点排查 1. 为什么我把 OLED 当成了 STM32 的日常调试窗口这个项目的起因特别实在当时要给一台不带串口的电机控制板做现场参数调试调试器没带上位机也没装包里只有一块 0.96 寸 SSD1306 OLED 屏幕和几根杜邦线。于是我把这块小屏直接当成了 STM32 的实时调试面板把电流、转速、温度、运行状态全部铺到屏上。结果不但问题查清了后来连量产样机上也保留了这块调试面板因为它在很多场景下比串口日志更顺手。先说个结论OLED 调试面板不是要替代串口或调试器而是解决“没有上位机、不能打断运行、需要直观看到趋势”这三类问题。串口调试有个天然的毛病就是必须连着电脑串口助手只能看文本流信息和时间戳混在一堆日志里找问题基本靠搜。而 OLED 可以把数据组织成“仪表盘”一眼看出当前处于哪个状态、哪个变量异常我实际用下来在现场查问题的效率比翻串口日志高得多。这块 128x64 的 OLED 很适合做这件事显示密度够用功耗低两线 I2C 只占两个引腳主控负载也不高。配合 STM32 的 HAL 库写一个精简 SSD1306 驱动只需要几百行 C 语言代码再加上页面管理和局部刷新逻辑就能实现一个像模像样的实时调试面板。下面我从硬件选型开始讲再把驱动、页面设计、坑点排查和进阶扩展都过一遍照着这个思路做最快一个晚上就能点亮第一屏。1.1 串口日志在实时场景里的三个痛点很多嵌入式项目刚开始时大家都习惯直接重定向 printf 到串口然后在串口助手看打印。这个做法在验证逻辑的时候没问题但在做现场调试和参数标定时痛点非常明显。第一现场环境往往没有电脑。设备装在机柜里、电机旁、户外箱体内旁边根本没有可靠的调试环境你总不能每次都背一台笔记本。更麻烦的是设备壳子已经装好了串口插座不一定引出来调试接口可能被盖住这时候 OLED 直接焊在板子上反而是唯一的窗口。第二串口打印会打断主循环。调试阶段用的多是用阻塞方式发送数据波特率 115200 时一字节大约 86 微秒打印几十个字符就要几百微秒到一毫秒在 1ms 中断控制周期里这是很可观的开销。如果你在中断里直接调用 printf那还要小心重入和阻塞搞不好直接卡死系统。第三日志流本身没有“画面感”。一串数字刷过去你看不出哪个变量异常只有停下来翻鼠标。而 OLED 上可以画趋势条、画状态图标、用颜色和位置区分实时变量这在分析问题比搜索日志要直观得多。我后来在调试电流环时直接在屏上画了一个小型滚动波形图哪个时刻电流异常立刻能看到比看一串十六进制数据强太多。1.2 OLED 调试面板的适用场景和边界OLED 面板适合几类场景PID 参数整定、传感器数据巡检、故障码实时显示、运行时间统计以及设备无上位机情况下的现场诊断。像 DHT11 温湿度、BH1750 光照度、电池电压、PWM 占空比这类数据串口也能打印但 OLED 的优势是“永远在线的仪表盘”不依赖外部工具。也要说清楚边界OLED 不适合输出大段日志不适合展示超过几十个变量的复杂信息。它只有 128x64 像素一屏能放下的文字有限所以我后来把它设计成“多页面 摘要信息”的结构一个页面只放一组相关的变量用按键翻页。这个思路跟手机设置菜单有点类似把信息分组每个页面只展示当前最关心的内容。对调试面板来说真正决定好不好用的并不是屏幕分辨率而是你对信息的分层和刷新策略。2. 硬件选型与接线I2C 四针模块为什么是调试首选做 OLED 调试面板第一步是选屏。市面上最常见的是 0.96 寸 128x64驱动 IC 是 SSD1306接口分 I2C 四针和 SPI 七针两种。我建议调试场景优先选 I2C 四针模块原因很简单只占用两根 GPIO接线少不容易出错而且 100k 或 400k 的 I2C 速率对调试面板来说完全够用。2.1 SSD1306 面板的基本参数SSD1306 是 128x64 点阵的 OLED 驱动芯片内部自带 1KB 显存128 列 x 8 页 x 8 位只需要把像素数据写到显存芯片就会持续刷新屏幕。它的工作电压通常是 1.65V 到 3.3V很多模块板上会带一个稳压和电平转换电路但不同商家模块差异不小接线前最好确认一下模块丝印。选屏时要注意分辨率并不都是 128x64。0.96 寸最常见的是 128x641.3 寸模块可能是 SSD1306 或 SH1106。SH1106 内部显存是 132x64列地址比 SSD1306 多 4 个所以同样命令序列会有一部分显示偏移需要额外处理。做调试面板建议直接用 0.96 寸 SSD1306通用性最好资料最多出问题也最好查。2.2 接线方案与上拉电阻I2C 四针模块的引脚定义一般是 VCC、GND、SCL、SDA。我的接法很简单VCC 接 3.3VGND 接 GNDSCL 和 SDA 接 STM32 的 I2C1 引脚。对于 STM32F103I2C1 的默认引脚是 PB6SCL和 PB7SDAI2C2 是 PB10 和 PB11。如果你用的是其他型号直接查数据手册就行。默认接线参考表模块引脚STM32F103 引脚说明VCC3.3V不建议接 5V部分模块板载稳压可以但调试点统一 3.3V 最稳GNDGND共地必须接好SCLPB6 或任意 GPIO软件 I2C需要上拉电阻模块没有的话用 4.7kΩSDAPB7 或任意 GPIO软件 I2C需要上拉电阻模块没有的话用 4.7kΩ这里有个容易踩的坑很多廉价模块板上的 I2C 上拉电阻是没焊的。如果 SCL 和 SDA 没有上拉通信会不稳定表现是屏幕偶尔不亮、显示乱码、刷屏时闪一下。我遇到过好多次用逻辑分析仪抓波形发现 SDA 信号边沿很缓后来加上 4.7kΩ 上拉电阻到 3.3V 就正常了。如果你的电路板上已经有 I2C 上拉比如板上还挂了 EEPROM 或其他传感器那就不需要重复加。2.3 小心 JTAG/SWD 复用引脚的问题这是初始化阶段最容易忽略的一个问题。STM32F103 的 PA13、PA14、PA15、PB3、PB4 默认是 JTAG 功能如果你为了省引脚把 OLED 的 SCL 或 SDA 接到了这些脚上那程序跑起来屏幕没有任何反应因为引脚功能还没被重映射成 GPIO 复用功能。此时要先用代码关掉 JTAG 功能__HAL_AFIO_REMAP_SWJ_NOJTAG();这条语句的作用是关闭 JTAG释放 PA15、PB3、PB4 给普通 GPIO 使用只保留 SWD 的 PA13 和 PA14。注意调用位置必须在 GPIO 初始化之前否则引脚配置不生效。我第一次踩这个坑是在 PA15 上挂 OLED 的 SDA初始化完屏幕死活不亮量引脚电平发现一直是高还以为是模块坏了。后来查资料才知道 STM32 的 JTAG 默认占用引脚。建议不要碰 PA13/PA14因为有 SWD 下载还要用PA15、PB3、PB4 如果必须用就把禁用 JTAG 的代码放在最前面。另外开启 AFIO 时钟也是必须的__HAL_RCC_AFIO_CLK_ENABLE();如果用的是 STM32G0、STM32L4 等新系列引脚复用逻辑略有不同但本质还是同样的坑先查引脚功能表再接线能省不少时间。3. 驱动层实现自写精简 SSD1306 驱动的关键细节这一节是整个项目最核心的部分。驱动层搞明白了后面做页面就只是画图的问题。驱动代码网上很多可以直接抄但我建议还是自己手写一遍因为你会更清楚每一个命令的含义排查硬件问题时不至于两眼一抹黑。3.1 SSD1306 的 I2C 协议和初始化序列SSD1306 在 I2C 总线上有两种数据包命令包和数据包。第一个字节是控制字节0x00 表示后面跟的是命令0x40 表示后面跟的是显存数据。如果直接用 HAL 库可以把控制字节拆成寄存器地址传给HAL_I2C_Mem_Write写起来非常方便#define OLED_I2C_ADDR 0x78 // 7位地址 0x3C 左移一位后的8位地址 void OLED_WriteCmd(uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, OLED_I2C_ADDR, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 100); } void OLED_WriteData(uint8_t *buf, uint16_t len) { HAL_I2C_Mem_Write(hi2c1, OLED_I2C_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, buf, len, 100); }关于地址有个常见疑问模块上丝印写 0x3C为什么代码里却用 0x78因为 I2C 的 7 位地址是 0x3C而 HAL 库期望的是 8 位地址左移一位后低位补 0所以 0x3C 1 0x78。如果模块的 SA0 引脚被拉高7 位地址是 0x3D8 位地址就是 0x7A。排查屏幕不亮时第一步就应该确认地址对不对。初始化序列是厂商规定的顺序不能随便换。下面是我一直在用的一套精简初始化配置序列命令字节作用10xAE关显示20x20, 0x00水平地址模式30xB0设置页地址为第0页40xC8COM扫描方向反向50xA1段重映射解决左右镜像60xA8, 0x3F设置多路复用比 1/6470xD3, 0x00显示偏移 080x40起始行 090xD5, 0x80时钟分频100x81, 0xCF对比度设置110xD9, 0xF1预充电周期120x8D, 0x14电荷泵使能130xAF开显示这里面最容易被漏掉的是 0x8D 和 0x14。电荷泵如果不使能屏幕即使收到像素数据也不会亮这是我调试 OLED 遇到最多的“不亮”原因。全屏黑、屏幕上什么都没有先检查这个命令有没有正确发出去。3.2 显存结构与写屏逻辑SSD1306 的显存是按“页 列”组织的纵向 64 像素分成 8 页每页 8 个像素横向 128 列。所以写入一个字节时低位对应当前页最上面的像素高位对应最下面的像素。要让某个像素点亮需要根据它的坐标算出页地址和对应的位掩码。页地址模式下写数据前必须设置当前目标页和起始列。列地址是 8 位的但 SSD1306 要拆成两次命令发送低 4 位用 0x00~0x0F高 4 位用 0x10~0x1F。示例代码如下void OLED_SetCursor(uint8_t page, uint8_t col) { OLED_WriteCmd(0xB0 page); // 页地址 OLED_WriteCmd(0x00 (col 0x0F)); // 列地址低4位 OLED_WriteCmd(0x10 ((col 4) 0x0F)); // 列地址高4位 }为什么要有这一步因为 SSD1306 内部的列指针在上次通信结束后停在某个位置如果不重置你会把数据写到莫名其妙的位置显示出来就是乱码。我见过不少“花屏”案例实际上是上次写完数据后没有恢复列地址下一次显示内容就错位了。有了 SetCursor画一个像素就很简单#define OLED_WIDTH 128 #define OLED_HEIGHT 64 uint8_t oled_buffer[OLED_WIDTH * OLED_HEIGHT / 8]; void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t on) { uint8_t page y / 8; uint8_t bit y % 8; if (x OLED_WIDTH || y OLED_HEIGHT) return; if (on) { oled_buffer[page * OLED_WIDTH x] | (1 bit); } else { oled_buffer[page * OLED_WIDTH x] ~(1 bit); } }画点函数是整个绘图系统的基础。画线、画矩形、画圆、画字符本质上都是把对应的像素点设置为 1 或 0。我现在项目里就只有这几个函数完全够用。3.3 自绘字库与基础绘图函数屏幕要显示文字就得有字库。字符可以自己去网上下一个单片机用的 ASCII 点阵字库也可以自己用取模软件生成。我用的是 8x16 字符集一个字符占 16 字节128x64 的屏幕一行能显示 16 个字符排版比较舒服。取模设置建议选择“行列式、阴码、字节正序”这样字符点阵和屏幕像素是一一对应的不容易出现镜像。取模软件生成的是常量数组比如const uint8_t ascii_8x16[96][16] { // 0x20 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // 0x30 0 {0x00,0x00,0x3C,0x66,0x66,0x66,0x66,0x66, 0x66,0x66,0x66,0x66,0x3C,0x00,0x00,0x00}, // 后面每个字符类似 };显示一个字符时先算出字符中心对应的页和列然后逐页把字模数据写进显存缓存。这里要注意8x16 字模分成上半部分 8 字节和下半部分 8 字节它们分别对应上页和下页。我早期经常写错把整个 16 字节当成一页的数据写进去显示出来的字就是上下错位的。在字库基础上封装字符串和数字绘制函数void OLED_ShowString(uint8_t x, uint8_t y, const char *str); void OLED_ShowNumber(uint8_t x, uint8_t y, int32_t num, uint8_t len); void OLED_ShowProgressBar(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t percent);进度条在调试面板里非常好用比如表示电机负载率、电池电量、缓冲占用率。它的实现也就是按百分比填充一个矩形区域但视觉效果比数字直观得多。3.4 点亮第一屏后的验证方法驱动写完先别急着做页面先把屏点亮并确认方向。我习惯按三步验证全屏填充 0xAA、0x55如果屏幕出现横条纹说明页方向和位顺序基本正确。单独在左上角画几个点验证 SetPixel 的坐标换算没问题。显示一行数字“0123456789”确认字符没有左右镜像或上下颠倒。如果字符反了多半是取模方向或者段重映射命令的问题。左右反了就把初始化里的 0xA1 改成 0xA0或者反过来试上下反了就把 0xC8 改成 0xC0。我见过有人改了半天的字库最后就是一条命令的事所以验证阶段一定要固定用同一套初始化序列去排查。4. 调试面板的信息架构页面分组、滚动日志与局部刷新驱动层只能让屏亮起来要做好调试面板关键在于信息架构。OLED 屏太小不能像电脑一样堆窗口所以要把信息拆成多个页面并且控制刷新频率。这一节我讲一下我实际用下来比较舒服的一套架构。4.1 页面管理用结构体数组做导航我设计的调试面板分为几个页面系统状态页跑太久、复位原因、主频、传感器页温度、湿度、光照、气体、参数调整页PID 目标值、当前值、输出、事件日志页、故障码页。页面导航用两个按键短按翻页长按进入/退出调整模式。页面管理用结构体数组最方便每个页面只暴露“进入时画一次”和“周期刷新”两个回调typedef struct { void (*init)(void); // 进入页面时调用绘制静态部分 void (*refresh)(void); // 周期刷新时调用只更新动态区域 uint16_t refresh_interval_ms; } PanelPage; const PanelPage pages[] { {PageSystem_Init, PageSystem_Refresh, 500}, {PageSensor_Init, PageSensor_Refresh, 100}, {PagePID_Init, PagePID_Refresh, 50}, {PageLog_Init, PageLog_Refresh, 200}, };进入页面时先调用 init把标题、表头、边框这些“静态内容”画上去refresh 负责刷新数值。这么做的好处是减少重复绘制静态内容画一次就行动态区域只更新变化部分屏幕看起来很干净也不闪。4.2 局部刷新和脏矩形怎么做到不闪烁OLED 刷新的最大误区就是每次把所有内容重新写一遍。全屏 1024 字节用 I2C 时钟 400kHz 传输理论时间也要 20ms 左右加上协议开销实际更长。如果每 100ms 全刷新一次屏幕会明显闪烁还会占用主循环大量时间。我在做项目时总结出一个策略只画“脏区域”。具体做法是维护一个缓冲每次绘制先写内存缓冲比较一下跟上次发到屏幕的缓冲有哪些字节变化只发送变化的字节。寻常一个数字的更新区域只有 8x16 像素也就是 32 字节I2C 传输不到 1ms刷新频率就能提得很高。首次实现时可以简单一点每个页面维护自己的“需刷新矩形”更新数值前调用Panel_MarkDirty(x0, y0, x1, y1)然后只发这个矩形对应的页和列范围。我甚至试过直接在某个数值区域右侧加一个“刷新戳记”数值没变就不重发这样连续读取相同数据时屏幕完全静止既不占用总线也不闪烁。4.3 环形日志缓冲区把 printf 搬到屏上页面上需要显示日志时屏幕空间不够直接滚动输出我用了一个环形缓冲区保存最近 20 条事件然后显示最新几条#define LOG_BUF_LINES 20 #define LOG_LINE_LEN 28 typedef struct { char lines[LOG_BUF_LINES][LOG_LINE_LEN]; uint8_t head; uint8_t count; } LogRing; void Log_Add(const char *fmt, ...); void Log_PageDraw(void);日志来源可以是系统启动过程、传感器异常、按键操作、掉电事件。例如开机时写入“SYS BOOT OK”发现某次传感器读不到就写入“DHT11 TIMEOUT”。显示时从最新一条往旧条数排列最底部永远是最新事件。这样 OLED 面板就像一个小型数字告警面板现场的问题第一时间就能看到。这个功能还有个好处日志内容可以直接复用已有的 printf 格式化函数只要把最终字符串塞进环形缓冲即可。如果以后接了串口也可以把相同内容同时输出到串口两边信息完全一致。4.4 按键交互翻页和调参都要防抖调试面板不能只有显示还得能操作。我一般用两个或三个按键上一个、下一个、确认。按键扫描放在主循环轮询配合 20ms 消抖状态机短按和长按分别触发不同动作。这里要尤其注意不要在按键中断里直接调 OLED 刷新函数因为屏幕刷新本身是耗时的 I2C 操作放在中断里会阻塞其他中断响应甚至造成数据错乱。实际效果最舒服的是长按进入“参数调整”模式此时短按加减参数值确认键保存。所有参数在内存中修改保存时才写入 Flash。这个交互方式不需要额外菜单系统代码量也少但现场调试足够用了。5. 实测中遇到的坑花屏、不亮、I2C 死锁的完整排查链路项目做完并不代表结束真正涨经验的是调试中出现的一堆坑。我按排查顺序写下来每个都是从实际问题出发。5.1 花屏先分清是数据错乱还是坐标错乱花屏看起来都是乱码但本质分两类。第一类是坐标错乱典型表现是显示内容分成左右两半、字符上下错位、或者某几行内容重叠。这类问题大概率出在页地址和列地址没设置对。我之前写过OLED_SetCursor之后又连续调用写数据但中间忘了重新设页地址导致数据流跨页写入结果就是字符被切成两段。第二类是数据错乱典型表现是字符左右镜像、上下反了、或者一个字里某些点阵不对。这种问题优先检查初始化里的段重映射方向、COM 扫描方向以及取模软件的“阴码/阳码”设置。镜像问题只需改命令不需要改字库。还有一种花屏是“幽灵显示”屏幕某个区域有残留暗影刷新后还有旧内容。这种通常不是软件问题而是电源纹波太大OLED 驱动芯片工作不稳定。有一次项目里接了一个密码锁电磁铁电磁铁吸合瞬间 OLED 就闪一下甚至出现乱码最后在 OLED 电源引脚旁边加了一个 100uF 电容问题基本消失。所以排查花屏时先用示波器看 VCC 和 I2C 波形再怀疑代码逻辑。5.2 OLED 不亮从电源到初始化命令逐条排查屏幕完全不亮是最让人焦虑的问题但排查路径其实固定。我通常按这个顺序走步骤检查项解决办法1VCC 是否有 3.3V用万用表量模块电源引脚2GND 是否共地检查接线和杜邦线接触3RES 引脚状态七针模块 RES 必须拉高或接 GPIO 并正确复位4I2C 地址用逻辑分析仪 / 写读地址函数确认是 0x3C 还是 0x3D5初始化命令是否完整重点确认电荷泵 0x8D 0x14 已经发送6是否执行了 0xAF没有开显示屏就永远是黑的四针 I2C 模块一般不单独引出 RES 引脚但七针模块必须有复位脉冲。有些模块的 RES 引脚悬空也会导致屏幕黑屏处理办法是把 RES 接到 GPIO初始化时拉低 10ms 再拉高确保屏内控制器复位成功。软件上还有一个快速验证技巧发送 0xAE 和 0xAF如果屏幕亮度有变化说明通信和数据链路都是通的问题就出在初始化命令或者像素数据上。如果连亮度变化都没有优先怀疑硬件连接和地址。5.3 I2C 死锁与重新初始化硬件 I2C 最怕的故障是 SDA 被从机拉低后不释放导致主机发停止位也无效总线一直处于忙状态。用 HAL 库时表现为HAL_I2C_Mem_Write一直返回HAL_BUSY或HAL_TIMEOUT。我遇到过一次 OLED 模块在异常掉电后进入奇怪状态SDA 线被它锁住整个系统的 I2C 总线全废了。最简单的恢复方案是加错误处理和重新初始化。超时时间不要设太长比如 100ms失败后先尝试释放 SDA 线然后重新调用MX_I2C1_Init()HAL_StatusTypeDef I2C_WriteSafe(uint8_t addr, uint8_t reg, uint8_t *data, uint16_t len) { HAL_StatusTypeDef status HAL_I2C_Mem_Write(hi2c1, addr, reg, I2C_MEMADD_SIZE_8BIT, data, len, 100); if (status ! HAL_OK) { // 释放总线 GPIO_InitTypeDef gpio {0}; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOB, gpio); // 具体引脚根据接线调整 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); // 重新初始化 I2C MX_I2C1_Init(); } return status; }如果项目里 I2C 总线上同时挂了多个传感器建议给每个设备的写操作都做同样的超时处理否则一个设备异常可能拖垮整条总线。我用软件 I2C 时也会加超时防止读不到 ACK 时无限等待。这个习惯后来在项目里救了我很多次。5.4 Delay 卡死和中断撞车调试 OLED 面板时经常遇到程序跑一会儿就卡住的情况。查到最后发现很多是把 OLED 刷新放在了定时器中断里或者中断里调用了HAL_Delay()。HAL_Delay依赖 SysTick 中断如果在中断上下文里调用它系统可能因为中断优先级问题死锁现象就是主循环不跑了。我的原则是OLED 刷新永远只放在主循环中执行中断里只置位一个“需要刷新页面”的标志位。页面刷新函数在主循环里检查标志然后一次性完成所有 I2C 传输。这样既不会阻塞中断又能保证刷新频率稳定。如果用了 FreeRTOS就把刷新放在独立任务里优先级尽量低避免抢占关键控制任务。6. 进阶玩法FreeRTOS OLED、PID 调参和故障记录驱动和页面架构做好以后这个面板的作用会越来越大。这里分享几个我自己用过的进阶玩法能覆盖不少实际项目需求。6.1 把面板做成独立显示任务跑 FreeRTOS 的项目里界面刷新应该独立成一个任务不要放在其他任务里。我通常的做法是显示任务用消息队列接收页面切换指令用事件组接收紧急故障信号。普通刷新周期由任务延时vTaskDelay控制任务优先级设低一些防止跟控制任务抢 CPU。显示任务伪代码大致是这样void DisplayTask(void *arg) { uint8_t focus_page 0; for (;;) { Panel_Nav(focus_page); // 处理按键和页面切换 pages[focus_page].init(); uint32_t tick HAL_GetTick(); while ((HAL_GetTick() - tick) pages[focus_page].refresh_interval_ms) { pages[focus_page].refresh(); vTaskDelay(pdMS_TO_TICKS(10)); } } }接入 FreeRTOS 之后要注意 I2C 总线的并发访问。如果传感器的读取也在任务里调用同一个 I2C 句柄必须加互斥信号量保护否则两个任务同时调用HAL_I2C_Mem_Write总线数据会交错屏幕就花了。我一开始忘了加锁结果 OLED 和传感器轮流刷新时偶尔花屏加了互斥信号量之后问题立刻消失。6.2 PID 调参场景屏上曲线和数字OLED 调试面板最大的应用价值之一就是 PID 调参。以前调 PID 要接串口上位机看曲线现在直接在屏上把目标值、实际值、输出值和误差画出来。我做过一个温控项目页面里显示当前温度、目标温度、PWM 输出百分比还用 128 像素宽的空间画了一条滚动误差曲线#define CURVE_W 100 #define CURVE_H 40 // 每次采样把误差值写入曲线缓冲右移一列再画到屏幕上 // 曲线对象本质上就是一个环形数组调参时用按键进入调整模式短按加减 Kp、Ki、Kd 的值屏幕实时显示当前参数和实际响应曲线。这个过程不需要电脑和串口现场把参数凑到“过冲小、稳速快”的状态最后再保存到 Flash。调试效率比对着数据瞎猜高很多。6.3 故障记录与掉电保存调试面板如果只显示实时数据掉电后就什么也看不到了。我后来把最近 10 条故障码和复位原因写到 STM32 内部 Flash 或 EEPROM开机后在“故障页”里自动显示“上次复位原因看门狗复位”之类的信息。这样哪怕设备已经断电重启了现场人员也能第一时间看到发生了什么。读复位原因在 STM32F103 上很简单if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST)) { Dbg_Log(FAULT: IWDG RESET); } else if (__HAL_RCC_GET_FLAG(RCC_FLAG_PORRST)) { Dbg_Log(POWER ON RESET); } __HAL_RCC_CLEAR_RESET_FLAGS();这类信息在 OLED 上一显示现场排查的效率立刻不一样。设备异常重启后第一屏就有复位原因不用再去翻日志或猜是不是程序 bug。6.4 现成库 U8g2 到底用不用很多教程推荐直接用 U8g2 库说功能丰富、字体漂亮。我也用过客观讲它的优点很明显支持的屏多字体多有绘图 API 和中文支持几乎可以做到“改改例程就能显示中文”。但它也有缺点代码占用 Flash 偏大实际裁剪后通常也有十几 KB对 flash 紧张的芯片不太友好而且出了问题不好排查底层。相比之下自写半套精简驱动的代码量很小一个 SSD1306 驱动加字库不到 3KB而且每个命令都在自己手里排查硬件问题非常直接。我的建议是如果你只是快速验证原型用 U8g2 最省事如果要做正式产品或者需要在多个项目里复用我建议花一个晚上把精简驱动写好后面省心得多。这个选择没有绝对对错取决于你的项目约束和“时间充裕度”的取舍。我后来在很多项目里都保留了这个“调用方法固定、底层自己控制”的 OLED 面板模块。刚开始写的时候觉得页面切换、刷新策略很麻烦但真正用起来才发现它的价值点不在屏幕本身而在于你愿意为调试工具多花多少思考。把显示面板设计好了现场发生的很多“玄学问题”一眼就能定位这比炫酷的 UI 重要得多。
返回列表