STM32F1移植SSD1306 OLED驱动:IIC模拟时序与避坑指南

简介:一套适配 STM32F1 微控制器、基于 IIC 接口的 OLED 显示驱动源码,采用 HAL 库编写,可直接集成到现有工程中。它重点解决屏幕初始化、清屏、命令与数据发送以及中文显示等常见需求,适合刚接触嵌入式显示或希望省去底层寄存器配置时间的开发者。资源包共 4 个文件,包括 3 个头文件与 1 个 C 源文件,整体仅 6KB;头文件集中声明函数接口、定义 OLED 配置结构体并内置汉字字库,C 源文件则实现 OLED_Init、OLED_Clear、OLED_SendData 等核心逻辑,代码紧凑,稍作引脚配置即可复用。目前已有 1096 人学习下载,实用性得到初步验证。通过阅读源码,可以理解 STM32F1 的 GPIO 复用功能配置、IIC 外设的初始化流程,以及汉字编码到像素点阵数据的转换方式;配合附带的字库与位图头文件,还能进一步扩展图片或图形显示,为后续开发菜单界面、仪表盘等更丰富的嵌入式人机交互打下基础。

1. 项目定位:STM32F1 点屏的一个稳当选择

做单片机显示这一块,最怕的不是屏不亮,而是驱动代码和平台死绑,换个引脚就重写大半。“OLED .C .H文件”这份资源把 SSD1306 控制器的驱动抽成了两个干净文件——oled.c 负责 IIC 底层时序、命令配置和绘制逻辑,oled.h 对外暴露初始化、清屏、显示字符/数字/中文/位图的入口——拿到手上基本不用改一行就能在 STM32F1 标准库工程里点亮一块 0.96 寸 IIC 接口 OLED。它适合三种人:刚学完 GPIO 和定时器、想用手上屏做数据展示的入门开发者;被硬件 I2C 外设折腾到怀疑人生的老手;以及想给自己的小项目快速集成显示模块的硬件爱好者。下面我从文件结构、核心时序、工程移植到避坑记录逐层拆开讲。

2. 文件解剖与技术选型:oled.c / oled.h / 字模头文件各司其职

先看清楚这套资源的文件布局。解压后,核心就是三个文件:oled.c 是所有功能的实现,从 IIC 的 GPIO 时序到 SSD1306 命令配置、字符绘制全部揉在一起,正常规模在三四百行;oled.h 是对外接口和引脚宏定义,改引脚、调地址、换延时都从这里入手;oledfont.h 是字模数据,里面带一套完整的 ASCII 8×16 点阵,如果后续要显示中文,再往这个文件里追加中文字模数组即可。

把这套文件挪进工程之前,先回答一个绕不开的问题:为什么用 IIC 而不用 SPI?以及为什么很多开源驱动选择 GPIO 模拟 IIC,而不是 STM32F1 自带的硬件 I2C 外设?

2.1 IIC 为何是主角:GPIO 模拟比硬件 I2C 更适合入门

OLED 模组常见的接口方式有 6800/8080 并行、SPI 和 IIC。0.96 寸 128×64 的 SSD1306 屏,绝大多数卖家默认给出四针 IIC 版本:VCC、GND、SCL、SDA。四根线一接就能跑,做小项目时非常省事——你只需要两个 GPIO 就能点亮屏幕,而且 IIC 协议本身是半双工的,速度对这个分辨率的屏来说完全够用。128×64×1bit 的显存也就 1KB,哪怕位宽只有 8 位,刷一帧全屏数据也就写 1024 个字节,在 100kHz 到 400kHz 的 IIC 时钟下,肉眼完全感觉不到卡顿。

