目录

I2C 控制器驱动

IIC设备树节点

i2c_imx_probe 函数

i2c_imx_algo 结构体

i2c_imx_xfer 函数

I2C 设备驱动编写流程

 I2C 设备信息描述

I2C 设备数据收发处理流程

i2c_transfer 函数

i2c_msg 结构体

i2c_master_send函数

i2c_master_recv函数


在上一讲内容里,imx6ull-驱动开发篇42——Linux I2C 驱动框架简介,我们了解了 Linux 下的 I2C 驱动框架,重点分为 I2C 适配器驱动和 I2C 设备驱动。

I2C 设备驱动是需要用户根据不同的 I2C 设备去编写,而 I2C 适配器驱动一般都是 SOC 厂商去编写的,比如 NXP 就编写好了 I.MX6U 的I2C 适配器驱动。

I2C 控制器驱动

IIC设备树节点

打开我们自己移植的linux源码路径下的 /arch/arm/boot/dts/imx6ull.dtsi,找到 I.MX6U 的 I2C1 控制器节点,节点内容如下所示:

这里i2c1节点的compatible属性值有两个:“fsl,imx6ul-i2c”和“fsl,imx21- i2c”,在 Linux 源码中搜索这两个字符串即可找到对应的驱动文件。

I.MX6U 的 I2C 适配器驱动文件为 drivers/i2c/busses/i2c-imx.c,在此文件中有如下内容:

static struct platform_device_id imx_i2c_devtype[] = {
    {
        .name = "imx1-i2c",
        .driver_data = (kernel_ulong_t)&imx1_i2c_hwdata,
    }, {
        .name = "imx21-i2c",
        .driver_data = (kernel_ulong_t)&imx21_i2c_hwdata,
    }, {
        /* sentinel */
    }
};
MODULE_DEVICE_TABLE(platform, imx_i2c_devtype);

static const struct of_device_id i2c_imx_dt_ids[] = {
    { .compatible = "fsl,imx1-i2c", .data = &imx1_i2c_hwdata, },
    { .compatible = "fsl,imx21-i2c", .data = &imx21_i2c_hwdata, },
    { .compatible = "fsl,vf610-i2c", .data = &vf610_i2c_hwdata, },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, i2c_imx_dt_ids);

static struct platform_driver i2c_imx_driver = {
    .probe = i2c_imx_probe,
    .remove = i2c_imx_remove,
    .driver = {
        .name = DRIVER_NAME,
        .owner = THIS_MODULE,
        .of_match_table = i2c_imx_dt_ids,
        .pm = IMX_I2C_PM,
    },
    .id_table = imx_i2c_devtype,
};

static int __init i2c_adap_imx_init(void)
{
    return platform_driver_register(&i2c_imx_driver);
}
subsys_initcall(i2c_adap_imx_init);

static void __exit i2c_adap_imx_exit(void)
{
    platform_driver_unregister(&i2c_imx_driver);
}
module_exit(i2c_adap_imx_exit);

当设备和驱动匹配成功以后 ,i2c_imx_probe 函数就会执行, i2c_imx_probe 函数就会完成 I2C 适配器初始化工作。

i2c_imx_probe 函数

i2c_imx_probe 函数内容如下所示(有省略):

