OLED(Organic Light Emitting Diode,有机发光二极管)是一种性能优异的新型显示屏,具有功耗低、响应速度快、宽视角、轻薄柔韧等优点。每个像素都是独立的发光二极管,无需背光,比 LCD 更省电。
上图:0.96 寸 OLED 模块及引脚定义(来源:STM32入门教程 PPT 第38页)
上图:STM32 与 OLED 模块接线图(来源:STM32入门教程 PPT 第39页)
STM32 标准库工程中的 OLED 驱动使用 GPIO 模拟 I2C 时序,引脚定义:
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))
I2C 时序关键函数:
| 函数 | 说明 |
|---|---|
OLED_I2C_Init() |
初始化 GPIO(开漏输出),SCL/SDA 初始拉高 |
OLED_I2C_Start() |
SDA 在高电平期间拉低,SCL 保持高 |
OLED_I2C_Stop() |
SDA 在低电平期间拉高,SCL 保持高 |
OLED_I2C_SendByte() |
高位先行,逐位输出,每字节后额外一个时钟不处理应答 |
OLED_WriteCommand() |
封装 Start + 地址 + 0x00 + Command + Stop |
OLED_WriteData() |
封装 Start + 地址 + 0x40 + Data + Stop |
写命令/写数据流程:
Start → 0x78(地址+写) → 0x00(命令类型) → Command → StopStart → 0x78(地址+写) → 0x40(数据类型) → Data → StopSSD1306 的 I2C 从机地址固定为 0x78(7 位地址 0x3C 左移一位)。
I2C 总线只有两根线:SCL(时钟)和 SDA(数据)。软件模拟的核心是按照 I2C 协议规范,用 GPIO 逐位翻转电平。
起始条件:SCL=高期间,SDA 从高→低(唯一允许 SDA 在 SCL=高时变化的情况)
SCL: ━━━━━┓ SCL保持高不变
┃
SDA: ━━━━┓┃ SDA先高→再低
┃┃
┃┃←── SDA下降沿, SCL为高 = 起始
停止条件:SCL=高期间,SDA 从低→高
SCL: ┃┃━━━ SCL保持高不变
┃┃
SDA: ┃┃━━ SDA先低→再高
┃┃
┃┃←── SDA上升沿, SCL为高 = 停止
数据位传输:SCL=低时改变 SDA,SCL=高时采样 SDA(高位先行,MSB first)
Bit7 Bit6 Bit5 ... Bit0
SCL: ━┃┃━━┃┃━━┃┃━ ━┃┃━
┃┃ ┃┃ ┃┃ ... ┃┃
SDA: ┃┃━━ ┃┃ ┃┃━━ ┃┃
┃┃ ┃┃ ┃┃ ┃┃
↑ ↑ ↑ ↑
SCL低 SCL高 SCL低 SCL高
时设 时采 时设 时采
数据 样数 数据 样数
据 据
/* 引脚初始化:开漏输出,50MHz */
void OLED_I2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出: 外部上拉决定高电平
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; // PB8 = SCL
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // PB9 = SDA
GPIO_Init(GPIOB, &GPIO_InitStructure);
OLED_W_SCL(1); // 初始SCL=高(空闲状态)
OLED_W_SDA(1); // 初始SDA=高(空闲状态)
}
void OLED_I2C_Start(void)
{
// I2C起始条件: SCL高电平时, SDA从高→低
OLED_W_SDA(1); // ① 确保SDA先为高(防SDA低时误判)
OLED_W_SCL(1); // ② SCL拉高(SCL高时SDA变化才有效)
OLED_W_SDA(0); // ③ SCL高→SDA下降沿 → 从机识别起始
OLED_W_SCL(0); // ④ SCL拉低(准备传输数据, SCL低时SDA可变化)
}
void OLED_I2C_Stop(void)
{
// I2C停止条件: SCL高电平时, SDA从低→高
OLED_W_SDA(0); // ① 确保SDA先为低
OLED_W_SCL(1); // ② SCL拉高
OLED_W_SDA(1); // ③ SCL高→SDA上升沿 → 从机识别停止
}
void OLED_I2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
// 提取Byte的第(7-i)位: 高位先行(MSB first)
// 0x80 >> i: i=0→0x80(1000 0000), i=1→0x40(0100 0000) ...
// & 按位与: 提取对应位, 结果=0或非0
// !! 双重取反: 将非0→1, 0→0, 确保(BitAction)参数为0或1
// 例: Byte=0xAA(10101010), i=0 → 0x80 & 0xAA=0x80 → !!=1 → SDA=1
// i=1 → 0x40 & 0xAA=0x00 → !!=0 → SDA=0
OLED_W_SDA(!!(Byte & (0x80 >> i)));
OLED_W_SCL(1); // SCL高→从机在上升沿读取SDA
OLED_W_SCL(0); // SCL低→准备下一位(SDA可在SCL低时变化)
}
// 第9个时钟: 从机应答位(ACK)
// 我们释放SDA(开漏输出, 写1=高阻, 由上拉拉高)
// 从机应答时会拉低SDA, 但这里不做检查(非阻塞应答)
OLED_W_SCL(1); // 第9个SCL高电平, 从机应答时间
OLED_W_SCL(0); // 拉低SCL, 结束应答周期
}
void OLED_WriteCommand(uint8_t Command)
{
OLED_I2C_Start(); // 起始条件
OLED_I2C_SendByte(0x78); // 发送从机地址: 0x78 = 7位地址0x3C << 1 + W(0)
// 二进制 0111 1000, Bit[0]=0=写操作
OLED_I2C_SendByte(0x00); // 控制字节: 0x00 = 写命令模式
// SSD1306约定: 0x00表示后续字节是命令
OLED_I2C_SendByte(Command); // 发送命令字节
OLED_I2C_Stop(); // 停止条件
}
void OLED_WriteData(uint8_t Data)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); // 从机地址+写
OLED_I2C_SendByte(0x40); // 控制字节: 0x40 = 写数据模式
// 与0x00的区别只在Bit[6]=1
OLED_I2C_SendByte(Data);
OLED_I2C_Stop();
}
/* 前提:使用 CubeMX 配置 I2C1,SCL=PB8, SDA=PB9 */
/* 在 i2c.c 中自动生成 MX_I2C1_Init(),此处仅封装 OLED 接口 */
#include "i2c.h"
#define OLED_I2C_ADDR 0x78 // 7位地址0x3C左移1位
#define OLED_I2C_TIMEOUT 100
void OLED_WriteCommand(uint8_t Command)
{
HAL_I2C_Mem_Write(&hi2c1, OLED_I2C_ADDR, 0x00,
I2C_MEMADD_SIZE_8BIT, &Command, 1, OLED_I2C_TIMEOUT);
}
void OLED_WriteData(uint8_t Data)
{
HAL_I2C_Mem_Write(&hi2c1, OLED_I2C_ADDR, 0x40,
I2C_MEMADD_SIZE_8BIT, &Data, 1, OLED_I2C_TIMEOUT);
}
/* 批量写数据(效率更高) */
void OLED_WriteData_Bulk(uint8_t *pData, uint16_t Length)
{
HAL_I2C_Mem_Write(&hi2c1, OLED_I2C_ADDR, 0x40,
I2C_MEMADD_SIZE_8BIT, pData, Length, OLED_I2C_TIMEOUT);
}
HAL 库下无需软件模拟 I2C,直接复用 STM32 的 I2C 外设。需在 CubeMX 中配置 I2C1(速度 400kHz,7 位地址),并在工程中包含
i2c.h。
OLED_Init() 在上电延时后按顺序发送以下命令序列:
0xAE — 关闭显示0xD5 0x80 — 设置显示时钟分频/振荡器频率0xA8 0x3F — 设置多路复用率(64 路)0xD3 0x00 — 设置显示偏移0x40 — 设置显示开始行0xA1 — 设置左右方向(正常)0xC8 — 设置上下方向(正常)0xDA 0x12 — 设置 COM 引脚硬件配置0x81 0xCF — 设置对比度0xD9 0xF1 — 设置预充电周期0xDB 0x30 — 设置 VCOMH 取消选择级别0xA4 — 设置整个显示打开0xA6 — 设置正常显示(非反色)0x8D 0x14 — 使能充电泵0xAF — 开启显示OLED_Clear() — 清屏void OLED_Init(void)
{
uint32_t i, j;
for (i = 0; i < 1000; i++) // 上电延时
for (j = 0; j < 1000; j++);
OLED_I2C_Init(); // GPIO 初始化
OLED_WriteCommand(0xAE); // 关闭显示
OLED_WriteCommand(0xD5); // 设置时钟分频/振荡器频率
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8); // 设置多路复用率
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3); // 设置显示偏移
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40); // 设置显示开始行
OLED_WriteCommand(0xA1); // 左右方向(正常)
OLED_WriteCommand(0xC8); // 上下方向(正常)
OLED_WriteCommand(0xDA); // COM 引脚硬件配置
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); // 设置对比度
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9); // 预充电周期
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); // VCOMH 取消选择级别
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4); // 整个显示打开
OLED_WriteCommand(0xA6); // 正常显示(非反色)
OLED_WriteCommand(0x8D); // 使能充电泵
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF); // 开启显示
OLED_Clear();
}
void OLED_Init(void)
{
HAL_Delay(100); // 上电延时 100ms
OLED_WriteCommand(0xAE);
OLED_WriteCommand(0xD5);
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8);
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3);
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40);
OLED_WriteCommand(0xA1);
OLED_WriteCommand(0xC8);
OLED_WriteCommand(0xDA);
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81);
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9);
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB);
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4);
OLED_WriteCommand(0xA6);
OLED_WriteCommand(0x8D);
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF);
OLED_Clear();
}
OLED_SetCursor(Y, X)SSD1306 内部将 128×64 像素划分为 8 页(Page),每页 8 个像素高。Y 范围 0~7 对应页号,X 范围 0~127。
SSD1306 的页地址和列地址使用分段编码:
0xB0 | Y — 0xB0 是页基址(1011 0000), Y(0~7)写入低3位0x10 | (X高4位) — 0x10=0001 0000, 拼接X的Bit[7:4]列地址低4位: 0x00 | (X低4位) — 0x00=0000 0000, 拼接X的Bit[3:0]
X=127(0x7F): 高4位=0x07, 低4位=0x0F
→ 命令序列: 0xB0|Y, 0x10|0x07=0x17, 0x00|0x0F=0x0F
→ 即: 页Y, 列高4位=7, 列低4位=15 → 列地址=0x7F=127
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
// | 按位或: 0xB0(1011 0000) 与 Y(0~7) 拼合
// 0xB0 = 页命令基址, Y写入Bit[2:0]
// 例: Y=3 → 0xB0 | 0x03 = 0xB3 = 第3页
OLED_WriteCommand(0xB0 | Y); // 设置页地址(Y)
// & 按位与: 先提取X的高4位(X & 0xF0)
// 0xF0 = 1111 0000, X=0x7F→0x70, X=0xAB→0xA0
// >> 右移4位: 将高4位移到Bit[3:0]位置
// X=0x7F: 0x70>>4 = 0x07
// | 按位或: 0x10(0001 0000) | 结果 → 列高4位命令
// 例: X=127 → 0x10 | 0x07 = 0x17
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); // 列地址高4位
// & 按位与: 提取X的低4位(X & 0x0F)
// 0x0F = 0000 1111, X=0x7F→0x0F
// | 按位或: 0x00 | 低4位 = 低4位本身
// 例: X=127 → 0x00 | 0x0F = 0x0F
OLED_WriteCommand(0x00 | (X & 0x0F)); // 列地址低4位
}
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
uint8_t cmds[3];
// | 按位或: 页地址基址0xB0 + Y(0~7的三位页号)
cmds[0] = 0xB0 | Y;
// & >> | 三步: 取X高4位→右移到低位→拼0x10
cmds[1] = 0x10 | ((X & 0xF0) >> 4);
// & |: 取X低4位→拼0x00(等价于X&0x0F)
cmds[2] = 0x00 | (X & 0x0F);
HAL_I2C_Mem_Write(&hi2c1, OLED_I2C_ADDR, 0x00,
I2C_MEMADD_SIZE_8BIT, cmds, 3, OLED_I2C_TIMEOUT);
}
OLED_Clear()遍历 8 页 × 128 列,每字节写入 0x00。
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++)
{
OLED_SetCursor(j, 0);
for (i = 0; i < 128; i++)
{
OLED_WriteData(0x00);
}
}
}
void OLED_Clear(void)
{
uint8_t i, j;
uint8_t zeroBuf[128];
for (i = 0; i < 128; i++) zeroBuf[i] = 0x00;
for (j = 0; j < 8; j++)
{
OLED_SetCursor(j, 0);
HAL_I2C_Mem_Write(&hi2c1, OLED_I2C_ADDR, 0x40,
I2C_MEMADD_SIZE_8BIT, zeroBuf, 128, OLED_I2C_TIMEOUT);
}
}
OLED_ShowChar(Line, Column, Char)OLED_F8x16 取数据void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
uint8_t i;
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); // 上半部分
for (i = 0; i < 8; i++)
OLED_WriteData(OLED_F8x16[Char - ' '][i]);
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); // 下半部分
for (i = 0; i < 8; i++)
OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]);
}
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
uint8_t i;
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8);
for (i = 0; i < 8; i++)
OLED_WriteData(OLED_F8x16[Char - ' '][i]);
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8);
for (i = 0; i < 8; i++)
OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]);
}
OLED_ShowString(Line, Column, String)逐字符循环调用 OLED_ShowChar,遇 \0 终止。
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++)
{
OLED_ShowChar(Line, Column + i, String[i]);
}
}
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++)
{
OLED_ShowChar(Line, Column + i, String[i]);
}
}
OLED_ShowNum(Line, Column, Number, Length)显示无符号十进制整数,逐位提取后调用 OLED_ShowChar:
/* 次方函数:X 的 Y 次方 */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
OLED_ShowChar(Line, Column + i,
Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
OLED_ShowChar(Line, Column + i,
Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
| 函数 | 说明 |
|---|---|
OLED_ShowSignedNum(Line, Column, Number, Length) |
有符号十进制,带 +/- 前缀 |
OLED_ShowHexNum(Line, Column, Number, Length) |
十六进制,0~F |
OLED_ShowBinNum(Line, Column, Number, Length) |
二进制,仅每位 0/1 |
所有数字显示函数均通过 OLED_ShowChar 实现,核心是一个通用的 OLED_Pow(X, Y) 次方函数。
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0)
{
OLED_ShowChar(Line, Column, '+');
Number1 = Number;
}
else
{
OLED_ShowChar(Line, Column, '-');
Number1 = -Number;
}
for (i = 0; i < Length; i++)
OLED_ShowChar(Line, Column + i + 1,
Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++)
{
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10)
OLED_ShowChar(Line, Column + i, SingleNumber + '0');
else
OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
}
}
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
OLED_ShowChar(Line, Column + i,
Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
}
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0)
{
OLED_ShowChar(Line, Column, '+');
Number1 = Number;
}
else
{
OLED_ShowChar(Line, Column, '-');
Number1 = -Number;
}
for (i = 0; i < Length; i++)
OLED_ShowChar(Line, Column + i + 1,
Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++)
{
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10)
OLED_ShowChar(Line, Column + i, SingleNumber + '0');
else
OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
}
}
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
OLED_ShowChar(Line, Column + i,
Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
}
OLED_F8x16位于 OLED_Font.h,定义为 const uint8_t OLED_F8x16[][16],共 95 个 ASCII 可见字符(从空格 ' ' 到 '~'),每个字符 16 字节(上半 8 字节 + 下半 8 字节)。字模数据为列行式:每字节的 8 位对应一列中 8 个像素的亮灭。
| 方法 | 优点 | 缺点 |
|---|---|---|
| 串口调试 | 可借助 PC 显示曲线、图形,可做复杂交互 | 需拖线,信息流显示,变化数据刷屏 |
| 显示屏调试 | 覆盖刷新,直接观察,可嵌入产品 | 屏幕小,内容有限 |
| Keil 调试模式 | 单步、断点、查看寄存器/变量 | 需调试器,不适合运行时观察 |
int main(void)
{
OLED_Init();
OLED_ShowChar(1, 1, 'A');
OLED_ShowString(1, 3, "HelloWorld!");
OLED_ShowNum(2, 1, 12345, 5);
OLED_ShowSignedNum(2, 7, -66, 2);
OLED_ShowHexNum(3, 1, 0xAA55, 4);
OLED_ShowBinNum(4, 1, 0xAA55, 16);
while (1);
}
屏幕坐标系:左上角为原点 (0,0),向右 X 轴 0~127,向下 Y 轴 0~63。驱动将屏幕划分为 4 行 16 列的逻辑网格,便于定位字符显示位置。
尚硅谷课程不使用 OLED(使用 LCD 1602/12864),但其 软件 I2C 驱动 实现与 OLED 的 GPIO 模拟 I2C 方式完全一致。核心结构:
// I2C 位操作宏(尚硅谷风格)
// BSRR(Bit Set/Reset Register): 低16位置1=引脚拉高, 写0不变
// = 直接赋值BSRR: 将Bit[8]置1 → PB8=高电平(SCL)
// BSRR是"原子操作", 写1自动置位, 无需读-改-写
#define I2C_SCL_H() GPIOB->BSRR = GPIO_BSRR_BS8
// BRR(Bit Reset Register): 写1=引脚拉低, 写0不变
// = 直接赋值BRR: 将Bit[8]置1 → PB8=低电平(SCL)
// BRR是BSRR的"清零专用寄存器", 只清零, 不改其他
#define I2C_SCL_L() GPIOB->BRR = GPIO_BRR_BR8
#define I2C_SDA_H() GPIOB->BSRR = GPIO_BSRR_BS9 // = BSRR Bit[9]=1 → PB9=高
#define I2C_SDA_L() GPIOB->BRR = GPIO_BRR_BR9 // = BRR Bit[9]=1 → PB9=低
// & 按位与: 读取IDR(输入数据寄存器)的Bit[9]
// IDR是只读寄存器, 引脚电平直通
// PB9=高时IDR Bit[9]=1, =低时=0
// 非0则SDA为高电平, 0则SDA为低电平
#define I2C_SDA_R() (GPIOB->IDR & GPIO_IDR_IDR9)
// 起始 / 停止 / 发送字节 / 接收字节
// 时序与 OLED 中 OLED_I2C_Start/Stop/SendByte 完全相同
BRR 与 BSRR 的分工:
| 操作 | BSRR(低16位) | BRR | BSRR(高16位) |
|---|---|---|---|
| 置1 | ✅ 写1 → 引脚高 | ❌ 不支持 | ❌ 不支持 |
| 清0 | ❌ 不支持 | ✅ 写1 → 引脚低 | ✅ 写1 → 引脚低 |
| 不影响 | 写0 | 写0 | 写0 |
为什么要用 BSRR/BRR 而非 ODR:BSRR/BRR 写操作是"单次原子写入"(只写不读),不存在"读-改-写"的竞态问题。如果在中断中操作 ODR 位,可能覆盖中断前的修改。BSRR/BRR 没有这个风险。
详见 I2C 章节的寄存器级位操作实现。