imx6ull-驱动开发篇43——I.MX6U 的 I2C 驱动分析
目录
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 控制器寄存器物理基地址,也就是 0X021A0000res = 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; // 从设备树获取硬件配置elsei2c_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; // 默认速率 = 100Kret = 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 = ® /* 读取的首地址 */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 驱动。