IIC驱动-基于EEPROM存储芯片AT24C02模块和三合一环境传感器AP3216C

news/2024/9/21 19:06:17

本文将基于IIC协议编写EEPROM芯片AT24C02存储芯片的IIC驱动程序,本文内容将分为三个部分:imx6ull的IIC控制器介绍,AT24C02存储芯片介绍,IIC的Linux驱动程序编写。关于IIC协议的内容与介绍这里不展开,相关资料很多,可以自行去查阅,但是这里需要注意的是,IIC协议本身就是一个协议,只是一些基础的规定,关键在于使用这个协议,主芯片CPU如何和支持IIC协议的从设备交互,需要结合具体的设备操作手册来进行。因此抛开具体的设备谈IIC没有太大的实践意义,在掌握了基本的理论之后,还要结合具体的设备去实践才能真正体会IIC协议。

1 imx6ull的IIC控制器
1.1 imx6ull的IIC控制器介绍
在arm系列的芯片中,像SPI,IIC这种总线一般都会通过控制器的形式向外提供接口。所谓控制器,其实就和以前在51单片机中使用通用引脚模拟IIC时序不一样了,在有IIC控制器的芯片中,CPU向IIC控制器发送命令,控制器就可以自动进行IIC相关工作而不再需要CPU运行指令来模拟,也即IIC控制器和CPU可以异步进行工作。下图是imx6ull芯片的IIC控制器原理图:

从图上也可以看出,每一个IIC控制器涉及的寄存器有IFDR,I2CR,I2SR,I2DR,IADR

此外,IMX6ULL的IIC控制器还有如下额外的特性:
(1)多主机运行
(2)时钟频率可编程
(3)软件可选择应答位
(4)中断驱动,逐字节传输
(5)仲裁丢失中断和自动模式切换从主到从
(6)启动信号和停止信号生成与检测
(7)重复启动信号生成,这个一般用在读时序中改变总线数据传输方向
(8)应答位生成和检测
(9)总线忙检测

下面是各个寄存器的用途
I2Cx_IADR寄存器

这个寄存器是imx6ull作为从设备且使用该控制器时的从设备地址,本实验用不到

I2Cx_IFDR寄存器,用于配置IIC的时钟

因为IIC控制器有自己的时钟,因此并不需要像51单片机模拟IIC那样还要手动产生时序,只需要配置上面这个寄存器配置时钟即可,并且该寄存器的分频表在芯片数据手册中有提供,如下:

I2Cx_I2CR寄存器,这是IIC控制器的控制寄存器,也是我们主要操作的寄存器


分析上面的寄存器各个位,开始一次完整的通信和结束通信的时候,我们通常就是通过MSTA位的变化来实现IIC控制器发出start或者stop信号。所以这里还有一个从51单片机的思维转变就是,在51单片机模拟IIC时序时,通过控制引脚电平发出start或者stop信号,这是是通过对寄存器的相关位进行读写来通知IIC控制器,由控制器自己发出信号。RSTA位用于控制IIC控制器发送重复开始信号,重复开始信号是用于数据线需要改变数据传输方向时。具体会在后面裸机代码里讲解。
I2Cx_I2SR寄存器,这是IIC控制器的状态寄存器,可以通过这个寄存器查询IIC总线状态,是否收到回应信号等信息,这里又有与51单片机软件模拟IIC时序不同,在软件模拟中,通常需要检测引脚的输入电平高低来确定是否收到了回应,而这里由IIC控制器自己检测,并更新对应的状态位。

I2CCx_I2DR寄存器,这是IIC控制器的数据寄存器

在写时序中,主机向这个寄存器写入数据,就会触发IIC控制器发送数据
在IIC控制器读时序中,首先要对I2DR寄存器读一次,这样才会触发IIC控制器发送时序,这样从设备才会在这个读时序下向主机发送数据,这个触发IIC控制器的读称之为假读。在下面的裸机代码会有体现。

1.2 imx6ull的ICC控制器裸机代码及注解
在使用IIC协议控制外设时,从对象上其实涉及了两个部分,分别是主控制器,另一个就是从机外部设备了。对于这类问题,从面向对象的思想来分析是最好的,对于主控制器比如imx6ull的IIC控制器,它是主机,在IIC协议中,它就负责发,以及读从设备的数据,至于IIC控制器发过去的数据从设备会怎么解释由从设备自己的协议规定,并且从设备的数据应该怎么被读也是主机IIC控制器控制发起,因此主机IIC控制器应该提供一套IIC控制器的收发程序接口,然后针对具体的IIC从设备,有一套它自己的程序,在啊程序里会根据从设备本身的特性去编写它的IIC工作逻辑。
IIC控制器的编程逻辑如下,资料参考韦东山老师教程:
IIC控制器的写流程如下:

IIC控制器的读流程如下:

下面的IIC控制器代码来自韦东山imx6ull裸机资料的例程,我将会对每个函数逐一解释。
先给出完整代码

点击查看代码