那为什么不用 STM32F1 的硬件 I2C 外设?这个我在项目里踩过坑:F1 的 I2C 外设处理总线错误和 BUSY 标志时,如果代码没写好,经常出现卡死在等待事件标志里的情况,网上流传的“F1 的 I2C 外设有玄学”就是这么来的。GPIO 模拟 IIC 的时序完全由延时控制,每一拍时钟都看得见摸得着,出问题用示波器一抓就知道是起始条件没拉出来,还是 ACK 位没读到。这套资源就是采用的 GPIO 模拟方案,好处是引脚不挑,任意两个普通 GPIO 都能驱动,移植到 F0/F4 或者别的厂家芯片时,只要把宏定义里的端口和引脚换掉就行。

引脚分配上,最常见的是 PB6 和 PB7,因为这两个引脚在 F1 上本来就是 I2C1 的 SCL/SDA,如果后续你想切回硬件 I2C,不用改 PCB。但模拟方式下它们只是普通 IO,你也可以用 PA5、PA7 之类的任意组合。驱动代码里对引脚的依赖全部集中在一组宏上,这一点在二次开发那一章我会展开说。

2.2 oled.h 接口总览:7 个函数覆盖 90% 显示需求

打开 oled.h,头文件保护、引脚宏、延时宏下面就是一串 API 声明。我整理成表方便看:

函数名参数含义作用
OLED_Init无初始化 GPIO 和 SSD1306 寄存器
OLED_Clear无全屏填 0,清除所有显示内容
OLED_SetPosx:0~127,y:0~7(页)设置当前读写的光标位置
OLED_ShowCharx,y,字符显示一个 ASCII 字符(8×16)
OLED_ShowStringx,y,字符串指针连续显示字符串,遇到 '\0' 停止
OLED_ShowNumx,y,uint32_t 数值,长度显示十进制整数,支持指定位数
OLED_ShowChinesex,y,汉字序号显示字库里的中文字模

整体看下来,这套 API 的覆盖面是“够用不花哨”的。日常做仪表盘、传感器数据页、菜单列表,上面这些函数换着组合就够了。OLED_ShowNum 在显示传感器数值时尤其好用,它会按你指定的长度补齐位数,比如要显示 3 位数字,传入 42 显示成 042,这在做某个固定格式的电压/温度界面时非常省心。

额外说明一点:OLED_SetPos 里的 y 参数不是像素行,而是页(Page)。SSD1306 把 64 行像素分成 8 页,每页 8 行,所以 y 的合法范围是 0 到 7。这个设计贯穿整个 oled.c 的实现,如果你对它不熟,后面看 OLED_ShowChar 的代码时容易懵,这里先记住。

2.3 oled.c 内部结构:时序层、命令层、绘制层三段式排布

从函数排布上,oled.c 大致分三段。文件开头是一组宏和延时函数,中间是四个 static 的 IIC 时序函数——IIC_Start、IIC_Stop、IIC_SendByte,以及两个写命令/写数据的入口;后面就是所有对外 API,每个 API 都调用前面的时序函数。这其实就是裸机驱动最常见的分层:对外接口与底层硬件隔离,改引脚只动宏,改时序只动中间那一坨。

很多人初学时会在一开始就把 IIC_Start 这些函数写成非 static 的,然后在 main.c 里直接调用。我一般不建议这么干,因为这些时序函数属于驱动内部实现,暴露出去会让调用关系变乱。这套资源把时序函数用 static 锁住,对外只留 WriteCmd 和 WriteData 两个口子,属于很规范的写法。你如果自己写驱动,也建议照这个方式藏内部细节。

3. 核心模块拆解:一条 IIC 指令从写入到上屏的完整链路

这一章我们顺着数据流走一遍:从调用一个显示函数开始,到屏幕点亮像素为止,中间经历了 IIC 起始条件、器件寻址、控制字节区分、数据写入、SSD1306 内部显存刷新这几个环节。每个环节我都贴出核心代码,并说明为什么要这么写。

3.1 IIC 底层时序:起始、停止、写字节三个状态

IIC 协议本身不复杂,空闲时 SCL 和 SDA 都是高电平,起始条件是在 SCL 为高时把 SDA 拉低,停止条件是在 SCL 为高时把 SDA 拉高。数据位则是 SCL 低电平时可以变化,SCL 高电平时必须稳定。OLED 驱动里这三个动作的实现如下:

