一、I2C总线基础回顾
I2C总线特点
-
两根线:SCL(串行时钟线)和SDA(串行数据线)
-
多主多从:支持多个主设备和从设备
-
地址寻址:每个从设备有唯一的7位或10位地址
-
半双工通信:同一时间只能单向传输
I2C通信帧结构
开始信号 + 从机地址(7位) + R/W位 + ACK + 数据 + ACK + ... + 停止信号
二、从机地址:设备的"身份证"
1. 从机地址的作用
从机地址是I2C总线上识别特定设备的唯一标识。当主设备发起通信时,它会先发送从机地址,只有地址匹配的从设备才会响应。
2. 地址格式
7位地址模式(最常用)
| 7位地址 | R/W |
|---------|-----|
| A6-A0 | 0/1 |
-
地址范围:0x08 到 0x77(0x00-0x07和0x78-0x7F为保留地址)
-
R/W位:0表示写操作,1表示读操作
-
实际发送:左移1位 + R/W位
cs
// 示例:从机地址为0x50,写操作
// 实际发送:0x50 << 1 | 0 = 0xA0
uint8_t slave_addr = 0x50;
uint8_t write_addr = (slave_addr << 1) | 0; // 0xA0
uint8_t read_addr = (slave_addr << 1) | 1; // 0xA1
10位地址模式
开始信号 + 11110+A9+A8+R/W + ACK + A7-A0 + ACK + 数据...
3. 常见的I2C设备地址
| 设备类型 | 典型地址范围 | 常见地址 |
|---|---|---|
| EEPROM | 0x50-0x57 | 0x50 |
| 实时时钟RTC | 0x68 | 0x68 |
| 温度传感器 | 0x48, 0x4E | 0x48 |
| 加速度计 | 0x1D, 0x53 | 0x1D |
| 显示屏 | 0x3C, 0x3D | 0x3C |
4. 地址冲突与解决方法
硬件配置
许多I2C设备有地址选择引脚(A0, A1, A2),通过连接VCC或GND改变地址:
cs
// MPU6050地址配置示例
// AD0引脚接地:地址 = 0x68
// AD0引脚接VCC:地址 = 0x69
#define MPU6050_ADDR_AD0_LOW 0x68
#define MPU6050_ADDR_AD0_HIGH 0x69
软件方案
cs
// 地址扫描程序
void I2C_ScanDevices(void)
{
printf("Scanning I2C devices...\n");
for(uint8_t addr = 0x08; addr <= 0x77; addr++)
{
HAL_StatusTypeDef status;
// 尝试与设备通信
status = HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 3, 10);
if(status == HAL_OK)
{
printf("Device found at address: 0x%02X\n", addr);
}
}
}
三、寄存器地址:设备内部的"房间号"
1. 寄存器地址的作用
对于大多数I2C设备,内部都有多个寄存器来存储配置参数、状态信息或测量数据。寄存器地址就是访问这些内部存储单元的指针。
2. 寄存器地址类型
单字节地址(8位)
大多数设备使用8位寄存器地址:
cs
// 典型的读写时序
// 写操作:开始 + 从机地址(写) + ACK + 寄存器地址 + ACK + 数据 + ACK + 停止
// 读操作:开始 + 从机地址(写) + ACK + 寄存器地址 + ACK + 重复开始 + 从机地址(读) + ACK + 读数据 + NACK + 停止
双字节地址(16位)
某些大容量设备使用16位寄存器地址:
// 需要发送两个字节的地址
// 高字节在前,低字节在后
3. 寄存器地址映射示例
以MPU6050加速度计为例:
| 寄存器地址 | 寄存器名称 | 功能描述 |
|---|---|---|
| 0x3B | ACCEL_XOUT_H | 加速度计X轴高字节 |
| 0x3C | ACCEL_XOUT_L | 加速度计X轴低字节 |
| 0x3D | ACCEL_YOUT_H | 加速度计Y轴高字节 |
| 0x3E | ACCEL_YOUT_L | 加速度计Y轴低字节 |
| 0x6B | PWR_MGMT_1 | 电源管理寄存器1 |
| 0x75 | WHO_AM_I | 设备ID寄存器 |
四、完整通信流程分析
1. 写入单个寄存器
cs
// 向MPU6050的PWR_MGMT_1寄存器(0x6B)写入0x00(唤醒设备)
HAL_StatusTypeDef MPU6050_WriteReg(uint8_t reg_addr, uint8_t data)
{
uint8_t buffer[2];
buffer[0] = reg_addr; // 寄存器地址
buffer[1] = data; // 要写入的数据
// 发送:从机地址(写) + 寄存器地址 + 数据
return HAL_I2C_Master_Transmit(&hi2c1,
MPU6050_ADDR << 1, // 从机地址(写模式)
buffer,
2, // 两个字节
100); // 超时时间
}
通信时序解析:
cs
主设备发送:
[开始] [0xD0] [ACK] [0x6B] [ACK] [0x00] [ACK] [停止]
解释:
0xD0 = 0x68 << 1 | 0 = 从机地址(写)
0x6B = 寄存器地址
0x00 = 写入的数据
2. 读取单个寄存器
cs
// 从MPU6050的WHO_AM_I寄存器(0x75)读取设备ID
HAL_StatusTypeDef MPU6050_ReadReg(uint8_t reg_addr, uint8_t *data)
{
// 第一步:发送寄存器地址(写模式)
HAL_StatusTypeDef status;
status = HAL_I2C_Master_Transmit(&hi2c1,
MPU6050_ADDR << 1,
®_addr,
1,
100);
if(status != HAL_OK) return status;
// 第二步:读取数据(读模式)
return HAL_I2C_Master_Receive(&hi2c1,
(MPU6050_ADDR << 1) | 1, // 从机地址(读模式)
data,
1,
100);
}
通信时序解析:
第一阶段(设置寄存器指针):
开始\] \[0xD0\] \[ACK\] \[0x75\] \[ACK\] \[停止
第二阶段(读取数据):
开始\] \[0xD1\] \[ACK\] \[数据\] \[NACK\] \[停止
解释:
0xD0 = 0x68 << 1 | 0 = 从机地址(写)
0x75 = 寄存器地址
0xD1 = 0x68 << 1 | 1 = 从机地址(读)
3. 连续读取多个寄存器
cs
// 从MPU6050读取加速度计数据(6个连续寄存器)
HAL_StatusTypeDef MPU6050_ReadAccel(int16_t *accel_data)
{
uint8_t buffer[6];
uint8_t reg_addr = 0x3B; // ACCEL_XOUT_H寄存器地址
// 发送起始寄存器地址
HAL_I2C_Master_Transmit(&hi2c1,
MPU6050_ADDR << 1,
®_addr,
1,
100);
// 连续读取6个字节
HAL_I2C_Master_Receive(&hi2c1,
(MPU6050_ADDR << 1) | 1,
buffer,
6,
100);
// 合并为16位数据
accel_data[0] = (buffer[0] << 8) | buffer[1]; // X轴
accel_data[1] = (buffer[2] << 8) | buffer[3]; // Y轴
accel_data[2] = (buffer[4] << 8) | buffer[4]; // Z轴
return HAL_OK;
}
五、实际应用:OLED显示屏驱动
1. SSD1306 OLED显示屏
设备地址
-
通常为0x3C或0x3D
-
由SA0引脚决定:接地=0x3C,接VCC=0x3D
寄存器/命令结构
SSD1306没有传统意义上的寄存器,而是通过发送命令和数据来控制:
cs
#define OLED_ADDRESS 0x3C
#define OLED_CMD_MODE 0x00 // 命令模式
#define OLED_DATA_MODE 0x40 // 数据模式
// 向OLED发送命令
void OLED_WriteCommand(uint8_t cmd)
{
uint8_t buffer[2];
buffer[0] = OLED_CMD_MODE; // 控制字节:Co=0, D/C#=0
buffer[1] = cmd; // 命令
HAL_I2C_Master_Transmit(&hi2c1,
OLED_ADDRESS << 1,
buffer,
2,
100);
}
// 向OLED发送数据
void OLED_WriteData(uint8_t data)
{
uint8_t buffer[2];
buffer[0] = OLED_DATA_MODE; // 控制字节:Co=0, D/C#=1
buffer[1] = data; // 显示数据
HAL_I2C_Master_Transmit(&hi2c1,
OLED_ADDRESS << 1,
buffer,
2,
100);
}
// 初始化OLED
void OLED_Init(void)
{
// 一系列初始化命令
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(0x8D); // 电荷泵设置
OLED_WriteCommand(0x14);
OLED_WriteCommand(0x20); // 内存地址模式
OLED_WriteCommand(0x00);
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(0x40);
OLED_WriteCommand(0xA4); // 显示全部点亮
OLED_WriteCommand(0xA6); // 正常显示
OLED_WriteCommand(0xAF); // 开启显示
}
六、EEPROM读写示例
AT24C02 EEPROM(256字节)
cs
#define EEPROM_ADDR 0x50 // 从机地址
// 向EEPROM写入数据
void EEPROM_WriteByte(uint16_t mem_addr, uint8_t data)
{
uint8_t buffer[3];
// AT24C02只有256字节,地址范围为0x00-0xFF
buffer[0] = (uint8_t)(mem_addr & 0xFF); // 内存地址
buffer[1] = data; // 数据
HAL_I2C_Master_Transmit(&hi2c1,
EEPROM_ADDR << 1,
buffer,
2, // 注意:只有地址和数据两个字节
100);
// EEPROM写入需要时间,等待5ms
HAL_Delay(5);
}
// 从EEPROM读取数据
uint8_t EEPROM_ReadByte(uint16_t mem_addr)
{
uint8_t data;
// 先发送要读取的地址
uint8_t addr_byte = (uint8_t)(mem_addr & 0xFF);
HAL_I2C_Master_Transmit(&hi2c1,
EEPROM_ADDR << 1,
&addr_byte,
1,
100);
// 然后读取数据
HAL_I2C_Master_Receive(&hi2c1,
(EEPROM_ADDR << 1) | 1,
&data,
1,
100);
return data;
}
// 连续写入多个字节(页写入)
void EEPROM_WritePage(uint16_t start_addr, uint8_t *data, uint8_t len)
{
uint8_t buffer[9]; // 1字节地址 + 最多8字节数据(AT24C02页大小为8字节)
if(len > 8) len = 8; // 确保不超过页大小
buffer[0] = (uint8_t)(start_addr & 0xFF); // 起始地址
// 复制数据
for(uint8_t i = 0; i < len; i++)
{
buffer[i + 1] = data[i];
}
HAL_I2C_Master_Transmit(&hi2c1,
EEPROM_ADDR << 1,
buffer,
len + 1,
100);
HAL_Delay(5); // 等待写入完成
}
七、常见问题与调试技巧
1. I2C通信失败排查
cs
void I2C_Debug_Info(void)
{
printf("I2C状态检查:\n");
// 检查总线是否繁忙
if(HAL_I2C_GetState(&hi2c1) == HAL_I2C_STATE_BUSY)
{
printf("I2C总线繁忙\n");
}
// 检查设备是否响应
for(uint8_t i = 0; i < 3; i++)
{
HAL_StatusTypeDef status;
status = HAL_I2C_IsDeviceReady(&hi2c1, 0x50 << 1, 1, 10);
if(status == HAL_OK)
{
printf("设备0x50响应正常\n");
break;
}
else
{
printf("设备0x50无响应,尝试 %d/3\n", i+1);
}
}
// 检查错误标志
if(__HAL_I2C_GET_FLAG(&hi2c1, I2C_FLAG_BERR))
{
printf("总线错误\n");
__HAL_I2C_CLEAR_FLAG(&hi2c1, I2C_FLAG_BERR);
}
if(__HAL_I2C_GET_FLAG(&hi2c1, I2C_FLAG_ARLO))
{
printf("仲裁丢失\n");
__HAL_I2C_CLEAR_FLAG(&hi2c1, I2C_FLAG_ARLO);
}
if(__HAL_I2C_GET_FLAG(&hi2c1, I2C_FLAG_AF))
{
printf("应答失败\n");
__HAL_I2C_CLEAR_FLAG(&hi2c1, I2C_FLAG_AF);
}
}
2. 上拉电阻的重要性
// I2C总线需要外部上拉电阻
// 典型值:4.7kΩ 到 10kΩ
// SCL和SDA线都需要上拉到VCC
3. 地址确认技巧
cs
// 通过读取WHO_AM_I等设备ID寄存器确认设备
uint8_t MPU6050_CheckID(void)
{
uint8_t id;
MPU6050_ReadReg(0x75, &id); // WHO_AM_I寄存器
if(id == 0x68) // MPU6050的ID是0x68
{
printf("MPU6050检测成功,ID=0x%02X\n", id);
return 1;
}
else
{
printf("设备ID错误:0x%02X\n", id);
return 0;
}
}
八、STM32 I2C硬件配置
使用CubeMX配置
cs
// 典型的I2C配置参数
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 100kHz标准模式
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0; // 主设备地址设为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;
软件模拟I2C(GPIO模拟)
cs
// 当硬件I2C有问题时,可以使用GPIO模拟
void I2C_Soft_Init(void)
{
// 配置SCL和SDA为开漏输出
GPIO_InitTypeDef GPIO_InitStruct = {0};
// SCL引脚
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// SDA引脚
GPIO_InitStruct.Pin = GPIO_PIN_7;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
// 软件I2C起始信号
void I2C_Soft_Start(void)
{
SDA_HIGH();
SCL_HIGH();
Delay_us(5);
SDA_LOW();
Delay_us(5);
SCL_LOW();
}
总结
从机地址 vs 寄存器地址
| 特性 | 从机地址 | 寄存器地址 |
|---|---|---|
| 作用 | 选择总线上的设备 | 选择设备内部的存储位置 |
| 大小 | 7位或10位 | 通常8位,有时16位 |
| 发送时机 | 每次通信开始 | 读写数据之前 |
| 唯一性 | 总线上唯一 | 设备内部唯一 |
关键要点
-
从机地址是设备选择的关键:确保总线上没有地址冲突
-
寄存器地址是数据访问的指针:理解设备的寄存器映射表
-
正确配置R/W位:写操作=0,读操作=1
-
注意ACK/NACK:正确响应是通信成功的基础
-
时序很重要:特别是重复开始条件和停止条件
最佳实践
-
始终检查设备的响应(ACK)
-
为每个I2C设备编写专用的驱动程序
-
实现设备检测和错误处理机制
-
使用逻辑分析仪或示波器调试复杂的I2C通信问题
-
注意上拉电阻的阻值选择,平衡速度和功耗
掌握从机地址和寄存器地址的概念,你就掌握了I2C通信的核心。在实际项目中,建议从简单的EEPROM读写开始,逐步尝试更复杂的传感器和显示屏驱动,这样能更好地理解I2C通信的细节和技巧。