03-OLED显示与调试.md 21 KB

OLED 显示与调试

概述

OLED(Organic Light Emitting Diode,有机发光二极管)是一种性能优异的新型显示屏,具有功耗低、响应速度快、宽视角、轻薄柔韧等优点。每个像素都是独立的发光二极管,无需背光,比 LCD 更省电。

SSD1306 规格

  • 尺寸: 0.96 寸
  • 分辨率: 128 × 64 像素
  • 供电: 3.0 ~ 5.5 V(兼容 3.3V 和 5V)
  • 通信协议: I2C 或 SPI
  • 像素颜色: 白色、蓝色、黄蓝双色等
  • 驱动 IC: SSD1306

OLED 模块 上图:0.96 寸 OLED 模块及引脚定义(来源:STM32入门教程 PPT 第38页)

OLED 硬件接线 上图:STM32 与 OLED 模块接线图(来源:STM32入门教程 PPT 第39页)

I2C 协议(软件模拟)

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 → Stop
  • 写数据: Start → 0x78(地址+写) → 0x40(数据类型) → Data → Stop

SSD1306 的 I2C 从机地址固定为 0x78(7 位地址 0x3C 左移一位)。

软件 I2C 时序原理详解(尚硅谷风格)

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高
       时设 时采 时设              时采
       数据 样数 数据              样数
            据                    据

标准库(GPIO 模拟 I2C)

/* 引脚初始化:开漏输出,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();
}

HAL库(硬件 I2C 外设)

/* 前提:使用 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 初始化序列

OLED_Init() 在上电延时后按顺序发送以下命令序列:

  1. 0xAE — 关闭显示
  2. 0xD5 0x80 — 设置显示时钟分频/振荡器频率
  3. 0xA8 0x3F — 设置多路复用率(64 路)
  4. 0xD3 0x00 — 设置显示偏移
  5. 0x40 — 设置显示开始行
  6. 0xA1 — 设置左右方向(正常)
  7. 0xC8 — 设置上下方向(正常)
  8. 0xDA 0x12 — 设置 COM 引脚硬件配置
  9. 0x81 0xCF — 设置对比度
  10. 0xD9 0xF1 — 设置预充电周期
  11. 0xDB 0x30 — 设置 VCOMH 取消选择级别
  12. 0xA4 — 设置整个显示打开
  13. 0xA6 — 设置正常显示(非反色)
  14. 0x8D 0x14 — 使能充电泵
  15. 0xAF — 开启显示
  16. 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();
}

HAL库

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位
  • 列地址高4位: 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位
}

HAL库

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);
        }
    }
}

HAL库

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)

  • Line: 1~4(每行对应 2 页 = 16 像素高)
  • Column: 1~16(每列对应 8 像素宽)
  • 一个字符占 8×16 像素,上半页 8 字节 + 下半页 8 字节,从字模表 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]);
}

HAL库

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]);
    }
}

HAL库

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');
}

HAL库

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');
}

HAL库(与标准库完全相同,此处仅列一次)

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 个像素的亮灭。

使用 OLED 进行调试

三种调试方法对比

方法 优点 缺点
串口调试 可借助 PC 显示曲线、图形,可做复杂交互 需拖线,信息流显示,变化数据刷屏
显示屏调试 覆盖刷新,直接观察,可嵌入产品 屏幕小,内容有限
Keil 调试模式 单步、断点、查看寄存器/变量 需调试器,不适合运行时观察

OLED 调试的优势

  • 可以直接接在单片机上,显示方式直观
  • 对不断变化的数据可以覆盖刷新显示
  • 适合作为人机交互界面的一部分

常用模式

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 列的逻辑网格,便于定位字符显示位置。


尚硅谷:软件I2C驱动参考

尚硅谷课程不使用 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 章节的寄存器级位操作实现。