I2C子系统-内核视角
2024年7月 · 预计阅读时间: 17 分钟
#
I2C 驱动层级一张图整理,可以看完后面的具体内容再回来看这张图:
接下来,会按照从上到下的顺序介绍整个驱动架构。
#
内核自带的通用 I2C 驱动程序 i2c-dev#
1.i2c-dev.c 注册过程入口函数中,请求字符设备号,并注册已存在 adapters 下面的所有 i2c 设备
同时也会生成对应的设备节点 i2c-X,以后只要打开这个设备节点,就是访问该设备,并且该设备的次设备号也绑定了对应的 adapter。
#
2.file_operations 函数分析回忆我们在 I2C-TOOLS 中调用 open、ioctl,最终就会调用到以下驱动结构体的函数。
所以我们就查看 open、ioctl。(read、write 提供了 i2c 简易写,读写一个字节)
#
2.1 i2cdev_openopen 函数里面可以看到上面,入口函数为什么把 adap 和次设备号绑定,这里就可以使用次设备号访问对应的 i2c_adapter;
然后分配一个 i2c_client 结构体,把它放入 file 的私有数据
#
2.2 i2cdev_ioctl设置从机地址 I2C_SLAVE/I2C_SLAVE_FORCE
通过 file 的私有数据就可以获得 open 函数里面放入的 i2c_client
读写 I2C_RDWR/I2C_SMBUS:最终就是调用 i2c-core 提供的函数
#
3.总结来自韦东山课程
#
编写一个设备驱动程序参考资料:
- Linux 内核文档:
Documentation\i2c\instantiating-devices.rst
Documentation\i2c\writing-clients.rst
- Linux 内核驱动程序示例:
drivers/eeprom/at24.c
#
1.I2C 总线-设备-驱动模型类似于通用字符设备总线-设备-驱动模型,I2C 设备也有一套I2C 总线-设备-驱动模型。整体结构如下图:
#
2.i2c_driver 设备驱动分配、设置、注册一个 i2c_driver 结构体,类似drivers/eeprom/at24.c
:
- 在 probe_new 函数中,分配、设置、注册 file_operations 结构体。
- 在 file_operations 的函数中,使用 i2c_transfer 等函数发起 I2C 传输。
参考 at24.c 的代码,可以给出一个 i2c_driver 的模板:
#
3.编写 I2C 设备-AP3216C 传感器的 i2c_driver 设备驱动AP3216C 是红外、光强、距离三合一的传感器,设备地址是 0x1E。
以读出光强、距离值为例,步骤如下:
- 复位:往寄存器 0 写入 0x4
- 使能:往寄存器 0 写入 0x3
- 读红外:读寄存器 0xA、0xB 得到 2 字节的红外数据
- 读光强:读寄存器 0xC、0xD 得到 2 字节的光强
- 读距离:读寄存器 0xE、0xF 得到 2 字节的距离值
(1)利用上面的 i2c 驱动框架,先实现入口与出口函数:入口函数里面添加总线驱动 i2c_driver,出口函数里面删除。同时提供 of_match_ids_ap3216c 匹配 i2c_client。
(2)在i2c_driver
的probe
函数里面,注册一个字符设备,使用字符设备的 file_operations 操作 ap3126c。
(3)实现字符设备的file_operations
#
4.i2c_client 生成#
(1)在用户态生成示例:
#
(2)编写代码i2c_new_device
i2c_new_probed_device
i2c_register_board_info
- 内核没有
EXPORT_SYMBOL(i2c_register_board_info)
- 使用这个函数的驱动必须编进内核里去
- 内核没有
#
(3)使用设备树生成- IMX6ULL
在某个 I2C 控制器的节点下,添加如下代码:
- STM32MP157
修改arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dts
,添加如下代码:
注意:设备树里 i2c1 就是 I2C BUS0。
编译设备树: 在 Ubuntu 的 STM32MP157 内核目录下执行如下命令, 得到设备树文件:
arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb
复制到 NFS 目录:
确定设备树分区挂载在哪里
由于版本变化,STM32MP157 单板上烧录的系统可能有细微差别。 在开发板上执行
cat /proc/mounts
后,可以得到两种结果:mmcblk2p2 分区挂载在/boot 目录下:无需特殊操作,下面把文件复制到/boot 目录即可
mmcblk2p2 挂载在/mnt 目录下
- 在视频里、后面文档里,都是更新/boot 目录下的文件,所以要先执行以下命令重新挂载:
mount /dev/mmcblk2p2 /boot
- 在视频里、后面文档里,都是更新/boot 目录下的文件,所以要先执行以下命令重新挂载:
更新设备树
重启开发板
#
5.补充:查看驱动和设备信息的命令查看设备节点:
查看 i2c client:
#
I2C_Adapter 驱动框架#
核心结构体其中,关键就是 i2c_algorithm 传输算法和 int nr 编号
master_xfer:这是最重要的函数,它实现了一般的 I2C 传输,用来传输一个或多个 i2c_msg
master_xfer_atomic:
- 可选的函数,功能跟 master_xfer 一样,在
atomic context
环境下使用 - 比如在关机之前、所有中断都关闭的情况下,用来访问电源管理芯片
- 可选的函数,功能跟 master_xfer 一样,在
smbus_xfer:实现 SMBus 传输,如果不提供这个函数,SMBus 传输会使用 master_xfer 来模拟
smbus_xfer_atomic:
- 可选的函数,功能跟 smbus_xfer 一样,在
atomic context
环境下使用 - 比如在关机之前、所有中断都关闭的情况下,用来访问电源管理芯片
- 可选的函数,功能跟 smbus_xfer 一样,在
functionality:返回所支持的 flags:各类 I2CFUNC*
reg_slave/unreg_slave:
- 有些 I2C Adapter 也可工作在 Slave 模式,用来实现或模拟一个 I2C 设备
reg_slave 就是让把一个 i2c_client 注册到 I2C Adapter,换句话说就是让这个 I2C Adapter 模拟该 i2c_client
- unreg_slave:反注册
#
驱动程序框架平台总线驱动模型:
分配、设置、注册 platform_driver 结构体。
核心是 probe 函数,它要做这几件事:
根据设备树信息设置硬件(引脚、时钟等)
分配、设置、注册一个 i2c_adpater 结构体:
i2c_adpater 的核心是 i2c_algorithm
i2c_algorithm 的核心是 master_xfer 函数
平台总线相关的都是老套路了,不赘述:
关键在于 probe 函数里面:
然后需要实现 i2c_bus_virtual_algo 结构体,以及里面的核心函数 master_xfer 函数(这里给出模板):
#
实现 master_xfer 函数在虚拟的 I2C_Adapter 驱动程序里,只要实现了其中的 master_xfer 函数,这个 I2C Adapter 就可以使用了。 在 master_xfer 函数里,我们模拟一个 EEPROM,思路如下:
- 分配一个 512 自己的 buffer,表示 EEPROM
- 对于 slave address 为 0x50 的 i2c_msg,
- 对于写:把 i2c_msg 的数据写入 eeprom_buffer,写到 buf[0]指定的起始位置
- 对于读:从 eeprom_buffer 中把数据读到 i2c_msg->buf
- 对于 slave address 为其他值的 i2c_msg,返回错误-EIO
编程:
#
设备树节点在设备树根节点下,添加如下代码:
- STM32MP157
修改
arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dts
,添加如下代码:编译设备树: 在 Ubuntu 的 STM32MP157 内核目录下执行如下命令, 得到设备树文件:
arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb
复制到 NFS 目录:
开发板上挂载 NFS 文件系统
vmware 使用 NAT(假设 windowsIP 为 192.168.1.100)
vmware 使用桥接,或者不使用 vmware 而是直接使用服务器:假设 Ubuntu IP 为 192.168.1.137
确定设备树分区挂载在哪里
由于版本变化,STM32MP157 单板上烧录的系统可能有细微差别。 在开发板上执行
cat /proc/mounts
后,可以得到两种结果:mmcblk2p2 分区挂载在/boot 目录下:无需特殊操作,下面把文件复制到/boot 目录即可
mmcblk2p2 挂载在/mnt 目录下
- 在视频里、后面文档里,都是更新/boot 目录下的文件,所以要先执行以下命令重新挂载:
mount /dev/mmcblk2p2 /boot
- 在视频里、后面文档里,都是更新/boot 目录下的文件,所以要先执行以下命令重新挂载:
更新设备树
重启开发板
#
编译安装驱动与测试Makefile:
安装:
在开发板上
挂载 NFS,复制文件,insmod,类似如下命令:
使用 i2c-tools 测试:
列出 I2C 总线
结果类似下列的信息:
注意:不同的板子上,i2c-bus-virtual 的总线号可能不一样,上问中总线号是 4。
检查虚拟总线下的 I2C 设备
读写模拟的 EEPROM
#
GPIO 模拟 I2C,实现 I2C_Adapter 驱动参考资料:
- i2c_spec.pdf
- Linux 文档
Linux-5.4\Documentation\devicetree\bindings\i2c\i2c-gpio.yaml
Linux-4.9.88\Documentation\devicetree\bindings\i2c\i2c-gpio.txt
- Linux 驱动源码
Linux-5.4\drivers\i2c\busses\i2c-gpio.c
Linux-4.9.88\drivers\i2c\busses\i2c-gpio.c
写在前面:
怎么使用 i2c-gpio?
我们只需要设置设备树,在里面添加一个节点即可。
compatible = "i2c-gpio";
使用 pinctrl 把 SDA、SCL 所涉及引脚配置为 GPIO、开极
- 可选
指定 SDA、SCL 所用的 GPIO
指定频率(2 种方法):
- i2c-gpio,delay-us = <5>; / ~100 kHz /
- clock-frequency = <400000>;
#address-cells = <1>;
#size-cells = <0>;
i2c-gpio,sda-open-drain:
- 它表示其他驱动、其他系统已经把 SDA 设置为 open drain 了
- 在驱动里不需要在设置为 open drain
- 如果需要驱动代码自己去设置 SDA 为 open drain,就不要提供这个属性
i2c-gpio,scl-open-drain:
- 它表示其他驱动、其他系统已经把 SCL 设置为 open drain 了
- 在驱动里不需要在设置为 open drain
- 如果需要驱动代码自己去设置 SCL 为 open drain,就不要提供这个属性
#
使用 GPIO 模拟 I2C 的硬件要求- 引脚设为 GPIO
- GPIO 设为输出、开极/开漏(open collector/open drain)
- 要有上拉电阻
#
设备树#
内核的 i2c-gpio.ci2c-gpio 的层次:
- 解析设备树
- i2c-gpio.c:i2c_bit_add_numbered_bus
- i2c-algo-bit.c:adap->algo = &i2c_bit_algo;
在 i2c-gpio.c 中是老套路,probe
函数注册驱动程序到总线平台,读取设备树节点信息进行配置(获得时间参数、SDA/SCL 引脚)。
probe
函数最后会使用i2c_bit_add_numbered_bus
注册adapter
,这个函数在drivers/i2c/algos/i2c-algo-bit.c
定义:
关键在于adap->algo = &i2c_bit_algo;
传输算法,里面有bit_xfer
使用 gpio 传输 i2c 消息。
i2c-algo-bit.c
#
具体分析从上面的 bit_xfer 函数开始看。
i2c_start
发生消息:
在i2c_outb
函数中,发送一个字节的数据,从 bit[7]到 bit[0]
- setsda
- 延迟 udelay/2
- sclhi 拉高 scl
- 延迟 udelay
- scllo 拉低 scl
- 延迟 udelay/2
由此可以得出传送一位数据需要 2*udelay 的时间
#
具体芯片下 I2C_Adapter 驱动参考资料:
- Linux 内核真正的 I2C 控制器驱动程序
- IMX6ULL:
Linux-4.9.88\drivers\i2c\busses\i2c-imx.c
- STM32MP157:
Linux-5.4\drivers\i2c\busses\i2c-stm32f7.c
- IMX6ULL:
- 芯片手册
- IMXX6ULL:
IMX6ULLRM.pdf
Chapter 31: I2C Controller (I2C)
- STM32MP157:
DM00327659.pdf
52 Inter-integrated circuit (I2C) interface
- IMXX6ULL:
#
I2C 控制器的通用结构一般含有以下寄存器:
- 控制寄存器
- 发送寄存器、移位寄存器
- 接收寄存器、移位寄存器
- 状态寄存器
- 中断寄存器
数据放入发送寄存器后,cpu 就可以返回继续执行了,i2c 控制器会通过移位寄存器一位一位地发送出去。
接收则是从移位寄存器放入接收寄存器,cpu 只需要一次性读取接收寄存器的数据即可。
#
IMX6ULL 和 MP157 的 I2C 控制器IMX6ull:
STM32MP157:
#
分析代码#
1.设备树IMX6ULL:
arch/arm/boot/dts/imx6ull.dtsi
STM32MP157:
arch/arm/boot/dts/stm32mp151.dtsi
#
2.驱动程序分析读 I2C 数据时,要先发出设备地址,这是写操作,然后再发起读操作,涉及写、读操作。所以以读 I2C 数据为例讲解核心代码。
同样也是看 adapter 里面的algo
成员master_xfer
:
i2c_imx_start
:设置状态寄存器、控制寄存器,开启 I2C 传输
i2c_imx_read
:
i2c_imx_stop(i2c_imx);
STM32MP157:函数stm32f7_i2c_xfer
分析
这函数完全由中断程序来驱动:启动传输后,就等待;在中断服务程序里传输下一个数据,知道传输完毕。
(太累了,过几天补充分析 - 2024.7.4)