#include "i2c.h"
void i2c_init(I2C_REGISTERS *I2C_BASE)
{/*I2C_I2CR是控制寄存器,* 可以: 使能I2C,使能中断, 选择主从模式.*//* 配置I2C控制器步骤: 关闭I2C,配置,打开I2C *//* 设置SCL时钟为100K* I2C的时钟源来源于IPG_CLK_ROOT=49.5Mhz*	PLL2 = 528 MHz*	PLL2_PFD2 = 528 *18 /24 = 396 MHz*	IPG_CLK_ROOT = (PLL2_PFD2 / ahb_podf )/ ipg_podf = (396 MHz/4)/2 = 49.5Mhz*	*	PER_CLK_ROOT = IPG_CLK_ROOT/perclk_podf = 49.5 MHz/1 = 49.5 MHz* 设置I2C的波特率为100K, 因此当分频值=49500000/100000=495	* 参考Table 31-3. I2C_IFDR Register Field Values 表中0x37对应的512最接近* 即寄存器IFDR的IC位设置为0X37*/	 I2C_BASE->I2CR &= ~(1 << 7);I2C_BASE->IFDR = 0x37;I2C_BASE->I2CR |= (1<<7);
}uint8_t i2c_check(I2C_REGISTERS *I2C_BASE, uint32_t status)
{/* 检查是否发生仲裁丢失错误(arbitration lost) */if(status & (1<<4)){I2C_BASE->I2SR &= ~(1<<4);	/* 清除仲裁丢失错误位 			*/I2C_BASE->I2CR &= ~(1 << 7);	/* 复位I2C: 先关闭I2C 				*/I2C_BASE->I2CR |= (1 << 7);	/* 再打开I2C 				*/return I2C_ARBITRATIONLOST;} else if(status & (1 << 0))     	/* 检查NAK */{return I2C_NAK;		/* 返回NAK(无应答) */}return I2C_OK;}uint8_t i2c_start(I2C_REGISTERS *I2C_BASE, uint8_t ucSlaveAddr, uint32_t ulOpcode)
{if(I2C_BASE->I2SR & (1 << 5))			/* I2C忙 */return 1;/** 设置控制寄存器I2CR* bit[5]: 1 主模式(master)* bit[4]: 1 发送(transmit)*/I2C_BASE->I2CR |=  (1 << 5) | (1 << 4);/** 设置数据寄存器I2DR* bit[7:0] : 要发送的数据, * START信号后第一个数据是从设备地址*/ I2C_BASE->I2DR = ((uint32_t)ucSlaveAddr << 1) | ((I2C_READ == ulOpcode)? 1 : 0);return 0;}
uint8_t i2c_stop(I2C_REGISTERS *I2C_BASE)
{uint16_t usTimeout = 0xffff;/** 清除控制寄存器I2CR[5:3]* 发出STOP信号*/I2C_BASE->I2CR &= ~((1 << 5) | (1 << 4) | (1 << 3));/* 等待STOP信号确实发出去了 */while((I2C_BASE->I2SR & (1 << 5))){usTimeout--;if(usTimeout == 0)	/* 超时跳出 */return I2C_TIMEOUT;}return I2C_OK;}uint8_t i2c_restart(I2C_REGISTERS *I2C_BASE, uint8_t ucSlaveAddr, uint32_t ulOpcode)
{/* I2C忙并且工作在从模式,跳出 */if(I2C_BASE->I2SR & (1 << 5) && (((I2C_BASE->I2CR) & (1 << 5)) == 0))		return 6;/** 设置控制寄存器I2CR* bit[4]: 1 发送(transmit)* bit[2]: 1 产生重新开始信号(Repeat start)*/I2C_BASE->I2CR |=  (1 << 4) | (1 << 2);/** 设置数据寄存器I2DR* bit[7:0] : 要发送的数据, * START信号后第一个数据是从设备地址*/ I2C_BASE->I2DR = ((uint32_t)ucSlaveAddr << 1) | ((I2C_READ == ulOpcode)? 1 : 0);return 0;}void i2c_write(I2C_REGISTERS *I2C_BASE, const uint8_t *pbuf, uint32_t len)
{/* 等待数据寄存器就绪,可以再次发送数据 */while(!(I2C_BASE->I2SR & (1 << 7))); I2C_BASE->I2SR &= ~(1 << 1); 	  /* 清除IICIF */I2C_BASE->I2CR |= 1 << 4;	      /* 发送数据(transmit) */while(len--){I2C_BASE->I2DR = *pbuf++; 	    /* 将buf中的数据写入到数据寄存器I2DR */while(!(I2C_BASE->I2SR & (1 << 1)));  /* 等待传输完成,完成或失败,中断状态位被置1 */	I2C_BASE->I2SR &= ~(1 << 1);			/* 清除中断状态位 *//* 检查有无错误 */if(i2c_check(I2C_BASE, I2C_BASE->I2SR))break;}I2C_BASE->I2SR &= ~(1 << 1);     /* 清除中断状态位 */i2c_stop(I2C_BASE); 	         /* 发送停止信号 */}void i2c_read(I2C_REGISTERS *I2C_BASE, uint8_t *pbuf, uint32_t len)
{volatile uint8_t dummy = 0;dummy++; 	/* 防止编译警告 *//* 等待数据寄存器就绪 */while(!(I2C_BASE->I2SR & (1 << 7))); I2C_BASE->I2SR &= ~(1 << 1); 			   /* 清除IICIF */I2C_BASE->I2CR &= ~((1 << 4) | (1 << 3));	/* 接收数据: Receive,TXAK *//* 如果只接收一个字节数据的话发送NACK信号 */if(len == 1)I2C_BASE->I2CR |= (1 << 3);dummy = I2C_BASE->I2DR; /* 假读 */ /*类似SPI的写入触发方式,通过读取I2DR寄存器告诉IIC控制器进行一次读操作可以反向思考,如果没有假读操作,控制器怎么知道什么时候发出读操作?*/while(len--){while(!(I2C_BASE->I2SR & (1 << 1))); 	/* 等待传输完成 */	I2C_BASE->I2SR &= ~(1 << 1);			/* 清除标志位 */if(len == 0){i2c_stop(I2C_BASE); 			/* 发送停止信号 */}if(len == 1)	/*如果下一次读是最后一个字节了,那么读完下一个字节后就要发送非应答信号,否则就会一直连续读*/{I2C_BASE->I2CR |= (1 << 3);}*pbuf++ = I2C_BASE->I2DR;}}uint8_t i2c_transfer(I2C_REGISTERS *I2C_BASE, I2C_TRANSFER *transfer)
{uint32_t ulRet = 0;uint32_t ulOpcode = transfer->ulOpcode;/*开始前准备工作,清除标志位*bit-4 IAL 仲裁位,bit-1 IIF 中断标志位*/I2C_BASE->I2SR &= ~((1 << 1) | (1 << 4));/* 等待传输完成 */while(!((I2C_BASE->I2SR >> 7) & 0X1)){}; /* 如果要读某个寄存区,寄存器地址要先"写"给从设备* 所以方向要"先写","后读"*/if ((transfer->ulSubAddressLen > 0) && (transfer->ulOpcode == I2C_READ)){ulOpcode = I2C_WRITE;}ulRet = i2c_start(I2C_BASE, transfer->ucSlaveAddress, ulOpcode);if (ulRet){return ulRet;}/* 等待传输完成: 中断状态为会被置1 */while(!(I2C_BASE->I2SR & (1 << 1))){};/* 检查是否出错 */ulRet = i2c_check(I2C_BASE, I2C_BASE->I2SR);if (ulRet){i2c_stop(I2C_BASE); 			/* 发送停止信号 */return ulRet;}/*如果ulSubAddressLen不为0,表示要发送寄存器地址*/if (transfer->ulSubAddressLen){do{/* 清除中断状态位 */I2C_BASE->I2SR &= ~(1 << 1); /* 调整长度, 也许寄存器地址有多个字节, 本程序最多支持4字节 */transfer->ulSubAddressLen--;I2C_BASE->I2DR = ((transfer->ulSubAddress) >> (8 * transfer->ulSubAddressLen)); while(!(I2C_BASE->I2SR & (1 << 1)));  	/* 等待传输完成: 中断状态位被置1 *//* 检查是否出错 */ulRet = i2c_check(I2C_BASE, I2C_BASE->I2SR);if(ulRet){i2c_stop(I2C_BASE); 				/* 出错:发送停止信号 */return ulRet;}}while ((transfer->ulSubAddressLen > 0) && (ulRet == I2C_OK));if (I2C_READ == transfer->ulOpcode){I2C_BASE->I2SR &= ~(1 << 1);			/* 清除中断状态位 */i2c_restart(I2C_BASE, transfer->ucSlaveAddress, I2C_READ); /* 发送重复开始信号和从机地址 */while(!(I2C_BASE->I2SR & (1 << 1))){}; /* 等待传输完成: 中断状态位被置1 *//* 检查是否出错 */ulRet = i2c_check(I2C_BASE, I2C_BASE->I2SR);if(ulRet){ulRet = I2C_ADDRNAK;i2c_stop(I2C_BASE); 		/* 出错:发送停止信号 */return ulRet;  }}}/* 发送数据 */if ((I2C_WRITE == transfer->ulOpcode) && (transfer->ulLenth > 0)){i2c_write(I2C_BASE, transfer->pbuf, transfer->ulLenth);}/* 读取数据 */if ((I2C_READ == transfer->ulOpcode) && (transfer->ulLenth > 0)){i2c_read(I2C_BASE, transfer->pbuf, transfer->ulLenth);}return 0;	}
uint8_t i2c_write_one_byte(uint8_t addr,uint8_t reg, uint8_t data,I2C_REGISTERS *I2C_BASE)
{uint8_t status = 0;uint8_t writedata=data;I2C_TRANSFER transfer;/* 配置I2C xfer结构体 */transfer.ucSlaveAddress = addr; 			/* 备地址 				*/transfer.ulOpcode = I2C_WRITE;			    /* 数据方向:写 			*/transfer.ulSubAddress = reg;				/* 发出设备地址后马上发寄存器地址 			*/transfer.ulSubAddressLen = 1;				/* 地址长度一个字节 			*/transfer.pbuf = &writedata;				    /* 要发出的数据 				*/transfer.ulLenth = 1;  					    /* 数据长度1个字节			*/status = i2c_transfer(I2C_BASE, &transfer);return status;
}uint8_t i2c_read_one_byte(uint8_t addr, uint8_t reg,I2C_REGISTERS *I2C_BASE)
{uint8_t val=0;uint8_t status = 0;	I2C_TRANSFER transfer;transfer.ucSlaveAddress = addr;				/* 设备地址 				*/transfer.ulOpcode = I2C_READ;			    /* 数据方向:读 				*/transfer.ulSubAddress = reg;				/* 发出设备地址后马上发寄存器地址,* 这是一个写操作 			* 之后会再次发出设备地址,读数据*/transfer.ulSubAddressLen = 1;				/* 地址长度一个字节 			*/transfer.pbuf = &val;						/* 接收数据缓冲区 				*/transfer.ulLenth = 1;					    /* 要读取的数据长度:1			*/status = i2c_transfer(I2C_BASE, &transfer);return val;
}

