LV110-I2C总线驱动
I2C 总线驱动其实就是 I2C 适配器驱动,也就是 SOC 的 I2C 控制器驱动。就相当于之前在裸机上控制 I.MX6U 的物理 I2C 外设。
I2C 设备驱动是需要用户根据不同的 I2C 设备去编写,而 I2C 适配器驱动一般都是 SOC 厂商去编写的,比如 NXP 就编写好了 I.MX6U 的 I2C 适配器驱动。
一、I.MX6U I2C 简介
可以参考《i.MX 6ULL Applications Processor Reference Manual》——Chapter 31 I2C Controller (I2C)
I.MX6U 提供了 4 个 I2C 外设,通过这四个 I2C 外设即可完成与 I2C 从器件进行通信。I.MX6U 的 I2C 支持两种模式:标准模式和快速模式,标准模式下 I2C 数据传输速率最高是 100Kbits/s,在快速模式下数据传输速率最高为 400Kbits/s。它的内部框图如下:

1. I2Cx_IADR(x = 1~4)
这个可以看《 i.MX 6ULL Applications Processor Reference Manual》—— 31.7.1 I2C Address Register (I2Cx_IADR) :

寄存器 I2Cx_IADR 只有 ADR(bit7:1)位有效,用来保存 I2C 从设备地址数据。当我们要访问某个 I2C 从设备的时候就需要将其设备地址写入到 ADR 里面。
2. I2Cx_IFDR (x = 1~4)
这个可以看《 i.MX 6ULL Applications Processor Reference Manual》—— 31.7.2 I2C Frequency Divider Register (I2Cx_IFDR) :

这个是 I2C 的分频寄存器,寄存器 I2Cx_IFDR 也只有 IC(bit5:0)这个位,用来设置 I2C 的波特率, I2C 的时钟源可以选择 IPG_CLK_ROOT = 66MHz,通过设置 IC 位既可以得到想要的 I2C 波特率。 IC 位可选的设置可以看《 i.MX 6ULL Applications Processor Reference Manual》—— Table 31-3. I2C_IFDR Register Field Values 。
不像其他外设的分频设置一样可以随意设置, Table 31-3. I2C_IFDR Register Field Values 中列出了 IC 的所有可选值。比如现在 I2C 的时钟源为 66MHz,我们要设置 I2C 的波特率为 100KHz,那么 IC 就可以设置为 0X15,也就是 640 分频。 66000000/640 = 103.125KHz ≈ 100KHz。
3. I2Cx_I2CR (x = 1~4)
这个可以看《 i.MX 6ULL Applications Processor Reference Manual》—— 31.7.3 I2C Control Register (I2Cx_I2CR),这个是 I2C 控制寄存器:

存器 I2Cx_I2CR 的各位含义如下:
IEN(bit7): I2C 使能位,为 1 的时候使能 I2C,为 0 的时候关闭 I2C。
IIEN(bit6): I2C 中断使能位,为 1 的时候使能 I2C 中断,为 0 的时候关闭 I2C 中断。
MSTA(bit5): 主从模式选择位,设置 IIC 工作在主模式还是从模式,为 1 的时候工作在主模式,为 0 的时候工作在从模式。
MTX(bit4): 传输方向选择位,用来设置是进行发送还是接收,为 0 的时候是接收,为 1 的时候是发送。
TXAK(bit3): 传输应答位使能,为 0 的话发送 ACK 信号,为 1 的话发送 NO ACK 信号。
RSTA(bit2): 重复开始信号,为 1 的话产生一个重新开始信号。
4. I2Cx_I2SR (x = 1~4)
这个可以看《 i.MX 6ULL Applications Processor Reference Manual》—— 31.7.4 I2C Status Register (I2Cx_I2SR) ,这个是 I2C 的状态寄存器 :