static int i2c_imx_probe(struct platform_device *pdev)
{
    // 1. 获取设备树匹配数据(如果存在)
    const struct of_device_id *of_id = of_match_device(i2c_imx_dt_ids, &pdev->dev);
    struct imx_i2c_struct *i2c_imx;
    struct resource *res;
    struct imxi2c_platform_data *pdata = dev_get_platdata(&pdev->dev);
    void __iomem *base;
    int irq, ret;
    dma_addr_t phy_addr;

    dev_dbg(&pdev->dev, "<%s>\n", __func__); // 调试日志

    // 2. 获取硬件资源
    irq = platform_get_irq(pdev, 0);          // 获取中断号
......
     // 获取 I2C1 控制器寄存器物理基地址,也就是 0X021A0000
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0); 
    base = devm_ioremap_resource(&pdev->dev, res); // 内存映射
    if (IS_ERR(base))
        return PTR_ERR(base);

    phy_addr = (dma_addr_t)res->start;        // 物理地址(用于DMA)

    // 3. 分配驱动数据结构
    i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL);
    if (!i2c_imx)
        return -ENOMEM;

    // 4. 初始化硬件数据(优先设备树,其次平台ID)
    if (of_id)
        i2c_imx->hwdata = of_id->data;       // 从设备树获取硬件配置
    else
        i2c_imx->hwdata = (struct imx_i2c_hwdata *)
            platform_get_device_id(pdev)->driver_data; // 传统平台ID方式

    // 5. 设置I2C适配器基础信息
    strlcpy(i2c_imx->adapter.name, pdev->name, sizeof(i2c_imx->adapter.name));
    i2c_imx->adapter.owner = THIS_MODULE;
    i2c_imx->adapter.algo = &i2c_imx_algo;   // 绑定算法(i2c_imx_algo实现master_xfer等)
    i2c_imx->adapter.dev.parent = &pdev->dev;
    i2c_imx->adapter.nr = pdev->id;          // 适配器编号
    i2c_imx->adapter.dev.of_node = pdev->dev.of_node;
    i2c_imx->base = base;                    // 寄存器基地址

    // 6. 时钟管理
    i2c_imx->clk = devm_clk_get(&pdev->dev, NULL); // 获取时钟
......
    ret = clk_prepare_enable(i2c_imx->clk);  // 启用时钟
.......

    // 7. 中断设置
    ret = devm_request_irq(&pdev->dev, irq, i2c_imx_isr,
                          IRQF_NO_SUSPEND, pdev->name, i2c_imx);
......

    // 8. 初始化等待队列(用于中断处理)
    init_waitqueue_head(&i2c_imx->queue);

    // 9. 设置适配器私有数据
    i2c_set_adapdata(&i2c_imx->adapter, i2c_imx);

    // 10. 配置时钟频率(优先设备树,其次平台数据)
    i2c_imx->bitrate = IMX_I2C_BIT_RATE;     // 默认速率 = 100K
    ret = of_property_read_u32(pdev->dev.of_node,
                              "clock-frequency", &i2c_imx->bitrate);
    if (ret < 0 && pdata && pdata->bitrate)
        i2c_imx->bitrate = pdata->bitrate;   // 使用平台数据覆盖

    // 11. 初始化硬件寄存器
    imx_i2c_write_reg(i2c_imx->hwdata->i2cr_ien_opcode ^ I2CR_IEN,
                     i2c_imx, IMX_I2C_I2CR); // 禁用中断
    imx_i2c_write_reg(i2c_imx->hwdata->i2sr_clr_opcode, 
                     i2c_imx, IMX_I2C_I2SR); // 清除状态寄存器

    // 12. 注册I2C适配器
    ret = i2c_add_numbered_adapter(&i2c_imx->adapter);
    if (ret < 0) {
        dev_err(&pdev->dev, "adapter registration failed\n");
        goto clk_disable;
    }

    // 13. 设置驱动私有数据
    platform_set_drvdata(pdev, i2c_imx);
    clk_disable_unprepare(i2c_imx->clk);     // 临时关闭时钟(后续使用时再开启)
......

    // 14. 可选DMA初始化
    i2c_imx_dma_request(i2c_imx, phy_addr);  // 如果支持DMA则配置

    return 0; // 成功返回

clk_disable:
    clk_disable_unprepare(i2c_imx->clk);     // 错误处理:关闭时钟
    return ret;
}

i2c_imx_probe 函数主要的工作就是两点:

  • 初始化 i2c_adapter,设置 i2c_algorithm 为 i2c_imx_algo,最后向 Linux 内核注册i2c_adapter。
  • 初始化 I2C1 控制器的相关寄存器。