1.2.1 iic初始化函数i2c_init

点击查看代码
void i2c_init(I2C_REGISTERS *I2C_BASE)
{/*I2C_I2CR是控制寄存器,* 可以: 使能I2C,使能中断, 选择主从模式.*//* 配置I2C控制器步骤: 关闭I2C,配置,打开I2C *//* 设置SCL时钟为100K* I2C的时钟源来源于IPG_CLK_ROOT=49.5Mhz*	PLL2 = 528 MHz*	PLL2_PFD2 = 528 *18 /24 = 396 MHz*	IPG_CLK_ROOT = (PLL2_PFD2 / ahb_podf )/ ipg_podf = (396 MHz/4)/2 = 49.5Mhz*	*	PER_CLK_ROOT = IPG_CLK_ROOT/perclk_podf = 49.5 MHz/1 = 49.5 MHz* 设置I2C的波特率为100K, 因此当分频值=49500000/100000=495	* 参考Table 31-3. I2C_IFDR Register Field Values 表中0x37对应的512最接近* 即寄存器IFDR的IC位设置为0X37*/	 I2C_BASE->I2CR &= ~(1 << 7);I2C_BASE->IFDR = 0x37;I2C_BASE->I2CR |= (1<<7);
}
在初始化函数里的工作主要就是设置IIC控制器的时钟频率,这个也决定了IIC的传输速率 需要注意的是在设置时钟频率前需要关闭IIC控制器,因此这个函数主要涉及I2CR和IFDR寄存器

1.2.2 IIC状态检测函数i2c_check

点击查看代码
uint8_t i2c_check(I2C_REGISTERS *I2C_BASE, uint32_t status)
{/* 检查是否发生仲裁丢失错误(arbitration lost) */if(status & (1<<4)){I2C_BASE->I2SR &= ~(1<<4);	/* 清除仲裁丢失错误位 			*/I2C_BASE->I2CR &= ~(1 << 7);	/* 复位I2C: 先关闭I2C 				*/I2C_BASE->I2CR |= (1 << 7);	/* 再打开I2C 				*/return I2C_ARBITRATIONLOST;} else if(status & (1 << 0))     	/* 检查NAK */{return I2C_NAK;		/* 返回NAK(无应答) */}return I2C_OK;}

每次IIC发生一次传输,包括发生起始信号,重新起始信号,发送一个子节后,对于这次传输过程的状态都会记录在I2Cx_I2SR状态寄存器中国,因此通过检测状态寄存器中的位信息就可以知道上一次传输是否成功,包括仲裁情况、是否收到应答信号等。其实大部分情况的功能就是用于检测是否收到应答信号。从这个函数其实也能印证前面的分析,就是IIC控制器发送完一个字节后会自动检测应答信号,我们只需要检查I2SR寄存器中的应答状态位就可以了。

1.2.3 IIC起始信号生成函数i2c_start

点击查看代码
uint8_t i2c_start(I2C_REGISTERS *I2C_BASE, uint8_t ucSlaveAddr, uint32_t ulOpcode)
{if(I2C_BASE->I2SR & (1 << 5))			/* I2C忙 */return 1;/** 设置控制寄存器I2CR* bit[5]: 1 主模式(master)* bit[4]: 1 发送(transmit)*/I2C_BASE->I2CR |=  (1 << 5) | (1 << 4);/** 设置数据寄存器I2DR* bit[7:0] : 要发送的数据, * START信号后第一个数据是从设备地址*/ I2C_BASE->I2DR = ((uint32_t)ucSlaveAddr << 1) | ((I2C_READ == ulOpcode)? 1 : 0);return 0;}
在发起始信号前,首先要检查总线是否空闲,这个通过检测I2SR中的第5位可以知道。如果忙,发起失败。函数中,通过将I2CR的bit4和bit5写1设置I2C控制器为主模式和发送模式。在IIC控制器失去总线时,其实也就是空闲的时候,bit5会被硬件自动清零,因此默认状态是0,在从0变成1的时候,IIC控制就会自动发出起始信号,也就在SCL线高电平期间将SDA拉低。发送起始信号后其实IIC控制器就会把SCL拉低,表示占有总线,这个在51单片机模拟IIC协议的代码有体现。可以看到,发送设备地址和读写方向时也是通过写入I2DR寄存器实现的。

1.2.4 IIC停止信号i2c_stop

点击查看代码
uint8_t i2c_stop(I2C_REGISTERS *I2C_BASE)
{uint16_t usTimeout = 0xffff;/** 清除控制寄存器I2CR[5:3]* 发出STOP信号*/I2C_BASE->I2CR &= ~((1 << 5) | (1 << 4) | (1 << 3));/* 等待STOP信号确实发出去了 */while((I2C_BASE->I2SR & (1 << 5))){usTimeout--;if(usTimeout == 0)	/* 超时跳出 */return I2C_TIMEOUT;}return I2C_OK;}
分析上述代码,通过清楚I2CR的[5;3]位来让IIC控制器发出停止信号,并等待停止信号发出成功。其中的关键在于bit5从1变成了0,这会触发IIC控制器发出停止信号,也就在SCL高电平期间将SDA从低电平变成高电平,并释放总线,此时SCL和SDA都变成了高电平。2和3位只是为配合结束将IIC主机控制器变成接收状态。发送完起始信号后根据IIC协议接着就要发送从设备地址以及数据传输方向。

1.2.5 IIC重新开始信号发起函数i2c_restart

