尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32F1 OLED驱动移植实战:IIC通信与显存刷新避坑指南
简介这份资源面向使用STM32F1系列微控制器进行嵌入式开发的工程师与学习者提供OLED显示屏的IIC通信驱动代码帮助快速在项目中集成显示功能。资源包共4个文件包含3个.h头文件与1个.c源文件压缩包约6KB其中头文件负责函数原型、字库与位图数据声明源文件实现OLED初始化、清屏、命令与数据发送等核心逻辑。内容围绕HAL库展开涉及GPIO引脚配置、IIC外设初始化、OLED显示模式设置以及汉字字库映射与像素数据发送读者可据此理解IIC协议在STM32F1上的具体落地方式。目前已有1095人学习下载适合需要参考驱动框架、对照硬件配置修改引脚定义或补充中文显示能力的开发者也可作为嵌入式显示模块的排错与移植参考。1. 从一份 OLED 驱动文件说起为什么 .c 和 .h 要拆开看手头拿到一份 OLED 的.c和.h文件很多人第一反应是直接拖进工程编译能亮就行。但真正在 STM32F1 上跑过 IIC 通信的人都知道OLED 点不亮的原因往往不在屏幕本身而在驱动文件里那几个宏定义和延时函数。这份资源就是一套面向 STM32F1 的 OLED 驱动源码核心走的是 IIC 通信协议包含初始化、写命令、写数据、字符显示、数字显示等基础功能。它适合正在用 STM32F1 系列芯片做小项目、需要一块 0.96 寸或 1.3 寸 OLED 做状态显示的开发者。如果你之前只会在 Arduino 上调用现成库或者拿到驱动文件后不知道从哪改起这份东西能帮你把 IIC 时序、显存映射和字库调用这三件事串起来。下面我按实际拆文件的顺序把这份资源从结构到落地讲透。2. 拆开驱动包.h 里的宏定义和 .c 里的时序逻辑2.1 先看 .h引脚、地址、尺寸都藏在这里拿到一份 OLED 驱动我习惯先翻.h文件因为所有跟硬件相关的配置基本都集中在这一层。这份资源的.h里通常包含几类内容IIC 引脚定义、从机地址、屏幕分辨率、字库数组声明、以及对外暴露的函数原型。引脚定义决定了你接的是哪组 GPIO从机地址决定了 IIC 总线上能不能找到设备分辨率决定了显存数组开多大。以 STM32F1 常见的软件模拟 IIC 为例.h里一般会看到这样的宏// OLED.h 硬件配置区 #ifndef __OLED_H #define __OLED_H #include sys.h // 位带操作或延时函数依赖 #include stdlib.h // IIC 引脚定义按实际接线改 #define OLED_SCL_PIN PBout(6) #define OLED_SDA_PIN PBout(7) #define OLED_SCL_GPIO GPIOB #define OLED_SDA_GPIO GPIOB // 从机地址0x78 是 7 位地址左移一位后的写地址 #define OLED_ADDRESS 0x78 // 屏幕分辨率0.96 寸通常是 128x64 #define OLED_WIDTH 128 #define OLED_HEIGHT 64 // 显存大小128*64/8 1024 字节 #define OLED_GRAM_SIZE 1024 void OLED_Init(void); void OLED_Clear(void); void OLED_ShowChar(u8 x, u8 y, u8 chr, u8 size); void OLED_ShowString(u8 x, u8 y, u8 *p, u8 size); void OLED_ShowNum(u8 x, u8 y, u32 num, u8 len, u8 size); #endif这段代码里最容易被忽略的是OLED_ADDRESS。市面上常见的 OLED 模块IIC 地址有0x78和0x7A两种区别在于模块背面的电阻跳线。如果你写0x78点不亮先别怀疑时序拿逻辑分析仪扫一下总线看看实际应答的地址是哪个。另一个坑是OLED_SCL_PIN这类位带操作宏它依赖sys.h里的位带映射如果你用的不是正点原子那套sys.h直接编译会报错得换成HAL_GPIO_WritePin或者自己写宏。.h里还会声明字库数组比如F8X16和F6X8。这两个数组决定了你能显示多大的字符。F8X16是 8 列 16 行一个字符占 16 字节F6X8是 6 列 8 行一个字符占 6 字节。如果你只需要显示数字和英文用F6X8能省一半显存如果要显示中文得额外加取模数组而且取模方式要跟驱动里的扫描顺序对上否则显示出来是乱码或者镜像。2.2 再看 .cIIC 起始、应答、写字节的完整链路.c文件是真正干活的地方。这份资源的.c通常按这样的顺序组织IIC 底层时序函数、OLED 写命令/写数据函数、显存刷新函数、上层显示函数。软件模拟 IIC 的核心就是四个时序起始条件、停止条件、发送一个字节、等待应答。// OLED.c IIC 底层时序 void IIC_Start(void) { OLED_SDA_PIN 1; OLED_SCL_PIN 1; delay_us(2); OLED_SDA_PIN 0; // SCL 高时 SDA 拉低起始条件 delay_us(2); OLED_SCL_PIN 0; } void IIC_Send_Byte(u8 txd) { u8 t; OLED_SDA_PIN 0; // 先拉低 SDA准备发送 OLED_SCL_PIN 0; for(t 0; t 8; t) { // 从最高位开始发 if(txd 0x80) OLED_SDA_PIN 1; else OLED_SDA_PIN 0; txd 1; delay_us(2); OLED_SCL_PIN 1; // 拉高时钟从机采样 delay_us(2); OLED_SCL_PIN 0; // 拉低时钟准备下一位 delay_us(2); } } u8 IIC_Wait_Ack(void) { u8 ucErrTime 0; OLED_SDA_PIN 1; // 释放 SDA由从机拉低 delay_us(1); OLED_SCL_PIN 1; delay_us(1); while(OLED_SDA_PIN) // 等待从机拉低 { ucErrTime; if(ucErrTime 250) { IIC_Stop(); return 1; // 超时无应答 } } OLED_SCL_PIN 0; return 0; }这段代码里delay_us(2)是玄学重灾区。STM32F1 在 72MHz 主频下delay_us的实现方式不同实际延时差别很大。如果你用的是 SysTick 延时2 微秒可能准如果你用的是空循环可能变成 0.5 微秒或者 5 微秒。IIC 标准模式 100kHz 下一个时钟周期 10 微秒高电平至少 4 微秒所以delay_us(2)是偏快的。实际跑的时候如果发现 OLED 偶尔花屏或者不响应先把延时加到delay_us(4)试试。另一个细节是IIC_Wait_Ack里的超时计数250 次循环在 72MHz 下大概几毫秒如果从机没接好这里会卡住所以超时后必须调IIC_Stop释放总线否则下次通信直接死锁。写命令和写数据的区别只在一个控制字节void OLED_WR_Byte(u8 dat, u8 mode) { IIC_Start(); IIC_Send_Byte(OLED_ADDRESS); // 写地址 IIC_Wait_Ack(); if(mode) IIC_Send_Byte(0x40); // 0x40 表示后面跟的是数据 else IIC_Send_Byte(0x00); // 0x00 表示后面跟的是命令 IIC_Wait_Ack(); IIC_Send_Byte(dat); IIC_Wait_Ack(); IIC_Stop(); }0x00和0x40是 SSD1306 控制字节的固定值写错的话屏幕要么不亮要么显示乱码。有些驱动文件里会写成0x80和0xC0那是另一种控制字节格式取决于初始化时发的命令。这份资源用的是0x00/0x40你在改的时候别混用。2.3 显存刷新为什么改了 GRAM 屏幕没反应OLED 的显示逻辑跟 LCD 不一样它没有独立的显存映射到屏幕上而是靠驱动芯片内部的 GDDRAM。STM32 这边要维护一个OLED_GRAM[128][8]数组所有画点、写字符的操作都是先改这个数组最后统一调OLED_Refresh_Gram把整个数组推到屏幕。// 显存数组128 列 8 页每页 8 行 u8 OLED_GRAM[128][8]; void OLED_Refresh_Gram(void) { u8 i, n; for(i 0; i 8; i) { OLED_WR_Byte(0xB0 i, OLED_CMD); // 设置页地址 OLED_WR_Byte(0x00, OLED_CMD); // 设置列低地址 OLED_WR_Byte(0x10, OLED_CMD); // 设置列高地址 for(n 0; n 128; n) OLED_WR_Byte(OLED_GRAM[n][i], OLED_CMD); } }这里有个血泪经验如果你在OLED_DrawPoint里改了OLED_GRAM但没调OLED_Refresh_Gram屏幕是不会变的。很多新手以为OLED_DrawPoint直接写屏幕其实它只改内存。另外OLED_Refresh_Gram里用的是OLED_CMD而不是OLED_DATA因为设置页地址和列地址属于命令只有最后写OLED_GRAM数据时才是数据。这个参数传错屏幕会花成一片。3. 移植到 STM32F1从引脚改接到第一个字符显示3.1 硬件接线与 GPIO 模式选择拿到驱动文件后第一步不是编译是确认接线。STM32F1 的 GPIO 要配成开漏输出还是推挽输出取决于你的 IIC 总线上有没有外部上拉电阻。OLED 模块背面通常自带 4.7k 上拉这时候 GPIO 配成开漏输出就行如果你用的是裸屏或者上拉电阻没焊得配成推挽输出否则 SDA 和 SCL 拉不高通信直接失败。常见接法是 SCL 接 PB6、SDA 接 PB7对应 STM32F1 的硬件 IIC1。但这份资源用的是软件模拟 IIC所以引脚可以随便改只要在.h里把OLED_SCL_PIN和OLED_SDA_PIN改掉就行。改完之后在OLED_Init里要先把这两个引脚初始化// GPIO 初始化以 PB6/PB7 为例 void OLED_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); // 空闲时拉高 }如果你用的是 HAL 库把GPIO_Mode_Out_OD换成GPIO_MODE_OUTPUT_OD初始化函数换成HAL_GPIO_Init。注意开漏模式下必须外部上拉否则总线永远是低电平。我见过有人把开漏输出直接接 OLED结果屏幕一直不亮查了半天以为是地址问题其实是上拉电阻没焊。3.2 初始化命令序列SSD1306 的启动流程OLED 上电后不会自动工作必须按 SSD1306 手册发一串初始化命令。这份资源的OLED_Init里通常包含这些void OLED_Init(void) { OLED_GPIO_Init(); delay_ms(200); // 上电延时等 OLED 内部复位 OLED_WR_Byte(0xAE, OLED_CMD); // 关闭显示 OLED_WR_Byte(0xD5, OLED_CMD); // 设置时钟分频 OLED_WR_Byte(0x80, OLED_CMD); // 建议值 OLED_WR_Byte(0xA8, OLED_CMD); // 设置多路复用率 OLED_WR_Byte(0x3F, OLED_CMD); // 1/64 duty OLED_WR_Byte(0xD3, OLED_CMD); // 设置显示偏移 OLED_WR_Byte(0x00, OLED_CMD); OLED_WR_Byte(0x40, OLED_CMD); // 设置起始行 OLED_WR_Byte(0x8D, OLED_CMD); // 电荷泵设置 OLED_WR_Byte(0x14, OLED_CMD); // 开启电荷泵 OLED_WR_Byte(0x20, OLED_CMD); // 设置内存寻址模式 OLED_WR_Byte(0x00, OLED_CMD); // 水平寻址 OLED_WR_Byte(0xA1, OLED_CMD); // 段重映射 OLED_WR_Byte(0xC8, OLED_CMD); // 扫描方向 OLED_WR_Byte(0xDA, OLED_CMD); // COM 硬件配置 OLED_WR_Byte(0x12, OLED_CMD); OLED_WR_Byte(0x81, OLED_CMD); // 对比度 OLED_WR_Byte(0xCF, OLED_CMD); OLED_WR_Byte(0xD9, OLED_CMD); // 预充电周期 OLED_WR_Byte(0xF1, OLED_CMD); OLED_WR_Byte(0xDB, OLED_CMD); // VCOMH 电压 OLED_WR_Byte(0x40, OLED_CMD); OLED_WR_Byte(0xA4, OLED_CMD); // 全局显示开启 OLED_WR_Byte(0xA6, OLED_CMD); // 正常显示非反色 OLED_WR_Byte(0xAF, OLED_CMD); // 开启显示 OLED_Clear(); }这一串里最容易翻车的是0x8D后面的0x14。有些模块电荷泵没开屏幕完全不亮但 IIC 通信是正常的。如果你用逻辑分析仪看到地址应答了命令也发了就是不亮先检查0x8D和0x14有没有漏。另一个是0xA1和0xC8这两个决定屏幕的镜像和翻转方向。如果你显示出来的字是反的把0xA1改成0xA0或者0xC8改成0xC0。3.3 显示第一个字符坐标、字库、刷新三步走初始化完成后调OLED_ShowString就能显示字符。但坐标系统跟常见的 LCD 不一样OLED 的x是列范围 0 到 127y是页范围 0 到 7每页 8 行。如果你写OLED_ShowString(0, 0, Hello, 16)字符会从左上角开始占两页高度。void OLED_ShowChar(u8 x, u8 y, u8 chr, u8 size) { u8 c 0, i 0; u8 temp; c chr - ; // 字库从空格开始偏移 32 if(size 16) { for(i 0; i 8; i) { temp F8X16[c * 16 i]; OLED_GRAM[x][y] temp; OLED_GRAM[x][y 1] F8X16[c * 16 i 8]; x; } } }这段代码里c chr - 是字库偏移。如果你的字库数组是从0开始而不是从空格开始这里要改成chr - 0否则显示出来的字符全是错的。另外OLED_GRAM[x][y]和OLED_GRAM[x][y1]分别对应上半页和下半页写完之后必须调OLED_Refresh_Gram才能看到。我一般会在OLED_ShowString末尾直接加一句刷新省得每次手动调。4. 避坑排查IIC 通信里最容易翻车的五个点4.1 现象屏幕完全不亮逻辑分析仪看不到应答原因通常是硬件接线或者上拉电阻问题。先量 SCL 和 SDA 对 VCC 的电压正常空闲时应该是 3.3V。如果只有 0V 或者 1V 左右说明上拉没起作用。解决方法是检查 OLED 模块背面的上拉电阻是否焊接或者直接在 SCL/SDA 到 3.3V 之间各接一个 4.7k 电阻。另外确认 GPIO 模式是开漏输出如果是推挽输出且总线上有多个设备可能烧掉引脚。4.2 现象能通信但屏幕花屏显示内容错位原因多半是显存刷新时的页地址和列地址设置错了。OLED_Refresh_Gram里0xB0 i是页地址0x00和0x10是列地址的低 4 位和高 4 位。如果你把0x10写成0x00列地址高 4 位永远是 0屏幕只会显示前 16 列的内容后面全是乱的。解决方法是逐行核对OLED_WR_Byte的参数确保页地址从0xB0到0xB7列地址低字节0x00高字节0x10。4.3 现象显示字符时上下颠倒或者左右镜像原因是段重映射和扫描方向设置反了。0xA1是段重映射开启0xC8是扫描方向从 COM63 到 COM0。如果你显示出来的字是上下颠倒把0xC8改成0xC0如果是左右镜像把0xA1改成0xA0。这两个命令在初始化序列里改完之后要重新上电或者重新调OLED_Init才生效。4.4 现象延时改小后通信时好时坏偶尔丢数据原因是软件 IIC 的延时不够从机还没采样完就被拉低了。STM32F1 在 72MHz 下delay_us(2)实际可能只有 1 微秒多而 SSD1306 在标准模式下要求 SCL 高电平至少 4 微秒。解决方法是在IIC_Send_Byte和IIC_Wait_Ack里把延时改成delay_us(4)或者delay_us(5)。如果主频不是 72MHz比如超频到 128MHz延时还要再加大。我一般会在调试阶段先用delay_us(10)确认能通信后再逐步减小到稳定值。4.5 现象编译报错提示sys.h找不到或者位带操作未定义原因是这份驱动依赖了特定的sys.h文件而你的工程里没有或者版本不对。位带操作宏PBout(6)需要sys.h里定义BITBAND宏和地址映射。解决方法是把sys.h一起加到工程里或者把OLED_SCL_PIN和OLED_SDA_PIN改成标准库的GPIO_WriteBit调用。如果你用的是 HAL 库直接换成HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET)这种写法虽然代码长一点但不用依赖额外的头文件。5. 进阶技巧用取模软件生成中文和自定义图标5.1 中文取模的扫描顺序要对上驱动这份驱动默认只带英文和数字字库要显示中文得自己取模。取模软件里设置很关键模式选“阴码”还是“阳码”取模方式选“逐列式”还是“逐行式”输出格式选“C51格式”还是“十六进制”。这份驱动的OLED_ShowChar是按列扫描的所以取模时要选“逐列式”每个汉字 16x16 点阵占 32 字节。// 中文取模示例“中”字 16x16 const u8 HZ_ZHONG[] { 0x00,0x00,0x3F,0xF8,0x20,0x08,0x20,0x08, 0x20,0x08,0x3F,0xF8,0x20,0x08,0x20,0x08, 0x20,0x08,0x3F,0xF8,0x20,0x08,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };取模软件里“逐列式”和“逐行式”的区别在于字节的排列顺序。逐列式是先取第一列 8 个点再取第二列逐行式是先取第一行 8 个点再取第二行。这份驱动的OLED_ShowChar里F8X16[c*16i]和F8X16[c*16i8]分别对应上半页和下半页说明它是按列取的。如果你取模时选了逐行式显示出来会是乱码。5.2 用OLED_DrawPoint画自定义图标除了字符OLED 还能画点、画线、画矩形。这份驱动里通常有OLED_DrawPoint函数原理就是改OLED_GRAM里对应的位void OLED_DrawPoint(u8 x, u8 y, u8 mode) { u8 pos, bx, temp 0; if(x 127 || y 63) return; // 越界保护 pos y / 8; // 算出在第几页 bx y % 8; // 算出在页内第几位 temp 1 bx; if(mode) OLED_GRAM[x][pos] | temp; // 画点 else OLED_GRAM[x][pos] ~temp; // 清点 }有了这个函数你可以把任何 128x64 的单色图片转成数组然后逐点画出来。我一般会先用 PC 端工具把图片转成 C 数组然后在OLED_Init之后调一个OLED_DrawPicture函数把数组刷进OLED_GRAM。注意图片数组的大小是 1024 字节对应 128x64 个点转换的时候别搞错分辨率。5.3 验证方法用逻辑分析仪抓一次完整通信如果你手头有逻辑分析仪把 SCL 和 SDA 接上去抓一次OLED_Init的波形。正常的起始条件是 SCL 高时 SDA 从高变低停止条件是 SCL 高时 SDA 从低变高。每个字节后面跟一个应答位从机拉低 SDA 表示应答。如果你看到起始条件后地址字节没有应答说明地址错了或者从机没上电。如果地址应答了但命令字节没应答说明控制字节写错了。抓一次波形比盲改代码快得多。从那以后我每次拿到新的 OLED 驱动文件都强制先翻.h确认地址和引脚再抓一次上电波形最后才改代码。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