i2c_imx_algo 结构体

i2c_imx_algo 包含 I2C1 适配器与 I2C 设备的通信函数 master_xfer,

i2c_imx_algo 结构体定义如下:

static struct i2c_algorithm i2c_imx_algo = {
    .master_xfer = i2c_imx_xfer,      // 标准I2C消息传输函数
    .functionality = i2c_imx_func,    // 查询适配器支持的功能
};

functionality用于返回此I2C适配器支持什么样的通信协议,在这里 functionality 就是 i2c_imx_func 函数.

i2c_imx_func 函数内容如下:

/**
 * i2c_imx_func - 声明I2C适配器支持的功能
 * @adapter: 关联的I2C适配器
 *
 * 返回值:I2C_FUNC_*标志的组合,指示硬件支持的功能
 */
static u32 i2c_imx_func(struct i2c_adapter *adapter)
{
    return I2C_FUNC_I2C |                // 支持标准I2C协议
           I2C_FUNC_SMBUS_EMUL |        // 支持SMBus基本命令模拟
           I2C_FUNC_SMBUS_READ_BLOCK_DATA; // 支持SMBus块读取(I2C_SMBUS_BLOCK_DATA)
}

i2c_imx_xfer 函数

i2c_imx_algo结构体里还注册了i2c_imx_xfer 函数,最终就是通过此函数来完成与 I2C 设备通信的。

i2c_imx_xfer 函数内容如下(有省略):

/**
 * i2c_imx_xfer - I2C 总线消息传输主函数
 * @adapter: I2C 适配器指针
 * @msgs: I2C 消息数组指针
 * @num: 消息数量
 * 
 * 返回值: 成功传输的消息数,错误时返回负的错误码
 */
static int i2c_imx_xfer(struct i2c_adapter *adapter,
                       struct i2c_msg *msgs, int num)
{
    unsigned int i, temp;
    int result;
    bool is_lastmsg = false;  // 标记是否为最后一条消息
    struct imx_i2c_struct *i2c_imx = i2c_get_adapdata(adapter);  // 获取适配器私有数据

    dev_dbg(&i2c_imx->adapter.dev, "<%s>\n", __func__);  // 调试信息

    /* 1. 启动 I2C 传输 */
    result = i2c_imx_start(i2c_imx);  // 发送起始条件
    if (result)
        goto fail0;  // 启动失败跳转到错误处理

    /* 2. 处理所有消息 */
    for (i = 0; i < num; i++) {
        /* 标记最后一条消息 */
        if (i == num - 1)
            is_lastmsg = true;

        /* 3. 非第一条消息需要发送重复起始条件 */
        if (i) {
            dev_dbg(&i2c_imx->adapter.dev, "<%s> repeated start\n", __func__);
            temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);  // 读取控制寄存器
            temp |= I2CR_RSTA;  // 设置重复起始位
            imx_i2c_write_reg(temp, i2c_imx, IMX_I2C_I2CR);  // 写回控制寄存器
            result = i2c_imx_bus_busy(i2c_imx, 1);  // 检查总线是否繁忙
            if (result)
                goto fail0;  // 总线忙则跳转到错误处理
        }

        dev_dbg(&i2c_imx->adapter.dev, "<%s> transfer message: %d\n", __func__, i);
......

        /* 4. 根据消息标志选择读/写操作 */
        if (msgs[i].flags & I2C_M_RD) {
            /* 读操作 */
            result = i2c_imx_read(i2c_imx, &msgs[i], is_lastmsg);
        } else {
            /* 写操作:根据数据长度选择 DMA 或寄存器方式 */
            if (i2c_imx->dma && msgs[i].len >= DMA_THRESHOLD) {
                result = i2c_imx_dma_write(i2c_imx, &msgs[i]);  // DMA 写
            } else {
                result = i2c_imx_write(i2c_imx, &msgs[i]);  // 寄存器写
            }
        }

