一、整体说明
这是一套 STM32 软件模拟 I2C 驱动 0.96 寸 OLED(SSD1306) 的完整工程代码。特点:
- 纯软件 I2C,不占用硬件 I2C 外设
- 引脚:
SCL=PB0、SDA=PB1 - 驱动芯片:
SSD1306 - 支持:清屏、显示字符、显示汉字、显示图片
二、文件总览
整个工程由 3 个核心文件 组成:
oledr.c------ 驱动核心(最重要,本文重点讲解)oledr.h------ 宏定义、函数声明codetab.h------ 字库(ASCII、汉字、图片取模数据)
三、oledr.c 超详细逐模块讲解(博客核心)
模块 1:延时函数(软件 I2C 必须靠延时控制时序)
// 微秒级延时(粗略延时,满足I2C时序即可)
static void delay_us(unsigned char num)
{
unsigned char i;
while(num--)
{
i = 10;
while(i--);
}
}
// 毫秒级延时(用于初始化等待、上电稳定)
static void delay_ms(unsigned int ms)
{
unsigned int x,y;
for(x = ms;x > 0;x--)
for(y=12000;y>0;y--);
}
作用
delay_us:控制 I2C 通信时序,保证 SDA/SCL 变化稳定delay_ms:OLED 上电需要稳定时间,初始化前必须等待
模块 2:GPIO 初始化(软件 I2C 最重要一步)
static void OLED_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
// 开 GPIOB + AFIO 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
// 模式:通用开漏输出(软件I2C必须用这个!)
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
// 引脚:PB0(SCL) + PB1(SDA)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
// 速度:50MHz
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// 初始状态:SCL=1,SDA=1(I2C 总线空闲状态)
OLED_SCLK_Set();
OLED_SDIN_Set();
}
关键知识点
- 必须开 AFIO 时钟
- 模式必须是
Out_OD开漏输出- 硬件 I2C 用
AF_OD - 软件 I2C 必须用通用开漏
- 硬件 I2C 用
- I2C 空闲状态 = SCL=1,SDA=1
模块 3:I2C 起始信号(通信的 "开场白")
static void OLED_IIC_Start(void)
{
OLED_SCLK_Set(); // SCL=1
OLED_SDIN_Set(); // SDA=1
delay_us(1); // 稳定
OLED_SDIN_CLR(); // SDA 从 1→0(产生起始信号)
delay_us(1);
OLED_SCLK_CLR(); // 拉低时钟,准备传输数据
}
I2C 起始信号规则(标准)
SCL 保持高电平时,SDA 由高变低 → 起始信号OLED 检测到这个信号,才知道 "要开始通信了"。
模块 4:I2C 停止信号(通信的 "结束语")
static void OLED_IIC_Stop(void)
{
OLED_SDIN_CLR();
delay_us(1);
OLED_SCLK_Set();
delay_us(1);
OLED_SDIN_Set(); // SDA 从 0→1(产生停止信号)
delay_us(1);
}
规则
SCL 高电平时,SDA 由低变高 → 停止信号
模块 5:I2C 等待 ACK 应答(确认 OLED 收到数据)
static unsigned char IIC_Wait_Ack(void)
{
unsigned char ack;
OLED_SCLK_CLR();
delay_us(1);
OLED_SDIN_Set();
delay_us(1);
OLED_SCLK_Set();
delay_us(1);
// 读取 SDA 电平
if(OLED_READ_SDIN())
ack = IIC_NO_ACK; // 无应答
else
ack = IIC_ACK; // 有应答
OLED_SCLK_CLR();
delay_us(1);
return ack;
}
作用
每发 1 个字节,OLED 必须拉低 SDA 表示 "我收到了"。如果无应答 → 通信失败。
模块 6:I2C 发送一个字节(核心收发函数)
static void Write_IIC_Byte(unsigned char IIC_Byte)
{
unsigned char i;
for(i=0;i<8;i++) // 一个字节8位
{
OLED_SCLK_CLR(); // 拉低时钟,准备放数据
delay_us(1);
// 从最高位开始发送
if(IIC_Byte & 0x80)
OLED_SDIN_Set();
else
OLED_SDIN_CLR();
IIC_Byte <<= 1; // 左移,准备下一位
delay_us(1);
OLED_SCLK_Set(); // 上升沿发送数据
delay_us(1);
}
OLED_SCLK_CLR();
delay_us(1);
IIC_Wait_Ack(); // 等待应答
}
原理
I2C 发送数据规则:
- SCL 低 → 放数据
- SCL 高 → 数据被读取
- 高位先发
模块 7:发送命令 / 发送数据(OLED 指令规则)
发送命令(设置 OLED:亮度、地址、模式等)
static void Write_IIC_Command(unsigned char IIC_Command)
{
OLED_IIC_Start();
Write_IIC_Byte(0x78); // OLED 地址
Write_IIC_Byte(0x00); // 0x00 = 写命令
Write_IIC_Byte(IIC_Command);
OLED_IIC_Stop();
}
发送数据(显示内容:点亮像素)
static void Write_IIC_Data(unsigned char IIC_Data)
{
OLED_IIC_Start();
Write_IIC_Byte(0x78); // OLED 地址
Write_IIC_Byte(0x40); // 0x40 = 写数据
Write_IIC_Byte(IIC_Data);
OLED_IIC_Stop();
}
超级重点
- 0x00 = 写命令
- 0x40 = 写数据
- 0x78 = OLED 设备地址这是 SSD1306 严格规定的通信格式!
模块 8:写命令 / 写数据 封装函数
void OLED_WR_Byte(unsigned char data, unsigned char cmd)
{
if(cmd)
Write_IIC_Data(data); // cmd=1 → 数据
else
Write_IIC_Command(data);// cmd=0 → 命令
}
模块 9:设置光标坐标(决定在哪里显示)
void OLED_Set_Pos(unsigned char x,unsigned char y)
{
OLED_WR_Byte(0xb0+y,OLED_CMD); // 设置页地址(Y)
OLED_WR_Byte((x&0x0f),OLED_CMD); // 列地址低4位
OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD); // 列地址高4位
}
OLED 128×64 分为 8 页 × 128 列
- 一页 = 8 行
- 所以 Y 方向 0~7
- X 方向 0~127
模块 10:开显示 / 关显示
void OLED_Display_On(void)
{
OLED_WR_Byte(0x8D,OLED_CMD);
OLED_WR_Byte(0x14,OLED_CMD); // 开启电荷泵
OLED_WR_Byte(0xAF,OLED_CMD); // 开显示
}
void OLED_Display_Off(void)
{
OLED_WR_Byte(0x8D,OLED_CMD);
OLED_WR_Byte(0x10,OLED_CMD); // 关闭电荷泵
OLED_WR_Byte(0xAE,OLED_CMD); // 关显示
}
模块 11:清屏函数
void OLED_Clear(void)
{
unsigned char i,n;
for(i=0;i<8;i++) // 8页
{
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++) // 128列
OLED_WR_Byte(0,OLED_DATA); // 全部写0
}
}
模块 12:显示一个 ASCII 字符
void OLED_ShowChar(unsigned char x,unsigned char y,unsigned char chr)
{
unsigned char c = 0,i = 0;
c = chr - ' '; // 偏移量计算
if(SIZE == 16) // 16×8大小字符
{
OLED_Set_Pos(x,y);
for(i=0;i<8;i++)
OLED_WR_Byte(F8X16[c*16+i],OLED_DATA);
OLED_Set_Pos(x,y+1);
for(i=0;i<8;i++)
OLED_WR_Byte(F8X16[c*16+8+i],OLED_DATA);
}
else // 6×8大小
{
OLED_Set_Pos(x,y);
for(i=0;i<6;i++)
OLED_WR_Byte(F6x8[c][i],OLED_DATA);
}
}
原理
- 一个 16×8 字符占 2 页
- 先写上 8 行,再写下 8 行
- 数据来自
codetab.h字库
模块 13:OLED 初始化(最关键!必须按顺序发)
void OLED_Init(void)
{
OLED_GPIO_Init();
delay_ms(200);
OLED_WR_Byte(0xAE,OLED_CMD); // 关显示
OLED_WR_Byte(0x00,OLED_CMD); // 列低地址
OLED_WR_Byte(0x10,OLED_CMD); // 列高地址
OLED_WR_Byte(0x40,OLED_CMD); // 起始行
OLED_WR_Byte(0xB0,OLED_CMD); // 页地址
OLED_WR_Byte(0x81,OLED_CMD);
OLED_WR_Byte(0xFF,OLED_CMD); // 亮度
OLED_WR_Byte(0xA1,OLED_CMD); // 左右反转
OLED_WR_Byte(0xA6,OLED_CMD); // 正常显示
OLED_WR_Byte(0xA8,OLED_CMD);
OLED_WR_Byte(0x3F,OLED_CMD); // 64行
OLED_WR_Byte(0xC8,OLED_CMD); // 上下反转
OLED_WR_Byte(0xD3,OLED_CMD);
OLED_WR_Byte(0x00,OLED_CMD);
OLED_WR_Byte(0xD5,OLED_CMD);
OLED_WR_Byte(0x80,OLED_CMD);
OLED_WR_Byte(0xD9,OLED_CMD);
OLED_WR_Byte(0xF1,OLED_CMD);
OLED_WR_Byte(0xDA,OLED_CMD);
OLED_WR_Byte(0x12,OLED_CMD);
OLED_WR_Byte(0xDB,OLED_CMD);
OLED_WR_Byte(0x40,OLED_CMD);
OLED_WR_Byte(0x8D,OLED_CMD);
OLED_WR_Byte(0x14,OLED_CMD);
OLED_WR_Byte(0xAF,OLED_CMD); // 开显示
OLED_Clear();
OLED_Set_Pos(0,0);
}
作用
按 SSD1306 官方要求初始化:
- 时钟
- 驱动路数
- 内存模式
- 扫描方向
- 对比度
- 开启电荷泵(必须开,否则不亮)
四、oledr.h 头文件
#ifndef _OLEDR_H
#define _OLEDR_H
#include "stm32f10x.h"
//==================== 1. I2C 引脚操作宏定义 ====================
#define OLED_SCLK_Set() GPIO_SetBits(GPIOB, GPIO_Pin_0) // SCL=1
#define OLED_SCLK_CLR() GPIO_ResetBits(GPIOB, GPIO_Pin_0) // SCL=0
#define OLED_SDIN_Set() GPIO_SetBits(GPIOB, GPIO_Pin_1) // SDA=1
#define OLED_SDIN_CLR() GPIO_ResetBits(GPIOB, GPIO_Pin_1) // SDA=0
#define OLED_READ_SDIN() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) // 读SDA电平
//==================== 2. 命令/数据标志位 ====================
#define IIC_ACK 0 // 应答信号
#define IIC_NO_ACK 1 // 非应答信号
#define OLED_CMD 0 // 写命令
#define OLED_DATA 1 // 写数据
//==================== 3. 字库选择 ====================
#define SIZE 16 // 字体大小:16=16*8, 其他=6*8
//==================== 4. 函数声明 ====================
void OLED_WR_Byte(unsigned char data, unsigned char cmd);
void OLED_Set_Pos(unsigned char x,unsigned char y);
void OLED_Display_On(void);
void OLED_Display_Off(void);
void OLED_Clear(void);
void OLED_ShowChar(unsigned char x,unsigned char y,unsigned char chr);
void OLED_Init(void);
#endif
详细解释
1. 防止头文件重复包含
#ifndef _OLEDR_H
#define _OLEDR_H
...
#endif
- 作用:防止一个
.c文件多次包含同一个头文件,导致重复定义报错。 - 所有正式的 C 语言头文件都必须这么写。
2. 包含 STM32 库文件
#include "stm32f10x.h"
- 作用:引入 STM32 的标准库,让编译器认识
GPIO_SetBits、GPIO_InitTypeDef等函数和结构体。 - 没有这一行,所有 GPIO 操作都无法编译。
3. I2C 引脚操作宏(核心!)
#define OLED_SCLK_Set() GPIO_SetBits(GPIOB, GPIO_Pin_0)
#define OLED_SCLK_CLR() GPIO_ResetBits(GPIOB, GPIO_Pin_0)
#define OLED_SDIN_Set() GPIO_SetBits(GPIOB, GPIO_Pin_1)
#define OLED_SDIN_CLR() GPIO_ResetBits(GPIOB, GPIO_Pin_1)
#define OLED_READ_SDIN() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)
作用:
把 STM32 复杂的库函数,简化成一句话:
SCLK_Set()→ 让 PB0 输出高电平SCLK_CLR()→ 让 PB0 输出低电平SDIN_Set()→ 让 PB1 输出高电平SDIN_CLR()→ 让 PB1 输出低电平READ_SDIN()→ 读取 PB1 现在是高电平还是低电平
为什么要写宏?
原来代码要写:
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
现在只需要写:
OLED_SCLK_CLR();
代码更简洁、可读性更强、移植更方便。
硬件对应:
- PB0 = I2C_SCL(时钟线)
- PB1 = I2C_SDA(数据线)
4. 命令 / 数据标志
#define OLED_CMD 0 // 写命令
#define OLED_DATA 1 // 写数据
- OLED 通信规则:
- 给
0→ 发送命令(设置屏幕、亮度、坐标) - 给
1→ 发送数据(显示内容、点亮像素点)
- 给
5. 字体大小定义
#define SIZE 16
SIZE=16→ 使用 16×8 大小的 ASCII 字符(两行高度)- 不是 16 → 使用 6×8 大小的 ASCII 字符(一行高度)
6. 函数声明
void OLED_Init(void);
void OLED_Clear(void);
void OLED_ShowChar(...);
...
- 作用:告诉编译器,这些函数在
oledr.c里实现, - 这样
main.c包含头文件后,就可以直接调用这些函数。
五、main.c 主函数详细解释
#include "stm32f10x.h"
#include "main.h"
#include "stdio.h"
#include "oledr.h"
// 毫秒级延时函数
void delay(uint16_t time)
{
uint16_t i = 0;
while(time--)
{
i = 12000;
while(i--);
}
}
extern const unsigned char BMP2[];
int main(void)
{
// 中断分组(本工程暂时没用)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 1. 初始化 OLED
OLED_Init();
// 2. 等待 2 秒
delay(2000);
// 3. 清屏(全部熄灭)
OLED_Clear();
// 4. 显示字符
OLED_ShowChar(30,2,'O');
OLED_ShowChar(38,2,'K');
// 死循环,程序停在这里
while(1)
{
}
}
1. 头文件包含
#include "stm32f10x.h"
#include "main.h"
#include "oledr.h"
stm32f10x.h:STM32 核心库oledr.h:OLED 驱动头文件(必须包含,否则不能用 OLED 函数)
2. 延时函数
void delay(uint16_t time)
{
uint16_t i = 0;
while(time--)
{
i = 12000;
while(i--);
}
}
- 作用:毫秒级粗略延时
- 用于:初始化后等待屏幕稳定、方便观察效果
3. 主函数入口
int main(void)
{
- 程序从这里开始执行
4. OLED 初始化(最重要)
OLED_Init();
- 内部执行:
- 初始化 PB0、PB1 为开漏输出
- 发送一系列 SSD1306 初始化命令
- 打开屏幕电荷泵(必须开,否则不亮)
- 默认清屏
5. 延时 2 秒
delay(2000);
- 等待屏幕稳定,防止刚上电就操作导致异常
6. 清屏
OLED_Clear();
- 把屏幕所有点都写 0 → 全部熄灭
7. 显示字符
OLED_ShowChar(30,2,'O');
OLED_ShowChar(38,2,'K');
- 格式:
OLED_ShowChar(x, y, 字符) x=30→ 水平第 30 列y=2→ 垂直第 2 页(一页 8 行)- 功能:在屏幕上显示 OK