ARTICLE DETAIL

资讯详情

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

STM32硬件IIC驱动0.96寸OLED屏完整教程与避坑指南

STM32硬件IIC驱动0.96寸OLED屏完整教程与避坑指南 简介这套基于STM32F103C8T6与0.96英寸OLED显示屏的硬件IIC例程包是典型的最小系统显示扩展方案适合智能小车、温湿度计、便携仪表等STM32应用场景。例程以一个可直接编译烧录的Keil工程演示如何借助硬件IIC接口驱动SSD1306或SH1106控制芯片覆盖显示初始化、坐标设置、字符串输出、清屏及像素绘制等关键操作。压缩包共包含177个文件整体大小约4.53MB内含标准外设库的C源文件、H头文件、目标文件、依赖文件、HEX烧录文件、分散加载文件以及工程清理脚本等目录分类清晰便于查看驱动代码与二次开发。目前已有1596人浏览学习该例程对STM32 OLED显示应用开发具有不错的参考价值。例程对硬件IIC的GPIO复用开漏模式、I2C时钟频率设定、起始条件与停止条件构建、地址读写字序等底层细节均有完整实现并将常用显示功能封装成可复用接口读者可对照源码理解底层时序快速移植到自身项目同时可借助map、lst、axf等编译产物分析代码与数据布局提升调试效率源码结构清晰便于在此基础上自行扩展功能也适合对照芯片手册理解IIC外设的寄存器操作。 最近在折腾STM32F103C8T6驱动0.96寸OLED屏的时候发现网上大量例程都是模拟IIC的写法GPIO翻转延时那种能用但是效率不高代码还占地方。后来我整理了一套基于硬件IICI2C外设的完整例程配合CubeMX加HAL库烧进去就能跑显示中文、数字、ASCII字符都没问题。这篇就把这套东西拆开讲清楚从接线、配置到代码逐行解读再把我调试过程中踩过的几个坑一并交代了给后面做同样事情的兄弟省点时间。这套例程解决的是很具体的一个问题STM32F103C8T6通过硬件I2C外设驱动0.96寸SSD1306 OLED屏底层通信不走模拟时序而是用芯片自带的I2C控制器配合中断或轮询方式收发数据。适合刚入门STM32的小白也适合想把手头模拟IIC代码换成硬件IIC的老手参考。0.96寸OLED分辨率128x64驱动IC大都是SSD1306IIC接口就四个脚VCC、GND、SCL、SDA。这屏幕在工作时其实就是一个挂在I2C总线上的从设备地址通常是0x78七位地址0x3C左移一位。硬件IIC的意义在于通信时序完全由外设硬件产生不需要CPU一条一条去拉电平代码简洁时序也稳定不容易出莫名其妙的花屏问题。1. 为什么不用模拟IIC效率和稳定性的真实差异先说结论模拟IIC不是不能用很多老工程师的代码跑了几年也没事。但如果你是用CubeMX加HAL库开发或者后续要在这个I2C总线上再挂温湿度传感器、RTC这类设备硬件IIC的优势一下子就出来了。1.1 模拟IIC的隐性成本模拟IIC本质就是两个GPIO口用软件去翻转电平按照I2C协议时序去凑起始信号、停止信号、数据位和ACK响应。代码本身不复杂但有几个绕不过去的问题。第一是CPU占用。每发一个字节CPU都要老老实实去翻转8个时钟周期中间还要插入延时去匹配SCL频率。屏幕刷新一帧数据要往SSD1306的显存里写1024个字节全屏刷新一次就是几千次的引脚操作CPU基本被拖死这时候你再跑个ADC采样或者串口打印就会明显感觉卡顿。第二是延时精度。模拟IIC的时序靠delay函数撑如果系统里有中断频繁打断SCL的波形就会变形高电平宽度和低电平宽度不一致严重的时候从设备会误判数据。这个故障排查起来相当隐蔽你看着代码逻辑一点问题没有就是偶尔花屏或者显示错乱。第三是代码复用差。你写了一套模拟IIC下次换个MCU或者换成硬件IIC代码基本要重写。这个人力成本才是大头。1.2 硬件IIC的资源占用和效率STM32F103C8T6有I2C1和I2C2两个I2C外设例程用的是I2C1。启用硬件外设之后SCL和SDA的时序由芯片内部的时钟控制器产生你只需要把数据写到数据寄存器里硬件会自动完成起始、停止、应答等时序操作。配合HAL库的HAL_I2C_Mem_Write函数往OLED写一个字节的命令或者数据就是一行代码的事而且HAL库的阻塞式接口内部有超时机制不会因为总线卡死让程序挂在那边。还有一点硬件IIC可以配置成400kHz快速模式而模拟IIC一般只能跑到100kHz左右。OLED这种小屏对刷新率要求不算高但如果你挂了多个I2C设备400kHz和100kHz的实际体感差距就很明显了。2. 接线和地址确定一上来最容易翻车的地方我非常理解大家拿到屏幕第一步想什么——赶紧接上电看能不能亮。但OLED屏有个和LCD不一样的地方上电默认是不亮的必须初始化之后才会显示内容。所以接线这一步不要急先确认几个细节。2.1 引脚分配和上拉电阻STM32F103C8T6的I2C1默认映射在PB6SCL和PB7SDA上例程也是用的这两个脚。注意F103的I2C引脚还有一个重映射选项可以映射到PB8和PB9但PB8/PB9同时又是另一个板上资源的位置建议新手就用默认的PB6/PB7不要去碰重映射。上拉电阻这个事要单独说。I2C总线的SCL和SDA是开漏输出必须要有上拉电阻才能输出高电平。STM32F103C8T6最小系统板上PB6和PB7不一定预留了上拉电阻这取决于板子的设计。比较稳的做法是在SCL和SDA上各接一个4.7k欧姆的电阻到3.3V。如果手头没有4.7k10k也能正常工作只是信号边沿会稍微慢一点。这个电阻一定要接不接的话你的I2C通信大概率是一会儿通一会儿不通波形完全乱掉。OLED供电是3.3V别接到5V上去SSD1306的IIC接口逻辑电平虽然兼容5V但稳妥起见都用3.3V避免长期运行发热。2.2 从机地址的坑0x78还是0x7ASSD1306的七位I2C地址默认是0x3C左移一位变成8位写地址就是0x78。但部分模组的SA0引脚接了高电平地址会变成0x7A。买屏幕的时候可以看一下背面丝印大部分国产模组默认是0x78。调试的时候如果发现写完地址之后没有ACK响应优先查两件事一是SA0引脚的电平二是你代码里写的地址到底是多少。HAL库的I2C地址参数需要传入的是8位地址含读写位所以驱动里传的是0x78。如果用标准库的老代码有的是传七位地址0x3C有的直接在函数内部左移一位这个要看仔细不要直接抄。3. CubeMX配置从新建工程到生成代码我用的是STM32CubeMX加Keil MDK的组合HAL库版本F1系列最新版。配置过程不复杂但有几个选项很容易被忽略会导致后面代码写了却不工作。3.1 时钟树和调试接口新建工程选芯片STM32F103C8T6第一步先把SYS里的Debug选成Serial Wire不然烧录一次之后第二次就下载不进去了——这是F103最经典的坑新手十有八九要踩一次。时钟配置直接用默认的HSI 8MHz也行但建议开启HSE外部晶振把主频拉到72MHz。操作方法是在Clock Configuration里HCLK那栏直接填72软件会自动帮你算好PLL倍频系数。I2C1的外设时钟挂在APB1上APB1最大36MHz这个关系后续算I2C时钟有用。3.2 I2C1参数和GPIO设置在Pinout视图里点击PB6选择I2C1_SCL点击PB7选I2C1_SDA。然后在Categories列表里选中I2C1把Mode改成I2CParameter Settings里的配置如下时钟速度Clock Speed400000快速模式时钟无源滤波Clock No Stretch ModeDisable这个要默认不要动硬件IIC的时钟速度在400kHz模式下APB1总线时钟36MHz则I2C时钟寄存器中的CCR值会自动计算。如果选100kHz标准模式CCR会变成36MHz除以2倍100kHz等于180也就是0xB4。用CubeMX的好处是这些都不用自己算但要理解这个关系排查时钟异常的时候会用到。GPIO配置那边PB6和PB7的模式会被自动设为开漏输出加上拉I2C模式。注意这里说的是GPIO内部的上拉但内部上拉阻值在30k到50k欧姆左右做I2C上拉电阻勉强能用但信号质量一般。建议还是外部加4.7k电阻设置里把GPIO的上拉关掉也行实测差别不大。3.3 生成代码前的工程设置Project Manager里把Toolchain选MDK-ARM V5Minimize Code Size可以勾上代码生成那页把“Generate peripheral initialization as a pair of .c/.h files per peripheral”选中这样每个外设一个单独文件代码结构清晰一点。另外记得把“Generate code for initializations of all peripherals”保持默认勾选HAL库的初始化函数都会在main.c里被调用。4. 核心驱动代码从SSD1306初始化到点亮屏幕代码生成之后在main.c里添加OLED驱动相关的函数。我建议把OLED的驱动单独拆成一个oled.c和oled.h不要全堆在main.c里后续扩展显示内容会方便很多。4.1 SSD1306初始化序列讲解SSD1306上电之后需要发送一串初始化指令才能进入正常显示状态。这段初始化代码网上有很多版本大同小异核心是以下几个命令// 初始化序列 OLED_WR_CMD(0xAE); // 关闭显示 OLED_WR_CMD(0xD5); // 设置时钟分频因子 OLED_WR_CMD(0x80); // 建议值0x80 OLED_WR_CMD(0xA8); // 设置驱动路数 OLED_WR_CMD(0x3F); // 1/64占空比 OLED_WR_CMD(0xD3); // 设置显示偏移 OLED_WR_CMD(0x00); // 偏移为0 OLED_WR_CMD(0x40); // 设置起始行 OLED_WR_CMD(0x8D); // 设置电荷泵 OLED_WR_CMD(0x14); // 开启电荷泵必须 OLED_WR_CMD(0x20); // 设置内存地址模式 OLED_WR_CMD(0x02); // 页地址模式 OLED_WR_CMD(0xA1); // 段重映射左右翻转 OLED_WR_CMD(0xC8); // 扫描方向上下翻转 OLED_WR_CMD(0xDA); // 设置COM引脚配置 OLED_WR_CMD(0x12); OLED_WR_CMD(0x81); // 设置对比度 OLED_WR_CMD(0xCF); OLED_WR_CMD(0xD9); // 设置预充电周期 OLED_WR_CMD(0xF1); OLED_WR_CMD(0xDB); // 设置VCOMH取消选择电平 OLED_WR_CMD(0x40); OLED_WR_CMD(0xA4); // 全局显示开启忽略RAM内容 OLED_WR_CMD(0xA6); // 正常显示非反色 OLED_WR_CMD(0xAF); // 开启显示其中最容易漏的是0x8D和0x14这两个指令也就是开启电荷泵。SSD1306内部有一个DC-DC电荷泵负责产生驱动OLED面板所需的高电压。如果这组命令没发屏幕就是死活不亮不管你后面写多少数据进显存都白搭。我调试的时候还遇到过一个奇怪现象发数据进显存之后屏幕偶尔闪一下但马上又灭了其实就是电荷泵状态不稳定。还有A1和C8这两个命令一个控制左右扫描方向一个控制上下扫描方向。你写一个“A”字在屏幕上如果发现是镜像的就是这两个命令的设置和屏幕硬件方向不一致。国产屏幕模组之间的差异比较大有的模块A1和C8配起来正好有的需要改成A0和C0才正常这个没有统一标准只能自己试。4.2 硬件IIC写命令和写数据的实现SSD1306通过I2C通信时第一个字节是控制字节用来告诉屏幕接下来是命令还是数据。控制字节为0x00表示后续字节是命令0x40表示后续字节是数据。void OLED_WR_CMD(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Mem_Write(hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); }这里我用的是HAL_I2C_Mem_Write把设备内部的寄存器地址当作控制字节的来源。这个函数会把MemAddress也就是0x00作为命令字节的第一个字节发出去再用pData发送cmd。第二个参数0x78是设备地址第三个参数0x00表示后续是命令数据长度2个字节。写数据函数区别仅仅是把缓冲区的第一个字节改成0x40。说到HAL_I2C_Mem_Write这个函数的用法很多人会混淆MemAddress到底填什么。SSD1306本身没有寄存器地址的概念这里的MemAddress实际上是I2C通信中控制字节的作用。填0x00走控制字节为命令的路径填0x40则是数据路径。理解了这个你在移植到其他OLED驱动时就不会糊涂了。4.3 全屏清屏和坐标显示的原理清楚了写命令和写数据的接口剩下的就是如何在指定位置显示内容。SSD1306内部显存有1KB分成8页每页128个字节对应屏幕8行的像素点。页地址模式下你写数据时地址会自动在当前页内自增写满128个字节之后会停住不会自动跳页。所以清屏或刷图的常规做法是void OLED_Clear(void) { uint8_t i, n; for (i 0; i 8; i) { OLED_WR_CMD(0xB0 i); // 设置页地址第i页 OLED_WR_CMD(0x00); // 设置列地址低四位 OLED_WR_CMD(0x10); // 设置列地址高四位 for (n 0; n 128; n) { OLED_WR_Data(0x00); // 清空该页 } } }这里每一次都要重新设置页地址和列地址是因为页地址模式下写完一页128个字节之后列地址会回到0但页地址不会自动加一必须手动发送B0i来切换。如果不设置就直接写所有数据都会覆盖在第一页上显示出来就是一堆错乱的横线。显示一个字符的原理也简单取模软件把字符的像素点阵按字节排列好每个字符是8x16大小也就是16个字节前8个字节是字符上半部分后8个字节是下半部分。显示时把光标设置到目标位置然后把点阵数据依次写入字符就出来了。汉字就是更大尺寸的点阵取模方式同理。4.4 显示函数封装与主循环调用一个最基础的OLED显示函数至少要包含显示字符串、清屏、显示单个字符。我习惯封装成下面这种形式void OLED_ShowString(uint8_t x, uint8_t y, char *str) { while (*str ! \0) { OLED_ShowChar(x, y, *str); x 8; if (x 120) { x 0; y 2; } } }y的含义是页编号0到7不是像素行号。如果你把y当像素行用填了一个大于7的数那命令0xB0y就变成非法的页地址显示会直接异常。这个细节很多新手搞不清楚所以单独提一下。主循环里的调用就非常简单了int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); OLED_Init(); OLED_Clear(); OLED_ShowString(0, 0, Hello STM32!); while (1) { // 你的业务代码 } }5. 实测中常见的硬件IIC问题和排查链路硬件IIC比模拟IIC稳定但也不是完全省心。这里我把实际调试中遇到的问题和排查思路完整贴出来方便大家对照。5.1 I2C总线锁死SDA被拉低无法释放现象程序运行一段时间后OLED突然不刷新了用示波器看SDA一直为低电平SCL正常。原因在通信过程中发生了错误的起始或停止条件导致SSD1306误判了SDA的状态一直等待下一个起始信号这时候总线就挂在那边了。模拟IIC遇到这种情况可以手动翻转GPIO去释放总线但硬件IIC模式下外设不会自动恢复。排查过程我分了三步。第一步先确认是软件问题还是硬件问题最简单的方法是把STM32断电再上电如果OLED恢复显示说明是代码或总线状态问题不是屏坏了。第二步在每次通信之前加一个I2C总线恢复的代码强制发送9个时钟脉冲让从设备复位状态机。第三步是排查中断优先级如果此时系统里有其他高优先级中断占用CPU时间过长I2C的时序会被拉长就容易触发从设备超时。我最终用的解决办法是在OLED_Init之前调用一个软件复位函数拉高SCL然后手动翻转SDA模拟9个时钟脉冲之后再重新初始化。这个方法对SSD1306非常有效具体实现就不贴代码了网上有很多成熟的写法。5.2 屏幕花屏或者显示残影现象刚上电时屏幕有杂乱的线条或噪点清屏之后仍然有残留。原因SSD1306上电时显存内容是随机的如果不做初始化直接显示就会有噪点。另外一个原因是供电不稳OLED的电荷泵在工作时会拉高瞬时电流如果3.3V的来源能力不足屏幕会出现横条纹闪烁。排查思路是先检查供电。我用万用表量过OLED全屏点亮时电流大约在20mA到30mA看起来不大但如果你的3.3V是直接从USB转串口的板子上取的那个板子的LDO可能已经超载了。解决方法是单独用AMS1117-3.3从5V转一路电给屏幕或者用主板上的3.3V电流余量大的那种。其次是确认初始化序列有没有完整执行。这里有个很容易被忽略的点OLED_WR_CMD函数里的延时。SSD1306内部执行指令需要时间尤其是开启电荷泵0x8D, 0x14之后最好延时100ms再继续后面的操作否则状态下一次就丢了。很多例程里初始化之后紧跟一个HAL_Delay(100)不是随手写的是实际需要。5.3 硬件IIC地址写对了但没ACK这个现象我在调试时遇到过一度以为是屏坏了。排查过程是这样的先用万用表量SCL和SDA的电平发现都被拉到3.3V说明上拉电阻工作正常。然后检查是不是把SDA和SCL接反了排线对调之后还是不行。后来用逻辑分析仪抓I2C波形发现起始信号之后设备地址字节发出去了但第9个时钟周期没有ACK低电平。最后查出来是SA0引脚的问题。模块上SA0默认是悬空状态内部下拉地址是0x78。但我这块模组SA0被设计成通过一个0欧电阻接到VCC实际地址变成了0x7A。把代码里的地址改成0x7A马上就好了。所以遇到没ACK先别急着怀疑代码用逻辑分析仪或者示波器看波形再检查板子上SA0的电平基本都能定位到问题。6. 例程包内的其他参考资源与后续扩展建议这套例程除了核心的OLED驱动我还打包了几个配套材料方便大家对照参考。6.1 最小系统板原理图和引脚功能速查STM32F103C8T6最小系统板的原理图版本很多我打包里放了一份我常用的参考。重点看三块电源部分USB 5V输入到3.3V LDO、复位电路、BOOT0和BOOT1的跳线设置。这里有个实用经验如果发现程序跑起来不对第一个排查的就是BOOT0是不是被拉高到了1如果是芯片会进入ISP引导模式而不执行用户Flash里的程序表现为程序完全没反应。引脚功能速查表我自己整理了一份把PA0到PB15每个引脚的默认复用功能、5V容忍能力、可用定时器通道都列出来了。做项目选引脚之前先瞄一眼这个表能避免很多踩坑比如直接把PB4和PB3当普通IO用结果发现调试口不工作。6.2 从硬件IIC扩展到其他传感器学会了硬件IIC驱动OLED你基本就掌握了STM32F103C8T6上I2C外设的使用方法。同样的API改一下从机地址和寄存器配置就能驱动AT24C02存储芯片、AHT20温湿度传感器、MPU6050六轴传感器。这些设备的IIC地址和寄存器定义在数据手册里都有例程里我也放了一个简易的I2C扫描程序可以帮你自动探测总线上有哪些设备地址调试的时候特别有用。6.3 显示库的后续增强方向例程目前支持ASCII字符、常用汉字、数字、字符串显示够日常用了。如果你需要图形界面效果可以在此基础上扩展画点、画线、画矩形、画圆这些都是基于显存操作实现的。思路是先在内存里维护一个128x8字节的显存数组所有图形绘制都是对这个数组做操作最后用一次全屏刷新把显存推到SSD1306里。这种方式比逐点写入效率高很多也方便做动画效果。我做这个例程时的体会是硬件IIC的代码写起来其实比模拟IIC还要简单难的是理解I2C协议本身以及掌握HAL库API的封装逻辑。OLED屏幕本身不复杂但它是一个特别好的学习载体通过它把I2C通信走通后面学传感器、学存储芯片都会顺畅很多。如果你在移植例程的过程中遇到问题先按我这篇文章的排查思路走一遍八成能在供电、地址、初始化顺序这三个环节里找到答案。本文还有配套的精品资源点击获取
返回列表