用 OLED 给 STM32 做实时调试面板
经典三问
Q: OLED 调试面板是什么?
A: 它是把程序中的状态、计数和测量值直接显示在小屏上的现场反馈界面。
Q: 为什么需要 OLED 面板?
A: 没有电脑或串口时,仍能看到变量是否变化,从而区分“程序没运行”和“数据显示链路有问题”。
Q: OLED 面板常见使用场景?
A: 显示按键状态、传感器原始值、控制目标、错误码和小车当前工作模式。
LED 点亮之后,程序终于会回应你了。可 LED 的回答只有一位:亮,或者灭。
按键计数到 1 还是 101?定时器有没有每隔 1ms 加一次?编码器正转时到底是正数还是负数?ADC 采到的是 800 还是 2800?PID 输出为什么突然顶到 100%?让一颗 LED 负责这些问题,它只能不停地闪,最后你还是在猜。
所以我把 OLED 提到这里。后面的定时器、电机、编码器、ADC 传感器和 PID 都会产生具体数值,一颗 LED 根本装不下这些信息;先用 OLED 做离线调试面板更合适。OLED 先作为离线调试面板用起来,I2C 的底层细节后面再拆。
先解释这四根线为什么这样接
先以常见的 0.96 英寸、128x64、SSD1306 OLED 为例,驱动采用软件 I2C:
| OLED | STM32F103C8T6 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SCL | PB8 |
| SDA | PB9 |


这里每根线都有自己的任务。
为了先把调试面板搭起来,我们先以江协科技 OLED-V2.0 中“4 针脚 I2C 接口、GB2312”版本的 OLED 驱动代码作为基础。底层初始化、字库和常用显示接口先沿用这套成熟实现,再围绕按键状态和调试信息组织调用,同时解释这些接口在工程里承担什么工作。代码来源、保留的文件和本系列做过的改动,以江协科技原始发布说明为准;原作者的版权和许可边界仍以原始发布说明为准。
VCC 给 OLED 的控制器和像素电路供电。GND 是双方共同的电压参考;没有共地,STM32 认为的高低电平和 OLED 认为的高低电平可能不是同一件事。SCL 是时钟线,STM32 用它规定每一位数据什么时候被采样;SDA 是数据线,地址、命令和显存数据都从这里走。
这里用 PB8 和 PB9,是因为软件 I2C 不依赖固定的硬件 I2C 通道,代码可以主动把这两根 GPIO 拉高、拉低,换引脚也比较容易。OLED 刷新速度不高,先让引脚和时序看得见,比一开始就钻硬件 I2C 状态机更适合这块调试屏。
模块的供电范围、控制器型号和地址要看丝印或原理图。这套驱动在代码里发送 0x78,它对应常说的 7 位地址 0x3C 左移一位后的写地址。有些资料直接写 0x3C,那是另一种地址表示方法,别把两种表示混用。
第一次只显示按键计数
工程里的业务代码先用到四个显示接口:
OLED_Init();
OLED_Clear();
OLED_ShowNum(0, 0, press_count, 5, OLED_6X8);
OLED_Update();
把它和前一篇的按键、LED 接起来:
include "stm32f10x.h"
include "key.h"
include "led.h"
include "OLED.h"
int main(void)
{
uint32_t press_count = 0;
uint8_t led_value = 0;
LED_Init();
Key_Init();
OLED_Init();
OLED_Clear();
OLED_ShowNum(0, 0, press_count, 5, OLED_6X8);
OLED_Update();
while (1)
{
if (Key_A_DetectPressEdge())
{
press_count++;
led_value = (uint8_t)((led_value + 1U) & 0x01U);
LED_ShowBinary(led_value);
OLED_ShowNum(0, 0, press_count, 5, OLED_6X8);
OLED_Update();
}
}
}
按键每产生一次按下沿,LED 翻转一次,屏幕上的数字加一。现在按键消抖还没有接入,所以数字如果一次跳了好几格,OLED 只是把程序收到的事件原样显示出来。屏幕没有冤枉按键,也没有替你修复按键。
第一次看到计数从 0 变成 1,再变成 2,和只看 LED 的感觉完全不一样。灯只能说“发生了”,数字开始告诉你“发生了几次”。
黑屏时,先不要同时改六处
OLED 不亮,最容易出现的动作是:换线、改地址、改初始化、换引脚一起做。最后屏幕真的亮了,你也不知道是哪一处起了作用。
先把问题缩小。只运行:
OLED_Init();
它仍然黑着并不奇怪。初始化只是给 SSD1306 设置显示方向、对比度等状态,显存里还没有你想看的数字。
再写入一个很小的输出:
OLED_Clear();
OLED_ShowNum(0, 0, 8, 1, OLED_6X8);
OLED_Update();
如果左上角出现数字 8,供电、接线、地址、初始化和最基本的显示路径至少已经接通。接下来再查坐标、字模和刷新范围,比一上来刷新整屏更容易判断。
故意把线和地址弄错
把 SCL 和 SDA 交换。PB8、PB9 仍然会被程序翻转,但 OLED 收不到正确的数据,屏幕继续黑着。GPIO 有动作,不代表从机理解了这段动作。
把 OLED.c 里 OLED_WriteCommand() 和 OLED_WriteData() 的从机地址字节从 0x78 改成 0x7A,就相当于把 7 位地址从 0x3C 换成 0x3D:
OLED_I2C_SendByte(0x7A);
程序仍然可以编译、下载和运行,只是 OLED 不回应。当前软件 I2C 驱动没有把 ACK 错误完整地交给主程序,所以你看到的往往只是“没有画面”。
如果只接 VCC、不接 GND,或者把 VCC 接到了模块不支持的电压,问题就不再是代码。先看模块资料和丝印,再动程序。
为什么现在先用软件 I2C
硬件 I2C 当然存在,但我不想让第一块调试屏同时带来两层黑盒:一层是 SSD1306,一层是硬件 I2C 外设状态机。软件 I2C 够慢,却把 SCL、SDA、起始和停止都摊在代码里;哪根线什么时候变化,更容易观察。
这不是说软件 I2C 永远更好。后面如果刷新速度、CPU 占用或总线可靠性成了问题,再把同一块 OLED 迁移到硬件 I2C,比较才有意义。先把屏幕用起来,再回头看它是怎么收到字节的。
最后橘猫说
电脑终端很方便,但调小车时你不一定总带着串口模块、USB 线和电脑。OLED 直接挂在车上,按键计数、定时器计数、编码器增量、ADC 原始值、PID 输出都可以显示在现场。
它也不是万能的。屏幕刷新太频繁会占用软件 I2C,显示函数放进中断会拖长中断时间,整屏反复清空还可能闪烁。业务代码只调用“显示什么”,驱动负责“怎么传过去”;先局部更新变化的数字,别每一圈都把整块屏幕擦掉重画。
后面的实验会不断把新的数据送到这块屏上:定时器告诉你时间有没有在走,编码器告诉你轮子到底转了多少,ADC 告诉你传感器读到了什么,PID 把目标、测量和输出摆在一起。LED 负责提醒,OLED 负责解释。
先别急着加定时器。把两个按键、三颗外接 LED 和这块屏接成一个小小的反馈面板,就能看清一次按下从输入电平走到变量、GPIO 和显示画面的全过程;时间基准随后再加入进来。

把按键计数搬到一块小屏上,程序第一次有了可以盯着的现场。屏幕一直黑着的时候,为什么先按顺序一处一处排除,而不是把所有可疑的地方一起改掉?
浙公网安备 33010602011771号