004004002_WPF Button 基类官方类定义逐行深度解析

004004002_WPF Button 基类官方类定义逐行深度解析

004004002_WPF Button 基类官方类定义逐行深度解析摘要:本文基于 .NET 8 官方源码,逐行解析 WPF Button 基类的完整定义与设计意图。内容涵盖类继承层次、类级特性、静态构造函数和核心依赖属性,重点剖析 IsDefault(默认按钮&…

📅 2026/10/15 5:22:55
Claude Code 实战指南:从安装配置到高效任务执行

Claude Code 实战指南:从安装配置到高效任务执行

1. 从零理解 Claude Code 到底在解决什么问题很多人第一次听到 Claude Code 这个名字,第一反应是"又一个 AI 编程助手"。市面上补全代码的工具已经够多了,为什么还要专门花时间学一个新东西?我一开始也是这个疑问,直到真…

📅 2026/10/15 5:22:55
烟雾背后的性能黑洞:20层透明图竟要画20遍

烟雾背后的性能黑洞:20层透明图竟要画20遍

先把场景定下来: 屏幕上有一团烟雾它由 20 张方形图片(面片)叠成,每张都是半透明的烟雾后面是一面黑色的墙我们看看 GPU 到底是怎么把它画出来的。 第 0 步:先画不透明的东西 GPU 先画不透明的物体,比如那面墙。 屏幕上每个像素现在存着:墙的颜色(黑色)同时记下“深度…

📅 2026/10/15 5:22:55
MORE NEWS

更多资讯

📰

用电器分析识别装置:电流波形特征提取与分类器实战

简介:这份PDF是2021年全国大学生电子设计竞赛本科组H题「用电器分析识别装置」的完整赛题文档,面向备战电赛的高校学生与指导教师,帮助读者快速掌握题目任务、指标约束与评分细则。资源包共1个PDF文件,约315KB,内容为官…

📰

第190篇 Messenger 与 AIDL 对比:轻量 IPC 的选择

先把结论放在前面:Messenger 是对 AIDL 的二次封装——它把"一次 RPC 调用"换成"往对方的 Handler 队列里塞一条 Message",用队列换来了串行语义与天然的任务隔离。 因此判断标准只有一条:低频、单向、不需要即时返回值的命令或回传用 Messenger;需要返…

📰

Java集合核心解析:从Collection到List/Set/Queue的底层原理与选型

很多Java开发者最开始接触Collection,脑子里只有一句话:这就是个装数据的容器。直到某天线上出问题——往Set里塞同一个自定义对象竟然塞进去了两个,或者一边遍历一边删元素删到ConcurrentModificationException,才发现这个“容器…

📰

建造堡垒,征伐巨龙 | 用这大屏打开《符文世界:龙之荒野》

9 月 15 日,《符文世界:龙之荒野》1.0 正式版上线,首发同步加入 XGP。作为《RuneScape》系列 25 年来首次登陆主机平台的作品,它将经典符文世界观与生存建造玩法融为一体,在烬落之地上,开启一场探索、建造、…

📰

ArtCraft:面向量产的结构化创意生产方法论

1. 项目概述:ArtCraft 不是“艺术手作”的简单拼贴,而是一套可落地的创意生产方法论ArtCraft 这个词乍看像英文单词 art(艺术)和 craft(工艺/手作)的合成词,很容易被理解成“艺术手作”“创意DI…

📰

2027创新选题:基于AI睡眠分期与数字梦境干预的“眠界”智能睡眠管理系统设计与实现

1. 项目背景与痛点 2027年,全球睡眠障碍人群预计突破 12亿,中国失眠人群占比高达 38.2%。褪黑素滥用、助眠产品泛滥、睡眠数据碎片化等问题日益严重。与此同时,梦境的神经机制逐步被解析,数字梦境干预成为睡眠科技的前沿方向。痛点…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