本文基于三星Cortex-A9架构,Exynos4412讲解I2C原理、以及基于I2C的mpu6050陀螺仪的数据读取实例(包括在裸机模式下数据的读取以及基于Linux驱动的读取)。还会分析Linux内核I2C架构,篇幅过长,绝对干货。

裸机篇

本篇首先详细讲解I2C时序,然后讲解如何基于三星I2C控制实现裸机读取从设备信息方法。

前言

I2C(Inter-Integrated Circuit)总线(也称 IIC 或 I2C) 是有PHILIPS公司开发的两线式串行总线,用于连接微控制器及外围设备,是微电子通信控制领域广泛采用的一种总线标准。它是同步通信的一种特殊形式,具有接口线少、控制方式简单、器件封装形式小、通信速率较高等优点。

Exynos4412 i2c控制器综述

Exynos4412精简指令集微处理器支持4个IIC总线控制器。为了能使连接在总线上的主和从设备之间传输数据,专用的数据线SDA和时钟信号线SCL被使用,他们都是双向的。

如果工作在多主机的IIC总线模式,多个4412处理器将从从机那接收数据或发送数据给从机。在IIC总线上的主机端4412会启动或终止一个数据传输。4412的IIC总线控制器会用一个标准的IIC总线仲裁机制去实现多主机和多从机传输数据。

通过控制如下寄存器以实现IIC总线上的多主机操作:

  1. 控制寄存器: I2CCON
  2. 状态寄存器: I2CSTAT
  3. Tx/Rx数据偏移寄存器: I2CDS
  4. 地址寄存器: I2CADD

如果I2C总线空闲,那么SCL和SDA信号线将都为高电平。在SCL为高电平期间,如果SDA有由高到低电平的跳变,那么将启动一个起始信号,如果SDA有由低到高电平的跳变,将启动一个结束信号。

主机端的设备总是提供起始和停止信号的一端。在起始信号被发出后,一个数据字节的前7位被当作地址通过SDA线被传输。这个地制值决定了总线上的主设备将要选择那个从设备作为传输对象,bit8决定传输数据的方向(是读还是写)。

I2C总线上的数据(即在SDA上传输的数据)都是以8位字节传输的,在总线上传输操作的过程中,对发送或接收的数据字节数是没有限制的。I2C总线上的主/从设备发送数据总是以一个数据的最高位开始传输(即MSB方式),传输完一个字节后,应答信号紧接其后。

Exynos4412 I2C总线接口特性

  1. 共有9个通道,支持多主、从I2C总线接口。其中8个通道作为普通接口(即I2C0、I2C1…),1个通道作为HDMI的专用接口。
  2. 7位地址模式。
  3. 串行,8位单向或双向的数据传输。
  4. 在标准模式中,每秒最多可以传输100k位,即12.5kB的数据量。
  5. 在快速模式中,每秒最多可以传输400k位,即50kB的数据量。
  6. 支持主机端发送、接收,从机端发送、接收操作。
  7. 支持中断和查询方式。

框图



从上图可以看出,4412提供4个寄存器来完成所有的IIC操作。SDA线上的数据从IICDS寄存器经过移位寄存器发出,或通过移位寄存器传入IICDS寄器;IICADD寄存器中保存4412当做从机时的地址;IICCON、IICSTAT两个寄存器用来控制或标识各种状态,比如选择工作工作模式,发出S信号、P信号,决定是否发出ACK信号,检测是否接收到ACK信号。

I2C总线接口操作

针对4412处理器的I2C总线接口,具备4种操作模式:

  1. 主机发送模式
  2. 主机接收模式
  3. 从机发送模式
  4. 从机接收模式

下面将描述这些操作模式之间的功能关系:

0、数据有效性

SDA线上的数据必须在时钟的高电平周期保持稳定。数据线的高或低电平状态IIC位传输数据的有效性在SCL线的时钟信号是低电平才能改变。

1. 开始和停止条件

当4412的I2C接口空闲时,它往往工作在从机模式。或者说,4412的的i2c接口在SDA线上察觉到一个起始信号之前它应该工作在从机模式。当控制器改变4412的i2c接口的工作模式为主机模式后,SDA线上发起数据传输并且控制器会产生SCL时钟信号。

开始条件通过SDA线进行串行的字节传输,一个停止信号终止数据传输,停止信号是指SCL在高电平器件SDA线有从低到高电平的跳变,主机端产生起始和停止条件。当主、从设备产生一个起始信号后,I2C总线将进入忙状态。这里需要说明的是上述主从设备都有可能作为主机端。

当一个主机发送了一个起始信号后,它也应该发送一个从机地址以通知总线上的从设备。这个地址字节的低7位表示从设备地址,最高位表示传输数据的方向,即主机将要进行读还是写。当最高位是0时,它将发起一个写操作(发送操作);当最高位是1时,它将发起一个读数据的请求(接收操作)。

主机端发起一个结束信号以完成传输操作,如果主机端想在总线上继续进行数据的传输,它将发出另外一个起始信号和从设备地址。用这样的方式,它们可以用各种各样的格式进行读写操作。

下图为起始和停止信号:

2. 数据传输格式

放到SDA线上的所有字节数据的长度应该为8位,在每次传输数据时,对传输数据量没有限制。在起始信号后的第一个数据字节应该包含地址字段,当4412的I2C接口被设置为主模式时,地址字节应该由控制器端发出。在每个字节后,应该有一个应答位。

如果从机要完成一些其他功能后(例如一个内部中断服务程序)才能继续接收或发送下一个字节,从机可以拉低SCL迫使主机进入等待状态。当从机准备好接收下一个数据并释放SCL后,数据传输继续。如果主机在传输数据期间也需要完成一些其他功能(例如一个内部中断服务程序)也可以拉低SCL以占住总线。

下面的图中将说明数据传输格式:

上图中说明,在传输完每个字节数据后,都会有一个应答信号,这个应答信号在第9个时钟周期。具体过程如下(注意下面描述的读写过程都是针对
4412处理器而言,当有具体的I2C设备与4412相连时,数据表示什么需要看具体的I2C设备,4412是不知道数据的含义的):

写过程:主机发送一个起始信号S→发送从机7位地址和1位方向,方向位表示写→主机释放SDA线方便从机给回应→有从机匹配到地址,拉低SDA线作为ACK→主机重新获得SDA传输8位数据→主机释放SDA线方便从机给回应→从机收到数据拉低SDA线作为ACK告诉主机数据接收成功→主机发出停止信号。

读过程:主机发送一个起始信号S→发送从机7位地址和1位方向,方向位表示读→主机释放SDA线方便从机给回应→有从机匹配到地址,拉低SDA线作为ACK→从机继续占用SDA线,用SDA传输8位数据给主机→从机释放SDA线(拉高)方便主机给回应→主机接收到数据→主机获得SDA线控制并拉低SDA线作为ACK告诉从机数据接收成功→主机发出停止信号。

注意:在具体的I2C通信时,要看I2C设备才能确定读写时序,比如下面即将描述的第七大点中的示例,读写EEPROM中就会说道具体的数据含义,读写过程。

3. 应答信号的传输

为了完成一个字节数据的传输,接收方将发送一个应答位给发送方。应答信号出现在SCL线上的时钟周期中的第九个时钟周期,为了发送或接收1个字节的数据,主机端会产生8个时钟周期,为了传输一个ACK位,主机端需要产生一个时钟脉冲。

