ARTICLE DETAIL

资讯详情

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

STM32驱动0.96寸OLED(SSD1306/IIC)从接线到取模显示

STM32驱动0.96寸OLED(SSD1306/IIC)从接线到取模显示 简介面向STM32F103ZET6入门者与嵌入式爱好者的0.96寸OLED显示驱动源码包采用IIC接口的四针四脚OLED模块代码注解清晰、原理明确重点解决初学者在OLED初始化、显示驱动以及IIC时序理解上的常见难点。压缩包共185个文件大小4.74MB以C源码.c/.h、Keil工程文件.uvprojx/.uvoptx和编译中间文件为主同时包含hex/axf固件与map/lst调试映射文件工程结构完整可在MDK中直接打开编译下载省去环境配置时间。目前已有3959人学习使用。整个工程除OLED核心驱动外还涉及TIM定时器、IIC、RCC时钟、ADC等外设模块读者可对照源码梳理IIC读写时序、显存操作及字符图形绘制方法代码风格简洁、注释充分便于修改和移植到其他STM32项目中对学习STM32标准外设库和OLED显示原理均有实用参考价值。无论用于课程设计、毕业设计还是日常练习都能提供直观的代码参考与实现思路。1. 项目概述为什么要拿ZET6去点一块小屏幕ST官方的F103系列里ZET6属于高配中的高配Cortex-M3内核跑到72MHz512KB Flash、64KB RAM外设资源更是给你拉满。拿它驱动一块0.96寸、分辨率只有128x64的OLED屏幕说白了就是“大炮打蚊子”。但实际项目里绝大多数人都是先把屏幕点亮、验证完显示逻辑再往智能小车、温湿度计或者手持仪表里集成所以这块小屏反而成了嵌入式开发里出镜率最高的“入门必修课”。这块0.96寸OLED屏幕驱动芯片基本是SSD1306接口有IIC和SPI两种版本。这篇博文聚焦IIC版本原因很直接IIC只占两根IO线SCL时钟、SDA数据接线简单、代码量小、调试成本低特别适合快速验证显示功能。加上STM32F103ZET6板载的I2C1外设就挂在PB6/PB7上硬件上几乎不需要额外改动直接飞线就能跑起来。这篇文章适合谁看如果你正准备用ZET6点屏或者手里有块0.96寸OLED但是对着代码一头雾水那这篇就是给你准备的。我会把硬件接线、IIC通信时序、SSD1306初始化流程、字符和汉字显示的原理全部拆开讲一遍再附上能直接跑通的源码。看完不说能成为OLED专家至少让屏幕亮起来、按你的想法显示内容轻轻松松。2. 方案选型为什么IIC、为什么SSD13062.1 IIC和SPI怎么选0.96寸OLED最常见的两种接口是四针IIC和七针SPI。IIC版只有VCC、GND、SCL、SDA四根线SPI版则是VCC、GND、D0(SCLK)、D1(MOSI)、RES、DC、CS七根线。两者的核心差异用一张表就能说清楚对比项IIC四针SPI七针接线数量2根信号线5根信号线最高刷新率约400kHz快速模式可达10MHz以上引脚占用少适合引脚紧张的项目多适合需要频繁刷新画面的场景驱动代码量少时序简单稍多还要处理RES/DC/CS适用场景静态页面、状态显示、菜单动画、波形刷新、视频帧显示我个人的建议是如果只是显示固定数据、菜单、传感器数值这类低频更新内容IIC完全够用。但如果你要做示波器波形刷新或者动态图表那还是选SPI版本带宽差距是实打实的。2.2 SSD1306内部结构1KB显存是怎么映射的SSD1306内置了一块1KB的GDDRAM显存正好对应128x64像素的单色位图。这里的“单色”指的是每个像素只有亮和灭两种状态所以一个字节的8个bit正好对应一列上的8个像素点。这就是SSD1306显存组织的核心逻辑按页Page分行、按列Column分成128段。整个屏幕被划分成8个Page每个Page包含8行像素。写入数据时先通过命令设置目标页地址和列地址再连续写入数据字节。每个字节的bit0对应上面第一行bit7对应下面第八行从低到高依次排列。这个映射关系决定了取模方式字模数据必须按“列行式”扫描生成也就是从上到下、从左到右读取每个像素点的亮灭状态。如果你用别的取模方式比如逐行式显示出来就是乱码或者镜像翻转。这个坑几乎每个初次上手的朋友都会踩一次后面我会专门讲取模设置。2.3 为什么IIC要接上拉电阻IIC总线是开漏输出结构这意味着设备只能把总线拉低不能主动拉高。所以SCL和SDA必须通过外部上拉电阻接到VCC才能保证总线空闲时处于高电平。STM32内部虽然有上拉电阻但阻值偏大约40kΩ在400kHz快速模式下会让信号上升沿变得很缓容易出现通信错误。上拉电阻怎么选标准模式下4.7kΩ是经典值快速模式2.2kΩ更稳。如果接线很短10cm以内且总线上只有OLED一个从设备4.7kΩ完全够用。但如果飞线比较长或者板上挂了多个IIC设备建议换成2.2kΩ。测量方法也简单用示波器看SCL的上升沿如果上升时间超过1μs就说明上拉电阻太大了换小一点。注意OLED模块本身在板上已经焊了上拉电阻如果你买到的是带板子的成品模块通常不需要再额外外接。但如果是自己画的PCB或者用裸屏一定要记得加否则IIC总线一直处于不确定状态通信大概率失败。3. 硬件接线与工程配置3.1 最小接线方案STM32F103ZET6和0.96寸OLEDIIC版之间只需要四条线OLED引脚STM32引脚说明VCC3.3V供电千万别接5VGNDGND共地SCLPB6I2C1_SCL时钟线SDAPB7I2C1_SDA数据线这里有一个必须强调的细节OLED的VCC一定要接3.3V。虽然SSD1306芯片本身支持1.65V到3.3V的工作电压范围但很多模块板载了稳压电路接5V也能跑这不意味着你可以随便接。稳妥起见ZET6开发板的3.3V输出直接供电就行不要赌模块的耐压余量。3.2 标准库和HAL库怎么选现在的开发环境里F103系列主要有两种固件库标准外设库Standard Peripheral Library和HAL库Hardware Abstraction Layer。对于ZET6点OLED这个场景两个库都能胜任但选择会影响代码风格和移植难度。标准库的IIC代码更“透明”所有寄存器操作都封装成函数你想看底层做了什么随时能翻源码适合学习IIC协议细节。HAL库则更抽象初始化流程被封装成HAL_I2C_Init()你只需要配置结构体参数适合快速开发。不过这里我要说句实在话STM32F103的硬件IIC在标准库时代口碑一般很多老工程师宁愿用GPIO模拟IIC也不想碰硬件外设。虽然实际用下来多数是配置问题而非芯片问题但为了稳定性和移植便捷性我自己写OLED驱动更倾向于软件模拟IIC。后面给出的源码就是基于GPIO模拟的不依赖具体库函数标准库和HAL库都能直接抄过去用。3.3 从原理图确认引脚复用如果你手里的ZET6板子是市面上常见的“战舰”“精英”系列I2C1对应的引脚大概率是PB6和PB7。但保险起见还是查一下板子的原理图确认这两个引脚没有被其他外设占用。有些开发板会把PB6/PB7连接到板载EEPROM或者其他传感器上如果它们被复用了你需要确认地址冲突或者干脆换到其他IO口模拟IIC。查询方法翻开板子原理图搜索“PB6”“PB7”字样看它们连接到哪些芯片。如果只连到了排针或者空闲插座那就放心用。如果已经连了板载芯片且地址和你的OLED设备地址一样就要小心了。4. 代码结构解析从底层时序到上层显示4.1 IIC起始、停止、读写字节的GPIO模拟实现软件模拟IIC的核心就是三个函数起始信号、停止信号、字节读写。理解了这三个函数的时序你就看懂了IIC通信的一半。起始信号SCL保持高电平时SDA从高电平跳变到低电平。void IIC_Start(void) { SDA_OUT(); // SDA配置为输出模式 SDA_High(); SCL_High(); delay_us(5); SDA_Low(); // SCL高电平时SDA拉低产生起始条件 delay_us(5); SCL_Low(); // 拉低SCL准备传输数据 }停止信号SCL保持高电平时SDA从低电平跳变到高电平。void IIC_Stop(void) { SDA_OUT(); SCL_Low(); SDA_Low(); delay_us(5); SCL_High(); // SCL先拉高 SDA_High(); // 然后SDA拉高产生停止条件 delay_us(5); }读写一个字节的过程可以用三个步骤来理解发送一个字节SCL低电平时把数据bit放到SDA上然后拉高SCL此时从设备在SCL高电平期间采样SDA的电平状态。重复8次从高位bit7到低位bit0。接收应答发送完8个数据位后释放SDA切换为输入模式主设备发送第9个时钟脉冲从设备通过拉低SDA来表示应答ACK。读取一个字节过程和发送类似但是主设备在SCL高电平期间读取SDA状态逐bit拼成一个完整的字节。void IIC_WriteByte(u8 data) { u8 i; SDA_OUT(); for (i 0; i 8; i) { if (data 0x80) SDA_High(); else SDA_Low(); data 1; delay_us(2); SCL_High(); delay_us(5); SCL_Low(); delay_us(2); } }4.2 向SSD1306写命令和写数据的区别SSD1306的所有操作都通过两条指令通道控制字节加数据字节。控制字节告诉屏幕“接下来是命令还是数据”数据字节则是对应的命令码或显示数据。在IIC模式下每次传输的控制字节固定是0x00表示后续字节是命令0x40表示后续字节是显存数据。这两个值来自SSD1306数据手册的IIC接口说明写错了屏幕不会响应而且不会报错你只会面对一块白屏干瞪眼。void OLED_WriteCmd(u8 cmd) { IIC_Start(); IIC_WriteByte(0x78); // 设备地址7位地址0x3C左移一位写方向 IIC_WriteByte(0x00); // 控制字节后续是命令 IIC_WriteByte(cmd); // 命令码 IIC_Stop(); } void OLED_WriteData(u8 dat) { IIC_Start(); IIC_WriteByte(0x78); // 设备地址 IIC_WriteByte(0x40); // 控制字节后续是数据 IIC_WriteByte(dat); // 显示数据 IIC_Stop(); }设备地址这里再强调一次SSD1306的7位IIC地址是0x3C如果SA0引脚接高电平则是0x3D。但在IIC通信中主机发送的地址字节是7位地址左移一位加上读写位所以写操作地址就是0x3C 1 0x78。很多朋友在代码里看到0x78不理解为什么不是0x3C这个转换关系搞明白了再去查地址问题就不慌了。4.3 SSD1306初始化序列为什么是这一串命令屏幕驱动的初始化本质是告诉SSD1306一系列显示参数。网络上流传的初始化序列大同小异核心命令就那么几条。我挑关键的说明一下0xAE关闭显示。初始状态下屏幕是不亮的状态避免初始化过程中出现乱码。0xD5, 0x80设置时钟分频因子和振荡器频率0x80是默认值不用改。0xA8, 0x3F设置复用比0x3F表示64行正好对应128x64分辨率。0xD3, 0x00显示偏移量0表示不移位。0x40设置显示起始行从第0行开始。0x8D, 0x14开启电荷泵。这个命令非常关键SSD1306内部的升压电路必须打开OLED才能正常点亮。很多人屏幕不亮就是漏了这条。0x20, 0x00设置内存寻址模式为水平寻址模式。0xA1段重映射。这个位控制列地址方向设置0xA1能让显示内容从左到右正常排列。0xC8COM扫描方向反转。0xDA, 0x12COM引脚硬件配置0x12是128x64常用的值。0x81, 0x7F对比度设置0x7F是中间值觉得暗可以调到0xFF。0xD9, 0xF1预充电周期。0xDB, 0x40VCOMH电压级别。0xA4显示内容从GDDRAM中直接输出。0xA6正常显示方向不反色。0xAF开启显示。初始化完成屏幕亮起。这套序列在几乎所有SSD1306驱动代码里都能看到背下来不是目的知道每一条在干什么才有意义。后面调问题时才会知道白屏该查哪条命令显示错乱该查哪条命令。5. 显示实现从清屏到汉字取模5.1 清屏函数和坐标系统SSD1306的显存组织是按页的所以清屏本质上是往所有页的所有列写入0x00。这里有个简单的做法直接通过设置页地址和列地址然后批量写0数据。void OLED_Clear(void) { u8 i, j; for (i 0; i 8; i) { OLED_WriteCmd(0xB0 i); // 设置页地址0~7 OLED_WriteCmd(0x00); // 设置列地址低4位 OLED_WriteCmd(0x10); // 设置列地址高4位 for (j 0; j 128; j) { OLED_WriteData(0x00); // 连续写入128个空数据 } } }这个函数做的事情就是遍历8页、每页128列全部填充0x00。坐标映射到SSD1306的规则是列地址就是X坐标0~127行坐标除以8得到页地址。比如你要在Y16的位置显示内容对应的Page就是16/82需要先设置页地址为0xB2再设置列地址。所以一切显示函数的本质都是“计算坐标、设置页地址/列地址、写数据”。5.2 显示一个字符的完整流程显示字符比显示汉字简单因为英文字符和数字只有8x16和6x8两种常用规格。以8x16为例一个字符占用2页每页8行宽度是8个像素。void OLED_ShowChar(u8 x, u8 y, u8 chr) { u8 c; u8 i; c chr - ; // 计算在字模数组中的偏移 if (x 120) // 防止超出屏幕边界128-8120 x 120; // 设置页地址为y/8 OLED_WriteCmd(0xB0 (y / 8)); // 设置列地址为x OLED_WriteCmd(0x00 (x % 16)); OLED_WriteCmd(0x10 (x / 16)); // 写入上半部分低8行 for (i 0; i 8; i) { OLED_WriteData(F8X16[c][i]); } // 写入下半部分高8行 OLED_WriteCmd(0xB0 (y / 8) 1); OLED_WriteCmd(0x00 (x % 16)); OLED_WriteCmd(0x10 (x / 16)); for (i 0; i 8; i) { OLED_WriteData(F8X16[c][i 8]); } }注意这里列地址设置的细节列地址分为低4位和高4位两段写入0x00 (x % 16)是列地址的低4位0x10 (x / 16)是高4位。所以列地址值 低4位值 高4位值 4完整等于x。这个操作方式在SSD1306的列地址设置命令里是固定的——先写低4位命令0x00~0x0F再写高4位命令0x10~0x1F。5.3 汉字显示的取模方法和坐标扩展汉字显示相比字符最大的区别是字模数据的获取方式和显示宽度。一个16x16汉字占用16列、2页字模数据32字节。取模软件设置是关键。我推荐使用PCtoLCD2002取模参数必须按下面配置取模方式列行式对应SSD1306的页地址扫描方向取模走向逆向低位在前输出数制十六进制自定义格式C51格式设置错了会出现什么情况字模方向不对会导致汉字左右翻转或者上下颠倒颜色取反会导致笔画显示为空白、背景变成黑色。这种问题排查起来容易让人崩溃因为代码逻辑完全没问题就是取模参数不对。汉字显示函数的实现思路和字符类似区别在于void OLED_ShowChinese(u8 x, u8 y, u8 index) { u8 i; // 设置页地址 OLED_WriteCmd(0xB0 (y / 8)); // 设置列地址 OLED_WriteCmd(0x00 (x % 16)); OLED_WriteCmd(0x10 (x / 16)); // 写左半部分 for (i 0; i 16; i) { OLED_WriteData(Hzk[index][i]); } // 切到下一页写右半部分 OLED_WriteCmd(0xB0 (y / 8) 1); OLED_WriteCmd(0x00 (x % 16)); OLED_WriteCmd(0x10 (x / 16)); for (i 0; i 16; i) { OLED_WriteData(Hzk[index][i 16]); } }5.4 显示字符串和数字的封装有了字符和汉字函数字符串显示就是循环调用。数字显示则需要借助C库函数snprintf把整数转成字符串。void OLED_ShowString(u8 x, u8 y, char *str) { while (*str ! \0) { OLED_ShowChar(x, y, *str); x 8; // 一个字符占8像素宽 str; } } void OLED_ShowNum(u8 x, u8 y, u32 num, u8 len) { char buf[12]; snprintf(buf, sizeof(buf), %0*d, len, num); OLED_ShowString(x, y, buf); }这里%0*d的意思是按指定宽度显示数字不足位用0补齐。比如OLED_ShowNum(0, 0, 42, 4)会显示“0042”用来显示传感器数值或者计时器效果很好数字位数变化时画面不会闪动错位。6. 常见问题与排查技巧实录6.1 屏幕完全不亮先排除供电问题。用万用表量OLED模块的VCC和GND之间的电压确认在2.8V到3.3V之间。如果电压正常再确认初始化序列里的0x8D, 0x14电荷泵命令是否被执行了。这条命令漏了或者顺序不对屏幕就是一点反应都没有。如果供电和命令都正常检查IIC地址。用逻辑分析仪抓一下SDA上的地址字节看看是不是0x78。很多模块的SA0引脚悬空默认地址是0x3C写地址0x78但个别模块把SA0接高了地址变成了0x7A。把初始化代码里的地址改成0x7A试试有时候就是这一位的差别。6.2 屏幕亮了但显示乱码乱码问题大概率出在取模方式和数据写入顺序上。先检查取模软件的设置是不是“列行式”“逆向”不是的话重新取模。还有一种情况初始化时内存寻址模式设置错误。SSD1306支持三种寻址模式页寻址、水平寻址、垂直寻址。如果0x20, 0x00这里的模式值和你的写入逻辑不匹配显示就会错乱。我习惯用页寻址模式0x02配合作者地址设置逻辑清晰不容易出问题。6.3 屏幕能显示但闪烁闪烁一般是刷新速度问题。IIC模式的理论速度上限是400kHz实际写一屏1KB显存大约需要40ms左右。如果代码里在每次显示数据前都执行了完整的清屏操作写入8192个0x00一帧的时间会翻倍看起来就是闪烁。优化思路有两个一是只更新变化的区域不要每次都全屏刷新二是用局部窗口刷新函数只写入需要变化的坐标区域内数据。这个优化在显示传感器数值或者动态菜单时效果特别明显。6.4 引脚被占用怎么办如果PB6/PB7被板载其他外设占用了最简单的方案是换用其他GPIO软件模拟IIC。软件IIC的优点就是接口灵活SCL和SDA可以定义在任何IO口上。你只需要修改头文件里的引脚宏定义和延时函数代码逻辑完全不用动。#define OLED_SCL_GPIO_PORT GPIOB #define OLED_SCL_GPIO_PIN GPIO_PIN_6 #define OLED_SDA_GPIO_PORT GPIOB #define OLED_SDA_GPIO_PIN GPIO_PIN_7换引脚时注意选定的两个引脚最好都是普通的推挽输出引脚复位后默认是浮空输入GPIO_Mode_IN_FLOATING需要在初始化里手动配置为推挽输出GPIO_Mode_Out_PP。6.5 常见问题速查表现象可能原因检查方法白屏地址错误、电荷泵未开启、接线错误示波器抓IIC波形检查0x78地址和0x8D命令显示乱码取模方式错误、寻址模式不匹配重新按列行式取模检查0x20命令参数显示上下颠倒段重映射或COM扫描方向设置错误检查0xA1和0xC8命令改反试试显示左右反了列扫描方向反了把0xA1改成0xA0屏幕闪烁全屏刷新太慢改用局部刷新只更新变化区域显示很暗对比度设置太低把0x81参数调到0xFF某些区域不显示页地址或列地址计算错误检查坐标计算x超过127或y超过63会截断干扰花屏供电不足、线太长换粗短线加100nF去耦电容7. 几个实操细节和我的经验体会代码能跑通和跑得稳中间还隔着几个细节。第一个是延时函数的精度。软件模拟IIC的时序全靠delay_us()拉高拉低的节奏如果延时不准确SCL频率会大幅降低屏幕显示就会出现肉眼可见的迟钝。我用的是SysTick实现的微秒级延时在72MHz主频下精度足够。第二个是功耗问题。OLED的功耗虽然比LCD低很多但全亮状态下电流也有20mA到30mA。如果用电池供电可以考虑让OLED进入睡眠模式发送命令0xAE需要显示时再唤醒。这个在便携设备里非常实用。第三个是字库管理。项目里汉字多了之后字模数组会变得很大。一个16x16汉字占32字节100个汉字就是3.2KB对ZET6这种512KB Flash的大户来说完全不是问题。但如果你以后换了Flash小的芯片就要考虑把字库放到外部Flash或者SD卡里按需加载。最后说一个我踩过的坑焊接OLED排针时方向焊反导致SDA和SCL互换。当时检查了半天代码最后万用表一量才发现是物理接线问题。所以遇到屏幕不亮先检查接线再查代码顺序千万别反。这套驱动代码在ZET6上测试跑通后我把同样的代码改成PC13/PC14模拟IIC驱动同款OLED放在一个小型的温湿度记录仪里跑了几个月没有一次死机或者花屏。所以说GPIO模拟IIC在稳定性和可靠性上完全不输硬件IIC关键是时序要写对、延时要给足。本文还有配套的精品资源点击获取
返回列表