文章目录
前言
-
关于此文一些名词术语不太理解的,可以去看我这篇博文
→ 《STM32F10x_模拟I2C读写EEPROM(1)》 -
读写E2函数(带备份区+校验和判断),可以去看我这篇博文
→ 《STM32F10x_模拟I2C读写EEPROM(3)(读写E2备份区 + 校验位 + 完整代码 + 应用实例)》 -
E2的中文资料可以到我博客资源里下载,没有积分下载的,可以评论Ding我o( ̄▽ ̄)ブ
一、宏定义
// I2C引脚
#define PORT_I2C_SCL GPIOx
#define PORT_I2C_SDA GPIOx
#define PIN_I2C_SCL GPIO_Pin_x
#define PIN_I2C_SDA GPIO_Pin_x
// 控制 SDA / SCL 高低电平
#define I2C_SCL_LOW (PORT_I2C_SCL->BRR = PIN_I2C_SCL)
#define I2C_SCL_HIGH (PORT_I2C_SCL->BSRR = PIN_I2C_SCL)
#define I2C_SDA_LOW (PORT_I2C_SDA->BRR = PIN_I2C_SDA)
#define I2C_SDA_HIGH (PORT_I2C_SDA->BSRR = PIN_I2C_SDA)
// 读 SDA 电平状态
#define I2C_SDA_READ (PORT_I2C_SDA->IDR & PIN_I2C_SDA)
// 应答位信息
#define I2C_ACK 0 //应答
#define I2C_NOACK 1 //非应答
/*
1、"地址长度"根据芯片型号不同略有不同
8位: AT24C01、AT24C02
16位: AT24C04、AT24C08、AT24C16、AT24C32、AT24C64、AT24C128、AT24C256、AT24C512
2、"页长度"根据芯片型号不同略有不同
8字节: AT24C01、AT24C02
16字节: AT24C04、AT24C08、AT24C16
32字节: AT24C32、AT24C64
64字节: AT24C128、AT24C256
128字节: AT24C512
*/
// 此文的E2型号 - AT24C512
#define EEPROM_WORD_ADDR_SIZE 16 //地址长度
#define EEPROM_PAGE_SIZE 128 //页长度
#define EEPROM_DEV_ADDR 0xA0 //地址(设备地址:与A2、A1、A0有关)
#define EEPROM_WR 0x00 //写
#define EEPROM_RD 0x01 //读
二、I2C延时函数
-
1. 注意
- 此函数实现的是非标准延时,请根据MCU速度 调节大小
/************************************************ 函数名称 : I2C_Delay 功 能 : I2C延时(非标准延时,请根据MCU速度 调节大小) 参 数 : 无 返 回 值 : 无 *************************************************/ static void I2C_Delay(void) { uint16_t cnt = 100; while(cnt--); }
- 此函数实现的是非标准延时,请根据MCU速度 调节大小
三、起始 / 停止信号
-
1. 时序图
-
2. 起始信号
- 时钟线 SCL 保持高电平期间 数据线 SDA 电平从高到低的跳变作为I2C总线的起始信号。
/************************************************ 函数名称 : I2C_Start 功 能 : I2C开始 参 数 : 无 返 回 值 : 无 *************************************************/ void I2C_Start(void) { I2C_SCL_HIGH; //SCL高 I2C_Delay(); I2C_SDA_HIGH; //SDA高 I2C_Delay(); I2C_SDA_LOW; //SDA低 I2C_Delay(); I2C_SCL_LOW; //SCL低 I2C_Delay(); }
- 时钟线 SCL 保持高电平期间 数据线 SDA 电平从高到低的跳变作为I2C总线的起始信号。
-
3. 停止信号
- 时钟线 SCL 保持高电平期间 数据线SDA 电平从低到高的跳变作为I2C总线的停止信号。
/************************************************ 函数名称 : I2C_Stop 功 能 : I2C停止 参 数 : 无 返 回 值 : 无 *************************************************/ void I2C_Stop(void) { I2C_SDA_LOW; //SDA低 I2C_Delay(); I2C_SCL_HIGH; //SCL高 I2C_Delay(); I2C_SDA_HIGH; //SDA高 I2C_Delay(); }
- 时钟线 SCL 保持高电平期间 数据线SDA 电平从低到高的跳变作为I2C总线的停止信号。
四、切换SDA方向
-
1. SDA配置为输入模式
/************************************************ 函数名称 : I2C_SDA_SetInput 功 能 : I2C_SDA设置为输入 参 数 : 无 返 回 值 : 无 *************************************************/ void I2C_SDA_SetInput(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = PIN_I2C_SDA; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // I2C_SDA设置为 浮空输入 GPIO_Init(PORT_I2C_SDA, &GPIO_InitStructure); }
-
2. SDA配置为输出模式
/************************************************ 函数名称 : I2C_SDA_SetOutput 功 能 : I2C_SDA设置为输出 参 数 : 无 返 回 值 : 无 *************************************************/ void I2C_SDA_SetOutput(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = PIN_I2C_SDA; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // I2C_SDA设置为 开漏输出 GPIO_Init(PORT_I2C_SDA, &GPIO_InitStructure); }
五、应答位信息
-
1. 主机(MCU)读取 应答位
- 主机(MCU)写数据的时候,从机(E2)产生应答,主机读取应答位进行检测;
/************************************************ 函数名称 : I2C_GetAck 功 能 : I2C主机读取应答(或非应答)位 参 数 : 无 返 回 值 : I2C_ACK ----- 应答 I2C_NOACK --- 非应答 *************************************************/ uint8_t I2C_GetAck(void) { uint8_t ack; I2C_SCL_LOW; //SCL低(此时从机可变化SDA电平产生应答位信息) I2C_Delay(); I2C_SDA_SetInput(); //SDA配置为输入模式(切换SDA方向) I2C_SCL_HIGH; //SCL高(SCL上升沿时,从机发应答位信息到SDA线上) I2C_Delay(); if(I2C_SDA_READ) //读取从机应答位信息 ack = I2C_NOACK; //非应答 else ack = I2C_ACK; //应答 I2C_SCL_LOW; //SCL低(防止误操作起始 / 结束信号) I2C_Delay(); I2C_SDA_SetOutput(); //SDA配置为输出模式(切换SDA方向) return ack; //返回应答位 }
- 主机(MCU)写数据的时候,从机(E2)产生应答,主机读取应答位进行检测;
-
2. 主机(MCU)产生 应答位
-
主机(MCU)读数据的时候,主机产生应答,从机(E2)读取应答位进行检测;
/************************************************ 函数名称 : I2C_PutAck 功 能 : I2C主机产生应答(或非应答)位 参 数 : I2C_ACK ----- 应答 I2C_NOACK --- 非应答 返 回 值 : 无 *************************************************/ void I2C_PutAck(uint8_t Ack) { I2C_SCL_LOW; //SCL低(此时主机可变化SDA电平产生应答位信息) I2C_Delay(); if(I2C_ACK == Ack) I2C_SDA_LOW; //主机产生 → 应答 else I2C_SDA_HIGH; //主机产生 → 非应答 I2C_Delay(); I2C_SCL_HIGH; //SCL高 (SCL上升沿时,主机发应答位信息到SDA线上) I2C_Delay(); I2C_SCL_LOW; //SCL低(防止误操作起始 / 结束信号) I2C_Delay(); }
-
六、I2C读 / 写 一个字节
-
2. I2C写一个字节
-
主机每写完一个字节后,读取从机返回的应答位:
- 应答位若为0,表示从机应答,能继续下一步操作;
- 应答位若为1,表示从机非应答,不能进行下一步操作。
/************************************************ 函数名称 : I2C_WriteByte 功 能 : I2C写一字节 参 数 : Data -------- 数据 返 回 值 : I2C_ACK ----- 应答 I2C_NOACK --- 非应答 *************************************************/ uint8_t I2C_WriteByte(uint8_t Data) { uint8_t cnt; // 发送一个字节(8位) for(cnt = 0; cnt < 8; cnt++) { I2C_SCL_LOW; //SCL低(SCL为低电平时,主机变化SDA有效,产生SDA数据) I2C_Delay(); if(Data & 0x80) I2C_SDA_HIGH; //SDA高 else I2C_SDA_LOW; //SDA低 Data <<= 1; I2C_Delay(); I2C_SCL_HIGH; //SCL高(SCL上升沿,主机发送数据出去) I2C_Delay(); } I2C_SCL_LOW; //SCL低(防止误操作起始 / 结束信号) I2C_Delay(); return I2C_GetAck(); //主机读取应答位 }
-
-
3. I2C读一个字节
- 主机每读到一个字节(8位)后,产生应答位给从机检测:
- 应答位若为0,表示主机应答,主机不继续读取数据了;
- 应答位若为1,表示主机非应答,主机可继续读取数据。
/************************************************ 函数名称 : I2C_ReadByte 功 能 : I2C读一字节 参 数 : ack --------- 产生应答(或者非应答)位 返 回 值 : data -------- 读取的一字节数据 *************************************************/ uint8_t I2C_ReadByte(uint8_t ack) { uint8_t cnt; uint8_t data; I2C_SCL_LOW; //SCL低(此时从机可变化SDA电平产生数据) I2C_Delay(); I2C_SDA_SetInput(); //SDA配置为输入模式(切换SDA方向) for(cnt = 0; cnt < 8; cnt++) { I2C_SCL_HIGH; //SCL高(SCL上升沿时,从机发数据到SDA线上) I2C_Delay(); data <<= 1; if(I2C_SDA_READ) //SDA为高(数据有效) data |= 0x01; I2C_SCL_LOW; //SCL低(防止误操作起始 / 结束信号) I2C_Delay(); } I2C_SDA_SetOutput(); //SDA配置为输出模式(切换SDA方向) I2C_PutAck(ack); //产生应答(或者非应答)位 return data; //返回数据 }
- 主机每读到一个字节(8位)后,产生应答位给从机检测:
七、E2 页写 / 连续读
-
1. E2 页写
-
一次页写操作写入的数据字节数最大值为E2的页大小;
-
不同型号的E2对应的页大小可能不同(下面简称页大小的值为 P ,举例:AT24C512 → P = 128);
-
E2 页写时序说明:
- 举例说明:MCU写n(n <= P)个字节数据进E2;
- ① 主器件发送起始信号和从器件地址信息( R/W 位置零 )给从器件,主器件检测返回应答位信息;
- ② 主器件发送起始字节地址,主器件检测返回应答位信息;
- ③ 非发送最后一个字节时,主器件每发送完一个字节后,不产生停止信号,主器件检测应答位信息。循环此步骤③ n - 1次,按顺序发送数据;
- ④ 主器件发送最后一个字节后,主器件检测应答位信息,然后主器件产生停止信号;
- ⑤ 从器件接收到停止信号后,从器件开始内部数据的擦写,在擦写过程中,从器件不再应答主器件的任何请求。
-
注意:如果进行页写操作时,n > p,地址计数器将自动翻转,先前写入的数据会被覆盖,所以要注意每次页写的字节数不能大于E2的页大小P,否则会影响数据的正确性。
/************************************************ 函数名称 : EEPROM_WritePage 功 能 : EEPROM写页 参 数 : Addr ------ 地址 pData ----- 数据 Length -----长度(<=EEPROM_PAGE_SIZE) 返 回 值 : I2C_ACK ----应答 I2C_NOACK - 非应答 *************************************************/ uint8_t EEPROM_WritePage(uint16_t Addr, uint8_t *Data, uint16_t Length) { uint8_t ack; uint8_t i; /* 1.开始信号 */ I2C_Start(); /* 2.设备地址/写 */ ack = I2C_WriteByte(EEPROM_DEV_ADDR | EEPROM_WR); if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } /* 3.数据地址 */ #if(8 == EEPROM_WORD_ADDR_SIZE) ack = I2C_WriteByte((uint8_t)(Addr&0x00FF)); //数据地址(8位) if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } #else ack = I2C_WriteByte((uint8_t)(Addr>>8)); //数据地址(16位) if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } ack = I2C_WriteByte((uint8_t)(Addr&0x00FF)); if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } #endif /* 4.写一字节数据(循环) */ for(i = 0; i < Length; i++) { ack = I2C_WriteByte(*(Data + i)); if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } } /* 5.停止信号 */ I2C_Stop(); return I2C_ACK; }
-
-
2. E2 连续读
- 从E2读取数据是一个复合的I2C时序,它实际上包含一个写过程和一个读过程。
- E2 连续读时序说明:
- 举例说明:MCU连续读E2里的n(n <= P)个字节数据;
- ① 主器件发送起始信号和从器件地址信息( R/W 位置1 )给从器件,主器件检测返回应答位信息;
- ② 主器件发送数据起始字节地址,主器件检测返回应答位信息;
- ③ 主器件再次发送起始信号和从器件地址信息( R/W 位置0 )给从器件,主器件检测返回应答位信息;
- ③ E2会向主机返回从"数据起始字节地址"开始的数据,一个字节一个字节地传输,主器件每接收完一个字节后,不产生停止信号,主器件响应“应答信号ACK”给E2。只要主器件的响应为"应答信号ACK",E2就会一直传输下去,循环此步骤③ n - 1次;
- ④ 主器件接收最后一个字节后,主器件响应“非应答信号NOACK”给E2,然后主器件产生停止信号;
- ⑤ 从器件接收到停止信号后,从器件停止传输数据,E2 连续读结束。
/************************************************ 函数名称 : EEPROM_ReadSequential 功 能 : EEPROM读一字节 参 数 : Addr ------ 地址 Data ------ 数据 Length -----长度 返 回 值 : I2C_ACK --- 应答 I2C_NOACK - 非应答 *************************************************/ uint8_t EEPROM_ReadSequential(uint16_t Addr, uint8_t *Data, uint16_t Length) { uint8_t ack; uint8_t i; /* 1.开始信号 */ I2C_Start(); /* 2.设备地址/写 */ ack = I2C_WriteByte(EEPROM_DEV_ADDR | EEPROM_WR); if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } /* 3.数据地址 */ #if (8 == EEPROM_WORD_ADDR_SIZE) ack = I2C_WriteByte((uint8_t)(Addr&0x00FF)); //数据地址(8位) if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } #else ack = I2C_WriteByte((uint8_t)(Addr>>8)); //数据地址(16位) if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } ack = I2C_WriteByte((uint8_t)(Addr&0x00FF)); if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } #endif /* 4.重新开始 */ I2C_Start(); /* 5.设备地址/读 */ ack = I2C_WriteByte(EEPROM_DEV_ADDR | EEPROM_RD); if(I2C_NOACK == ack) { I2C_Stop(); return I2C_NOACK; } /* 6.读一字节数据 */ for(i = 0; i < Length - 1; i++) { *(Data + i) = I2C_ReadByte(I2C_ACK); //只读取1字节(产生应答) } *(Data + i) = I2C_ReadByte(I2C_NOACK); //只读取1字节(产生非应答) /* 7.停止信号 */ I2C_Stop(); return I2C_ACK; }
八、小结
- 写的着急,欢迎纠正
- ☆⌒(*^-゜)v THX!!
- 码字不易,记得点小心心 ( •̀ ω •́ )✧