ACK时钟脉冲到来之际,发送方会在SDA线上设置高电平以释放SDA线。在ACK时钟脉冲之间,接收方会驱动和保持SDA线为低电平,这发生在第9个时钟脉冲为高电平期间。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号(即不发出ACK信号),以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号P。

4. 读写操作

当I2C控制器在发送模式下发送数据后,I2C总线接口将等待直到移位寄存器(I2CDS)接收到一个数据。在往此寄存器写入一个新数据前,SCL线应该保持为低电平,写完数据后,I2C控制器将释放SCL线。当前正在传输的数据传输完成后,4412会捕捉到一个中断,然后cpu将开始往I2CDS寄存器中写入一个新的数据。

当I2C控制器在接收模式下接收到数据后,I2C总线接口将等待直到I2CDS寄存器被读。在读到新数据之前,SCL线会被保持为低电平,读到数据后I2C控制器将释放掉SCL线。一个新数据接收完成后,4412将收到一个中断,cpu收到这个中断请求后,它将从I2CDS寄存器中读取数据。

5. 总线仲裁机制

总线上可能挂接有多个器件,有时会发生两个或多个主器件同时想占用总线的情况,这种情况叫做总线竞争。I2C总线具有多主控能力,可以对发生在SDA线上的总线竞争进行仲裁,其仲裁原则是这样的:当多个主器件同时想占用总线时,如果某个主器件发送高电平,而另一个主器件发送低电平,则发送电平与此时SDA总线电平不符的那个器件将自动关闭其输出级。总线竞争的仲裁是在两个层次上进行的。首先是地址位的比较,如果主器件寻址同一个从器件,则进入数据位的比较,从而确保了竞争仲裁的可靠性。由于是利用I2C总线上的信息进行仲裁,因此不会造成信息的丢失。

6. 终止条件

当一个从接收者不能识别从地址时,它将保持SDA线为高电平。在这样的情况下,主机会产生一个停止信号并且取消数据的传输。当终止传输产生后,主机端接收器会通过取消ACK的产生以告诉从机端发送器结束发送操作。这将在主机端接收器接收到从机端发送器发送的最后一个字节之后发生,为了让主机端产生一个停止条件,从机端发送者将释放SDA线。

7. 配置I2C总线

如果要设置I2C总线中SCL时钟信号的频率,可以在I2CCON寄存器中设置4位分频器的值。I2C总线接口地址值存放在I2C总线地址寄存器(I2CADD)中,默认值未知。

8. 每种模式下的操作流程图

在I2C总线上执行任何的收发Tx/Rx操作前,应该做如下配置:

(1)在I2CADD寄存器中写入从设备地址

(2)设置I2CCON控制寄存器

  • a. 使能中断
  • b. 定义SCL频率

(3)设置I2CSTAT寄存器以使能串行输出

下图为主设备发送模式

下图为主设备接收模式

下图为从设备发送模式

下图为从设备接收模式

I2C控制器寄存器

I2C控制器用到的寄存器如下所示:

1-- I2C总线控制寄存器

IICCON寄存器用于控制是否发出ACK信号、设置发送器的时钟、开启I2C中断,并标识中断是否发生

使用IICCON寄存器时,有如下注意事项

  1. 发送模式的时钟频率由位[6]、位[3:0]联合决定。另外,当 IICCON[6]=0时,IICCON[3:0]不能取0或1。

  2. 位[4]用来标识是否有I2C中断发生,读出为0时标识没有中断发生,读出为1时标识有中断发生。当此位为1时,SCL线被拉低,此时所以I2C传输停止;如果要继续传输,需写入0清除它。

    中断在以下3种情况下发生:

  • 当发送地址信息或接收到一个从机地址并且吻合时;
  • 当总线仲裁失败时;
  • 当发送/接收完一个字节的数据(包括响应位)时;
  1. 基于SDA、SCL线上时间特性的考虑,要发送数据时,先将数据写入IICDS寄存器,然后再清除中断。

  2. 如果IICCON[5]=0,IICCON[4]将不能正常工作,所以,即使不使用I2C中断,也要将IICCON[5]设为1.

2 – I2C状态寄存器

IICSTAT寄存器用于选择I2C接口的工作模式,发出S信号、P信号,使能接收/发送功能,并标识各种状态,比如总线仲裁是否成功、作为从机时是否被寻址、是否接收到0地址、是否接收到ACK信号等。

3 – I2C数据发送/接收移位寄存器

fs4412的i2c总线上挂载了mpu6050

mpu6050每次读取或者要写入数据时,必须先告知从设备要操作的内部寄存器地址(RA),然后紧跟着读取或者写入数据(DATA),内部寄存器的配置和读取一次最多1个data,交互时序如下:

【注意】上述两个时序非常重要,下面我们编写基于linux的驱动编写i2c_msg还要再依赖他。

上述简化时序的术语解释如下

【寄存器使用规则】

下面先提前讲一下具体应用中如何启动和恢复IIC的传输

启动或恢复4412的I2C传输有以下两种方法。

1) 当IICCON[4]即中断状态位为0时,通过写IICSTAT寄存器启动I2C操作。有以下两种情况。

  • 1–在主机模式,

    令IICSTAT[5:4]等于0b11,将发出S信号和IICDS寄存器的数据(寻址),

    令IICSTAT[5:4]等于0b01,将发出P信号。
  • 2–在从机模式,令IICSTAT[4]等于1将等待其他主机发出S信号及地址信息。

2)当IICCON[4]即中断状态为1时,表示I2C操作被暂停。在这期间设置好其他寄存器之后,向IICCON[4]写入0即可恢复I2C操作。所谓“设置其他寄存器”,有以下三种情况:

  • 1–对于主机模式,可以按照上面1的方法写IICSTAT寄存器,恢复I2C操作后即可发出S信号和IICDS寄存器的值(寻址),或发出P信号。
  • 2–对于发送器,可以将下一个要发送的数据写入IICDS寄存器中,恢复I2C操作后即可发出这个数据。
  • 3–对于接收器,可以从IICDS寄存器读出接收到的数据。最后向IICCON[4]写入0的同时,设置IICCON[7]以决定是否在接收到下一个数据后是否发出ACK信号。

MPU6050

MPU-6000(6050)为全球首例整合性6轴运动处理组件,相较于多组件方案,免除了组合陀螺仪与加速器时间轴之差的问题,减少了大量的封装空间。当连接到三轴磁强计时,MPU-60X0提供完整的9轴运动融合输出到其主I2C或SPI端口(SPI仅在MPU-6000上可用)。

MPU-6000(6050)的角速度全格感测范围为±250、±500、±1000与±2000°/sec

(dps),可准确追踪快速与慢速动作,并且,用户可程式控制的加速器全格感测范围为±2g、±4g±8g与±16g。产品传输可透过最高至400kHz的IIC或最高达20MHz的SPI(MPU-6050没有SPI)。

电路图

【MPU6050硬件电路图】(实际板子电路图不一定和下面一样,具体问题具体分析,本例参考exynos-fs4412开发板)

1 AD0接地的 值为 0



所以从设备地址为0x68;

2 SCL、SDA连接的i2c_SCL5、i2c_SDA5

由此可得这两个信号线复用了GPIO的GPB的2、3引脚;

3 查阅exynos4412 datasheet 6.2.2 Part 1可得

所以设置GPIO 的 GPB 【15:8】= 0x33 即可。

MPU6050内部寄存器

mpu6050内部寄存器的使用,参考datasheet《MPU-6000 and MPU-6050

Register Map and Descriptions Revision 4.0 》。

Mpu6050内部有100多个寄存器。比如:

这个寄存器是用来设置加速度属性的,当bit[4:3] 设置为0,表示3个轴的加速度量程最大为±2g。

mpu6050的内部寄存器非常多,并不需要每一个寄存器都需要搞懂,在如下代码实例中,我已经列举出常用的寄存器以及他们的典型值,其他的寄存器不再一一介绍。

下面是个IIC总线实例:

用IIC总线实现CPU与MPU-6050的数据查询

具体代码如下:

//****************************************
// MPU6050常用内部地址,以下地址在mpu6050内部
//****************************************
#define SMPLRT_DIV 0x19 //陀螺仪采样率,典型值:0x07(125Hz)
#define CONFIG 0x1A //低通滤波频率,典型值:0x06(5Hz)
#define GYRO_CONFIG 0x1B //陀螺仪自检及测量范围,典型值:0x18(不自检,2000deg/s)
#define ACCEL_CONFIG 0x1C //加速计自检、测量范围及高通滤波频率,典型值:0x01(不自检,2G,5Hz)
#define ACCEL_XOUT_H 0x3B
#define ACCEL_XOUT_L 0x3C
#define ACCEL_YOUT_H 0x3D
#define ACCEL_YOUT_L 0x3E
#define ACCEL_ZOUT_H 0x3F
#define ACCEL_ZOUT_L 0x40
#define TEMP_OUT_H 0x41
#define TEMP_OUT_L 0x42
#define GYRO_XOUT_H 0x43
#define GYRO_XOUT_L 0x44
#define GYRO_YOUT_H 0x45
#define GYRO_YOUT_L 0x46
#define GYRO_ZOUT_H 0x47
#define GYRO_ZOUT_L 0x48
#define PWR_MGMT_1 0x6B //电源管理,典型值:0x00(正常启用)
#define WHO_AM_I 0x75 //IIC地址寄存器(默认数值0x68,只读)
#define SlaveAddress 0xD0 //IIC写入时的地址字节数据,+1为读取 typedef struct {
unsigned int CON;
unsigned int DAT;
unsigned int PUD;
unsigned int DRV;
unsigned int CONPDN;
unsigned int PUDPDN;
}gpb;
#define GPB (* (volatile gpb *)0x11400040) typedef struct {
unsigned int I2CCON;
unsigned int I2CSTAT;
unsigned int I2CADD;
unsigned int I2CDS;
unsigned int I2CLC;
}i2c5;
#define I2C5 (* (volatile i2c5 *)0x138B0000 ) void mydelay_ms(int time)
{
int i, j;
while(time--)
{
for (i = 0; i < 5; i++)
for (j = 0; j < 514; j++);
}
}
/**********************************************************************
* @brief iic read a byte program body
* @param[in] slave_addr, addr, &data
* @return None
**********************************************************************/ void iic_read(unsigned char slave_addr, unsigned char addr, unsigned char *data)
{
/*根据mpu6050的datasheet,要读取数据必须先执行写操作:写入一个从设备地址,
然后执行读操作,才能读取到该内部寄存器的内容*/
I2C5.I2CDS = slave_addr; //将从机地址写入I2CDS寄存器中
I2C5.I2CCON = (1 << 7)|(1 << 6)|(1 << 5); //设置时钟并使能中断
I2C5.I2CSTAT = 0xf0; //[7:6]设置为0b11,主机发送模式;
//往[5:4]位写0b11,即产生启动信号,发出IICDS寄存器中的地址 while(!(I2C5.I2CCON & (1 << 4))); // 等待传输结束,传输结束后,I2CCON [4]位为1,标识有中断发生; // 此位为1时,SCL线被拉低,此时I2C传输停止;
I2C5.I2CDS = addr; //写命令值
I2C5.I2CCON = I2C5.I2CCON & (~(1 << 4));// I2CCON [4]位清0,继续传输
while(!(I2C5.I2CCON & (1 << 4)));// 等待传输结束 I2C5.I2CSTAT = 0xD0; // I2CSTAT[5:4]位写0b01,发出停止信号
I2C5.I2CDS = slave_addr | 1; //表示要读出数据 I2C5.I2CCON = (1 << 7)|(1 << 6) |(1 << 5) ; //设置时钟并使能中断
I2C5.I2CSTAT = 0xb0;//[7:6]位0b10,主机接收模式; //往[5:4]位写0b11,即产生启动信号,发出IICDS寄存器中的地址
// I2C5.I2CCON = I2C5.I2CCON & (~(1 << 4)); 如果强行关闭,将读取不到数据 while(!(I2C5.I2CCON & (1 << 4)));//等待传输结束,接收数据 I2C5.I2CCON &= ~((1<<7)|(1 << 4));/* Resume the operation & no ack*/
// I2CCON [4]位清0,继续传输,接收数据,
// 主机接收器接收到最后一字节数据后,不发出应答信号 no ack // 从机发送器释放SDA线,以允许主机发出P信号,停止传输;
while(!(I2C5.I2CCON & (1 << 4)));// 等待传输结束 I2C5.I2CSTAT = 0x90;
*data = I2C5.I2CDS;
I2C5.I2CCON &= ~(1<<4); /*clean interrupt pending bit */
mydelay_ms(10);
*data = I2C5.I2CDS;
}
/**************************************************************
* @brief iic write a byte program body
* @param[in] slave_addr, addr, data
* @return None
*************************************************************/ void iic_write (unsigned char slave_addr, unsigned char addr, unsigned char data)
{
I2C5.I2CDS = slave_addr;
I2C5.I2CCON = (1 << 7)|(1 << 6)|(1 << 5) ;
I2C5.I2CSTAT = 0xf0; while(!(I2C5.I2CCON & (1 << 4)));
I2C5.I2CDS = addr;
I2C5.I2CCON = I2C5.I2CCON & (~(1 << 4));
while(!(I2C5.I2CCON & (1 << 4))); I2C5.I2CDS = data;
I2C5.I2CCON = I2C5.I2CCON & (~(1 << 4)); while(!(I2C5.I2CCON & (1 << 4))); I2C5.I2CSTAT = 0xd0;
I2C5.I2CCON = I2C5.I2CCON & (~(1 << 4));
mydelay_ms(10);
}
void MPU6050_Init ()
{
iic_write(SlaveAddress, PWR_MGMT_1, 0x00);
iic_write(SlaveAddress, SMPLRT_DIV, 0x07);
iic_write(SlaveAddress, CONFIG, 0x06);
iic_write(SlaveAddress, GYRO_CONFIG, 0x18);
iic_write(SlaveAddress, ACCEL_CONFIG, 0x01);
}
/*读取mpu6050某个内部寄存器的内容*/
int get_data(unsigned char addr)
{
char data_h, data_l; iic_read(SlaveAddress, addr, &data_h);
iic_read(SlaveAddress, addr+1, &data_l);
return (data_h<<8)|data_l;
}
/*
* 裸机代码,不同于LINUX 应用层, 一定加循环控制
*/
int main(void)
{
int data;
unsigned char zvalue; GPB.CON = (GPB.CON & ~(0xff<<8)) | 0x33<<8; // GPBCON[3], I2C_5_SCL GPBCON[2], I2C_5_SDAmydelay_ms(100);
uart_init(); /*---------------------------------------------------------------*/
I2C5.I2CSTAT = 0xD0;
I2C5.I2CCON &= ~(1<<4); /*clean interrupt pending bit */ /*--------------------------------------------------------------*/
mydelay_ms(100);
MPU6050_Init();
mydelay_ms(100); printf("\n********** I2C test!! ***********\n");
while(1)
{
data = get_data(GYRO_ZOUT_H); printf(" GYRO --> Z <---:Hex: %x", data);
data = get_data(GYRO_XOUT_H);
printf(" GYRO --> X <---:Hex: %x", data); printf("\n");
mydelay_ms(1000);
}
return 0;
}

