资讯动态

STM32用IIC驱动OLED:从初始化到单页刷新优化与避坑指南

发布时间:2026/10/15 11:36:49 来源:尧图企业网站定制
简介面向STM32学习者的IIC接口OLED显示例程基于标准外设库实现演示如何通过IIC总线驱动OLED屏幕完成字符与图形显示适合入门、课程设计及显示模块快速选型参考。程序工程完整共130个文件以32个C源文件与35个头文件为主体覆盖OLED驱动、STM32外设初始化及主程序逻辑另有Keil工程配置文件uvprojx、uvoptx、可直接烧录的hex与调试用的axf、map、lst等编译输出压缩包整体仅383KB结构紧凑易检索。目前已有2040人学习下载。借助该工程读者可以直观理解IIC时序、OLED写命令/写数据流程以及自发光、高对比度、宽视角等特点如何映射为代码操作同时可参照标准库工程学习GPIO、时钟、IIC外设的初始化方式积累模块化程序组织经验。结合描述中OLED技术背景还能顺带了解OLED与LCD差异为后续移植到其他主控或扩展菜单、传感器显示提供扎实基础。1. STM32 用 IIC 驱动 OLED显示刷新慢但引脚最省的方案0.96 寸、128×64 像素、四个引脚VCC、GND、SCL、SDA这是 OLED 屏在 IIC 模式下最典型的形态。很多第一次接触这块屏的人都会想两根线就能刷出一屏字是不是很轻松实际移植一遍就会发现地址偏移、上电时序、初始化序列这三个地方任何一个错一个字节结果就是白屏或满屏雪花。这篇笔记基于一份可直接下载运行的 STM32 IIC OLED 驱动工程从 IIC 物理层讲到 HAL 库驱动移植再讲到字符、汉字、图形显示最后把白屏、花屏、IIC 卡死这些高频问题按现场逐一排查。适合正在调屏的嵌入式初学者也适合想快速把 OLED 驱动移植进现有项目的熟手。2. IIC 总线与 OLED 硬件选型上拉电阻、驱动芯片和 128×64 的边界2.1 IIC 协议要点两线寻址、应答机制与速度选择IIC 是半双工同步串行总线SCL 提供时钟SDA 负责数据一次完整传输包含起始条件、7 位从机地址、读写位、数据字节和应答位。STM32 作为主机时只需要把地址和缓冲区交给 HAL 库的HAL_I2C_Master_Transmit硬件会自己处理起始、停止和应答不需要手动翻转时钟线。速度上标准模式 100kHz快速模式 400kHz。对 OLED 这种 1024 字节显存的小屏100kHz 也能跑但全屏刷新大概要 90ms 出头改成 400kHz 之后同样刷一屏大约 23ms肉眼感觉就是“不闪烁”和“轻微闪烁”的区别。所以配置 I2C 外设时优先选快速模式代价只是 SCL 上升沿更陡对布线的要求稍微高一点。还有一个容易忽略的点IIC 是漏极开路SCL 和 SDA 必须有上拉电阻才能输出高电平。内部上拉一般在 40kΩ 左右驱动能力偏弱外部再并联 4.7kΩ 到 10kΩ 是常见做法。这个电阻值不是随便选的它决定上升沿时间和总线电流是后面很多奇怪问题的根源。2.2 驱动芯片差异SSD1306 与 SH1106 不能共用初始化序列市面上的 0.96 寸 OLED 模块绝大多数用 SSD13061.3 寸模块有些用 SH1106。两者外观接近但内部 GRAM 大小不一样SSD1306 是 128×64SH1106 是 132×64只是实际显示区仍只取其中 128 列。项目SSD1306SH1106内部 GRAM128×64真正 128 列132×64显示起始列有偏移常见尺寸0.96 寸1.3 寸列地址设置0x00 低列 / 0x10 高列0x02 起偏移 2 列初始化序列0xA8 后跟 0x3F部分寄存器值不同把 SSD1306 的初始化序列直接烧给 SH1106屏幕大概率能亮但画面会整体向右偏 2 个像素或者最右侧出现竖条噪声。识别方法很简单看模块背面丝印SH1106 字样通常直接印在 PCB 或者驱动芯片封装上。一般建议拿到模块先确认驱动型号再选择对应的初始化序列省得后面反复怀疑代码逻辑有问题。2.3 接线与上拉3.3V 电平、屏线长度和外部上拉叠加OLED 的 IIC 是从机供电电压通常 3.3V逻辑电平也是 3.3V。如果直接接到 5V 的单片机引脚上高电平会超过芯片耐压长期使用可能损坏驱动芯片。STM32 大部分型号 I/O 是 5V 容忍但 OLED 模块不一定所以最简单粗暴的原则是VCC 和逻辑引脚都接 3.3V两边同电源共地。屏的排线长度也要控制。IIC 在 400kHz 下排线超过 20cm 之后信号反射和线上电容会让 SCL 上升沿变缓出现在调试器里看起来一切正常、但偶尔第一帧显示不正确的情况。这个时候不是代码的错是物理层的锅。解决办法是降速到 100kHz或者换更短的杜邦线。关于上拉电阻很多模块上已经焊了板载 2.2kΩ 上拉。如果你在 MCU 侧又接了 4.7kΩ 外部上拉并联后等效电阻约 1.5kΩ总线电流变大虽然一般不会立刻烧坏但会让低电平最低电压值升高可能导致从机无法正确识别。判断方法用万用表测 SDA 对地电阻如果已经有几十千欧以下的阻值就不再外加上拉。3. HAL 库 IIC 驱动移植初始化序列、命令字节与显存缓存3.1 CubeMX 配置 I2C1从引脚分配到时钟频率工程使用 STM32CubeMX 生成I2C1 配置为快速模式引脚一般默认 PB6/PB7具体看你使用的芯片型号。以下是初始化函数I2C_HandleTypeDef hi2c1; void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; // I2C总线时钟 400kHz hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; // 快速模式占空比 2:1 hi2c1.Init.OwnAddress1 0; // 主机模式本机地址不参与寻址 hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); } }ClockSpeed决定总线速率400000 对应快速模式如果能稳定运行就不动它如果出现随机错误第一件事是改成 100000 验证物理链路。OwnAddress1是主机自己的地址OLED 从机不需要用到填 0 即可。NoStretchMode保持关闭这样从机拉低 SCL 请求等待时主机能硬件感知并等待在 OLED 内部忙的时候很关键。3.2 从机地址 0x78 的由来和控制字节命令与数据怎么区分SSD1306 的 IIC 从机地址由模块上的地址选择引脚决定常见默认值是 0x3C7 位地址。IIC 传输时最低位是读写标志所以写操作地址为 0x3C 1 0x78。这也是 OLED 驱动代码里出现 0x78 而不是 0x3C 的原因。每个数据帧的第一个字节叫控制字节用来告诉 SSD1306 后续字节是命令还是数据0x00 表示命令0x40 表示数据。这个机制是 IIC OLED 特有的SPI 模式则靠 D/C 引脚区分所以 IIC 驱动里写命令和写数据是两个独立函数#define OLED_ADDR 0x78 // 7位地址 0x3C 左移一位加上写标志后固定为 0x78 #define OLED_CMD 0x00 // 控制字节命令 #define OLED_DATA 0x40 // 控制字节数据 void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] {OLED_CMD, cmd}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, 2, 50); } void OLED_WriteData(uint8_t dat) { uint8_t buf[2] {OLED_DATA, dat}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, 2, 50); }两个函数都把控制字节和有效数据拼成一个 2 字节数组再一次性发出去。这样做的原因是 IIC 每帧可以携带多个字节而 SSD1306 只认每帧第一个字节是控制字节后续字节全部按控制字节类型处理。HAL_I2C_Master_Transmit最后一个参数是超时时间单位毫秒给 50ms 已经足够IIC 一帧最多几毫秒就结束了。3.3 OLED 初始化序列每一行寄存器到底在干什么初始化序列是移植过程中最容易出岔子的地方少了任何一行都可能白屏或者显示异常。下面是工程中使用的完整序列void OLED_Init(void) { HAL_Delay(100); // 等待电源稳定防止上电瞬间初始化失败 OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); OLED_WriteCmd(0x02); // 页寻址模式后续翻页只需 0xB0~0xB7 OLED_WriteCmd(0x40); // 显示起始行 0 OLED_WriteCmd(0xA1); // 段重映射常规方向不镜像 OLED_WriteCmd(0xC8); // COM 扫描方向从上到下 OLED_WriteCmd(0xA6); // 正常显示非反色 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 复用比 64 行对应 128×64 屏 OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移 0 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 时钟分频默认值即可 OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期偏大一点显示更均匀 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM 引脚硬件配置 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 开启电荷泵必须打开否则白屏 OLED_WriteCmd(0x81); OLED_WriteCmd(0x7F); // 对比度0x00 最暗0xFF 最亮 OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH 电压 OLED_WriteCmd(0xAF); // 打开显示 }前面 100ms 延时是很多人会跳过的模块刚上电时内部电容还没充满驱动芯片处于不稳定状态此时立刻发命令大概率无效表现出来就是白屏。如果把延时去掉偶尔能亮、偶尔不能亮这个现象特别像玄学实际上就是上电时序问题。0x8D 0x14是电荷泵开关SSD1306 内部升压电路不开的话屏幕完全没显示这一行最容易被精简后误删。0x81对比度直接影响显示亮度0x7F 在多数环境下清晰又不刺眼。如果模块显示偏淡可以往上调到 0xCF 甚至 0xFF但注意太高时长时间显示固定内容会加速 OLED 烧屏尤其是室内调试经常一屏静止几小时的项目。3.4 显存缓存与坐标抽象1024 字节的 GRAM 操作方式SSD1306 在页寻址模式下GRAM 分成 8 页每页高度 8 像素宽度 128 像素。驱动代码在 MCU 侧维护一个完全相同大小的缓存数组所有绘图先写缓存刷新时再整体推到屏幕uint8_t OLED_GRAM[8][128]; // 页 × 列共 1024 字节 void OLED_Clear(void) { memset(OLED_GRAM, 0, sizeof(OLED_GRAM)); } void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t on) { if (x 128 || y 64) return; // 越界直接丢弃 uint8_t page y 3; // 行号除以 8得到所在页 uint8_t bit y 0x07; // 行号对 8 取余得到页内位 if (on) { OLED_GRAM[page][x] | (1 bit); } else { OLED_GRAM[page][x] ~(1 bit); } }缓存数组把显示坐标拆成页和列第 0 页对应屏幕最上方 8 行第 7 页对应最下方 8 行。OLED_SetPixel把像素写入对应页的对应字节再用位运算修改该像素所在位。凡是涉及绘图、画线、显示字符最后都落到这个函数上坐标转换逻辑就统一了。刷新函数负责把缓存推送到 SSD1306void OLED_Refresh(void) { for (uint8_t page 0; page 8; page) { OLED_WriteCmd(0xB0 page); // 设置页地址 OLED_WriteCmd(0x00); // 列地址低 4 位 OLED_WriteCmd(0x10); // 列地址高 4 位 for (uint8_t col 0; col 128; col) { OLED_WriteData(OLED_GRAM[page][col]); } } }每页都要先写页地址和列地址然后连续写入 128 个字节。SSD1306 在页寻址模式下写入数据后列指针自动加一不需要每列重新设置地址。整个刷新过程是 8 页 × 128 字节共 1024 字节这也是 IIC 模式下全屏刷新的固定开销。4. 字符、汉字与图形显示ASCII 字库、取模工具和分页坐标换算4.1 ASCII 8×16 字库查表而不是逐像素计算显示字符最直接的方法是准备好字库表工程中内置了 95 个可打印 ASCII 字符每个字符宽度 8 像素、高度 16 像素存放为 16 个字节。前 8 字节表示字符上半部分的 8 列后 8 字节表示下半部分const unsigned char ASCII8x16[95][16] { // 0x20 空格 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // 0x21 感叹号 {0x00,0x00,0x18,0x3C,0x3C,0x18,0x18,0x00, 0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00}, // 其他字符按 ASCII 码顺序依次排列 }; void OLED_ShowChar(uint8_t x, uint8_t page, uint8_t ch) { if (ch 0x20 || ch 0x7E) return; // 只接受可打印字符 uint8_t idx ch - 0x20; // 索引偏移 for (uint8_t i 0; i 8; i) { OLED_GRAM[page][x i] ASCII8x16[idx][i]; // 上半行写入当前页 OLED_GRAM[page 1][x i] ASCII8x16[idx][i 8]; // 下半行写入下一页 } }page参数要求字符起始位置对齐到 8 的倍数也就是页边界。因为 16 像素高的字符必然占用两页页地址不齐的话上半行会出现在一个页的中间位置显示结果就是上下两半错位。实际项目中把字符串显示的位置限制在 y0、y8、y16 这些整行坐标问题就杜绝了。4.2 汉字 16×16 点阵取模方向决定显示是否正常中文显示比 ASCII 多一步取模。16×16 的汉字需要 32 个字节常见取模格式是逐列取模前 16 字节对应上半页 16 列后 16 字节对应下半页 16 列。下图示例是“温”“度”两个字的结构const unsigned char Font16x16[][32] { // “温”字的 32 字节点阵数据由取模软件生成 {0x00,0x00,0x7F,0x08,0x08,0x08,0x7F,0x00, 0x04,0x07,0x04,0x04,0xFF,0x04,0x04,0x00, 0x00,0x00,0x3F,0x20,0x20,0x20,0x3F,0x00, 0x04,0x04,0x04,0x04,0xFF,0x04,0x04,0x00}, // 更多汉字依次排列 }; void OLED_ShowChinese(uint8_t x, uint8_t page, uint8_t index) { for (uint8_t i 0; i 16; i) { // 前 16 字节写上半页后 16 字节写下半页 OLED_GRAM[page][x i] Font16x16[index][i]; OLED_GRAM[page 1][x i] Font16x16[index][i 16]; } }取模设置里有一个关键选项取模方式是“逐列”还是“逐行”。逐列取模得到的字节每一位对应纵向的像素正好能直接写入 OLED 的页逐行取模得到的字节每一位对应横向像素直接写入 OLED 就会得到竖条状乱码。常用的取模软件里需要选择“纵向取模”“逆时针 90 度”或者“列行式”这类选项不同软件叫法不一样但结果应该是同一个数据格式。4.3 字符串与数字显示封装好的调用入口单个字符比较简单实际项目里要显示的是“温度25.3°C”这类整行内容。把字符函数再包一层就成了字符串显示函数void OLED_ShowString(uint8_t x, uint8_t page, const char *str) { while (*str ! \0) { OLED_ShowChar(x, page, *str); x 8; // 下一个字符横向偏移 8 像素 if (x 120) // 超出屏幕宽度则换到下一行 { x 0; page 2; // 一个字符高两页所以行间隔是 2 } } }数字显示我一般直接用snprintf转成字符串再调上面的函数避免自己写除 10 取余的循环。注意嵌入式工程里要启用微库或者开启浮点 printf 支持否则%f格式化不出来。整型数字没有这个问题所以传感器上报的整数用snprintf(buf, sizeof(buf), %ld, value)就够了。4.4 简单图形绘制画线、矩形和进度条绘图基础是OLED_SetPixel。画线用线性插值在两端点之间按步数逐点落笔void OLED_DrawLine(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) { int16_t dx x2 - x1; int16_t dy y2 - y1; int16_t steps (dx dy) ? dx : dy; if (steps 0) steps -steps; steps steps 1; // 包含两端点 for (int16_t i 0; i steps; i) { uint8_t x x1 (int32_t)dx * i / steps; uint8_t y y1 (int32_t)dy * i / steps; OLED_SetPixel(x, y, 1); } }这个实现没有用经典的 Bresenham 算法而是按最大轴步数做插值。处理斜率接近水平的线时精度足够代码也短适合不会频繁调用画线接口的场景。如果要做仪表盘刻度这种每帧几十条线的画面再换成 Bresenham 也不迟。进度条是仪表类界面的常用元素void OLED_DrawProgressBar(uint8_t x, uint8_t page, uint8_t width, uint8_t percent) { uint8_t fill width * percent / 100; for (uint8_t i 0; i width; i) { OLED_SetPixel(x i, page * 8, 1); // 上边框 OLED_SetPixel(x i, page * 8 7, 1); // 下边框 } for (uint8_t col 0; col fill; col) { for (uint8_t row 1; row 7; row) OLED_SetPixel(x col, page * 8 row, 1); // 内部填充 } }percent在 0 到 100 之间fill是对应宽度的填充列数。边框和内部填充都落在同一页的两条边界行上刷新后显示效果就是一条扁平的进度条。边界判断已经在OLED_SetPixel里做了所以传入越界坐标不会造成数组越界。5. OLED 显示避坑手册白屏、花屏、IIC 卡死与亮度异常排查5.1 白屏上电时序和电荷泵未开启现象程序运行后屏幕全白或者初始化后什么都显示不出来。原因分两类。一是上电后立刻初始化SSD1306 的电源还没稳定命令被当作无效数据丢弃。模块电源从零开始充满内部电容需要几十毫秒初始化前的延时不够第一批命令很容易丢。二是 0x8D 0x14 这两条电荷泵命令缺失内部升压电路没开OLED 面板没有驱动电压屏幕完全没有内容。解决初始化函数最前面加HAL_Delay(100)确保延时不少于 100ms检查初始化序列里必须有0x8D、0x14、0xAF三段。还有一个细节如果模块上有 RES 引脚需要拉高至少 100ms 再发命令复位引脚悬空会导致芯片工作状态不确定。5.2 花屏GRAM 未清零或页地址越界现象屏幕局部显示乱码或者字符显示一半正常一半错位。原因多数是缓存数组没有初始化。MCU 上电后 SRAM 内容随机如果第一次刷新就把这个随机值推到屏幕自然就是雪花点。另一个原因是页地址循环从 0 写到 7 之后又写了第 8 页而 SSD1306 只有 8 页超过部分的行为未定义。解决初始化调用OLED_Clear()把OLED_GRAM全部清零刷新函数里页循环变量用uint8_t page条件写page 8不要写成 8。如果显示字符串时出现上下错位检查OLED_ShowString里换行时page 2后有没有超过 6超过 6 表示已经写到屏幕外了。5.3 亮度偏暗或闪烁对比度寄存器与刷新方式不当现象屏幕能显示但整体发暗或者刷新时高频闪烁。原因对比度寄存器 0x81 的值设得太低电荷泵本身输出电压正常但液晶驱动能力不足。闪烁则是因为每次更新都先擦除整屏再重画中间有个全黑的过程人眼就感知为刷屏闪烁。解决把 0x81 后的参数调到 0x7F 以上0xCF 是多数人常用的亮度值。闪烁问题要从显示策略入手显示变化的内容时用页刷新而不是全屏刷新先改缓存再一次性调用OLED_Refresh不要在清屏之后做多次部分刷新。特别注意的是刷屏闪烁往往不是硬件问题而是软件把清屏和重画分开了两个步骤执行。5.4 IIC 卡死超时设置与 NACK 处理现象程序运行到HAL_I2C_Master_Transmit之后不再往下走或者运行一段时间后 OLED 无响应重新上电才恢复。原因IIC 总线上有设备拉低 SDA 导致无法产生起始条件或者从机故障不发应答HAL 库默认等待应答会一直等到超时。OLED 排线接触不良时也会出现总线电平被拉死的情况这在接插式模块上很常见。解决把超时参数从代码里的 50ms 适当加大到 200ms至少不会让系统因为一个瞬时错误就停在任务里。更稳妥的做法是检测HAL_I2C_Master_Transmit的返回值返回HAL_ERROR时调用HAL_I2C_DeInit重新初始化 I2C 外设相当于软复位总线。如果是运行几分钟后随机卡死优先检查排线插头和引脚接触这个问题我用杜邦线调了三天才确认是接触问题。5.5 模块型号识别错误导致显示偏移现象屏幕能显示但内容整体偏右最左边空出两列或者右侧多出噪声。原因驱动芯片实际是 SH1106却用了 SSD1306 的初始化序列。SH1106 内部 GRAM 有 132 列显示区前两列偏移需要从第 2 列开始映射数据。解决查看模块背面丝印确认驱动型号。SH1106 的初始化序列里列地址设置要在高 4 位部分加上偏移很多维护成熟的 OLED 库内部已经做了区分。在这份工程里直接把底层的列地址命令改为0x02开始的顺序即可适配 SH1106 模块。5.6 个别模块怎么调都不亮检查地址引脚和供电能力现象同一份代码换一个模块就白屏换回原来的模块又能显示。原因模块的地址选择引脚被焊到了另一侧从机地址从 0x3C 变成了 0x3D对应 IIC 发送地址变成 0x7A。还有一种情况是模块要求的供电电流超过开发板 3.3V 输出的能力满亮度时电压被拉低芯片工作不稳定。解决先测模块 IIC 引脚对地逻辑电平再用示波器看 SDA 上有没有正常波形。确认地址位后把OLED_ADDR从 0x78 改成 0x7A 再测试。供电方面如果 3.3V 端压降超过 100mV 并且模块发热明显建议用单独的稳压芯片供电不要再从 MCU 的 3.3V 引脚取电。6. 进阶优化单页刷新和 DMA 传输让 IIC OLED 的刷新更快IIC OLED 给人的固有印象就是慢但实际上把刷新策略做对显示动态数据完全够用。先算一笔账400kHz 总线速率下IIC 传输一个字节需要 9 位时钟8 位数据加 1 位应答一字节约 22.5 微秒。全屏 1024 字节算上每页的页地址命令开销大约 2.4 万微秒也就是 23ms 左右对应每秒 43 帧。这个帧率对于显示传感器数值、状态切换、菜单界面已经足够了真正的瓶颈往往不在于总线速度而在于每次刷新把整个屏幕的数据都重新推了一遍。优化思路是把“全屏刷新”拆成“单页刷新”。页面寻址模式下一次只更新需要变化的页而不是每帧都刷 8 页。比如显示温度数值变化时数字所在的页通常只有 1 到 2 页就只刷这两页传输量从 1024 字节降到 256 字节左右实际耗时约 5ms。实现起来很简单void OLED_RefreshPage(uint8_t page) { OLED_WriteCmd(0xB0 page); // 页地址0xB0~0xB7 OLED_WriteCmd(0x00); // 列低地址 OLED_WriteCmd(0x10); // 列高地址 for (uint8_t col 0; col 128; col) { OLED_WriteData(OLED_GRAM[page][col]); } }调用OLED_RefreshPage(3)就只刷新第 3 页覆盖的 8 行区域。配合显示逻辑比如第 0 到 5 页是固定标题第 6 到 7 页是动态数字那么平时只刷 6、7 两页标题完全不用动。这里有一个习惯凡是把数据写入OLED_GRAM的接口顺便记录哪些页被修改过刷新时只对修改过的页调用OLED_RefreshPage。再进一步用 DMA 把刷屏从主循环里解放出来。普通HAL_I2C_Master_Transmit是阻塞调用1024 字节刷屏期间 CPU 一直在等待虽然 23ms 不算长但如果在中断频繁的系统里很容易影响实时任务。DMA 传输方式把数据交给硬件搬运CPU 只需在完成中断里做收尾uint8_t refresh_buf[130]; // 1 字节控制字节 128 字节数据 void OLED_RefreshPageDMA(uint8_t page) { refresh_buf[0] OLED_DATA; // 控制字节后面都是显示数据 memcpy(refresh_buf[1], OLED_GRAM[page][0], 128); OLED_WriteCmd(0xB0 page); OLED_WriteCmd(0x00); OLED_WriteCmd(0x10); HAL_I2C_Master_Transmit_DMA(hi2c1, OLED_ADDR, refresh_buf, 130); }这里假设同页内 128 列数据是连续的所以直接用memcpy一次拷入发送缓冲区。DMA 完成后HAL 库会调用发送完成回调在回调里置一个标志位下一次刷新前检查上一个传输是否结束。要注意的是I2C 的 DMA 传输虽然不占 CPU但总线上依然是逐字节搬运不会比阻塞方式更快它的价值在于 CPU 可以在等待期间处理其他任务。实际项目里我习惯把刷新函数放到低优先级任务里不追求每帧都刷新而是用一个 20ms 左右的软件定时器触发刷新。经过这几步调整之后同样的硬件和总线速度动态数字的响应速度从肉眼可见的迟滞变成基本无感。从那以后我每次移植 IIC OLED 驱动都会强制走一遍这套流程确认驱动芯片型号、检查初始化序列、先做单页刷新再考虑 DMA。这套组合下来我基本没再遇到过 IIC OLED 的翻车问题希望也能帮你少踩一些坑。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