前言
写 I2C 驱动时,几乎所有人都会碰到一行代码:7bit地址 << 1。很多人只记住这是 I2C 的固定写法,却不知道这是一套通用的位域打包思想,I2C 寻址字节只是其中一个特例。
位域打包,就是把多个短小的二进制字段,拼装到同一个字节里,用来组成总线命令、寻址帧。
本文从 I2C 寻址字节切入,讲清楚 (field << n) | flag 的通用范式,顺带对比 SPI 那种读写位在最高位的布局,避开移位方向的坑。
一、打包通用范式
packed_val = (field_1 << offset_1) | (field_2 << offset_2);
规则:
offset= 该字段最低位在最终打包结果中的 bit 编号(bit0 是最低位);- 所有字段统一使用左移
<<,把字段抬到目标 bit 区间; - 要求各个字段的 bit 范围互不重叠,才能安全按位或
|; - 打包的目标:组装成总线要求的一个字节 / 字,发给外设。
核心一句话:哪一块字段要放在更高 bit,就给这块字段更大的左移偏移。
二、案例 1|标准 I2C 寻址字节:R/W 在 bit0
协议规定单字节布局(MSB 先行):
| bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 |
|---|---|---|---|---|---|---|---|
| A6 | A5 | A4 | A3 | A2 | A1 | A0 | R/W |
-
7bit 地址 A6~A0,目标区间 bit7~bit1,最低位偏移 = 1 →
addr7 << 1 -
R/W 标记,目标区间 bit0,最低位偏移 = 0 →
rw << 0uint8_t i2c_addr_byte = (addr7 << 1) | rw;
示例:addr7 = 0x50 (0b1010000)
- 写:
(0x50 << 1) | 0 = 0xA0 - 读:
(0x50 << 1) | 1 = 0xA1
三、案例 2|SPI 风格命令字节:R/W 在 bit7
大量 SPI ADC、Flash 命令格式:
| bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 |
|---|---|---|---|---|---|---|---|
| R/W | A6 | A5 | A4 | A3 | A2 | A1 | A0 |
-
7bit 地址放在低 7 位,偏移 = 0,不需要移位;
-
R/W 标记放在 bit7,偏移 = 7 →
rw << 7uint8_t spi_cmd_byte = addr7 | (rw << 7);
示例:addr7 = 0x50
- 写:
0x50 | (0 << 7) = 0x50 - 读:
0x50 | (1 << 7) = 0xD0
高频误区:RW 在高位,不需要把地址右移。打包永远是把字段左移就位,不是右移。
四、拓展:多字段打包示例
单字节内分配:bit7~bit5(3bit 命令 cmd),bit4~bit0(5bit 参数 param)
uint8_t pack = (cmd << 5) | (param << 0);
五、工程避坑:STM32 HAL I2C 不对称地址
HAL_I2C_Mem_Read(..., DevAddr,...):主机发送寻址字节,需要你手动打包,7bit 地址 <<1 | rwhi2c1.Init.OwnAddress1:STM32 作为 I2C 从机的匹配地址,寄存器内部右对齐存放,直接填原始 7bit 地址,HAL 内部自动打包
很多 I2C 驱动 bug,根源就是混淆两处的打包责任。
六、打包篇小结
- 打包 = 拼积木,为发送做准备;
- 统一公式:
(字段 << 偏移) | (另一个字段 << 偏移); - I2C 左移 1 只是范式的一个特例,不是独立规则;
- 打包只用左移,打包不用右移。
配套阅读:《位域解包:寄存器标志位拆分方法》