<Linux开发>驱动开发 -之- Linux I2C 驱动
交叉编译环境搭建:
<Linux开发> linux开发工具-之-交叉编译环境搭建
uboot移植可参考以下:
<Linux开发> -之-系统移植 uboot移植过程详细记录(第一部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第三部分)(uboot移植完结)
Linux内核及设备树移植可参考以下:
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第一部分)
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第二部分完结)
Linux文件系统构建移植参考以下:
<Linux开发>系统移植 -之- linux构建BusyBox根文件系统及移植过程详细记录
<Linux开发>系统移植 -之-使用buildroot构建BusyBox根文件系统
Linux驱动开发参考以下:
<Linux开发>驱动开发 -之-pinctrl子系统
<Linux开发>驱动开发 -之-gpio子系统
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的LED驱动
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的beep驱动
<Linux开发>驱动开发 -之-资源的并发与竞争处理
<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知
<Linux开发>驱动开发 -之-Linux MISC 驱动
<Linux开发>驱动开发 -之-Linux INPUT 子系统
<Linux开发>驱动开发 -之- Linux LCD 驱动
<Linux开发>驱动开发 -之- Linux RTC 驱动
资料:
I.MX6UL芯片手册
I.MX6ULL芯片手册
SecureCRT安装包
I2c驱动快速体验版请查阅:<Android开发>–内核驱动开发-- I2C设备驱动
I2C 是很常用的一个串行通信接口,用于连接各种外设、传感器等器件。本文学习如何在 Linux 下开发 I2C 接口器件驱动,重点是学习 Linux 下的 I2C 驱动框架,按照指定的框架去编写 I2C 设备驱动。笔者所用的 I.MX6U 开发板上有一个 AP3216C, 这是个三合一环境光传感器,通过 AP3216C 讲解一下如何编写 Linux 下的 I2C 设备驱动程序。
I2C 是很常见的一种总线协议,I2C 是 NXP 公司设计的,I2C 使用两条线在主控制器和从机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线),这两条数据线需要接上拉电阻,总线空闲的时候 SCL 和 SDA 处于高电平。I2C 总线标准模式下速度可以达到 100Kb/S,快速模式下可以达到 400Kb/S。I2C 总线工作是按照一定的协议来运行的; 如果学习过STM32单片机的读者应该会很熟悉了,在STM32等单片机中,有时我们会用硬件I2C,也会用软件I2C;硬件I2C就是有I2C控制器去控制通信时序进行通信,与linux下类似;软件I2C,则是有人为控制IO空输出高低电平的时序达到通信。
接下来就看一下 I2C 协议。
I2C 是支持多从机的,也就是一个 I2C 控制器下可以挂多个 I2C 从设备,这些不同的 I2C从设备有不同的器件地址,这样 I2C 主控制器就可以通过 I2C 设备的器件地址访问指定的 I2C设备了,一个 I2C 总线连接多个 I2C 设备如下图所示:
图 2.1.1
图 2.1.1 中 SDA 和 SCL 这两根线必须要接一个上拉电阻,一般是 4.7K。其余的 I2C 从器件都挂接到 SDA 和 SCL 这两根线上,这样就可以通过 SDA 和 SCL 这两根线来访问多个 I2C设备。
接下来看一下 I2C 协议有关的术语。
顾名思义,也就是 I2C 通信起始标志,通过这个起始位就可以告诉 I2C 从机,“我”要开始进行 I2C 通信了。在 SCL 为高电平的时候,SDA 出现下降沿就表示为起始位,如下图所示:
图 2.1
其实我们可以关注到上图,在SDA由高变低之前,SCL会有一段高电平。
停止位就是停止 I2C 通信的标志位,和起始位的功能相反。在 SCL 位高电平的时候,SDA出现上升沿就表示为停止位,如下图所示:
图 2.2
同样我们可以关注到上图,在SDA由低变高之前,SCL会有一段高电平。
I2C 总线在数据传输的时候要保证在 SCL 高电平期间,SDA 上的数据稳定,因此 SDA 上的数据变化只能在 SCL 低电平期间发生,如下图所示:
图 2.3
当 I2C 主机发送完 8 位数据以后会将 SDA 设置为输入状态,等待 I2C 从机应答,也就是等到 I2C 从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所需的时钟,主机发送完 8 位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
主机通过 I2C 总线与从机之间进行通信不外乎两个操作:写和读,I2C 总线单字节写时序如下图所示:
图 2.5
上图就是 I2C 写时序,我们来看一下写时序的具体步骤:
-----1)、开始信号。
-----2)、发送 I2C 设备地址,每个 I2C 器件都有一个设备地址,通过发送具体的设备地址来决定访问哪个 I2C 器件。这是一个 8 位的数据,其中高 7 位是设备地址,最后 1 位是读写位,为1 的话表示这是一个读操作,为 0 的话表示这是一个写操作。
-----3)、 I2C 器件地址后面跟着一个读写位,为 0 表示写操作,为 1 表示读操作。
-----4)、从机发送的 ACK 应答信号,从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
-----5)、重新发送开始信号。
-----6)、发送要写写入数据的寄存器地址。
-----7)、从机发送的 ACK 应答信号,从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
-----8)、发送要写入寄存器的数据。
-----9)、从机发送的 ACK 应答信号,从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
-----10)、停止信号。
I2C 单字节读时序比写时序要复杂一点,读时序分为 4 大步,第一步是发送设备地址,第二步是发送要读取的寄存器地址,第三步重新发送设备地址,最后一步就是 I2C 从器件输出要读取的寄存器值,我们具体来看一下这几步。
—1)、主机发送起始信号。
—2)、主机发送要读取的 I2C 从设备地址。
—3)、读写控制位,因为是向 I2C 从设备发送数据,因此是写信号。为 0 表示写操作,为 1 表示读操作
—4)、从机发送的 ACK 应答信号,从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
—5)、重新发送 START 信号。
—6)、主机发送要读取的寄存器地址。
—7)、从机发送的 ACK 应答信号,从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
—8)、重新发送 START 信号。
—9)、重新发送要读取的 I2C 从设备地址。
—10)、读写控制位,这里是高电平1读信号,表示接下来是从 I2C 从设备里面读取数据。
—11)、从机发送的 ACK 应答信号,从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
—12)、从 I2C 器件里面读取到的数据。
—13)、主机发出高电平1 NO ACK 信号,表示读取完成,不需要从机再发送 ACK 信号了。
—14)、主机发出 STOP 信号,停止 I2C 通信。
有时候我们需要读写多个字节,多字节读写时序和单字节的基本一致,只是在读写数据的时候可以连续发送多个自己的数据,其他的控制时序都是和单字节一样的。
对于I2C的了解我们就分析那么多,如果想更加深入了解I2C协议的可自行查阅相关资料。
I.MX6U 提供了 4 个 I2C 外设,通过这四个 I2C 外设即可完成与 I2C 从器件进行通信,I.MX6U 的 I2C 外设特性如下:
①•与I2C总线标准的兼容性
②•多主机操作
③•软件可编程性的64种不同串行时钟频率之一
④•软件可选确认位
⑤•中断驱动,逐字节数据传输
⑥•仲裁丢失中断,自动模式从主模式切换到从模式
⑦•主叫地址识别中断
⑧•启动和停止信号生成/检测
⑨•重复启动信号生成
⑩•确认位生成/检测
⑪•总线繁忙检测
I.MX6U的 I2C主要以两种功能模式运行:标准模式和快速模式。
•在标准模式下,I2C支持高达100 kbits/s的数据传输速率。
•在快速模式下,可以实现高达400 kbits/s的数据传输速率。
每个块操作,快速或标准模式不需要特殊配置。区分标准模式和快速模式的是数据传输速率。
I2C系统是一个真正的多主控总线,包括仲裁和碰撞检测,可以在多个设备试图同时控制总线时防止数据损坏。该特性支持具有多处理器控制的复杂应用程序,并可用于通过到装配线计算机的外部连接来快速测试和对齐最终产品。
IMX6U的I2C框图如下:
图 3.1
我们接下来看一下 I2C 的几个重要的寄存器。
(1) I2Cx_IADR(x=1~4)寄存器,这是I2C 的地址寄存器,此寄存器结构如下图所示:
图3.3.1
寄存器 I2Cx_IADR 只有 ADR(bit7:1)位有效,用来保存 I2C 从设备地址数据。当我们要访问某个 I2C 从设备的时候就需要将其设备地址写入到 ADR 里面。
(2) 寄存器I2Cx_IFDR,这个是 I2C 的分频寄存器,寄存器结构如下图所示:
图3.3.2
寄存器 I2Cx_IFDR 也只有 IC(bit5:0)这个位,用来设置 I2C 的波特率,I2C 的时钟源可以选择 IPG_CLK_ROOT=66MHz,通过设置 IC 位既可以得到想要的 I2C 波特率。IC 位可选的设置如下图所示:
图3.3.3
I2C_IFDR提供了一个可编程的预调节器来配置时钟的比特率选择。寄存器不会通过软件重置而被重置。I2C时钟来自PERCLK_ROOT,是路由的IPG_CLK_ROOT。I2C时钟频率可以很容易地得到: I2C时钟频率=(PERCLK_ROOT频率)/(除因子对应IFDR)默认情况下,IPG_CLK_ROOT和PERCLK_ROOT频率设置为49.5 MHz,其中根时钟来自PLL2的PFD2。获取频率可通过以下方式实现:
PLL2 = 528 MHz
PLL2_PFD2 = 528 MHz * 18 / 24 = 396 MHz
IPG_CLK_ROOT = (PLL2_PFD2 / ahb_podf )/ ipg_podf = (396MHz/4)/2 = 49.5MHz
PER_CLK_ROOT = IPG_CLK_ROOT/perclk_podf = 49.5 MHz/1 = 49.5 MHz
不像其他外设的分频设置一样可以随意设置,图3.3.3中列出了 IC 的所有可选值。比如现在I2C的时钟源为66MHz,我们要设置I2C的波特率为100KHz,那么IC就可以设置为0X15,也就是 640 分频。66000000/640=103.125KHz≈100KHz。
(3) 寄存器 I2Cx_I2CR,这个是 I2C 控制寄存器,此寄存器结构如下图:
图3.3.4
寄存器 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,这个是 I2C 的状态寄存器,寄存器结构如下图:
图3.3.5
寄存器 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,是 I2C 的数据寄存器,此寄存器只有低 8 位有效,当要发送数据的时候将要发送的数据写入到此寄存器,如果要接收数据的话直接读取此寄存器即可得到接收到的数据。结构如下图:
图3.3.6
I.MX6U开发板上通过 I2C1 连接了一个三合一环境传感器:AP3216C,AP3216C是由敦南科技推出的一款传感器,其支持环境光强度(ALS)、接近距离(PS)和红外线强度(IR)这三个环境参数检测。该芯片可以通过 IIC 接口与主控制相连,并且支持中断,AP3216C 的特点如下:
①、I2C 接口,快速模式下波特率可以到 400Kbit/S
②、多种工作模式选择:ALS、PS+IR、ALS+PS+IR、PD 等等。
③、内建温度补偿电路。
④、宽工作温度范围(-30°C ~ +80°C)。
⑤、超小封装,4.1mm x 2.4mm x 1.35mm
⑥、环境光传感器具有 16 位分辨率。
⑦、接近传感器和红外传感器具有 10 位分辨率。
AP3216C 常被用于手机、平板、导航设备等,其内置的接近传感器可以用于检测是否有物体接近,比如手机上用来检测耳朵是否接触听筒,如果检测到的话就表示正在打电话,手机就会关闭手机屏幕以省电。也可以使用环境光传感器检测光照强度,可以实现自动背光亮度调节。
AP3216C 结构如图4.1所示:
图 4.1
根据手册说明,AP3216 的设备地址为 0X1E,同几乎所有的 I2C 从器件一样,AP3216C 内部也有一些寄存器,通过这些寄存器我们可以配置 AP3216C 的工作模式,并且读取相应的数据。AP3216C 我们用的寄存器如表 4.1 所示:
表 4.1
ADD(Hex) | 寄存器名 | Bit | 寄存器command | 描述 |
---|---|---|---|---|
0x00 | 系统配置(默认:0x00) | 2:0 | System Mode(Default : 000) | 000:掉电模式(默认)。001:使能 ALS。010:使能 PS+IR。011:使能 ALS+PS+IR。100:软复位。101:ALS 单次模式。110:PS+IR 单次模式。111:ALS+PS+IR 单次模式。 |
0x01 | 中断状态 | 1 | PS Int (Read only)(Default : 0) | 0:中断已清除或尚未触发1:触发中断 |
0x02 | 清楚中断方式 | 0 | 清楚方式(默认:0) | 0:INT通过读取自动清除数据寄存器.1:写入1后软件清除 |
0x0A | IR 低位数据 | 7和1:0 | 7:IR溢出 1:0(只读) | ADC输出的IR低位字节 |
0x0B | IR 高位数据 | 7:0 | (只读) | ADC输出的IR高位字节 |
0x0C | ALS 低位数据 | 7:0 | (只读) | ADC输出的ALS低位字节 |
0x0D | ALS 高位数据 | 7:0 | (只读) | ADC输出的ALS高位字节 |
0x0E | PS 低位数据 | 7 | 目标检测 (只读) | 0,物体在远离;1,物体在接近。 |
0x0E | PS 低位数据 | 6 | IR溢出 (只读) | 0,IR&PS 数据有效;1,IR&PS 数据无效 |
0x0E | PS 低位数据 | 3:0 | (只读) | PS 最低 4 位数据。 |
0x0F | PS 高位数据 | 7 | 目标检测 (只读) | 0,物体在远离;1,物体在接近。 |
0x0F | PS 高位数据 | 6 | IR溢出 (只读) | 0,IR&PS 数据有效;1,IR&PS 数据无效 |
0x0F | PS 高位数据 | 5:0 | (只读) | PS 最低 6 位数据。 |
在表 4.1 中,0X00 这个寄存器是模式控制寄存器,用来设置 AP3216C 的工作模式,一般开始先将其设置为 0X04,也就是先软件复位一次 AP3216C。接下来根据实际使用情况选择合适的工作模式,比如设置为 0X03,也就是开启 ALS+PS+IR。从 0X0A~0X0F 这 6 个寄存器就是数据寄存器,保存着 ALS、PS 和 IR 这三个传感器获取到的数据值。如果同时打开 ALS、PS 和 IR 则读取间隔最少要 112.5ms,因为 AP3216C 完成一次转换需要 112.5ms。关于 AP3216C的介绍就到这里,如果要想详细的研究此芯片的话,请大家自行查阅其数据手册。
一般使用配置步骤如下:
(1)初始化相应的 IO
初始化 I2C1 相应的 IO,设置其复用功能,如果要使用 AP3216C 中断功能的话,还需要设置 AP3216C 的中断 IO。
(2)初始化 I2C1
初始化 I2C1 接口,设置波特率。
(3)初始化 AP3216C
初始化 AP3216C,读取 AP3216C 的数据。
在前面讲 platform 的时候就说过,platform 是虚拟出来的一条总线,目的是为了实现总线、设备、驱动框架。对于 I2C 而言,不需要虚拟出一条总线,直接使用 I2C总线即可。I2C 总线驱动重点是 I2C 适配器(也就是 SOC 的 I2C 接口控制器)驱动。
在Linux设计中,将I2C代码框架分为三个部分:I2C总线、I2C核心、I2C驱动。
I2C核心(i2c-core): 主要定义i2c驱动所用到的通用API,高内聚的代码会放到i2c-core.c。
I2C总线驱动(i2c adapter): 根据平台定制的i2c驱动,其中包含i2c传输的算法设计。主要工作负责生成i2c_client,注册适配器,以及i2c_client与i2c_driver的匹配。
I2C设备驱动(i2c client driver): 驱动I2C设备的代码。I2C设备驱动定义了外设的交互方式,与不同的I2C外设需要不同的设备驱动。I2C设备驱动对上和用户应用程序打交道,对下和I2C核心对接。
对于i2c-core而言,不需要我们实现,这个是linux内核中已经有的,属于标准内核代码。
i2c adapter,则是SOC芯片的I2C控制器驱动,这个根据不同的平台,I2C控制器会有差异,所以这部分代码是SOC厂商会实现,也不需要开发人员写,但我们需要了解一下也是可以的。
i2c client driver,则是具体的I2C设备驱动,一般驱动开发人员需要编写这部分代码。
我们先来看一下i2c adapter的有关内容。
对于i2c adapter相关驱动代码已经有NXP实现,相关内容如下。
I2C控制器的设备树节点如下:
路径:arch/arm/boot/dts/imx6ull.dtsi
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = ;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
};
i2c2: i2c@021a4000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a4000 0x4000>;
interrupts = ;
clocks = <&clks IMX6UL_CLK_I2C2>;
status = "disabled";
};
i2c3: i2c@021a8000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a8000 0x4000>;
interrupts = ;
clocks = <&clks IMX6UL_CLK_I2C3>;
status = "disabled";
};
上述代码内容就是IMX6ULL中的3个I2C,I2C1、I2C2和I2C3的地址分别是0x021a0000、0x021a4000、0x021a8000。根据compatible属性,我们可以找到对于的驱动程序,搜索 “fsl,imx6ul-i2c”, “fsl,imx21-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);
文件i2c-imx.c中的内容就是NXP原厂针对IMX6ULL等SOC中I2C控制器的驱动i2c adapter。我们不深入研究i2c adapter,因为这是SOC原厂会完成这部分驱动的编写。
i2c adapter用到两个重要的数据结构:i2c_adapter 和 i2c_algorithm,Linux 内核将 SOC 的 I2C 适配器(控制器)抽象成 i2c_adapter,i2c_adapter 结构体内容如下:
路径:
/*i2c_adapter是用于识别物理i2c总线以及访问该总线所需的访问算法的结构。
*/
struct i2c_adapter {
struct module *owner;
unsigned int class; /*允许探测的类*/
const struct i2c_algorithm *algo; /*访问总线的算法*/
void *algo_data;
/*对所有设备有效的数据字段*/
struct rt_mutex bus_lock;
int timeout; /* in jiffies */
int retries;
struct device dev; /*适配器设备*/
int nr;
char name[48];
struct completion dev_released;
struct mutex userspace_clients_lock;
struct list_head userspace_clients;
struct i2c_bus_recovery_info *bus_recovery_info;
const struct i2c_adapter_quirks *quirks;
};
第 7 行,i2c_algorithm 类型的指针变量 algo,对于一个 I2C 适配器,肯定要对外提供读写 API 函数,设备驱动程序可以使用这些 API 函数来完成读写操作。i2c_algorithm 就是 I2C 适配器与 IIC 设备进行通信的方法。
i2c_algorithm 结构体定义内容如下:
路径:include/linux/i2c.h
/**
*结构i2c_algorithm-表示i2c传输方法
*@master_xfer:向msgs数组定义的给定i2c适配器发出一组i2c事务,其中num条消息可通过adap指定的适配器传输。
*@smbus_xfer:向给定的I2C适配器发出smbus事务。如果这不存在,那么总线层将尝试将SMBus调用转换为I2C传输。
*@functionality:从I2C_FUNC_*标志返回该算法/适配器对支持的标志。
*@reg_slave:将给定的客户端注册到此适配器的I2C从属模式
*@unreg_slave:从该适配器的I2C从机模式注销给定的客户端
*
*以下结构适用于那些喜欢实现新总线驱动程序的人:
*i2c_算法是一类硬件解决方案的接口,这些解决方案可以使用相同的总线算法来解决,即位碰撞或PCF8584,列举两种最常见的算法。
*@master_xfer字段的返回代码应指示传输过程中发生的错误代码类型,如内核文档文件Documentation/i2c/fault codes中所述。
*/
struct i2c_algorithm {
/*如果适配器算法不能进行I2C级访问,请将master_xfer设置为NULL。如果适配器算法可以进行SMBus访问,请设置SMBus_xfer。如果设置为NULL,则使用常见I2C消息模拟SMBus协议*/
/*master_xfer应返回成功处理的消息数,或返回错误时的负值*/
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
int num);
int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
unsigned short flags, char read_write,
u8 command, int size, union i2c_smbus_data *data);
/*确定适配器支持的内容*/
u32 (*functionality) (struct i2c_adapter *);
#if IS_ENABLED(CONFIG_I2C_SLAVE)
int (*reg_slave)(struct i2c_client *client);
int (*unreg_slave)(struct i2c_client *client);
#endif
};
第 20 行,master_xfer 就是 I2C 适配器的传输函数,可以通过此函数来完成与 IIC 设备之间的通信。
第 22 行,smbus_xfer 就是 SMBUS 总线的传输函数。
i2c adapter驱动的probe部分内容如下:
路径:drivers/i2c/busses/i2c-imx.c
static int i2c_imx_probe(struct platform_device *pdev)
{
const struct of_device_id *of_id = of_match_device(i2c_imx_dt_ids,
&pdev->dev);
..........
/* Set up adapter data */
i2c_set_adapdata(&i2c_imx->adapter, i2c_imx);
................
/* Add I2C adapter */
ret = i2c_add_numbered_adapter(&i2c_imx->adapter);
.........
第 4 行:根据i2c_imx_dt_ids中的属性值,查找对于设备树节点。
第 8 行:初始化i2c_adapter 结构体变量。
第 11 行:将i2c_adapter 注册到系统中。
综上所述,I2C 总线驱动,或者说 I2C 适配器驱动的主要工作就是初始化 i2c_adapter 结构体变量,然后设置 i2c_algorithm 中的 master_xfer 函数。完成以后通过 i2c_add_numbered_adapter或 i2c_add_adapter 这两个函数向系统注册设置好的 i2c_adapter,这两个函数的原型如下:
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
这两个函数的区别在于 i2c_add_adapter 使用动态的总线号,而 i2c_add_numbered_adapter使用静态总线号。
函数参数和返回值含义如下:
adapter 或 adap:要添加到 Linux 内核中的 i2c_adapter,也就是 I2C 适配器。
返回值:0,成功;负值,失败。
如果要删除 I2C 适配器的话使用 i2c_del_adapter 函数即可,函数原型如下:
void i2c_del_adapter(struct i2c_adapter * adap)
函数参数和返回值含义如下:
adap:要删除的 I2C 适配器。
返回值:无。
关于 I2C 的总线(控制器或适配器)驱动就讲解到这里,一般 SOC 的 I2C 总线驱动都是由半导体厂商编写的,比如 I.MX6U 的 I2C 适配器驱动 NXP 已经编写好了,这个不需要用户去编写。因此 I2C 总线驱动对我们这些 SOC 使用者来说是被屏蔽掉的,我们只要专注于 I2C 设备驱动即可。除非你是在半导体公司上班,工作内容就是写 I2C 适配器驱动。当然如果真的很感兴趣,也可以自行研究研究看看。
I2C 设备驱动重点关注两个数据结构:i2c_client 和 i2c_driver,根据总线、设备和驱动模型,I2C 总线上一小节已经讲了。还剩下设备和驱动,i2c_client 就是描述设备信息的,i2c_driver 描述驱动内容,类似于 platform_driver。
i2c_client 结构体定义内容如下:
路径:include/linux/i2c.h
struct i2c_client {
unsigned short flags; /* 标志 */
unsigned short addr; /* 芯片地址,7 位,存在低 7 位*/
char name[I2C_NAME_SIZE]; /* 名字 */
struct i2c_adapter *adapter; /* 对应的 I2C 适配器 */
struct device dev; /* 设备结构体 */
int irq; /* 中断 */
struct list_head detected; /* i2c_driver.client列表或i2c核心的userspace_devices列表的成员 */
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb; /*从模式的回调*/
#endif
};
一个设备对应一个 i2c_client,每检测到一个 I2C 设备就会给这个 I2C 设备分配一个i2c_client。
i2c_driver 类似 platform_driver,是我们编写 I2C 设备驱动重点要处理的内容,i2c_driver 结构体定义内容如下:
路径:include/linux/i2c.h
struct i2c_driver {
unsigned int class;
/* Notifies the driver that a new bus has appeared. You should avoid
* using this, it will be removed in a near future.
*/
int (*attach_adapter)(struct i2c_adapter *) __deprecated;
/* Standard driver model interfaces */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *);
/* Alert callback, for example for the SMBus alert protocol.
* The format and meaning of the data value depends on the protocol.
* For the SMBus alert protocol, there is a single bit of data passed
* as the alert response's low bit ("event flag").
*/
void (*alert)(struct i2c_client *, unsigned int data);
/* a ioctl like command that can be used to perform specific functions
* with the device.
*/
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
struct device_driver driver;
const struct i2c_device_id *id_table;
/* Device detection callback for automatic device creation */
int (*detect)(struct i2c_client *, struct i2c_board_info *);
const unsigned short *address_list;
struct list_head clients;
};
第 11 行,当 I2C 设备和驱动匹配成功以后 probe 函数就会执行,和 platform 驱动一样。
第 29 行,device_driver 驱动结构体,如果使用设备树的话,需要设置 device_driver 的of_match_table 成员变量,也就是驱动的兼容(compatible)属性。
第 30 行,id_table 是传统的、未使用设备树的设备匹配 ID 表。
对于我们 I2C 设备驱动编写人来说,重点工作就是构建 i2c_driver,构建完成以后需要向Linux 内核注册这个 i2c_driver。
i2c_driver 注册函数为 int i2c_register_driver,此函数原型如下:
路径:drivers/i2c/i2c-core.c
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
函数参数和返回值含义如下:
owner:一般为 THIS_MODULE。
driver:要注册的 i2c_driver。
返回值:0,成功;负值,失败。
另外 i2c_add_driver 也常常用于注册 i2c_driver,i2c_add_driver 是一个宏,定义如下:
路径:include/linux/i2c.h
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver)
i2c_add_driver 就是对 i2c_register_driver 做了一个简单的封装,只有一个参数,就是要注册的 i2c_driver。
注销 I2C 设备驱动的时候需要将前面注册的 i2c_driver 从 Linux 内核中注销掉,需要用到i2c_del_driver 函数,此函数原型如下:
路径:drivers/i2c/i2c-core.c
void i2c_del_driver(struct i2c_driver *driver)
函数参数和返回值含义如下:
driver:要注销的 i2c_driver。
返回值:无。
i2c_driver 的注册示例代码如下:
/* i2c 驱动的 probe 函数 */
static int xxx_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
/* 函数具体程序 */
return 0;
}
/* i2c 驱动的 remove 函数 */
static int xxx_remove(struct i2c_client *client)
{
/* 函数具体程序 */
return 0;
}
/* 传统匹配方式 ID 列表 */
static const struct i2c_device_id xxx_id[] = {
{"xxx", 0},
{}
};
/* 设备树匹配列表 */
static const struct of_device_id xxx_of_match[] = {
{ .compatible = "xxx" },
{ /* Sentinel */ }
};
/* i2c 驱动结构体 */
static struct i2c_driver xxx_driver = {
.probe = xxx_probe,
.remove = xxx_remove,
.driver = {
.owner = THIS_MODULE,
.name = "xxx",
.of_match_table = xxx_of_match,
},
.id_table = xxx_id,
};
/* 驱动入口函数 */
static int __init xxx_init(void)
{
int ret = 0;
ret = i2c_add_driver(&xxx_driver);
return ret;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
i2c_del_driver(&xxx_driver);
}
module_init(xxx_init);
module_exit(xxx_exit);
第 16~19 行,i2c_device_id,无设备树的时候匹配 ID 表。
第 22~25 行,of_device_id,设备树所使用的匹配表。
第 28~37 行,i2c_driver,当 I2C 设备和 I2C 驱动匹配成功以后 probe 函数就会执行,这些和 platform 驱动一样,probe 函数里面基本就是标准的字符设备驱动那一套了。
I2C 设备和驱动的匹配过程是由 I2C core来完成的,drivers/i2c/i2c-core.c 就是 I2C 的核心部分,I2C core提供了一些与具体硬件无关的 API 函数,比如前面讲过的:
(1) i2c_adapter 注册/注销函数
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
void i2c_del_adapter(struct i2c_adapter * adap)
(2) i2c_driver 注册/注销函数
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
int i2c_add_driver (struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)
设备和驱动的匹配过程也是由 I2C 总线完成的,I2C 总线的数据结构为 i2c_bus_type,定义在 drivers/i2c/i2c-core.c 文件,i2c_bus_type 内容如下:
路径:drivers/i2c/i2c-core.c
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match,
.probe = i2c_device_probe,
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
};
.match 就是 I2C 总线的设备和驱动匹配函数,在这里就是 i2c_device_match 这个函数,此函数内容如下:
路径:drivers/i2c/i2c-core.c
static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
if (!client)
return 0;
/* Attempt an OF style match */
if (of_driver_match_device(dev, drv))
return 1;
/* Then ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
driver = to_i2c_driver(drv);
/* match on an id table if there is one */
if (driver->id_table)
return i2c_match_id(driver->id_table, client) != NULL;
return 0;
}
第 11 行,of_driver_match_device 函数用于完成设备树设备和驱动匹配。比较 I2C 设备节点的 compatible 属性和 of_device_id 中的 compatible 属性是否相等,如果相当的话就表示 I2C设备和驱动匹配。
第 470 行,acpi_driver_match_device 函数用于 ACPI 形式的匹配。
第 476 行,i2c_match_id 函数用于传统的、无设备树的 I2C 设备和驱动匹配过程。比较 I2C设备名字和 i2c_device_id 的 name 字段是否相等,相等的话就说明 I2C 设备和驱动匹配。
上一小节我们讲解了 Linux 下的 I2C 驱动框架,重点分为 I2C 适配器驱动和 I2C 设备驱动,其中 I2C 适配器驱动就是 SOC 的 I2C 控制器驱动。I2C 设备驱动是需要用户根据不同的 I2C 设备去编写,而 I2C 适配器驱动一般都是 SOC 厂商去编写的,比如 NXP 就编写好了 I.MX6U 的I2C 适配器驱动。
在 imx6ull.dtsi 文件中找到 I.MX6U 的 I2C1 控制器节点,节点内容如下所示:
路径:arch/arm/boot/dts/imx6ull.dtsi
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = ;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
};
i2c2: i2c@021a4000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a4000 0x4000>;
interrupts = ;
clocks = <&clks IMX6UL_CLK_I2C2>;
status = "disabled";
};
i2c3: i2c@021a8000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a8000 0x4000>;
interrupts = ;
clocks = <&clks IMX6UL_CLK_I2C3>;
status = "disabled";
};
上述代码内容就是IMX6ULL中的3个I2C,I2C1、I2C2和I2C3的地址分别是0x021a0000、0x021a4000、0x021a8000。重点关注 i2c1 节点的 compatible 属性值,因为通过 compatible 属性值可以在 Linux 源码里面找到对应的驱动文件。这里i2c1节点的compatible属性值有两个:“fsl,imx6ul-i2c”和“fsl,imx21-i2c”,在 Linux 源码中搜索这两个字符串即可找到对应的驱动文件。
I.MX6U 的 I2C 适配器驱动驱动文件为 drivers/i2c/busses/i2c-imx.c,在此文件中有如下内容:
路径: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);
从上述代码可以看出,I.MX6U 的 I2C 适配器驱动是个标准的 platform 驱动,由此可以看出,虽然 I2C 总线为别的设备提供了一种总线驱动框架,但是 I2C 适配器却是 platform驱动。
第 17 行,“fsl,imx21-i2c”属性值,设备树中 i2c1 节点的 compatible 属性值就是与此匹配上的。因此 i2c-imx.c 文件就是 I.MX6U 的 I2C 适配器驱动文件。
第 24 行,当设备和驱动匹配成功以后 i2c_imx_probe 函数就会执行,i2c_imx_probe 函数就会完成 I2C 适配器初始化工作。
i2c_imx_probe 函数内容如下所示:
路径:drivers/i2c/busses/i2c-imx.c
static int i2c_imx_probe(struct platform_device *pdev)
{
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__);
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "can't get irq number\n");
return irq;
}
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;
i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL);
if (!i2c_imx)
return -ENOMEM;
if (of_id)
i2c_imx->hwdata = of_id->data;
else
i2c_imx->hwdata = (struct imx_i2c_hwdata *)
platform_get_device_id(pdev)->driver_data;
/* Setup i2c_imx driver structure */
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->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 */
i2c_imx->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(i2c_imx->clk)) {
dev_err(&pdev->dev, "can't get I2C clock\n");
return PTR_ERR(i2c_imx->clk);
}
ret = clk_prepare_enable(i2c_imx->clk);
if (ret) {
dev_err(&pdev->dev, "can't enable I2C clock\n");
return ret;
}
/* Request IRQ */
ret = devm_request_irq(&pdev->dev, irq, i2c_imx_isr,
IRQF_NO_SUSPEND, pdev->name, i2c_imx);
if (ret) {
dev_err(&pdev->dev, "can't claim irq %d\n", irq);
goto clk_disable;
}
/* Init queue */
init_waitqueue_head(&i2c_imx->queue);
/* Set up adapter data */
i2c_set_adapdata(&i2c_imx->adapter, i2c_imx);
/* Set up clock divider */
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;
/* Set up chip registers to defaults */
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 */
ret = i2c_add_numbered_adapter(&i2c_imx->adapter);
if (ret < 0) {
dev_err(&pdev->dev, "registration failed\n");
goto clk_disable;
}
/* Set up platform driver data */
platform_set_drvdata(pdev, i2c_imx);
clk_disable_unprepare(i2c_imx->clk);
dev_dbg(&i2c_imx->adapter.dev, "claimed irq %d\n", irq);
dev_dbg(&i2c_imx->adapter.dev, "device resources: %pR\n", res);
dev_dbg(&i2c_imx->adapter.dev, "adapter name: \"%s\"\n",
i2c_imx->adapter.name);
dev_info(&i2c_imx->adapter.dev, "IMX I2C adapter registered\n");
/* Init DMA config if supported */
i2c_imx_dma_request(i2c_imx, phy_addr);
return 0; /* Return OK */
clk_disable:
clk_disable_unprepare(i2c_imx->clk);
return ret;
}
第 15 行,调用 platform_get_irq 函数获取中断号。
第 21~22 行,调用 platform_get_resource 函数从设备树中获取 I2C1 控制器寄存器物理基地址,也就是 0X021A0000。获取到寄存器基地址以后使用 devm_ioremap_resource 函数对其进行内存映射,得到可以在 Linux 内核中使用的虚拟地址。
第 27 行,NXP 使用 imx_i2c_struct 结构体来表示 I.MX 系列 SOC 的 I2C 控制器,这里使用 devm_kzalloc 函数来申请内存。
第 39~44 行,imx_i2c_struct 结构体要有个叫做 adapter 的成员变量,adapter 就是i2c_adapter,这里初始化i2c_adapter。第40行设置i2c_adapter 的algo成员变量为i2c_imx_algo,也就是设置 i2c_algorithm。
第 59~60 行,注册 I2C 控制器中断,中断服务函数为 i2c_imx_isr。
第 73~75 行,设置 I2C 频率默认为 IMX_I2C_BIT_RATE=100KHz,如果设备树节点设置了“clock-frequency”属性的话 I2C 频率就使用 clock-frequency 属性值。
第 80~82 行,设置 I2C1 控制的 I2CR 和 I2SR 寄存器。
第 85 行,调用 i2c_add_numbered_adapter 函数向 Linux 内核注册 i2c_adapter。
第 102 行,申请 DMA,看来 I.MX 的 I2C 适配器驱动采用了 DMA 方式。
i2c_imx_probe 函数主要的工作就是一下两点:
①、初始化 i2c_adapter,设置 i2c_algorithm 为 i2c_imx_algo,最后向 Linux 内核注册i2c_adapter。
②、初始化 I2C1 控制器的相关寄存器。
i2c_imx_algo 包含 I2C1 适配器与 I2C 设备的通信函数 master_xfer,i2c_imx_algo 结构体定义如下:
路径:drivers/i2c/busses/i2c-imx.c
static struct i2c_algorithm i2c_imx_algo = {
.master_xfer = i2c_imx_xfer,
.functionality = i2c_imx_func,
};
我们先来看一下. functionality,functionality用于返回此I2C适配器支持什么样的通信协议,在这里 functionality 就是 i2c_imx_func 函数,i2c_imx_func 函数内容如下:
路径:drivers/i2c/busses/i2c-imx.c
static u32 i2c_imx_func(struct i2c_adapter *adapter)
{
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL
| I2C_FUNC_SMBUS_READ_BLOCK_DATA;
}
重点来看一下 i2c_imx_xfer 函数,因为最终就是通过此函数来完成与 I2C 设备通信的,此函数内容如下:
路径:drivers/i2c/busses/i2c-imx.c
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__);
/* Start I2C transfer */
result = i2c_imx_start(i2c_imx);
if (result)
goto fail0;
/* 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
temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);
dev_dbg(&i2c_imx->adapter.dev,
"<%s> CONTROL: IEN=%d, IIEN=%d, MSTA=%d, MTX=%d, TXAK=%d, RSTA=%d\n",
__func__,
(temp & I2CR_IEN ? 1 : 0), (temp & I2CR_IIEN ? 1 : 0),
(temp & I2CR_MSTA ? 1 : 0), (temp & I2CR_MTX ? 1 : 0),
(temp & I2CR_TXAK ? 1 : 0), (temp & I2CR_RSTA ? 1 : 0));
temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2SR);
dev_dbg(&i2c_imx->adapter.dev,
"<%s> STATUS: ICF=%d, IAAS=%d, IBB=%d, IAL=%d, SRW=%d, IIF=%d, RXAK=%d\n",
__func__,
(temp & I2SR_ICF ? 1 : 0), (temp & I2SR_IAAS ? 1 : 0),
(temp & I2SR_IBB ? 1 : 0), (temp & I2SR_IAL ? 1 : 0),
(temp & I2SR_SRW ? 1 : 0), (temp & I2SR_IIF ? 1 : 0),
(temp & I2SR_RXAK ? 1 : 0));
#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_imx_stop(i2c_imx);
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;
}
第 13 行,调用 i2c_imx_start 函数开启 I2C 通信。
第 53 行,如果是从 I2C 设备读数据的话就调用 i2c_imx_read 函数。
第 54~59 行,向 I2C 设备写数据,如果要用 DMA 的话就使用 i2c_imx_dma_write 函数来完成写数据。如果不使用 DMA 的话就使用 i2c_imx_write 函数完成写数据。
第 66 行,I2C 通信完成以后调用 i2c_imx_stop 函数停止 I2C 通信。
i2c_imx_start、i2c_imx_read、i2c_imx_write 和 i2c_imx_stop 这些函数就是 I2C 寄存器的具体操作函数,函数内容基本这里我们就不详细的分析了。不同SOC其I2C适配器也有差异,所以读者有兴趣可自行阅读自己手上的源码并对照SOC的I2C数据手册即可。
I2C 适配器驱动 SOC 厂商已经替我们编写好了,我们需要做的就是编写具体的设备驱动。那我们一般是如何着手写I2C设备的驱动的呢?
在笔者的其它文章也分析过,描述设备信息 分两种情况,一种是无设备树时,需要自行定义一个设备信息的结构体并初始化;另一种就是有设备树时,在设备树中新建设备节点,并配置设备信息。
(1) 未使用设备树的时候
首先肯定要描述 I2C 设备节点信息,先来看一下没有使用设备树的时候是如何在 BSP 里面描述 I2C 设备信息的,在未使用设备树的时候需要在 BSP 里面使用 i2c_board_info 结构体来描述一个具体的 I2C 设备。i2c_board_info 结构体如下:
路径:include/linux/i2c.h
struct i2c_board_info {
char type[I2C_NAME_SIZE]; /* I2C 设备名字 */
unsigned short flags; /* 标志 */
unsigned short addr; /* I2C 器件地址 */
void *platform_data;
struct dev_archdata *archdata;
struct device_node *of_node;
struct fwnode_handle *fwnode;
int irq;
};
type 和 addr 这两个成员变量是必须要设置的,一个是 I2C 设备的名字,一个是 I2C 设备的
器件地址。
打开 arch/arm/mach-imx/mach-mx35_3ds.c 文件,此文件中关于 OV2640 的 I2C 设备信息描述如下:
路径:arch/arm/mach-imx/mach-mx35_3ds.c
static struct i2c_board_info mx35_3ds_i2c_camera = {
I2C_BOARD_INFO("ov2640", 0x30),
};
上述代码中使用 I2C_BOARD_INFO 来完成 mx35_3ds_i2c_camera 的初始化工作,I2C_BOARD_INFO 是一个宏,定义如下:
路径:include/linux/i2c.h
#define I2C_BOARD_INFO(dev_type, dev_addr) \
.type = dev_type, .addr = (dev_addr)
可以看出,I2C_BOARD_INFO 宏其实就是设置 i2c_board_info 的 type 和 addr 这两个成员
变量,因此mx35_3ds_i2c_camera 结构体成员的主要工作就是设置 I2C 设备名字为 ov2640,ov2640 的器件地址为 0X30。
大家可以在 Linux 源码里面全局搜索 i2c_board_info,会找到大量以 i2c_board_info 定义的
I2C 设备信息,这些就是未使用设备树的时候 I2C 设备的描述方式,当采用了设备树以后就不
会再使用 i2c_board_info 来描述 I2C 设备了。
(2) 使用设备树的时候
使用设备树的时候 I2C 设备信息通过创建相应的节点就行了,比如 NXP 官方的 EVK 开发板在 I2C1 上接了 mag3110 这个磁力计芯片,因此必须在 i2c1 节点下创建 mag3110 子节点,然后在这个子节点内描述 mag3110 这个芯片的相关信息。打开 imx6ull-14x14-evk.dts 这个设备树文件,然后找到如下内容:
路径:arch/arm/boot/dts/imx6ull-14x14-evk.dts
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
mag3110@0e {
compatible = "fsl,mag3110";
reg = <0x0e>;
position = <2>;
};
fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt-parent = <&gpio5>;
interrupts = <0 8>;
};
};
第 8~12 行,向 i2c1 添加 mag3110 子节点,第 8 行“mag3110@0e”是子节点名字,“@”后面的“0e”就是 mag3110 的 I2C 器件地址。第 9 行设置 compatible 属性值为“fsl,mag3110”。
第 10 行的 reg 属性也是设置 mag3110 的器件地址的,因此值为 0x0e。I2C 设备节点的创建重点是 compatible 属性和 reg 属性的设置,一个用于匹配驱动,一个用于设置器件地址。
在 5.2 小节已经说过了,I2C 设备驱动首先要做的就是初始化 i2c_driver 并向 Linux 内核
注册。当设备和驱动匹配以后 i2c_driver 里面的 probe 函数就会执行,probe 函数里面所做的就是字符设备驱动那一套了。一般需要在 probe 函数里面初始化 I2C 设备,要初始化 I2C 设备就必须能够对 I2C 设备寄存器进行读写操作,这里就要用到 i2c_transfer 函数了。
i2c_transfer 函数最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_xfer 函数,对于 I.MX6U 而言就是i2c_imx_xfer 这个函数。i2c_transfer 函数原型如下:
路径:drivers/i2c/i2c-core.c
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 类型的指针参数,I2C 进行数据收发
说白了就是消息的传递,Linux 内核使用 i2c_msg 结构体来描述一个消息。i2c_msg 结构体定义在 include/uapi/linux/i2c.h 文件中,结构体内容如下:
路径:include/uapi/linux/i2c.h
struct i2c_msg {
__u16 addr; /*从地址*/
__u16 flags; /* 标志 */
#define I2C_M_TEN 0x0010 /*这是一个10位芯片地址*/
#define I2C_M_RD 0x0001 /*读取数据,从从属到主*/
#define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */
__u16 len; /*消息长度*/
__u8 *buf; /*指向消息数据的指针*/
};
除了 i2c_transfer 函数,还有另外两个I2C的API函数,这两个函数最终也都会调用i2c_transfer 函数。
I2C 数据发送函数 i2c_master_send,函数原型如下:
路径:drivers\i2c\i2c-core.c
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 数据接收函数为 i2c_master_recv,函数原型如下
路径:drivers\i2c\i2c-core.c
nt i2c_master_recv(const struct i2c_client *client, char *buf, int count)
函数参数和返回值含义如下:
client:I2C 设备对应的 i2c_client。
buf:要接收的数据。
count:要接收的数据字节数,要小于 64KB,因为 i2c_msg 的 len 成员变量是一个 u16(无符号 16 位)类型的数据。
返回值:负值,失败,其他非负值,发送的字节数。
使用 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);
}
第2~5行:设备结构体,在设备结构体里面添加一个指向void的指针成员变量private_data,此成员变量用于保存设备的私有数据。在 I2C 设备驱动中我们一般将其指向 I2C 设备对应的i2c_client。
第 15~39行:xxx_read_regs 函数用于读取 I2C 设备多个寄存器数据。第 18 行定义了一个i2c_msg 数组,2 个数组元素,因为 I2C 读取数据的时候要先发送要读取的寄存器地址,然后再读取数据,所以需要准备两个 i2c_msg。一个用于发送寄存器地址,一个用于读取寄存器值。对于 msg[0],将 flags 设置为 0,表示写数据。msg[0]的 addr 是 I2C 设备的器件地址,msg[0]的 buf成员变量就是要读取的寄存器地址。对于 msg[1],将 flags 设置为 I2C_M_RD,表示读取数据。msg[1]的 buf 成员变量用于保存读取到的数据,len 成员变量就是要读取的数据长度。调用i2c_transfer 函数完成 I2C 数据读操作。
第 49~66 行:xxx_write_regs 函数用于向 I2C 设备多个寄存器写数据,I2C 写操作要比读操作简单一点,因此一个 i2c_msg 即可。数组 b 用于存放寄存器首地址和要发送的数据,第 58 行设置 msg 的 addr 为 I2C 器件地址。第 59 行设置 msg 的 flags 为 0,也就是写数据。第 61 行设置要发送的数据,也就是数组 b。第 62 行设置 msg 的 len 为 len+1,因为要加上一个字节的寄存器地址。最后通过 i2c_transfer 函数完成向 I2C 设备的写操作。
前面我们对I2C的介绍基本都了了解,接下来,就按照前面的分析在编写AP3216C 这是个三合一环境光传感器的驱动程序,并验证它。
在开发板上AP3216C用的是 I2C1 接口使用到了 UART4_TXD 和 UART4_RXD。
AP_INT 对应的 GIO1_IO01 这个 IO,如果使用AP3216C中断功能可配置使用,本次不使用。
当前绝大部分驱动都是基于设备树来写的,所以我们也是基于设备树编写驱动,那我们就来新增对应的节点。
新增I2C的pinctrl节点,内容如下:
(注:该节点NXP已经配置好了)
路径:arch\arm\boot\dts\imx6ull-water-emmc.dts
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;
};
pinctrl_i2c1 就是 I2C1 的 IO 节点,这里将 UART4_TXD 和 UART4_RXD 这两个 IO 分别复用为 I2C1_SCL 和 I2C1_SDA,电气属性都设置为 0x4001b8b0。
新增AP3216C设备节点,内容如下:
路径:arch\arm\boot\dts\imx6ull-water-emmc.dts
ap3216c@1e {
compatible = "water,ap3216c";
reg = <0x1e>;
};
第 231 行,clock-frequency 属性为 I2C 频率,这里设置为 100KHz。
第 233 行,pinctrl-0 属性指定 I2C 所使用的 IO 为示例代码 61.5.1.1 中的 pinctrl_i2c1 子节点。
第 236 行,ap3216c 子节点,@后面的“1e”是 ap3216c 的器件地址。
第 237 行,设置 compatible 值为“water,ap3216c”。
第 238 行,reg 属性也是设置 ap3216c 器件地址的,因此 reg 设置为 0x1e。
第 242~246 行,mag3110 是个磁力计,NXP 官方的 EVK 开发板上接了 mag3110,因此 NXP在 i2c1 节点下添加了 mag3110 这个子节点。笔者的 I.MX6ULL开发板上没有用到mag3110,因此需要将此节点删除掉。
第 247~253 行,NXP 官方 EVK 开发板也接了一个 fxls8471,笔者的 I.MX6ULL开发板上同样没有此器件,所以也要将其删除掉。
修改完设备树后,使用”make dtbs“重新编译后,使用新的dtb启动,可在”/sys/bus/i2c/devices“目录查看I2C设备,查询后如下图所示:
其中”0-001e“就是我们添加的ap3216c设备,可使用”cat 0-001e/name“查看设备节点名称,如下图:
对于i2c设备,其本质也是字符设备的一种,所以我们就把它放到“drivers/char”目录下吧,当然如果实际开发项目中有要求,可自行定义路径。,在“drivers/char”目录下,新建ap3216c.c 和 ap3216creg.h 这两个文件。
ap3216c.c内容如下:
路径:drivers/char/ap3216c.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "ap3216creg.h"
/***************************************************************
/// Copyright © 2018-2023 XINWEN Electronic Technology Co., Ltd.
/// All rights reserved.
/// @file(文件) ap3216c.c
/// @brief(简述) AP3216C驱动程序
/// @details(详情) AP3216C驱动程序
/// @version(版本) 1.0
/// @author(作者) water
/// @date(日期) 2023-06-19
/// Edit History
/// -----------------------------------------------------------
/// DATE NAME DESCRIPTION
/// 2023-07-01 water Create.
///
***************************************************************/
#define AP3216C_CNT 1
#define AP3216C_NAME "ap3216c"
struct ap3216c_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
int major; /* 主设备号 */
void *private_data; /* 私有数据 */
unsigned short ir, als, ps; /* 三个光传感器数据 */
};
static struct ap3216c_dev ap3216cdev;
/*
* @description : 从ap3216c读取多个寄存器数据
* @param - dev: ap3216c设备
* @param - reg: 要读取的寄存器首地址
* @param - val: 读取到的数据
* @param - len: 要读取的数据长度
* @return : 操作结果
*/
static int ap3216c_read_regs(struct ap3216c_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; /* ap3216c地址 */
msg[0].flags = 0; /* 标记为发送数据 */
msg[0].buf = ® /* 读取的首地址 */
msg[0].len = 1; /* reg长度*/
/* msg[1]读取数据 */
msg[1].addr = client->addr; /* ap3216c地址 */
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 {
printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
ret = -EREMOTEIO;
}
return ret;
}
/*
* @description : 向ap3216c多个寄存器写入数据
* @param - dev: ap3216c设备
* @param - reg: 要写入的寄存器首地址
* @param - val: 要写入的数据缓冲区
* @param - len: 要写入的数据长度
* @return : 操作结果
*/
static s32 ap3216c_write_regs(struct ap3216c_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; /* ap3216c地址 */
msg.flags = 0; /* 标记为写数据 */
msg.buf = b; /* 要写入的数据缓冲区 */
msg.len = len + 1; /* 要写入的数据长度 */
return i2c_transfer(client->adapter, &msg, 1);
}
/*
* @description : 读取ap3216c指定寄存器值,读取一个寄存器
* @param - dev: ap3216c设备
* @param - reg: 要读取的寄存器
* @return : 读取到的寄存器值
*/
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
u8 data = 0;
ap3216c_read_regs(dev, reg, &data, 1);
return data;
#if 0
struct i2c_client *client = (struct i2c_client *)dev->private_data;
return i2c_smbus_read_byte_data(client, reg);
#endif
}
/*
* @description : 向ap3216c指定寄存器写入指定的值,写一个寄存器
* @param - dev: ap3216c设备
* @param - reg: 要写的寄存器
* @param - data: 要写入的值
* @return : 无
*/
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
u8 buf = 0;
buf = data;
ap3216c_write_regs(dev, reg, &buf, 1);
}
/*
* @description : 读取AP3216C的数据,读取原始数据,包括ALS,PS和IR, 注意!
* : 如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
* @param - ir : ir数据
* @param - ps : ps数据
* @param - ps : als数据
* @return : 无。
*/
void ap3216c_readdata(struct ap3216c_dev *dev)
{
unsigned char i =0;
unsigned char buf[6];
/* 循环读取所有传感器数据 */
for(i = 0; i < 6; i++)
{
buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);
}
if(buf[0] & 0X80) /* IR_OF位为1,则数据无效 */
dev->ir = 0;
else /* 读取IR传感器的数据 */
dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);
dev->als = ((unsigned short)buf[3] << 8) | buf[2]; /* 读取ALS传感器的数据 */
if(buf[4] & 0x40) /* IR_OF位为1,则数据无效 */
dev->ps = 0;
else /* 读取PS传感器的数据 */
dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int ap3216c_open(struct inode *inode, struct file *filp)
{
filp->private_data = &ap3216cdev;
/* 初始化AP3216C */
ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x04); /* 复位AP3216C */
mdelay(50); /* AP3216C复位最少10ms */
ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X03); /* 开启ALS、PS+IR */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
short data[3];
long err = 0;
struct ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;
ap3216c_readdata(dev);
data[0] = dev->ir;
data[1] = dev->als;
data[2] = dev->ps;
err = copy_to_user(buf, data, sizeof(data));
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int ap3216c_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* AP3216C操作函数 */
static const struct file_operations ap3216c_ops = {
.owner = THIS_MODULE,
.open = ap3216c_open,
.read = ap3216c_read,
.release = ap3216c_release,
};
/*
* @description : i2c驱动的probe函数,当驱动与
* 设备匹配以后此函数就会执行
* @param - client : i2c设备
* @param - id : i2c设备ID
* @return : 0,成功;其他负值,失败
*/
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
/* 1、构建设备号 */
if (ap3216cdev.major) {
ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
} else {
alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);
ap3216cdev.major = MAJOR(ap3216cdev.devid);
}
/* 2、注册设备 */
cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);
/* 3、创建类 */
ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
if (IS_ERR(ap3216cdev.class)) {
return PTR_ERR(ap3216cdev.class);
}
/* 4、创建设备 */
ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);
if (IS_ERR(ap3216cdev.device)) {
return PTR_ERR(ap3216cdev.device);
}
ap3216cdev.private_data = client;
return 0;
}
/*
* @description : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
* @param - client : i2c设备
* @return : 0,成功;其他负值,失败
*/
static int ap3216c_remove(struct i2c_client *client)
{
/* 删除设备 */
cdev_del(&ap3216cdev.cdev);
unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);
/* 注销掉类和设备 */
device_destroy(ap3216cdev.class, ap3216cdev.devid);
class_destroy(ap3216cdev.class);
return 0;
}
/* 传统匹配方式ID列表 */
static const struct i2c_device_id ap3216c_id[] = {
{"water,ap3216c", 0},
{}
};
/* 设备树匹配列表 */
static const struct of_device_id ap3216c_of_match[] = {
{ .compatible = "water,ap3216c" },
{ /* Sentinel */ }
};
/* i2c驱动结构体 */
static struct i2c_driver ap3216c_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.owner = THIS_MODULE,
.name = "ap3216c",
.of_match_table = ap3216c_of_match,
},
.id_table = ap3216c_id,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init ap3216c_init(void)
{
int ret = 0;
ret = i2c_add_driver(&ap3216c_driver);
return ret;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit ap3216c_exit(void)
{
i2c_del_driver(&ap3216c_driver);
}
/* module_i2c_driver(ap3216c_driver) */
module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("water");
ap3216c.h内容如下:
路径:drivers/char/ap3216c.h
#ifndef AP3216C_H
#define AP3216C_H
/***************************************************************
/// Copyright © 2018-2023 XINWEN Electronic Technology Co., Ltd.
/// All rights reserved.
/// @file(文件) ap3216c.h
/// @brief(简述) AP3216C寄存器地址描述头文件
/// @details(详情) AP3216C寄存器地址描述头文件
/// @version(版本) 1.0
/// @author(作者) water
/// @date(日期) 2023-06-19
/// Edit History
/// -----------------------------------------------------------
/// DATE NAME DESCRIPTION
/// 2023-07-01 water Create.
///
***************************************************************/
#define AP3216C_ADDR 0X1E /* AP3216C器件地址 */
/* AP3316C寄存器 */
#define AP3216C_SYSTEMCONG 0x00 /* 配置寄存器 */
#define AP3216C_INTSTATUS 0X01 /* 中断状态寄存器 */
#define AP3216C_INTCLEAR 0X02 /* 中断清除寄存器 */
#define AP3216C_IRDATALOW 0x0A /* IR数据低字节 */
#define AP3216C_IRDATAHIGH 0x0B /* IR数据高字节 */
#define AP3216C_ALSDATALOW 0x0C /* ALS数据低字节 */
#define AP3216C_ALSDATAHIGH 0X0D /* ALS数据高字节 */
#define AP3216C_PSDATALOW 0X0E /* PS数据低字节 */
#define AP3216C_PSDATAHIGH 0X0F /* PS数据高字节 */
#endif
修改Makefile 和Kconfig,增加AP3216C编译选项;
修改Makefile,末尾新增以下内容:
路径:drivers/char/Makefile
obj-$(CONFIG_WATER_AP3216C) += ap3216c.o
修改Kconfig,末尾新增以下内容:
路径:drivers/char/Kconfig
config WATER_AP3216C
tristate "OneFu AP3216C sensor"
depends on I2C && SYSFS
help
If you say yes here you get support for the OneFu AP3216C sensor.
This driver can also be built as a module. If so, the module
will be called AP3216C.
上述新增完成后,即可使用“make menuconfig”进行配置AP3216C一起编译。
新建文件ap3216c_App.c并输入以下内容:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include
#include
#include
#include
#include
/***************************************************************
/// Copyright © 2018-2023 XINWEN Electronic Technology Co., Ltd.
/// All rights reserved.
/// @file(文件) ap3216c_App.c
/// @brief(简述) ap3216c设备测试APP
/// @details(详情) ap3216c设备测试APP
/// @version(版本) 1.0
/// @author(作者) water
/// @date(日期) 2023-07-01
/// Edit History
/// -----------------------------------------------------------
/// DATE NAME DESCRIPTION
/// 2023-07-01 water Create.
/// 测试命令:./ap3216c_App /dev/ap3216c
***************************************************************/
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd;
char *filename;
unsigned short databuf[3];
unsigned short ir, als, ps;
int ret = 0;
if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0) {
printf("can't open file %s\r\n", filename);
return -1;
}
while (1) {
ret = read(fd, databuf, sizeof(databuf));
if(ret == 0) { /* 数据读取成功 */
ir = databuf[0]; /* ir传感器数据 */
als = databuf[1]; /* als传感器数据 */
ps = databuf[2]; /* ps传感器数据 */
printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
}
usleep(200000); /*100ms */
}
close(fd); /* 关闭文件 */
return 0;
}
//编译命令: arm-linux-gnueabihf-gcc ap3216c_App.c -o ap3216c_App
由于修改的驱动,我们将其编进kernel(也可单独模块编译),运行kernel顶级目录下的编译脚本water_build_kernel.sh;或单独运行以下命令:
#!/bin/sh
#清理工程
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
#使用默认配置文件 imx_v7_water_emmc_defconfig 来配置Linux内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_water_emmc_defconfig
#打开Linux的图形配置界面
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
#编译Linux
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
#注1: ARCH=arm 设置目标为arm架构
#注2: CROSS_COMPILE=arm-linux-gnueabihf- 指定编译工具链前缀
#注3: -j16 使用16核编译
运行“make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig”命令时,进入配置界面,我们需要配置一下前面新增的ap3216c驱动,如下:
上图选中“OneFu AP3216C sensor”之后按下“Y”键,选项前面就会出现“*”,然后选择“”保存;
保存到指定使用的配置文件。
然后执行“make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16”等待编译完成即可。
打开命令行,进入到ap3216c_App.c 路径下,执行以下命令编译:
arm-linux-gnueabihf-gcc ap3216c_App.c -o ap3216c_App
将新编译的kernel 和dts 复制到ftp挂载路径下,用来启动。命令如下:
sudo cp arch/arm/boot/dts/imx6ull-water-emmc.dtb ../../../tftp
sudo cp arch/arm/boot/zImage ../../../tftp/
将APP执行文件拷贝到文件系统对应的目录下:
cp ap3216c_App ../../nfs/buildrootfs/root/water_soft/i2c/
文件拷贝完成后,可在设备上查看app执行文件:
然后运行app:
./ap3216c_App /dev/ap3216c
关于i2c_tools的使用请参考以下文章:
<Android开发> Android开发工具- 之-I2C TOOLS工具使用
<Linux开发> linux开发工具-之-I2C TOOLS工具使用
首先,连接到 I2C 上的设备是开漏输出的。以漏极开漏输出(OD)为例,是指将输出级电路结构改为一个漏极开路输出的 MOS 管。这样做的好处在于:
(1)防止短路。
(2)可以实现“线与”逻辑,可以减少一个与门的使用,简化电路。
结论:I2C支持多个主设备与多个从设备连接在同一根总线上,如果不开漏输出,会出现短路现象。
采用开漏输出时,如果没有上拉电阻的存在,只能输出低电位,无法输出高电位。
结论:连接上拉电阻后,才可以输出高电位。
在验证 I2C 接口时,我们同样要对上拉电阻做处理。
如下:以从端驱动 SDA 和 SCL 为例
当从端驱动 SDA/SCL 为低(0)时,从端会直接给 SDA/SCL 一个0值;
如果从端驱动 SDA/SCL 不为低(0)时,从端会给 SDA/SCL 一个高阻态值。
因为高阻态是一个无效驱动,而高电位(1)是一个有效驱动,如果置为1会跟其他的驱动源造成多驱动冲突。为了避免多重驱动的问题,当 Slave 或者 Master 不打算驱动任何一根线时(SDA/SCL),那必须把 SDA/SCL 给一个高阻态(z)值。
经过上拉处理:如果在整个总线上,master 和 slave 都没有给 SDA/SCL 一个有效驱动(即低电位),会通过上拉电阻将 SCL/SDA置为高电位(1)。
注意:这个给的高电位(1)是一个弱信号,可以防止形成多驱动源造成冲突
遵循3个机制
(1)“线与”机制。多主机时,总线具有“线与”的逻辑功能,即只要有一个节点发送低电平时,总线上就表现为低电平。
(2)SDA回读机制。总线被启动后,多个主机在每发送一个数据位时都要对自己的输出电平进行检测,只要检测的电平与自己发出的电平一致,就会继续占用总线。
(3)低电平优先机制。由于线与的存在,当多主机发送时,谁先发送低电平谁就会掌握对总线的控制权。
在多主的通信系统中。总线上有多个节点,它们都有自己的寻址地址,可以作为从节点被别的节点访问,同时它们都可以作为主节点向其他的节点发送控制字节和传送数据。
但是如果有两个或两个以上的节点都向总线上发送启动信号并开始传送数据,这样就形成了冲突。要解决这种冲突,就要进行仲裁的判决,这就是I 2C总线上的仲裁。
I2C总线上的仲裁分两部分:SCL线的同步和SDA线的仲裁。
(1) SCL线的同步(时钟同步)
SCL同步是由于总线具有线“与”的逻辑功能,即只要有一个节点发送低电平时,总线上就表现为低电平。当所有的节点都发送高电平时,总线才能表现为高电平。
正是由于线“与”逻辑功能的原,当多个节点同时发送时钟信号时,在总线上表现的是统一的时钟信号。这就是SCL的同步原理
(2) SDA仲裁
SDA线的仲裁也是建立在总线具有线“与”逻辑功能的原理上的。节点在发送1位数据后,比较总线上所呈现的数据与自己发送的是否一致。是,继续发送;否则,退出竞争。
SDA线的仲裁可以保证I2C总线系统在多个主节点同时企图控制总线时通信正常进行并且数据不丢失。总线系统通过仲裁只允许一个主节点可以继续占据总线。
(3) 仲裁过程
上图是以两个节点为例的仲裁过程。DATA1和DATA2分别是主节点向总线所发送的数据信号,SDA为总线上所呈现的数据信号,SCL是总线上所呈现的时钟信号。当主节点1、2同时发送起始信号时,两个主节点都发送了高电平信号。这时总线上呈现的信号为高电平,两个主节点都检测到总线上的信号与自己发送的信号相同,继续发送数据。第2个时钟周期,2个主节点都发送低电平信号,在总线上呈现的信号为低电平,仍继续发送数据。在第3个时钟周期,主节点1发送高电平信号,而主节点2发送低电平信号。根据总线的线“与”的逻辑功能,总线上的信号为低电平,这时主节点1检测到总线上的数据和自己所发送的数据不一样,就断开数据的输出级,转为从机接收状态。这样主节点2就赢得了总线,而且数据没有丢失,即总线的数据与主节点2所发送的数据一样,而主节点1在转为从节点后继续接收数据,同样也没有丢掉SDA线上的数据。因此在仲裁过程中数据没有丢失。
总结:SDA仲裁和SCL时钟同步处理过程没有先后关系,而是同时进行的。
对于I2C总线通信,这是非常普遍使用的一种通信方式,不管是在MCU中还是SOC中都是常用的功能,同时I2C相关知识也是面试中常会提到的知识,熟悉掌握I2C通信,是作为底层开发人员的必要技术。
本文如有不足,欢迎交流。