SSD1306 OLED驱动开发:STM32工程与I2C时序解析 📅 发布时间:2026/9/13 11:41:20 👁 浏览次数: 简介一份围绕STM32F103C8T6微控制器的OLED显示屏驱动程序资源面向嵌入式开发、物联网及智能硬件爱好者帮助解决OLED屏与STM32之间的接口驱动与显示控制问题。资源包共134个文件包含C源文件与H头文件、Keil工程配置、编译生成的axf/hex等固件文件以及备份与说明文档整体约1.79MB。已有6544人学习下载。驱动代码涵盖屏幕初始化、SPI/I2C或GPIO接口通信、字符与图像显示逻辑并附带TIM、Flash等外设配置示例工程文件可直接在MDK中编译调试便于开发者对照硬件时序排查问题。对于需要在智能仪表、便携设备上实现低功耗高对比度显示的应用场景这份资料能帮助快速上手OLED驱动开发减少底层适配时间也适合初学者通过源码学习STM32的显示编程思路。1. 一块SSD1306 OLED接上STM32F103C8T6就白屏了买过0.96寸OLED模块的人基本都经历过这个场景四根杜邦线往STM32F103C8T6最小系统板上一插例程烧进去屏幕没有任何反应甚至供电正常也只是一片均匀的背光。问题几乎都出在驱动上——SSD1306这颗控制芯片的初始化序列、I2C时序和数据通道切换任何一个环节错了屏就是一块白板。这个压缩包里的OLED驱动工程就是直接可用的Keil MDK项目针对F103C8T6标准外设库环境覆盖了I2C通信、SSD1306初始化、字符显示和工程文件管理。适合正在做电子钟、温湿度计、仪表显示或者想搞明白OLED底层时序的嵌入式开发者和物联网爱好者阅读。2. SSD1306驱动核心I2C时序与页寻址显存模型2.1.1 从硬件连接看I2C的优势F103C8T6在最小系统板上引出了全部GPIO驱动0.96寸OLED模块只需要两根信号线。这个工程用的是I2C模式具体引脚定义在工程头文件的宏里常见接法是PB8接SCL、PB9接SDA也可以用PA8/PA9或者任意两个普通IO口模拟时序。硬件I2C和GPIO模拟I2C的区别在于前者依赖芯片内部外设需要处理总线状态寄存器后者用工时换灵活度。实际项目里我倾向于GPIO模拟原因有两个一是OLED的I2C速率只需要100kHz到400kHzCPU完全忙得过来二是模拟I2C可以把SCL和SDA接到任意引脚不受硬件外设复用限制改板子时不用重新配置AFIO。2.1.2 SSD1306显存的分页机制SSD1306内置了128 byte的显存但它不是线性排列的而是按页组织。128x64分辨率的屏幕被分成8页每页高8个像素页0到页7对应从屏幕顶部到底部的8个水平条带。每一页里有128列每列对应一个字节这个字节的每一位代表这一列上某一行像素是否点亮位0对应页内最上面那行。理解了这套模型画点函数就好写了先算y坐标属于哪一页再算该页第几行最后对相应列做位运算。页地址通过命令0xB0到0xB7设置列地址分低4位和高4位两部分0x00到0x0F是列低地址0x10到0x17是列高地址。写入顺序是先发页地址、再发列低地址、最后发列高地址。这套寻址方式困扰过不少人尤其是从LCD转过来的朋友习惯了线性显存会觉得页地址莫名其妙。2.1.3 指令通道和数据通道的切换I2C总线上所有内容都走同一条物理链路SSD1306靠控制字节区分指令和数据。第一个字节是slave地址0x78是写地址、0x7A是读地址市面上绝大多数OLED模块的I2C地址是0x3C左移一位就是0x78。第二个字节是控制通道标识0x00表示后续字节是命令0x40表示是显存数据。这个设计非常克制只有两个值但搞反了就会出现屏幕乱码或者完全不亮。处理时序的底层函数里起始条件、停止条件、应答位这些细节都要逐位操作。用GPIO模拟时先拉高SDA再拉低SCL产生起始条件停止条件则相反。总线忙碌时两条线都要保持高电平这是I2C协议的硬性规定很多白屏问题就出在起始条件前总线状态没复位。3. 从零写OLED驱动初始化序列与显示函数实现3.1.1 底层I2C读写函数的实现// 起始条件SCL高电平期间SDA由高变低 void I2C_Start(void) { OLED_SDA_H(); OLED_SCL_H(); OLED_SDA_L(); OLED_SCL_L(); } // 停止条件SCL高电平期间SDA由低变高 void I2C_Stop(void) { OLED_SDA_L(); OLED_SCL_H(); OLED_SDA_H(); }起始条件的时序要求是SDA必须先于SCL跳变而且SCL为高时SDA保持稳定。代码里先拉高SDA、再拉高SCL、然后拉低SDA、最后拉低SCL顺序反了设备会不识别起始位。停止条件的高低电平切换过程恰好相反先把SDA拉低拉高SCL后再释放SDA。3.1.2 写字节与ACK处理void I2C_WriteByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) OLED_SDA_H(); else OLED_SDA_L(); dat 1; OLED_SCL_H(); OLED_SCL_L(); } } void OLED_WriteCmd(uint8_t cmd) { I2C_Start(); I2C_WriteByte(0x78); // 器件地址0x3C左移一位 I2C_WriteByte(0x00); // 后续字节为命令 I2C_WriteByte(cmd); I2C_Stop(); } void OLED_WriteData(uint8_t dat) { I2C_Start(); I2C_WriteByte(0x78); I2C_WriteByte(0x40); // 后续字节为显存数据 I2C_WriteByte(dat); I2C_Stop(); }写一个完整字节需要8个时钟周期每个周期内先设置SDA电平再产生SCL上升沿。0x78是器件写地址0x3C是7位地址两者关系是左移一位这是I2C协议固化的格式。ACK应答位这里没有单独判断因为SSD1306对指令的响应速度足够快在400kHz时钟下不会出现死等省略NACK检查能减少代码量和总线占用时间。0x00和0x40这两个控制字节是硬编码的如果后期需要打开I2C的连续读写模式才需要额外处理控制字节中的Co位和D/C位。3.1.3 SSD1306初始化序列逐条解释void OLED_Init(void) { delay_ms(100); // 上电后等电源稳定 OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); // 设置内存寻址模式 OLED_WriteCmd(0x02); // 页寻址模式 OLED_WriteCmd(0xB0); // 页地址0 OLED_WriteCmd(0xC8); // 扫描方向从下到上 OLED_WriteCmd(0x00); // 列地址低字节0 OLED_WriteCmd(0x10); // 列地址高字节0 OLED_WriteCmd(0x40); // 显示起始行第0行 OLED_WriteCmd(0x81); // 对比度设置 OLED_WriteCmd(0x7F); // 对比度值范围0x00~0xFF OLED_WriteCmd(0xA1); // 段重映射左右反置 OLED_WriteCmd(0xA6); // 正常显示A7为反色 OLED_WriteCmd(0xA8); // 多路复用比率 OLED_WriteCmd(0x3F); // 64行 OLED_WriteCmd(0xA4); // 关闭整屏点亮 OLED_WriteCmd(0xD3); // 显示偏移 OLED_WriteCmd(0x00); // 偏移0 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(0x40); OLED_WriteCmd(0x8D); // 电荷泵开关 OLED_WriteCmd(0x14); // 开启 OLED_WriteCmd(0xAF); // 开启显示 OLED_Clear(); }0xAE和0xAF这组开关是最容易忽略的。很多人写完初始化序列发现屏幕不亮检查半天硬件最后发现是漏了0xAF。0x8D加0x14是电荷泵使能没有这一步屏幕连背光都不会亮这是SSD1306特有的电源管理机制。0xC8和0xA1决定了屏幕的上下左右方向如果显示的是镜像或者倒置问题就出在这两条命令上。0x20加0x02把寻址模式锁定在页模式显存地址自动换行适合字符类UI绘制画图或者滚动显示时更适合改成水平寻址模式0x00。3.1.4 画点、清屏与整屏刷新void OLED_SetPos(uint8_t x, uint8_t y) { OLED_WriteCmd(0xB0 y / 8); // 页地址y范围0~63 OLED_WriteCmd(0x00 (x 0x0F)); // 列低地址 OLED_WriteCmd(0x10 (x 4)); // 列高地址 } void OLED_Clear(void) { for (uint8_t page 0; page 8; page) { OLED_WriteCmd(0xB0 page); OLED_WriteCmd(0x00); OLED_WriteCmd(0x10); for (uint8_t col 0; col 128; col) OLED_WriteData(0x00); } }OLED_SetPos里的关键运算是y / 8和x 0x0F前者把像素行坐标换算成页号后者取出列坐标的低四位作为列低地址。清屏函数循环遍历8页和128列每次写入0x00。整屏刷新一次需要写1024个字节在400kHz I2C下大约耗时几十毫秒肉眼能感觉到轻微闪烁。这个工程没有做显存缓冲直接写SSD1306的内部RAM好处是省了F103的RAM空间——要知道C8T6只有20KB RAM开一个128x64的缓冲要占8KB。代价是连续刷新时I2C总线占用率高这和后面要讲的局部刷新方案直接相关。3.1.5 显示字符串与ASCII字库const uint8_t F8x16[][16] { // 字符 A字体宽8像素、高16像素 {0x00,0x00,0x00,0xFC,0x66,0x66,0x66,0x7C, 0x60,0x60,0x62,0x3C,0x00,0x00,0x00,0x00}, // 其他字符... }; void OLED_ShowChar(uint8_t x, uint8_t y, char ch) { OLED_SetPos(x, y); for (uint8_t i 0; i 8; i) OLED_WriteData(F8x16[ch - ][i]); OLED_SetPos(x, y 8); for (uint8_t i 8; i 16; i) OLED_WriteData(F8x16[ch - ][i]); }16像素高的字符被拆成上下两半写入上半部分在y页下半部分在y8页。取模工具里常用的阴码、逐列式、C51格式就是生成这类数组的标准配置。字库数组用ch - 做索引ASCII码0x20之前都是控制字符不需要存储空格从0号索引开始这样能省一部分Flash空间。F103C8T6的64KB Flash装几个完整字库绰绰有余但如果是1.44寸的OLED带中文字库128KB Flash的芯片会更有余量。4. Keil MDK工程集成标准外设库配置与ST-Link烧录4.1.1 工程文件梳理哪些能删哪些不能删压缩包里那几个uvgui和.bak文件是不同使用者在不同电脑上打开工程时留下的窗口布局缓存对编译结果零影响可以放心删除。STM32-DEMO.axf是上一次编译生成的ELF格式镜像文件每次重新编译会自动覆盖删掉也不影响工程。真正不能动的是stm32f10x_tim.c和stm32f10x_flash.c这两个源文件它们属于标准外设库的定时器和Flash驱动模块。这个工程用的是标准外设库SPL而不是HAL库从文件命名和#include stm32f10x.h的风格就能判断。标准外设库的API更底层操作寄存器更直接适合想弄明白外设工作原理的场景。如果更习惯HAL库工程改写时主要关注点有两个GPIO初始化从GPIO_InitTypeDef变成HAL_GPIO_Init有延时需求的函数从delay_ms换成HAL_Delay。但I2C模拟时序部分几乎可以原封不动搬过去这部分的代码不依赖任何库函数只用了GPIO的高低电平操作。4.1.2 引脚配置与I2C模式选择GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);推挽输出模式下GPIO可以直接驱动OLED模块的SCL和SDA引脚不需要外接上拉电阻——模块板上已经自带了。这里用50MHz的翻转速率是保守选择模拟I2C的时序由代码里的延时函数控制GPIO本身的翻转速度远超400kHz不会成为瓶颈。如果改用硬件I2C外设还需要额外配置AFIO时钟和I2C_InitTypeDef结构体把I2C_ClockSpeed设为400000、I2C_DutyCycle设为I2C_DutyCycle_16_9工作量比模拟方式大不少。4.1.3 编译链接与ST-Link烧录工程在Keil MDK里的配置要点如下配置项推荐值注意事项DeviceSTM32F103C8不要选成C8T6的封装变体C/C DefineUSE_STDPERIPH_DRIVER标准外设库必须启用DebuggerST-Link对应CMSIS-DAP选别的选项Flash DownloadReset and Run 勾选烧录后直接运行优化级别-O0调试阶段发布前可以改-O2烧录时ST-Link和板子之间用SWD四线连接SWDIO、SWCLK、GND、3.3V。F103C8T6支持SWD和JTAG两种方式SWD只需要两根信号线接错的概率低很多。如果烧录时报No target connected先检查BOOT0跳线帽是否接地再确认板上有没有独立供电导致电位冲突。4.1.4 白屏、花屏问题的排查顺序白屏是最常见的故障现象排查顺序有讲究。第一步确认OLED模块的I2C地址是0x3C还是0x3D部分模块的地址选择电阻位不同第二步检查初始化序列里0xAF和0x8D0x14是否执行到位第三步用万用表量SCL和SDA的对地电压正常空闲状态应该都是高电平第四步接逻辑分析仪抓起始条件看slave地址有没有正确发出。花屏或者乱码则优先怀疑寻址模式混乱把0x20后面跟的0x02改成0x00试试因为很多网上流传的例程用的是水平寻址模式初始化序列混用就会出问题。提示判断OLED是否正常工作可以先把整个显存全部写入0xFF屏幕全亮说明硬件链路无问题再逐段排查驱动代码。5. 进阶16x16汉字显示、局部刷新与DMA加速5.1.1 汉字取模与数据组织汉字显示是OLED应用的常见需求用16x16点阵存一个汉字需要32个字节。取模软件里设置为阴码、列行式、逆向即可生成的数据按每8个字节一行、共4行的顺序排列。显示时把汉字当作两个8x16字符的拼接来处理先显示左半边再显示右半边就能拼出完整的16x16字符。中文字库数组按GB2312编码顺序排列查询时用待显示汉字的机内码减去0xA0再计算索引。F103C8T6的64KB Flash存进几百个常用汉字没压力但如果要装全字库建议改用外部Flash或SD卡存储。5.1.2 局部刷新策略整屏刷新一次要传1024字节的数据在400kHz I2C下大约需要20毫秒动态更新时钟或者温度值时会出现明显的闪烁感。把寻址模式改为水平寻址后可以有效缩小刷新范围。比如只更新时间数字所在的8x16字符区域先定位到字符左上角的页地址和列地址再连续写入16字节即可。这样的局部刷新能把单次传输量从1024字节降到一个字符的16字节耗时缩短一个数量级肉眼基本感知不到闪烁。5.1.3 用空闲DMA通道推显存如果项目里同时用到了定时器中断或者串口CPU忙不过来可以考虑用DMA搬运显存。F103C8T6内置2个DMA控制器每个控制器有7个通道驱动OLED刷屏时通常占用DMA1通道7它是I2C1的发送请求映射。配置DMA前先用SPL库函数把I2C外设初始化好再设置DMA的外设地址为I2C1-DR、内存地址为显存数组首地址、传输数据量为1024。关键一步是把DMA传输完成中断打开在中断处理函数里关闭I2C的DMA请求否则下一次刷屏会冲突。DMA方式下CPU只需要发起一次传输请求剩下的字节搬移全交给DMA控制器运行频率72MHz下刷完整屏的时间能压进10毫秒以内。整屏刷新代码的核心在于先关DMA请求再改DMA内存地址指向新的显存缓冲然后重新使能通道。DMA的增量模式设置为固定外设地址存到寄存器里内存地址按字节递增。中断标志位在高位为1时说明一次传输完成此时在中断里做清标志和失能操作。本文还有配套的精品资源点击获取