前言
7 月份做电赛准备的时候,需要把一块 GW 八路灰度传感器的驱动移植到 TI 的 MSPM0 单片机上。当时手头的头文件里写着默认地址:
#define GW_GRAY_ADDR_DEF 0x4C写 I²C 初始化和收发函数的时候,我突然犯了嘀咕:传给 TI 驱动库的地址,到底是直接传 0x4C,还是要先左移一位变成 0x98?
以前写 STM32 HAL 库的时候,大家习惯了顺手把 7 位地址左移一位再塞进函数里;结果换到 MSPM0 上如果还照着老习惯写,地址就被左移了两次,总线上死活读不到应答。趁着整理那份头文件,我把 I²C 地址的几种写法和头文件里的宏好好理了一遍。
重要
核心结论
- TI MSPM0 DriverLib(如
DL_I2C_setTargetAddress):函数内部已经帮你做了位偏移,直接传 7 位地址0x4C,千万别自己左移。- STM32 HAL 库(如
HAL_I2C_Master_Transmit):函数注释明确要求调用者先把 7 位地址左移一位(即传入0x4C << 1)。
7 位设备地址与 8 位读写地址
I²C 标准使用的是 7 位从机地址,但在总线上实际发送第一个字节的时候,高 7 位放地址,最低位(第 0 位)用来表示读写方向(0 是写,1 是读):
总线上的地址字节 = (7 位设备地址 << 1) | 读写位如果这块灰度传感器的 7 位地址是 0x4C,算下来就会出现三个数字:
| 写法 | 数值 | 二进制 | 意思 |
|---|---|---|---|
| 7 位地址 | 0x4C | 100 1100 | 传感器本身的设备地址 |
| 8 位写地址 | 0x98 | 1001 1000 | 左移 1 位 + 写标志(0) |
| 8 位读地址 | 0x99 | 1001 1001 | 左移 1 位 + 读标志(1) |
很多淘宝模块手册写得比较随意,有的给 7 位地址,有的直接把 0x98 叫作“设备地址”;用逻辑分析仪抓波形的时候,软件里也有“按 7 位显示”和“按 8 位显示”的开关(参考 TI MSPM0 I²C 说明)。
为了不让自己隔几天又看糊涂,我在自己的公共头文件里干脆把宏名写成了 ADDR7:
#define GW_GRAY_ADDR7 0x4CU不同平台的地址参数约定
1. TI MSPM0 DriverLib
去翻 MSPM0 SDK 的 dl_i2c.h 源码 就能看到,设置目标从机地址的函数叫 DL_I2C_setTargetAddress。它往寄存器里写的时候自己会做移位,所以我们直接传 GW_GRAY_ADDR7 就行:
DL_I2C_setControllerAddressingMode(
I2C_INST, DL_I2C_CONTROLLER_ADDRESSING_MODE_7_BIT);
DL_I2C_setTargetAddress(I2C_INST, GW_GRAY_ADDR7);说明
提示 有些旧帖子里会出现
DL_I2C_setControllerTargetAddress这个函数名,实际写代码时以自己工程里 SysConfig 和 SDK 头文件里的声明为准。
2. STM32 HAL 库
而在 STM32 的传统 HAL 库里(见 STM32 HAL I²C 源码注释),DevAddress 参数要求传入左移一位之后的值:
uint16_t hal_addr = (uint16_t)(GW_GRAY_ADDR7 << 1);最低位的读写方向位 HAL 库自己会在收发函数里处理,不用我们手动传 0x99。
比较省心的做法是:传感器驱动的头文件里只定义 0x4C(7 位地址),是否需要左移一位,统一放到最底层的 MCU I²C 读写封装函数里处理。这样以后把驱动从 STM32 搬到 MSPM0 时,上层代码一行都不用改。
传感器协议命令常量
当时拿到的厂家头文件里定义了这几个命令常量:
| 宏名称 | 数值 | 头文件里的说明 |
|---|---|---|
GW_GRAY_PING | 0xAA | 连通性测试命令 |
GW_GRAY_PING_OK | 0x66 | 正常应答返回值 |
GW_GRAY_DIGITAL_MODE | 0xDD | 开关量(数字量)读取模式 |
GW_GRAY_ANALOG_MODE | 0xB0 | 连续读取模拟量模式 |
GW_GRAY_ANALOG(n) | 0xB0 + (n) | 读取第 1~8 路单通道模拟量 |
GW_GRAY_ANALOG_CHANNEL_ENABLE | 0xCE | 通道使能设置 |
GW_GRAY_FIRMWARE | 0xC1 | 读取固件版本号 |
GW_GRAY_REBOOT | 0xC0 | 软件重启模块 |
另外头文件里还有一个归一化命令 0xCF,注释标明了只有特定固件版本之后才支持。需要注意,光看这份头文件只能知道命令字本身,像发完命令后要不要延时、读模拟量返回几个字节,上板写收发时还要对着模块手册和实际抓出来的波形确认。
通道位操作与命令封装
1. 开关量通道位提取
原来的头文件里用了一个宏来取某一路探头的开关状态:
#define GET_NTH_BIT(sensor_value, nth_bit) \
(((sensor_value) >> ((nth_bit) - 1)) & 0x01)因为传感器外壳上的探头是按 1 到 8 标号的,而 C 语言移位是从 0 到 7 算的,所以宏里写了 (nth_bit) - 1。但如果调用的时候不小心传了个 0 进去,就会算出 -1 移位(C 语言里的未定义行为)。
把它改成一个带参数范围检查的 static inline 小函数要安全得多:
#include <stdint.h>
static inline int
gw_gray_get_channel(uint8_t value, unsigned channel, uint8_t *out)
{
if (out == 0 || channel < 1U || channel > 8U)
return -1;
*out = (uint8_t)((value >> (channel - 1U)) & 1U);
return 0;
}2. 单通道模拟量命令构造
同理,单通道模拟量命令 0xB0 + n 里的通道号 n 也是 1~8(0xB0 本身是连续模式命令),写成内联函数顺便把通道范围拦一下,免得误传 0 变成了切模式:
static inline int
gw_gray_analog_command(unsigned channel, uint8_t *out)
{
if (out == 0 || channel < 1U || channel > 8U)
return -1;
*out = (uint8_t)(0xB0U + channel);
return 0;
}硬件联调步骤
把驱动往新板子上搬的时候,不用一上来就把所有循迹算法全接上:
- 先拿万用表确认供电、共地和 SDA/SCL 上拉正常;
- 先只发一个
GW_GRAY_PING(0xAA),看看能不能收到 ACK 并读回0x66——这一步通了,就说明 7 位地址没多移位、引脚复用也没配错; - 最后再去测
0xDD开关量读取和0xB0模拟量读取,同时在底层收发函数里处理好超时和 NACK 返回值,别在断线的时候拿上一次的旧数据继续跑小车控制。