寄存器 I2Cx_I2SR 的各位含义如下:
- ICF(bit7): 数据传输状态位,为 0 的时候表示数据正在传输,为 1 的时候表示数据传输完成。
- IAAS(bit6): 当为 1 的时候表示 I2C 地址,也就是 I2Cx_IADR 寄存器中的地址是从设备地址。
- IBB(bit5): I2C 总线忙标志位,当为 0 的时候表示 I2C 总线空闲,为 1 的时候表示 I2C 总线忙。
- IAL(bit4): 仲裁丢失位,为 1 的时候表示发生仲裁丢失。
- SRW(bit2): 从机读写状态位,当 I2C 作为从机的时候使用,此位用来表明主机发送给从机的是读还是写命令。为 0 的时候表示主机要向从机写数据,为 1 的时候表示主机要从从机读取数据。
- IIF(bit1): I2C 中断挂起标志位,当为 1 的时候表示有中断挂起,此位需要软件清零。
- RXAK(bit0): 应答信号标志位,为 0 的时候表示接收到 ACK 应答信号,为 1 的话表示检测到 NO ACK 信号。
5. I2Cx_I2DR (x = 1~4)
这个可以看《 i.MX 6ULL Applications Processor Reference Manual》—— 31.7.5 I2C Data I/O Register (I2Cx_I2DR) ,这是 I2C 的数据寄存器,此寄存器只有低 8 位有效,当要发送数据的时候将要发送的数据写入到此寄存器,如果要接收数据的话直接读取此寄存器即可得到接收到的数据。