        if (result)
            goto fail0;  // 传输失败跳转到错误处理
    }

fail0:
    /* 5. 停止 I2C 传输 */
    i2c_imx_stop(i2c_imx);  // 发送停止条件

    /* 6. 输出调试信息并返回结果 */
    dev_dbg(&i2c_imx->adapter.dev, "<%s> exit with: %s: %d\n",
           __func__, (result < 0) ? "error" : "success msg",
           (result < 0) ? result : num);

    return (result < 0) ? result : num;  // 返回传输结果
}
  • 调用 i2c_imx_start 函数,开启 I2C 通信。
  • 如果是从 I2C 设备读数据的话,就调用 i2c_imx_read 函数。
  • 向 I2C 设备写数据,如果要用 DMA 的话就使用 i2c_imx_dma_write 函数来完成写数据。如果不使用 DMA 的话就使用 i2c_imx_write 函数完成写数据。
  • I2C 通信完成以后,调用 i2c_imx_stop 函数停止 I2C 通信。

I2C 设备驱动编写流程

 I2C 设备信息描述

使用设备树的时候,添加 I2C 设备信息就在设备树里创建相应的节点。

比如 NXP 官方的 EVK 开发板在 I2C1 上接了 mag3110 这个磁力计芯片,因此必须在 i2c1 节点下创建 mag3110 子节点,然后在这个子节点内描述 mag3110 这个芯片的相关信息,如图:

I2C 设备节点的创建重点是 compatible 属性和 reg 属性的设置,一个用于匹配驱动,一个用于设置器件地址。

I2C 设备数据收发处理流程

一般需要在 probe 函数里面初始化 I2C 设备,要初始化 I2C 设备就必须能够对 I2C 设备寄存器进行读写操作。

i2c_transfer 函数

i2c_transfer 函数,最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_xfer 函数,对于 I.MX6U 而言就是i2c_imx_xfer 这个函数。

i2c_transfer 函数原型如下:

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
  • adap: 所使用的 I2C 适配器, i2c_client 会保存其对应的 i2c_adapter。
  • msgs: I2C 要发送的一个或多个消息。
  • num: 消息数量,也就是 msgs 的数量。
  • 返回值: 负值,失败,其他非负值,发送的 msgs 数量。

msgs 参数,是一个 i2c_msg 类型的指针参数,Linux 内核使用 i2c_msg 结构体来描述一个消息。

i2c_msg 结构体

i2c_msg 结构体定义在 include/uapi/linux/i2c.h 文件中,结构体内容如下:

/**
 * struct i2c_msg - I2C 消息结构体
 * @addr:  从机地址(7位或10位,取决于I2C_M_TEN标志)
 * @flags: 消息标志位(见下方定义)
 * @len:   消息数据长度(字节数)
 * @buf:   消息数据缓冲区指针
 *
 * 描述I2C总线上的单个消息帧,用于读/写操作
 */
struct i2c_msg {
    __u16 addr;  /* 从机地址(低7/10位有效) */
    __u16 flags; /* 标志位组合(位掩码) */
#define I2C_M_TEN         0x0010  // 使用10位地址模式(默认为7位)
#define I2C_M_RD          0x0001  // 读操作(1=读,0=写)
#define I2C_M_STOP        0x8000  // 强制在本消息后发送STOP条件
#define I2C_M_NOSTART     0x4000  // 不发送START条件(需前一条消息无STOP)
#define I2C_M_REV_DIR_ADDR 0x2000 // 方向位反转(某些特殊设备需要)
#define I2C_M_IGNORE_NAK  0x1000  // 忽略从机NAK应答(强制继续传输)
#define I2C_M_NO_RD_ACK   0x0800  // 读操作时不发送ACK(用于最后字节)
#define I2C_M_RECV_LEN    0x0400  // 首字节包含接收长度(SMBus块读)
    __u16 len;   /* 消息数据长度(buf的有效字节数) */
    __u8 *buf;   /* 数据缓冲区(写入时为发送数据,读取时为接收缓冲区) */
};

