本文将基于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);
}
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;
}
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;
}
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;
}
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;
}
}
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;
}
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.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;
}
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相关的寄存器地址使用宏定义在头文件中定义好,方便使用
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_buf
msgs[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,IR
if(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,0x0D
ps: 0x0E,0x0F
*/
unsigned short data[3]; //data[0] data[1] IR data[2] data[3] ALS data[4] data[5] PS
int 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_buf
msgs[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编程可以分为两层,一层是主机控制器层面,这一层面只负责收发,另一层是从设备层面,这一层要对收发的数据进行解析。
标签:控制器,transfer,存储芯片,BASE,AT24C02,IIC,寄存器,I2C From: https://www.cnblogs.com/starstxg/p/18255175