实验结果如下:

********** I2C test!! ***********
GYRO --> Z <---:Hex: 1c GYRO --> X <---:Hex: feda
GYRO --> Z <---:Hex: fefc GYRO --> X <---:Hex: fed6
GYRO --> Z <---:Hex: fefe GYRO --> X <---:Hex: fed6
GYRO --> Z <---:Hex: fefe GYRO --> X <---:Hex: fedc
GYRO --> Z <---:Hex: fefe GYRO --> X <---:Hex: feda
GYRO --> Z <---:Hex: fefc GYRO --> X <---:Hex: fed6
GYRO --> Z <---:Hex: fefe GYRO --> X <---:Hex: feda
GYRO --> Z <---:Hex: fcf2 GYRO --> X <---:Hex: 202
GYRO --> Z <---:Hex: ec GYRO --> X <---:Hex: faa0
GYRO --> Z <---:Hex: 4c GYRO --> X <---:Hex: e
GYRO --> Z <---:Hex: fe GYRO --> X <---:Hex: fed8
GYRO --> Z <---:Hex: 0 GYRO --> X <---:Hex: fede
GYRO --> Z <---:Hex: 0 GYRO --> X <---:Hex: feda

读写操作代码解析:

写入一个数据流程:

读数据流程:



上图阅读注意点:

  1. 从设备地址是在用的时候应该左移一位|读写位,比如写reg=0x68<1|0,即0xD0;
  2. 主设备发出S信号,需要将I2CSTATn 的bite:5设置为1;
  3. 主设备发出p信号,需要将I2CSTATn 的bite:5设置为0;
  4. 主机发送数据需要将寄存器I2CCONn的bit:4置0,to reume the operation;
  5. 主机等待从设备发送的ack或者data,需要轮训判断I2CCONn的bit:4是否置1;
  6. 代码的理解除了结合功能流程图、时序图、源代码还要结合寄存器说明;
  7. 代码的编写顺序必须严格按照时序和模块流程图执行;
  8. 时序中的每一个数据信号(包括ack、data、reg)的产生或者发送对应的代码都用箭头以及相同的颜色框处;
  9. 对1于read操作,NACK的回复需要在接收最后一个data之前设置I2CCONn :7位为0,这样在收到从设备的data后,才会将SDA拉低。

I2C Linux驱动篇

本篇讲解mpu6050基于Linux的驱动的实现。

Linux I2C架构

Linux内核已经为我们编写好了I2C的架构,从设备信息可以在内核文件中直接写死,也可以通过设备树来提供,我们只需要实现i2c_driver,然后注册到i2c架构中即可。

i2c的内核架构源码位于:

\drivers\i2c

I2C核心(i2c_core)

I2C核心维护了i2c_bus结构体,提供了I2C总线驱动和设备驱动的注册、注销方法,维护了I2C总线的驱动、设备链表,实现了设备、驱动的匹配探测。此部分代码由Linux内核提供。

I2C总线驱动

I2C总线驱动维护了I2C适配器数据结构(i2c_adapter)和适配器的通信方法数据结构(i2c_algorithm)。所以I2C总线驱动可控制I2C适配器产生start、stop、ACK等。此部分代码由具体的芯片厂商提供,比如Samsung、高通。

I2C设备驱动

I2C设备驱动主要维护两个结构体:i2c_driver和i2c_client,实现和用户交互的文件操作集合fops、cdev等。此部分代码就是驱动开发者需要完成的。

Linux内核中描述I2C的四个核心结构体

1)i2c_client—挂在I2C总线上的I2C从设备

每一个i2c从设备都需要用一个i2c_client结构体来描述,i2c_client对应真实的i2c物理设备device。

但是i2c_client不是我们自己写程序去创建的,而是通过以下常用的方式自动创建的:

方法一: 分配、设置、注册i2c_board_info

方法二: 获取adapter调用i2c_new_device

方法三: 通过设备树(devicetree)创建

方法1和方法2通过platform创建,这两种方法在内核3.0版本以前使用所以在这不详细介绍;方法3是最新的方法,3.0版本之后的内核都是通过这种方式创建的,文章后面的案例就按方法3。

2)i2c_adapter

I2C总线适配器,即soc中的I2C总线控制器,硬件上每一对I2C总线都对应一个适配器来控制它。在Linux内核代码中,每一个adapter提供了一个描述它的结构(struct
i2c_adapter),再通过i2c core层将i2c设备与i2c
adapter关联起来。主要用来完成i2c总线控制器相关的数据通信,此结构体在芯片厂商提供的代码中维护。

3)i2c_algorithm

I2C总线数据通信算法,通过管理I2C总线控制器,实现对I2C总线上数据的发送和接收等操作。亦可以理解为I2C总线控制器(适配器adapter)对应的驱动程序,每一个适配器对应一个驱动程序,用来描述适配器和设备之间的通信方法,由芯片厂商去实现的。

4)i2c_driver

用于管理I2C的驱动程序和i2c设备(client)的匹配探测,实现与应用层交互的文件操作集合fops、cdev等。

设备树

  1. 硬件电路图如下:

由上图所示硬件使用的是I2C通道5,



2. 查找exnos4412的datasheet 29.6.1节,对应的基地址为0x138B0000。



3. 由上图可知中断引脚复用的是GPX3_3。

4. 在上一篇中,我们已经得到mpu6050从设备地址为0x68。

linux内核中三星已经为I2C控制器和设备节点的编写提供了说明手册:

\Documentation\devicetree\bindings\i2c\i2c-s3c2410.txt

该文档提供了一个具体范例,如下:

Example:

  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>;
};
};

注意:三星的exynos4412的i2c控制器驱动仍然沿用了s3c2410的驱动。

综上,最终I2C设备树节点编写如下:

 i2c@138B0000 {          基地址是 138B0000
samsung,i2c-sda-delay = <100>;
samsung,i2c-max-bus-freq = <20000>;
pinctrl-0 =<&i2c5_bus>; 通道5
pinctrl-names = "default";
status = "okay";
mpu6050-3-asix@68 {
compatible = "invensense,mpu6050";
reg= <0x68>; 从设备地址
interrupt-parent = <&gpx3>; 中断父节点
interrupts= <3 2>; 中断index=3,中断触发方式:下降沿触发
};
};

其中 外面节点 i2c@138B0000{}是i2c控制器设备树信息,子节点;mpu6050-3-asix@68{}是从设备mpu6050的设备树节点信息。

【注意】关于设备树的编译烧录,本篇不做详细说明,后续会开一篇详细讲述设备树的使用。

结构体之间关系如下:

  1. 设备树节点分为控制器和从设备两部分,控制器节点信息会通过platform总线与控制器驱动匹配,控制器驱动已经由内核提供,结构体如下:
static struct platform_driver s3c24xx_i2c_driver = {
.probe = s3c24xx_i2c_probe,
.remove = s3c24xx_i2c_remove,
.id_table = s3c24xx_driver_ids,
.driver = {
.owner = THIS_MODULE,
.name = "s3c-i2c",
.pm = S3C24XX_DEV_PM_OPS,
.of_match_table = of_match_ptr(s3c24xx_i2c_match),
},
};
#ifdef CONFIG_OF
static const struct of_device_id s3c24xx_i2c_match[] = {
{ .compatible = "samsung,s3c2410-i2c", .data = (void *)0 },
{ .compatible = "samsung,s3c2440-i2c", .data = (void *)QUIRK_S3C2440 },
{ .compatible = "samsung,s3c2440-hdmiphy-i2c",
.data = (void *)(QUIRK_S3C2440 | QUIRK_HDMIPHY | QUIRK_NO_GPIO) },
{ .compatible = "samsung,exynos5440-i2c",
.data = (void *)(QUIRK_S3C2440 | QUIRK_NO_GPIO) },
{ .compatible = "samsung,exynos5-sata-phy-i2c",
.data = (void *)(QUIRK_S3C2440 | QUIRK_POLL | QUIRK_NO_GPIO) },
{},
};
MODULE_DEVICE_TABLE(of, s3c24xx_i2c_match);
#endif
  1. 从设备节点信息最终会通过i2c_bus与i2c_driver匹配,i2c_driver需要由开发者自己注册,并实现字符设备接口和创建设备节点/dev/mpu6050;

  2. 用户通过字符设备节点/dev/mpu6050调用内核的注册的接口函数mpu6050_read_byte、mpu6050_write_byte;

  3. 内核的i2c core模块提供了i2c协议相关的核心函数,在实现读写操作的时候,需要通过一个重要的函数i2c_transfer(),这个函数是i2c核心提供给设备驱动的,通过它发送的数据需要被打包成i2c_msg结构,这个函数最终会回调相应i2c_adapter->i2c_algorithm->master_xfer()接口将i2c_msg对象发送到i2c物理控制器。

【注】实例所用soc是exynos4412,为三星公司所出品,所以i2c控制器设备树节点信息可以参考linux内核根目录以下件:

Documentation\devicetree\bindings\i2c\i2c-s3c2410.txt。

不同的公司设计的i2c控制器设备树节点信息填写格式不尽相同,需要根据具体产品填写。

编写驱动代码

分配、设置、注册i2c_driver结构体

i2c总线驱动模型属于设备模型中的一类,同样struct
i2c_driver结构体继承于struct
driver,匹配方法和设备模型中讲的一样,这里要去匹配设备树,所以必须实现i2c_driver结构体中的driver成员中的of_match_table成员:

如果和设备树匹配成功,那么就会调用probe函数

实现文件操作集合

如何填充i2c_msg?

根据mpu6050的datasheet可知,向mpu6050写入1个data和读取1个值的时序分别如下图所示。

基于Linux的i2c架构编写驱动程序,我们需要用struct i2c_msg结构体来表示上述所有信息。

编写i2c_msg信息原则如下:

  1. 有几个S信号,msg数组就要有几个元素;
  2. addr为从设备地址,通过i2c总线调用注册的probe函数的参数i2c_client传递下来;
  3. len的长度不包括S、AD、ACK、P;
  4. buf为要发送或者要读取的DATA的内存地址。

综上所述:

  1. Single-Byte Write Sequence时序只需要1个i2c_msg,len值为2,buf内容为是RA、DATA;
  2. Single-Byte Read Sequence时序需要2个i2c_msg,len值分别都为1,第1个msg的buf是RA,第2个msg的buf缓冲区用于存取从设备发送的DATA。

I2C内核架构分析

本章以linux3.14.0为参考, 讨论Linux中的i2c控制器驱动是如何实现的。

驱动入口

三星的i2c控制器驱动是基于platform总线实现的,struct platform_driver定义如下:

当设备树节点信息的compatible信息和注册的platform_driver.driver. of_match_table字符串会通过platform总线的macth方法进行配对,匹配成功后会调用probe函数s3c24xx_i2c_probe()。

驱动核心结构

要理解i2c的内核架构首先必须了解一下这几个机构体:

s3c24xx_i2c

该结构体是三星i2c控制器专用结构体,描述了控制器的所有资源,包括用于等待中断唤醒的等待队列、传输i2c_msg的临时指针、记录与硬件通信的状态、中断号、控制器基地址、时钟、i2c_adapter、设备树信息pdata等。i2c控制器初始化的时候会为该控制器创建该结构体变量,并初始化之。

i2c_adapter

对象实现了一组通过一个i2c控制器发送消息的所有信息,
包括时序, 地址等等, 即封装了i2c控制器的"控制信息"。它被i2c主机驱动创建,
通过clien域和i2c_client和i2c_driver相连,
这样设备端驱动就可以通过其中的方法以及i2c物理控制器来和一个i2c总线的物理设备进行交互。

i2c_algorithm

描述一个i2c主机的发送时序的信息,该类的对象algo是i2c_adapter的一个域,其中注册的函数master_xfer()最终被设备驱动端的i2c_transfer()回调。

i2c_msg

描述一个在设备端和主机端之间进行流动的数据, 在设备驱动中打包并通过i2c_transfer()发送。相当于skbuf之于网络设备,urb之于USB设备。

这几个结构体之间关系:

i2c_client

描述一个挂接在硬件i2c总线上的设备的设备信息,即i2c设备的设备对象,与i2c_driver对象匹配成功后通过detected和i2c_driver以及i2c_adapter相连,在控制器驱动与控制器设备匹配成功后被控制器驱动通过i2c_new_device()创建。从设备所挂载的i2c控制器会在初始化的时候保存到成员adapter。

i2c_driver

描述一个挂接在硬件i2c总线上的设备的驱动方法,即i2c设备的驱动对象,通过i2c_bus_type和设备信息i2c_client匹配,匹配成功后通过clients和i2c_client对象以及i2c_adapter对象相连。

如上图所示:Linux内核维护了i2c bus总线,所有的i2c从设备信息都会转换成i2c_client,并注册到i2c总线,没有设备的情况下一般填写在一下文件中:

linux-3.14-fs4412\arch\arm\mach-s5pc100\ Mach-smdkc100.c

内核启动会将i2c_board_info结构体转换成i2c_client。

有设备树的情况下,内核启动会自动将设备树节点转换成i2c_client。

i2c_adapter

我首先说i2c_adapter, 并不是编写一个i2c设备驱动需要它, 通常我们在配置内核的时候已经将i2c控制器的设备信息和驱动已经编译进内核了, 就是这个adapter对象已经创建好了, 但是了解其中的成员对于理解i2c驱动框架非常重要, 所有的设备驱动都要经过这个对象的处理才能和物理设备通信
//include/linux/i2c.h

  • 428–>这个i2c控制器需要的控制算法, 其中最重要的成员是master_xfer()接口, 这个接口是硬件相关的, 里面的操作都是基于具体的SoC i2c寄存器的, 它将完成将数据发送到物理i2c控制器的"最后一公里"
  • 436–>表示这个一个device, 会挂接到内核中的链表中来管理, 其中的
  • 443–>这个节点将一个i2c_adapter对象和它所属的i2c_client对象以及相应的i2c_driver对象连接到一起

下面是2个i2c-core.c提供的i2c_adapter直接相关的操作API, 通常也不需要设备驱动开发中使用。

Adapter初始化

i2c控制器设备树节点信息通过platform总线传递下来,即参数pdev。probe函数主要功能是初始化adapter,申请i2c控制器需要的各种资源,同时通过设备树节点初始化该控制器下的所有从设备,创建i2c_client结构体。

ps3c24xx_i2c_probe

