3. GPIO与基础外设驱动开发
I2C总线上设备怎么被找到?说白了就是靠地址。每个从设备都有一个唯一地址,主设备发起通信时,先把这个地址发到总线上,所有从设备都听着,只有地址匹配的那个才会应答。
这个机制看起来简单,但我做项目这么多年,因为地址问题翻车的案例真不少。地址冲突、保留地址误用、7位和10位模式搞混……嗯,咱们今天就把这些坑一个一个填平。
3.1 7位地址模式
标准I2C设备绝大多数都用7位地址。主设备发送起始条件后,先发7位地址 + 1位读写位(0表示写,1表示读)。
关键点:7位地址范围是0x00 ~ 0x7F,但实际可用地址只有112个。为什么?因为有一部分被保留做特殊用途了。
举个例子,你买了一个温度传感器,数据手册说它的7位地址是0x48。那你在代码里初始化时,就要把这个地址传给HAL库的I2C函数。像这样:
// STM32 HAL库示例:7位地址模式
#define TEMP_SENSOR_ADDR 0x48 << 1 // 左移1位,因为HAL库需要8位地址格式
uint8_t data[2];
HAL_I2C_Master_Transmit(&hi2c1, TEMP_SENSOR_ADDR, data, 2, 100);
这里有个容易搞混的地方:HAL库的地址参数是左移1位后的8位值。比如设备地址0x48,传给HAL库时要写成0x90。我刚开始用STM32时,就因为这个吃了亏,折腾了半天才发现是地址格式没对齐。
3.2 10位地址模式
7位地址最多112个设备,有些复杂系统不够用。所以I2C规范定义了10位地址模式。怎么区分是7位还是10位?看第一个字节的高5位:如果高5位是11110,就表示后面跟的是10位地址。
| 地址模式 | 第一个字节格式 | 地址范围 | 可用设备数 |
|---|---|---|---|
| 7位 | AAAAAAA + R/W | 0x00 ~ 0x7F | 112 |
| 10位 | 11110AA + R/W(前2位地址) | 0x000 ~ 0x3FF | 1024 |
10位地址的传输分两步:先发11110 + 地址高2位 + R/W,再发剩下的8位地址。从设备收到第一个字节后就知道自己是10位地址,会继续等第二个字节。
我的建议:除非你的系统真的需要挂上百个I2C设备,否则老老实实用7位地址。10位地址兼容性问题多,很多便宜的单片机甚至不支持。我在一个工业项目里试过10位地址,结果某款国产传感器死活不认,最后只能换方案。
3.3 广播地址与保留地址
I2C规范里有一些地址是保留的,不能分配给普通设备。最常见的两个:
- 广播地址(0x00):主设备发0x00,所有从设备都必须应答。一般用于主机想通知所有从设备「我要复位了」或者「大家注意听」。实际项目中很少用,因为容易造成总线混乱。
- 保留地址(0x01 ~ 0x07):这些地址留给特殊总线协议用,比如CBUS、SMBus等。普通I2C设备不能占用。
完整的保留地址列表如下:
| 地址(7位) | 用途 | 说明 |
|---|---|---|
| 0x00 | 广播/通用呼叫地址 | 所有从设备必须应答 |
| 0x01 | 起始字节 | 用于慢速设备同步 |
| 0x02 | CBUS地址 | 保留给CBUS协议 |
| 0x03 ~ 0x07 | 保留 | 未来扩展用途 |
| 0x78 ~ 0x7F | 10位地址前缀 | 高5位为11110 |
我曾经踩过的坑:有次选型一个EEPROM,手册说地址是0x50,结果焊上去发现和板上另一个设备冲突。查了半天才发现,那个「0x50」其实是7位地址,但另一个设备用的是8位格式(0xA0)。两个设备地址一样,总线直接罢工。所以,看数据手册时一定要确认地址是7位还是8位格式!
3.4 地址冲突检测
地址冲突是I2C调试中最常见的问题之一。两个设备地址一样,总线上的数据就乱了。怎么检测?我总结了一套实战方法:
- 读数据手册:每个I2C设备都有几个可选地址,一般通过引脚电平配置。比如A0/A1/A2引脚接VCC或GND,可以组合出8种地址。设计电路时就要规划好,确保同一总线上没有重复。
- 逐个扫描:写一个地址扫描函数,遍历0x01 ~ 0x7F,看哪些地址有应答。这是最直接的检测手段。
- 逻辑分析仪抓波形:如果扫描发现异常,用逻辑分析仪看实际波形。地址字节对不对?ACK位有没有拉低?一眼就能看出来。
下面是一个实用的地址扫描代码,我在调试时经常用:
// I2C地址扫描函数
void I2C_ScanAddress(I2C_HandleTypeDef *hi2c)
{
uint8_t found = 0;
printf("Scanning I2C bus...\n");
for (uint8_t addr = 0x01; addr < 0x7F; addr++)
{
// 尝试发送设备地址,看是否有ACK
if (HAL_I2C_IsDeviceReady(hi2c, addr << 1, 1, 10) == HAL_OK)
{
printf("Found device at 0x%02X\n", addr);
found++;
}
}
if (found == 0)
{
printf("No devices found!\n");
}
else
{
printf("Total %d device(s) found.\n", found);
}
}
避坑指南:扫描时跳过0x00(广播地址)和0x78~0x7F(10位地址前缀),这些地址即使有应答也不代表有设备。我曾经扫描到0x78有应答,兴奋了半天,结果发现是逻辑分析仪探头造成的假象。
3.5 地址模式选择与HAL库配置
在STM32 HAL库中,I2C地址模式通过初始化结构体配置:
I2C_InitTypeDef hi2c1_init;
hi2c1_init.OwnAddress1 = 0x32; // 本机7位地址
hi2c1_init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; // 7位模式
// 或者
hi2c1_init.AddressingMode = I2C_ADDRESSINGMODE_10BIT; // 10位模式
如果你用的是10位地址,HAL库的传输函数参数也要对应调整:
// 10位地址传输示例
#define DEVICE_10BIT_ADDR 0x01A3
// HAL库会自动处理10位地址的两次发送
HAL_I2C_Master_Transmit(&hi2c1, DEVICE_10BIT_ADDR, data, 2, 100);
我个人习惯:除非必须,否则一律用7位地址。10位地址在HAL库中虽然支持,但底层实现复杂,出问题后不好排查。你想想看,本来一个简单的读写操作,因为地址模式搞错,波形对不上,查半天都找不到原因——这种经历我至少有过三次。
3.6 知识结构图:I2C地址体系
下面这张图把I2C地址的完整体系画出来了,从地址分类到冲突检测,一目了然:
3.7 实战要点总结
- 地址格式要统一:数据手册写的是7位还是8位?HAL库需要左移1位。这两个搞错,通信必失败。
- 保留地址别占用:0x00、0x01~0x07、0x78~0x7F这些地址,普通设备不能用。用了就是给自己挖坑。
- 10位地址慎用:除非系统真的需要超过112个设备,否则别碰10位模式。兼容性问题会让你怀疑人生。
- 扫描函数是调试利器:写一个地址扫描函数,上电先跑一遍,看看总线上有哪些设备。这个习惯帮我省了无数排查时间。
最后提醒一句:地址冲突有时候不是硬件问题,而是代码里地址写错了。我曾经在一个项目里,明明设备地址是0x3C,代码里写成了0x3C << 1(0x78),结果扫描到了0x78还以为有设备,折腾了两天才发现是代码bug。所以,先确认代码,再怀疑硬件。