// IIC 起始条件:SCL 高电平期间,SDA 产生下降沿 static void IIC_Start(void) { OLED_SDA_H(); // 数据线先拉高 OLED_SCL_H(); // 时钟线拉高,进入空闲态 OLED_Delay_5us(); OLED_SDA_L(); // 下降沿,从机检测到起始条件 OLED_Delay_5us(); OLED_SCL_L(); // 拉低时钟线,准备传第一位数据 }

这里有个容易被忽略的细节:起始条件产生后,要立刻把 SCL 拉低。如果不拉低,后面的 IIC_SendByte 在第一次翻转 SDA 时就会因为 SCL 还是高电平而被从机误判为又一个起始或停止条件。我在初学时就翻过车,示波器抓出来的波形前后矛盾,查了好久才发现是这里少了一句。

// 写一个字节,高位在前 static void IIC_SendByte(uint8_t dat) { uint8_t i; for (i = 0; i < 8; i++) { if (dat & 0x80) // 先取最高位 OLED_SDA_H(); else OLED_SDA_L(); dat <<= 1; // 左移一位,准备下一 bit OLED_SCL_H(); // 时钟高电平,从机在此时采样数据 OLED_Delay_5us(); OLED_SCL_L(); // 时钟低电平,允许 SDA 变化 OLED_Delay_5us(); } }

IIC_SendByte 的 5us 延时决定了通信速率。72MHz 主频下,这个延时实际跑出来大概是几百纳秒到一微秒的量级,整体时钟频率落在 100kHz 至 400kHz 之间,SSD1306 完全吃得消。如果你用的是主频更低的芯片,可以把两个延时改成 10us,稳定性更好;追求速度则适当缩小,但不要低于 1us,否则从机采样可能来不及。

停止条件的代码逻辑和起始对称:

static void IIC_Stop(void) { OLED_SDA_L(); // 先保证 SDA 为低 OLED_SCL_H(); // 拉高时钟 OLED_Delay_5us(); OLED_SDA_H(); // SCL 高电平期间 SDA 上升沿,产生停止条件 OLED_Delay_5us(); }

这段代码没有读取 ACK。模拟 IIC 可以不管 ACK,反正屏幕不响应时你发再多字节也是往空气里写,最终表现是屏不亮。如果你要做可靠性要求更高的通信,可以在写字节后把 SDA 切换为输入模式读一位 ACK,但这会让代码复杂不少,对点屏这个场景来说收益很低。

3.2 命令通路与数据通路:0x00 与 0x40 的区分

SSD1306 的 IIC 传输帧由三部分组成:器件地址、控制字节、数据字节。器件地址是 0x78(写方向),如果屏的 SA0 引脚被拉高,地址会变成 0x7A。控制字节决定了后面的数据是命令还是显示数据,这是 SSD1306 特有的设计。

// 写命令:控制字节 0x00 表示后跟命令 void OLED_WriteCmd(uint8_t cmd) { IIC_Start(); IIC_SendByte(0x78); // 器件地址 + 写位,0x78 对应 SA0 接地 IIC_SendByte(0x00); // 控制字节:Co=0,D/C#=0,命令模式 IIC_SendByte(cmd); IIC_Stop(); } // 写数据:控制字节 0x40 表示后跟显示数据 void OLED_WriteData(uint8_t dat) { IIC_Start(); IIC_SendByte(0x78); // 器件地址同上 IIC_SendByte(0x40); // 控制字节:Co=0,D/C#=1,数据模式 IIC_SendByte(dat); IIC_Stop(); }

这个接口是整套驱动的地基,所有命令配置和像素写入最终都归结到这两个函数上。OLED_WriteCmd 用于配置寄存器,比如开关显示、设置对比度、选择页地址;OLED_WriteData 用于往显存里写字节点阵和清屏数据。值得注意,在命令模式里如果你连续发命令,第二个命令命令还可以用 Co=1 的 0x80 控制字节省掉一遍地址发送,但这个优化对点屏速度没有本质提升,一般驱动都不做。

3.3 SSD1306 初始化序列:每个配置命令的作用

初始化是屏能不能正常工作的关键。SSD1306 芯片上电后处于一个未知状态,必须通过一段命令序列把它配置到我们想要的显示模式。下面是这套驱动里典型的初始化序列:

void OLED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 1. 使能 GPIOB 时钟并配置 PB6/PB7 为推挽输出 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); OLED_Delay_MS(100); // 等待模组内部电源稳定 // 2. SSD1306 寄存器配置序列 OLED_WriteCmd(0xAE); // 关闭显示,进入配置态 OLED_WriteCmd(0x20); // 设置内存寻址模式 OLED_WriteCmd(0x02); // 选择页寻址模式,一次操作一页 OLED_WriteCmd(0x81); // 设置显示对比度 OLED_WriteCmd(0xCF); // 对比度值 207,太暗可以调大 OLED_WriteCmd(0x8D); // 电荷泵设置 OLED_WriteCmd(0x14); // 开启电荷泵,这是点亮 OLED 的关键 OLED_WriteCmd(0xAF); // 打开显示,屏幕此时才真正亮起来 }