static int   s3c24xx_i2c_probe(struct platform_device *pdev)
{
struct s3c24xx_i2c *i2c;//最重要的结构体
//保存设备树信息
struct s3c2410_platform_i2c *pdata = NULL;
struct resource *res;
int ret; if (!pdev->dev.of_node) {
pdata = dev_get_platdata(&pdev->dev);
if (!pdata) {
dev_err(&pdev->dev, "no platform data\n");
return -EINVAL;
}
} /*为结构体变量i2c分配内存*/
i2c = devm_kzalloc(&pdev->dev, sizeof(struct s3c24xx_i2c), GFP_KERNEL);
if (!i2c) {
dev_err(&pdev->dev, "no memory for state\n");
return -ENOMEM;
}
i2c->pdata = devm_kzalloc(&pdev->dev, sizeof(*pdata), GFP_KERNEL);
if (!i2c->pdata) {
dev_err(&pdev->dev, "no memory for platform data\n");
return -ENOMEM;
}
/*i2c控制器的一些特殊行为
#define QUIRK_S3C2440 (1 << 0)
#define QUIRK_HDMIPHY (1 << 1)
#define QUIRK_NO_GPIO (1 << 2)
#define QUIRK_POLL (1 << 3)
其中bite:3如果采用轮训方式与底层硬件通信值为1,中断方式值为0*/
i2c->quirks = s3c24xx_get_device_quirks(pdev);
if (pdata)
memcpy(i2c->pdata, pdata, sizeof(*pdata));
else
s3c24xx_i2c_parse_dt(pdev->dev.of_node, i2c); strlcpy(i2c->adap.name, "s3c2410-i2c", sizeof(i2c->adap.name));
i2c->adap.owner = THIS_MODULE;
/*为i2c_msg传输方法赋值,*/
i2c->adap.algo = &s3c24xx_i2c_algorithm;
i2c->adap.retries = 2;
i2c->adap.class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
i2c->tx_setup = 50;
//初始化等待队列,该等待队列用于唤醒读写数据的进程
init_waitqueue_head(&i2c->wait);
/* find the clock and enable it */
i2c->dev = &pdev->dev;
//获取时钟
i2c->clk = devm_clk_get(&pdev->dev, "i2c");
if (IS_ERR(i2c->clk)) {
dev_err(&pdev->dev, "cannot get clock\n");
return -ENOENT;
}
dev_dbg(&pdev->dev, "clock source %p\n", i2c->clk);
/* map the registers */
//通过pdev得到i2c控制器的寄存器地址资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
//映射i2c控制器的物理基地址为虚拟基地址
i2c->regs = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(i2c->regs))
return PTR_ERR(i2c->regs);
dev_dbg(&pdev->dev, "registers %p (%p)\n",
i2c->regs, res); /* setup info block for the i2c core */
/*将结构体变量i2c保存到i2c_adapter的私有变量指针algo_data,
编写i2c设备驱动可以通过adapter指针找到结构体i2c*/
i2c->adap.algo_data = i2c;
i2c->adap.dev.parent = &pdev->dev;
i2c->pctrl = devm_pinctrl_get_select_default(i2c->dev); /* inititalise the i2c gpio lines */
//得到i2c复用的gpio引脚并初始化
if (i2c->pdata->cfg_gpio) {
i2c->pdata->cfg_gpio(to_platform_device(i2c->dev));
} else if (IS_ERR(i2c->pctrl) && s3c24xx_i2c_parse_dt_gpio(i2c)) {
return -EINVAL;
} /* initialise the i2c controller */
clk_prepare_enable(i2c->clk); /*将从设备地址写入寄存器S3C2410_IICADD,同时初始化时钟频率*/
ret = s3c24xx_i2c_init(i2c);
clk_disable_unprepare(i2c->clk);
if (ret != 0) {
dev_err(&pdev->dev, "I2C controller init failed\n");
return ret;
} /* find the IRQ for this unit (note, this relies on the init call to
* ensure no current IRQs pending
*/
if (!(i2c->quirks & QUIRK_POLL)) {
/*从plat_device中获得中断号*/
i2c->irq = ret = platform_get_irq(pdev, 0);
if (ret <= 0) {
dev_err(&pdev->dev, "cannot find IRQ\n");
return ret;
}
/*注册中断处理函数s3c24xx_i2c_irq()*/
ret = devm_request_irq(&pdev->dev, i2c->irq, s3c24xx_i2c_irq, 0,
dev_name(&pdev->dev), i2c); if (ret != 0) {
dev_err(&pdev->dev, "cannot claim IRQ %d\n", i2c->irq);
return ret;
}
} ret = s3c24xx_i2c_register_cpufreq(i2c);
if (ret < 0) {
dev_err(&pdev->dev, "failed to register cpufreq notifier\n");
return ret;
} /* Note, previous versions of the driver used i2c_add_adapter()
* to add the bus at any number. We now pass the bus number via
* the platform data, so if unset it will now default to always
* being bus 0.
*/
/*保存i2c控制器的通道号,本例是bus 5*/
i2c->adap.nr = i2c->pdata->bus_num;
i2c->adap.dev.of_node = pdev->dev.of_node;
//注册adapter
ret = i2c_add_numbered_adapter(&i2c->adap);
if (ret < 0) {
dev_err(&pdev->dev, "failed to add bus to i2c core\n");
s3c24xx_i2c_deregister_cpufreq(i2c);
return ret;
} /*保存私有变量i2c到pdev->dev->p->driver_data*/
platform_set_drvdata(pdev, i2c);
pm_runtime_enable(&pdev->dev);
pm_runtime_enable(&i2c->adap.dev);
dev_info(&pdev->dev, "%s: S3C I2C adapter\n", dev_name(&i2c->adap.dev)); return 0;
}

i2c_add_numbered_adapter

老版本的注册函数为i2c_add_adapter()新的版本对该函数做了封装,将i2c控制的通道号做了注册,默认情况下nr值为0.

i2c_add_numbered_adapter->__i2c_add_numbered_adapter-> i2c_register_adapter

int   i2c_add_numbered_adapter(struct i2c_adapter *adap)
{
if (adap->nr == -1) /* -1 means dynamically assign bus id */
return i2c_add_adapter(adap);
return __i2c_add_numbered_adapter(adap);
}

i2c_add_adapter

