I2C(Inter-Integrated Circuit)总线只有两根线:
总线上可以挂载多个设备,每个设备都有唯一的7 位地址。STM32 通常作为主机(Master),外设作为从机(Slave)。
I2C 的 SCL 和 SDA 都采用开漏输出(Open-Drain),必须外接上拉电阻(典型值 4.7kΩ):
多个设备可以同时向总线输出:只要有任何一个设备输出低电平,总线就是低电平——这就是线与(Wire-AND)逻辑。上拉电阻的阻值决定了总线上升时间,阻值越小速度越快但功耗越大。
上图:I2C 总线硬件连接(开漏输出 + 上拉电阻)(来源:STM32入门教程 PPT 第128页)
对比 UART:
空闲状态:SCL = 高,SDA = 高
起始条件(Start Condition):SCL 高电平时,SDA 从高→低跳变
SCL ──┬──────
│
SDA ──┘───
停止条件(Stop Condition):SCL 高电平时,SDA 从低→高跳变
SCL ──┬──
│
SDA ────┘
数据传输:SCL 低电平时 SDA 可变化,SCL 高电平时 SDA 保持稳定(接收方在 SCL 上升沿采样)。
应答(ACK):每传输完 8 位数据,接收方在第 9 个 SCL 周期拉低 SDA 表示应答。
上图:I2C 起始条件(Start)与停止条件(Stop)时序(来源:STM32入门教程 PPT 第129页)
标准的 I2C 写数据帧:
起始位 | 7位地址 | R/W(0) | ACK | 数据1 | ACK | ... | 数据N | ACK/NACK | 停止位
标准的 I2C 读数据帧:
起始位 | 7位地址 | R/W(1) | ACK | 数据1 | ACK | ... | 数据N | NACK | 停止位
注意读操作结束后,主机需要发送 NACK(即释放 SDA)告诉从机停止发送,再发停止位。
| 模式 | 最大波特率 |
|---|---|
| 标准模式 (SM) | 100 kbit/s |
| 快速模式 (FM) | 400 kbit/s |
| 快速增强模式 (FM+) | 1 Mbit/s |
| 高速模式 (HS) | 3.4 Mbit/s |
STM32F103 的 I2C 外设支持标准模式和快速模式。
上图:STM32 I2C 模块框图(来源:STM32入门教程 PPT 第141页)
上图:STM32 I2C 内部结构(时钟控制、数据寄存器、移位寄存器)(来源:STM32入门教程 PPT 第142页)
以 I2C1 为例:
| 默认引脚 | 重映射后 |
|---|---|
| PB6 (SCL) | PB8 (SCL) |
| PB7 (SDA) | PB9 (SDA) |
引脚模式:复用开漏输出(GPIO_Mode_AF_OD)
为什么用复用 + 开漏?
I2C1 挂在 APB1 总线上(注意不是 APB2):
// 1. 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
// 2. 复位 I2C 模块(类似重启)
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE); // 按下复位
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE); // 松开复位
I2C_InitTypeDef I2C_InitStructure;
// 1. 波特率:400kHz
I2C_InitStructure.I2C_ClockSpeed = 400000;
// 2. 模式:标准 I2C(非 SMBus)
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
// 3. 占空比(快速模式下可选)
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 2:1 或 16:9
// 4. 本机地址(本设备作为从机时才用,主机可填任意值)
I2C_InitStructure.I2C_OwnAddress1 = 0x00;
// 5. 应答使能
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
// 6. 7 位地址模式
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE); // 闭合总开关
void MyI2C_Init(void) {
// 1. 开启 GPIOB 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 2. 开启 AFIO 时钟(重映射需要)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 3. 重映射 I2C1 到 PB8/PB9
GPIO_PinRemapConfig(GPIO_Remap_I2C1, ENABLE);
// 4. 初始化 PB8(SCL) 和 PB9(SDA) 为复用开漏输出
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 5. 开启 I2C1 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
// 6. 复位 I2C1
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE);
// 7. 初始化 I2C1
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_ClockSpeed = 400000;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = 0x00;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &I2C_InitStructure);
// 8. 使能 I2C1
I2C_Cmd(I2C1, ENABLE);
}
使用 STM32CubeMX 生成的 HAL 库代码,I2C 初始化通过 I2C_HandleTypeDef 结构体完成:
/* HAL库 — I2C 句柄定义与初始化 */
I2C_HandleTypeDef hi2c1;
void MX_I2C1_Init(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000; // 400kHz
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 占空比 2:1
hi2c1.Init.OwnAddress1 = 0; // 本机地址(主机模式不用)
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
}
/* HAL 库的 MSP 初始化(引脚与时钟)由 HAL_I2C_MspInit 回调完成 */
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (hi2c->Instance == I2C1) {
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_I2C1_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; // PB6=SCL, PB7=SDA
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
}
关键区别:
I2C_InitTypeDef + I2C_Init();HAL 库用 I2C_HandleTypeDef + HAL_I2C_Init()HAL_I2C_MspInit() 统一管理引脚时钟和 GPIO 初始化HAL_I2C_Mem_Write/Read 要求传入7 位原始地址(不含 R/W 位),如 MPU6050 传入 0x68;而标准库中传的是左对齐的 0xD0关键部件:
| 标志位 | 含义 |
|---|---|
| BUSY (SR2) | 总线忙标志,=1 时总线正在通信 |
| SB (SR1) | 起始位已发送 |
| ADDR (SR1) | 地址已发送且收到 ACK(寻址成功) |
| AF (SR1) | 应答失败(NACK received) |
| TXE (SR1) | 发送数据寄存器空,可写入下一个数据 |
| BTF (SR1) | Byte Transfer Finished,字节传输完成(移位寄存器和 DR 均空) |
| RXNE (SR1) | 接收数据寄存器非空,有数据可读 |
/**
* 通过 I2C 发送多个字节
* @param I2Cx I2C 外设地址(I2C1 或 I2C2)
* @param Addr 从机 7 位地址(左对齐)
* @param pData 要发送的数据数组
* @param Size 要发送的字节数
* @return 0=成功, 1=寻址失败, 2=数据被拒收
*/
int MyI2C_SendBytes(I2C_TypeDef* I2Cx, uint8_t Addr, uint8_t *pData, uint16_t Size) {
// === 阶段一:等待总线空闲 ===
while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY));
// === 阶段二:发送起始位 ===
I2C_GenerateSTART(I2Cx, ENABLE);
while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB)); // 等待起始位发送完成
// === 阶段三:寻址阶段 ===
I2C_ClearFlag(I2Cx, I2C_FLAG_AF); // 先清除 AF 标志位
I2C_SendData(I2Cx, Addr & 0xFE); // 地址 + R/W=0(写)
// 等待寻址结果
while (1) {
if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR)) // 寻址成功
break;
if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF)) { // 寻址失败
I2C_GenerateSTOP(I2Cx, ENABLE);
return 1;
}
}
// 清除 ADDR 标志位(必须:先读 SR1,再读 SR2)
I2C_ReadRegister(I2Cx, I2C_Register_SR1);
I2C_ReadRegister(I2Cx, I2C_Register_SR2);
// === 阶段四:发送数据 ===
for (uint16_t i = 0; i < Size; i++) {
// 等待可以发送下一个字节
while (1) {
if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF)) { // 数据被拒收
I2C_GenerateSTOP(I2Cx, ENABLE);
return 2;
}
if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_TXE)) // DR 空,可以写入
break;
}
I2C_SendData(I2Cx, pData[i]); // 写入发送数据寄存器
}
// 等待最后一个字节发送完成
while (1) {
if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF)) {
I2C_GenerateSTOP(I2Cx, ENABLE);
return 2;
}
if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_BTF)) // 整个字节传输完成
break;
}
// === 阶段五:发送停止位 ===
I2C_GenerateSTOP(I2Cx, ENABLE);
return 0; // 成功
}
关键点:
0111 1000,高 7 位 0111 100 即 0x3C 左移一位uint8_t Command[] = {0x00, 0x8D, 0x14, 0xAF, 0xA5}; // OLED 开启命令
int main(void) {
OLED_Init(); // 内部调用 MyI2C_Init()
MyI2C_SendBytes(I2C1, 0x78, Command, 5);
// OLED 屏幕点亮
while (1);
}
OLED 地址是 0x78,这里传的是已经左对齐的地址(最低位为 R/W 位,函数内做了 & 0xFE 处理)。
HAL 库将 I2C 写操作封装为两个高级函数,无需手动操作 EV5/EV6/EV7 事件标志位:
/* HAL库 — I2C 主机发送(直接发送数据,无寄存器地址) */
// 原型:HAL_I2C_Master_Transmit(&hi2c1, DevAddress, pData, Size, Timeout)
// DevAddress:7位从机地址左移1位(即含R/W位的8位地址)
// pData:要发送的数据缓冲区
// Size:字节数
// Timeout:超时时间(ms)
HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(
&hi2c1,
0xD0, // MPU6050 地址(左对齐,含R/W位)
Command, // 数据缓冲区
5, // 字节数
100 // 超时100ms
);
if (status != HAL_OK) {
Error_Handler(); // 处理错误
}
/* HAL库 — I2C 写寄存器(Mem Write,带寄存器地址) */
// 原型:HAL_I2C_Mem_Write(&hi2c1, DevAddress, MemAddress, MemAddSize, pData, Size, Timeout)
// DevAddress:7位从机地址左移1位
// MemAddress:寄存器地址
// MemAddSize:寄存器地址长度(8位或16位)
HAL_StatusTypeDef ret = HAL_I2C_Mem_Write(
&hi2c1,
0xD0, // 从机地址
0x6B, // 寄存器地址(PWR_MGMT_1)
I2C_MEMADD_SIZE_8BIT, // 寄存器地址为8位
(uint8_t[]){0x01}, // 写入的数据
1, // 1个字节
100
);
/* HAL库 — 同时写多个寄存器 */
uint8_t configData[] = {0x6B, 0x01, 0x6C, 0x00, 0x19, 0x09};
HAL_I2C_Master_Transmit(&hi2c1, 0xD0, configData, 6, 100);
HAL 库内部自动处理了:
读操作的流程和写类似,但有三个关键区别:
Addr | 0x01)需要重复起始位(Repeated Start):先写寄存器地址,再切换为读模式
uint8_t MyI2C_ReadByte(I2C_TypeDef* I2Cx, uint8_t Addr, uint8_t RegAddr) {
uint8_t Data;
// 阶段 1-2:起始位
while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY));
I2C_GenerateSTART(I2Cx, ENABLE);
while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB));
// 阶段 3:写从机地址(写方向)—— 先写寄存器地址
I2C_ClearFlag(I2Cx, I2C_FLAG_AF);
I2C_SendData(I2Cx, Addr & 0xFE);
while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR));
I2C_ReadRegister(I2Cx, I2C_Register_SR1);
I2C_ReadRegister(I2Cx, I2C_Register_SR2);
// 发送寄存器地址
while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_TXE));
I2C_SendData(I2Cx, RegAddr);
while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_BTF));
// 阶段 4:重复起始位 —— 切换方向
I2C_GenerateSTART(I2Cx, ENABLE);
while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB));
// 阶段 5:写从机地址(读方向)
I2C_ClearFlag(I2Cx, I2C_FLAG_AF);
I2C_SendData(I2Cx, Addr | 0x01);
while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR));
I2C_ReadRegister(I2Cx, I2C_Register_SR1);
I2C_ReadRegister(I2Cx, I2C_Register_SR2);
// 阶段 6:接收数据 + 发 NACK
I2C_AcknowledgeConfig(I2Cx, DISABLE); // 不发 ACK
while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_RXNE));
Data = I2C_ReceiveData(I2Cx);
// 阶段 7:停止位
I2C_GenerateSTOP(I2Cx, ENABLE);
I2C_AcknowledgeConfig(I2Cx, ENABLE); // 恢复 ACK
return Data;
}
/* HAL库 — I2C 主机接收(直接读取从机数据,无寄存器地址) */
// 原型:HAL_I2C_Master_Receive(&hi2c1, DevAddress, pData, Size, Timeout)
uint8_t rxBuffer[6];
HAL_StatusTypeDef status = HAL_I2C_Master_Receive(
&hi2c1,
0xD0, // 从机地址(左对齐)
rxBuffer, // 接收缓冲区
6, // 接收6个字节
100
);
/* HAL库 — I2C 读寄存器(Mem Read,先写寄存器地址,再读数据) */
// 原型:HAL_I2C_Mem_Read(&hi2c1, DevAddress, MemAddress, MemAddSize, pData, Size, Timeout)
// 内部自动完成:Write(RegAddr) → Repeated Start → Read
uint8_t data;
HAL_StatusTypeDef ret = HAL_I2C_Mem_Read(
&hi2c1,
0xD0, // 从机地址
0x3B, // 起始寄存器地址(ACCEL_XOUT_H)
I2C_MEMADD_SIZE_8BIT, // 8位寄存器地址
&data, // 接收缓冲区
1, // 读取1个字节
100
);
/* HAL库 — 连续读取多个寄存器(MPU6050 获取全部6轴数据) */
uint8_t mpuBuffer[14]; // ACCEL(6) + TEMP(2) + GYRO(6) = 14 bytes
HAL_I2C_Mem_Read(&hi2c1, 0xD0, 0x3B, I2C_MEMADD_SIZE_8BIT,
mpuBuffer, 14, 100);
// 解析数据
int16_t AccX = (mpuBuffer[0] << 8) | mpuBuffer[1];
int16_t AccY = (mpuBuffer[2] << 8) | mpuBuffer[3];
int16_t AccZ = (mpuBuffer[4] << 8) | mpuBuffer[5];
int16_t GyroX = (mpuBuffer[8] << 8) | mpuBuffer[9];
int16_t GyroY = (mpuBuffer[10] << 8) | mpuBuffer[11];
int16_t GyroZ = (mpuBuffer[12] << 8) | mpuBuffer[13];
HAL 库的 HAL_I2C_Mem_Read 内部自动执行:Start → 地址+W → 寄存器地址 → Repeated Start → 地址+R → NACK → Stop,完全封装了重复起始位和 NACK 发送逻辑。
如果单片机没有硬件 I2C 外设,或者引脚不够灵活,可以用 GPIO 模拟 I2C 时序。
PB10 (SCL) 和 PB11 (SDA) 配置为开漏输出(也可用推挽输出模拟):
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
#define I2C_SCL_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_10)
#define I2C_SCL_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_10)
#define I2C_SDA_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_11)
#define I2C_SDA_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_11)
#define I2C_SDA_READ() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11)
void MyI2C_Start(void) {
I2C_SDA_HIGH();
I2C_SCL_HIGH();
Delay_us(4);
I2C_SDA_LOW(); // SCL 高电平时 SDA 下降沿 → 起始位
Delay_us(4);
I2C_SCL_LOW(); // 拉低 SCL 准备传输数据
}
void MyI2C_Stop(void) {
I2C_SDA_LOW();
I2C_SCL_HIGH();
Delay_us(4);
I2C_SDA_HIGH(); // SCL 高电平时 SDA 上升沿 → 停止位
Delay_us(4);
}
void MyI2C_SendByte(uint8_t Byte) {
for (uint8_t i = 0; i < 8; i++) {
if (Byte & 0x80) I2C_SDA_HIGH();
else I2C_SDA_LOW();
Byte <<= 1;
I2C_SCL_HIGH(); // SCL 高电平采样
Delay_us(4);
I2C_SCL_LOW();
Delay_us(4);
}
}
uint8_t MyI2C_ReceiveByte(void) {
uint8_t Byte = 0;
I2C_SDA_HIGH(); // 释放 SDA,让从机控制
for (uint8_t i = 0; i < 8; i++) {
Byte <<= 1;
I2C_SCL_HIGH();
Delay_us(2);
if (I2C_SDA_READ()) Byte |= 0x01;
I2C_SCL_LOW();
Delay_us(2);
}
return Byte;
}
void MyI2C_SendAck(uint8_t AckBit) {
if (AckBit) I2C_SDA_HIGH(); // NACK
else I2C_SDA_LOW(); // ACK
I2C_SCL_HIGH();
Delay_us(4);
I2C_SCL_LOW();
I2C_SDA_HIGH(); // 释放 SDA
Delay_us(4);
}
uint8_t MyI2C_ReceiveAck(void) {
uint8_t AckBit;
I2C_SDA_HIGH(); // 释放 SDA,让从机拉低
I2C_SCL_HIGH();
Delay_us(4);
AckBit = I2C_SDA_READ(); // 0=ACK, 1=NACK
I2C_SCL_LOW();
Delay_us(4);
return AckBit;
}
MPU6050 是常用的六轴传感器(三轴加速度 + 三轴陀螺仪),I2C 从机地址为 0xD0(含 R/W 位,即 7 位地址 0x68 左移一位)。
#define MPU6050_ADDRESS 0xD0 // 含 R/W 位
// 写寄存器
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data) {
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS); // 地址 + 写方向
MyI2C_ReceiveAck();
MyI2C_SendByte(RegAddress); // 寄存器地址
MyI2C_ReceiveAck();
MyI2C_SendByte(Data); // 写入数据
MyI2C_ReceiveAck();
MyI2C_Stop();
}
// 读寄存器
uint8_t MPU6050_ReadReg(uint8_t RegAddress) {
uint8_t Data;
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS); // 写方向
MyI2C_ReceiveAck();
MyI2C_SendByte(RegAddress); // 指定寄存器地址
MyI2C_ReceiveAck();
MyI2C_Start(); // 重复起始位
MyI2C_SendByte(MPU6050_ADDRESS | 0x01); // 读方向
MyI2C_ReceiveAck();
Data = MyI2C_ReceiveByte();
MyI2C_SendAck(1); // 发 NACK
MyI2C_Stop();
return Data;
}
// 初始化 MPU6050
void MPU6050_Init(void) {
MyI2C_Init();
// 电源管理:取消睡眠模式,选择 X 轴陀螺仪为时钟源
MPU6050_WriteReg(0x6B, 0x01); // PWR_MGMT_1
// 使能所有轴
MPU6050_WriteReg(0x6C, 0x00); // PWR_MGMT_2
// 采样率:1kHz
MPU6050_WriteReg(0x19, 0x09); // SMPLRT_DIV
// DLPF 配置
MPU6050_WriteReg(0x1A, 0x06); // CONFIG
// 陀螺仪满量程 ±2000°/s
MPU6050_WriteReg(0x1B, 0x18); // GYRO_CONFIG
// 加速度计满量程 ±16g
MPU6050_WriteReg(0x1C, 0x18); // ACCEL_CONFIG
}
// 读取原始数据
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ) {
uint8_t DataH, DataL;
DataH = MPU6050_ReadReg(0x3B); // ACCEL_XOUT_H
DataL = MPU6050_ReadReg(0x3C);
*AccX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(0x3D); // ACCEL_YOUT_H
DataL = MPU6050_ReadReg(0x3E);
*AccY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(0x3F); // ACCEL_ZOUT_H
DataL = MPU6050_ReadReg(0x40);
*AccZ = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(0x43); // GYRO_XOUT_H
DataL = MPU6050_ReadReg(0x44);
*GyroX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(0x45); // GYRO_YOUT_H
DataL = MPU6050_ReadReg(0x46);
*GyroY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(0x47); // GYRO_ZOUT_H
DataL = MPU6050_ReadReg(0x48);
*GyroZ = (DataH << 8) | DataL;
}
如果使用 STM32 硬件 I2C 外设 + 标准库,MPU6050 驱动通过 I2C 事件标志位控制时序(EV5 = SB起始已发送,EV6 = ADDR寻址成功,EV7 = RXNE数据可读):
/* 标准库 — 硬件 I2C MPU6050 驱动(基于 I2C2,PB10/PB11) */
#define MPU6050_ADDRESS 0xD0
/* 等待 I2C 事件,带超时 */
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout = 10000;
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
{
Timeout--;
if (Timeout == 0) break;
}
}
/* 硬件 I2C 写寄存器:发送起始位 → EV5 → 地址+W → EV6 → 数据 → EV8 → EV8_2 → 停止 */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); // EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // EV6
I2C_SendData(I2C2, RegAddress);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); // EV8
I2C_SendData(I2C2, Data);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // EV8_2
I2C_GenerateSTOP(I2C2, ENABLE);
}
/* 硬件 I2C 读寄存器:起始 → EV5 → 地址+W → EV6 → 寄存器地址 → EV8_2 → 重复起始 → EV5 → 地址+R → EV6 → 数据 → EV7 → 停止 */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); // EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // EV6
I2C_SendData(I2C2, RegAddress);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // EV8_2
I2C_GenerateSTART(I2C2, ENABLE); // 重复起始位
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); // EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); // EV6
I2C_AcknowledgeConfig(I2C2, DISABLE); // 最后一字节前失能ACK
I2C_GenerateSTOP(I2C2, ENABLE); // 提前请求停止
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); // EV7
Data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2, ENABLE); // 恢复ACK
return Data;
}
void MPU6050_Init(void)
{
/* GPIO 初始化 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* I2C2 初始化 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_ClockSpeed = 50000;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_OwnAddress1 = 0x00;
I2C_Init(I2C2, &I2C_InitStructure);
I2C_Cmd(I2C2, ENABLE);
/* MPU6050 配置 */
MPU6050_WriteReg(0x6B, 0x01);
MPU6050_WriteReg(0x6C, 0x00);
MPU6050_WriteReg(0x19, 0x09);
MPU6050_WriteReg(0x1A, 0x06);
MPU6050_WriteReg(0x1B, 0x18);
MPU6050_WriteReg(0x1C, 0x18);
}
事件与标志位对应关系:
| I2C 事件 | 标志位 | 说明 |
|---|---|---|
I2C_EVENT_MASTER_MODE_SELECT |
SB | EV5:起始位已发送 |
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED |
ADDR | EV6:地址已发送且收到 ACK(发送模式) |
I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED |
ADDR | EV6:地址已发送且收到 ACK(接收模式) |
I2C_EVENT_MASTER_BYTE_TRANSMITTING |
TXE | EV8:数据寄存器空,可发送下一字节 |
I2C_EVENT_MASTER_BYTE_TRANSMITTED |
BTF | EV8_2:字节传输完成(DR + 移位寄存器均空) |
I2C_EVENT_MASTER_BYTE_RECEIVED |
RXNE | EV7:收到一个字节,数据可读 |
如果使用 HAL 库,MPU6050 驱动可简化为直接调用 HAL_I2C_Mem_Write/Read,不需要自己实现软件 I2C 时序:
/* HAL库 — MPU6050 驱动(基于 HAL_I2C_Mem_Write/Read) */
#define MPU6050_ADDR_7BIT 0x68 // 7位原始地址(不含R/W位)
#define MPU6050_ADDR 0xD0 // 左对齐地址(含R/W位),供 HAL 库函数使用
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, RegAddress,
I2C_MEMADD_SIZE_8BIT, &Data, 1, 100);
}
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, RegAddress,
I2C_MEMADD_SIZE_8BIT, &Data, 1, 100);
return Data;
}
void MPU6050_Init(void)
{
MX_I2C1_Init(); // 初始化硬件 I2C
MPU6050_WriteReg(0x6B, 0x01); // PWR_MGMT_1
MPU6050_WriteReg(0x6C, 0x00); // PWR_MGMT_2
MPU6050_WriteReg(0x19, 0x09); // SMPLRT_DIV
MPU6050_WriteReg(0x1A, 0x06); // CONFIG
MPU6050_WriteReg(0x1B, 0x18); // GYRO_CONFIG (±2000°/s)
MPU6050_WriteReg(0x1C, 0x18); // ACCEL_CONFIG (±16g)
}
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
uint8_t buf[14];
HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, 0x3B,
I2C_MEMADD_SIZE_8BIT, buf, 14, 100);
*AccX = (buf[0] << 8) | buf[1];
*AccY = (buf[2] << 8) | buf[3];
*AccZ = (buf[4] << 8) | buf[5];
*GyroX = (buf[8] << 8) | buf[9];
*GyroY = (buf[10] << 8) | buf[11];
*GyroZ = (buf[12] << 8) | buf[13];
}
对比标准库方案,HAL 库方案的优点:
HAL_I2C_Mem_Read 一次性完成写寄存器地址 + 重复起始 + 读数据HAL_I2C_Mem_Write/Read 的 DevAddress 参数传左对齐地址(含 R/W 位,如 0xD0)| 对比 | 硬件 I2C | 软件 I2C |
|---|---|---|
| CPU 占用 | 低(硬件控制时序) | 高(CPU 逐位模拟) |
| 引脚灵活性 | 需指定引脚 | 任意 GPIO |
| 速度 | 可达 400kHz | 取决于延时精度 |
| 抗干扰 | 好(硬件滤波) | 差 |
| 编程复杂度 | 较高(寄存器/标志位多) | 简单直观 |
| 适用场景 | 高速、复杂系统 | 低速、原型验证、引脚不足 |
I2C 总线有时会出现 SDA 一直被拉低的情况。可能原因:
解决方法:强制发送 9 个时钟脉冲,让从机释放 SDA。
I2C 总线上必须接上拉电阻。如果未接或阻值过大,SCL/SDA 波形上升沿过缓,通信不可靠。建议阻值 4.7kΩ(标准模式)到 1kΩ(快速模式)。
多个 I2C 设备使用相同地址会导致冲突。MPU6050 的 AD0 引脚可改变地址:
10-1 软件I2C读写MPU6050\Hardware\MyI2C.c — 软件 I2C GPIO 模拟实现10-1 软件I2C读写MPU6050\Hardware\MPU6050.c — MPU6050 驱动10-1 软件I2C读写MPU6050\User\main.c — 主程序示例14-[STM32 HAL库][I2C]基础知识 — I2C 总线协议15-[STM32 HAL库][I2C]简单数据收发实验 — OLED 使用 I2C 通信本笔记综合整理自三套 STM32 教程:
- [STM32 HAL库] I2C 基础知识 / 数据收发实验
- 铁头山羊 I2C 系列(4.1~4.8)
- 江协科技 I2C 系列(10-1~10-5)