
简介这是一份基于 STM32 标准外设库HAL 库的 OLED 显示驱动示例采用 IIC 串行通信协议完成数据交互面向嵌入式入门开发者或需要快速集成显示模块的工程师解决微控制器与屏幕之间通信和驱动移植的实际问题。压缩包共三个文件含两个头文件与一个 C 源文件头文件用于函数声明与 ASCII 字符点阵数据源文件实现底层 IIC 读写时序、屏幕初始化、显示与清屏等函数整体大小仅 8KB轻量简洁便于直接嵌入工程。已有 1081 人学习/下载反馈良好适合直接用于项目参考或二次开发。通过阅读这份代码开发者能理解 IIC 设备驱动的核心流程包括主从设备通信、寄存器读写和时序控制同时借助 ASCII 字模可快速实现字符与简单图形的显示并为后续扩展菜单、仪表盘等界面功能打下基础。代码注释清晰结构紧凑也适合用于课堂教学或毕业设计。 搞嵌入式这些年最常被问到的外设之一就是OLED屏怎么点亮。尤其现在大家习惯用STM32CubeMX生成工程、再用HAL库写驱动网上能找到的例程不少但很多是拿过来能跑、一改就废要么显示花屏要么IIC总线直接卡死。我最近在做一个小项目需要实时显示传感器数据手头正好有一块0.96寸的IIC接口OLED于是把整套驱动重新整理了一遍从HAL库的IIC配置到上层显示API再到调试中遇到的几个大坑都记录下来。这篇文章适合刚接触HAL库、准备用IIC驱动OLED的开发者也适合已经把屏幕点亮但遇到显示异常、卡死等问题的朋友。1. 为什么我选了OLED IIC而不是SPI1.1 引脚占用的账要算清楚很多人纠结OLED用IIC还是SPI接口我的答案很直接看你的引脚余量和显示刷新需求。SPI接口的OLED虽然刷新速度快但至少要占用SCK、MOSI、CS、DC、RST五个引脚IIC接口的OLED只需要SDA和SCL两根线而且可以挂在总线上和其他IIC器件共享。做传感器数据展示这类场景刷新率要求并不高一秒钟更新几次就够了IIC的带宽完全够用。省下来的几个引脚可以用来接按键、编码器或者别的传感器整体的资源利用率高不少。如果你的项目需要频繁刷全屏动画或者显示视频级画面那才需要认真考虑SPI接口。1.2 HAL库和标准外设库的差异早些年大家习惯用标准外设库寄存器操作都是直接读写代码写起来虽然繁琐但心里有底。HAL库的好处是抽象层做得比较统一CubeMX里勾选一下就能生成初始化代码换芯片平台的时候移植成本低。但代价是HAL库封装了很多超时判断和状态检查IIC这种带状态机的通信协议在HAL库下的行为跟标准库比有不小差别尤其体现在错误处理和通信超时上。我见过不少初学者直接在标准库例程上套HAL库的函数名结果初始化后屏幕没反应第一反应是代码写错了实际上往往是IIC初始化参数或者时序处理不对。后面我会专门讲HAL_I2C的配置细节和容易出问题的地方。2. 先弄懂屏幕的IIC接口地址怎么算、时序怎么走2.1 7位地址和8位地址绝大多数0.96寸OLED屏用的是SSD1306或者SH1106驱动芯片IIC接口的从机地址由硬件引脚决定。常见的是SA0引脚接地7位地址为0x3C如果SA0接高则7位地址为0x3D。这里有一个高频错误点HAL库的HAL_I2C_Mem_Write和HAL_I2C_Master_Transmit函数里地址参数用的是8位地址也就是7位地址左移一位后的值。0x3C左移一位是0x78不是直接填0x3C。如果填错了驱动的返回值一直是HAL_ERROR屏幕毫无反应。2.2 控制字节与寄存器地址SSD1306通过IIC接收数据时每个数据包的结构是起始信号、从机地址写位、控制字节、数据字节。控制字节很关键0x00表示后续字节是命令0x40表示后续字节是显示数据。举个例子发送“开显示”命令0xAF完整的发送序列是uint8_t buf[2] {0x00, 0xAF}; HAL_I2C_Master_Transmit(hi2c1, 0x78, buf, 2, 100);如果把控制字节写成0x40那0xAF会被当作显示数据写进显存屏幕不会执行开显示命令反而可能出现花屏或者显示乱码。很多人在网上抄例程时看到两套写法一套用HAL_I2C_Master_Transmit发控制字节另一套用HAL_I2C_Mem_Write写寄存器地址其实原理是一样的。// 用Mem_Write的写法第二个参数是寄存器地址0x00对应控制字节 HAL_I2C_Mem_Write(hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 100);这两种方式都可以但我更推荐前一种Master_Transmit因为它把控制字节和数据字节放在同一个buffer里发送协议层面更直观也方便一次发多字节数据。2.3 时序与上拉电阻IIC总线是开漏结构所以SCL和SDA必须有上拉电阻才能工作。STM32内部虽然有上拉但驱动能力有限而且CubeMX默认配置的IIC引脚不是开漏输出如果你忘了手动改那内部上拉根本没用。我建议硬件上至少加4.7kΩ或者10kΩ的上拉电阻SCL和SDA各一个。上拉电阻太小总线灌电流过大可能导致电平拉不低上拉电阻太大上升沿太慢高速通信时信号边沿不合格。我在调试时遇到过一种诡异现象屏幕偶尔能点亮但显示数据容易错乱。后来用示波器看波形发现SDA上升沿接近1μs明显太慢。把上拉电阻从10kΩ换成4.7kΩ后问题消失这就是iic上升沿测试标准里的基本要求——上升时间要在协议规定的范围内。3. 驱动代码的落地从底层写函数到上层显示字符串3.1 初始化序列SSD1306上电后需要发送一串初始化命令网上流传的版本很多但核心命令是一致的。我在项目里用的是这样一组static void OLED_InitSequence(void) { OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); // 设置内存寻址模式 OLED_WriteCmd(0x02); // 页寻址模式 OLED_WriteCmd(0xB0); // 设置页地址 OLED_WriteCmd(0xC8); // 扫描方向 OLED_WriteCmd(0x00); // 低列地址 OLED_WriteCmd(0x10); // 高列地址 OLED_WriteCmd(0x40); // 起始行 OLED_WriteCmd(0x81); // 对比度设置 OLED_WriteCmd(0x7F); // 对比度值 OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xA6); // 正常显示 OLED_WriteCmd(0xA8); // 多路复用比 OLED_WriteCmd(0x3F); // 1/64 duty OLED_WriteCmd(0xA4); // 从RAM内容显示 OLED_WriteCmd(0xD3); // 显示偏移 OLED_WriteCmd(0x00); OLED_WriteCmd(0xD5); // 时钟分频 OLED_WriteCmd(0x80); OLED_WriteCmd(0xD9); // 预充电周期 OLED_WriteCmd(0xF1); OLED_WriteCmd(0xDA); // COM引脚配置 OLED_WriteCmd(0x12); OLED_WriteCmd(0xDB); // VCOMH电平 OLED_WriteCmd(0x30); OLED_WriteCmd(0x8D); // 电荷泵 OLED_WriteCmd(0x14); // 开启电荷泵 OLED_WriteCmd(0xAF); // 开启显示 }初始化顺序不是绝对不能动但有几个关键点要注意电荷泵0x8D, 0x14必须在开启显示0xAF之前设置好否则屏幕亮度会非常低甚至黑屏。很多人的OLED显示极暗实际上是漏掉了电荷泵命令。3.2 底层写函数用HAL库实现底层写命令和写数据函数核心是HAL_I2C_Master_Transmit。注意超时时间要留够IIC在400kHz速率下发送一个字节大约几十微秒100ms超时足够但如果系统时钟配置有问题超时太短会导致发送失败。void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR_8BIT, buf, 2, 100); } void OLED_WriteData(uint8_t data) { uint8_t buf[2] {0x40, data}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR_8BIT, buf, 2, 100); }3.3 显存与刷新SSD1306内部有个显存128x64像素对应1024字节。IIC接口的OLED不能像SPI那样连续写整个显存但可以通过页寻址模式配合列地址连续发送。我常用的做法是在单片机内部维护一个128x8的二维数组作为显存缓存需要更新内容时先修改缓存再一次性把整屏推到屏幕。uint8_t OLED_GRAM[8][128]; void OLED_Refresh(void) { for (uint8_t page 0; page 8; page) { OLED_WriteCmd(0xB0 page); // 设置页地址 OLED_WriteCmd(0x00); // 低列地址 OLED_WriteCmd(0x10); // 高列地址 HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR_8BIT, OLED_GRAM[page], 128, 200); } }这里有个性能细节使用页寻址模式时连续发送128个数据字节会写满当前页的128列不需要每写一个字节都重设列地址所以整屏刷新只需要8次Master_Transmit调用每次发128字节效率比逐字节发送高得多。3.4 封装上层API有了显存和刷新函数剩下的画点、画字符、显示字符串就好办了。画点就是修改缓存数组里对应的位void OLED_DrawPoint(uint8_t x, uint8_t y) { if (x 128 || y 64) return; uint8_t page y / 8; uint8_t bit y % 8; OLED_GRAM[page][x] | (1 bit); }字符显示需要准备字库我用的是6x8和8x16两套ASCII字库。8x16字库比较常用一个字符占16字节每字节对应一列的上半部分和下半部分。void OLED_ShowString(uint8_t x, uint8_t y, char *str) { while (*str) { OLED_ShowChar(x, y, *str); x 8; if (x 120) { x 0; y 16; } } }到此一个完整的驱动文件就算写完了。我习惯把写命令、写数据、显存操作、画点、字符显示、字符串显示、清屏这些函数统一放一个oled.c头文件里只暴露上层接口业务代码根本不用关心底层是HAL_I2C_Master_Transmit还是寄存器操作。4. 我在实测里踩过的坑4.1 第一帧花屏的罪魁祸首是上电时序我最初把OLED初始化和外设初始化放在同一个函数里上电后立刻执行初始化命令。结果发现前两次上电偶尔花屏按压复位键后能正常。排查了很久最后定位到是** OLED模块上电后内部驱动芯片还没稳定紧接着的IIC通信命令没有正确执行。**解决办法是在OLED初始化之前加一个延时等电源稳定。我在CubeMX生成的main函数里在OLED_Init()之前加了HAL_Delay(100)花屏问题基本消失。如果是电池供电或者电源纹波比较大的场景建议把延时加到200ms。4.2 HAL_I2C_XXX返回HAL_BUSY的排查链路有次换了一块新的OLED屏屏上没有任何反应HAL_I2C_Master_Transmit返回HAL_BUSY。我排查的顺序是这样的先检查IIC引脚配置发现CubeMX默认把PA11和PA12配置成了开漏这没问题再检查上拉电阻发现手头的模块板载了上拉电阻排除。然后用示波器看SCL和SDA波形发现SDA一直为低。进一步查才发现问题不在OLED而是IIC总线上还挂了一个MPU6050它的地址和OLED冲突了而且MPU6050的SDA输出配置有问题把总线拉死了。移除MPU6050后OLED正常。如果你遇到IIC总线异常优先怀疑总线上其他器件用万用表量一下SDA对地电阻如果接近0基本就是某个从机把总线拉死了。4.3 软件IIC和硬件IIC的取舍很多时候屏幕点不亮不是因为代码问题而是硬件IIC引脚被复用占用或者CubeMX配置的引脚与实际接线不一致。这种时候最直接的排查办法就是临时改用软件IIC用GPIO模拟时序。软件IIC的好处是不受引脚复用限制随便找两个GPIO就能用而且时序完全可控方便逻辑分析仪抓数据。我当时的做法是写了一个software_iic.c用两个GPIO模拟起始信号、停止信号、发送字节。软件IIC虽然占CPU但在显示这种低频场景下完全够用。如果你只是为了快速验证屏幕好坏软件IIC是最快的路径如果做正式产品我还是建议用硬件IIC省CPU资源配合DMA还能进一步提升性能。4.4 地址位和时序标准别想当然热词里有一条“iic上升沿测试标准”这个在调试时确实会碰到。IIC标准模式下上升时间最大1000ns快速模式最大300ns。如果波形上升沿不达标数据建立时间不够从机就可能采样到错误电平体现为屏幕偶尔乱码。测试方法很简单用示波器看SDA和SCL的上升沿配合上拉电阻阻值调整。如果没有示波器可以通过降低IIC时钟速度来规避。CubeMX里I2C的时钟速度默认100kHz这个速度比较稳。如果想跑400kHz务必确认上拉电阻和总线电容是否满足要求。4.5 不要忽略引脚的电气属性配置HAL库的GPIO初始化里IIC引脚需要配置为开漏输出。CubeMX在配置I2C外设时会自动生成正确的GPIO初始化和AF复用配置但如果你是在已有工程里手动添加IIC功能很容易漏掉AF配置。漏配置的后果是引脚保持推挽输出SCL和SDA的电平无法被从机拉低通信必然失败。检查方法是在调试器里看GPIO寄存器配置或者直接看引脚能否被外部拉低。我第一次手动移植工程时就栽在这里后来凡是涉及引脚复用我都优先用CubeMX重新生成而不是手改寄存器。5. 关于这个驱动文件我最后的几点心得这个OLED驱动.zip里的内容核心就是上面这些代码和排查思路。整理文件的时候我把oled.c、oled.h、字库文件、软件IIC备用驱动一起打包方便不同的项目直接复用。在实际使用中还有两个小技巧值得分享。一是如果不确定屏幕驱动芯片是SSD1306还是SH1106先看驱动IC的丝印再选初始化命令序列。两者初始化命令基本一致但SH1106在列地址处理上略有区别如果你的屏幕是SH1106且用SSD1306的驱动显示内容会整体偏移几列。二是不要在中断服务函数里直接调用HAL_I2C_Master_TransmitIIC本身带状态机中断里调用容易导致HAL_BUSY正确做法是在主循环里处理。另外建议把HAL_I2C_ErrorCallback写一下哪怕只是放个断点或者记录错误码。HAL库的IIC错误处理比较隐蔽有时通信失败不会立刻反映出来有了错误回调排查问题能省不少时间。这个驱动文件我已经在STM32F103C8T6和STM32G431上各跑过一轮基本是开箱即用。如果你在移植过程中遇到屏幕不亮、显示乱码或者IIC卡死按上面的排查链路走一遍大概率能找到问题。本文还有配套的精品资源点击获取