简介:这份资源面向使用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 再看 .c:IIC 起始、应答、写字节的完整链路
.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 到 127,y是页,范围 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][y+1]分别对应上半页和下半页,写完之后必须调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*16+i]和F8X16[c*16+i+8]分别对应上半页和下半页,说明它是按列取的。如果你取模时选了逐行式,显示出来会是乱码。
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确认地址和引脚,再抓一次上电波形,最后才改代码。希望帮到你。
本文还有配套的精品资源,点击获取