天空星STM32F407实战:AT24C02 EEPROM I2C驱动移植与数据掉电存储
天空星STM32F407实战AT24C02 EEPROM I2C驱动移植与数据掉电存储很多朋友在用单片机做项目时都遇到过这样的问题设备断电重启后之前设置的参数、累计的数据全都没了。比如一个温控器你设置好的目标温度一断电就恢复默认值这肯定不行。今天咱们就来解决这个问题给天空星STM32F407开发板加上一个“小本本”——AT24C02 EEPROM芯片让它能记住关键数据即使断电也不会丢失。我会手把手带你完成整个移植过程从硬件连接到软件驱动再到最后的读写验证。就算你之前没接触过I2C通信跟着做一遍也能完全掌握。1. 认识我们的“小本本”AT24C02 EEPROM在开始动手之前咱们先了解一下要用的芯片。AT24C02是一种基于I2C总线的EEPROM存储器。EEPROM的全称是“电可擦除可编程只读存储器”说人话就是你可以像写内存一样随时修改里面的数据而且断电后数据还能保存下来。AT24C02后面的“02”代表它的容量是2K bit也就是256字节2048位 ÷ 8。别看容量不大存一些关键参数、设备序列号、运行计数什么的完全够用。如果你需要更大容量这个系列还有AT24C04512字节、AT24C081KB等可选驱动代码基本通用。这块芯片有几个关键特性咱们做硬件设计时要注意工作电压范围宽1.8V到5.5V都能工作兼容3.3V和5V系统天空星开发板的3.3V供电正合适。I2C接口只需要两根线SDA数据线、SCL时钟线就能通信节省单片机引脚。时钟速度在5V供电时最高支持1000KHz1MHz在咱们的3.3V系统里用400KHz完全没问题。注意I2C总线上的设备都需要有上拉电阻。好在天空星开发板和很多AT24C02模块都内置了上拉电阻如果你是自己画的电路板记得在SDA和SCL线上各加一个4.7kΩ到10kΩ的上拉电阻到3.3V。2. 硬件连接把芯片“插”到开发板上硬件连接超级简单就四根线。我一般习惯用开发板上的PB8和PB9这两个引脚因为它们正好是STM32F407的I2C1外设的复用引脚不过咱们今天用的是“模拟I2C”就是自己写代码控制引脚电平来模拟I2C时序这样更灵活也更容易理解原理。这是接线表AT24C02模块引脚天空星STM32F407引脚作用说明VCC3.3V电源正极GNDGND电源地SDAPB8I2C数据线SCLPB9I2C时钟线提示有些模块上可能还有“WP”写保护和“A0/A1/A2”地址选择引脚。WP引脚接高电平时会禁止写入我们做实验时直接接地GND就行。A0/A1/A2用于设置芯片的I2C从机地址如果模块上这三个引脚都悬空或接地那么芯片的7位设备地址就是1010000二进制。接好线后硬件部分就搞定了。接下来就是重头戏——软件驱动。3. 驱动代码移植与详解原始资料里提供了完整的驱动代码但光把代码复制过去还不够咱们得弄懂每一行是干什么的这样以后出问题了才知道怎么调。3.1 文件准备与工程配置首先在你的工程目录下比如Drivers/BSP文件夹里新建两个文件bsp_at24c02.c和bsp_at24c02.h。然后把原始资料里的代码分别复制进去。接着在你的工程管理软件比如Keil MDK里把这两个文件添加到工程中并设置好头文件包含路径。这一步和添加其他外设驱动比如之前的DHT11完全一样。3.2 头文件bsp_at24c02.h解析头文件主要做了三件事宏定义、函数声明。咱们挨个看。#ifndef _BSP_AT24C02_H_ #define _BSP_AT24C02_H_ #include stm32f4xx.h // 1. 端口宏定义告诉驱动我们用哪两个引脚 #define RCC_AT24C02 RCC_AHB1Periph_GPIOB #define PORT_AT24C02 GPIOB #define GPIO_SDA GPIO_Pin_8 #define GPIO_SCL GPIO_Pin_9这里把SDA和SCL分别定义到了PB8和PB9。如果你想换到其他引脚比如PC10和PC11只需要修改这里的宏定义就行非常方便。// 2. SDA引脚方向控制宏 // 设置SDA为输出模式主机要发送数据时 #define SDA_OUT() { \ GPIO_InitTypeDef GPIO_InitStructure; \ GPIO_InitStructure.GPIO_Pin GPIO_SDA; \ GPIO_InitStructure.GPIO_Mode GPIO_Mode_OUT; \ GPIO_InitStructure.GPIO_OType GPIO_OType_OD; // 注意是开漏输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; \ GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; \ GPIO_Init(PORT_AT24C02, GPIO_InitStructure); \ } // 设置SDA为输入模式主机要接收数据时 #define SDA_IN() { \ GPIO_InitTypeDef GPIO_InitStructure; \ GPIO_InitStructure.GPIO_Pin GPIO_SDA; \ GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN; \ GPIO_InitStructure.GPIO_OType GPIO_OType_OD; \ GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; \ GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; \ GPIO_Init(PORT_AT24C02, GPIO_InitStructure); \ }这里有个关键点GPIO_OType_OD开漏输出。I2C协议要求总线是“线与”的任何设备都可以把总线拉低。开漏输出模式下引脚只能主动拉低电平输出0或者释放高阻态靠外部上拉电阻把总线拉高。这样多个设备挂在总线上才不会冲突。// 3. 引脚电平读写宏 #define SDA_GET() GPIO_ReadInputDataBit(PORT_AT24C02, GPIO_SDA) // 读取SDA引脚电平 #define SDA(x) GPIO_WriteBit(PORT_AT24C02, GPIO_SDA, (x?Bit_SET:Bit_RESET) ) // 设置SDA输出 #define SCL(x) GPIO_WriteBit(PORT_AT24C02, GPIO_SCL, (x?Bit_SET:Bit_RESET) ) // 设置SCL输出 // 4. 函数声明 void AT24C02_GPIO_Init(void); void AT24C02_WriteByte(unsigned char WordAddress,unsigned char Data); unsigned char AT24C02_ReadByte(unsigned char WordAddress); #endif3.3 核心I2C时序模拟与底层函数I2C通信就像两个人打电话有一套固定的“开场白”和“结束语”。驱动代码里最核心的就是模拟这套时序。咱们打开bsp_at24c02.c看看几个关键函数。首先是GPIO初始化这个很简单就是打开GPIOB的时钟把PB8和PB9配置为开漏输出模式并上拉。void AT24C02_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AT24C02, ENABLE); // 使能GPIOB时钟 GPIO_InitStructure.GPIO_Pin GPIO_SDA|GPIO_SCL; GPIO_InitStructure.GPIO_Mode GPIO_Mode_OUT; // 输出模式 GPIO_InitStructure.GPIO_OType GPIO_OType_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; // 内部上拉 GPIO_Init(PORT_AT24C02, GPIO_InitStructure); }I2C起始信号IIC_Start就像打电话先说“喂你好”。时序是SCL高电平期间SDA产生一个下降沿。void IIC_Start(void) { SDA_OUT(); // 先把SDA设置为输出模式 SDA(1); // SDA拉高 delay_us(5); SCL(1); // SCL拉高 delay_us(5); SDA(0); // 在SCL高电平时SDA从高变低这就是起始信号 delay_us(5); SCL(0); // 拉低SCL为后续传输数据做准备 delay_us(5); }I2C停止信号IIC_Stop通话结束说“再见”。时序是SCL高电平期间SDA产生一个上升沿。void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); // 先确保SDA是低 SCL(1); // SCL拉高 delay_us(5); SDA(1); // 在SCL高电平时SDA从低变高这就是停止信号 delay_us(5); }发送一个字节Send_ByteI2C规定数据在SCL低电平期间变化在SCL高电平期间保持稳定。发送时高位MSB先发。void Send_Byte(uint8_t dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟开始准备数据 for( i 0; i 8; i ) // 循环8次发8个bit { // 取出最高位0x80是1000 0000右移7位变成0或1 SDA( (dat 0x80) 7 ); delay_us(1); SCL(1); // 拉高SCL从机在此时采样SDA数据 delay_us(5); SCL(0); // 拉低SCL准备下一个bit delay_us(5); dat1; // 数据左移次高位变成最高位 } }等待应答I2C_WaitAck主机发送完8位数据后会释放SDA设置为输入然后拉高SCL。从机如果成功收到数据应该在第九个时钟周期把SDA拉低。如果SDA一直为高说明从机没应答可能出错了。unsigned char I2C_WaitAck(void) { char ack 0; unsigned char ack_flag 10; // 超时计数 SCL(0); SDA(1); // 主机先释放SDA SDA_IN(); // 把SDA设置为输入准备读取从机的应答 delay_us(5); SCL(1); // 拉高SCL产生第9个时钟脉冲 delay_us(5); // 等待SDA被从机拉低应答 while( (SDA_GET()1) ( ack_flag ) ) { ack_flag--; delay_us(5); } if( ack_flag 0 ) // 超时了从机没应答 { IIC_Stop(); // 发生错误发送停止信号 return 1; // 返回1表示错误 } else { SCL(0); SDA_OUT(); // 重新把SDA设置为输出准备后续操作 } return ack; // 返回0表示成功收到应答 }读取一个字节Read_Byte过程和发送类似但方向相反。主机控制SCL产生时钟从机控制SDA输出数据。主机在SCL高电平时读取SDA。unsigned char Read_Byte(void) { unsigned char i,receive0; SDA_IN(); // SDA设置为输入准备读取从机数据 for(i0;i8;i ) { SCL(0); delay_us(5); SCL(1); // 拉高SCL此时从机数据已稳定 delay_us(5); receive1; // 左移为接收新bit腾出最低位 if( SDA_GET() ) // 读取SDA电平 { receive|1; // 如果SDA为高最低位置1 } delay_us(5); } SCL(0); return receive; }3.4 上层应用函数读写AT24C02底层时序模拟好了读写AT24C02就很简单了就是按照芯片规定的通信流程来。写入一个字节AT24C02_WriteByte发送起始信号。发送器件写地址0xA0。发送要写入的存储单元地址0-255。发送要写入的数据。发送停止信号。void AT24C02_WriteByte(unsigned char WordAddress,unsigned char Data) { IIC_Start(); Send_Byte(AT24C02_ADDRESS_READ); // 注意这里是写操作但宏定义名字是READ实际值是0xA0写地址 I2C_WaitAck(); Send_Byte(WordAddress); // 发送内存地址 I2C_WaitAck(); Send_Byte(Data); // 发送要存储的数据 I2C_WaitAck(); IIC_Stop(); }注意AT24C02在写入数据后内部需要一定时间约5ms进行擦写操作。在这段时间内芯片不会响应新的I2C命令。所以写操作后最好加一个delay_ms(5)的延时。读取一个字节AT24C02_ReadByte发送起始信号。发送器件写地址0xA0。发送要读取的存储单元地址。再次发送起始信号这叫“重复起始条件”。发送器件读地址0xA1。读取一个字节数据。主机发送非应答信号告诉从机“我不要更多数据了”。发送停止信号。unsigned char AT24C02_ReadByte(unsigned char WordAddress) { unsigned char Data; IIC_Start(); Send_Byte(AT24C02_ADDRESS_READ); // 发送写地址准备告诉芯片我要读哪个地址 I2C_WaitAck(); Send_Byte(WordAddress); // 发送要读的内存地址 I2C_WaitAck(); IIC_Start(); // 再次发送起始信号 Send_Byte(AT24C02_ADDRESS_WRITE);// 发送读地址0xA1 I2C_WaitAck(); DataRead_Byte(); // 读取数据 IIC_Send_Ack(1); // 发送非应答(1)表示读取结束 IIC_Stop(); return Data; }4. 实战验证让代码跑起来代码都准备好了现在咱们写个主程序来测试一下。在main.c里咱们先初始化串口方便打印信息和AT24C02然后往芯片里写两个数再读出来看看对不对。#include board.h #include bsp_uart.h #include stdio.h #include bsp_at24c02.h int main(void) { unsigned char dat1 0; unsigned char dat2 0; board_init(); // 开发板基础初始化 uart1_init(9600U); // 初始化串口1波特率9600用于打印 AT24C02_GPIO_Init(); // 初始化AT24C02的GPIO printf(AT24C02 Test Start!\r\n); // 测试1向地址0写入数据48 AT24C02_WriteByte(0, 48); delay_ms(5); // 等待芯片内部写操作完成 // 测试2向地址8写入数据66 AT24C02_WriteByte(8, 66); delay_ms(5); // 测试3从地址0读出数据 dat1 AT24C02_ReadByte(0); delay_ms(5); // 测试4从地址8读出数据 dat2 AT24C02_ReadByte(8); delay_ms(5); // 通过串口打印结果 printf(Read from Address 0: dat1 %d\r\n, dat1); printf(Read from Address 8: dat2 %d\r\n, dat2); while(1) { // 主循环可以添加其他应用代码 } }把代码编译下载到天空星开发板打开串口助手比如Putty、XCOM设置好波特率9600。如果一切正常你会看到串口打印出AT24C02 Test Start! Read from Address 0: dat1 48 Read from Address 8: dat2 66恭喜你这说明AT24C02的读写功能完全正常。你可以尝试修改写入的地址和数据或者断电后再上电看看读出来的数据是不是还是你之前写入的。这就是“掉电存储”的魅力。5. 常见问题与调试心得移植过程中可能会遇到一些小问题这里分享几个我踩过的坑读出来的数据全是0xFF或0x00首先检查硬件连接VCC和GND有没有接反或接触不良。然后确认SDA和SCL的上拉电阻是否正常模块一般自带。最后用逻辑分析仪或者示波器抓一下I2C波形看看起始信号、地址、数据、应答信号是不是都对。只能写一次再写就不行了AT24C02有写入次数限制通常100万次。但更常见的原因是写操作后没有加足够的延时。芯片内部完成一次擦写需要几毫秒在这期间它不会应答I2C。务必在每次AT24C02_WriteByte后加上delay_ms(5)。地址冲突如果你的I2C总线上挂了多个设备要确保它们的I2C地址不一样。AT24C02的地址由硬件引脚A0/A1/A2决定。查看你的模块如果这三个引脚都接地地址就是0xA0写/0xA1读。时序问题如果用的是不同的单片机比如主频比STM32F407慢可能需要调整代码中的delay_us(5)延时。时序太快或太慢都可能通信失败。可以尝试增大延时试试。这个模拟I2C的驱动虽然代码量比用硬件I2C外设多一些但好处是清晰、可控、可移植。一旦调通了你可以把它用到任何支持GPIO的单片机上。希望这篇教程能帮你彻底掌握I2C和EEPROM的使用在你的项目里实现可靠的数据存储。