这里面最关键的三个命令是 0x8D/0x14 的电荷泵开关、0x20/0x02 的寻址模式选择,以及 0xAF 的显示开关。电荷泵不开,屏幕永远不亮;寻址模式选错,清屏和画字符时页面跳转逻辑会乱;0xAE 和 0xAF 的顺序不能反,先关显示再配置,配置完再开显示,否则中间状态不可控。

有些模组还带有 RES 复位引脚,如果这块屏把 RES 引出来且接了 MCU 的 GPIO,在初始化开头应该先给 RES 一个低电平脉冲再拉高。这套驱动默认 RES 悬空或接上拉,所以直接用延时代替硬件复位。如果你手里的屏带 RES,我建议在 OLED_Init 的第一行加上“拉低 RES、延时 10ms、拉高 RES”这个动作,能显著提高冷启动成功率。

3.4 字符显示与字模查找:从 ASCII 到中文的取模约定

字符显示是这套驱动里最体现“套路”的部分。SSD1306 的显存按页组织,每页 8 行像素,正好和 8×16 的字符两页对应。oledfont.h 里的字模数组结构如下:

// 8×16 ASCII 字模:每个字符 16 字节 // 前 8 字节为字符上半部分,按列从上到下取值 // 后 8 字节为字符下半部分 const uint8_t F8x16[96][16] = { // 0x20 空格 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // 0x21 '!' {0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00}, // ... 其余字符按 ASCII 顺序排列 };

OLED_ShowChar 的写法体现了页寻址模式的核心用法:一个字符占两页,先写上半页,再写下半页。

