STM32(三)I2C总线

3.1 基本原理

上一章的串口只能一对一通信,这节的I2C总线可以一对多通信。如下图所示:

  • I2C总线由时钟线(SCL数据线(SDA组成,主机和从机都需要连接这两条线。
    • 时钟信号由主机产生;
    • 数据信号由主机或从机产生。
  • I2C有一个主机和多个从机,不同从机用7位地址区分。
  • 主从机所有的SCL和SDA引脚都是开漏输出(即写0拉低,写1高阻),SCL和SDA需要接上拉电阻
  • 发送信号:以主机产生时钟为例,所有从机的SCL都置1悬空:
    • 主机写0,SCL拉低,时钟线低电平;
    • 主机写1,SCL悬空,此时所有设备都悬空,上拉至高电平。

3.2 I2C协议

I2C更细致的讲解可以参考FPGA(八)EEPROM

3.2.1 起始和停止位

3.2.2 寻址

#代表0,即W0R1

关于应答:第8周期末主机释放SDA,第9周期从机拉低SDA表示应答,第9周期末从机释放SDA。

3.2.3 传输数据

  • 主机发送从机接收:从机每收到一个字节都要应答(拉低)。
  • 从机发送主机接收:主机除了最后一个字节都要应答(拉低),最后一个字节主机发送非应答(NACK,拉高),然后发送停止信号。(如果不发NACK,从机可能再次发送数据,和停止位冲突)

3.3 I2C模块

3.3.1 内部结构

  • “SDA控制”下方的寄存器是用来发送起始位(START)、停止位(STOP)、应答位(ACK)的。

    • 其中ACK只作用于正在被接收的字节。即如果当前正在接收一个字节,此时先为ACK寄存器赋值,则该字节接收完成后会发送ACK寄存器中的值;如果该字节已经接收完了,此时再为ACK寄存器赋值只会作用到下一个接收的字节。
  • “SCL控制”左边的寄存器是控制时钟的寄存器。

  • 右上角是状态寄存器SR1SR2

    • BUSY:总线忙。

    • SB(Start Bit):起始位发送完成。

    • AF(Acknowledge Failure):应答失败。

    • ADDR:寻址成功。

    • TxE:发送数据寄存器空。

    • BTF(Byte Transfer Finished):字节传输完成。

3.3.2 引脚配置

3.3.3 连线

3.3.4 速度模式

只支持前两种。

只有快速模式下可以设置时钟信号的占空比(高电平占一个周期比例),一般选择左边的T_low/T_high = 2/1

3.3.5 I2C初始化

参见3.4节My_I2C_Init函数。

3.4 写数据

OLED屏幕的地址是0x78,发送如下值点亮屏幕。

#include "stm32f10x.h"

void My_I2C_Init(void);
int My_I2C_SendBytes(I2C_TypeDef *I2Cx, uint8_t Addr, const uint8_t *pData, uint16_t Size);

int main(void)
{
    My_I2C_Init();
    uint8_t commands[] = {0x00, 0x8d, 0x14, 0xaf, 0xa5};
    My_I2C_SendBytes(I2C1, 0x78, commands, 5);

    while (1) {}
}

void My_I2C_Init(void) {
    // 1. 初始化 IO 引脚:PB6 PB7 复用开漏输出
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 2. 初始化 I2C(注意是 APB1)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE);    // 施加复位
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE);   // 释放复位
    
    I2C_InitTypeDef I2C_InitStruct;
    I2C_InitStruct.I2C_ClockSpeed = 400000;         // 波特率 400k
    I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;         // 标准 I2C
    I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; // 占空比 2:1
    I2C_Init(I2C1, &I2C_InitStruct);

    I2C_Cmd(I2C1, ENABLE);      // 闭合 I2C1 总开关
}

