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 多字节写流程

I2C多字节写协议时序 起始条件 从地址+写 ACK 寄存器地址 ACK 数据字节1 ACK 停止条件 更多字节... 说明: 1. 主控发送起始条件后,依次发送从设备地址(写方向)、寄存器地址、数据字节。 2. 每个字节后从设备回复ACK,数据字节可连续发送,地址自动递增。

3.3.2 多字节读流程

I2C多字节读协议时序(组合格式) 起始条件 从地址+写 ACK 寄存器地址 ACK 重复起始 从地址+读 ACK 数据字节1 ACK 数据字节N NACK 停止条件 说明: 1. 先写寄存器地址,然后发送重复起始条件,切换为读方向。 2. 最后一个数据字节主控回复NACK,表示读结束,随后发送停止条件。

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 = &reg_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从设备地址与寄存器映射是驱动开发的“硬件契约”。设备树中的地址必须与原理图一致,寄存器偏移必须与数据手册严格对应。任何偏差都会导致通信失败或数据错误。