大家好,欢迎来到IT知识分享网。
IIC与E2PROM通信实验
IIC工作逻辑图
IIC中事件标志
从发送器
在接收到地址和清除ADDR位后,从发送器将字节从DR寄存器经由内部移位寄存器发送到SDA 线上。
说明:S=Start(起始条件),Sr=重复的起始条件,P=Stop(停止条件),A=响应,NA=非响应,
EVx=事件(ITEVFEN=1时产生中断)
事件名称 |
事件产生原因 |
含义 |
如何清除事件标志位 |
EV1 |
ADDR=1,收到地址且地址匹配 |
地址已被发送(主模式)/地址匹配(从模式) |
读SR1然后读SR2将清除该事件(即先读SR1后读SR2这样ADDR就会被置位0) |
EV3-1 |
TxE=1,移位寄存器空,数据寄存器空 |
未发送任何数据或者刚发送完地址数据(在发送数据时,数据寄存器为空时该位被置’1’,在发送地址阶段不设置该位) |
软件写数据到DR寄存器可清除该位;或在发生一个起始或停止条件后 |
EV3 |
TxE=1,移位寄存器非空,数据寄存器空 |
正在将DR数据寄存器存储的数据通过移位寄存器转移至SDL数据总线上,即正在发送数据 |
写DR数据寄存器将清除该事件 |
EV3-2 |
AF=1,应答失败,数据传输中断 |
停止数据传输(当没有返回应答(返回NACK空应答信号)时,硬件将置该位为’1’) |
在SR1寄存器的AF位写’0’可清除AF位,或者当我们再次使能IIC设备时,AF位自动清除 |
注意:
① EV1和EV3-1拉长了SCL低电平时间,也就是说如果我们不清除这两个事件的标志位,那么从发送器是不会发送信息的,如果我们不清除这两个事件,那么SCL上的电平会被无限拉低,这导致信息发送不出去;
② AF位不用手动清除也是可以的,AF标志一旦出现说明我们的IIC传输已经结束,IIC传输再次使能建立时,AF位可以被自动清除;
③ EV3事件标志只会在数据传输时出现,一个字节数据传输完后,EV3事件会自动消失。
事件标志 |
事件名称 |
介绍 |
从发送 |
||
I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED |
EV1 |
成功发送地址 |
I2C_EVENT_SLAVE_BYTE_TRANSMITTED |
|
成功传输一个字节数据 |
I2C_EVENT_SLAVE_BYTE_TRANSMITTING |
EV3 |
正在传输一个字节 |
I2C_EVENT_SLAVE_ACK_FAILURE |
EV3_2 |
应答失败/停止传输 |
从接收器
在接收到地址并清除ADDR(为了清除EV1事件标志)后,从接收器将通过内部移位寄存器从SDA线接收到的字节存进DR 寄存器。
说明:S=Start(起始条件),Sr=重复的起始条件,P=Stop(停止条件),A=响应,NA=非响应, EVx=事件(ITEVFEN=1时产生中断)
事件名称 |
事件产生原因 |
含义 |
如何清除事件标志位 |
EV1 |
ADDR=1(在接收时,当数据寄存器不为空,该位被置’1’。在接收地址阶段,该位不被置位) |
成功接收发送器地址,建立数据传输联系 |
读SR1然后读SR2将清除该事件 |
EV2 |
RxNE=1(数据寄存器非空(接收时)) |
接收数据非空 |
读DR数据寄存器将清除该事件 |
EV4 |
STOPF=1(停止条件检测位(从模式)) |
检测到停止条件 |
读SR1然后写CR1寄存器将清除该事件 |
事件标志 |
事件名称 |
介绍 |
从接收 |
||
I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED |
EV1 |
从模式下,成功接收地址 |
I2C_EVENT_SLAVE_BYTE_RECEIVED |
EV2 |
从模式下,成功接收一个字节数据 |
I2C_EVENT_SLAVE_STOP_DETECTED |
EV4 |
从模式下,接收停止 |
主发送器
说明:S=Start(起始条件),Sr=重复的起始条件,P=Stop(停止条件),A=响应,NA=非响应, EVx=事件(ITEVFEN=1时产生中断)。
事件名称 |
事件产生原因 |
含义 |
事件标志位如何清除 |
EV5 |
SB=1 |
起始条件以发送,已占用总线 |
读SR1然后将地址写入DR寄存器将清除该事件 |
EV6 |
ADDR=1,地址发送成功 |
主模式下,第一个含有地址传输方向的数据发送成功 |
读SR1然后读SR2将清除该事件 |
EV8_1 |
TxE=1(在发送数据时,数据寄存器为空时该位被置’1’,在发送地址阶段不设置该位),移位寄存器空,数据寄存器空 |
主模式下,刚发送完地址数据,还未发送正式的数据 |
写DR数据寄存器 |
EV8 |
TxE=1(在发送数据时,数据寄存器为空时该位被置’1’,在发送地址阶段不设置该位),移位寄存器非空,数据寄存器空 |
主模式下,表示数据已写入数据寄存器并正在移出 |
写入DR寄存器将清除该事件 |
EV8_2 |
TxE=1,BTF=1,请求设置停止位 |
不想发送数据了,等待停止信号,即一个字节传输完成后传输出现间隔 |
TxE和BTF位由硬件在产生停止条件时清除 |
注意:
① EV6,EV8_1和EV5如果不清楚对应的事件标志位,那么该事件就会一直存在,EV5事件标志不清楚就无法进行“地址与传输方向”数据的传输,EV6和EV8_1事件标志位不清楚就无法进行以后的正常数据传输;
② EV6事件的检测如果加上BTF一个字节传输完成标志可以更加精确的判断第一个字节数据已经发送完毕;
③ 当TxE或BTF位置位时,停止条件应安排在出现EV8_2事件时,即当我们判断EV8_2事件发生时,我们可以发送停止传输信号;
④ 在DR寄存器中写入最后一个字节后,通过设置STOP位产生一个停止条件(见图245的EV8_2), 然后I2C接口将自动回到从模式(M/S位清除)-I2C默认工作在从模式;
⑤ EV8_2也比EV8更适合于最后一次数据传输的测试(停止条件生成之前)。
⑥ ADDR被置1的条件:
⑴ 7位地址模式时,当收到地址的ACK后该位被置’1’(主模式);
⑵ 当收到的从地址与OAR寄存器中的内容相匹配;
⑦ 主发送模式通信关闭方式:
在DR寄存器中写入最后一个字节后,通过设置STOP位产生一个停止条件(见图245的EV8_2), 然后I2C接口将自动回到从模式(M/S位清除)。
事件标志 |
事件名称 |
介绍 |
主发送 |
||
I2C_EVENT_MASTER_MODE_SELECT |
EV5 |
模式为“主模式” |
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED |
EV6 |
模式为“主发送” |
I2C_EVENT_MASTER_BYTE_TRANSMITTED |
EV8_2 |
主模式下,字节传输完毕 |
I2C_EVENT_MASTER_BYTE_TRANSMITTING |
EV8 |
主模式下,一个字节正在发生 |
主接收器
说明:S=Start(起始条件),Sr=重复的起始条件,P=Stop(停止条件),A=响应,NA=非响应, EVx=事件(ITEVFEN=1时产生中断)
事件名称 |
事件发生条件 |
含义 |
事件标志为清楚方式 |
EV5 |
SB=1 |
起始条件发送成功 |
读SR1然后将地址写入DR寄存器将清除该事件 |
EV6 |
ADDR=1,地址发送完成标志置位 |
接收地址成功与目标地址匹配 |
读SR1然后读SR2将清除该事件 |
EV7 |
RxNE=1(在接收时,当数据寄存器不为空,该位被置’1’。在接收地址阶段,该位不被置位),接收数据非空 |
正在接收数据 |
读DR寄存器清除该事件 |
EV7_1 |
RxNE=1(在接收时,当数据寄存器不为空,该位被置’1’。在接收地址阶段,该位不被置位),接收数据非空 |
最后一个字节正在发送,接下来应该设置ACK=0和STOP请求了 |
读DR寄存器清除该事件 |
注意:
① 主机必须等待事件EV7完成,然后读取从设备接收的数据(I2C_ReceiveData()函数);
② EV7主模式一个字节传输完成事件检测的同时应检测,是否已经完成传输一个字节(检查SR1状态寄存器的BTF标志);
③ 主接受模式通信关闭方式:
主设备在从从设备接收到最后一个字节后发送一个NACK。接收到NACK后,从设备释放对SCL 和SDA线的控制;主设备就可以发送一个停止/重起始条件。
● 为了在收到最后一个字节后产生一个NACK脉冲,在读倒数第二个数据字节之后(在倒数第 二个RxNE事件之后)必须清除ACK位;
● 为了产生一个停止/重起始条件,软件必须在读倒数第二个数据字节之后(在倒数第二个 RxNE事件之后)设置STOP/START位;
● 只接收一个字节时,刚好在EV6之后(EV6_1时,清除ADDR之后)要关闭应答和停止条件的 产生位。 在产生了停止条件后,I 2 C接口自动回到从模式(M/SL位被清除);
事件标志 |
事件名称 |
介绍 |
主发送 |
||
I2C_EVENT_MASTER_MODE_SELECT |
EV5 |
模式为“主模式” |
I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED |
EV5 |
模式为“主接收” |
I2C_EVENT_MASTER_BYTE_RECEIVED |
EV7 |
主模式下,一个字节接收完毕 |
IIC的状态标志
状态标志 |
状态位1/0代表的含义 |
I2C_FLAG_TRA |
0:接收到数据; 1:数据已发送; |
I2C_FLAG_BUSY |
0:在总线上无数据通讯; 1:在总线上正在进行数据通讯; |
I2C_FLAG_MSL |
0:从模式; 1:主模式; |
I2C_FLAG_TIMEOUT |
0:无超时错误; 1 : SCL 处于低已达到 25ms( 超 时 ) ;或者主机低电平累积时钟扩展时间超过 10ms(Tlow:mext);或从设备低电平累积时钟扩展时间超过25ms(Tlow:sext); |
I2C_FLAG_AF |
0:没有应答失败; 1:应答失败; |
I2C_FLAG_RXNE |
0:数据寄存器为空; 1:数据寄存器非空; |
I2C_FLAG_STOPF |
0:没有检测到停止条件; 1:检测到停止条件; |
I2C_FLAG_BTF |
0:字节发送未完成; 1:字节发送结束; |
I2C_FLAG_ADDR |
0:地址不匹配或没有收到地址; 1:收到的地址匹配(从模式)/ 0:地址发送没有结束; 1:地址发送结束(主模式) |
I2C_FLAG_SB |
0:未发送起始条件; 1:起始条件已发送 |
I2C_FLAG_BERR |
0:无起始或停止条件出错; 1:起始或停止条件出错 |
I2C_FLAG_OVR |
0: 无过载/欠载; 1: 出现过载/欠载 |
注:
① TIMEOUT—超时或Tlow错误,对应的处理方式:
⑴ 当在从模式下设置该位:从设备复位通讯,硬件释放总线;
⑵ 当在主模式下设置该位:硬件发出停止条件;
⑶ 该位由软件写’0’清除,或在PE=0(I2C失能)时由硬件清除;
② AF—应答失败,在那种情况下被置位:
当没有返回应答(NACK)时,硬件将置该位为’1’;
③ 如果使用I2C_ClearFlag函数清楚不了状态标志位,就仔细看看特定位的清楚方式,例如:ADDR位的清除方式是“读取SR1然后读取SR2”;
④ OVR状态标志位置位原因:
⑴ 在接收模式中当收到一个新的字节时(包括ACK应答脉冲),数据寄存器里的内容还未被读 出,则新接收的字节将丢失;
⑵ 在发送模式中当要发送一个新的字节时,却没有新的数据写入数据寄存器,同样的字节将被发送两次。
IIC的中断标志
中断标志 |
中断含义 |
I2C_IT_TIMEOUT |
超时中断 |
I2C_IT_AF |
应答失败/停止数据传输中断 |
I2C_IT_TXE |
发送数据为空中断 |
I2C_IT_RXNE |
接受数据为空中断 |
I2C_IT_STOPF |
检测到停止信号中断 |
I2C_IT_BTF |
一个字节发送完毕中断 |
I2C_IT_ADDR |
地址发送成功(从模式)/地址接收成功且匹配(主模式)中断 |
I2C_IT_SB |
起始位传输成功中断 |
I2C_IT_BERR |
总线错误中断 |
I2C_IT_OVR |
过载错误(欠过载/过载) |
注意:
① 如果用I2C_ClearITPendingBit函数清除不了对应的中断标志位,那就看看该中断涉及哪些状态标志位,然后了解这些状态标志位的清楚方式,例如:ADDR位的清除方式是“读取SR1然后读取SR2”;
② BERR总线错误标志位置位和清除方式:
⑴ 置位:当接口检测到错误的起始或停止条件,硬件将该位置’1’;
⑵ 置零:该位由软件写’0’清除,或在PE=0时由硬件清除;
③ OVR过载错误触发条件:
占空比对于I2C的重要性
通常来讲,Clock 都是以占空比为 50%来输出的。前面提到过,I2C 通信过程中,SCL 为低电平时,SDA 的数据信号会进行改变;SCL 为高电平时,SDA 上保持数据信号。因此,若高电平持续时间过短,可能导致数据来不及读取,信号状态就发生了改变,因而数据传输不准确;若高电平持续时间过长,虽然保证了数据的有效性,但通信效率变低,所以合理的占空比对 I2C 通信是很重要的。
三种常见的IIC总线错误
BERR总线错误
在一个地址或数据字节传输期间,当I2C接口检测到一个外部的停止或起始条件则产生总线错误,即当传输数据的过程中,还未完成一次传输时,这时候来了一个起始信号或者停止信号。此时BERR错误产生,BERR错误产生后:
● BERR位被置位为’1’;如果设置了ITERREN位,则产生一个中断;
● 在从模式情况下,数据被丢弃,硬件释放总线:
─ 如果是错误的开始条件,从设备认为是一个重启动,并等待地址或停止条件。
─ 如果是错误的停止条件,从设备按正常的停止条件操作,同时硬件释放总线。
● 在主模式情况下,硬件不释放总线,同时不影响当前的传输状态。此时由软件决定是否要 中止当前的传输。
AF应答失败
当接口检测到一个无应答位(NACK)时,产生应答错误(AF)。此时:
● AF位被置位,如果设置了ITERREN位,则产生一个中断;
● 当发送器接收到一个NACK时,必须复位通讯:
─ 如果是处于从模式,硬件释放总线。
─ 如果是处于主模式,软件必须生成一个停止条件。
OVR过载错误
① 在从模式下,如果禁止时钟延长,I 2 C接口正在接收数据时,当它已经接收到一个字节 (RxNE=1),但在DR寄存器中前一个字节数据还没有被读出,则发生过载错误。此时:
● 最后接收的数据被丢弃;
● 在过载错误时,软件应清除RxNE位,发送器应该重新发送最后一次发送的字节。
② 在从模式下,如果禁止时钟延长,I2C接口正在发送数据时,在下一个字节的时钟到达之前,新 的数据还未写入DR寄存器(TxE=1),则发生欠载错误。此时:
● 在DR寄存器中的前一个字节将被重复发出;
● 用户应该确定在发生欠载错时,接收端应丢弃重复接收到的数据。
发送端应按I2C总线标准 在规定的时间更新DR寄存器。 在发送第一个字节时,必须在清除ADDR之后并且第一个SCL上升沿之前写入DR寄存器;如果 不能做到这点,则接收方应该丢弃第一个数据。
状态标志位+中断允许标志位=中断
E2PROM简介
E2PROM器件地址
我们知道,挂接在 I2C 总线上的器件是依赖其地址来进行定位的,器件地址由类型码 和地址码两部分所构成。对于 7 位地址模式的器件而言:
类型码(bit7~bit4)表明器件的类别,如 I2C EEPROM 为 1010,I2C 温度传感器类型 编码为 1001,这一部分由器件厂商生产时写入。
地址码(bit3~bit1):由用户进行电路设计时确定,如图 8-13 的连接关系,A2A1A0 (bit3~bit1)=000。
最低位 bit0 表示读写方向,bit0=1 时表示读,bit0=0 时表示写,因此不属于地址位。
因此,器件地址是0x.
E2PROM与I2C总线连接图
连接引脚以及引脚属性配置
EEPROM与IIC通信的代码讲解
EEPROM相关GPIO初始化
// 端口引脚定义 #define IIC_SCL_Pin GPIO_Pin_6 #define IIC_SDA_Pin GPIO_Pin_7 #define IIC_Port GPIOB // PB6,PB7引脚IIC初始化 void EEPROM_GPIO_InitConfig() { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); // GPIOB时钟使能 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Pin = IIC_SCL_Pin | IIC_SDA_Pin; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(IIC_Port, &GPIO_InitStructure); }
EEPROM的IIC属性配置
// EEPROM器件地址设置 #define EEPROM_ADDRESS 0b // 端口引脚定义 #define IICx I2C1 // IIC速度配置 #define IIC_SPEED // IIC EEPROM初始化 void EEPROM_IIC_InitConfig() { I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE); // IIC时钟使能 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = EEPROM_ADDRESS; I2C_InitStructure.I2C_ClockSpeed = IIC_SPEED; // 配置速度为Hz的快速模式 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_16_9; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_OwnAddress1 = I2C_AcknowledgedAddress_7bit; // 设置I2C的应答地址为7Bits I2C_Init(IICx, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); // 使能I2C1 }
EEPROM的读操作
清除ADDR位的两种方式:
① 在软件读取SR1寄存器后,对SR2寄存器的读操作将清除该位;
当调用I2C_CheckEvent固件库函数时,自动清除ADDR:
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT) { uint32_t lastevent = 0; uint32_t flag1 = 0, flag2 = 0; ErrorStatus status = ERROR; /* Check the parameters */ assert_param(IS_I2C_ALL_PERIPH(I2Cx)); assert_param(IS_I2C_EVENT(I2C_EVENT)); /* Read the I2Cx status register */ flag1 = I2Cx->SR1; flag2 = I2Cx->SR2; flag2 = flag2 << 16; /* Get the last event value from I2C status register */ lastevent = (flag1 | flag2) & FLAG_Mask; /* Check whether the last event contains the I2C_EVENT */ if ((lastevent & I2C_EVENT) == I2C_EVENT) { /* SUCCESS: last event is equal to I2C_EVENT */ status = SUCCESS; } else { /* ERROR: last event is different from I2C_EVENT */ status = ERROR; } /* Return status */ return status; }
注意:我们说过清除ADDR地址发送成功标志位时,应该“先读取SR1在读取SR2”,然后ADDR标志位自动清除。这里,正好进行了以上操作:
flag1 = I2Cx->SR1; flag2 = I2Cx->SR2;
② 当PE=0时,由硬件清除该位:
I2C_Cmd(I2C1, ENABLE); // 再次使能I2C1可以清除ADDR
第一步:
先将I2C配置为主发送模式:发送器件地址信息,等待器件响应,响应完成后,就标志着“发送端与接收端连接成功”;
第二步:
将I2C配置为主接收模式,此时器件发送其地址,I2C接收地址并识别匹配,然后发送应答信号,之后E2PROM就向I2C发送数据。
// 从readAddr开始读取bytesCounter字节的数据存到pBuf中 void EEPROM_ReadBuffer(uint8_t* pBuf, uint8_t readAddr, uint16_t bytesCounter) { while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY) == SET); // 轮询等待总线空闲 I2C_GenerateSTART(I2C1,ENABLE); // 发送起始信号 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待EV5事件发生并且清除事件标志位SB(通过读取SR1寄存器) I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); // 等待EV6事件-等待发送地址完成 I2C_Cmd(I2C1, DISABLE); // 再次使能I2C1可以清除ADDR(触发EV6事件的标志位) - 先失能再使能 I2C_Cmd(I2C1, ENABLE); I2C_SendData(I2C1, readAddr); // 发送E2PROM器件内部的地址 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED) && !I2C_GetFlagStatus(I2C1, I2C_FLAG_BTF)); // EV8_2事件与BTF字节传输完毕标志位 I2C_GenerateSTART(I2C1, ENABLE); // 再次发送起始信号 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待EV5事件发生并且清除事件后,清除标志位ADDR I2C_Send7bitAddress(I2C1,readAddr,I2C_Direction_Receiver); // 将I2C配置为主接收模式,设置写入内存区域的首地址 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 等待EV6事件发生 while(bytesCounter) { if(bytesCounter == 1) // 最后一次传输后关闭应答并且发送停止位 { I2C_AcknowledgeConfig(I2C1, DISABLE); I2C_GenerateSTOP(I2C1, ENABLE); } if(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) // 接收完一个字节数据 { *pBuf = I2C_ReceiveData(I2C1); pBuf++; bytesCounter--; } } I2C_AcknowledgeConfig(I2C1, ENABLE); // 开启应答模式为下一次读操作做准备 }
这里我们要特别注意的是:
① 在最后一个字节发送过程中,我们要发送停止位并且失能应答功能,其实,发送停止位并不是立刻发送,而是当这一个字节传输完成后,再发送停止位;
② 我们这里采取的清除EV5事件标志位的方式为:
失能I2C,再使能I2C,因为STM32中文参考手册中提及“当PE = 0即I2C失能后,ADDR位被自动清除”;
③ 读E2PROM中数据的时候,大体操作流程如下:
⑴ 先将I2C配置为发送模式,然后发送器件地址,等待器件响应,这样发送端接收端就建立了联系;
⑵ 按照下图所示的流程,来一步一步进行:
④ 主接受模式通信关闭方式:
主设备在从从设备接收到最后一个字节后发送一个NACK。接收到NACK后,从设备释放对SCL 和SDA线的控制;主设备就可以发送一个停止/重起始条件。
● 为了在收到最后一个字节后产生一个NACK脉冲,在读倒数第二个数据字节之后(在倒数第 二个RxNE事件之后)必须清除ACK位;
● 为了产生一个停止/重起始条件,软件必须在读倒数第二个数据字节之后(在倒数第二个 RxNE事件之后)设置STOP/START位;
● 只接收一个字节时,刚好在EV6之后(EV6_1时,清除ADDR之后)要关闭应答和停止条件的 产生位。 在产生了停止条件后,I 2 C接口自动回到从模式(M/SL位被清除);
A. 发送起始信号,配置为主发送模式,发送器件地址等待对应器件应答;
B. 再次发送起始信号,配置为主接受模式,接受器件地址,地址匹配成功后,接收器件发送的数据;
C. 最后一个字节传输时,发送停止信号并且失能应答功能。
EEPROM的写操作
扇区写操作(页写操作):
① 主设备发送起始信号,然后等待EV5事件发生;
② 发送E2PROM器件的地址,等待E2PROM应答成功,然后等待EV6事件的出现;
③ 清除EV6事件的标志位(ADDR位),发送一个E2PROM中要写入区域的首地址,等待EV8_2事件(字节传输完毕事件)发生;
④ 不断向E2PROM发送数据进行写入操作;
⑤ 按照主发送模式下的关闭通信操作步骤进行操作(再发送完所有字节后,发送停止信号);
// 将pBuffer中的NumByteToWrite字节写入E2PROM中首地址为WriteAddr的区域当中 void EEPROM_WirtePageBuffer(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite) { while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // 轮询等待释放总线 I2C_GenerateSTART(I2C1, ENABLE); // 发送起始信号 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 等待EV6事件-等待发送地址完成 I2C_Cmd(I2C1, DISABLE); // 清除ADDR地址发送成功标志位 - PE = 0 -> PE = 1 - 清除ADDR标志位 I2C_Cmd(I2C1, ENABLE); I2C_SendData(I2C1, WriteAddr); // 告知E2PROM,我们要写区域的首地址 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 数据发送完毕 while(NumByteToWrite) { I2C_SendData(I2C1, *pBuffer); pBuffer++; while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待一个字节发送完毕 } I2C_GenerateSTOP(I2C1, ENABLE); // 传输完毕后发送停止信号 }
注意:
① 主发送模式通信关闭方式:
在DR寄存器中写入最后一个字节后,通过设置STOP位产生一个停止条件(见图245的EV8_2), 然后I2C接口将自动回到从模式(M/S位清除);
② 这里,为什么函数名叫做“EEPROM_WirtePageBuffer-按页将数据写入E2PROM中”?
在E2PROM中,页的概念其实就是“若干个字节组成的一片内存区”,当我们按照一个一个字节写入E2PROM时,时序图如下:
但是,当我们按照一块一块区域进行写的时候,时序逻辑图如下:
这样写入字节的速度更加的快捷,更加的方便。
写操作(基于扇区写操作):
有些人发出了这样的疑问:为什么我们不一下子全部写入呢?为什么还要以扇区为单位进行写操作呢?这不是降低写入速度吗?
其实扇区写入有他的好处也有它的坏处,相较于一下子全部将数据全部写入,以扇区为单位进行写入确实可以降低数据写入速度,但是如果我们传输过程中出现错误,不至于会扰乱整个EEPROM。
以扇区为单位进行写操作的步骤:
// 将pBuffer中的NumByteToWrite个字节的数据写入器件地址为WriteAddr void EEPROM_WirteBuffer(uint8_t* pBuffer, uint16_t WriteAddr, uint16_t NumByteToWrite) { uint8_t NumOfPage = 0, NumOfSingle = 0, count = 0, Addr = 0; Addr = WriteAddr % EE_PAGESIZE; // 查看初始内存地址是否是扇区的倍数 - Addr = 0 -> 是 / Addr != 0 -> 不是 count = EE_PAGESIZE - Addr; // 如果初始内存地址不是扇区的整数倍,未对齐的字节(未对齐的剩余空间) NumOfPage = NumByteToWrite / EE_PAGESIZE; // 一共有多少页要写,即一共有多少个扇区要写 NumOfSingle = NumByteToWrite % EE_PAGESIZE; // 需要写的不足一页的字节数 if(Addr == 0) // 初始地址是页的整数倍 { if(NumOfPage == 0) // 写入字节的数量不足一页 { EEDataNum = EE_PAGESIZE; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); } else // 写入字节的数量超过一页 { while(NumOfPage--) // 先整页的写数据 { EEDataNum = EE_PAGESIZE; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); WriteAddr += EE_PAGESIZE; // 更新写入区域首地址 pBuffer += EE_PAGESIZE; // 更新内存中数组的地址 } if(NumOfSingle != 0) // 然后再写不足一页的数据 { EEDataNum = NumOfSingle; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); } } } else // 初始地址不是页的整数倍 { if(NumOfPage == 0) // 写入数据不足一页 { if(NumByteToWrite > count) // 写入数据超出一页中剩余字节数量 { EEDataNum = count; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); EEDataNum = NumByteToWrite - count; EEPROM_WirtePageBuffer(pBuffer + count, WriteAddr + count, EEDataNum); WaitE2PROMStandbyState(); } else // 写入数据未超出一页中剩余字节数量 { EEDataNum = NumByteToWrite; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); } } else // 初始地址不是页的倍数 { NumByteToWrite -= count; NumOfPage = NumByteToWrite / EE_PAGESIZE; NumOfSingle = NumByteToWrite % EE_PAGESIZE; if(count != 0) // 先写满一页 { EEDataNum = count; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); pBuffer += count; WriteAddr += count; } while(NumOfPage--) // 然后在整页的写数据 { EEDataNum = EE_PAGESIZE; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); WriteAddr += EE_PAGESIZE; // 更新写入区域首地址 pBuffer += EE_PAGESIZE; // 更新内存中数组的地址 } if(NumOfSingle != 0) // 最后再写不足一页的数据 { EEDataNum = NumOfSingle; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); } } } } // 当以页为单位进行写操作时,每写完一页需要重新启动下次的写操作 uint32_t WaitE2PROMStandbyState() { uint16_t tmpSR1 = 0; while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); while(!tmpSR1) { I2C_GenerateSTART(I2C1, ENABLE); I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter); tmpSR1 = I2C1->SR1; tmpSR1 &= I2C_SR1_ADDR; } (void)I2C1->SR2; // 先读取SR1,在读取SR2寄存器可以清除ADDR位 I2C_GenerateSTOP(I2C1, ENABLE); return EE_OK; }
这里,重要的几个变量解释:
Addr = WriteAddr % EE_PAGESIZE; // 查看初始内存地址是否是扇区的倍数 – Addr = 0 -> 是 / Addr != 0 -> 不是
count = EE_PAGESIZE – Addr; // 如果初始内存地址不是扇区的整数倍,未对齐的字节(未对齐的剩余空间)
NumOfPage = NumByteToWrite / EE_PAGESIZE; // 一共有多少页要写,即一共有多少个扇区要写
NumOfSingle = NumByteToWrite % EE_PAGESIZE; // 需要写的不足一页的字节数
Count变量(内存余量):
我们写数据时会遇到以下几种情况:
① 写入区域的首地址是扇区的整数倍
⑴ 写入字节数量小于一个扇区
操作:我们可以一次性全部写入
⑵ 写入字节数量大于一个扇区
操作:
A. 先将数据按照扇区写入,不断更新E2PROM中的内存地址;
B. 将不足一个扇区的剩余数据一次性写入;
② 写入区域的首地址不是扇区的整数倍(此时,这个扇区会有内存余量)
⑴ 写入字节数量小于内存余量
操作:可以一次性将全部字节写入;
⑵ 写入字节大于内存余量
操作:
A. 先将内存余量填满,并更新E2PROM的内存地址;
B. 剩下字节的数据按照“首地址为整扇区”的操作方式进行写入;
注:
① 当我们以扇区为单位进行写操作时,需要每写一个扇区就要重新停止写操作,并且为下一次进行写操作做准备,对应的函数如下:
// 当以页为单位进行写操作时,每写完一页需要重新启动下次的写操作 uint32_t WaitE2PROMStandbyState() { uint16_t tmpSR1 = 0; while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); while(!tmpSR1) { I2C_GenerateSTART(I2C1, ENABLE); I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter); tmpSR1 = I2C1->SR1; tmpSR1 &= I2C_SR1_ADDR; } (void)I2C1->SR2; // 先读取SR1,在读取SR2寄存器可以清除ADDR位 I2C_GenerateSTOP(I2C1, ENABLE); return EE_OK; }
② 在上面这个函数中,while循环部分不断轮询等待着“接受端接收地址并且应答发送端,即如果地址发送成功那么ADDR=1”,最重要的是“清除ADDR标志位”的操作,一共有以下两种方式:
⑴ 在软件读取SR1寄存器后,对SR2寄存器的读操作将清除该位;
⑵ 当PE=0时,由硬件清除该位;
这里,我们采用的是第一种方法(先读取SR1,然后读取SR2 -> ADDR被置零)。
I2C每一步的进行为何与事件的判断密切相关?
事件存在的意义
在I2C中事件发生就是告诉我们这一阶段的工作已经完成,我们准备下一阶段工作了,但是千万不要忘记“事件发生后,一定要进行收尾工作,即清除该事件的标志位,以供后续操作不受影响”。
主发送模式
主发送模式下,I2C工作流程如下:
工作步骤 |
这一阶段完成标志/可以进行下一阶段的标志 |
事件含义 |
主设备发送起始信号 |
EV5事件发生 |
发送起始信号成功 |
地址和传输方向发送 |
EV6事件发生 |
地址匹配并且数据传输方向设置成功 |
数据传输 |
EV8_1事件发生 |
一个字节的数据发送完成 |
数据不断发送…… |
||
数据发送完成 |
EV8_2事件发生 |
最后一个字节发送完毕 |
发送停止位 |
无 |
无 |
注:
① STM32官方固件库中,对于EV8_2的解释是:
⑴ Using EV8_2 leads to a slower communication but ensure more reliable test.
⑵ EV8_2 is also more suitable than EV8 for testing on the last data transmission (before Stop condition generation).
代码示例
Eeprom.c
#include "eeprom.h" #include "stm32f10x.h" #include "stm32f10x_i2c.h" #include "sys.h" u16 EEDataNum = 0; // PB6,PB7引脚IIC初始化 void EEPROM_GPIO_InitConfig() { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); // GPIOB时钟使能 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Pin = IIC_SCL_Pin | IIC_SDA_Pin; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(IIC_Port, &GPIO_InitStructure); } // IIC EEPROM初始化 void EEPROM_IIC_InitConfig() { I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE); // IIC时钟使能 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = EEPROM_ADDRESS; I2C_InitStructure.I2C_ClockSpeed = IIC_SPEED; // 配置速度为Hz的快速模式 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_16_9; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_OwnAddress1 = I2C_AcknowledgedAddress_7bit; // 设置I2C的应答地址为7Bits I2C_Init(IICx, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); // 使能I2C1 } // 从readAddr开始读取bytesCounter字节的数据存到pBuf中 void EEPROM_ReadBuffer(uint8_t* pBuf, uint8_t readAddr, uint16_t bytesCounter) { while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY) == SET); // 轮询等待总线空闲 I2C_GenerateSTART(I2C1,ENABLE); // 发送起始信号 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待EV5事件发生并且清除事件标志位SB(通过读取SR1寄存器) I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); // 等待EV6事件-等待发送地址完成 I2C_Cmd(I2C1, DISABLE); // 再次使能I2C1可以清除ADDR(触发EV6事件的标志位) - 先失能再使能 I2C_Cmd(I2C1, ENABLE); I2C_SendData(I2C1, readAddr); // 发送E2PROM器件内部的地址 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED) && !I2C_GetFlagStatus(I2C1, I2C_FLAG_BTF)); // EV8_2事件与BTF字节传输完毕标志位 I2C_GenerateSTART(I2C1, ENABLE); // 再次发送起始信号 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待EV5事件发生并且清除事件后,清除标志位ADDR I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver); // 将I2C配置为主接收模式,识别匹配E2PROM器件的地址 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 等待EV6事件发生 while(bytesCounter) { if(bytesCounter == 1) // 最后一次传输后关闭应答并且发送停止位 { I2C_AcknowledgeConfig(I2C1, DISABLE); I2C_GenerateSTOP(I2C1, ENABLE); } if(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) // 接收完一个字节数据 { *pBuf = I2C_ReceiveData(I2C1); pBuf++; bytesCounter--; } } I2C_AcknowledgeConfig(I2C1, ENABLE); // 开启应答模式为下一次读操作做准备 } // 将pBuffer中的NumByteToWrite字节写入E2PROM中首地址为WriteAddr的区域当中 void EEPROM_WirtePageBuffer(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite) { while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // 轮询等待释放总线 I2C_GenerateSTART(I2C1, ENABLE); // 发送起始信号 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 等待EV6事件-等待发送地址完成 I2C_Cmd(I2C1, DISABLE); // 清除ADDR地址发送成功标志位 - PE = 0 -> PE = 1 - 清除ADDR标志位 I2C_Cmd(I2C1, ENABLE); I2C_SendData(I2C1, WriteAddr); // 告知E2PROM,我们要写区域的首地址 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 数据发送完毕 while(NumByteToWrite) { I2C_SendData(I2C1, *pBuffer); pBuffer++; while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待一个字节发送完毕 } I2C_GenerateSTOP(I2C1, ENABLE); // 传输完毕后发送停止信号 } // 将pBuffer中的NumByteToWrite个字节的数据写入器件地址为WriteAddr void EEPROM_WirteBuffer(uint8_t* pBuffer, uint16_t WriteAddr, uint16_t NumByteToWrite) { uint8_t NumOfPage = 0, NumOfSingle = 0, count = 0, Addr = 0; Addr = WriteAddr % EE_PAGESIZE; // 查看初始内存地址是否是扇区的倍数 - Addr = 0 -> 是 / Addr != 0 -> 不是 count = EE_PAGESIZE - Addr; // 如果初始内存地址不是扇区的整数倍,未对齐的字节(未对齐的剩余空间) NumOfPage = NumByteToWrite / EE_PAGESIZE; // 一共有多少页要写,即一共有多少个扇区要写 NumOfSingle = NumByteToWrite % EE_PAGESIZE; // 需要写的不足一页的字节数 if(Addr == 0) // 初始地址是页的整数倍 { if(NumOfPage == 0) // 写入字节的数量不足一页 { EEDataNum = EE_PAGESIZE; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); } else // 写入字节的数量超过一页 { while(NumOfPage--) // 先整页的写数据 { EEDataNum = EE_PAGESIZE; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); WriteAddr += EE_PAGESIZE; // 更新写入区域首地址 pBuffer += EE_PAGESIZE; // 更新内存中数组的地址 } if(NumOfSingle != 0) // 然后再写不足一页的数据 { EEDataNum = NumOfSingle; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); } } } else // 初始地址不是页的整数倍 { if(NumOfPage == 0) // 写入数据不足一页 { if(NumByteToWrite > count) // 写入数据超出一页中剩余字节数量 { EEDataNum = count; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); EEDataNum = NumByteToWrite - count; EEPROM_WirtePageBuffer(pBuffer + count, WriteAddr + count, EEDataNum); WaitE2PROMStandbyState(); } else // 写入数据未超出一页中剩余字节数量 { EEDataNum = NumByteToWrite; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); } } else // 初始地址不是页的倍数 { NumByteToWrite -= count; NumOfPage = NumByteToWrite / EE_PAGESIZE; NumOfSingle = NumByteToWrite % EE_PAGESIZE; if(count != 0) // 先写满一页 { EEDataNum = count; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); pBuffer += count; WriteAddr += count; } while(NumOfPage--) // 然后在整页的写数据 { EEDataNum = EE_PAGESIZE; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); WriteAddr += EE_PAGESIZE; // 更新写入区域首地址 pBuffer += EE_PAGESIZE; // 更新内存中数组的地址 } if(NumOfSingle != 0) // 最后再写不足一页的数据 { EEDataNum = NumOfSingle; EEPROM_WirtePageBuffer(pBuffer, WriteAddr, EEDataNum); WaitE2PROMStandbyState(); } } } } // 当以页为单位进行写操作时,每写完一页需要重新启动下次的写操作 uint32_t WaitE2PROMStandbyState() { uint16_t tmpSR1 = 0; while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); while(!tmpSR1) { I2C_GenerateSTART(I2C1, ENABLE); I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter); tmpSR1 = I2C1->SR1; tmpSR1 &= I2C_SR1_ADDR; } (void)I2C1->SR2; // 先读取SR1,在读取SR2寄存器可以清除ADDR位 I2C_GenerateSTOP(I2C1, ENABLE); return EE_OK; } // EEPROM初始化 void EEPROM_InitConfig() { EEPROM_GPIO_InitConfig(); // EEPROM的GPIO口初始化 EEPROM_IIC_InitConfig(); // EEPROM的IIC属性初始化 }
Eeprom.h
#ifndef _EEPROM_H #define _EEPROM_H #include "sys.h" void EEPROM_InitConfig(); // EEPROM器件初始化 void EEPROM_GPIO_InitConfig(); // EEPROM的GPIO口初始化 void EEPROM_IIC_InitConfig(); // EEPROM的IIC属性初始化 void EEPROM_ReadBuffer(uint8_t* pBuf, uint8_t readAddr, uint16_t bytesCounter); // 从readAddr开始读取bytesCounter字节的数据存到pBuf中 void EEPROM_WirtePageBuffer(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite); // 将pBuffer中的NumByteToWrite个字节的数据写入器件地址为WriteAddr void EEPROM_WirteBuffer(uint8_t* pBuffer, uint16_t WriteAddr, uint16_t NumByteToWrite); // 将pBuffer中的NumByteToWrite个字节的数据写入器件地址为WriteAddr uint32_t WaitE2PROMStandbyState(void); // 开启下一次扇区写操作(以扇区为单位进行读写操作) // 端口引脚定义 #define IIC_SCL_Pin GPIO_Pin_6 #define IIC_SDA_Pin GPIO_Pin_7 #define IIC_Port GPIOB #define IICx I2C1 // EEPROM器件地址设置 #define EEPROM_ADDRESS 0b // IIC速度配置 #define IIC_SPEED // 扇区最大写字节数 #define EE_PAGESIZE 8 // 状态 #define EE_OK 1 #define EE_NO 0 #endif
Main.c
#include "stm32f10x.h" #include "eeprom.h" #include "usart.h" #include "delay.h" int main() { u8 Buffer[] = {1,2,3,4,5,6,7,8,9,10}; u8 Buffer_Read[10] = {0}; u8 i = 0; delay_init(); uart_init(); EEPROM_InitConfig(); while(1) { EEPROM_WirteBuffer(Buffer,0,10); EEPROM_ReadBuffer(Buffer_Read,0,10); for(i = 0; i<10; i++) { printf("%d",*Buffer_Read); } } }
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/152806.html