3. I2C从设备地址与寄存器映射
I2C从设备地址配置与内部寄存器映射是嵌入式驱动开发中连接硬件与软件的核心桥梁。本章将深入解析从设备地址的构成、配置方法、内部寄存器地址空间的布局策略,以及多字节读写协议在Linux内核中的实现方式。
3.1 从设备地址配置
每个I2C从设备在总线上拥有唯一的7位或10位地址。7位地址是最常见的配置方式,由器件固定部分和可配置引脚共同决定。
3.1.1 7位地址结构
| 位域 | 位数 | 说明 | 示例(0x48) |
|---|---|---|---|
| 固定器件ID | 4位 | 由芯片厂商分配,不可更改 | 1001 |
| 地址引脚 | 3位 | 通过A2/A1/A0引脚电平配置 | 000 |
| 读写位 | 1位 | 0=写,1=读(由主控在传输时附加) | 0/1 |
关键点: 7位地址左移1位后与读写位组合,构成8位传输地址。例如设备地址0x48,写操作发送0x90,读操作发送0x91。
3.1.2 设备树中地址配置
在Linux设备树中,通过reg属性指定从设备地址:
&i2c1 {
status = "okay";
clock-frequency = <100000>;
temperature_sensor: tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>; /* 7位从设备地址 */
#address-cells = <1>;
#size-cells = <0>;
};
eeprom: at24c02@50 {
compatible = "atmel,24c02";
reg = <0x50>; /* 地址引脚A2/A1/A0全部接地 */
pagesize = <8>;
size = <256>;
};
};
工程建议: 同一总线上多个同型号设备通过不同的地址引脚电平区分。设计PCB时预留上拉/下拉电阻位置,便于地址冲突时调整。
3.2 内部寄存器地址空间
I2C从设备内部通常包含多个寄存器,用于控制、状态读取和数据传输。寄存器地址空间的组织方式直接影响驱动设计的复杂度。
3.2.1 寄存器地址宽度
| 地址宽度 | 典型设备 | 地址范围 | 驱动处理方式 |
|---|---|---|---|
| 8位 | 温度传感器、ADC | 0x00 ~ 0xFF | 单字节地址发送 |
| 16位 | EEPROM、RTC | 0x0000 ~ 0xFFFF | 双字节地址发送(高字节在前) |
| 混合 | PMIC、音频编解码 | 分页+偏移 | 先选页,再发偏移地址 |
3.2.2 寄存器映射示例(以TMP102为例)
/* TMP102寄存器映射 */
#define TMP102_REG_TEMP 0x00 /* 温度寄存器(只读,2字节) */
#define TMP102_REG_CONFIG 0x01 /* 配置寄存器(读写,2字节) */
#define TMP102_REG_TLOW 0x02 /* 低温阈值(读写,2字节) */
#define TMP102_REG_THIGH 0x03 /* 高温阈值(读写,2字节) */
/* 配置寄存器位域定义 */
#define TMP102_CFG_SD BIT(8) /* 关断模式 */
#define TMP102_CFG_TM BIT(9) /* 比较/中断模式选择 */
#define TMP102_CFG_POL BIT(10) /* 极性 */
#define TMP102_CFG_F0 BIT(11) /* 故障队列位0 */
#define TMP102_CFG_F1 BIT(12) /* 故障队列位1 */
#define TMP102_CFG_R0 BIT(13) /* 分辨率位0 */
#define TMP102_CFG_R1 BIT(14) /* 分辨率位1 */
#define TMP102_CFG_OS BIT(15) /* 单次转换 */
常见陷阱: 部分设备寄存器地址在写入后会自动递增,而有些设备需要每次重新发送地址。务必查阅数据手册确认地址行为。
3.3 多字节读写协议
多字节读写是I2C通信的核心能力,允许一次传输连续访问多个寄存器,提高效率并保证数据一致性。
3.3.1 多字节写流程
3.3.2 多字节读流程
3.3.3 Linux内核多字节读写API
Linux I2C子系统提供标准化的多字节读写接口:
#include <linux/i2c.h>
/* 多字节写:向从设备寄存器地址写入多个字节 */
static int i2c_write_regs(struct i2c_client *client, u8 reg_addr,
const u8 *data, int len)
{
struct i2c_msg msg;
u8 buf[256];
int ret;
/* 构造发送缓冲区:寄存器地址 + 数据 */
buf[0] = reg_addr;
memcpy(&buf[1], data, len);
msg.addr = client->addr;
msg.flags = 0; /* 写标志 */
msg.len = len + 1;
msg.buf = buf;
ret = i2c_transfer(client->adapter, &msg, 1);
if (ret != 1) {
dev_err(&client->dev, "I2C write failed: %d\n", ret);
return ret < 0 ? ret : -EIO;
}
return 0;
}
/* 多字节读:从从设备寄存器地址读取多个字节 */
static int i2c_read_regs(struct i2c_client *client, u8 reg_addr,
u8 *data, int len)
{
struct i2c_msg msgs[2];
int ret;
/* 消息0:写寄存器地址 */
msgs[0].addr = client->addr;
msgs[0].flags = 0; /* 写 */
msgs[0].len = 1;
msgs[0].buf = ®_addr;
/* 消息1:读数据 */
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD; /* 读 */
msgs[1].len = len;
msgs[1].buf = data;
ret = i2c_transfer(client->adapter, msgs, 2);
if (ret != 2) {
dev_err(&client->dev, "I2C read failed: %d\n", ret);
return ret < 0 ? ret : -EIO;
}
return 0;
}
性能优化: 对于频繁访问的寄存器,可以使用
regmap框架封装I2C读写操作,自动处理缓存、批量传输和字节序转换。
3.4 地址空间映射实战案例
以AT24C02 EEPROM为例,展示完整的寄存器映射与多字节读写实现:
/* AT24C02 EEPROM驱动 - 页写入实现 */
#define AT24C02_PAGE_SIZE 8
#define AT24C02_DEV_SIZE 256
static int at24c02_write_page(struct i2c_client *client, u16 offset,
const u8 *data, int len)
{
struct i2c_msg msg;
u8 buf[AT24C02_PAGE_SIZE + 2]; /* 2字节地址 + 最多8字节数据 */
int ret;
if (len > AT24C02_PAGE_SIZE)
len = AT24C02_PAGE_SIZE;
/* 16位地址,高字节在前 */
buf[0] = (offset >> 8) & 0xFF;
buf[1] = offset & 0xFF;
memcpy(&buf[2], data, len);
msg.addr = client->addr;
msg.flags = 0;
msg.len = len + 2;
msg.buf = buf;
ret = i2c_transfer(client->adapter, &msg, 1);
if (ret != 1)
return ret < 0 ? ret : -EIO;
/* EEPROM写入需要5ms内部编程时间 */
msleep(5);
return 0;
}
/* 跨页写入处理 */
static int at24c02_write(struct i2c_client *client, u16 offset,
const u8 *data, size_t count)
{
int ret;
size_t written = 0;
int page_offset;
while (written < count) {
/* 计算当前页内剩余空间 */
page_offset = offset % AT24C02_PAGE_SIZE;
int chunk = min_t(int, AT24C02_PAGE_SIZE - page_offset,
count - written);
ret = at24c02_write_page(client, offset,
&data[written], chunk);
if (ret)
return ret;
written += chunk;
offset += chunk;
}
return 0;
}
边界处理: EEPROM页写入不能跨页边界,必须手动计算每页剩余空间并分次写入。忽略此规则会导致数据覆盖或写入失败。
3.5 地址映射调试技巧
| 调试场景 | 常用命令/工具 | 排查要点 |
|---|---|---|
| 设备地址探测 | i2cdetect -y <bus> |
确认设备是否在预期地址响应 |
| 寄存器读写验证 | i2cget / i2cset |
逐字节验证寄存器映射是否正确 |
| 时序分析 | 逻辑分析仪 / 示波器 | 检查起始条件、地址、ACK/NACK时序 |
| 内核调试 | i2c_transfer 返回值 |
-5(EIO)表示无应答,-6(ENXIO)表示设备不存在 |
核心原则: I2C从设备地址与寄存器映射是驱动开发的“硬件契约”。设备树中的地址必须与原理图一致,寄存器偏移必须与数据手册严格对应。任何偏差都会导致通信失败或数据错误。