IIC简介
物理层
连接多个devices
它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
两根线
一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA)
,一条串行时钟线(SCL)
。数据线即用来表示数据,时钟线用于数据收发同步。
设备地址
每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
上拉电阻和高阻态
总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
仲裁
多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
三种速度模式
具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
最大devices限制
连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。
协议层
I2C 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
I2C 基本读写过程
主机写数据到从机
主机由从机中读数据
I2C 通讯复合格式
数据由主机传输至从机
数据由从机传输至主机
: 传输开始信号
: 传输方向选择位,1 为读,0 为写
: 应答(ACK)或非应答(NACK)信号
停止传输信号
1.S是起始信号,由主机产生,挂载在总线上的设备都会收到,准备接收主机下一个数据
2.设备聆听地址信息,选择从机: 起始信号之后,主机会广播从机地址,在 I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。根据 I2C 协议,这个从机地址可以是 7 位或 10 位。
3.传输方向:主机广播传输方向,0是主机到从机(主机往从机写),1为从机到主机(从机往主机写)
4.从机应答:回复ack与nack,只有收到ack之后,主机继续发送或者接收数据
写数据
主机首先在 IIC 总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送从机地+0(写操作)组成的 8bit 数据,所有从机接收到该 8bit 数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。注意:IIC 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
读数据
若配置的方向传输位为“读数据”方向,即第二幅图的情况,广播完地址,接收到应答信号后,从机开始向主机返回数据(DATA),数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号(ACK),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输。
信号电平
起始 停止信号
当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA 线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。
数据有效性 SCL的高电平
SCL为高电平的时候 SDA表示的数据有效,SCL为低电平时数据变换
地址及数据方向
I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS)来查找从机。I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向。
读数据方向时,主机会释放对 SDA 信号线的控制,由从机控制 SDA 信号线,主机接收信号,写数据方向时,SDA 由主机控制,从机接收信号。
大端(高有效位存在低地址),波形高位先行
ACK NACK
主站发送起始信号,地址,读写信号之后释放SDA控制权,从站开始自动控制SDA信号发送ACK NACK
整体控制逻辑
整体控制逻辑负责协调整个 I2C 外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR1 和 SR2)”,我们只要读取这些寄存器相关的寄存器位,就可以了解 I2C的工作状态。除此之外,控制逻辑还根据要求,负责控制产生 I2C 中断信号、DMA 请求及各种 I2C 的通讯信号(起始、停止、响应信号等)。
IIC使用 读写EEPROM为例
要点
(1) 配置通讯使用的目标引脚为开漏模式;
(2) 使能 I2C 外设的时钟;
(3) 配置 I2C 外设的模式、地址、速率等参数并使能 I2C 外设;
(4) 编写基本 I2C 按字节收发的函数;
(5) 编写读写 EEPROM 存储内容的函数;
硬件IIC
配置IIC复用的GPIO
#配置 GPIO 复用
GPIO_InitTypeDef GPIO_InitStructure;/* 使能与 I2C 有关的时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB)/* 配置GPIO为开漏输出模式 */
//配置Gpio初始化结构体略
GPIO_Init(GPIOB, &GPIO_InitStructure);
1.配置IIC模式
I2C_InitTypeDef I2C_InitStructure;/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;/*!< 指定工作模式,可选 I2C 模式及 SMBUS 模式 *//* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; /*指定时钟占空比,可选 low/high = 2:1 及 16:9 模式*//*!< 指定自身的 I2C 设备地址 */
I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7;/*!< 指定自身的 I2C 设备地址 */I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ; /*!< 使能或关闭响应(一般都要使能) *//* I2C 的寻址模式 */
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; /*!< 指定地址的长度,可为 7 位及 10 位 */I2C_InitStructure.I2C_ClockSpeed = I2C_Speed; /*!< 设置 SCL 时钟频率,此值要低于 400000*/I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE);
1.1作为主机端,进行数据发送
即作为 I2C 通讯的主机端时,对外部发送数据的过程
以写一个E2ROM为例
(1) 控制产生起始信号(S),当发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送;
(2) 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”及“EV8”,这时 SR1 寄存器的“ADDR”位及“TXE”位被置 1,ADDR 为 1 表示地址已经发送,TXE 为 1 表示数据寄存器为空;
(3) 以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的“数据寄存器 DR”写入要发送的数据,这时TXE位会被重置0,表示数据寄存器非空,I2C外设通过SDA信号线一位位把数据发送出去后,又会产生“EV8”事件,即 TXE 位被置 1,重复这个过程,就可以发送多个字节数据了;
(4) 当我们发送数据完成后,控制 I2C 设备产生一个停止信号§,这个时候会产生EV8_2 事件,SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。
假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。
IIC STM32固件库函数链接: IIC STM32固件库函数链接
1.1.1发送单字节数据
AT24C02的单字节时序规定,向它写入数据的时候,第一个字节为内存地址,第二个字节是要写入的数据内容。在发送device地址之后,需要发送eeprom的内部存储地址和需要存的数据
#define EEPROM_I2Cx I2C1/* 具体修改的寄存器位置,查看固件库函数*/uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr){/* 产生 I2C 起始信号 *///I2Cx->CR1 |= I2C_CR1_START; 操作I2C中的CR1寄存器I2C_GenerateSTART(I2C1, ENABLE);/*设置超时等待时间*/I2CTimeout = I2CT_FLAG_TIMEOUT;//I2CTimeout 常数/* 检测 EV5 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT)){/* 超时,报错*/if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);}/* 发送 EEPROM 设备地址 */I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS,I2C_Direction_Transmitter);/* 检测 EV6 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);}/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) *//* EEPROM 内部存储器读写的规则是第一个数据为要读写的地址,第二个数据是具体读写的数据 */I2C_SendData(EEPROM_I2Cx, WriteAddr);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);}/* 发送一字节要写入的数据 */I2C_SendData(EEPROM_I2Cx, *pBuffer);I2CTimeout = I2CT_FLAG_TIMEOUT;I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);}/* 发送停止信号 */I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);}
1.1.2发送多字节数据
等待存储设备准备好
这个函数主要实现是向 EEPROM 发送它设备地址,检测 EEPROM 的响应,若EEPROM 接收到地址后返回应答信号,则表示 EEPROM 已经准备好,可以开始下一次通讯。函数中检测响应是通过读取 STM32 的 SR1 寄存器的 ADDR 位及 AF 位来实现的,当I2C 设备响应了地址的时候,ADDR 会置 1,若应答失败,AF 位会置 1。
//等待 EEPROM 到准备状态void I2C_EE_WaitEepromStandbyState(void){vu16 SR1_Tmp = 0;do {/* 发送起始信号 */I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);/* 读 I2C1 SR1 寄存器 */SR1_Tmp = I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1);/* 发送 EEPROM 地址 + 写方向 */I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS,I2C_Direction_Transmitter);}// SR1 位 1 ADDR:1 表示地址发送成功,0 表示地址发送没有结束// 等待地址发送成功while (!(I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1) & 0x0002));/* 清除 AF 位 */I2C_ClearFlag(EEPROM_I2Cx, I2C_FLAG_AF);/* 发送停止信号 */I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
}
循环单字节发送
uint8_t I2C_EE_ByetsWrite(uint8_t* pBuffer,uint8_t WriteAddr,uint16_t NumByteToWrite)
{uint16_t i;uint8_t res;/*每写一个字节调用一次 I2C_EE_ByteWrite 函数,前文的单字节写入*/for (i=0; i<NumByteToWrite; i++){/*等待 EEPROM 准备完毕*/I2C_EE_WaitEepromStandbyState();/*按字节写入数据*/res = I2C_EE_ByteWrite(pBuffer++,WriteAddr++);}return res;
}
1.1.3 EEPROM 的页写入
EEPROM 定义了一种页写入时序,只要告诉 EEPROM 第一个内存地址 address1,后面的数
据按次序写入到 address2、address3… 这样可以节省通讯的时间,加快速度。
根据页写入时序,第一个数据被解释为要写入的内存地址 address1,后续可连续发送 n个数据,这些数据会依次写入到内存中。其中 AT24C02 型号的芯片页写入时序最多可以一次发送 8 个数据(即 n = 8 ),该值也称为页大小,某些型号的芯片每个页写入时序最多可传输 16 个数据。
但是这种方式还是比较慢,拘泥于页的大小。
//在 EEPROM 的一个写循环中可以写多个字节,但一次写入的字节数不能超过 EEPROM 页的大小,AT24C02 //每页有 8 个字节
//NumByteToWrite:要写的字节数要求 NumByToWrite 小于页大小uint8_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr,uint8_t NumByteToWrite)
{ I2CTimeout = I2CT_LONG_TIMEOUT;while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);}/* 产生 I2C 起始信号 */I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV5 事件并清除标志 */while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);}/* 发送 EEPROM 设备地址 */I2C_Send7bitAddress(EEPROM_I2Cx,EEPROM_ADDRESS,I2C_Direction_Transmitter);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV6 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);}/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */I2C_SendData(EEPROM_I2Cx, WriteAddr);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (! I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);}/* 循环发送 NumByteToWrite 个数据 */while (NumByteToWrite--){/* 发送缓冲区中的数据 */I2C_SendData(EEPROM_I2Cx, *pBuffer);/* 指向缓冲区中的下一个数据 */pBuffer++;I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);}}/* 发送停止信号 */I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
}
1.1.4 EEPROM 的页写入plus
利用 EEPROM 的页写入方式,可以改进前面的“多字节写入”函数,加快传输速度
// AT24C01/02 每页有 8 个字节#define I2C_PageSize 8void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr,u16 NumByteToWrite){u8 NumOfPage=0,NumOfSingle=0,Addr =0,count=0,temp =0;/*mod 运算求余,若 writeAddr 是 I2C_PageSize 整数倍,运算结果 Addr 值为 0*//*判断目标地址(起始地址)是否为一页的开始*/Addr = WriteAddr % I2C_PageSize;/*差 count 个数据值,刚好可以对齐到页地址*/count = I2C_PageSize - Addr;/*计算出要写多少整数页*/NumOfPage = NumByteToWrite / I2C_PageSize;/*mod 运算求余,计算出剩余不满一页的字节数*/NumOfSingle = NumByteToWrite % I2C_PageSize;// Addr=0,则 WriteAddr 刚好按页对齐 aligned,也就是从这页的第一个byte开始// 这样就很简单了,直接写就可以,写完整页后, 把剩下的不满一页的写完即可if (Addr == 0) {//从某页的第一位写起/* 如果 NumByteToWrite < I2C_PageSize *//* 总字数还不满一页 */if (NumOfPage == 0) {//总页数不满一页I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);I2C_EE_WaitEepromStandbyState();}/* 如果 NumByteToWrite > I2C_PageSize */else {/*先把整数页都写了*/while (NumOfPage--) {I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);//写一页I2C_EE_WaitEepromStandbyState();//等待rom准备好WriteAddr += I2C_PageSize; //写完一页之后,下一页的地址pBuffer += I2C_PageSize; //写完一页之后,要传输的源地址也要加}/*若有多余的不满一页的数据,把它写完*/if (NumOfSingle!=0) {I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);I2C_EE_WaitEepromStandbyState();}}}从某页的第一位写起// 如果 WriteAddr 不是按 I2C_PageSize 对齐// 那就算出对齐到页地址还需要多少个数据,先把这几个数据写完,剩下开始的地址就已经对齐else{//不是从某页的第一位写起/* 如果 NumByteToWrite < I2C_PageSize */if (NumOfPage== 0){//总数不满一页if (NumOfSingle > count) {//count:还需要写多少个字节才能页面对齐//NumOfSingle:当前页面空闲的字节数// temp 的数据要写到写一页temp = NumOfSingle - count;I2C_EE_PageWrite(pBuffer, WriteAddr, count);//写count数目的到当前页I2C_EE_WaitEepromStandbyState();//等待准备完毕WriteAddr += count;//目标地址移动pBuffer += count;I2C_EE_PageWrite(pBuffer, WriteAddr, temp);//在下一页写完剩下的I2C_EE_WaitEepromStandbyState();}else{ /*若 count 比 NumOfSingle 大*/I2C_EE_PageWrite(pBuffer, WriteAddr, NumByteToWrite);I2C_EE_WaitEepromStandbyState();}}/* 如果 NumByteToWrite > I2C_PageSize *//* 如果 不止写一页 */else { /* 如果 不止写一页 *//*地址不对齐多出的 count 分开处理,不加入这个运算*/NumByteToWrite -= count;NumOfPage = NumByteToWrite / I2C_PageSize;NumOfSingle = NumByteToWrite % I2C_PageSize;/*先把 WriteAddr 所在页的剩余字节写了*/if (count != 0) {I2C_EE_PageWrite(pBuffer, WriteAddr, count);I2C_EE_WaitEepromStandbyState();/*WriteAddr 加上 count 后,地址就对齐到页了*/WriteAddr += count;pBuffer += count;}/*把整数页都写了*/while (NumOfPage--) {I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);I2C_EE_WaitEepromStandbyState();WriteAddr += I2C_PageSize;pBuffer += I2C_PageSize;}/*若有多余的不满一页的数据,把它写完*/if (NumOfSingle != 0) {I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);I2C_EE_WaitEepromStandbyState();}}/*把整数页都写了*/}/* 如果 不止写一页 */ }
1.2作为主机端,进行数据接收
即作为 I2C 通讯的主机端时,从外部接收数据的过程
(1) 同主发送流程,起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送;
(2) 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时SR1 寄存器的“ADDR”位被置 1,表示地址已经发送。
(3) 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7”事件,SR1 寄存器的 RXNE被置 1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控制I2C 发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输;
(4) 发送非应答信号后,产生停止信号P,结束传输。
uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr,u16 NumByteToRead){I2CTimeout = I2CT_LONG_TIMEOUT;while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);}/* 产生 I2C 起始信号 */I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV5 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);}/* 发送 EEPROM 设备地址 */I2C_Send7bitAddress(EEPROM_I2Cx,EEPROM_ADDRESS,I2C_Direction_Transmitter);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV6 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);} /*通过重新设置 PE 位清除 EV6 事件 */I2C_Cmd(EEPROM_I2Cx, ENABLE);/* 发送要读取的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) *//* EEPROM 内部存储器读写的规则是第一个数据为要读写的地址,第二个数据是具体读写的数据 */I2C_SendData(EEPROM_I2Cx, ReadAddr);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);}/* 产生第二次 I2C 起始信号 */I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);I2CTimeout = I2CT_FLAG_TIMEOUT; /* 检测 EV5 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);}/* 发送 EEPROM 设备地址 */I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Receiver);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV6 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);}/* 读取 NumByteToRead 个数据*/while (NumByteToRead){/*若 NumByteToRead=1,表示已经接收到最后一个数据了,发送非应答信号,结束传输*/if (NumByteToRead == 1){/* 发送非应答信号 */I2C_AcknowledgeConfig(EEPROM_I2Cx, DISABLE);/* 发送停止信号 */I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);}I2CTimeout = I2CT_LONG_TIMEOUT;while (I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED)==0){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);}{/*通过 I2C,从设备中读取一个字节的数据 */*pBuffer = I2C_ReceiveData(EEPROM_I2Cx);/* 存储数据的指针指向下一个地址 */pBuffer++;/* 接收数据自减 */NumByteToRead--;} } /* 读取 NumByteToRead 个数据*//* 使能应答,方便下一次 I2C 传输 */I2C_AcknowledgeConfig(EEPROM_I2Cx, ENABLE);}
1.3作为从机端,进行数据接收
1.4作为从机端,进行数据接收
GPIO模拟IIC
我之前使用这套方法读取一个外置的RTC模块,参考正点原子的例程,这边就不赘述了。或者之后有机会把例程贴出来哈
1.配置GPIO
2.编写微秒级软延时
3.根据时序编写各个函数
DMA 与IIC
可以使用DMA,对IIC的DR中要传输或者接收的数据进行搬运,DMA请求(当被使能时)仅用于数据传输。发送时数据寄存器变空或接收时数据寄存器变满,则产生DMA请求。DMA请求必须在当前字节传输结束之前被响应。当为相应DMA通道设置的数据传输量已经完成时,DMA控制器发送传输结束信号ETO到I2C接口,并且在中断允许时产生一个传输完成中断。
主发送器:在EOT中断服务程序中,需禁止DMA请求,然后在等到BTF事件后设置停止条件。
主接收器:当要接收的数据数目大于或等于2时,DMA控制器发送一个硬件信号EOT_1,它对应DMA传输(字节数-1)。如果在I2C_CR2寄存器中设置了LAST位,硬件在发送完EOT_1后的下一个字节,将自动发送NACK。在中断允许的情况下,用户可以在DMA传输完成的中断服务程序中产生一个停止条件。
意思是说当DMA产生EOT标志后,(如果开启了EOT相关中断就进中断程序,没有开启就进行软件查询做后续处理)关闭DMA请求,然后等待BTF事件,之后执行STOP操作。 这里的BTF事件就是I2C数据收发过程中的数据字节是否传输完成的的事件。
IIC通讯过程 标志位
配置DMA
对DMA进行配置
在IIC的配置中开启DMA
通过设置I2C_CR2寄存器中的DMAEN位可以激活DMA模式。只要TxE位被置位,数据将由DMA从预置的存储区装载进I2C_DR寄存器。为I2C分配一个DMA通道,须执行以下步骤(x是通道号)。
利用DMA发送
-
在DMA_CPARx寄存器中设置I2C_DR寄存器地址。数据将在每个TxE事件后从存储器传
送至这个地址。 -
在DMA_CMARx寄存器中设置存储器地址。数据在每个TxE事件后从这个存储区传送至
I2C_DR。 -
在DMA_CNDTRx寄存器中设置所需的传输字节数。在每个TxE事件后,此值将被递减。
-
利用DMA_CCRx寄存器中的PL[0:1]位配置通道优先级。
-
设置DMA_CCRx寄存器中的DIR位。
-
根据应用要求可以配置在整个传输完成一半或全部完成时发出中断请求。
-
通过设置DMA_CCTx寄存器上的EN位激活通道。
当DMA控制器中设置的数据传输数目已经完成时,DMA控制器给I2C接口发送一个传输结束的EOT/ EOT_1信号。在中断允许的情况下,将产生一个DMA中断。
如果使用DMA进行发送时,不要设置I2C_CR2寄存器的ITBUFEN位。
利用DMA接收
通过设置I2C_CR2寄存器中的DMAEN位可以激活DMA接收模式。每次接收到数据字节时,将由DMA把I2C_DR寄存器的数据传送到设置的存储区(参考DMA说明)。设置DMA通道进行I2C接收,须执行以下步骤(x是通道号):
- 在DMA_CPARx寄存器中设置I2C_DR寄存器的地址。数据将在每次RxNE事件后从此地
址传送到存储区。 - 在DMA_CMARx寄存器中设置存储区地址。数据将在每次RxNE事件后从I2C_DR寄存器
传送到此存储区。 - 在DMA_CNDTRx寄存器中设置所需的传输字节数。在每个RxNE事件后,此值将被递
减。 - 用DMA_CCRx寄存器中的PL[0:1]配置通道优先级。
- 清除DMA_CCRx寄存器中的DIR位,根据应用要求可以设置在数据传输完成一半或全部
完成时发出中断请求。 - 设置DMA_CCRx寄存器中的EN位激活该通道。
当DMA控制器中设置的数据传输数目已经完成时,DMA控制器给I2C接口发送一个传输结束的
EOT/ EOT_1信号。在中断允许的情况下,将产生一个DMA中断。
注: 如果使用DMA进行接收时,不要设置I2C_CR2寄存器的ITBUFEN位