①、与标准 I2C 总线兼容。
②、多主机运行
③、软件可编程的 64 中不同的串行时钟序列。
④、软件可选择的应答位。⑤、开始/结束信号生成和检测。⑥、重复开始信号生成。⑦、确认位生成。⑧、总线忙检测
二、I2C1 控制器节点
在 imx6ul.dtsi 文件中找到 I.MX6U 的 I2C1 控制器节点,节点内容如下所示:
i2c1: i2c@21a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
};
可以看到,I2C1 的起始地址就是 0x021A_0000。不过这里重点关注 i2c1 节点的 compatible 属性值,因为通过 compatible 属性值可以在 Linux 源码里面找到对应的驱动文件。
这里 i2c1 节点的 compatible 属性值有两个:“fsl, imx6ul-i2c”和“fsl, imx21- i2c”,在 Linux 源码中搜索这两个字符串即可找到对应的驱动文件。
三、I2C 适配器驱动
我们打开 i2c-imx.c - drivers/i2c/busses/i2c-imx.c 文件,找到这些内容:
static struct platform_driver i2c_imx_driver = {
.probe = i2c_imx_probe,
.remove = i2c_imx_remove,
.driver = {
.name = DRIVER_NAME,
.pm = I2C_IMX_PM_OPS,
.of_match_table = i2c_imx_dt_ids,
},
.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);这部分是驱动的入口和退出函数,可以看到 I.MX6U 的 I2C 适配器驱动是个标准的 platform 驱动,由此可以看出,虽然 I2C 总线为别的设备提供了一种总线驱动框架,但是 I2C 适配器却是 platform 驱动。
1. i2c_imx_driver.of_match_table
既然是 platform 平台总线驱动,那么就和前面肯定是一样的,我们先看一下 i2c_imx_driver.of_match_table,可以看到它的值是 i2c_imx_dt_ids :
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);这里就是和设备树进行匹配的匹配表啦,前面 i2c1 的属性 compatible :
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";所以这里就可以匹配上了。
2. i2c_imx_driver. i2c_imx_probe
按照 platform 平台总线的逻辑,当 i2c 适配器和驱动匹配后就会调用 i2c_imx_driver. i2c_imx_probe 函数
static int i2c_imx_probe(struct platform_device *pdev)
{
// ......
// 调用 platform_get_irq 函数获取中断号。
irq = platform_get_irq(pdev, 0);
//......
// 调用 platform_get_resource 函数从设备树中获取 I2C1 控制器寄存器物理基地址,也就是 0X021A0000。
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// 获取到寄存器基地址以后使用 devm_ioremap_resource 函数对其进行内存映射,得到可以在 Linux 内核中使用的虚拟地址。
base = devm_ioremap_resource(&pdev->dev, res);
//......
// NXP 使用 imx_i2c_struct 结构体来表示 I.MX 系列 SOC 的 I2C 控制器,这里使用 devm_kzalloc 函数来申请内存。
phy_addr = (dma_addr_t)res->start;
i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL);
//......
/* Setup i2c_imx driver structure */
// mx_i2c_struct 结构体要有个叫做 adapter 的成员变量, adapter 就是 i2c_adapter,这里初始化 i2c_adapter。
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_adapter 的 algo 成员变量为 i2c_imx_algo,也就是设置 i2c_algorithm。
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;
/* Get I2C clock */
//......
/* Request IRQ */
//注册 I2C 控制器中断,中断服务函数为 i2c_imx_isr。
ret = devm_request_irq(&pdev->dev, irq, i2c_imx_isr, IRQF_SHARED,
pdev->name, i2c_imx);
//......
/* Set up clock divider */
//设置 I2C 频率默认为 IMX_I2C_BIT_RATE = 100KHz,如果设备树节点设置了“clock-frequency”属性的话 I2C 频率就使用 clock-frequency 属性值。
i2c_imx->bitrate = IMX_I2C_BIT_RATE;
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;
i2c_imx->clk_change_nb.notifier_call = i2c_imx_clk_notifier_call;
clk_notifier_register(i2c_imx->clk, &i2c_imx->clk_change_nb);
i2c_imx_set_clk(i2c_imx, clk_get_rate(i2c_imx->clk));
/* Set up chip registers to defaults */
// 设置 I2C1 控制的 I2CR 和 I2SR 寄存器。
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);
//......
/* Add I2C adapter */
//调用 i2c_add_numbered_adapter 函数向 Linux 内核注册 i2c_adapter。
ret = i2c_add_numbered_adapter(&i2c_imx->adapter);
//......
/* Init DMA config if supported */
//申请 DMA, 看来 I.MX 的 I2C 适配器驱动采用了 DMA 方式。
i2c_imx_dma_request(i2c_imx, phy_addr);
return 0; /* Return OK */
//.......
}i2c_imx_probe 函数主要的工作就是一下两点:①、初始化 i2c_adapter,设置 i2c_algorithm 为 i2c_imx_algo,最后向 Linux 内核注册 i2c_adapter。②、初始化 I2C1 控制器的相关寄存器。
3. i2c_imx_algo
我们来看一下 i2c_imx_probe() 函数的 1084 行:
i2c_imx->adapter.algo = &i2c_imx_algo;这里是在设置 i2c_adapter 的 algo 成员变量为 i2c_imx_algo ,也就是设置 i2c_algorithm。
static const struct i2c_algorithm i2c_imx_algo = {
.master_xfer = i2c_imx_xfer, // 完成与 I2C 设备通信的
.functionality = i2c_imx_func, // functionality 用于返回此 I2C 适配器支持什么样的通信协议
};前面我们就知道 i2c_algorithm 中的关键函数 i2c_algorithm.master_xfer() 用于产生 i2c 访问周期需要的 start stop ack 信号,以 i2c_msg(即 i2c 消息)为单位发送和接收通信数据。在这里就对应 i2c_imx_algo.i2c_imx_xfer():
static int i2c_imx_xfer(struct i2c_adapter *adapter,
struct i2c_msg *msgs, int num)
{
//......
/* Start I2C transfer 开启 I2C 通信 */
result = i2c_imx_start(i2c_imx);
//......
/* read/write data */
for (i = 0; i < num; i++) {
if (i == num - 1)
is_lastmsg = true;
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);
/* write/read data */
#ifdef CONFIG_I2C_DEBUG_BUS
//......
#endif
if (msgs[i].flags & I2C_M_RD)
result = i2c_imx_read(i2c_imx, &msgs[i], is_lastmsg);
else {
if (i2c_imx->dma && msgs[i].len >= DMA_THRESHOLD)
result = i2c_imx_dma_write(i2c_imx, &msgs[i]);
else
result = i2c_imx_write(i2c_imx, &msgs[i]);
}
if (result)
goto fail0;
}
fail0:
/* Stop I2C transfer 停止 I2C 通信 */
i2c_imx_stop(i2c_imx);
//......
return (result < 0) ? result : num;
}这里涉及到这几个函数:i2c_imx_start、 i2c_imx_read、 i2c_imx_write 和 i2c_imx_stop(),这里就是和裸机中 i2c 的驱动是一样的了,可以参考 20_i2c/01_i2c_demo/bsp/i2c/bsp_i2c.c · 苏木/imx6ull-bare-demo - 码云 - 开源中国 这个 demo 中的 i2c 外设的操作进行理解。不过这里都是物理 I2C 的操作,并不是用 GPIO 模拟的,所以都是在操作 I2C 外设相关寄存器。 
4. i2c_adapter 注册/注销函数
4.1 i2c_add_adapter()
int i2c_add_adapter(struct i2c_adapter *adapter)
{
//......
}4.2 i2c_add_numbered_adapter()
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
{
//......
}4.3 i2c_del_adapter()
void i2c_del_adapter(struct i2c_adapter *adap)
{
//......
}四、编写一个虚拟的 i2c_adapter
1. 设备树
在设备树根节点里构造 I2C Bus 节点:
/ {
i2c-bus-virtual {
compatible = "alpha,i2c-bus-virtual";
};
};这个其实就相当于 i2c_adapter 的设备。
2. 适配器的驱动
2.1 要做哪些?
前面我们知道 I2C 适配器驱动是个标准的 platform 驱动,所以这里其实就按照 platform 驱动的编写流程,进行分配、设置、注册 platform_driver 结构体即可。核心是 probe 函数,它要做这几件事:
- 根据设备树信息设置硬件(引脚、时钟等)
- 分配、设置、注册 i2c_apdater,i2c_apdater 的核心是 master_xfer 函数,它的实现取决于硬件,大概代码如下
static int xxx_master_xfer(struct i2c_adapter *adapter,
struct i2c_msg *msgs, int num)
{
for (i = 0; i < num; i++) {
struct i2c_msg *msg = msgs[i];
{
// 1. 发出 S 信号: 设置寄存器发出 S 信号
CTLREG = S;
// 2. 根据 Flag 发出设备地址和 R/W 位: 把这 8 位数据写入某个 DATAREG 即可发出信号
// 判断是否有 ACK
if (!ACK)
return ERROR;
else {
// 3. read / write
if (read) {
STATUS = XXX; // 这决定读到一个数据后是否发出 ACK 给对方
val = DATAREG; // 这会发起 I2C 读操作
} else if(write) {
DATAREG = val; // 这会发起 I2C 写操作
val = STATUS; // 判断是否收到 ACK
if (!ACK)
return ERROR;
}
}
// 4. 发出 P 信号
CTLREG = P;
}
}
return i;
}2.2 i2c_adapter 框架
框架代码可以看这里:30_i2c_subsystem/02_i2c_adapter_virtual/drivers_demo/sdriver_demo.c,更新设备树,然后加载驱动,我们用 i2cdetect 列出所有的 i2c 总线:
i2cdetect -l
注意:不同的板子上,i2c-bus-virtual 的总线号可能不一样,上图中总线号是 4。
我们在这个总线上创建一个 i2c 设备:
echo sdeveeprom 0x50 > /sys/bus/i2c/devices/i2c-4/new_device
发现也是可以的。
2.3 模拟一个 EEPROM
在前面完成的虚拟的 I2C_Adapter 驱动框架里,只要实现了其中的 master_xfer 函数,这个 I2C Adapter 就可以使用了。在 master_xfer 函数里,我们模拟一个 EEPROM,思路如下:
- 分配一个 512 字节的 buffer,表示 EEPROM
- 对于 slave address 为 0x50 的 i2c_msg,解析并处理
对于写:把 i2c_msg 的数据写入 buffer。对于读:从 buffer 中把数据写入 i2c_msg。
- 对于 slave address 为其他值的 i2c_msg,返回错误
完整的 demo 看这里:30_i2c_subsystem/03_i2c_adapter_virtual_ok
3. 模拟 EEPROM 示例
3.1 demo 源码
demo 源码看这里:30_i2c_subsystem/03_i2c_adapter_virtual_ok
3.2 测试结果
更新开发板中的设备树,然后应该可以 i2c/devices/ 在看到对应的 i2c 适配器:

然后我们使用 i2c-tools 进行测试:
- 列出 I2C 总线
i2cdetect -l结果类似下列的信息:

注意:不同的板子上,i2c-bus-virtual 的总线号可能不一样,上问中总线号是 4。
- 检查虚拟总线下的 I2C 设备
# 假设虚拟 I2C BUS 号为 4
i2cdetect -y -a 4
这里似乎并不需要自己创建设备。
- 读写模拟的 EEPROM
i2cset -f -y 4 0x50 0 0x55 # 假设虚拟 I2C BUS 号为 4 往 0 地址写入 0x55
i2cget -f -y 4 0x50 0 # 读 0 地址
参考资料:
【驱动】linux 下 I2C 驱动架构全面分析 - Leo.cheng - 博客园
Linux i2c 子系统(一) _动手写一个 i2c 设备驱动_Linux 编程_Linux 公社-Linux 系统门户网站
I2C 子系统–mpu6050 驱动实验 野火嵌入式 Linux 驱动开发实战指南——基于 i.MX6ULL 系列 文档