static int   i2c_register_adapter(struct i2c_adapter *adap)
{
int res = 0; /* Can't register until after driver model init */
if (unlikely(WARN_ON(!i2c_bus_type.p))) {
res = -EAGAIN;
goto out_list;
} /* Sanity checks */
if (unlikely(adap->name[0] == '\0')) {
pr_err("i2c-core: Attempt to register an adapter with "
"no name!\n");
return -EINVAL;
} if (unlikely(!adap->algo)) {
pr_err("i2c-core: Attempt to register adapter '%s' with "
"no algo!\n", adap->name); return -EINVAL;
}
rt_mutex_init(&adap->bus_lock);
mutex_init(&adap->userspace_clients_lock);
INIT_LIST_HEAD(&adap->userspace_clients); /* Set default timeout to 1 second if not already set */
if (adap->timeout == 0)
adap->timeout = HZ; //设置adapter名字,本例注册后会生成以下节点/dev/i2c-5
dev_set_name(&adap->dev, "i2c-%d", adap->nr);
adap->dev.bus = &i2c_bus_type;
adap->dev.type = &i2c_adapter_type;
res = device_register(&adap->dev);
if (res)
goto out_list; dev_dbg(&adap->dev, "adapter [%s] registered\n", adap->name);
#ifdef CONFIG_I2C_COMPAT
res = class_compat_create_link(i2c_adapter_compat_class, &adap->dev,
adap->dev.parent);
if (res)
dev_warn(&adap->dev,
"Failed to create compatibility class link\n");
#endif /* bus recovery specific initialization */
/*初始化sda、scl,通常这两个引脚会复用gpio引脚*/
if (adap->bus_recovery_info) {
struct i2c_bus_recovery_info *bri = adap->bus_recovery_info; if (!bri->recover_bus) {
dev_err(&adap->dev, "No recover_bus() found, not using recovery\n");
adap->bus_recovery_info = NULL;
goto exit_recovery;
} /* Generic GPIO recovery */
if (bri->recover_bus == i2c_generic_gpio_recovery) {
if (!gpio_is_valid(bri->scl_gpio)) {
dev_err(&adap->dev, "Invalid SCL gpio, not using recovery\n");
adap->bus_recovery_info = NULL;
goto exit_recovery;
} if (gpio_is_valid(bri->sda_gpio))
bri->get_sda = get_sda_gpio_value;
else
bri->get_sda = NULL; /*sda、scl资源赋值*/
bri->get_scl = get_scl_gpio_value;
bri->set_scl = set_scl_gpio_value;
} else if (!bri->set_scl || !bri->get_scl) {
/* Generic SCL recovery */
dev_err(&adap->dev, "No {get|set}_gpio() found, not using recovery\n");
adap->bus_recovery_info = NULL;
}
}
exit_recovery:
/* create pre-declared device nodes */
/*通过设备树节点注册所有该控制器下的所有从设备*/
of_i2c_register_devices(adap);
acpi_i2c_register_devices(adap);
/*与动态分配的总线号相关,动态分配的总线号应该是从已经现有最大总线号基础上+1的,
这样能够保证动态分配出的总线号与板级总线号不会产生冲突
在没有设备树情况下,会基于队列__i2c_board_list, 创建i2c_client
其中节点struct i2c_board_info手动填写*/
if (adap->nr < __i2c_first_dynamic_bus_num)
i2c_scan_static_board_info(adap); /* Notify drivers */
mutex_lock(&core_lock);
bus_for_each_drv(&i2c_bus_type, NULL, adap, __process_new_adapter);
mutex_unlock(&core_lock);
return 0;
out_list:
mutex_lock(&core_lock);
idr_remove(&i2c_adapter_idr, adap->nr);
mutex_unlock(&core_lock);
return res;
}

of_i2c_register_devices

该函数用于将从设备节点转换成i2c_client,并注册到i2c总线上。

static void   of_i2c_register_devices(struct i2c_adapter *adap)
{
void *result;
struct device_node *node; /* Only register child devices if the adapter has a node pointer set */
if (!adap->dev.of_node)
return; dev_dbg(&adap->dev, "of_i2c: walking child nodes\n");
for_each_available_child_of_node(adap->dev.of_node, node) {
struct i2c_board_info info = {};
struct dev_archdata dev_ad = {};
const __be32 *addr;
int len; dev_dbg(&adap->dev, "of_i2c: register %s\n", node->full_name); if (of_modalias_node(node, info.type, sizeof(info.type)) < 0) {
dev_err(&adap->dev, "of_i2c: modalias failure on %s\n",
node->full_name); continue;
} /*获取从设备的地址*/
addr = of_get_property(node, "reg", &len);
if (!addr || (len < sizeof(int))) {
dev_err(&adap->dev, "of_i2c: invalid reg on %s\n",
node->full_name);
continue;
} /*存储从设备地址*/
info.addr = be32_to_cpup(addr);
if (info.addr > (1 << 10) - 1) {
dev_err(&adap->dev, "of_i2c: invalid addr=%x on %s\n",
info.addr, node->full_name);
continue;
} /*获取中断号*/
info.irq = irq_of_parse_and_map(node, 0);
info.of_node = of_node_get(node);
info.archdata = &dev_ad;
/*获取设备树节点wakeup-source信息*/
if (of_get_property(node, "wakeup-source", NULL))
info.flags |= I2C_CLIENT_WAKE;
request_module("%s%s", I2C_MODULE_PREFIX, info.type);
/*将i2c_board_info转换成i2c_client并注册到i2c总线*/
result = i2c_new_device(adap, &info);
if (result == NULL) {
dev_err(&adap->dev, "of_i2c: Failure registering %s\n",
node->full_name);
of_node_put(node);
irq_dispose_mapping(info.irq);
continue;
}
}
}

i2c_new_device ( )

将i2c_board_info转换成i2c_client并注册到Linux核心。

{

       struct i2c_client      *client;
int status; /*给i2c_client分配内存*/
client = kzalloc(sizeof *client, GFP_KERNEL);
if (!client)
return NULL; /*将adapter的地址保存到i2c_client->adapter,
在驱动函数中可以通过i2c_client找到adapter*/
client->adapter = adap;
client->dev.platform_data = info->platform_data; if (info->archdata)
client->dev.archdata = *info->archdata; /*保存从设备地址类型*/
client->flags = info->flags;
/*保存从设备地址*/
client->addr = info->addr;
/*保存从设备中断号*/
client->irq = info->irq; strlcpy(client->name, info->type, sizeof(client->name)); /* Check for address validity */
/*检测从设备地址是否合法,主要检查位数*/
status = i2c_check_client_addr_validity(client);
if (status) {
dev_err(&adap->dev, "Invalid %d-bit I2C address 0x%02hx\n",
client->flags & I2C_CLIENT_TEN ? 10 : 7, client->addr);
goto out_err_silent;
} /* Check for address business */
/*检测从设备地址是否被占用,同一个控制器下同一个从设备地址只能注册一次*/
status = i2c_check_addr_busy(adap, client->addr);
if (status)
goto out_err; /*建立从设备与适配器的父子关系*/
client->dev.parent = &client->adapter->dev;
client->dev.bus = &i2c_bus_type;
client->dev.type = &i2c_client_type;
client->dev.of_node = info->of_node;
ACPI_COMPANION_SET(&client->dev, info->acpi_node.companion); i2c_dev_set_name(adap, client);
/*注册到Linux核心*/
status = device_register(&client->dev);
if (status)
goto out_err; dev_dbg(&adap->dev, "client [%s] registered with bus id %s\n",
client->name, dev_name(&client->dev)); return client;
out_err:
dev_err(&adap->dev, "Failed to register i2c client %s at 0x%02x "
"(%d)\n", client->name, client->addr, status);
out_err_silent:
kfree(client);
return NULL;
}

i2c_msg如何传递?

核心方法i2c_transfer

l i2c_transfer()是i2c核心提供给设备驱动的发送方法, 通过它发送的数据需要被打包成i2c_msg, 这个函数最终会回调相应i2c_adapter->i2c_algorithm->master_xfer()接口将i2c_msg对象发送到i2c物理控制器,

i2c_adapte->algo在函数s3c24xx_i2c_probe()中赋值:

该变量定义如下:



i2c_transfer()最终会调用函数s3c24xx_i2c_xfer();

i2c_msg中断传输

以下是一次i2c_msg传输的中断模式的大概步骤:

  1. i2c_transfer()首先通过函数i2c_trylock_adapter()尝试获得adapter的控制权。如果adapter正在忙则返回错误信息;

  2. __i2c_transfer()通过调用方法adap->algo->master_xfer(adap, msgs, num)传输i2c_msg,如果失败会尝试重新传送,重传次数最多adap->retries;

  3. adap->algo->master_xfer()就是函数s3c24xx_i2c_xfer(),该函数最终调用 s3c24xx_i2c_doxfer(i2c, msgs, num)传输信息;

  4. s3c24xx_i2c_doxfer()通过函数 s3c24xx_i2c_message_start(i2c, msgs)产生S和AD+W的信号,然后通过函数wait_event_timeout( )阻塞在等待队列i2c->wait上;

  5. 右上角时序mpu6050的写和读的时序,从设备回复ACK和DATA都会发送中断信号给CPU。每次中断都会调用s3c24xx_i2c_irq->i2c_s3c_irq_nextbyte,

  6. 最后一次中断,所有数据发送或读取完毕会调用s3c24xx_i2c_stop->s3c24xx_i2c_master_complete,通过wake_up唤醒阻塞在等待队列i2c->wait上的任务。