点击查看代码
uint8_t i2c_restart(I2C_REGISTERS *I2C_BASE, uint8_t ucSlaveAddr, uint32_t ulOpcode)
{/* I2C忙并且工作在从模式,跳出 */if(I2C_BASE->I2SR & (1 << 5) && (((I2C_BASE->I2CR) & (1 << 5)) == 0))		return 6;/** 设置控制寄存器I2CR* bit[4]: 1 发送(transmit)* bit[2]: 1 产生重新开始信号(Repeat start)*/I2C_BASE->I2CR |=  (1 << 4) | (1 << 2);/** 设置数据寄存器I2DR* bit[7:0] : 要发送的数据, * START信号后第一个数据是从设备地址*/ I2C_BASE->I2DR = ((uint32_t)ucSlaveAddr << 1) | ((I2C_READ == ulOpcode)? 1 : 0);return 0;}
这个函数的核心其实就是给I2CR的bit2写1,这样IIC控制器就会自动发送重新起始信号,同时bit4要设置IIC控制器为发送模式。 同样的,发送完重新起始信号后,根据IIC协议就要重新发送从设备地址和读写方向。重新起始信号是ARM芯片的IIC控制器提供的额外功能,主要是为了在数据线需要改变传输方向时不丢失总线。通常在读时序中会用到重新起始信号。

1.2.6 IIC写函数i2c_write

点击查看代码
void i2c_write(I2C_REGISTERS *I2C_BASE, const uint8_t *pbuf, uint32_t len)
{/* 等待数据寄存器就绪,可以再次发送数据 */while(!(I2C_BASE->I2SR & (1 << 7))); I2C_BASE->I2SR &= ~(1 << 1); 	  /* 清除IICIF */I2C_BASE->I2CR |= 1 << 4;	      /* 发送数据(transmit) */while(len--){I2C_BASE->I2DR = *pbuf++; 	    /* 将buf中的数据写入到数据寄存器I2DR */while(!(I2C_BASE->I2SR & (1 << 1)));  /* 等待传输完成,完成或失败,中断状态位被置1 */	I2C_BASE->I2SR &= ~(1 << 1);			/* 清除中断状态位 *//* 检查有无错误 */if(i2c_check(I2C_BASE, I2C_BASE->I2SR))break;}I2C_BASE->I2SR &= ~(1 << 1);     /* 清除中断状态位 */i2c_stop(I2C_BASE); 	         /* 发送停止信号 */}

在函数中,同样的要先检测总线是否就绪,然后清除标志位,并设置IIC控制器为发送模式。接着将要发送的数据写入I2DR寄存器,IIC控制器就会产生时序自动发送数据。在IIC写时序中,可以连续写入多个字节。在写完后,IIC检测到应答信号后,可以发送停止信号结束本次通信。

1.2.7 IIC读函数i2c_read

点击查看代码
void i2c_read(I2C_REGISTERS *I2C_BASE, uint8_t *pbuf, uint32_t len)
{volatile uint8_t dummy = 0;dummy++; 	/* 防止编译警告 *//* 等待数据寄存器就绪 */while(!(I2C_BASE->I2SR & (1 << 7))); I2C_BASE->I2SR &= ~(1 << 1); 			   /* 清除IICIF */I2C_BASE->I2CR &= ~((1 << 4) | (1 << 3));	/* 接收数据: Receive,TXAK *//* 如果只接收一个字节数据的话发送NACK信号 */if(len == 1)I2C_BASE->I2CR |= (1 << 3);dummy = I2C_BASE->I2DR; /* 假读 */ /*类似SPI的写入触发方式,通过读取I2DR寄存器告诉IIC控制器进行一次读操作可以反向思考,如果没有假读操作,控制器怎么知道什么时候发出读操作?*/while(len--){while(!(I2C_BASE->I2SR & (1 << 1))); 	/* 等待传输完成 */	I2C_BASE->I2SR &= ~(1 << 1);			/* 清除标志位 */if(len == 0){i2c_stop(I2C_BASE); 			/* 发送停止信号 */}if(len == 1)	/*如果下一次读是最后一个字节了,那么读完下一个字节后就要发送非应答信号,否则就会一直连续读*/{I2C_BASE->I2CR |= (1 << 3);}*pbuf++ = I2C_BASE->I2DR;}}
在函数中,同样的要先检测总线是否就绪,然后清除状态标志位,设置IIC控制器为接收模式,并且根据需要读的字节数以及当前剩余要读的字节数确定IIC控制器在接收一个字节后是发送应答信号还是非应答信号。如果发送应答信号,那么意味着读完这个字节后从设备会继续发送。如果已经到了最后一个字节了,那么读完这个字节后IIC控制器就要发送非应答信号告诉从设备别再发数据了,到此为止了。在代码中有一条语句dummy = I2C_BASE->I2DR; /* 假读 */ ,这就是前面提到的假读,是为了触发IIC控制器发送读时序。**注意,如果只读一个字节,显然,while循环也会执行一次,并且在进去之后执行到if(len == 0)其实就已经完成了一次读并且数据放在I2DR寄存器中了,因此这里有个判断用于发送停止信号。**这个while循环的代码逻辑值得多次品味。

1.2.8 IIC读写综合函数i2c_transfer

点击查看代码
uint8_t i2c_transfer(I2C_REGISTERS *I2C_BASE, I2C_TRANSFER *transfer)
{uint32_t ulRet = 0;uint32_t ulOpcode = transfer->ulOpcode;/*开始前准备工作,清除标志位*bit-4 IAL 仲裁位,bit-1 IIF 中断标志位*/I2C_BASE->I2SR &= ~((1 << 1) | (1 << 4));/* 等待传输完成 */while(!((I2C_BASE->I2SR >> 7) & 0X1)){}; /* 如果要读某个寄存区,寄存器地址要先"写"给从设备* 所以方向要"先写","后读"*/if ((transfer->ulSubAddressLen > 0) && (transfer->ulOpcode == I2C_READ)){ulOpcode = I2C_WRITE;}ulRet = i2c_start(I2C_BASE, transfer->ucSlaveAddress, ulOpcode);if (ulRet){return ulRet;}/* 等待传输完成: 中断状态为会被置1 */while(!(I2C_BASE->I2SR & (1 << 1))){};/* 检查是否出错 */ulRet = i2c_check(I2C_BASE, I2C_BASE->I2SR);if (ulRet){i2c_stop(I2C_BASE); 			/* 发送停止信号 */return ulRet;}/*如果ulSubAddressLen不为0,表示要发送寄存器地址*/if (transfer->ulSubAddressLen){do{/* 清除中断状态位 */I2C_BASE->I2SR &= ~(1 << 1); /* 调整长度, 也许寄存器地址有多个字节, 本程序最多支持4字节 */transfer->ulSubAddressLen--;I2C_BASE->I2DR = ((transfer->ulSubAddress) >> (8 * transfer->ulSubAddressLen)); while(!(I2C_BASE->I2SR & (1 << 1)));  	/* 等待传输完成: 中断状态位被置1 *//* 检查是否出错 */ulRet = i2c_check(I2C_BASE, I2C_BASE->I2SR);if(ulRet){i2c_stop(I2C_BASE); 				/* 出错:发送停止信号 */return ulRet;}}while ((transfer->ulSubAddressLen > 0) && (ulRet == I2C_OK));if (I2C_READ == transfer->ulOpcode){I2C_BASE->I2SR &= ~(1 << 1);			/* 清除中断状态位 */i2c_restart(I2C_BASE, transfer->ucSlaveAddress, I2C_READ); /* 发送重复开始信号和从机地址 */while(!(I2C_BASE->I2SR & (1 << 1))){}; /* 等待传输完成: 中断状态位被置1 *//* 检查是否出错 */ulRet = i2c_check(I2C_BASE, I2C_BASE->I2SR);if(ulRet){ulRet = I2C_ADDRNAK;i2c_stop(I2C_BASE); 		/* 出错:发送停止信号 */return ulRet;  }}}/* 发送数据 */if ((I2C_WRITE == transfer->ulOpcode) && (transfer->ulLenth > 0)){i2c_write(I2C_BASE, transfer->pbuf, transfer->ulLenth);}/* 读取数据 */if ((I2C_READ == transfer->ulOpcode) && (transfer->ulLenth > 0)){i2c_read(I2C_BASE, transfer->pbuf, transfer->ulLenth);}return 0;	}

