Linux之I2C驱动
本文将介绍Linux环境下的I2C驱动开发。文中内容涉及shell脚本,C语言和Linux内核驱动的基础知识。限于篇幅,本文只专注I2C的相关内容,且假设读者对以上基础知识已熟悉。
I2C总线仅使用两条信号线就可实现与设备之间的数据通信。其应用广泛且使用简单。但在Linux内核中却是相当复杂。其核心部分的代码量就有几千行。目的是要构成一个通用的,统一的I2C框架,以适用于各种I2C设备和应用。这个框架自底向上包括四个部分:I2C适配器,I2C核心,I2C通用设备驱动以及I2C用户设备驱动。
I2C适配器
I2C适配器实际上就是一个标准的,用platform_driver结构定义的内核驱动。它包含驱动名称,驱动匹配,以及probe和remove函数等(这些是Linux内核驱动的基础内容,这里就不解释了)。在probe函数中,芯片的I2C接口会被初始化并使能,然后最重要的是它会生成一个I2C适配器结构体(struct i2c_adapter),并调用I2C核心中的i2c_add_numbered_adapter或i2c_add_adapter函数,将这个适配器加载进I2C核心中。
由于各个芯片厂家的I2C总线控制接口不同,因而适配器种类也非常多。Linux内核drivers/i2c/busses目录下的上百个文件就是各种芯片的I2C适配器。I2C适配器通常是由芯片厂家提供的,无需开发者自行开发。
尽管I2C适配器很多,但它们都会生成一个共通的I2C适配器结构体(struct i2c_adapter)作为对上层的 接口。这个结构体定义在include/linux/i2c.h文件中,其中有一项非常重要。它是一个指针,指向一个定义在相同头文件中的I2C算法结构体(struct i2c_algorithm)。该结构体中的master_xfer函数具体实现了对芯片的I2C硬件控制,即I2C的读写操作。这个函数还为上层提供了一个统一的指针接口,指向I2C传参结构体(struct i2c_msg),它定义在include/uapi/linux/i2c.h文件中,是一个应用层也会使用到的结构体。
好了,关于I2C适配器的重要内容都讲到了。归纳起来就是一个内核驱动和三个数据结构体:
- I2C适配器内核驱动:负责初始化和使能I2C,然后生成I2C适配器结构体,并加载进I2C核心。
- I2C适配器结构体(struct i2c_adapter):提供统一的I2C数据接口。
- I2C算法结构体(struct i2c_algorithm):提供I2C的读写操作接口。
- I2C传参结构体(struct i2c_msg):提供I2C收发通信的传参接口。
芯片的I2C接口
上面讲了I2C适配器是内核驱动,用于驱动各类芯片的I2C硬件总线。与之对应,还要有被驱动的设备,即各芯片还要定义I2C设备的各种硬件信息,比如I2C的控制和状态寄存器的地址,中断,时钟,IO口等。这些内容通常都在设备树(.dtsi或.dts)文件中定义。其形式如:
i2c2: i2c@21a4000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6sx-i2c", "fsl,imx21-i2c";
reg = <0x021a4000 0x4000>;
interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6SX_CLK_I2C2>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c2>;
clock-frequency = <100000>;
status = "okay";
};
该I2C设备通过compatible字段与I2C适配器匹配,然后调用probe函数加载进I2C核心。
I2C核心
I2C核心提供了I2C设备的统一管理,并提供了大量的公共函数供内核驱动调用。其代码主要在drivers/i2c/i2c-core-base.c文件中。通过I2C核心提供的函数可以查找到所有加载的I2C适配器。同时也对I2C用户设备和驱动进行统一管理。这在后面的I2C用户设备驱动中会讲到。尽管I2C核心非常重要,代码量也很大,但它涉及I2C内核驱动的内部结构,限于篇幅这里就不对其深入解析了。
I2C通用设备驱动
I2C通用设备驱动不针对任何一种具体的I2C外围硬件设备。它只为应用层提供访问芯片I2C硬件的能力,进而利用其功能与外围设备通信。
I2C通用设备驱动的代码在drivers/i2c/i2c-dev.c文件中。它通过I2C适配器和I2C核心为所有在设备树中定义的I2C设备建立一个设备入口文件。设备文件名是/dev/i2c-N,其中N是一个正整数,当芯片有多个I2C时,N代表各个I2C的序号。通过这个设备文件,用户可以直接访问芯片的I2C。
访问I2C通用设备文件的方法之一
Linux系统中的i2c-tools软件包为访问I2C设备文件提供了极大的方便。用户可以使用命令行与I2C外围设备通信。下面简单介绍这些命令的使用方法。
- 列出I2C设备接口
本例使用的是意大利SECO公司的UDOO-NEO开发板(见https://www.udoo.org/udoo-neo/),该开发板使用的是NXP i.MX 6SoloX芯片。此芯片最多支持4个I2C。上面的结果显示只有3个I2C在设备树中被使能。下面再用i2cdetect扫描一下各个接口,看看有哪些设备地址挂接在这些I2C上。 - 扫描I2C设备地址
由于已知i2c-1没有挂接设备,这里只扫描了i2c-0和i2c-3。I2c-0在地址0x08有一个设备,它连接到了电源管理芯片上,UU表示这个设备已被某个驱动所占用了。I2c-2上挂接有两个设备,地址分别是0x1e和0x20,且还没有被占用。 - 读写I2C设备
这个开发板上0x20地址挂接的是FXAS21002陀螺仪。这个芯片的寄存器地址是从0x00至0x15。其中0x0C寄存器是陀螺仪设备ID,其值固定为0xD7。下面用i2cdump命令查看一下。
也可以使用i2cget命令读某个指定地址的值。
当然也可以使用i2cset命令设置某个指定地址的值。这里就不示范了。
另外还有一个命令i2ctransfer可以支持I2C的多字节连续读写,读写时寄存器地址自动加1或循环至某个指定位置。比如FXAS21002陀螺仪就支持从0x00至0x06地址的连续循环读操作。下面的命令利用这个功能反复读0x00至0x06地址的内容四遍。
有了i2c-tools软件包的支持,就可以编写一个简单的陀螺仪采样监控脚本了。
#!/bin/sh
GYRO_PORT=3
GYRO_ADDR=0x20
gyro_start() {
# 启动陀螺仪采样
i2cset -y $GYRO_PORT $GYRO_ADDR 0x13 0x1A
# 循环读取陀螺仪的状态和采样值,直到用户按CTRL-C键
while true; do
# 读取陀螺仪采样状态和数据
OUT=`i2ctransfer -y $GYRO_PORT r7@$GYRO_ADDR`
STS=$(echo $OUT | cut -d' ' -f 1)
# 判断采样状态OK后打印出采样值
[ "$STS" != "0x00" ] && echo $OUT
done
}
gyro_stop() {
# 停止陀螺仪采样
i2cset -y $GYRO_PORT $GYRO_ADDR 0x13 0
exit 0
}
trap "gyro_stop" 2
trap "gyro_stop" 3
gyro_start
i2c-tools软件包的使用虽然方便,但也有局限。比如上面这个脚本运行速度很慢,并且对采样值的加工也不方便。那么如果使用编程语言代替脚本就更为有利了。
访问I2C通用设备文件的方法之二
下面是用C语言编写的陀螺仪采样监控程序,其功能与上面脚本基本相同,只是提高了采样速率,并且采样输出改进为实际陀螺仪的采样浮点值。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>