详细的代码流程如下:

  1. i2c_transfer()首先通过函数i2c_trylock_adapter()尝试获得adapter的控制权。如果adapter正在忙则返回错误信息;

  2. __i2c_transfer()通过调用方法adap->algo->master_xfer(adap,msgs, num)传输i2c_msg,如果失败会尝试重新传送,重传次数最多adap->retries;

  3. adap->algo->master_xfer()就是函数s3c24xx_i2c_xfer(),该函数最终调用 s3c24xx_i2c_doxfer(i2c, msgs, num)传输信息;

  4. s3c24xx_i2c_doxfer()通过函数 s3c24xx_i2c_message_start(i2c, msgs)产生S和AD+W的信号,然后通过函数wait_event_timeout()阻塞在等待队列i2c->wait上;

  5. 右上角时序mpu6050的写和读的时序,从设备回复ACK和DATA都会发送中断信号给CPU。每次中断都会调用s3c24xx_i2c_irq->i2c_s3c_irq_nextbyte,

  6. 最后一次中断,所有数据发送或读取完毕会调用s3c24xx_i2c_stop->s3c24xx_i2c_master_complete,通过wake_up唤醒阻塞在等待队列i2c->wait上的任务。

详细的代码流程如下:

对着可以根据上图代码行号一步步去跟代码,涉及到寄存器设置可以参考第一章的寄存器使用截图。

参考文献:

mpu6050 datasheet、exnos4412 datasheet以及源代码下载请关注订阅号一口Linux,回复‘exynos4412’,即可获取。

关注公众号:一口Linux,更多Linux 驱动相关资料分享给你。

fs4412 I2C驱动基于Cortex-A9,mpu6050裸机程序,驱动,I2C架构,有这一篇够了的更多相关文章

  1. MPU6050带字符驱动的i2c从设备驱动2

    #include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> #inclu ...

  2. MPU6050带字符驱动的i2c从设备驱动1

    开干: 1.闲言碎语 这个驱动,越写觉的越简单,入门难,入门之后感觉还好.Linux开发还是比较友好的. 2.编写MPU6050带字符驱动的i2c从设备驱动 要实现的功能就是,将MPU6050作为字符 ...

  3. ok6410[002] ubuntu1604系统下搭配ckermit和dnw基于RAM的裸机程序调试环境

    ubuntu1604系统下搭配ckermit和dnw基于RAM的裸机程序调试环境 系统:  ubuntu16.04 裸板: 飞凌公司OK6410开发板 目标:搭建基于ubuntu1604系统和基于RA ...

  4. sd 卡驱动--基于高通平台

    点击打开链接 内容来自以下博客: http://blog.csdn.net/qianjin0703/article/details/5918041 Linux设备驱动子系统第二弹 - SD卡 (有介绍 ...

  5. 基于FT5x06嵌入式Linux电容触摸屏驱动

    **************************************************************************************************** ...

  6. 基于STM32F1的时钟芯片DS1302驱动

    目录 DS1302.h源代码 DS1302.c源代码 DS1302.h源代码 /** ********************************************************* ...

  7. 《linux设备驱动开发详解》笔记——15 linux i2c驱动

    结合实际代码和书中描述,可能跟书上有一定出入.本文后续芯片相关代码参考ZYNQ. 15.1 总体结构 如下图,i2c驱动分为如下几个重要模块 核心层core,完成i2c总线.设备.驱动模型,对用户提供 ...

  8. 基于335X平台Linux交换芯片驱动开发

    基于335X平台Linux交换芯片驱动开发   一.软硬件平台资料 1.开发板:创龙AM3359核心板,网口采用RMII形式. 2.Kernel版本:4.4.12,采用FDT 3.交换芯片MARVEL ...

  9. 基于KEIL4开发ARM9(S3C2440)的裸机程序

    本文主要介绍如何使用Keil4开发ARM9(S3C2440)裸机程序. 说明: 一.平台: 操作系统:Windows XP系统 KEIL版本:4.73 开发板:ARM9(S3C2440) 二.建立工程 ...

  10. tiny4412 裸机程序 九、串口排查驱动原因及字符图片显示【转】

    本文转载自:http://blog.csdn.net/eshing/article/details/37410571 版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[+]   一 ...

随机推荐

  1. redhat6.5 升级 openssl

    上传版本包: openssl-1.0.1u.tar.gz 解压缩安装包: tar -xf openssl-1.0.1u.tar.gz 安装版本: ./config --prefix=/usr/loca ...

  2. Linux中expr命令

    Linux中expr命令 shell中不能简简单单的加减乘除,需要使用expr.expr只能用于整数值,一般格式为 expr arg1 operator arg2 注意: 1. 运算符左右都有空格,如 ...

  3. 02-Python基础

    文件编码 Python2中 在Python2中:默认文件编码是ASC II,所以无法正常输出中文,会报错. 解决办法 在文件的开头添加# -- coding: UTF-8 -- 或者 # coding ...

  4. 12-CSS浮动

    01 介绍 02 浮动规则 03 案例练习 3.1 缝隙的解决方案 <!DOCTYPE html> <html lang="en"> <head> ...

  5. 高通Android UEFI中的LCD分析(2):关键的函数

    # 高通Android UEFI中的LCD分析(2):关键的函数 背景 在启动流程分析中,看到了几个经常出现的函数,这里实际分析一下有关的实现.以搞清楚高通做了什么,以及我们能做什么. 重要函数 MD ...

  6. 最新最全的BMS/EMS/PCS六大国产“储能方案”,不信你全都看过!

    作为国内领先的嵌入式产品平台提供商,创龙科技在"能源电力"行业拥有超过1000家客户,接下来就让小编向大家分享创龙科技推出的BMS/EMS/PCS"六大储能方案" ...

  7. LaTeX 编辑协作平台 Overleaf 安装和使用教程

    在学术界和科技行业,LaTeX 已成为撰写高质量文档的标准工具.然而,传统的 LaTeX 使用体验常常伴随着以下挑战: 学习曲线陡峭 环境配置复杂 多人协作困难 实时预览不便 当然,市面上不乏很多在线 ...

  8. CF911G 题解

    考虑分块. 如果你做过未来日记就会知道一个很明显的做法--值域并查集. 先考虑整块: 块内没有 \(x\) 则跳过本次操作. 块内有 \(x\) 没有 \(y\) 则令 \(fa[x] = y\) 块 ...

  9. Figma数值输入框支持拖拽调整功能实现

    最近再研究Figma的一些功能设计, 对其中的数值输入框可以直接鼠标拖拽的这个设计印象非常深刻. 这里用了其他网友的一张动态截图演示一下效果. 实际这个拖拽的功能不止看到的这么简单, 在深度研究使用之 ...

  10. lvs的nat和dr模式混合用

    机器部署信息 lvs : 10.0.0.200  vip 10.0.0.19 外网IP , 172.168.1.19 内网IP dr rs: 10.0.0.200 vip 10.0.0.18 rip ...