这个函数综合前面的函数,给出了IIC可能的四种读写方式:单纯写,先写寄存器地址再写寄存器命令,单纯读,先写寄存器地址再读。这个函数的功能就是根据IIC传输结构体的内容执行上述四种时序中的某一种。

1.2.9 IIC写一个字节函数i2c_write_one_byte

点击查看代码
uint8_t i2c_write_one_byte(uint8_t addr,uint8_t reg, uint8_t data,I2C_REGISTERS *I2C_BASE)
{uint8_t status = 0;uint8_t writedata=data;I2C_TRANSFER transfer;/* 配置I2C xfer结构体 */transfer.ucSlaveAddress = addr; 			/* 备地址 				*/transfer.ulOpcode = I2C_WRITE;			    /* 数据方向:写 			*/transfer.ulSubAddress = reg;				/* 发出设备地址后马上发寄存器地址 			*/transfer.ulSubAddressLen = 1;				/* 地址长度一个字节 			*/transfer.pbuf = &writedata;				    /* 要发出的数据 				*/transfer.ulLenth = 1;  					    /* 数据长度1个字节			*/status = i2c_transfer(I2C_BASE, &transfer);return status;
}
这个函数其实就是构造一个I2C_TRANSFER结构体然后调用上一个函数

1.2.10 IIC读一个字节函数i2c_read_one_byte

点击查看代码
uint8_t i2c_read_one_byte(uint8_t addr, uint8_t reg,I2C_REGISTERS *I2C_BASE)
{uint8_t val=0;uint8_t status = 0;	I2C_TRANSFER transfer;transfer.ucSlaveAddress = addr;				/* 设备地址 				*/transfer.ulOpcode = I2C_READ;			    /* 数据方向:读 				*/transfer.ulSubAddress = reg;				/* 发出设备地址后马上发寄存器地址,* 这是一个写操作 			* 之后会再次发出设备地址,读数据*/transfer.ulSubAddressLen = 1;				/* 地址长度一个字节 			*/transfer.pbuf = &val;						/* 接收数据缓冲区 				*/transfer.ulLenth = 1;					    /* 要读取的数据长度:1			*/status = i2c_transfer(I2C_BASE, &transfer);return val;
}
代码逻辑与1.2.9类似

1.2.10 补充51单片机软件模拟IIC时序,作为对比

点击查看代码
#include"i2c.h"
/****************************************************************
***************
* 函数名 : Delay10us()
* 函数功能 : 延时 10us
* 输入 : 无
* 输出 : 无
*****************************************************************
**************/
void Delay10us()
{
unsigned char a,b;
for(b=1;b>0;b--)
for(a=2;a>0;a--);
}
/****************************************************************
***************
* 函数名 : I2cStart()
* 函数功能 : 起始信号:在 SCL 时钟信号在高电平期间 SDA 信号产生
一个下降沿
* 输入 : 无
* 输出 : 无
* 备注 : 起始之后 SDA 和 SCL 都为 0
*****************************************************************
**************/
void I2cStart()
{
SDA=1;
Delay10us();
SCL=1;
Delay10us();//建立时间是 SDA 保持时间>4.7us
SDA=0;
Delay10us();//保持时间是>4us
SCL=0;
Delay10us();
}
/****************************************************************
***************
* 函数名 : I2cStop()
* 函数功能 : 终止信号:在 SCL 时钟信号高电平期间 SDA 信号产生一
个上升沿
* 输入 : 无
* 输出 : 无
* 备注 : 结束之后保持 SDA 和 SCL 都为 1;表示总线空闲
*****************************************************************
**************/
void I2cStop()
{
SDA=0;
Delay10us();
SCL=1;
Delay10us();//建立时间大于 4.7us
SDA=1;
Delay10us();
}
/****************************************************************
***************
* 函数名 : I2cSendByte(unsigned char dat)
* 函数功能 : 通过 I2C 发送一个字节。在 SCL 时钟信号高电平期间,
保持发送信号 SDA 保持稳定
* 输入 : num
* 输出 : 0 或 1。发送成功返回 1,发送失败返回 0
* 备注 : 发送完一个字节 SCL=0,SDA=1
*****************************************************************
**************/
unsigned char I2cSendByte(unsigned char dat)
{
unsigned char a=0,b=0;//最大 255,一个机器周期为 1us,最大延时
255us。
for(a=0;a<8;a++)//要发送 8 位,从最高位开始
{
SDA=dat>>7; //起始信号之后 SCL=0,所以可以直接改变 SDA 信
号
dat=dat<<1;
Delay10us();
SCL=1;
Delay10us();//建立时间>4.7us
SCL=0;
Delay10us();//时间大于 4us
}
SDA=1;
Delay10us();
SCL=1;
while(SDA)//等待应答,也就是等待从设备把 SDA 拉低
{
b++;
if(b>200) //如果超过 2000us 没有应答发送失败,或者为非应答,
表示接收结束
{
SCL=0;
Delay10us();
return 0;
}
}
SCL=0;
Delay10us();
return 1;
}
/****************************************************************
***************
* 函数名 : I2cReadByte()
* 函数功能 : 使用 I2c 读取一个字节
* 输入 : 无
* 输出 : dat
* 备注 : 接收完一个字节 SCL=0,SDA=1.
*****************************************************************
**************/
unsigned char I2cReadByte()
{
unsigned char a=0,dat=0;
SDA=1; //起始和发送一个字节之后 SCL 都是 0
Delay10us();
for(a=0;a<8;a++)//接收 8 个字节
{
SCL=1;
Delay10us();
dat<<=1;
dat|=SDA;
Delay10us();
SCL=0;
Delay10us();
}
return dat;
}
/****************************************************************
***************
* 函数名 : void At24c02Write(unsigned char addr,unsigned
char dat)
* 函数功能 : 往 24c02 的一个地址写入一个数据
* 输入 : 无
* 输出 : 无
*****************************************************************
**************/
void At24c02Write(unsigned char addr,unsigned char dat)
{
I2cStart();
I2cSendByte(0xa0);//发送写器件地址
I2cSendByte(addr);//发送要写入内存地址
I2cSendByte(dat); //发送数据
I2cStop();
}
/****************************************************************
***************
* 函数名 : unsigned char At24c02Read(unsigned char addr)
* 函数功能 : 读取 24c02 的一个地址的一个数据
* 输入 : 无
* 输出 : 无
*****************************************************************
**************/
unsigned char At24c02Read(unsigned char addr)
{
unsigned char num;
I2cStart();
I2cSendByte(0xa0); //发送写器件地址
I2cSendByte(addr); //发送要读取的地址
I2cStart();
I2cSendByte(0xa1); //发送读器件地址
num=I2cReadByte(); //读取数据
I2cStop();
return num;
}
分析上面的代码,其实也是分为了两个部分,一部分是IIC的接口函数,只不过在51单片机中并没有IIC控制器,因此使用软件模拟。第二部分是AT24C02使用IIC函数接口实现的读写函数。在软件模拟IIC时,发送完起始信号后,主机引脚控制时钟线SCL总是保持低电平,发送完一个字节后如果不结束通信,主机引脚也是让SCL保持低电平,因此低电平实际上表示了主机在持有总线。

