赞
踩
\qquad
IIC总线是Philips公司在八十年代初推出的一种串行、半双工总线主要用于近距离、低速的芯片之间的通信;IIC总线有两根双向的信号线一根数据线SDA
用于收发数据,一根时钟线SCL
用于通信双方时钟的同步;IIC总线硬件结构简单,成本较低,因此在各个领域得到了广泛的应用
- 主机->从机,主机对从机发一个字节之后,主机要读取从机的响应信号(主机读SDA线)
A) 主机读SDA为高电平,说明从机无应答(意味着从机接收完毕,主机发送停止信号)
B) 主机读SDA为低电平,说明从机有应答。(可继续发送下一个字节)
- 从机->主机, 主机读取从机一个字节之后,主机要向从机发送一个响应信号(主机写SDA线)
A) 主机写SDA为高电平,从机收到主机的无应答信号之后,从机停止传输,等待主机的停止信号。
B) 主机写SDA为低电平,从机收到主机的应答信号之后,从机继续输出下一字节
在fs4412的数据手册中,关于I2C的寄存器主要涉及如下四个:
在手册中,可以相到这些寄存器对应的地址:
- 第7位:决定是否允许产生应答信号,无论发送还是接收前,需置1
- 第6位:传输时时钟线分频,一般选置1
- 第5位:决定是否开启 发送或接收结束时发通知,无论发送还是接收前,需置1,就是发送或接收结束后要发通知,而实际这个通知就是发送就是给第4位。
- 第4位:接收或发送是否完毕可以通过检查此位是否为1,接收或发送完毕后需置0。状态位,通过检查是否为1来判断是否已完成接怍或发送。(实际编程中,第4位置0后,就开始再次传送。)
I2CSTAT寄存器:状态寄存器
- 第6、7位:每次传输前需选择传输模式
- 第5位: 1、在写模式下,置0产生将产生终止信号,传输前置1产生起始信号。 2、在读模式下,置0表读不忙,置1表读忙
- 第4位:使能数据输出,传输前需置1
(编程中,第4位置1后,就开始传送,之后这位就不动了,关键靠判断I2CCON的第4位是否置0来决定是否再次传送)
\qquad
一般I2C从机设备中,也是由一组属于从机的寄存器在操作从机设备的。这些寄存器,在从机内部也是有地址的,是通过从机内部的地址进行读写操作的。
\qquad
因此,I2C总线通讯时,传送的数据,是一种广义的说法,这些数据可能是从机的地址,从机中的寄存器地址,从机的数据等。这就使得在具体从机设备的通讯时,总线的传送时序存在着人为的定义逻辑,不同从机设备可能不同,具体在传送完成从机的设备地址后,会紧跟着一个要读写的从机的内部工作寄存器地址。如下:
由于从机本身都自带寄存器,与从机设备通信的目的就是读写这些寄存器,而这些寄存器自身也是有地址编码的。所以在具体从机设备的IIC操作过程中,还需要把寄存器地址编码(RA)当成数据来传送,但逻辑上,需要很清楚这是代表了从机寄存器地址编码。因此针对具体从机设备的IIC读、写顺序如下:
主机发送一次数据的伪代码实现:
由于从机本身都自带寄存器,与从机设备通信的目的就是读写这些寄存器,而这些寄存器自身也是有地址编码的。所以在具体从机设备的IIC操作过程中,还需要把寄存器地址编码(RA)当成数据来传送,但逻辑上,需要很清楚这是代表了从机寄存器地址编码。因此针对具体从机设备的IIC读、写顺序如下:
以上注意,NACK表示不给应答信号了,然后直接发一个P(停止位)。
主机接收一次数据的伪代码实现:
\qquad
由于I2C的适配器对SOC来说,是一个二级外设控制器。每个I2C二级外设适配器adapter会提供两路的I2C总线(SDA、SCL)。这两路I2C总线可以外挂多个(二级)外设。这是I2C在硬件层的结构,如下图:
\qquad 根据I2C的硬件逻辑,LINUX在底层的驱动架构上充分应用了软件的分层思想。首先,Linux在内核中对应硬件提供了I2C总线(适配器)驱动层。用于直接驱动I2C Adaptor,这部分已由厂家完成,我们只需要使用即可。其次,I2C core核心层为驱动与设备等提供了对应的接口。最后,则是最上层的I2C从设备驱动层,这层过错成I2C上外接设备的驱动,需要由开发者来实现。以上三层的作用和对应关系详见下面这张图:
\qquad
对于开发者而言,最关心的是I2C从设备驱动层的编写方法。该层的驱动架构与上一章的platform虚拟总线驱动类似。
我们的驱动编写主要关注的是这张图里的i2c_client和i2c_driver这两个对象。
在fs4412板上,有一个MPU6050陀螺仪。这个陀螺仪的驱动接口是I2C。可以拿来做I2C总线驱动的试验。
MPU6050芯片的SCL 引脚接MCU的-> I2C_SCL5 , SDA 引脚接MCU的-> I2C_SDA5。 中断INT引脚接MCU的-> GYRO_INT。查询芯片的数据手册,如下:
mpu6050的中断引脚如图,连接的是MCU的gpx3-3引脚。
SDA、SCL引脚复用了GPB_2 与 GPB_3脚。查询MCU的数据手册,可以知道对应的寄存器数据定义如下。
另外,ADO这个地址控制引脚接地,意味着AD0 = 0 ,查MPU6050的数据手册,如下:
意味着,MPU6050的I2C地址为1101 000(左移一位转成8位为0xD0)。
MPU6050的常用寄存器地址编号
MPU6050的工作需要其内的寄存器充分配合。在MPU6050中每个寄存器都要相应的地址编号,这些地址编号将在I2C的通信中使用。因此,程序中预先把这些地址编号处理成宏,如下(具体含义要查询MPU6050的数据手册):
#define SMPLRT_DIV 0x19 //陀螺仪采样率,典型值:0x07(125Hz) #define CONFIG 0x1A //低通滤波频率,典型值:0x06(5Hz) #define GYRO_CONFIG 0x1B //陀螺仪自检及测量范围,典型值:0xF8(不自检,+/-2000deg/s) #define ACCEL_CONFIG 0x1C //加速计自检、测量范围,典型值:0x19(不自检,+/-G) #define ACCEL_XOUT_H 0x3B //X方向的加速度高8位 #define ACCEL_XOUT_L 0x3C //X方向的加速度低8位 #define ACCEL_YOUT_H 0x3D //Y方向的加速度高8位 #define ACCEL_YOUT_L 0x3E //Y方向的加速度低8位 #define ACCEL_ZOUT_H 0x3F //Z方向的加速度高8位 #define ACCEL_ZOUT_L 0x40 //Z方向的加速度低8位 #define TEMP_OUT_H 0x41 //温度的高8位 #define TEMP_OUT_L 0x42 //温度的低8位 #define GYRO_XOUT_H 0x43 //x轴角速度的高8位 #define GYRO_XOUT_L 0x44 //x轴角速度的低8位 #define GYRO_YOUT_H 0x45 //Y轴角速度的高8位 #define GYRO_YOUT_L 0x46 //Y轴角速度的低8位 #define GYRO_ZOUT_H 0x47 //Z轴角速度的高8位 #define GYRO_ZOUT_L 0x48 //Z轴角速度的低8位 #define PWR_MGMT_1 0x6B //电源管理,典型值:0x00(正常启用)
由于这里所用的fs4412开发板,是samsung 的exynos4系列的开发板系列。因此,对于设备树的含义,需要找linux3.14版本中的binding文档。这里在/Documentation/devicetree/bindings/i2c/i2c-s3c2410.txt文件里有说明:如下:
Samsung’s I2C controller(三星I2C控制器),三星的I2C控制器用于与I2C设备接口。
一、设备树节点所需属性:
- compatible:value应为以下值之一。
(a) “samsung, s3c2410-i2c” ------用于与s3c2410 i2c兼容的i2c。
(b) “samsung, s3c2440-i2c” ------用于与s3c2440 i2c兼容的i2c。
(c ) “samsung, s3c2440-hdmiphy-i2c” ------用于在多个三星SoC上的hdmiphy块中使用的类似于i2c的s3c2440
(d) “samsung, exynos5440-i2c” ------用于s3c2440,如exynos5440上使用的i2c,不需要GPIO配置。
(e) “samsung, exynos5-sata-phy-i2c”------用于类似i2c的s3c2440,用作内部总线上sata phy控制器的主机。- reg:控制器的物理基址和内存映射区域的长度
- interrupts: cpu的中断号。
- samsung,i2c-sda-delay: 应用于数据线(sda)边缘的延迟(以ns为单位)。
二、除“三星,s3c2440-hdmiphy-i2c”之外的所有情况都需要:
a、 Samsung GPIO变体(已弃用):
- gpios:gpios的顺序应如下:<SDA,SCL>。gpio说明符取决于gpio控制器。在所有情况下都需要,但“三星,s3c2440-hdmiphy-i2c”除外,其输入/输出线路永久连接到相应的客户端。
b、Pinctrl变体(首选,如果可用):
- pinctrl-0: 用于此控制器的引脚控制组。
- pinctrl-names: 应该只包含一个值-“default”。
三、Optional properties(可选属性:)
- samsung,i2c-slave-addr: 多主机环境中的从机地址。如果指定,默认值为0。
- samsung,i2c-max-bus-freq: 总线的期望频率(Hz)。如果不指定时,默认值(Hz)为100000。
四、示例:
i2c@13870000 { compatible = "samsung,s3c2440-i2c"; reg = <0x13870000 0x100>; interrupts = <345>; samsung,i2c-sda-delay = <100>; samsung,i2c-max-bus-freq = <100000>; /* Samsung GPIO variant begins here */ gpios = <&gpd1 2 0 /* SDA */ &gpd1 3 0 /* SCL */>; /* Samsung GPIO variant ends here */ /* Pinctrl variant begins here */ pinctrl-0 = <&i2c3_bus>; pinctrl-names = "default"; /* Pinctrl variant ends here */ #address-cells = <1>; #size-cells = <0>; wm8994@1a { compatible = "wlf,wm8994"; reg = <0x1a>; }; };
下面涉及到的设备树为i2c总线的节点,用于内核自带的驱动使用:
exynos4412平台每个i2c通道的信息是通过设备树提供的,因此需要首先在exynos4412-fs4412.dts中增加5通道的节点:
实际编译过后,是这样子的,参考一下:
为了使用内核提供的i2c总线驱动代码,需要进行menuconfig操作,使设备驱动代码i2c-dev.c编译入内核:
有了以上的条件准备。接下来可以开始编程操作MPU6050了。操作方式有两种,
i2c-dev.c 做为官方提供的i2c设备驱动函数,其中定义了应用层文件标准函数open,read,write,ioctl所对应的操作函数i2cdev_open、i2cdev_read、i2cdev_write、i2cdev_ioctl。由于i2c的通讯过程分成不同阶段,因此操作函数如何对应不同阶段。在使用这些函数时就要明确。通过查看源码,把对应的操作对应如下:
应用层 | 驱动 | 对应说明 |
---|---|---|
open | i2cdev_open | \qquad 构造了i2c驱动需要的数据结构,没有对i2c设备进行任何操作。打开该字符特殊文件的实例后,文件描述符开始时仅与i2c_adapter(和总线)关联。 |
ioctl | i2cdev_ioctl |
\qquad
使用I2C_RDWR ioctl(),然后可以立即向该适配器使用的总线上的任何设备发出I2C_msg流量。这是因为i2c_msg向量嵌入了它们需要的所有寻址信息,并直接提交给i2c_adapter。然而,仅SMBus适配器不支持该接口。 \qquad 要在该文件描述符上使用read()/write()系统调用,或使用SMBus接口(并仅与SMBus主机一起工作!),必须首先发出I2C_SLAVE(或I2C_SLAVE_FORCE)ioctl,这会把设备地址写入到i2c_client结构中去,供接下来的read 、write使用。 |
read | i2cdev_read | 该操作完成了发送从设备地址,读标志,并从I2c总线上读取指定的字节数。也就是说read函数完成了如下的操作。 |
write | i2cdev_write | 该操作完成将数据发送到从设备的操作。即如图的操作 |
ioctrl的request参数
这里要专门解释一下ioctrl函数的第二个参数request,由于在i2c_dev.h中有专门的定义,这样实际使用时,需要根据标准定义来使用:
#define I2C_RETRIES 0x0701 /* number of times a device address should be polled when not acknowledging */
#define I2C_TIMEOUT 0x0702 /* set timeout in units of 10 ms */
/* NOTE: Slave address is 7 or 10 bits, but 10-bit addresses are NOT supported! (due to code brokenness)
*/
#define I2C_SLAVE 0x0703 /* Use this slave address */
#define I2C_SLAVE_FORCE 0x0706 /* Use this slave address, even if it is already in use by a driver! */
#define I2C_TENBIT 0x0704 /* 0 for 7 bit addrs, != 0 for 10 bit */
#define I2C_FUNCS 0x0705 /* Get the adapter functionality mask */
#define I2C_RDWR 0x0707 /* Combined R/W transfer (one STOP only) */
#define I2C_PEC 0x0708 /* != 0 to use PEC with SMBus */
#define I2C_SMBUS 0x0720 /* SMBus transfer */
所以通过标准文件I/O接口操作I2C设备。步骤如下:
int fd = open("/dev/i2c-1", O_RDWR);
int addr = 0x50;
if (ioctl(fd, I2C_SLAVE, addr) < 0) {
/* Error */
}
char buf[2] = {0x01, 0x02};
if (write(fd, buf, 2) != 2) {
/* Error */
}
char recv[2] = {0};
if (read(fd, recv, 2) != 2) {
/* Error */
}
close(fd);
通过这种标准文件I/O方式,我们可以很轻松地操作Linux系统中的I2C总线和I2C器件,实现I2C的读写控制。
因为篇幅的原因,把例程放到了
【嵌入式环境下linux内核及驱动学习笔记-(15-1)例程】里了。
请查看其第1节。
以下为翻译了linux3.14源代码中的 /Documentation/i2c/instantiating-devices 文件。
如何实例化I2C设备
\qquad 与PCI或USB设备不同,I2C设备不在硬件级别枚举。相反,软件必须知道每个I2C总线段上连接了哪些设备,以及这些设备使用的地址。因此,内核代码必须显式实例化I2C设备。根据上下文和需求,有几种方法可以实现这一点。
方法1a:通过总线编号声明I2C设备
\qquad 当I2C总线是系统总线时,这种方法适用于许多嵌入式系统。在这样的系统上,每个I2C总线具有预先已知的编号。因此,可以预先声明该总线上的I2C设备。这是通过调用i2c_register_board_info()注册的结构体i2c_board_info的数组来完成的。
示例(来自omap2 h4):
static struct i2c_board_info h4_i2c_board_info[] __initdata = { { I2C_BOARD_INFO("isp1301_omap", 0x2d), .irq = OMAP_GPIO_IRQ(125), }, { /* EEPROM on mainboard */ I2C_BOARD_INFO("24c01", 0x52), .platform_data = &m24c01, }, { /* EEPROM on cpu card */ I2C_BOARD_INFO("24c01", 0x57), .platform_data = &m24c01, }, }; static void __init omap_h4_init(void) { (...) i2c_register_board_info(1, h4_i2c_board_info , ARRAY_SIZE(h4_i2c_board_info)); (...) }
\qquad
上面的代码声明I2C总线1上的3个设备,包括它们各自的地址和它们的驱动程序所需的自定义数据。当讨论的I2C总线注册时,I2C设备将由I2C内核自动实例化。
\qquad
当设备所在的I2C总线消失时,设备将自动解除绑定并销毁(如果有)
方法1b:通过devicetree声明I2C设备
\qquad 该方法与方法1a具有相同的含义。这里通过设备树将I2C设备声明为主控制器的子节点。
i2c1: i2c@400a0000 { /* ... master properties skipped ... */ clock-frequency = <100000>; flash@50 { compatible = "atmel,24c256"; reg = <0x50>; }; pca9532: gpio@60 { compatible = "nxp,pca9532"; gpio-controller; #gpio-cells = <2>; reg = <0x60>; }; };
\qquad 这里,使用100kHz的速度将两个设备连接到总线。有关设置设备可能需要的其他属性,请参阅documentation/devicetree/bindings/中的devicetree文档。
方法1c:通过ACPI声明I2C设备
\qquad ACPI还可以描述I2C设备。对此有专门的文档,目前位于documentation/acpi/enumeration.txt。
方法2:显式实例化设备
\qquad 当较大的设备使用I2C总线进行内部通信时,该方法是适当的。典型的情况是电视适配器。它们可以具有调谐器、视频解码器、音频解码器等,通常通过I2C总线连接到主芯片。您不会预先知道I2C总线的编号,因此无法使用上面描述的方法1。相反,您可以显式实例化I2C设备。这是通过填充结构i2c_board_info并调用i2c_new_device()来完成的。
static struct i2c_board_info sfe4001_hwmon_info = {
I2C_BOARD_INFO("max6647", 0x4e),
};
int sfe4001_init(struct efx_nic *efx)
{
(...)
efx->board_info.hwmon_client =
i2c_new_device(&efx->i2c_adap, &sfe4001_hwmon_info);
(...)
}
\qquad 上面的代码在I2C总线上实例化1个I2C设备,该总线位于所讨论的网络适配器上。
\qquad 这种情况的一种变体是当您不确定I2C设备是否存在时(例如,对于可选功能部件,该功能部件不存在于电路板的廉价变体上,但您无法区分它们),或者它可能在不同的电路板之间具有不同的地址(制造商在未通知的情况下更改其设计)。在这种情况下,您可以调用i2c_new_probed_device(),而不是i2c_new_device.()。
static const unsigned short normal_i2c[] = { 0x2c, 0x2d, I2C_CLIENT_END }; static int usb_hcd_nxp_probe(struct platform_device *pdev) { (...) struct i2c_adapter *i2c_adap; struct i2c_board_info i2c_info; (...) i2c_adap = i2c_get_adapter(2); memset(&i2c_info, 0, sizeof(struct i2c_board_info)); strlcpy(i2c_info.type, "isp1301_nxp", I2C_NAME_SIZE); isp1301_i2c_client = i2c_new_probed_device(i2c_adap, &i2c_info, normal_i2c, NULL); i2c_put_adapter(i2c_adap); (...) }
\qquad 上述代码在OHCI适配器上的I2C总线上实例化多达1个I2C设备。它首先在地址0x2c处尝试,如果在那里找不到任何东西,则尝试地址0x2d,如果仍然找不到,则简单地放弃。
\qquad 实例化I2C设备的驱动程序负责在清理时销毁它。这是通过对先前由i2c_new_device()或i2c_new_probed_device.()返回的指针调用i2c_unregister_device。
方法3:探测某些设备的I2C总线
\qquad 有时,您没有关于I2C设备的足够信息,甚至无法调用I2C_new_probed_device()。典型的例子是PC主板上的硬件监控芯片。有几十种型号,可以存在25个不同的地址。考虑到那里有大量的主板,建立一个正在使用的硬件监控芯片的详尽列表几乎是不可能的。幸运的是,大多数芯片都有制造商和设备ID寄存器,因此可以通过探测来识别它们。
\qquad 在这种情况下,I2C设备既不显式声明也不显式实例化。相反,一旦加载了这些设备的驱动程序,i2c-core将立即探测这些设备,如果找到任何设备,i2c设备将自动实例化。为了防止该机制的任何不当行为,适用以下限制:
*I2C设备驱动程序必须实现detect()方法,该方法通过从任意寄存器读取来标识支持的设备。
*只有可能具有支持的设备并同意被探测的总线才会被探测。例如,这避免了在电视适配器上探测硬件监控芯片。
示例:
\qquad
请参阅drivers/hwmon/lm90.c中的lm90_driver和lm90_detect()
\qquad
当检测到I2C设备的驱动程序被删除时,或当底层I2C总线本身被破坏时,作为这种成功探测的结果实例化的I2C器件将自动销毁,以先发生的为准。
\qquad 熟悉2.4内核和早期2.6内核的i2c子系统的人会发现,这种方法3在本质上类似于在那里所做的工作。两个显著差异是:
*探测现在只是实例化I2C设备的一种方法,而它是当时唯一的方法。在可能的情况下,应首选方法1和2。方法3只能在没有其他方法的情况下使用,因为它可能会产生不良的副作用。
*I2C总线现在必须显式地指出哪些I2C驱动程序类可以探测它们(通过类位字段),而在当时默认情况下探测所有I2C母线。默认值是空类,这意味着不会发生探测。类位字段的目的是限制上述不良副作用。
\qquad 同样,应尽可能避免使用方法3。显式设备实例化(方法1和2)更受欢迎,因为它更安全、更快。
方法4:从用户空间实例化
\qquad 一般来说,内核应该知道连接了哪些I2C设备以及它们所在的地址。然而,在某些情况下,它不知道,因此添加了一个sysfs接口,让用户提供信息。该接口由在每个I2C总线目录中创建的2个属性文件组成:new_device和delete_device。这两个文件都是只写的,您必须将正确的参数写入它们,以便正确地实例化(分别删除)I2C设备。
\qquad 文件new_device采用2个参数:I2C设备的名称(字符串)和I2C设备的地址(数字,通常以十六进制表示,以0x开头,但也可以以十进制表示。)
\qquad 文件delete_device采用单个参数:I2C设备的地址。由于在给定的I2C段上没有两个设备可以位于相同的地址,因此该地址足以唯一地标识要删除的设备。
示例:
# echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-3/new_device
\qquad 虽然此接口应仅在无法进行内核内设备声明时使用,但在各种情况下,它都是有帮助的:
该接口是一些I2C驱动程序实现的force_*模块参数的替代。它在i2c-core中实现,而不是单独在每个设备驱动程序中实现,效率要高得多,并且还有一个优点,即您不必重新加载驱动程序来更改设置。您还可以在加载驱动程序之前或甚至在可用之前实例化设备,并且不需要知道设备需要什么驱动程序。
所在的头文件 /include/linux/i2c.h
struct i2c_client {
unsigned short flags; 一些标志位,用于表示客户端的一些属性。默认是7位地址。其它值见下面的解释
unsigned short addr; 该客户端的 I2C 地址,注意 I2C 使用 7 位地址,所以 addr 的低 7 位存储实际地址
char name[I2C_NAME_SIZE]; 该客户端的名称,最大长度为 I2C_NAME_SIZE
struct i2c_adapter *adapter; 该客户端所连接的 I2C 控制器(adapter)
struct device dev; 该客户端的设备结构(用于内核的设备模型)
int irq; 该客户端发出的中断号
struct list_head detected; 用于 I2C 检测使用的链表
};
#define to_i2c_client(d) container_of(d, struct i2c_client, dev); 宏,用于从device结构体获取i2c_client结构体
这个结构体表示Linux内核中一个I2C客户端设备。分析如下:
这个结构体表示一个I2C从设备,它包含了该设备必要的信息,并且作为设备模型的一部分,可以被统一检测和管理。
当一个I2C设备被检测到时,会分配一个i2c_client结构体来表示它,填充必要信息如地址、适配器、名称等。然后将其添加到适配器上的从设备列表中,以便内核进行管理。
驱动在probe回调中会接收此结构体,并根据信息进一步检测和初始化设备。之后它被作为驱动与设备的连接点,驱动通过它来控制和访问设备。
所以,i2c_client结构体的主要作用是:
所在的头文件 /include/linux/i2c.h
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short flags;
unsigned short addr;
void *platform_data;
struct dev_archdata *archdata;
struct device_node *of_node;
struct acpi_dev_node acpi_node;
int irq;
};
\qquad 在I2C子系统不直接创建i2c_client结构,只是提供struct i2c_board_info结构体息,让子系统动态创建,并注册。因此i2c_board_info是用来协助内核创建i2c_client对象的,所以两个数据结构的成员有一一对应的关系,如下:
这个结构体包含了一个 I2C 设备的所有基本信息,如其类型、地址、中断号、以及一些设备专有的数据等。
内核使用 i2c_board_info 结构体的数组来表示主板上所有的 I2C 设备信息。这样,通过检测 I2C 总线,内核 I2C 核心可以将检测到的设备与这些信息相匹配,选择正确的驱动来管理这些设备。
struct i2c_board_info 和 struct i2c_client 之间的关联:
#include <linux/i2c.h>
#define I2C_BOARD_INFO(dev_type, dev_addr) \
.type = dev_type, .addr = (dev_addr)
这个宏用于静态定义一个i2c_board_info结构体,提供I2C设备的硬件信息。
它有两个参数:
{
.type = dev_type,
.addr = (dev_addr)
}
所以它被用来方便地定义一个i2c_board_info结构体,只需要提供设备类型名称和地址两个信息,其他成员使用默认值。
例如:
static struct i2c_board_info mx25xx_info = {
I2C_BOARD_INFO("mx25xx", 0x35)
};
这会定义一个mx25xx_info,它的值是:
{
.type = "mx25xx",
.addr = 0x35
#include <linux/i2c.h>
int
i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned n);
这个函数用于向内核静态注册I2C设备信息。它接收三个参数:
该函数会将info中提供的n个I2C设备的硬件信息注册到内核中,供内核在系统启动阶段来检测和创建设备。
主要实现步骤为:
例如:
static struct i2c_board_info info[] = {
{ I2C_BOARD_INFO("dev1", 0x10) },
{ I2C_BOARD_INFO("dev2", 0x20) }
};
i2c_register_board_info(2, info, ARRAY_SIZE(info));
这会在I2C总线2上注册两个设备的信息,内核随后会在0x10和0x20地址检测"dev1"和"dev2"两个设备,并创建对应的i2c_client来管理。
这种静态注册信息的方式是Linux内核管理I2C从设备的两种主要方式之一,另一种是 dynamicaly创建设备。两个方式的关键步骤分别由i2c_register_board_info和i2c_new_device两个函数来实现。
理解这个函数,有助于加深对I2C设备管理机制的理解,特别是静态方式下,内核如何根据注册的硬件信息来自动检测和创建设备。这也是Linux内核自动枚举硬件设备的一个途径。
作为板级初始化代码来说,调用这个函数注册I2C设备信息是非常必要的一步,否则内核无法获得设备的存在信息,自然也无法完成自动检测和初始化
所在的头文件 /include/linux/i2c.h
struct i2c_adapter { struct module *owner; unsigned int class; /* classes to allow probing for */ const struct i2c_algorithm *algo; /* the algorithm to access the bus */ void *algo_data; /* data fields that are valid for all devices */ struct rt_mutex bus_lock; int timeout; /* in jiffies */ int retries; struct device dev; /* the adapter device */ 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; }; #define to_i2c_adapter(d) container_of(d, struct i2c_adapter, dev);
struct i2c_adapter 表示一个 I2C 适配器(主控制器)。它包含以下主要字段:
对于 i2c_adapter.class成员的取值定义:
#define I2C_CLASS_HWMON (1<<0) 用于硬件监控设备(如 lm_sensors)
#define I2C_CLASS_DDC (1<<3) 用于显示器的数据通道(Display Data Channel)
#define I2C_CLASS_SPD (1<<7) 用于存储模块(如内存模块)
该函数是根据传入的适配器编号 nr 获取对应的 i2c_adapter 结构体。
原型:
#include <linux/i2c.h>
struct i2c_adapter *i2c_get_adapter(int nr);
参数:
返回值:
NULL:没有找到指定总线编号适配器结构
非NULL:指定nr的适配器结构内存地址*/
该函数会遍历内核的 i2c_adapter 链表,找出编号匹配 nr 的 i2c_adapter,并返回一个指向它的指针。然后用户可以使用这个结构地址就可以给i2c_client结构使用,从而实现i2c_client进行总线绑定,从而增加适配器引用计数。
这个函数在以下情况下典型被调用:
注意:
当使用·i2c_get_adapter·后,需要使用 void i2c_put_adapter(struct i2c_adapter *adap);函数来减少引用计数。当然如果你的适配器驱动不需要卸载,可以不使用)
i2c_put_adapter() 函数的作用是释放之前通过 i2c_get_adapter() 获取的 i2c_adapter 结构体。
它的函数原型如下:
#include <linux/i2c.h>
void i2c_put_adapter(struct i2c_adapter *adap);
参数:
当外部通过 i2c_get_adapter() 获取了一个 i2c_adapter 结构体之后,使用完毕需要调用 i2c_put_adapter() 来释放它。
释放一个 i2c_adapter 主要完成以下工作:
另外,关于i2c_adapter->flags成员的解释:
I2C_ADAP_UNREGISTERING 和 I2C_ADAP_DYING 都是 i2c_adapter->flags 中的标志位。它们的意思如下:
它们存在的目的在于:
#include <linux/i2c.h>
int i2c_add_adapter(struct i2c_adapter *);
void i2c_del_adapter(struct i2c_adapter *);
int i2c_add_numbered_adapter(struct i2c_adapter *);
这三个函数分别用于向内核注册一个新的I2C总线适配器、注销一个已经注册的总线适配器以及注册一个编号指定的总线适配器。
首先是i2c_add_adapter(),它接收一个I2C总线适配器结构体作为参数,用于向内核注册一个新的I2C总线适配器。
主要实现步骤如下:
其次是i2c_del_adapter(),它也接收一个I2C总线适配器作为参数,用于注销一个已经注册的总线适配器。
主要实现步骤如下:
最后是i2c_add_numbered_adapter(),它也接收一个I2C总线适配器作为参数,但是还会接收一个参数nr来指定要注册的适配器编号。除了改变注册后返回的adap->nr编号外,其它实现步骤与i2c_add_adapter()相同。
这三个函数实现了I2C总线适配器的注册、注销和编号指定注册。驱动程序通过调用i2c_add_adapter()或i2c_add_numbered_adapter()来向内核注册一个新的I2C总线,并在不再使用时通过i2c_del_adapter()将其注销。
理解这三个接口有助于理解内核是如何管理I2C总线资源的,特别是总线适配器的注册及注销流程。这也是驱动开发中必不可少的一部分,熟练使用这些函数可以更好地理解I2C子系统中的总线管理机制。
I2C总线驱动的开发工作首先需要向内核注册一个对应的总线适配器,才可以支持该总线上的I2C从设备管理与访问。所以这三个函数的使用也是I2C总线驱动开发中最为基础和必要的部分。
i2c_adapter_id()函数用于根据I2C总线适配器指针获取其ID号。
#include <linux/i2c.h>
static inline int i2c_adapter_id(struct i2c_adapter *adap)
{
return adap->nr;
}
i2c_new_probed_device() 函数的作用是根据传入的 i2c_adapter 和 i2c_board_info 在地址列表 addr_list 中检测设备,并为找到的第一个设备分配一个 i2c_client 结构体。并进行注册。
#include <linux/i2c.h>
struct i2c_client *i2c_new_probed_device(struct i2c_adapter *adap,
struct i2c_board_info *info,
unsigned short const *addr_list,
int (*probe)(struct i2c_adapter * , unsigned short addr));
参数:**
返回值:
该函数的工作过程如下:
实例
另一个例子:
static const unsigned short addr_list[] = { 0x10, 0x15, 0x20 };
static int probe_func(struct i2c_adapter *adap, unsigned short addr)
{
// 实现对addr地址的检测操作
...
return 1; // 检测成功
}
struct i2c_client *client;
client = i2c_new_probed_device(adapter, info, addr_list, probe_func);
这会在0x10、0x15和0x20三个地址上调用probe_func进行检测,如果在0x15成功检测到设备,则会利用info信息动态创建一个i2c_client,并将其添加到adapter上,client指向该新设备。
该函数用于真实I2C总线上动态检测和创建设备,它结合了静态的设备信息和运行时的地址扫描及检测机制。这是Linux内核管理I2C从设备的重要方式之一。
理解这个函数,可以加深对内核如何动态管理I2C从设备的理解。它利用静态信息和运行时检测相结合的方式来实现设备的动态创建
i2c_new_device() 函数的作用是根据传入的 i2c_adapter 和 i2c_board_info 在 info 指定的设备地址上创建一个新的 i2c_client,并注册到 adap 上。这个函数用于动态创建一个I2C从设备。
#include <linux/i2c.h>
struct i2c_client *i2c_new_device(
struct i2c_adapter *adap,
struct i2c_board_info const *info
);
参数:
该函数的主要工作是:
例如:
struct i2c_board_info info = {
I2C_BOARD_INFO("abc", 0x50),
};
struct i2c_client *client;
client = i2c_new_device(adapter, &info);
这会为0x50地址上的"abc"类型设备,动态创建一个i2c_client,并将其添加到adapter总线上。
该函数是动态创建I2C从设备的关键接口,内核利用它根据info中的硬件信息来动态分配和初始化一个i2c_client结构体,代表一个I2C从设备,并将其加入内核设备管理体系。
理解这个函数,可以加深对i2c_board_info和i2c_client结构体作用的理解,以及它们如何协同工作来动态创建I2C从设备的认识。
动态创建设备和静态定义信息后由内核自动检测创建设备,是Linux内核管理I2C从设备的两个主要方式。这个函数则实现了前者的关键步骤。
i2c_unregister_device() 函数的作用是注销一个已注册的 i2c_client 设备。
它的函数原型如下:
#include <linux/i2c.h>
void i2c_unregister_device(struct i2c_client *client);
其中 client 指定要注销的 i2c_client 结构体。
该函数会完成以下工作:
#include <linux/i2c.h> struct i2c_driver { unsigned int class; 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 *); int (*suspend)(struct i2c_client *, pm_message_t mesg); int (*resume)(struct i2c_client *); void (*alert)(struct i2c_client *, unsigned int data); 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; }; #define to_i2c_driver(d) container_of(d, struct i2c_driver, driver)
i2c_driver 结构体代表一个 I2C 设备驱动。它包含了驱动的所有信息,以及不同阶段需要实现的回调函数。
i2c_driver 的主要成员如下:
所以,i2c_driver 封装了一个 I2C 设备驱动模型需要的所有信息 - 支持的设备类型、回调函数、设备列表等,它代表一个完整的 I2C 设备驱动。
I2C 核心根据 i2c_driver 中提供的信息来匹配新发现的 I2C 设备,调用相关回调以初始化设备,并管理整个设备生命周期。
关于i2c_client 与 i2c_driver的匹配:
该数据结构在include/linux/mod_devicetable.h头文件里定义
struct i2c_device_id {
char name[I2C_NAME_SIZE];
kernel_ulong_t driver_data; /* Data private to the driver */
};
当 I2C 主设备检测到一块名为 “mpu6050” 的从设备时,会查找已经注册的哪个 I2C 驱动程序也支持名为 “mpu6050” 的设备,并调用该驱动程序来访问这个从设备。
driver_data 字段允许驱动程序将一些私有数据与每个设备实例相关联。当驱动程序被调用来处理一个设备时,这个字段的数据可被提取,以帮助驱动程序区分不同的设备对象并调用正确的常规方法。
#include <linux/i2c.h>
struct i2c_msg {
__u16 addr; /* slave address */
__u16 flags;
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RD 0x0001 /* read data, from slave to master */
#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; /* msg length */
__u8 *buf; /* pointer to msg data */
};
i2c_msg 结构体代表一个 I2C 消息,它包含执行一个 I2C 操作所需要的所有信息。
i2c_msg 的主要字段如下:
flags 可以包含以下值:
I2C 核心提供的 API 中会使用 i2c_msg 来指定一个 I2C 操作,比如:
i2c_register_driver() 函数的作用是注册一个 i2c_driver,将其加入 I2C 核心的管理。
它的函数原型如下:
#include <linux/i2c.h>
int i2c_register_driver(struct module *, struct i2c_driver *);
#define i2c_add_driver(driver) i2c_register_driver(THIS_MODULE, driver)
参数:
该函数会完成以下工作:
当一个 I2C 设备驱动在初始化时调用该函数,将它的 i2c_driver 注册到 I2C 核心,就完成了驱动与设备的匹配和初始化,驱动就可以开始正常工作了。所以这个函数是实现一个 I2C 设备驱动的关键 - 通过它来注册 i2c_driver,实现与设备的匹配和初始化,使驱动可以开始运行。
对应的,有 i2c_del_driver() 用来注销一个 i2c_driver。
所以这两个函数与 i2c_driver 的生命周期直接相关:
而 i2c_add_driver(driver)是一个宏,可以简化对i2c_register_driver函数的使用。
/* use a define to avoid include chaining to get THIS_MODULE */
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver)
i2c_del_driver() 函数的作用是注销一个已注册的 i2c_driver。
它的函数原型如下:
#include <linux/i2c.h>
void i2c_del_driver(struct i2c_driver *);
参数:
driver 指定要注销的 i2c_driver。
该函数会完成以下工作:
驱动代码如下:
static struct i2c_device_id my_id_table[] = { { "mydev", 0 }, { } }; static int my_probe(struct i2c_client *client, const struct i2c_device_id *id) { // 实现设备初始化 ... } static int my_remove(struct i2c_client *client) { // 实现设备注销 ... } struct i2c_driver my_driver = { .driver = { .name = "mydriver", }, .probe = my_probe, .remove = my_remove, .id_table = my_id_table, }; static int __init my_init(void) { return i2c_register_driver(THIS_MODULE , &my_driver); } static void __exit my_exit(void) { i2c_del_driver(&my_driver); } module_init(my_init); module_exit(my_exit);
这个驱动的主要步骤是:
这 3 个函数的关系是:
i2c_transfer()是最广泛和通用的传输函数,可实现主机与从机之间的任意组合的发送和接收。
i2c_master_send()和i2c_master_recv()是i2c_transfer()的特化版,分别专门用于主机单向发送和接收数据,更简单易用,但功能也比较单一。
所以,在不同的应用场景下,我们可以选择使用:
i2c_transfer() 函数的作用是执行一组 i2c_msg 中描述的 I2C 操作。这个函数实现了在一个 I2C 适配器上执行一组较复杂 I2C 操作的简单机制,通过一组 i2c_msg 进行描述,它会依次执行里面的每个消息,直到全部成功或遇错误。
它的函数原型如下:
#include <linux/i2c.h>
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num);
参数
返回值:
机制:
使用这个函数的典型步骤是:
它使我们可以通过简单的 i2c_msg 来描述一系列的 I2C 读写操作,并统一通过这个函数执行,而不必编写循环来逐个调用 read/write 函数,简化了 I2C 数据交互过程的实现难度。
这是 I2C 核心 API 中一个重要的函数,简化和统一了执行一组复杂 I2C 操作的实现过程,理解并熟练运用这个函数是使用 I2C 核心的基础。
综上,我们只需要构造好所需执行的 I2C 操作描述(以 i2c_msg 形式),然后调用这个函数,就可以在一个 I2C 适配器上执行该组操作,简化了 I2C 数据通信过程的实现。
i2c_master_recv() 函数的作用是从 I2C 总线上的一个从设备读取数据。
它的函数原型如下:
#include <linux/i2c.h>
int i2c_master_recv(const struct i2c_client *client,
char *buf,
int count);
参数:
返回值:
该函数的作用就是在一个 I2C 从设备上执行一个简单的读操作,它发送地址后开始接收数据,直到 count 个字节的数据被读取或出现错误为止。
使用这个函数的基本步骤是:
i2c_master_send() 函数的作用是向 I2C 总线上的一个从设备发送数据。
它的函数原型如下:
#include <linux/i2c.h>
int i2c_master_send(const struct i2c_client *client,
const char *buf,
int count);
参数:
返回值:
失败:负数
成功:成功发送的字节数
该函数的作用就是在一个 I2C 从设备上执行一个简单的写操作,它发送从 buf 中读取的 count 个字节的数据,然后结束操作。
使用这个函数的基本步骤是:
//其它struct file_operations函数实现原理同硬编驱动 static int mpu6050_probe(struct i2c_client *pclt,const struct i2c_device_id *pid) { //做硬编驱动模块入口函数的活 } static int mpu6050_remove(struct i2c_client *pclt) { //做硬编驱动模块出口函数的活 } 以下,把两种匹配方式(a,名称及id匹配;b,设备树匹配)都写在这个框架里了。但实际使用一般只保留对应的方式即可,即以下要么用a方式,要么用b方式。 a、<名称匹配或ID匹配>时定义struct i2c_device_id数组 static struct i2c_device_id mpu6050_ids[] = { {"mpu6050",0}, //..... {} }; b、<设备树>匹配时定义struct of_device_id数组 static struct of_device_id mpu6050_dts[] = { {.compatible = "invensense,mpu6050"}, //.... {} }; 通过定义struct i2c_driver类型的全局变量来创建i2c_driver对象,同时对其主要成员进行初始化 struct i2c_driver mpu6050_driver = { .driver = { .name = "mpu6050", .owner = THIS_MODULE, .of_match_table = mpu6050_dts, }, .probe = mpu6050_probe, .remove = mpu6050_remove, .id_table = mpu6050_ids, }; 以下其实是个宏,展开后相当于实现了模块入口函数和模块出口函数 module_i2c_driver(mpu6050_driver); MODULE_LICENSE("GPL");
框架里有一个宏 module_i2c_driver(mpu6050_driver);
展开以后相当于如下代码:
static int __init mpu6050_driver_init(void)
{
return i2c_add_driver(&mpu6050_driver);
}
static void __exit mpu6050_driver_exit(void)
{
i2c_del_driver(&mpu6050_driver);
}
module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);
设备端根据对I2C的设备地址是否确定可知,又分为两种框架
因为篇幅的原因,把例程放到了
【嵌入式环境下linux内核及驱动学习笔记-(15-1)例程】里了。
请查看其第2节及第3节。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。