4. Zephyr I2C API概览:设备树绑定、I2C控制器配置、API函数详解

在驱动I2C触摸屏之前,必须掌握Zephyr RTOS中I2C子系统的核心编程模型。本章将深入剖析从设备树绑定到API调用的完整链路,帮助你在实战中准确配置控制器、解析设备树节点,并熟练使用 i2c_write、i2c_read 和 i2c_transfer 三大核心函数。

4.1 设备树绑定:I2C设备节点的声明与解析

Zephyr使用设备树(Devicetree)描述硬件拓扑。对于I2C触摸屏,需要在设备树中完成两件事:使能I2C控制器和声明触摸屏从设备节点。

4.1.1 I2C控制器节点配置

以STM32系列为例,I2C1控制器的设备树节点通常位于 soc.dtsi 中。开发者只需在板级 .dts 或 .overlay 文件中设置 status = "okay" 并指定引脚复用。

/* 板级设备树片段:使能I2C1并配置引脚 */
&i2c1 {
    status = "okay";
    clock-frequency = <I2C_BITRATE_FAST>;  /* 400 kHz */
    pinctrl-0 = <&i2c1_sda_pb7 &i2c1_scl_pb6>;
    pinctrl-names = "default";
};

4.1.2 触摸屏从设备节点

在I2C控制器节点内部,添加触摸屏子节点,必须包含 compatible、reg(7位从机地址)和 label 属性。

/* 在i2c1节点内添加触摸屏 */
&i2c1 {
    status = "okay";
    clock-frequency = <I2C_BITRATE_FAST>;

    touch_ft5336: ft5336@38 {
        compatible = "focaltech,ft5336";
        reg = <0x38>;               /* 7位I2C地址 */
        label = "TOUCH";
        irq-gpios = <&gpioc 13 GPIO_ACTIVE_LOW>;
        reset-gpios = <&gpioc 0 GPIO_ACTIVE_LOW>;
    };
};
关键提示: reg 属性值必须与触摸屏数据手册中的7位从机地址一致。地址错误将导致所有I2C通信失败。使用 i2c scan shell命令可验证地址是否正确。

4.1.3 设备树绑定宏与API映射

Zephyr提供 DT_NODELABEL 和 DT_INST 宏在C代码中引用设备树节点。推荐使用 DT_INST 系列宏实现驱动实例化。

宏 用途 示例
DT_NODELABEL(label) 通过节点标签获取节点标识符 DT_NODELABEL(touch_ft5336)
DT_INST(idx, compat) 按兼容字符串和实例索引获取节点 DT_INST(0, focaltech_ft5336)
DT_INST_REG_ADDR(idx) 获取实例的I2C从机地址 DT_INST_REG_ADDR(0)
DT_INST_PROP(idx, label) 获取实例的label属性值 DT_INST_PROP(0, label)
设备树节点 → 驱动实例映射流程 板级 .dts / .overlay ft5336@38 { ... } 编译 DT_INST 宏展开 获取 reg / label / gpios 驱动实例化 struct device *dev 💡 映射关键点 1. compatible 属性匹配驱动注册时的兼容字符串。 2. reg 属性通过 DT_INST_REG_ADDR(idx) 获取,作为 i2c_write/read 的从机地址。 3. 中断和复位GPIO通过 DT_INST_PROP 或 DT_INST_GPIO 宏解析。

4.2 I2C控制器配置:初始化与参数设置

在应用层或驱动初始化阶段,需要获取I2C控制器设备指针并配置通信参数。Zephyr的I2C API通过 struct device 句柄操作控制器。

4.2.1 获取I2C控制器设备

使用 DEVICE_DT_GET 或 device_get_binding 获取控制器设备结构体指针。

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>

/* 方法1:通过设备树标签获取(推荐) */
#define I2C1_NODE DT_NODELABEL(i2c1)
const struct device *i2c_dev = DEVICE_DT_GET(I2C1_NODE);

/* 方法2:通过label字符串获取(不推荐,运行时开销) */
/* const struct device *i2c_dev = device_get_binding("I2C_1"); */

if (!device_is_ready(i2c_dev)) {
    printk("I2C controller not ready\n");
    return -ENODEV;
}

4.2.2 配置I2C通信参数

i2c_configure 函数用于设置控制器的工作模式、速度和寻址模式。触摸屏通常使用标准模式(100 kHz)或快速模式(400 kHz),7位寻址。

uint32_t i2c_cfg = I2C_SPEED_SET(I2C_SPEED_FAST) | I2C_MODE_CONTROLLER;
int ret = i2c_configure(i2c_dev, i2c_cfg);
if (ret != 0) {
    printk("I2C configure failed: %d\n", ret);
    return ret;
}
配置宏 值 说明
I2C_SPEED_STANDARD 1 标准模式,100 kHz
I2C_SPEED_FAST 2 快速模式,400 kHz
I2C_SPEED_FAST_PLUS 3 快速+模式,1 MHz
I2C_MODE_CONTROLLER 0 控制器模式(主模式)
I2C_MODE_TARGET 1 目标模式(从模式,极少用于触摸屏)
⚠️ 配置陷阱: 某些触摸屏对I2C时序敏感,过高的时钟频率可能导致通信不稳定。如果触摸屏在快速模式下出现数据错误,尝试降级到标准模式(100 kHz)进行调试。

4.3 I2C API函数详解:i2c_write / i2c_read / i2c_transfer

Zephyr提供三级I2C API:简单读写、带寄存器地址的读写和组合传输。触摸屏驱动主要使用后两者。

4.3.1 i2c_write:向从设备发送数据

函数原型:

int i2c_write(const struct device *dev, const uint8_t *buf, uint32_t num_bytes, uint16_t addr);
  • dev:I2C控制器设备指针。
  • buf:待发送数据缓冲区。
  • num_bytes:发送字节数。
  • addr:从设备7位I2C地址(无需左移,API内部处理)。
  • 返回值:0成功,负值错误码。

触摸屏应用示例——向寄存器0x01写入配置值0x02:

uint8_t cmd[2] = {0x01, 0x02};
int ret = i2c_write(i2c_dev, cmd, sizeof(cmd), TOUCH_I2C_ADDR);
if (ret != 0) {
    printk("I2C write failed: %d\n", ret);
}

4.3.2 i2c_read:从从设备读取数据

函数原型:

int i2c_read(const struct device *dev, uint8_t *buf, uint32_t num_bytes, uint16_t addr);

注意:i2c_read 执行一次完整的I2C读传输,不包含前导的寄存器地址写入。对于需要先指定寄存器地址再读取数据的触摸屏,必须使用 i2c_write_read 或 i2c_transfer。

❌ 错误用法: 直接调用 i2c_read 读取触摸屏坐标数据,而不先写入寄存器地址,将导致读取到错误的数据。

4.3.3 i2c_transfer:组合传输(核心函数)

函数原型:

int i2c_transfer(const struct device *dev, struct i2c_msg *msgs, uint8_t num_msgs, uint16_t addr);

i2c_transfer 支持在一次I2C总线操作中执行多个消息段(message),每个消息段可以是写或读,且支持重复起始条件(Restart)。这是触摸屏驱动中最常用的函数。

4.3.4 i2c_msg结构体与组合传输示例

struct i2c_msg {
    uint8_t      *buf;   /* 数据缓冲区 */
    uint32_t     len;    /* 数据长度 */
    uint8_t      flags;  /* 消息标志 */
};

/* 常用标志 */
#define I2C_MSG_WRITE   0x00    /* 写消息 */
#define I2C_MSG_READ    0x01    /* 读消息 */
#define I2C_MSG_STOP    0x02    /* 传输结束后发送停止条件 */
#define I2C_MSG_RESTART 0x04    /* 发送重复起始条件 */

触摸屏读取坐标的典型组合传输:先写寄存器地址,再读数据。

uint8_t reg_addr = 0x02;       /* 触摸点状态寄存器 */
uint8_t touch_data[6];         /* 读取6字节触摸数据 */

struct i2c_msg msgs[2];

/* 消息0:写寄存器地址 */
msgs[0].buf = &reg_addr;
msgs[0].len = 1;
msgs[0].flags = I2C_MSG_WRITE;

/* 消息1:读触摸数据,使用重复起始,最后发停止 */
msgs[1].buf = touch_data;
msgs[1].len = sizeof(touch_data);
msgs[1].flags = I2C_MSG_READ | I2C_MSG_STOP;

int ret = i2c_transfer(i2c_dev, msgs, 2, TOUCH_I2C_ADDR);
if (ret == 0) {
    /* 成功读取触摸数据,解析 touch_data */
}
i2c_transfer 组合传输:写寄存器地址 + 读触摸数据 I2C总线时序: START Addr+W ACK RegAddr ACK RESTART Addr+R ACK Data[0..5] STOP i2c_msg 数组对应关系: msgs[0] flags=I2C_MSG_WRITE buf=&reg_addr, len=1 → 写寄存器地址 msgs[1] flags=I2C_MSG_READ | I2C_MSG_STOP buf=touch_data, len=6 → 读取触摸数据 💡 组合传输优势 1. 一次总线操作完成写寄存器地址和读数据,避免总线竞争。 2. 重复起始条件(RESTART)保证操作的原子性,其他I2C设备不会中途介入。 3. 相比两次独立调用 i2c_write + i2c_read,效率更高且更可靠。

4.4 三大API函数对比与选型指南

函数 适用场景 是否支持组合传输 触摸屏驱动使用频率
i2c_write 仅向从设备发送数据,无需读取响应 否 低(配置寄存器时偶尔使用)
i2c_read 仅从从设备读取数据,无需前导地址 否 极低(几乎不用)
i2c_transfer 需要先写寄存器地址再读数据,或执行复杂I2C事务 是(支持多消息、重复起始) 高(核心函数)
实战建议: 在触摸屏驱动中,统一使用 i2c_transfer 实现所有I2C通信。即使只需要写一个寄存器,也可以构造单消息的 i2c_msg 数组调用 i2c_transfer,保持代码风格一致,便于后续扩展。

4.5 错误处理与调试技巧

I2C API返回负值错误码,常见错误码及排查方向:

  • -EIO (负5):I2C总线通信错误。检查硬件连接、上拉电阻、从机地址是否正确。
  • -ENXIO (负6):从设备无应答。确认设备是否上电、地址是否匹配、控制器是否使能。
  • -EAGAIN (负11):总线忙,重试即可。可在驱动中增加重试机制。
  • -EINVAL (负22):参数无效。检查 i2c_msg 标志位、缓冲区长度是否合理。
/* 带重试的I2C传输封装 */
#define I2C_RETRY_CNT  3

static int i2c_transfer_with_retry(const struct device *dev,
                                    struct i2c_msg *msgs,
                                    uint8_t num_msgs,
                                    uint16_t addr)
{
    int ret;
    for (int i = 0; i < I2C_RETRY_CNT; i++) {
        ret = i2c_transfer(dev, msgs, num_msgs, addr);
        if (ret == 0) {
            return 0;
        }
        if (ret == -EAGAIN) {
            k_sleep(K_MSEC(1));
            continue;
        }
        /* 非重试错误,直接返回 */
        break;
    }
    return ret;
}