2 Linux驱动开发中的IIC驱动程序接口及用法

3 基于imx6ull主控芯片和AT24C02存储芯片、AP3216C三合一环境传感器的IIC驱动程序

3.1 AT24C02存储器芯片
这一部分我一开始用的51单片机开发板上的AT24C02芯片做实验,IIC传输时一直返回-5,-5表示找不到设备或者硬件有问题,后面分析可能是供电电压不一致,51单片机时5V电压,而imx6ull是3.3V,显然两边总线的电平不一致,肯定有问题,严重还会出现电流倒灌,这是很严重的事情,好在IMX6ULL芯片有这方面的内部保护电路

3.1.1 AT24C02芯片用法及IIC接口介绍

3.1.2 基于AT24C02芯片的Linux IIC驱动程序

3.2 AP3216C三合一环境传感器

3.2.1 AP3216C三合一传感器用法及IIC接口介绍

AP3216C是一个三合一环境传感器,可以检测环境光强度ALS,接近距离PS,和红外线强度IR,通常用于手机、平板、导航设备等,比如手机的屏幕自动亮度调节和接听电话将听筒靠近耳朵时自动息屏防误触就是用了类似AP3216C的环境传感器。
AP3216C的内部结构图如下所示:

从结构体可以看到,AP3216C通过IIC总线进行控制,还支持中断信号。
AP3216C内部有很多寄存器,都是通过IIC总线进行读写,传感器的采样值也会保存到其内部的寄存器中,因此通过读取对应得寄存器值就可以得到环境采样值,其中接近距离PS和红外强度IR是10位ADC采样值,环境光强度ALS是16位ADC采样值。大多数使用场景中,会用到以下这些寄存器就足够了,对于更多的寄存器及其用法,可以查看芯片的参考手册。

在AP3216C的IIC驱动程序中,首先通过0x00寄存器复位,然后使能ALS+PS+IR即可
根据芯片手册,三个同时使能时,每一次采样大约需要112.5ms,因此可以间隔200ms读取一次数据

在正点原子的IMX6ULL开发板中,AP3216C和imx6ull的连接电路原理图如下:

从原理图上看得出来,imx6ull上的UART4_RXD和UART4_TXD引脚分别被用作了I2C1_SDA和I2C_SCL连接到了AP3216C的IIC接口上。
在ARM芯片中,引脚是可以复用的,在引脚支持所需要的功能的情况下是可以将引脚配置成需要的功能的,因此在使用具体的引脚时,至少会涉及三大模块:时钟模块(这个是所有外设都需要的)、引脚复用控制器模块(用于复用引脚的功能)、引脚复用为某一功能后这个功能对应得控制器模块,每一个模块都会有对应的寄存器组。
以本章的IIC实验为例,imx6ull中的IIC1用到了UART4_RXD和UART4_TXD这两个引脚,这两个引脚的名字其实就它们可以复用的功能之一,但是我们需要将其复用为IIC1_SDA和IIC1_SCL,查看芯片参考手册的32章《IOMUC Coontroler》,在里面找到UART4_RXD和UART4_TXD这两个寄存器

从这两个寄存器可以看出,是可以把这两个引脚复用为IIC1_SDA和IIC1_SCL功能的。
然后再看IIC Controler章节

这一章节里面就列出了IIC控制器相关的控制器

在设置好时钟,配置引脚功能和电气属性后,就可以通过IIC控制器的寄存器来对IIC控制器进行操作进而实现IIC通信了。

3.2.2 基于AP3216C的Linux IIC驱动程序

AP3216C的IIC驱动程序将会基于设备树的总线设备驱动模型进行编写,其中IIC控制器部分的引脚复用和配置已经由IIC子系统和pinctrl子系统帮我们实现了,我们只需要在对应的IIC控制器节点下添加我们的IIC设备即可。

根据原理图,AP3216C连接到了imx6ull得到IIC1,因此在IIC1节点下添加设备子节点,完成后的设备树节点代码如下:

点击查看代码
&i2c1 {clock-frequency = <100000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c1>;status = "okay";at24c02@50 {compatible = "at24c02_iic_driver";reg = <0x50>;	/*从设备地址,这个在IIC子系统里就是规定好的属性用于保存从设备地址*/};ap3216c@1e {compatible = "ap3216c_driver";reg = <0x1e>;};};
在上述设备树代码中,ap3216c这个节点就是我们的设备子节点,其中reg是会被IIC子系统使用的属性,它会将这个解析为设备地址。注意,设备地址只看七位,后续的读写控制位会由IIC函数接口自动加上。我们可以看i2c1节点里面有一个pinctrl-0 = <&pinctrl_i2c1>;这其实就是使用了IIC子系统进行引脚复用,可以点击pinctrl_i2c1查看发现这就是一个pinctrl子节点,里面就写上了所用到的UART4_TX_D和UART4_RX_D两个引脚。 在内核解析设备树时,会把iic节点下的子节点转换为client,每个子节点对应一个client结构体,里面就包含了设备的地址。在驱动程序中,会定义一个i2c_driver结构体的,里面的driver.of_match_table会有一个compatible属性,如果这个属性的值和client节点也即设备树中设备节点的compatible的值对上,就会执行i2c_driver里面的函数,在里面就可以进行一些获取信息和初始化的操作。

可以将ap3216c相关的寄存器地址使用宏定义在头文件中定义好,方便使用
ap3216c.h头文件内容如下:

点击查看代码
#ifndef AP3216C_H
#define AP3216C_H#define AP3216C__SYSTEMCONG     0X00
#define AP3216C__IRDATALOW      0x0A
#define AP3216C__IRDATAHIGH     0x0B
#define AP3216C__ALSDATALOW     0x0C
#define AP3216C__ALSDATAHIGH    0x0D
#define AP3216C__PSDATALOW      0x0E
#define AP3216C__PSDATAHIGH     0x0F#endif

ap3216c_driver.c代码如下,其中定义了一个结构体用于存放设备的client节点和传感器值