//
// @简介:通过I2C向从机写入多个字节
// 
// @参数 I2Cx:填写要操作的I2C的名称,可以是I2C1或I2C2
// @参数 Addr:填写从机的地址,左对齐 - A6 A5 A4 A3 A2 A1 A0 0
// @参数 pData:要发送的数据(数组)
// @参数 Size:要发送的数据的数量,以字节为单位
//
// @返回值:0 - 发送成功, -1 - 寻址失败, -2 - 数据被拒收
//
int My_I2C_SendBytes(I2C_TypeDef *I2Cx, uint8_t Addr, const uint8_t *pData, uint16_t Size) {
    // 1. 等待总线空闲
    while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY) == SET);
    
    // 2. 发送起始位
    I2C_GenerateSTART(I2Cx, ENABLE);
    // 等待起始位发送完成
    while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB) == RESET);
    
    // 3. 寻址阶段
    I2C_ClearFlag(I2Cx, I2C_FLAG_AF);   // 清除AF
    I2C_SendData(I2Cx, Addr & 0xfe);    // 发送地址(写,最低位是0)
    
    while (1) {
        // ADDR = 1 表示寻址成功
        if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR) == SET) {
            break;
        }
        // AF = 1 表示未收到ACK,寻址失败,发送停止位
        if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF) == SET) {
            I2C_GenerateSTOP(I2Cx, ENABLE);
            return -1; // 寻址失败
        }
    }
    
    // 清除ADDR(通过读这两个寄存器来清除,规定了必须这样)
    I2C_ReadRegister(I2Cx, I2C_Register_SR1);
    I2C_ReadRegister(I2Cx, I2C_Register_SR2);
    
    // 4. 发送数据
    for (uint16_t i = 0; i < Size; i++) {
        while (1) {
            if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF) == SET) {
                I2C_GenerateSTOP(I2Cx, ENABLE);
                return -2; // 数据被拒收
            }
            // 数据寄存器空,可以开始发送
            if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_TXE) == SET) {
                break;
            }
        }
        I2C_SendData(I2Cx, pData[i]);
    }
    
    // 等待全部发送完成
    while (1) {
        if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF) == SET) {
                I2C_GenerateSTOP(I2Cx, ENABLE);
                return -2; // 数据被拒收			
        }
        // BTF = 1 表示发送完成
        if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BTF) == SET) {
            break;
        }
    }
    
    // 5. 发送停止位
    I2C_GenerateSTOP(I2Cx, ENABLE);
    return 0;
}

3.5 读数据

从OLED读1字节数据,取D6判断屏幕是否点亮,点亮则开启板载LED。

#include "stm32f10x.h"

void My_I2C_Init(void);
void My_OnBoardLED_Init(void);
int My_I2C_ReceiveBytes(I2C_TypeDef *I2Cx, uint8_t Addr, uint8_t *pBuffer, uint16_t Size);

int main(void)
{
    My_I2C_Init();
    My_OnBoardLED_Init();
    uint8_t rcvd;
    My_I2C_ReceiveBytes(I2C1, 0x78, &rcvd, 1);
    if ((rcvd & (1 << 6)) == 0) {
        GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);   // 点亮 LED
    } else {
        GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);     // 熄灭 LED
    }

    while (1) {}
}

void My_I2C_Init(void) {
    // 1. 初始化 IO 引脚:PB6 PB7 复用开漏输出
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 2. 初始化 I2C(注意是 APB1)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE);    // 施加复位
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE);   // 释放复位
    
    I2C_InitTypeDef I2C_InitStruct;
    I2C_InitStruct.I2C_ClockSpeed = 400000;         // 波特率 400k
    I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;         // 标准 I2C
    I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; // 占空比 2:1
    I2C_Init(I2C1, &I2C_InitStruct);

    I2C_Cmd(I2C1, ENABLE);      // 闭合 I2C1 总开关
}

void My_OnBoardLED_Init(void) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitTypeDef GPIO_InitStruct;

    // PC13 通用输出开漏
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOC, &GPIO_InitStruct);

    // 默认关闭 LED
    GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
}

//
// @简介:通过I2C从从机读多个字节
// 
// @参数 I2Cx:填写要操作的I2C的名称,可以是I2C1或I2C2
// @参数 Addr:填写从机的地址,左对齐 - A6 A5 A4 A3 A2 A1 A0 0
// @参数 pBuffer:接收缓冲区(数组)
// @参数 Size:要读取的数据的数量,以字节为单位
//
// @返回值:0 - 发送成功, -1 - 寻址失败
//
int My_I2C_ReceiveBytes(I2C_TypeDef *I2Cx, uint8_t Addr, uint8_t *pBuffer, uint16_t Size) {
    // 1. 等待总线空闲
    while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY) == SET);
    
    // 2. 发送起始位
    I2C_GenerateSTART(I2Cx, ENABLE);
    while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB) == RESET);
    
    // 3. 寻址阶段
    I2C_ClearFlag(I2Cx, I2C_FLAG_AF);   // 清除AF
    I2C_SendData(I2Cx, Addr | 0x01);    // 发送地址(读,最低位是1)
    
    while (1) {
        // ADDR = 1 表示寻址成功
        if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR) == SET) {
            break;
        }
        // AF = 1 表示未收到ACK,寻址失败,发送停止位
        if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF) == SET) {
            I2C_GenerateSTOP(I2Cx, ENABLE);
            return -1; // 寻址失败
        }
    }
    
    // 4. 数据读取
    if (Size == 1) {
        // 清除ADDR
        I2C_ReadRegister(I2Cx, I2C_Register_SR1);
        I2C_ReadRegister(I2Cx, I2C_Register_SR2);
        // 向ACK写0(要趁接收未完成赶快写0)
        I2C_AcknowledgeConfig(I2Cx, DISABLE);
        // 发送停止位
        I2C_GenerateSTOP(I2Cx, ENABLE);
        // 等待RxNE置位
        while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_RXNE) == RESET);
        // 读取数据
        pBuffer[0] = I2C_ReceiveData(I2Cx);
    } else {
        // 清除ADDR
        I2C_ReadRegister(I2Cx, I2C_Register_SR1);
        I2C_ReadRegister(I2Cx, I2C_Register_SR2);
        // 向ACK写1
        I2C_AcknowledgeConfig(I2Cx, ENABLE);
        // 读取前Size-1个字节,都发送ACK
        for (uint16_t i = 0; i < Size - 1; i++) {
            // 等待RxNE置位
            while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_RXNE) == RESET);
            // 读取
            pBuffer[i] = I2C_ReceiveData(I2Cx);
        }
        // 向ACK写0
        I2C_AcknowledgeConfig(I2Cx, DISABLE);
        // 发送停止位
        I2C_GenerateSTOP(I2Cx, ENABLE);
        // 等待RxNE置位
        while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_RXNE) == RESET);
        // 读取最后一个数据
        pBuffer[Size - 1] = I2C_ReceiveData(I2Cx);
    }
    return 0;
}

3.6 软I2C

STM32提供的I2C引脚有时候都会被占用,我们可以用普通的GPIO引脚来模拟I2C。

3.6.1 发送起始位和停止位

只有在SCL为低电平时才能改变数据,否则就是起始或停止位了。

3.6.2 发送一个字节

发送一个比特原理如下:

发送一个字节并读取ACK:

3.6.3 读取一个字节

读取一个比特:

读取一个字节并发送ACK/NACK:

3.6.4 连线与最终代码

#include "stm32f10x.h"

void My_SI2C_Init(void);
void scl_write(uint8_t level);
void sda_write(uint8_t level);
uint8_t sda_read(void);
void delay_us(uint32_t us);
void SendStart(void);
void SendStop(void);
uint8_t SendByte(uint8_t byte);
uint8_t ReceiveByte(uint8_t ack);
int My_SI2C_SendBytes(uint8_t addr, uint8_t *pData, uint16_t size);
int My_SI2C_ReceiveBytes(uint8_t addr, uint8_t *pBuffer, uint16_t size);

int main(void)
{
    My_SI2C_Init();
    uint8_t commands[] = {0x00, 0x8d, 0x14, 0xaf, 0xa5};
    My_SI2C_SendBytes(0x78, commands, 5);
    while (1) {}
}

void My_SI2C_Init(void) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;   // 通用输出开漏
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 初始化高电平
    GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
    GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET);
}

void scl_write(uint8_t level) {
    if (level == 0) {
        GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
    } else {
        GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
    }
}

void sda_write(uint8_t level) {
    if (level == 0) {
        GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET);
    } else {
        GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET);
    }
}

uint8_t sda_read(void) {
    if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == Bit_SET) {
        return 1;
    } else {
        return 0;
    }
}

void delay_us(uint32_t us) {
    // 每次循环是 1/8 us
    for (uint32_t i = 0; i < us * 8; i++);
}

// 发送起始位,SCL高电平时拉低SDA
void SendStart(void) {
    sda_write(0);
    delay_us(1);
}