使用 i2c_transfer 函数发送数据之前要先构建好 i2c_msg。

使用 i2c_transfer 进行 I2C 数据收发,示例代码如下:

/* 设备结构体 */
struct xxx_dev {
    ......
    void *private_data; /* 私有数据,一般会设置为 i2c_client */
};

/*
 * @description : 读取 I2C 设备多个寄存器数据
 * @param – dev : I2C 设备
 * @param – reg : 要读取的寄存器首地址
 * @param – val : 读取到的数据
 * @param – len : 要读取的数据长度
 * @return : 操作结果
 */
static int xxx_read_regs(struct xxx_dev *dev, u8 reg, void *val, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev->private_data;

    /* msg[0],第一条写消息,发送要读取的寄存器首地址 */
    msg[0].addr = client->addr; /* I2C 器件地址 */
    msg[0].flags = 0; /* 标记为发送数据 */
    msg[0].buf = &reg; /* 读取的首地址 */
    msg[0].len = 1; /* reg 长度 */

    /* msg[1],第二条读消息,读取寄存器数据 */
    msg[1].addr = client->addr; /* I2C 器件地址 */
    msg[1].flags = I2C_M_RD; /* 标记为读取数据 */
    msg[1].buf = val; /* 读取数据缓冲区 */
    msg[1].len = len; /* 要读取的数据长度 */

    ret = i2c_transfer(client->adapter, msg, 2);
    if(ret == 2) {
        ret = 0;
    } else {
        ret = -EREMOTEIO;
    }
    return ret;
}

/*
 * @description : 向 I2C 设备多个寄存器写入数据
 * @param – dev : 要写入的设备结构体
 * @param – reg : 要写入的寄存器首地址
 * @param – buf : 要写入的数据缓冲区
 * @param – len : 要写入的数据长度
 * @return : 操作结果
 */
static s32 xxx_write_regs(struct xxx_dev *dev, u8 reg, u8 *buf, u8 len)
{
    u8 b[256];
    struct i2c_msg msg;
    struct i2c_client *client = (struct i2c_client *)dev->private_data;

    b[0] = reg; /* 寄存器首地址 */
    memcpy(&b[1],buf,len); /* 将要发送的数据拷贝到数组 b 里面 */

    msg.addr = client->addr; /* I2C 器件地址 */
    msg.flags = 0; /* 标记为写数据 */

    msg.buf = b; /* 要发送的数据缓冲区 */
    msg.len = len + 1; /* 要发送的数据长度 */

    return i2c_transfer(client->adapter, &msg, 1);
}

另外还有两个API函数分别用于I2C数据的收发操作,这两个函数最终都会调用i2c_transfer。

i2c_master_send函数

首先来看一下 I2C 数据发送函数 i2c_master_send,函数原型如下:

int i2c_master_send(const struct i2c_client *client, const char *buf, int count)
  • client: I2C 设备对应的 i2c_client。
  • buf:要发送的数据。
  • count: 要发送的数据字节数,要小于 64KB, 因为 i2c_msg 的 len 成员变量是一个 u16(无符号 16 位)类型的数据。
  • 返回值: 负值,失败,其他非负值,发送的字节数。

i2c_master_recv函数

I2C 数据接收函数为 i2c_master_recv,函数原型如下:

int i2c_master_recv(const struct i2c_client *client, char *buf, int count)
  • client: I2C 设备对应的 i2c_client。
  • buf:要接收的数据。
  • count: 要接收的数据字节数,要小于 64KB, 因为 i2c_msg 的 len 成员变量是一个 u16(无
  • 符号 16 位)类型的数据。
  • 返回值: 负值,失败,其他非负值,发送的字节数。

下讲内容我们学习怎么编写AP3216C 这个 I2C 设备的 Linux 驱动。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