点击查看代码
#include "asm-generic/current.h"
#include "asm-generic/errno-base.h"
#include "asm-generic/poll.h"
#include "asm-generic/siginfo.h"
#include "asm/signal.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/gpio/driver.h"
#include "linux/i2c.h"
#include "linux/irqreturn.h"
#include "linux/kdev_t.h"
#include "linux/mod_devicetable.h"
#include "linux/nfs_fs.h"
#include "linux/of.h"
#include "linux/socket.h"
#include "linux/wait.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/ktime.h>
#include <linux/delay.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <asm/current.h>#include "ap3216c.h"static int major;  //驱动的主设备号
static struct class *ap3216c_class; //设备类
/*定义跟设备相关的结构体*/
struct ap3216c_dev {struct i2c_client *ap3216c_client;  //设备树中对应设备的client节点unsigned short ir, als, ps; //三个传感器数据,无符号16位数据
};static struct ap3216c_dev ap3216cDev;/*
向ap3216c的一个寄存器写入数据
reg_addr: 要写入的寄存器地址
data: 要写入寄存器的数据
*/
static int ap3216c_write_reg(struct ap3216c_dev *dev, unsigned char reg_addr, unsigned char data)
{int err;struct i2c_msg msg[1];unsigned char kernel_buf[2];msg[0].addr = dev->ap3216c_client->addr;    //从设备地址msg[0].flags = 0; //写msg[0].buf = kernel_buf;    //要发送的内容kernel_buf[0] = reg_addr;//寄存器地址kernel_buf[1] = data;   //要写入的数据msg[0].len = 2;err = i2c_transfer(dev->ap3216c_client->adapter, msg, 1);if(err != 1){printk("i2c write err,err: %d\n", err);return -1;}return err;
}/*
读ap3216c的一个寄存器
dev:对应设备的结构体
reg_addr:要读的寄存器地址返回读取到的寄存器值
*/
static int ap3216c_read_reg(struct ap3216c_dev *dev, unsigned char reg_addr)
{int err;struct i2c_msg msgs[2];unsigned char kernel_buf[1];unsigned char val;/*先发送要读的寄存器地址*/msgs[0].addr = dev->ap3216c_client->addr;msgs[0].flags = 0;  //写msgs[0].buf = kernel_buf;kernel_buf[0] = reg_addr;msgs[0].len = 1;/*再读*/msgs[1].addr = dev->ap3216c_client->addr;msgs[1].flags = I2C_M_RD;   //读msgs[1].buf = kernel_buf;   //读取的数据保存到kernel_bufmsgs[1].len = 1;    //读取一个字节err = i2c_transfer(dev->ap3216c_client->adapter, msgs, 2);if(err != 2){printk("read err! err: %d\n", err);return -1;}val = kernel_buf[0];return val;   //返回读取到的值}static int ap3216c_open(struct inode *node, struct file *file)
{/*打开设备节点时执行复位ap3216c,通过向寄存器0x00写0x4实现,然后再写入0x3使能ALS,PS,IR*/int err;err = ap3216c_write_reg(&ap3216cDev, AP3216C__SYSTEMCONG, 0x04);  //复位if(err == -1){printk("write err!");return -1;}mdelay(50); //等待复位完成err = ap3216c_write_reg(&ap3216cDev, AP3216C__SYSTEMCONG, 0x03);  //使能ALS,PS,IRif(err == -1){printk("write err!");return -1;}return 0;
}static void ap3216c_read_data(struct ap3216c_dev *dev)
{unsigned char i;unsigned char data[6];for(i = 0; i < 6; i++){data[i] = ap3216c_read_reg(dev, AP3216C__IRDATALOW + i);}/*对数据进行处理*//*IR,10位*/if(data[0] & 0x80){/*数据无效*/dev->ir = 0;}else{dev->ir = ((unsigned short)data[1] << 2) | (data[0] & 0x03); }/*ALS,16位*/dev->als = ((unsigned short)data[3] << 8) | data[2];/*PS,10位*/if(data[4] & 0x40){/*无效*/dev->ps = 0;}else{dev->ps = ((unsigned short)(data[5] & 0x3f) << 4) | (data[4] & 0xf);}}static ssize_t ap3216c_read(struct file *file, char __user *user_buf, size_t size, loff_t *offset)
{/*读取传感器数据IR: 0x0A,0x0B als:0x0C,0x0Dps: 0x0E,0x0F*/unsigned short data[3]; //data[0] data[1] IR data[2] data[3] ALS data[4] data[5] PSint err;ap3216c_read_data(&ap3216cDev);/*将数据拷贝到用户空间*/data[0] = ap3216cDev.ir;data[1] = ap3216cDev.als;data[2] = ap3216cDev.ps;err = copy_to_user(user_buf, data, sizeof(data));return 6;}static ssize_t ap3216c_write(struct file *file, const char __user *user_buf, size_t size, loff_t *offset)
{/*不需要写*/return 0;}static int ap3216c_release(struct inode *node, struct file *file)
{/*关闭ap3216c,进入掉电模式*/int err;err = ap3216c_write_reg(&ap3216cDev, AP3216C__SYSTEMCONG, 0x0);  //进入掉电模式if(err == -1){printk("close err!");return -1;}return 0;
}static struct file_operations ap3216c_oprs = {.owner = THIS_MODULE,.read = ap3216c_read,.write = ap3216c_write,.open = ap3216c_open,.release = ap3216c_release,
};int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{/*驱动和iic的client匹配时执行*/printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/*获取client节点*/ap3216cDev.ap3216c_client = client;/*注册驱动*/major = register_chrdev(major, "ap3216c_driver", &ap3216c_oprs);/*创建设备类*/ap3216c_class = class_create(THIS_MODULE, "ap3216c_class");if(IS_ERR(ap3216c_class)){printk("%s %s line %d, ap3216c_class create failed.\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "ap3216c_driver");return PTR_ERR(ap3216c_class);}/*创建设备节点*/device_create(ap3216c_class, NULL, MKDEV(major,0), NULL, "ap3216c");return 0;
}int ap3216c_remove(struct i2c_client *client)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);device_destroy(ap3216c_class, MKDEV(major, 0));class_destroy(ap3216c_class);unregister_chrdev(major, "ap3216c_driver");return 0;
}static struct of_device_id iic_ap3216c_match[] = {{.compatible = "ap3216c_driver",},
};static const struct i2c_device_id ap3216c_ids[] = {{ "xxxxyyy",	(kernel_ulong_t)NULL },{ /* END OF LIST */ }
};static struct i2c_driver iic_ap3216c_driver = {.probe = ap3216c_probe,.remove = ap3216c_remove,.driver = {.name = "ap3216c",.owner = THIS_MODULE,.of_match_table = iic_ap3216c_match,},.id_table = ap3216c_ids,
};static int __init ap3216c_drv_init(void)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = i2c_add_driver(&iic_ap3216c_driver);return err;
}static void __exit ap3216c_drv_exit(void)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);i2c_del_driver(&iic_ap3216c_driver);
}module_init(ap3216c_drv_init);
module_exit(ap3216c_drv_exit);
MODULE_LICENSE("GPL");

在上述代码中,核心还是iic_driver结构体和file_operation结构体,当驱动和设备树中的ap3216c设备的client节点匹配时就会执行iic_driver结构体中的probe函数,在这个函数里会记录对应的client结构体到一个全局变量中,然后使用file_operation结构体注册驱动程序,创建设备类和设备节点。
file_operation结构体中就定义了一系列的函数指针,把对应的函数赋给这个结构体,比如open,read,write等,上层应用根据设备节点使用系统调用时就会执行相应的函数。

重点分析ap3216_read_reg函数,它就是使用了Linux内核中的IIC接口实现ap3216c的IIC读时序,代码如下:

点击查看代码
static int ap3216c_read_reg(struct ap3216c_dev *dev, unsigned char reg_addr)
{int err;struct i2c_msg msgs[2];unsigned char kernel_buf[1];unsigned char val;/*先发送要读的寄存器地址*/msgs[0].addr = dev->ap3216c_client->addr;msgs[0].flags = 0;  //写msgs[0].buf = kernel_buf;kernel_buf[0] = reg_addr;msgs[0].len = 1;/*再读*/msgs[1].addr = dev->ap3216c_client->addr;msgs[1].flags = I2C_M_RD;   //读msgs[1].buf = kernel_buf;   //读取的数据保存到kernel_bufmsgs[1].len = 1;    //读取一个字节err = i2c_transfer(dev->ap3216c_client->adapter, msgs, 2);if(err != 2){printk("read err! err: %d\n", err);return -1;}val = kernel_buf[0];return val;   //返回读取到的值}

在函数中构造了两个i2c_msg结构体变量,使用一个数组来保存,IIC信息就是通过这两个结构体来传输。其实对比下前面的逻辑IIC程序,就会发现裸机的代码就是模仿内核的IIC驱动代码格式。对于每一个iic_msg结构体变量,要填充里面的内容,包括器件地址、读写方向,读/写的内存缓冲区。以msgs[0]为例,设置其中的addr为器件地址,设置flag为0表示写,buf表示要写入的内容,这是一个指向某个内存区域的指针,len表示要发送的字节数,同理msg[1]类似。然后调用i2c_transfer(dev->ap3216c_client->adapter, msgs, 2)发送两个i2c_msg,每一个i2c_msg都会重新发起IIC通信。

两个msgs的IIC发送流程如下:
首先主机发起Start信号,然后发送器件地址和读写控制位(此时是写)的组合一字节,IIC子系统中会把msgs[0].addr 和 msgs[0].flags 组合成一个字节发送,实际上就是把这个字节放入IIC控制器的I2DR寄存器,然后IIC控制器就会自动发送该字节,然后从设备在第9个周期拉低低电平,IIC控制器检测到低电平的应答信号,接着再把msgs[0].buf中的内容逐个字节的发送出去,这里就是发送一个寄存器的地址,一个字节。然后从设备发送低电平应答信号。接着在SCL低电平期间将SDA拉高,然后SCL高电平,在这期间再把SDA拉低发送一个重新开始信号,接着开始发送msgs[1],一样的先发送器件地址和读写控制位(此时是读)的组合字节,然后从设备发送低电平应答,IIC控制器检测到之后,通过读取I2DR进行一次假读触发了主机IIC控制器的读时序,在SCK时序下读取到一个字节保存在msgs[1].buf中,此时主机发送高电平非应答信号,表示不读了,然后发送停止信号。如果要来连续读,主机可以发送低电平的应答信号,然后从I2DR寄存器读取数据,再次触发IIC读时序。至于连续读,读的地址如何变化,由从设备芯片手册确定。

下面是AP3216C芯片手册中IIC读一字节的IIC时序

从时序图中也可以看到,重复启动信号就是在传输完成一个字节并且应答后,先再SCL低电平期间将SDA拉高,然后再将SCL拉高,然后SDA拉低,从而发出重新起始信号,在这之前并不需要停止信号,更不能有非应答信号。重复起始信号表示继续占用总线,大多数情况下用于转换IIC的数据传输方向。

同理ap3216c_write_reg函数的内部工作流程也是类似分析。

下面的ap3216c的IIC应用程序
ap3216c_test.c

点击查看代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>/*使用方法:
./ap3216c_test <dev>
*/int main(int argc, char *argv[])
{int fd;unsigned short data[3];unsigned short ir,als,ps;int ret;if(argc != 2){printf("Usage: %s <dev>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR);if(fd < 0){printf("can not open %s\n", argv[0]);return -1;}while(1){ret = read(fd, data, sizeof(data));if(ret == -1){/*读取错误*/printf("read err\n");return -1;}ir = data[0];als = data[1];ps = data[2];printf("ir: %d, als: %d, ps: %d\n", ir, als, ps);usleep(200000);}close(fd);return 0;
}

本次实验的笔记到此结束。关于IIC总线协议,是嵌入式开发中经常用到的一类总线,许多外设的操作都会用到,需要对IIC非常熟悉,同时要结合具体的从设备芯片手册来编写IIC时序,IIC编程可以分为两层,一层是主机控制器层面,这一层面只负责收发,另一层是从设备层面,这一层要对收发的数据进行解析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/47246.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

秒懂双亲委派机制

前言 最近知识星球中,有位小伙伴问了我一个问题:JDBC为什么会破坏双亲委派机制? 这个问题挺有代表性的。 双亲委派机制是Java中非常重要的类加载机制,它保证了类加载的完整性和安全性,避免了类的重复加载。 这篇文章就跟大家一起聊聊,Java中类加载的双亲委派机制到底是怎…

这就叫“面试造火箭,工作拧螺丝!”

你好呀,我是歪歪。 我想再讨论一下上次的这篇文章《哎,被这个叫做at least once的玩意坑麻了》 因为有些朋友看完之后再评论区给出了自己的思考,也有朋友和我私聊,分享了自己的看法,我觉得有些想法很好,所以我决定一鱼两吃,再聊聊这个问题。 假设,我们是一场面试,面试…

税务规则中存在满足条件的多个规则,取值逻辑

若税务规则中存在满足条件多个规则时, 如物料\客户, 系统会优先取客户的税率。

【JS逆向百例】某点数据逆向分析,多方法详解

前言 最近收到粉丝的私信,其在逆向某个站点时遇到了些问题,在查阅资料未果后,来询问K哥,K哥一向会尽力满足粉丝的需求。网上大多数分析该站点的教程已经不再适用,本文K哥将提供 3 种解决方案,对于 webpack 不太熟练的小伙伴来说,这是一个很好的练手案例:逆向目标目标:…

实验7 文件应用编程

4. 实验任务4:1 #include<stdio.h>2 3 int main()4 {5 int i = 0;6 FILE *fp;7 char str[100];8 char ch;9 10 fp = fopen("data4.txt","r"); 11 if(fp == NULL) 12 { 13 printf("fail to open file\n&q…

电商现状的简单分析和后续的思考

目录前言我的经营策略商品特点商业模式:找大客户B2B不适合现在的电商目标用户不同流量费用过于昂贵我的破解方法:地推+自媒体地推自媒体 前言 我大概是从24年3月份开始搞电商的,开的是淘宝店,简单说一下我的感受 我的经营策略 商品特点 我家是做五金阀门的,所以我的商品的…

Java大文件上传、分片上传、多文件上传、断点续传、上传文件minio、分片上传minio等解决方案

根据不同场景使用不同方案进行实现尤为必要。通常开发过程中,文件较小,直接将文件转化为字节流上传到服务器,但是文件较大时,用普通的方法上传,显然效果不是很好,当文件上传一半中断再次上传时,发现需要重新开始,这种体验不是很爽,下面介绍几种好一点儿的上传方式。这…

CentOS 7安装Docker,并进行docker加速,拉取镜像

# step 1: 安装必要的一些系统工具 sudo yum install -y yum-utils device-mapper-persistent-data lvm2 # Step 2: 添加软件源信息 sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # Step 3: 更新并安装 Docker-CE sud…