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,即W是0、R是1。关于应答:第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控制”左边的寄存器是控制时钟的寄存器。
右上角是状态寄存器
SR1和SR2: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_lib的i2c.c和si2c.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/