EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。
1AT24C02原理图
![](https://img-blog.csdnimg.cn/img_convert/fb8d6c63beb16e5a930010006860ef0c.png)
A0,A1,A2:硬件地址引脚
WP:写保护引脚,接高电平只读,接地允许读和写
SCL和SDA:IIC总线
24C02后面的 02 表示的是可存储 2Kbit 的数据,转换为字节的存储量为2*1024/8 = 256byte;256个字节一共分为32页,每页8个字节。
2设备地址
在AT24C02的参考手册第9页 可以看到下图
![](https://img-blog.csdnimg.cn/img_convert/41c558072e1b4dea104732c874a40048.png)
AT24C设备地址为如下:
前四位固定 1010
A2、A1、A0为由管脚电平决定。此处原理图都接地,默认为000。
最后一位表示读操作还是写操作。读地址为0xA1,写地址为0xA0。
结论:
写24C02的时候,从器件地址为10100000(0xA0)
读24C02的时候,从器件地址为10100001(0xA1)
片内地址寻址:
芯片寻址可对 内部256个字节 中的任一个进行 读/写操作,其寻址范围为00~FF,共256个寻址单位。
3 读写时序
写一个字节
![](https://img-blog.csdnimg.cn/img_convert/462412e34c1b6008af0562b13a6c1dfc.png)
从时序图上可以看出(上面是MCU的信号,下面是存储芯片的信号),写一个字节数据的操作顺序为:
1.MCU 先给芯片发送一个开始信号(START)
2.开始信后之后的第一个字节,发送要写入的设备地址(DEVICE ADDRESS)(注意,因为总线上可能由多个设备,是根据设备地址选择不同的设备的),然后发送写数据命令(0xA0),然后等待应答信号(ACK)
3.发送数据的存储地址。一共有256个字节的存储空间,地址从0x00~0xFF,想存到哪个地址,就发哪个地址
4.发送要存储的数据,发送完成之后MCU会收到应答信号
5.数据发送完成之后,发送结束信号(STOP)停止总线。
读一个字节
![](https://img-blog.csdnimg.cn/img_convert/df455a7d5d00f8f4767a7ae38e2cf785.png)
从时序图上可以看出(上面是MCU的信号,下面是存储芯片的信号),读一个字节数据的操作顺序为:
1.MCU 先给芯片发送一个开始信号(START)
2.开始信号之后的第一个字节,发送要读取的设备地址(DEVICE ADDRESS)(注意,因为总线上可能由多个设备,是根据设备地址选择不同的设备的),然后发送写数据命令(0xA0),并发送要读取的数据地址(WORD ADDRESS),然后等待应答信号(ACK)
3.再次发送开始信号(START)
4.开始信号之后的第一个字节,发送要读取的设备地址(DEVICE ADDRESS),发送读取数据命令(0xA1)
5.此时,24C02会自动给MCU发送数据。
6.MCU发送结束信号(STOP)停止总线。
写一页数据
![](https://img-blog.csdnimg.cn/img_convert/4b461047bbaadd3386267f5cb8b95542.png)
时序图和写单个字节差不多,只是每个字节写完之后存储器都会给MCU发送应答信号,之后继续发送下一个字节,写完之后,MCU发送停止信号即可。
256个字节一共分为32页,每页8个字节。AT24C02页写入只支持8个byte,所以需要分32次写入。如果按照上述时序连续写入8个字节后,会重复的继续往该页写数据。(当然也可以 往256个地址中分别写入一个字节。。。)
连续读数据
![](https://img-blog.csdnimg.cn/img_convert/8f6929e52f8f20f24d35086fd614a1d2.png)
时序图和都单个字节差不多,存储器给MCU发送完每个字节,MCU要发送应答信号给存储器,直到MCU发送停止信号。且读数据没有8个字节的限制。
4.代码部分
虽然时序看起来很复杂,但是不用担心,很多都已经有实现了。
在生成的工程中,打开stm32f4xx_hal.h,可以看到已经生成了轮询,中断和DMA三种控制方式的代码。
![](https://img-blog.csdnimg.cn/img_convert/f4c881be0222e019b8c5500b7005bed7.png)
我们只看轮询的,其他的也都差不多,只是应用场景不一样。
// 作为主机 发送数据
// 参数:iic接口、设备地址、发送的数据、数据长度、超时时间
HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 作为主机 接收数据
// 参数:iic接口、设备地址、存储读取到的数据、数据长度、超时时间
HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 作为从机 发送数据
// 参数:iic接口、设备地址、发送的数据、数据长度、超时时间
HAL_I2C_Slave_Transmit(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 作为从机 接收数据
// 参数:iic接口、设备地址、存储读取到的数据、数据长度、超时时间
HAL_I2C_Slave_Receive(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
/* 第1个参数为I2C操作句柄 hi2c: I2C设备号指针,设置使用的是那个IIC 例:&hi2c1
第2个参数为从机设备地址 DevAddress: 从设备地址 从设备的IIC地址 例E2PROM的设备地址 0xA0
第3个参数为从机寄存器地址 MemAddress: 从机寄存器地址 ,每写入一个字节数据,地址就会自动+1
第4个参数为从机寄存器地址长度 写入数据的字节类型 8位还是16位 I2C_MEMADD_SIZE_8BIT I2C_MEMADD_SIZE_16BIT
第5个参数为发送的数据的起始地址
第6个参数为传输数据的大小
第7个参数为操作超时时间 */
// 直接发送两个字节数据,并接受数据(就用于我们现在的情况,发送命令 + 接收数据)
// 参数:iic接口、设备地址、发送的数据1、发送的数据2、存储读取到的数据、数据长度、超时时间
HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 查询设备是否就绪
HAL_I2C_IsDeviceReady(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout);
//例子 while (HAL_I2C_IsDeviceReady(&I2C_Handle, EEPROM_ADDRESS,EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);
我们可以直接看HAL_I2C_Mem_Write、HAL_I2C_Mem_Read,刚好可以满足我们这里需要发送 指令 + 地址 的情况,其在发送或者读取数据的过程中,地址还可以自己增加,很方便。
在i2c.h中声明如下代码
/* USER CODE BEGIN Prototypes */
#define AT24C02_ADDR_WRITE 0xA0 // 写命令
#define AT24C02_ADDR_READ 0xA1 // 读命令
uint8_t At24c02_Write_Byte(uint16_t addr, uint8_t* dat);
uint8_t At24c02_Read_Byte(uint16_t addr, uint8_t* read_buf);
uint8_t At24c02_Write_Amount_Byte(uint16_t addr, uint8_t* dat, uint16_t size);
uint8_t At24c02_Read_Amount_Byte(uint16_t addr, uint8_t* recv_buf, uint16_t size);
/* USER CODE END Prototypes */
在i2c.c中添加如下代码
/* USER CODE BEGIN 1 */
#include <string.h>
/**
* @brief AT24C02任意地址写一个字节数据
* @param addr —— 写数据的地址(0-255)
* @param dat —— 存放写入数据的地址
* @retval 成功 —— HAL_OK
*/
uint8_t At24c02_Write_Byte(uint16_t addr, uint8_t* dat)
{
HAL_StatusTypeDef result;
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, 1, 0xFFFF);
HAL_Delay(5); // 写一个字节,延迟一段时间,不能连续写
return result;
}
/**
* @brief AT24C02任意地址读一个字节数据
* @param addr —— 读数据的地址(0-255)
* @param read_buf —— 存放读取数据的地址
* @retval 成功 —— HAL_OK
*/
uint8_t At24c02_Read_Byte(uint16_t addr, uint8_t* read_buf)
{
return HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_READ, addr, I2C_MEMADD_SIZE_8BIT, read_buf, 1, 0xFFFF);
}
/**
* @brief AT24C02任意地址连续写多个字节数据
* @param addr —— 写数据的地址(0-255)
* @param dat —— 存放写入数据的地址
* @retval 成功 —— HAL_OK
*/
uint8_t At24c02_Write_Amount_Byte(uint16_t addr, uint8_t* dat, uint16_t size)
{
uint8_t i = 0;
uint16_t cnt = 0; // 写入字节计数
HAL_StatusTypeDef result; // 返回是否写入成功
/* 对于起始地址,有两种情况,分别判断 */
if(0 == addr % 8)
{
/* 起始地址刚好是页开始地址 */
/* 对于写入的字节数,有两种情况,分别判断 */
if(size <= 8)
{
// 写入的字节数不大于一页,直接写入
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, size, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
return result;
}
else
{
// 写入的字节数大于一页,先将整页循环写入
for(i = 0; i < size/8; i++)
{
HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], 8, 0xFFFF);
// 一次写入了八个字节,延迟久一点
HAL_Delay(20); // 写完八个字节,延迟久一点
addr += 8;
cnt += 8;
}
// 将剩余的字节写入
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], size - cnt, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
return result;
}
}
else
{
/* 起始地址偏离页开始地址 */
/* 对于写入的字节数,有两种情况,分别判断 */
if(size <= (8 - addr%8))
{
/* 在该页可以写完 */
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, size, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
return result;
}
else
{
/* 该页写不完 */
// 先将该页写完
cnt += 8 - addr%8;
HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, cnt, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
addr += cnt;
// 循环写整页数据
for(i = 0;i < (size - cnt)/8; i++)
{
HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], 8, 0xFFFF);
HAL_Delay(20); // 写完八个字节,延迟久一点
addr += 8;
cnt += 8;
}
// 将剩下的字节写入
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], size - cnt, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
return result;
}
}
}
/**
* @brief AT24C02任意地址连续读多个字节数据
* @param addr —— 读数据的地址(0-255)
* @param dat —— 存放读出数据的地址
* @retval 成功 —— HAL_OK
*/
uint8_t At24c02_Read_Amount_Byte(uint16_t addr, uint8_t* recv_buf, uint16_t size)
{
return HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_READ, addr, I2C_MEMADD_SIZE_8BIT, recv_buf, size, 0xFFFF);
}
/* USER CODE END 1 */
在main.c中添加如下代码
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
// 单个字节 读写测试
uint8_t simple_write_dat = 0xa5; // 一个字节
uint8_t simple_recv_buf = 0;
if(HAL_OK == At24c02_Write_Byte(10, &simple_write_dat)){
printf("Simple data write success \r\n");
} else {
printf("Simple data write fail \r\n");
}
HAL_Delay(50); // 写一次和读一次之间需要短暂的延时
if(HAL_OK == At24c02_Read_Byte(10, &simple_recv_buf)){
printf("Simple data read success, recv_buf = 0x%02X \r\n", simple_recv_buf);
} else {
printf("Simple data read fail \r\n");
}
printf("--------------------- \r\n");
// 单个字节读写 测试结束
// 浮点数 读写测试
union float_union{
float float_write_dat; // 浮点数占4个字节
double double_write_dat; // 双精度浮点数占8个字节
uint8_t buf[8]; // 定义 8个字节 的空间
};
union float_union send_float_data; // 用来发送
union float_union rev_float_data; // 用来接收
// 先测试第一个 浮点数
send_float_data.float_write_dat = 3.1415f;
if(HAL_OK == At24c02_Write_Amount_Byte(20, send_float_data.buf, 4)){
printf("Float data write success \r\n");
} else {
printf("Float data write fail \r\n");
}
HAL_Delay(50);
if(HAL_OK == At24c02_Read_Amount_Byte(20, rev_float_data.buf, 4)){
// 默认输出六位小数
printf("Float data read success, recv_buf = %f \r\n", rev_float_data.float_write_dat);
} else {
printf("Float data read fail \r\n");
}
// 测试第二个 双精度浮点数
send_float_data.double_write_dat = 3.1415f;
if(HAL_OK == At24c02_Write_Amount_Byte(20, send_float_data.buf, 8)){
printf("Double data write success \r\n");
} else {
printf("Double data write fail \r\n");
}
HAL_Delay(50);
if(HAL_OK == At24c02_Read_Amount_Byte(20, rev_float_data.buf, 8)){
// 最多15位小数
printf("Double data read success, recv_buf = %.15f \r\n", rev_float_data.double_write_dat);
} else {
printf("Double data read fail \r\n");
}
printf("--------------------- \r\n");
// 浮点数读写测试 测试结束
// 连续数据读写测试
uint8_t write_dat[22] = {0}; // 22个字节
uint8_t recv_buf[22] = {0};
printf("正在往数组中填充数据... \r\n");
for(int i = 0; i < 22; i++){
write_dat[i] = i;
printf("%02X ", write_dat[i]);
}
printf("\r\n 数组中数据填充完毕... \r\n");
if(HAL_OK == At24c02_Write_Amount_Byte(0, write_dat, 22)){
printf("24c02 write success \r\n");
} else {
printf("24c02 write fail \r\n");
}
HAL_Delay(50); // 写一次和读一次之间需要短暂的延时
if(HAL_OK == At24c02_Read_Amount_Byte(0, recv_buf, 22)){
printf("read success \r\n");
for(int i = 0; i < 22; i++) {
printf("0x%02X ", recv_buf[i]);
}
} else {
printf("read fail\r\n");
}
// 连续数据读写 测试结束
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
为每次发送或者接收只能按照一个字节的单位进行,因此对于 uint8_t 类型的整数没什么问题。但是对于浮点数等,占用多个字节的,就可以通过共用体(union)的方式进行。
此方法在许多应用场景中都有应用,比如串口发送浮点数,也可以用这样的方式进行。
5.效果验证
编译、烧录
用串口助手观察现象。
![](https://img-blog.csdnimg.cn/img_convert/24753a4f1166317d618dd665ea63b151.png)