前言:
本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM(MX6U)裸机篇”视频的学习笔记,在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。
引用:
正点原子IMX6U仓库 (GuangzhouXingyi) - Gitee.com
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.2.pdf》
正点原子资料下载中心 — 正点原子资料下载中心 1.0.0 文档
正文:
本文是 “正点原子[第二期]Linux之ARM(MX6U)裸机篇--第23讲 I2C驱动。本节将参考正点原子的视频教程第23讲和配套的正点原子开发指南文档进行学习。
0. 概述
I2C 是最常用的通信接口,众多的传感器都会提供 I2C 接口来和主控相连,比如陀螺仪、加速度计、触摸屏等等。所以 I2C 是做嵌入式开发必须掌握的, I.MX6U 有 4 个 I2C 接口,可以通过这 4 个 I2C 接口来连接一些 I2C 外设。 I.MX6U-ALPHA 使用 I2C1 接口连接了一个距离传感器 AP3216C,本章我们就来学习如何使用 I.MX6U 的 I2C 接口来驱动 AP3216C,读取AP3216C 的传感器数据。
1. I.MX6U 硬件I2C接口产生I2C Start,Stop,Repeated-Stop信号
1.1 产生I2C Start 信号
根据I2C总线协议,在I2C读/写时需要先发出一个Start信号,在SCL为高电平时,SDA出现下降沿从高电平变为低电平指示Start信号。然后是7比特的sub-address从机地址先传输高位在前地位在后(MSB-LSB),然后是1比特的方向(读写)指示位,1表示读0表示写,每发送8个数据位之后需要从机回复ACK/NAK,所以在第9个时钟周期,I2C主机释放SDA总线让从机来通过SDA拉低或拉高来回复ACK/NACK,I2C寻址匹配的从机再第9个时钟周期高电平期间拉低SDA回复ACK,保持SDA高电平表示回复NAK。
NACK信号出现的情况有如下几种:
- 1-无:I2C主机寻址时没有匹配的从机
- 2-忙:I2C从机忙,无法回复
- 3-错:I2C从机检测到数据错误或者处理错误
- 4-停:I2C主机 Master-Receiver 模式下通知从机停止发送
对于产生I2C Start信号I.MX6ULL手册中说:
After completion of the initialization procedure, serial data can be transmitted by selecting the Master Transmit mode. On a multimaster bus system, the busy bus (I2C_I2SR[IBB]) must be tested to determine whether the serial bus is free. If the bus is free (IBB = 0), the Start signal and the first byte (the slave address) can be sent. The data written to the data register comprises the address of the desired slave and the LSB indicates the transfer direction.
I.MX6U 参考手册中说I2C硬件接口的 Start 信号产生条件为:I2C选择Master-Transmit模式,等待I2C总线不忙时(busy=0),开始信号和第一字节(Slave-Address,从机地址+R/W方向位)可以被发送。写到I2Cx->I2DR数据寄存器的值包括呼叫的从机地址(7bit MSB优先)和读写方向位(1bit, R:1/W:0)。发送Start信号+7bit sub-address+R/W 这8位数据之后,在第9个时钟周期需要等待子设备回复ACK。
如下图片1,是正点原子示例源码和视频教程里提供的 I.MX6U I2C 硬件接口发起 Start 信号,并在I2Cx->I2DR数据加上i2c子设备呼叫地址的示例源码,通过此正点原子提供的I2C示例源码可以正确读取出来 AP3216C 红外/接近/环境光传感器的i2c数据。
但是根据I2C协议,在I2C Master主机发送Start信号+子设备地址呼叫之后,需要等待并确认是否有子设备被回复ACK信号表示有子设备被正确寻址。正点原理的示例源码里缺少了则一部分。
下面是我自己根据I.MX6U 参考手册里提供的 Polling Routine 模式下编程框图在正点原子示例源码的基础上,在 i2c_start() 函数做增强发出 start + sub-address 之后,等待并检查ACK回复。
下图是我自己在正点原子I.MX6U I2C 硬件接口 start 信号产生函数的基础上修改的源码,新增了发送Start+Sub-address+R/W 8位数据之后等待并检查从机ACK信号的步骤。
- I2C主机发送Start信号和Sub-address子设备地址+R/W读写方向 8位数据之后,需要检查子设备寻址是否正确,通过检查子设备回复的ACK信号来确认子设备地址呼叫成功。因为可能地址错误在I2C总线上并不存在被呼叫的字设备地址。
- I.MX6ULL 手册 I2C 硬件接口的指导文档说明,在关闭I2C接口中断的时候软件程序需要通过 Polling循环检测 IIF 标志位的方式等待数据传输(发送/接收)完成,然后检查ICF标志位来确认IIF被置位的原因(区分是传输完成IIF置1,还是仲裁丢失IIF置1)。
- 等待IFF,ICF被置1 数据传输完成之后,就可以检查ACK标志位了,ACK=1表示收到了子设备的ACK信号,地址呼叫成功。ACK=0表示收到了NACK信号,地址寻址失败。
1.2 I.MX6U I2C 硬件发送/接收之后的软件处理流程
如下图是I.MX6U 手册中说明的“Post-Transer software response” ,也就是在 I.MX6U I2C 硬件家口发送(Transmit)或者接收(Recive)一个字节之后软件需要进行的处理步骤。
- 这里说,在发送或者接收一个字节完成之后,I2Cx->I2SR 的 ICF 就会被设置为1
在完成之后 IIF 中断标志位也会别设置位1- 必须在软件里清除 IIF 中断标志位
- ICF标志位,在I2C Master Tx模式时写 I2DR写清除或者 I2C Master Rx模式时读清零
- 在从Address-Cycle周期结束之后,必须Toggle翻转I2C_L2CR[MTX]位,并且做一个 假读 dummy-read 来触发接收数据
- 手册同时指出在I.MX6U I2C 硬件接口的中断关闭之后,软件需要轮询检查(Polling)检查IIF标志位来确认传输是否完成,当IIF标志位置1时再检查ICF标志位来用来区分其他IIF中断的情况,例如,Arbitration-Lost 仲裁丢失。
I.MX6U 参考手册给出的I2C硬件中断关闭时Polling轮询模式编程代码框图。
- I.MX6ULL 参考手册的流程框图说,需要循环检测polling IIF 标志位来检查传输是否完成。
- 并且在Address-Cycle结束之后需要toggle(翻转)MTX方向并且加一个 dummy -read 假读来触发Rx接收数据。
1.3 产生I2C Stop 信号
在I2C数据传输(读/写)结束之后需要I2C主机发送一个 STOP 标志位,I.MX6U I2C硬件接口产生STOP信号的方式是:
通过清除I2Cx->I2CR寄存器的 MSTA bit[5] 标志位来产生一个 I2C STOP 信号。
- Master主机模式,通过MSTA从1修改0,触发一个STOP信号
- Slave从机模式,通过MSTA从0修改为1,触发一个Start信号并且选择为Master模式
这里I.MX6ULL 手册说在主机接收模式 Master-Received模式,再倒数第二个字节,I2C主机应该发送NAK信号给从机,并且在最后一个字节被读取之前需要触发一个Stop信号。
A data transfer ends when the master signals a Stop, which can occur after all data is sent.
For a master receiver to terminate a data transfer, it must inform the slave transmitter by
not acknowledging the last data byte. This is done by setting the transmit acknowledge
bit (I2C_I2CR[TXAK]) before reading the next-to-last byte. Before the last byte is read,
a Stop signal must be generated.
1.4 产生Repeated-Start信号
在I2C读时序中在发送Restat+从机地址+W+ACK+寄存器地址+ACK之后,需要重新发送一次Restart+从机地址+R。
I.MX6U I2C硬件接口产生Repeast-Start信号的方法如下:
2. I.MX6U I2C硬件接口为什么需要Dummy Read
在正点原子的视频教程里,左盟主说参考的NXP提供的I.MX6U I2C SDK源码发现在读取I2C硬件接口之前需要先做一次假读 Dummy-Read,为什么需要做 dummy-read 假读哪?
In Master Receive mode, reading the data register allows a read to occur and initiates the next byte to be received.
在I2C主机Master-Receive模式,read动作读取一个数据并且指示I2C接口开始接收下一个数据。
When an interrupt occurs at the end of the address cycle, the master is always in Transmit
mode; that is, the address is sent. If Master Receive mode is required, then I2C_I2CR[MTX] should be toggled and a dummy read of the I2C_I2DR register must be executed to trigger receive data.
在寻址周期结束的时候,I2C主机一定是在Master-Transmit模式,此时如果需要读取从机数据就必须先翻转 I2C-I2CR[MTX]位为Rx模式,并且做一次假读 dummy-read 触发I2C接口来读取数据。
- I.MX6ULL 参考手册的流程框图说,需要循环检测polling IIF 标志位来检查传输是否完成。
- 并且在Address-Cycle结束之后需要toggle(翻转)MTX方向并且加一个 dummy -read 假读来触发Rx接收数据。
2.1 I2C 读取数据
I.MX6U I2C硬件接口读取数据的部分如下,这里需要注意的是按照I.MX6U I2C 硬件接口的指导说明按照规定的时序回复 NAK 信号给从机,并且发送STOP信号给从机。
这里容易理解错误,并且源码编写错位的地方是 I.MX6ULL 手册要求 I2C Master Received 模式下 读 Rx 数据的时候,要求在倒数第二个读设置发送发送 NAK信号,在最后一次读时设置发送 STOP 信号。
如下源码,读I2Cx->I2DR寄存器动作本身的结果有两个:第一个是读取到当前已经接收到的Rx数据,第二个是read动作触发I2C接口进行下一个数据的接收。
/* 读取当前已接收数据,并触发下一次接收 */*buf++ = base->I2DR;
如上分析读I2Cx->I2DR寄存器读动作本身不进可以读取当前已经接收的数据还同时触发I2C接口进行下一个数据的Rx接收。按照这个分析,我们可以指导 “假读 Dummy-Read” 触发I.MX6U I2C 硬件接口开始接收数据,这也是I.MX6U 手册中明确要求的。
/* dummy read */dummy = base->I2DR;
2.2 I2C 发送数据
I.MX6U I2C硬件接口发送数据的部分如下,
3. I2C驱动源码编写
完整的驱动源码见如下我的Gitee链接,源码参考了正点的原子的示例源码和NXP提供I2C SDK示例源码:
imx6ull_mini: 基于正点原子I.MX6ULL Mini 视频教程的Linux ARM开发学习笔记,这里记录各个实验中所写的源码。 - Gitee.comhttps://gitee.com/iPickCan/imx6ull_mini/tree/master/17_i2c
bsp/bsp_i2c.c
#include "bsp_i2c.h"
#include "bsp_gpio.h"
#include "bsp_delay.h"
#include "stdio.h"/* 初始化I2C */
void i2c_init(I2C_Type *base)
{base->I2CR &= ~(1 << 7); /* 关闭I2C *//* 设置I2C时钟 */base->IFDR = 0x15 << 0; /* 640分频,IPG_CLK=66MHZ/640=103.125KHz */base->I2CR |= (1 << 7); /* 使能I2C */
}// /* I2C 等待ACK回复 */
// void i2c_wait_ACK(I2C_Type *base){
// /* 参考NXP的SDK驱动, bit[1]中断标志位 */
// while(!(base->I2SR & (1 << 1)));
// base->I2SR &= ~(1 << 1);
// }/* I2C Start 信号产生以及从机地址 */
unsigned char i2c_master_start(I2C_Type *base, unsigned char address, enum i2c_direction direction)
{uint8_t ack;/* 判断是否为忙 */if((base->I2SR & (1 << 5))){/* 当前I2C忙 */return 1;}/* 设置为主机模式 */// base->I2CR |= (1 << 5); /* bit[5]主机模式 Master */// base->I2CR |= (1 << 4); /* bit[4]主机发送模式 Tx*/base->I2CR |= (1 << 5) | (1 << 4);//清除IIF中断标志I2C1->I2SR &= ~(1 << 1);/* 产生Start 信号 *//* I2C从设备地址(bit[7:1]) *//* bit[0] 0 表示写,1表示read */base->I2DR = ((unsigned int)address) << 1 | ((direction == kI2C_Write)? 0 : 1);/* 检查I2C address calling 地址呼叫是否成功 */while(!(I2C1->I2SR & (1 << 1))); /* 检查IIF */if((I2C1->I2SR & (1 << 7))){ /* 检查ICF *//* 检查Start信号后面的sub-address calling 地址呼叫是否收到ACK */if((ack = i2c_check_and_clear_error(I2C1, I2C1->I2SR))){if(I2C_STATUS_NAK == ack){printf("I2C calling failed\r\n");return I2C_STATUS_ADDRNAK;}else{printf("I2C Arbitration lost error\r\n");return I2C_STATUS_ARBITRATIONLOST;}}}return 0;
}/* I2C Stop信号 */
unsigned char i2c_master_stop(I2C_Type *base)
{unsigned short timeout = 0xFFFF;/* bit[3] Transmit acknowledge enable, *//* bit[4] Transmit/Receive mode select, *//* bit[5] master/slave mode select, 软件清0产生一个stop信号 *///base->I2CR &= ~(1 << _I2C_I2CR_TXAK_SHIFT) | ~(1 << _I2C_I2CR_MTX_SHIFT) | ~(1 << _I2C_I2CR_MSTA_SHIFT);base->I2CR &= ~((1 << 3) | (1 << 4) | (1 << 5));/* 检查busy */while(base->I2SR & (1 << _I2C_I2SR_IBB_SHIFT)){timeout--;if(timeout == 0){return I2C_STATUS_TIMEOUT;}}return I2C_STATUS_OK;
}/* repeat-restart 信号*/
unsigned char i2c_master_repeated_start(I2C_Type *base, unsigned char address, enum i2c_direction direction)
{/* 判断是否为忙并且工作在从机模式下*/if((base->I2SR & (1 << 5)) && ((base->I2CR & (1 << 5)) == 0)){/* 判断是否为忙并且工作在从机模式下*/return 1;}/* bit[2] 写1生成一个repeat-start信号 *//* bit[5] 写1 master模式 *//* bit[4] 写1 tx模式 */base->I2CR |= (1 << 4) | (1 << 2);/* 写从机地址 */base->I2DR = ((unsigned int)address) << 1 | ((direction == kI2C_Write)? 0 : 1);return 0;
}/* 错误检查和清除函数 */
unsigned char i2c_check_and_clear_error(I2C_Type *base, unsigned int state)
{/* 先检查是否为仲裁丢失错误 *//* 仲裁丢失 */if(state & (1 << 4)){base->I2SR &= ~(1 << 4); /* 先清除标志位 */base->I2CR &= ~(1 << 7); /* 关闭I2C*/base->I2CR |= (1 << 7); /* 重新打开I2C, 关闭打开复位I2C */return I2C_STATUS_ARBITRATIONLOST;}/* 收到NACK信号 */else if(state & (1 << 0)){return I2C_STATUS_NAK;}return I2C_STATUS_OK;
}/* 发送数据函数 */
void i2c_master_write(I2C_Type *base, const unsigned char *buf, unsigned int size)
{//等待上一个发送传输完成while(!(base->I2SR & (1 << 7)));//清除中断标记位base->I2SR &= ~(1 << 1);/* 发送模式 */base->I2CR |= (1 << 4);while(size--){base->I2DR = *buf++;/* 参考NXP的SDK驱动, bit[1]中断标志位,等待数据发送完成 */while(!(base->I2SR & (1 << 1)));base->I2SR &= ~(1 << 1);/* 每发送8bit 检查ACK *///if(i2c_check_and_clear_error(base, base->I2SR) != I2C_STATUS_OK){if(i2c_check_and_clear_error(base, base->I2SR)){/* 有错误发生 */break;}}/* bit[1]中断标志位 */base->I2SR &= ~(1 << 1);/* Stop 停止位 */i2c_master_stop(base);
}/* 读数据函数 */
void i2c_master_read(I2C_Type *base, unsigned char *buf, unsigned int size)
{volatile uint8_t dummy = 0; /* 假读 *//* clean compile warning */dummy++;//等待上一个发送传输完成while((base->I2SR & (1 << 7)) == 0);/* bit[1]中断标志位 */base->I2SR = ~(1 << 1);base->I2CR &= ~( 1<< 4); /* bit[4] MTX Select, 选择Rx */base->I2CR &= ~(1 << 3); /* bit[3] TXAK 发送ACK */if(size == 1){/* 参考NXP的SDK驱动, 发送NACK信号 */base->I2CR |= (1 << 3);}/* dummy read */dummy = base->I2DR;while(size--){/* 等待数据接收完成 */while(!(base->I2SR & (1 << 1))); /* 等待数据传输完成 *//* bit[1]中断标志位 */base->I2SR = ~(1 << 1);if(size == 1){/* 参考NXP的SDK驱动, 发送NACK信号 */base->I2CR |= (1 << 3);}if(size == 0){/* 数据发送完成 */i2c_master_stop(base);}/* 读取当前已接收数据,并触发下一次接收 */*buf++ = base->I2DR;}
}/** @description : I2C数据传输,包括读和写* @param - base: 要使用的IIC* @param - xfer: 传输结构体* @return : 传输结果,0 成功,其他值 失败;*/
unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer)
{unsigned char ret = 0;enum i2c_direction direction = xfer->direction; base->I2SR &= ~((1 << 1) | (1 << 4)); /* 清除标志位,中断标志位和仲裁丢失标志位 *//* 等待传输完成 */while(!((base->I2SR >> 7) & 0X1)){}; /* 如果是读的话,要先发送寄存器地址,所以要先将方向改为写 */if ((xfer->subaddressSize > 0) && (xfer->direction == kI2C_Read)){direction = kI2C_Write;}ret = i2c_master_start(base, xfer->slaveAddress, direction); /* 发送开始信号 */if(ret){ return ret;}while(!(base->I2SR & (1 << 1))){}; /* 等待传输完成 */ret = i2c_check_and_clear_error(base, base->I2SR); /* 检查是否出现传输错误 */if(ret){i2c_master_stop(base); /* 发送出错,发送停止信号 */return ret;}/* 发送寄存器地址 */if(xfer->subaddressSize){do{base->I2SR &= ~(1 << 1); /* 清除标志位 */xfer->subaddressSize--; /* 地址长度减一 */base->I2DR = ((xfer->subaddress) >> (8 * xfer->subaddressSize)); //向I2DR寄存器写入子地址while(!(base->I2SR & (1 << 1))); /* 等待传输完成 *//* 检查是否有错误发生 */ret = i2c_check_and_clear_error(base, base->I2SR);if(ret){i2c_master_stop(base); /* 发送停止信号 */return ret;} } while ((xfer->subaddressSize > 0) && (ret == I2C_STATUS_OK));if(xfer->direction == kI2C_Read) /* 读取数据 */{base->I2SR &= ~(1 << 1); /* 清除中断挂起位 */i2c_master_repeated_start(base, xfer->slaveAddress, kI2C_Read); /* 发送重复开始信号和从机地址 */while(!(base->I2SR & (1 << 1))){};/* 等待传输完成 *//* 检查是否有错误发生 */ret = i2c_check_and_clear_error(base, base->I2SR);if(ret){ret = I2C_STATUS_ADDRNAK;i2c_master_stop(base); /* 发送停止信号 */return ret; }}} /* 发送数据 */if ((xfer->direction == kI2C_Write) && (xfer->dataSize > 0)){i2c_master_write(base, xfer->data, xfer->dataSize);}/* 读取数据 */if ((xfer->direction == kI2C_Read) && (xfer->dataSize > 0)){i2c_master_read(base, xfer->data, xfer->dataSize);}return 0;
}
4. 编译烧写SD卡验证结果
编译源码烧录SD卡验证本节的 I.MX6U I2C驱动实验。预期烧录SD卡后正点原子I.MX6ULL ALPHA/Mini 开发板后,可以通过I2C总线读取到AP3216C 红外/接近/环境光 IR/PS/ALS传感器的采集值。
我本地验证的结果是烧录SD卡后,在串口里可以打印出通过I2C总线读取到AP3216C 红外/接近/环境光 IR/PS/ALS传感器的采集值。
5. 总结和实验遇到问题记录
5.1 错误1: if 判断条件里将运算符 "&" 错误的写成了 "&=",这样寄存器的值就完全给搞错了
错误1: if 判断条件里将运算符 "&" 错误的写成了 "&=",这样寄存器的值就完全给搞错了,这样的错误写法最重要的是将 I2Cx->CR 的 bit[7] 给设置为了0关闭了 I2C 接口
5.2 错误2:将运算符 “&” 错误的写成了“<<"运算符,if条件执行的结果完全不同并且把寄存器状态给搞错乱了。
错误2:将运算符 “&” 错误的写成了“<<"运算符,if条件执行的结果完全不同并且把寄存器状态给搞错乱了。
5.3 错误3:将运算符“&=”,错写成了 “=” 运算符,本来是清除一个bit位,结果却成了赋值,寄存器的状态完全错了
错误3:将运算符“&=”,错写成了 “=” 运算符,本来是清除一个bit位,结果却成了赋值,寄存器的状态完全错了
6. 结束
本文至此结束。