// 发送停止位,SCL高电平时拉高SDA
void SendStop(void) {
    scl_write(0);   // 先拉低SCL,才能修改SDA
    sda_write(0);
    delay_us(1);
    scl_write(1);   // SCL拉高
    delay_us(1);
    sda_write(1);   // SDA拉高
    delay_us(1);
}

// 发送一个字节,返回ACK位(0表示ACK,1表示NACK)
uint8_t SendByte(uint8_t byte) {
    for (int8_t i = 7; i >= 0; i--) {
        scl_write(0);   // 先拉低SCL,才能修改SDA
        sda_write((byte >> i) & 0x01);   // 发送当前位
        delay_us(1);
        scl_write(1);   // 拉高SCL
        delay_us(1);
    }
    // 读取ACK
    scl_write(0);   // 先拉低SCL,才能修改SDA
    sda_write(1);   // 释放SDA,等待从设备拉低
    delay_us(1);
    scl_write(1);   // 拉高SCL,准备读取ACK
    delay_us(1);
    return sda_read();   // 读取ACK位
}

// 读取一个字节,并发送 ACK(ack == 1) / NACK(ack == 0)
uint8_t ReceiveByte(uint8_t ack) {
    uint8_t byte = 0;
    for (int8_t i = 7; i >= 0; i--) {
        scl_write(0);   // 先拉低SCL,才能修改SDA
        sda_write(1);   // 释放SDA,等待从设备发送数据
        delay_us(1);
        scl_write(1);   // 拉高SCL,准备读取数据位
        delay_us(1);
        byte |= (sda_read() << i);   // 读取当前位
    }
    // 发送ACK/NACK
    scl_write(0);       // 先拉低SCL,才能修改SDA
    sda_write(!ack);    // 发送ACK或NACK
    delay_us(1);
    scl_write(1);       // 拉高SCL
    delay_us(1);
    return byte;
}

int My_SI2C_SendBytes(uint8_t addr, uint8_t *pData, uint16_t size) {
    SendStart();
    if (SendByte(addr & 0xfe) != 0) {   // 发送地址+写位
        SendStop();
        return -1;  // 从设备未响应
    }
    for (uint16_t i = 0; i < size; i++) {
        if (SendByte(pData[i]) != 0) {   // 发送数据字节
            SendStop();
            return -1;  // 从设备未响应
        }
    }
    SendStop();
    return 0;   // 成功发送
}

int My_SI2C_ReceiveBytes(uint8_t addr, uint8_t *pBuffer, uint16_t size) {
    SendStart();
    if (SendByte(addr | 0x01) != 0) {   // 发送地址+读位
        SendStop();
        return -1;  // 从设备未响应
    } 
    for (uint16_t i = 0; i < size - 1; i++) {
        pBuffer[i] = ReceiveByte(1);   // 读取数据字节,发送ACK
    }
    pBuffer[size - 1] = ReceiveByte(0);   // 读取最后一个字节,发送NACK
    SendStop();
    return 0;   // 成功接收
}

3.7 封装常用功能

my_libi2c.csi2c.c有写好的函数。

// i2c.h
int My_I2C_SendBytes(I2C_TypeDef *I2Cx, uint8_t Addr, const uint8_t *pData, uint16_t Size);
int My_I2C_ReceiveBytes(I2C_TypeDef *I2Cx, uint8_t Addr, uint8_t *pBuffer, uint16_t Size);
// si2c.h
void My_SI2C_Init(SI2C_TypeDef *SI2C);
int My_SI2C_SendBytes(SI2C_TypeDef *SI2C, uint8_t Addr, const uint8_t *pData, uint16_t Size);
int My_SI2C_ReceiveBytes(SI2C_TypeDef *SI2C, uint8_t Addr, uint8_t *pBuffer, uint16_t Size);

3.8 OLED库的使用

3.8.1 OLED介绍

原理是将数据写入OLED的数据存储中,然后就会自动显示到屏幕上。

左上角是(0, 0),分别表示x和y坐标。该OLED屏幕宽(x)128,高(y)64。

使用提供的oled.h中的函数即可显示。

3.8.2 屏幕初始化

#include "stm32f10x.h"
#include "si2c.h"
#include "oled.h"

int i2c_write_bytes(uint8_t addr, const uint8_t *pdata, uint16_t size);

SI2C_TypeDef si2c;      // 软I2C实例
OLED_TypeDef oled;      // OLED实例

int main(void)
{
    // 1. 初始化软I2C
    si2c.SCL_GPIOx = GPIOB;
    si2c.SCL_GPIO_Pin = GPIO_Pin_6;
    si2c.SDA_GPIOx = GPIOB;
    si2c.SDA_GPIO_Pin = GPIO_Pin_7;
    My_SI2C_Init(&si2c);

    // 2. 初始化OLED,只需要赋值回调函数
    OLED_InitTypeDef oled_init_struct;
    oled_init_struct.i2c_write_cb = i2c_write_bytes;
    OLED_Init(&oled, &oled_init_struct);

    while (1) {}
}

int i2c_write_bytes(uint8_t addr, const uint8_t *pdata, uint16_t size) {
    return My_SI2C_SendBytes(&si2c, addr, pdata, size);
}

3.8.3 文字相关操作

#include "hyjk16.h"

int main(void)
{
    // 1. 初始化软I2C
    // 2. 初始化OLED,只需要赋值回调函数
	......

    // 3. 打印字符串
    OLED_SetBrush(&oled, BRUSH_TRANSPARENT);    // 设置透明画刷
    OLED_SetPen(&oled, PEN_COLOR_WHITE, 1);     // 设置白色画笔,宽度为1
    OLED_SetCursor(&oled, 24, 50);              // 设置光标位置
    OLED_DrawString(&oled, "Hello, World");     // 打印字符串
    
    // 4. 打印文字
    OLED_SetFont(&oled, &hyjk16);               // 设置字体
    uint16_t x = (OLED_GetScreenWidth(&oled) - OLED_GetStrWidth(&oled, "你好世界")) / 2; // 计算水平居中位置
    OLED_SetCursor(&oled, x, 28);               // 设置光标位置
    OLED_DrawString(&oled, "你好世界");          // 打印字符串
    
    // 5. 格式化打印日期
    OLED_SetFont(&oled, &default_font);         // 恢复到默认字体
    OLED_SetCursor(&oled, 58, 64);
    OLED_Printf(&oled, "%04d-%02d-%02d", 2026, 4, 25);
    
    // 6. 文本区域
    OLED_StartTextRegion(&oled, 0, 0, 128, 64); // 定义文本区域
    // 需要使用\r使每行光标移到最前
    OLED_DrawString(&oled, "There was a man named Jhon.\r\n");
    OLED_DrawString(&oled, "He was 70 years old.\r\n");
    OLED_DrawString(&oled, "He lived in a small village.\r\n");
    OLED_DrawString(&oled, "He loved to tell stories.");

    OLED_SendBuffer(&oled);                     // 发送缓冲区内容到屏幕

    while (1) {}
}

3.8.4 绘图相关操作

图片转位图网站,生成bitmap。

// 1. 初始化软I2C
// 2. 初始化OLED,只需要赋值回调函数
......
        
// 3. 画点
OLED_SetPen(&oled, PEN_COLOR_WHITE, 3);
OLED_SetCursor(&oled, 29, 32);
OLED_DrawDot(&oled);

// 4. 画线
OLED_SetCursor(&oled, 0, 0);
// 从当前位置画线到指定位置
OLED_DrawLine(&oled, 127, 63);

OLED_SetCursor(&oled, 84, 22);
// 从当前位置画线到指定位置并移动光标到指定位置
OLED_LineTo(&oled, 44, 22);
OLED_LineTo(&oled, 44, 42);
OLED_LineTo(&oled, 84, 42);

// 5. 画矩形和圆
OLED_SetCursor(&oled, 20, 20);
OLED_SetPen(&oled, PEN_COLOR_WHITE, 1);
OLED_SetBrush(&oled, BRUSH_WHITE);
OLED_DrawRect(&oled, 40, 20);

OLED_SetCursor(&oled, 65, 30);
OLED_DrawCircle(&oled, 5);

// 6. 绘制图像
const uint8_t bitmap[] = {......};
OLED_SetCursor(&oled, 0, 0);
OLED_DrawBitmap(&oled, 128, 64, bitmap);

OLED_SendBuffer(&oled);                     // 发送缓冲区内容到屏幕

STM32(三)I2C总线
https://shuusui.site/blog/2026/04/24/hardware/stm32-3/
作者
Shuusui
发布于
2026年4月24日
更新于
2026年4月25日
许可协议