void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t ch) { uint8_t idx, i; if (x > 120) { // 8 像素宽的字符,超出 120 就放不下 return; } idx = ch - ' '; // 字模表从空格开始,偏移即索引 OLED_SetPos(x, y); // 光标定位到第 y 页(字符上半部分) for (i = 0; i < 8; i++) { OLED_WriteData(F8x16[idx][i]); // 写上半 8 字节 } OLED_SetPos(x, y + 1); // 光标移动到下一页 for (i = 8; i < 16; i++) { OLED_WriteData(F8x16[idx][i]); // 写下半 8 字节 } }

OLED_SetPos 的实现必须配合页寻址模式,它由三个命令拼成:0xB0+页号、列地址低四位、列地址高四位。这里有一个经典的坑:列地址命令 0x00 和 0x10 只是“低四位”和“高四位”,很多新手误以为这是列地址高低字节,顺序搞反之后字符位置全乱。

void OLED_SetPos(uint8_t x, uint8_t y) { OLED_WriteCmd(0xB0 + y); // 页地址,y 取 0~7 OLED_WriteCmd(0x00 + (x & 0x0F)); // 列地址低 4 位 OLED_WriteCmd(0x10 + (x >> 4)); // 列地址高 4 位 }

中文字模的原理完全一样,不同的是中文字模是 16×16 点阵,占 4 个页(两列叠加),每字 32 字节。OLED_ShowChinese 里需要连续定位四个页位置,横向两页、纵向两页。字模取模时要用“列行式、高位在下”的参数,和英文的取模约定保持一致,否则中文显示出来是镜像或者错位。

4. 快速落地:在标准库工程里把 OLED 点亮

前面把代码逻辑讲透了,这一章讲操作。不管你用的是正点原子、野火还是自己搭的标准库工程,移植步骤都差不太多。我按 Keil MDK 环境下的典型流程走一遍。

4.1 工程准备:文件拷贝、头文件路径与引脚定义

第一步把三个文件放进工程目录,一般放在 HARDWARE/OLED 文件夹下,和你的其他外设驱动并列。然后在 Keil 的工程树里新建一个 OLED 分组,把 oled.c 添加进去。这一步容易漏的是头文件路径:在 Options for Target → C/C++ → Include Paths 里加上 HARDWARE/OLED,否则编译直接报找不到 oled.h。

第二步是核对引脚宏。打开 oled.h,找到类似下面的定义:

// 接线说明:VCC->3.3V,GND->GND,SCL->PB6,SDA->PB7 #define OLED_SCL_PORT GPIOB #define OLED_SCL_PIN GPIO_Pin_6 #define OLED_SDA_PORT GPIOB #define OLED_SDA_PIN GPIO_Pin_7

如果你的板子把 SCL/SDA 接到了别的引脚,只需要改这四个宏。这也是我前面说“这套资源移植方便”的原因——整个驱动里没有任何一处直接写死 GPIOB 或 Pin_6,所有硬件操作都经过这些宏。改完宏之后,记得确认 GPIOB 的时钟有没有在 OLED_Init 里打开,前面代码里已经写了 RCC_APB2PeriphClockCmd,有些移植者会自作聪明把这段删掉然后说屏点不亮。

4.2 初始化调用顺序与上电时序

初始化顺序看起来简单,实际有讲究。正确的顺序是:先初始化系统时钟和延时函数,再初始化 OLED,最后清屏。反过来的后果是 OLED_Init 里用了 delay 但延时没初始化,Delay 函数可能变成死循环或者延时为零。

// main.c 中典型的初始化顺序 int main(void) { Delay_Init(); // 初始化系统 tick 延时,主频 72MHz OLED_Init(); // 内部已包含 100ms 上电延时 OLED_Clear(); // 清屏,消除上电随机亮点 // 开始显示内容... }

OLED_Init 里那个 100ms 延时是给模组内部电源管理用的。IIC 接口的 OLED 模组一般不带硬件复位电路,上电瞬间如果立刻发命令,SSD1306 内部电荷泵可能还没起来,命令会被吞掉。这个延时我给到 100ms 是比较保守的,实际 20ms 以上基本都能过,但第一次跑的时候宁可多等。

4.3 验证程序:跑数字、跑字符串、跑绘图

代码移植完,先用一个最简单的 demo 验证通路。不要一上来就跑动画或者刷图,那样问题点和现象混在一起很难查。我的习惯是分三步验证:先显示固定字符串,跑通 IIC 基本通路;再显示动态数字,验证重复写入;最后全屏清一次屏,确认没有残影。

#include "stm32f10x.h" #include "oled.h" #include "delay.h" int main(void) { Delay_Init(); OLED_Init(); OLED_Clear(); OLED_ShowString(0, 0, "STM32F1 OLED Test"); OLED_ShowString(0, 2, "IIC 128x64 OK"); OLED_ShowString(0, 4, "Hello, World!"); while (1) { // 每隔 500ms 刷新一次计数,验证动态显示 OLED_ShowNum(0, 6, 2024, 4); Delay_MS(500); OLED_ShowNum(0, 6, 2025, 4); Delay_MS(500); } }

OLED_ShowNum 直接传一个 uint32_t 数值进去,最后一个参数是指定位数。这里传 4 就表示固定显示 4 位数字,不足位补零。注意这个函数不会自动清掉高位的旧数字,比如从 9999 变到 1,如果 len 传的是 2,那前两位的 99 会残留。要避免这种问题,要么 len 固定传最大值,要么在刷新前用空格覆盖旧值,这个我在避坑章会单独讲。

5. 避坑笔记:IIC 点屏最常见的四个翻车现场

裸机驱动这东西,跑通很简单,出问题却往往很玄学。下面这四个坑是我在实际使用这套 OLED 驱动时真遇到过的,每个都按“现象、原因、解决”三段写清楚,希望能帮你少走弯路。

5.1 坑一:OLED 上电全黑,程序却在正常跑

  • 现象:main 里 OLED_Init 和 OLED_ShowString 都执行了,单步调试时代码全程走完,但屏幕就是黑的,一点反应没有。
  • 原因:最常见的是 SCL 和 SDA 接反了。网上买的模组,排针顺序有 GND/VCC/SCL/SDA 和 VCC/GND/SCL/SDA 两种,有些卖家标注还不准。接反之后,IIC 起始条件发在错误的引脚上,SSD1306 根本没被唤醒,但 MCU 这边代码照常执行,看起来就是“程序在跑,屏是黑的”。
  • 解决:对照模组丝印重新核对四根线,然后用万用表量一下 VCC 引脚对地电压。如果 VCC 是 3.3V、GND 是 0V,再用示波器或逻辑分析仪抓 SCL 引脚,看有没有时钟波形。没有波形就是引脚配置不对,有波形还不亮就要查器件地址。

5.2 坑二:模拟 IIC 抓不到 ACK,数据发不出去

  • 现象:逻辑分析仪抓 IIC 总线,只能看到起始条件和一两个字节,之后就异常停止;或者同一个字节发送两次,从机都没有回应。
  • 原因:GPIO 被配置成了推挽输出。推挽模式下 SDA 引脚一直被 MCU 强驱动,从机想拉低 SDA 送回 ACK 时拉不动,总线时序直接乱掉。很多初学模拟 IIC 的人都会在这上面栽跟头。
  • 解决:把 SDA 引脚配置成开漏输出,并打开内部上拉或者外部加 4.7k 上拉电阻。开漏模式下,MCU 输出低电平可以拉低总线,释放引脚时总线由上拉电阻拉高,从机才有机会拉低 SDA 发 ACK。SCL 保持推挽输出即可,因为时钟线只有主机在驱动。修改后初始化代码里的 GPIO_Mode_Out_PP 要改成 GPIO_Mode_Out_OD。

5.3 坑三:显示出现残影和字符叠影

  • 现象:刷新数字后,旧数字的笔画残留;或者两个字符重叠在一起分不清。清屏之后重影消失,但一刷新又出现。
  • 原因:清屏或刷新时页面范围没覆盖全。SSD1306 共 8 页,如果 OLED_Clear 只循环了 4 页,或者 OLED_ShowNum 刷新时没有先清掉旧数据,显存里的残留位就会和新数据叠加显示。另一个常见原因是列地址没有重置,在页寻址模式下,写完一页后列地址自动回到 0,但页地址不会自动加一,如果刷新逻辑没考虑页边界,就会出现越界写串。
  • 解决:检查 OLED_Clear 里的循环次数是否为 8,页地址命令 0xB0+0 到 0xB0+7 是否全部覆盖。数字刷新时,OLED_ShowNum 的外部调用要传入足够大的长度,并在写入新数字前先用空格覆盖旧数字区域。我在项目里的做法是写一个 OLED_ShowNumEx 函数,参数里带一个 oldLen,刷新前先按 oldLen 打空格,再写新值。

5.4 坑四:中文全部乱码,英文显示正常

  • 现象:英文和数字完全正常,一旦调用 OLED_ShowChinese,屏幕上出现的是镜像字、碎片或者没有规律的横线,完全不是预期的汉字。
  • 原因:字模取模方向和显示扫描方向不匹配。英文 8×16 字模是“列行式、高位在下”的取模方式,即每个字节表示一列 8 个像素点,从下往上读位。如果你的中文字模用 PCtoLCD2002 之类的软件按“行列式”或“高位在前”的标准取模,字节排列顺序和显存的写入顺序完全对不上,结果就是乱码。
  • 解决:重新用字模软件取模,参数设置改成“阴码、列行式、逆向(低位在前)、每行显示 2 个字节”。取模完成后,对照字模数组里的前两个字节,用笔画验证一下:第一个字节的最低位对应着汉字左上角的第一列最上面那个像素,如果反了说明取模方向还是不对。这块没有捷径,就是反复对照。

6. 二次开发:把固定驱动改成可裁剪、可移植的显示框架

6.1 用宏抽象 GPIO,做到换引脚不换逻辑

这套驱动已经用宏隔离了引脚变化,但如果你要让它跑在非 STM32F1 的平台上,还需要更彻底的一步:把宏定义提升为结构体或回调函数。比如新平台用 Arduino 的 digitalWrite,可以定义一个 HAL 层:

#define OLED_SCL_W(v) digitalWrite(OLED_SCL_PIN, v) #define OLED_SDA_W(v) digitalWrite(OLED_SDA_PIN, v)

这样一来,oled.c 里的时序代码完全不用动,换平台只改这几行。我一般会在移植新平台时先在 oled.h 顶部加一个“平台适配区”注释,把所有硬件相关的宏集中放在一个区块里,避免它们在文件中散落。

6.2 局部刷新改造

原驱动的 OLED_ShowString 和 OLED_ShowNum 都是全量写入字符区域,在显示大量数据时会造成不必要的 IIC 流量。SSD1306 支持按页定位写入,因此可以针对某个区域只刷变化的数据。实现思路是额外增加一个显存缓冲数组,先在内存里修改对应位置的像素,再把变化的部分写到屏幕。这样在做多级菜单或者波形绘制时,不会因为整屏刷新而闪烁。

6.3 内置滚动命令与菜单联动

SSD1306 自带硬件滚动功能,不需要软件一帧帧搬数据。在 OLED_ShowString 显示完文本后,开启水平滚动命令 0x26 和 0x2F 即可让整屏内容平移。实际使用中要把滚动方向、起始页和结束页配置好,否则滚动区域会覆盖到不想滚动的行。建议在菜单切换时关闭滚动(0x2E),切换完成后再重新开启,避免切换过程中画面撕裂。

从那以后,我每次移植这套 OLED 驱动,都强制走一遍“引脚核对、电平确认、单字符测试、区域刷新验证、全屏滚动测试”五步流程,再也没在点屏这种事上熬过夜。希望帮到你。

本文还有配套的精品资源,点击获取

相关新闻

基于深度学习的智慧识花系统:图像分类到Web部署完整实战

基于深度学习的智慧识花系统:图像分类到Web部署完整实战

前阵子有同学拿着“基于深度学习的智慧识花系统”这个题目来找我&#xff0c;说课程设计要交源码、万字报告&#xff0c;还要准备讲解。我接过题目第一反应是&#xff1a;这项目看着不起眼&#xff0c;实际上把计算机视觉里图像分类的整条链路都串起来了——数据集怎么搭、模型…

2026/10/19 17:10:34 阅读更多 →
开源大模型本地部署实战:从硬件选型到企业私有化落地

开源大模型本地部署实战:从硬件选型到企业私有化落地

自从ChatGPT把大模型这个概念彻底带出圈之后&#xff0c;我身边越来越多朋友开始问同一个问题&#xff1a;我也想用AI&#xff0c;但每个月交订阅费有点心疼&#xff0c;数据还全在人家服务器上&#xff0c;有没有别的路子&#xff1f;有&#xff0c;而且这条路这几年越走越宽—…

2026/10/19 17:10:34 阅读更多 →
open-code-review:开源自建代码审查工具链与团队落地实践

open-code-review:开源自建代码审查工具链与团队落地实践

代码审查这事&#xff0c;我在团队里推过很多轮&#xff0c;从最开始的“互相看一眼改了什么”&#xff0c;到后来引入自动化检查工具&#xff0c;再到尝试把审查规范沉淀成团队制度&#xff0c;一路踩了不少坑。前阵子借着一个内部项目的契机&#xff0c;把整套流程重新梳理了…

2026/10/19 17:10:34 阅读更多 →

最新新闻

PowerToys v0.97.0实测:窗口管理与快捷启动的效率指南

PowerToys v0.97.0实测:窗口管理与快捷启动的效率指南

说实话&#xff0c;我挺理解那些在Windows上折腾窗口管理的人。你可能跟我一样&#xff0c;桌面一多开几个应用&#xff0c;就开始在屏幕前手动拖拽&#xff0c;好不容易对齐了&#xff0c;换个显示器分辨率又全乱套。后来又去装各种窗口管理软件&#xff0c;要么收费&#xff…

2026/10/19 17:52:02 阅读更多 →
黑客攻击的7种常见手法:从原理到防御的完整指南

黑客攻击的7种常见手法:从原理到防御的完整指南

1. 先从一件小事说起&#xff1a;为什么你总觉得黑客离你很远&#xff0c;又很近做安全攻防这些年&#xff0c;被问最多的一句话&#xff0c;既不是某个新漏洞的技术细节&#xff0c;也不是某个安全产品的配置方法&#xff0c;而是特别朴素的一句&#xff1a;黑客到底是怎么打进…

2026/10/19 17:52:02 阅读更多 →
artcraft从创意到成品:手工创作全流程与工具材料选择指南

artcraft从创意到成品:手工创作全流程与工具材料选择指南

1. 从“artcraft”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“artcraft”这个词&#xff0c;我脑子里蹦出来的第一反应是“艺术”加“手艺”的组合。这不是一个随便拼出来的名字&#xff0c;它背后指向的是一类非常具体的东西——把创意想法变成看得见摸得着的实…

2026/10/19 17:52:02 阅读更多 →
具身智能人形机器人从舞台到量产:核心技术栈与工程实现解析

具身智能人形机器人从舞台到量产:核心技术栈与工程实现解析

前阵子朋友圈刷到一条消息&#xff0c;说的是某家做人形机器人的创业团队上了台大型晚会的现场展示&#xff0c;还放风说正在把商业融资节奏往前推、为冲击公开资本市场做准备。底下评论五花八门&#xff0c;有说“这赛道终于要起飞了”的&#xff0c;也有说“又是PPT造机器人”…

2026/10/19 17:52:02 阅读更多 →
AI辅助教材写作全攻略:从原创内容到低重复率

AI辅助教材写作全攻略:从原创内容到低重复率

一提起“低查重AI教材写作”&#xff0c;很多人第一反应是“又有人教人钻空子了”。我在出版和内容策划这行做了快十年&#xff0c;类似的询问接了不少。但我要先泼一盆冷水&#xff1a;如果你的目标只是把重复率压下去&#xff0c;那你大概率写不出能用的教材。真正的好教材&a…

2026/10/19 17:52:02 阅读更多 →
研究生论文写作效率提升:8个亲测好用的AI辅助工具

研究生论文写作效率提升:8个亲测好用的AI辅助工具

我读研究生那几年&#xff0c;论文写作占掉了差不多三分之二的精力。最折磨人的不是“想不出写什么”&#xff0c;而是大量时间耗在重复劳动里&#xff1a;文献翻了半天找不到一句合适的话、段落写完读着像白开水、参考文献格式改到半夜、最后降重又降得面目全非。后来我把市面…

2026/10/19 17:51:01 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介&#xff1a;本资源为电源技术方向的学术研究文档&#xff0c;聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略&#xff0c;面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址&#xff1a; https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集&#xff0c;共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →