ARTICLE DETAIL

资讯详情

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

STM32零基础驱动0.96寸OLED:I2C时序与SSD1306移植实战

STM32零基础驱动0.96寸OLED:I2C时序与SSD1306移植实战 简介这是一份面向零基础STM32学习者的Proteus无实物仿真入门教程配套资源主题为0.96英寸OLED屏的I2C接口显示工程。内容采用模块化设计底层驱动可直接调用稍作修改即可移植到51或其他嵌入式平台适合想在不购置实物硬件的情况下快速掌握OLED驱动原理的初学者。资源包共201个文件约4.99MB包含C源文件、头文件、Keil工程文件uvprojx/uvoptx、Proteus仿真工程pdsprj、编译中间文件及hex烧录文件等目录结构清晰便于对照学习。目前已有4034人学习下载。通过该工程可学习0.96英寸OLED的4线制硬件连接和I2C工作原理掌握PB10、PB11引脚的驱动方法理解主程序中哪些语句是驱动核心、哪些可省略。驱动库支持升级扩展性好有助于读者从零开始独立编写OLED底层驱动。1. 0.96 OLED I2C 为什么是零基础入门 STM32 的第一块屏幕很多新手学 STM32卡在第一步往往是屏幕。LCD1602 要 11 根线12864 并口要十几根在 Proteus 里光连线就劝退一批人0.96 寸 OLED 则不同4 线制里真正用来通信的只有 SCL 和 SDA 两根I2C 模式下 STM32F103C8T6 只用 PB10、PB11 两个引脚就能驱动代码量压到几十行供电 3.3V 单电源Proteus 仿真和实物表现基本一致。这个项目就是给零基础准备的目标不是背初始化数组而是从 I2C 时序到 OLED 写入字节完整走一遍最后拿到一个可以直接调用的模块化驱动后续换到 HAL 库、51、甚至 K210 都能改最少代码搬过去。2. 显示原理与 I2C 通信基础SSD1306、四线制和时序2.1 屏上的线不止两根先搞清四线制0.96 寸 OLED 常见是 7 针和 4 针两种4 针的其实就是把并口、SPI 全部砍掉只保留 I2C 接口。所谓 4 线制指的是 VCC、GND、SCL、SDA 四根物理连接而不是 I2C 只有 4 根线。很多初学者看到资料里写“四线制 I2C”以为 I2C 只有 SCL/SDA其实电源和地是绕不开的。引脚作用接到 STM32F103C8T6VCC3.3V 电源3.3VGND地GNDSCLI2C 时钟PB10也可换任意开漏 IOSDAI2C 数据PB11OLED 上那颗驱动芯片通常是 SSD1306I2C 模式下的从机地址由硬件引脚 SA0 决定SA0 接地时 7 位地址是 0x3C接 VCC 是 0x3D。Proteus 的 OLED 模型一般默认 0x3C所以驱动代码里就按 0x3C 写。这里注意I2C 传输时发送的是 8 位地址字节由 7 位地址左移一位再加读写位组成写操作为 0x78很多移植失败是因为在代码里写了 0x78 却又把从机地址寄存器也设成 0x78搞混了两层概念。为什么不用 SPI 或者并口SPI 的 OLED 要额外占用 DC、CS、RST 引脚Proteus 仿真倒无所谓但真做出来会让最小系统板上的杜邦线多三根排查故障时更容易碰到杜邦线虚接。I2C 用两根线能换回整块屏的控制权代价是刷新率低一些但入门和做菜单界面完全够用。2.2 I2C 时序起始、停止、字节与 ACKI2C 的底层协议说穿了就是两条线的电平配合。SCL 高电平期间 SDA 产生一个下降沿表示起始条件SCL 高电平期间 SDA 产生一个上升沿表示停止条件。数据在 SCL 为高时有效发送方必须在 SCL 高电平期间保持 SDA 稳定SCL 低电平时才允许切换 SDA。标准库下我一般用 GPIO 模拟 I2C这样能精确控制每个边沿的时序也方便移植到任何单片机。下面是最核心的三个函数代码里我加了详细注释。#define SCL_H GPIO_SetBits(GPIOB, GPIO_Pin_10) #define SCL_L GPIO_ResetBits(GPIOB, GPIO_Pin_10) #define SDA_H GPIO_SetBits(GPIOB, GPIO_Pin_11) #define SDA_L GPIO_ResetBits(GPIOB, GPIO_Pin_11) void I2C_Start(void) { SDA_H; // 初始空闲态SCL、SDA 均为高 SCL_H; delay_us(5); // 保持一段时间满足起始条件建立时间 SDA_L; // SCL 高电平期间SDA 拉低产生起始条件 delay_us(5); SCL_L; // 拉低 SCL释放总线准备传数据 } void I2C_Stop(void) { SDA_L; // SCL 低电平期间先把 SDA 拉低 SCL_H; // 拉高 SCL满足停止条件要求的高电平窗口 delay_us(5); SDA_H; // SCL 高电平期间SDA 拉高产生停止条件 delay_us(5); } uint8_t I2C_SendByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x80) SDA_H; // 高位先出先把数据放到 SDA else SDA_L; dat 1; SCL_H; // 拉高 SCL通知从机采样 delay_us(5); SCL_L; // 拉低 SCL允许 SDA 变化为下一位 delay_us(5); } SDA_H; // 释放 SDA让从机可以拉低回应 ACK SCL_H; delay_us(5); if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) 0) return 0; // 返回 0 表示从机 ACK else return 1; // 返回 1 表示 NACK }起始条件必须在 SCL 为高时拉低 SDA停止条件相反发送字节时 SCL 高电平期间的数据必须稳定否则从机采样到错误电平。GPIO 模拟 I2C 的延时决定了 SCL 频率5us 对应 100kHz 级别SSD1306 支持最快 400kHz但仿真和实物都不建议跑满因为 GPIO 翻转本身也有时间开销。这里我把 SDA 配置成开漏输出加外部上拉改读 ACK 时可以直接读输入数据寄存器开漏模式下即使输出寄存器输出高电平外部上拉也会把电平拉高所以能读到 SDA 状态。这就是为什么 I2C 总线上的 GPIO 建议用开漏模式而不是推挽模式推挽输出强拉高会与从机的应答拉低冲突严重时可能损坏引脚。2.3 写给 SSD1306 的指令控制字节与初始化命令SSD1306 在 I2C 模式下每次传输是先发从机地址字节 0x78写然后是控制字节最后是数据。控制字节的 D/C 位决定后面的数据是命令还是显存内容0x00 表示命令0x40 表示显示数据。这也是为什么很多驱动代码里有两个函数 OLED_WR_CMD 和 OLED_WR_Dat它们唯一的区别就是控制字节不同。下面这段代码是典型的初始化命令注意不是每次都要全部执行后面第五章会讲精简方案。void OLED_Init(void) { delay_ms(100); // 上电稳定 OLED_WR_CMD(0xAE); // 关闭显示 OLED_WR_CMD(0x20); // 设置内存地址模式水平、垂直、页地址 OLED_WR_CMD(0x02); // 页地址模式最常见 OLED_WR_CMD(0x81); // 设置对比度 OLED_WR_CMD(0xCF); // 对比度值 0~255 OLED_WR_CMD(0x8D); // 电荷泵设置 OLED_WR_CMD(0x14); // 开启电荷泵OLED 内部升压必需 OLED_WR_CMD(0xAF); // 打开显示 }这里最关键的三条是 0xAE、0x8D/0x14、0xAF关显示、开电荷泵、开显示。电荷泵不打开屏幕即使初始化成功也不亮Proteus 里表现就是 OLED 区域全黑但没有报错。0x20 后的 0x02 选页地址模式后续显示字符串时需要先设置页地址和列地址具体值在 OLED_SetPos 函数里给出。如果你想一开始就把代码压缩到最短可以先跳过对比度等参数只保留那三条命令然后把显示内容直接写进显存也能看到像素点。I2C 通信的详细讲解网上很多但放到 OLED 场景里你只需记住一点所有命令和数据本质上都是通过 I2C 发送字节SSD1306 靠控制字节来区分这一字节属于命令还是显存数据。3. 用 Proteus 搭出可仿真的 STM32 OLED 工程3.1 元件选择与接线别忘了上拉电阻Proteus 里新建工程后从元件库中搜索 STM32F103C8T6以及 OLED。OLED 模型在不同版本里名字不完全一致常见的有“OLED 0.96”也有“SSD1306”如果你用的 Proteus 版本没有 OLED可以找一个 I2C 接口的图形 LCD 代替但最好还是用 0.96 的模型因为分辨率、初始化和实物对得上。找到后把元件放到图纸上VCC 接 3.3V 电源GND 接 GNDSCL 连 PB10SDA 连 PB11。SCL、SDA 上各加一颗 4.7k 欧姆上拉电阻到 3.3V虽然 Proteus 的 I2C 模型不一定强制要求但实物上不加上拉会出现随机花屏严谨起见先加上去。给每个网络打上 net label避免肉眼找线。我一般还会在工程里放一个 I2C Debugger它挂在总线上可以实时捕获起始条件、地址字节和 ACK。它的作用先不展开第五章用它验证时序会非常直观。还有一点STM32F103C8T6 的 PB10 和 PB11 同时是 USART3 的 TX/RX如果同一个工程里还有串口功能换个普通 IO 比如 PB6/PB7 或 PB8/PB9 也可以代码里改一下 GPIO 引脚和 RCC 时钟即可。3.2 配置 Keil 工程标准外设库与开漏模式软件部分用 Keil5 标准外设库 V3.5新建工程时选 STM32F103C8T6 芯片启动文件选 startup_stm32f10x_md.s因为是中等容量器件。在 main 函数里最先要打开 GPIOB 的时钟然后初始化 PB10、PB11。以下代码就是整个工程里必须的初始化void GPIO_I2C_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; // 开漏输出 GPIO_Init(GPIOB, GPIO_InitStructure); }这里 GPIO_Mode 必须用开漏输出。标准库的 GPIO_Mode_Out_OD 是开漏GPIO_Mode_Out_PP 是推挽。上面说过开漏配合外部上拉才能与 I2C 从机做线与逻辑。如果把 GPIO 设成推挽输出OLED 会表现出一个很隐蔽的问题初始化看起来成功但多传几个字节后总线卡死。原因是推挽输出在发送 1 时强上拉而从机 ACK 需要把 SDA 拉低两边对抗导致时序错乱。这在 Proteus 仿真里不一定复现因为 Proteus 的模型有时候就是普通逻辑电平不模拟驱动能力但你一旦烧到实物芯片十有八九会遇到。所以从一开始就按实物标准写。工程里保留的 keilkill.bat 是 Keil 的清理脚本双击后可以删除 .uvgui、.axf、.o 等中间文件只留源码。它的用处是每次重新仿真前清理工程避免 Proteus 加载到的 hex 文件路径存在但内容还是老固件导致现象不对。我通常在编译后用命令行跑一次 keilkill再刷新 Proteus 的 program file。3.3 编译烧录设置HEX 路径与 Proteus 加载Keil 中点击魔法棒在 Output 选项卡勾选 Create HEX File然后编译生成 hex。记录输出路径比如 ./Objects/Project.hex。回到 Proteus双击 STM32F103C8T6 芯片在 Program File 里选择该 hex 文件。这里有个细节如果 Keil 工程和 Proteus 工程不在同一个目录建议直接把 hex 复制到 Proteus 工程目录下然后把芯片的 program file 路径设置为相对路径避免换电脑或者移动整个文件夹后路径失效。仿真时如果屏幕没显示先检查左下角仿真状态确认芯片时钟配置里 HSE 是否与实际一致STM32F103C8T6 的 HSE 可以是 8MHzProteus 模型默认也按 8MHz代码里 SystemInit 会去配置 PLL 到 72MHz。如果 HSE 频率设置错了I2C 模拟时序的延时虽然不受系统时钟太大影响但最终主频不对会让 delay_us 时间不准进而导致 SCL 频率过高。4. 精简到极致的驱动代码核心函数与主程序4.1 模块化文件结构oled.h、oled.c、main.c下载包里保持了一套标准的分层结构oled.h 定义引脚宏和对外接口oled.c 实现 I2C 底层、控制字节、初始化、清屏、显示字符串main.c 只做三件事——初始化 GPIO、初始化 OLED、进入循环显示。这种分法的好处是当你想换硬件 I2C 或者把驱动移到 HAL 库时只需要修改 oled.c 里的底层四个函数I2C_Start、I2C_Stop、I2C_SendByte、I2C_ReceiveByte本例未用到接收上层显示函数完全不用动。下表是文件职责文件功能oled.h引脚定义、函数声明、行列宏128x64oled.cI2C 时序、命令/数据写入、初始化和字符显示main.c时钟使能、调用 OLED_Init、实现显示逻辑4.2 写命令和写数据控制字节的区别OLED 的 I2C 写入流程固定为起始条件 → 发送从机地址 0x78写→ 等待 ACK → 发送控制字节0x00 命令或 0x40 数据→ 等待 ACK → 发送实际数据 → 等待 ACK → 停止。所以 OLED_WR_CMD 和 OLED_WR_Dat 实际上是同一个函数的参数化版本。void OLED_WR_CMD(uint8_t cmd) { I2C_Start(); // 起始条件 I2C_SendByte(0x78); // 从机地址 0x3C 1读写位为 0 I2C_SendByte(0x00); // 控制字节后续是命令 I2C_SendByte(cmd); // 命令字节 I2C_Stop(); // 停止条件 } void OLED_WR_Dat(uint8_t dat) { I2C_Start(); I2C_SendByte(0x78); I2C_SendByte(0x40); // 控制字节后续是显存数据 I2C_SendByte(dat); I2C_Stop(); }这里将 0x78 写死省去每次左移运算代码更短。若你的 OLED 的 SA0 接高电平把地址改成 0x7A即 0x3D 1 加写位。如果 I2C_SendByte 返回 1NACK你可以在读回返回值后打印一个调试标志但多数情况下仿真里看不到效果因为 Proteus 模型中即使无应答也不会报错。注意发送数据时每次传输都是完整的 I2C 起始/停止周期SSD1306 允许连续地址内多个数据字节但驱动里如果每次只发一个字节实时刷新数据时会慢一些显示静态字符串没问题。4.3 显示一个字符坐标计算与字模字符显示的本质是往页地址和列地址对应的显存区域写 8x16 或 8x8 的字模。SSD1306 的显存按页划分128x64 像素分成 8 页每页 8 行像素水平方向每页 128 字节。页地址模式需要先设置位置void OLED_SetPos(uint8_t x, uint8_t y) // x:0~127, y:0~7 { OLED_WR_CMD(0xB0 y); // 页地址 B0~B7 OLED_WR_CMD(((x 0xF0) 4) | 0x10); // 列地址高四位 OLED_WR_CMD(x 0x0F); // 列地址低四位 }列地址分高四位和低四位两次发送且需要与 0x10 和 0x00 做位或这是 SSD1306 数据手册里的硬性要求漏掉任何一段都会导致字符显示到了错误位置。字模数据我用 PCtoLCD2002 软件取模取模方式选择“列行式”“顺向取模”对应 SSD1306 的显存排列。OLED_ShowStr 函数拿到字模后一字节一字节地调用 OLED_WR_Dat 写入写完一页后如果字符高度超过 16 像素需要循环写相邻的两三页区域。4.4 主程序真的可以只有十行逻辑下面是 main.c 的骨架没有经过系统初始化之外的任何多余函数但能完成驱动点亮#include stm32f10x.h #include oled.h int main(void) { SystemInit(); // 默认 72MHz 系统时钟 GPIO_I2C_Init(); // PB10/PB11 开漏初始化 OLED_Init(); // SSD1306 初始化开电荷泵 OLED_Clear(0x00); // 清屏全灭 OLED_ShowStr(0, 0, STM32 OLED); OLED_ShowStr(0, 2, Hello I2C); while (1); }代码里 OLED_ShowStr 的第二个参数是页不是像素纵坐标。如果写成 16、32 这类像素坐标需要先除以 8 再传给函数很多移植教程的坑就在这。OLED_Clear(0x00) 清屏是必须的因为 SSD1306 的 RAM 上电后不保证全为 0Proteus 里表现为满屏雪花点或随机亮斑。清屏之所以放在初始化之后、显示之前是因为第一步要建立一个干净的显存环境。测试时可以在刚上电后加一个 delay_ms(1000)让 OLED 完成内部上电复位但这属于可省略的语句我在代码里已经做了注释说明实际跑的时候如果屏幕没亮先去掉省略的部分看是不是初始化顺序问题。5. 验证与移植避坑从 Proteus 仿真到实物的三板斧5.1 用 I2C 调试器验证地址和 ACK把 I2C Debugger 放在 SCL、SDA 和地之间运行仿真打开调试器窗口。看捕获到的第一帧起始条件后第一个字节应为 0x78随后字节后面有 ACK 标记。如果显示 0x7A 或找不到 ACK先检查 oled.h 里的从机地址。还要看每个命令后面的控制字节是 0x00 还是 0x40。I2C 调试器标注的是 8 位地址看到 0x78 是正常的并不意味着驱动里写错。5.2 常见故障速查表现象原因排查动作屏幕全黑但仿真运行电荷泵未开启检查 0x8D,0x14 命令是否在初始化中满屏雪花点没有清屏或显存随机值调用 OLED_Clear(0x00)字符错位页地址或列地址未设置检查 OLED_SetPos 的 B0 和列高四位I2C 调试器无数据引脚连错或开漏配置缺失检查 PB10/PB11 网络标号和 GPIO_Mode_Out_OD仿真能亮实物不亮驱动能力不足或上拉缺失实物加上拉电阻VCC 确保 3.3V这里想强调一下 I2C 协议中的 ACK 坑如果 SDA 配置为推挽输出ACK 读取时会读到异常逻辑甚至卡在 SCL 拉低状态。Proteus 仿真中开漏和推挽都能跑所以容易让新手以为推挽也行等到焊接实物才怀疑人生。从第一天起就按开漏写后面会省下很多调板时间。5.3 跨平台移植时只需要改四个底层函数标准库版本可以很平滑地切到 STM32 HAL 库把 GPIO 操作替换为 HAL_GPIO_WritePin时钟使能替换为 __HAL_RCC_GPIOB_CLK_ENABLE()初始化结构体 Mode 设为 GPIO_MODE_OUTPUT_OD。HAL 库点亮 OLED 全流程与标准库一致网上江协科技那一版用的就是 HAL 库对照学习时会发现除了寄存器命名规则不同I2C 模拟时序完全一样。从 STM32 搬到 51 单片机时把 GPIO_SetBits/ResetBits 替换成 sbit 操作即可。由于 OLED 驱动对总线速度不敏感100kHz 慢速也能正常显示所以不需要修改延时参数但 STM32F407 这类 M4 内核主频更高记得把延时函数换成系统时钟校准过的版本。最后分享一个能直接抄的入门技巧把 OLED_Init 里的命令精简为三行——0xAE 关显示、0x8D/0x14 开启电荷泵、0xAF 打开显示。保留这三条屏幕至少能亮起来然后再慢慢加入对比度、内存模式、页地址设置每加一条命令就重新仿真一次观察屏幕变化规律。这样既能建立对指令集的直觉也能在自己写驱动时快速定位是哪一条命令的问题这就是从“照着抄初始化数组”到“理解 OLED 驱动”的临界点。本文还有配套的精品资源点击获取
返回列表