麒麟座STM32 IIC读写AT24C02 EEPROM例程详解与调试经验
简介麒麟座IIC测试AT24C02RET6.zip是一套基于STM32F103开发板的IIC通信协议测试工程面向嵌入式初学者、单片机爱好者以及需要快速上手IIC总线与EEPROM读写应用的开发者。项目以中国移动麒麟座开发板为硬件平台借助STM32F103内置IIC外设与板载AT24C04存储芯片完成数据交互内容涵盖微秒级延时函数、IIC初始化与收发驱动、基于设备的抽象对接层等模块可帮助读者深入理解IIC多主机总线协议、从机响应机制以及非易失性存储器的应用逻辑。压缩包整体约161.83MB资源中包含基本工程框架、延时函数、IIC通信模块以及可能的触摸屏IIC参考文件同时带有git版本控制目录便于追溯修改过程代码结构清晰适合边读边练。目前已有166人学习适合作为IIC协议入门、嵌入式驱动开发或毕业设计的参考资料能够有效缩短从原理学习到实际调试验证的周期。 搞到或下载到「麒麟座IIC测试AT24C02RET6.zip」这个例程包的朋友大概率正在折腾麒麟座开发板或者正在给STM32项目找一颗掉电不丢数据的存储芯片。这个包看名字就挺直白麒麟座 IIC AT24C02 RET6也就是在STM32F103RET6上通过IIC总线读写AT24C02这颗EEPROM的完整测试工程。我拿到这个包之后把里面的代码、接线方式、时序细节全部过了一遍又自己在板子上完整测了一轮这篇就把整个过程复盘出来。这篇文章主要解决三类人的问题刚接触STM32、想把IIC协议跑通的新手在麒麟座板上做项目、需要存参数或掉电保存数据的开发者以及拿到例程包但不知道怎么接线、怎么改引脚、怎么看波形验证结果的半入门玩家。我会把IIC协议拆开讲清楚把AT24C02的读写要点一条条列出来再把调试中真正容易踩的坑全部抖出来确保你看完能直接复现而不是只收藏吃灰。1. 例程包里到底装了什么先搞清楚硬件再动手1.1 麒麟座开发板上的IIC资源在哪麒麟座这块板子是典型的物联网入门板主控用了一片STM32F103RET6LQFP64封装主频最高72MHz。板载资源里温湿度传感器、部分接口扩展都挂在IIC总线上所以你在板子原理图上会看到一组SCL/SDA的走线。例程包里的AT24C02测试要么直接复用板上预留的IIC引脚要么通过排针引出来外接模块。我打开例程后第一件事不是看代码而是翻原理图确认引脚。这步很关键如果例程用的引脚和你实际接线的引脚对不上代码写得再对也没用。AT24C02作为标准IIC从设备只要SCL、SDA两根线接对电源和地接对地址引脚电平匹配基本就能工作。它不像SPI那样还要纠结MISO/MOSI方向也不像UART要交叉收发接错概率相对低但初次上电前还是建议用万用表量一下电源引脚。1.2 RET6主控引脚、时钟和例程的对应关系麒麟座板上用的STM32F103RET6片上有512KB Flash、64KB SRAM、5个串口、3个I2C外设跑这个测试工程绰绰有余。例程里如果是软件模拟IIC那么任意两个GPIO都能当SCL/SDA用如果是硬件I2C那就必须接在I2C1或I2C2的复用引脚上比如PB6/PB7就是I2C1PB10/PB11是I2C2。我建议拿到例程后先看工程里GPIO初始化部分用的是哪个引脚再去板子原理图里找对应丝印两相对照确定是用杜邦线外接AT24C02模块还是板载已经预留了EEPROM焊盘。RET6这颗芯片的IIC外设本身支持100kHz标准模式和400kHz快速模式但例程为了兼容性和教学效果一般会把时钟配到标准模式或者直接用软件时序。后面对比硬件IIC和软件IIC的时候我会详细说这两种方式的取舍。注意麒麟座板上如果还有其他IIC设备挂在同一条总线上比如温湿度传感器那地址就要错开。AT24C02默认7位地址是0x50A0/A1/A2均接地如果和板载设备冲突就需要检查是否有跳线或地址选择电阻。2. AT24C02读写原理不懂时序就很难把代码调通2.1 IIC协议的一帧数据拆开看就是这几个动作IIC协议看起来玄乎本质就四个动作起始、数据传输、应答、停止。起始条件是SCL为高电平时SDA由高变低停止条件是SCL为高电平时SDA由低变高。数据在SCL高电平期间必须保持稳定只有在SCL低电平期间SDA才允许变化。这句话是IIC时序的核心读代码和看波形时都在验证这一点。打个比方IIC总线像一条只有两根线的电话线SCL是时钟节拍SDA是话筒。主设备先拨号起始条件然后报出从设备号码设备地址加读写位对方接了电话ACK应答后面就是正事——发数据或收数据说完挂电话停止条件。AT24C02这种从设备不会主动说话所有动作都是主设备发起它只负责应答和执行。发送一个字节时数据从最高位开始一位一位往SDA上送每一位对应SCL的一个高电平脉冲第9个脉冲是应答位。从设备在第9个时钟周期把SDA拉低表示“我收到了继续”如果它不拉低也就是读到高电平NACK表示“我没准备好”或“通信结束”。2.2 设备地址、页写入和那个不能省的写延时AT24C02的容量是2Kbit也就是256字节。器件地址是固定组合前4位是1010后3位由A2、A1、A0引脚的电平决定最后1位是读写控制。A0/A1/A2都接地时7位设备地址就是0x50写地址是0xA0读地址是0xA1。如果外接模块上A0/A1/A2有拨码开关或跳线改动后地址就得跟着算。很多新手第一次调EEPROM时最容易忽略两个时间问题一个是页写入的边界另一个是写周期延时。AT24C02内部按页组织一页是8字节。连续写多个字节时如果跨越页边界地址会回卷到本页开头把前面刚写的数据覆盖掉。我见过有人连续写十几个字节结果数据错乱查了半天才发现是跨页写的问题。靠谱的做法是要么一页一页地写要么干脆单字节写。单字节写看似啰嗦但每一字节写完EEPROM内部都要进行擦写操作这个时间典型值是5ms手册里叫写周期tWR。在这个时间段内芯片不响应任何总线命令你发什么它都当没听见。所以写完一个字节后必须延时5ms以上再写下一个字节。延时不够就会数据丢字节或读到旧值。这个坑在逻辑分析仪上最容易暴露写完地址后ACK突然没了多半就是写周期还没结束。3. 工程实测软件IIC读写AT24C02的完整实现3.1 为什么例程里用的是软件模拟IICSTM32F103系列自带的硬件I2C外设官方手册写得很漂亮但实际调试时不少人遇到过busy位卡死、时钟延展处理麻烦的问题。很多老工程师宁可用GPIO软件模拟时序也不碰硬件I2C。例程包用软件IIC我认为是出于三层考虑第一教学演示更直观每一句代码对应一个时序动作看代码就等于看波形第二不依赖特定引脚复用方便在不同板子上移植第三稳定性更好控制遇到问题可以直接用GPIO电平来排查不会陷入硬件外设状态的怪圈。软件IIC并不是性能最优解但在这个场景下性能足够。AT24C02本身就是慢速设备IIC时钟100kHz用GPIO翻转来实现完全没压力。如果哪天要在高速总线上挂多个高速IIC传感器再考虑上硬件I2C也不迟。学习阶段先把协议逻辑吃透比盲目追求外设效率重要得多。3.2 核心函数逐段拆解与主流程演示例程核心代码不复杂本质就几个函数。我把它简化整理成一份便于理解、也方便自己项目搬用的版本。先说IIC起始和停止void IIC_Start(void) { SDA_H(); SCL_H(); delay_us(5); SDA_L(); delay_us(5); SCL_L(); }这个函数做的事情就是在SCL高电平期间把SDA从高拉低。注意顺序不能反先拉低SDA再拉低SCL这就是起始条件。停止函数反过来SCL高电平期间SDA从低变高void IIC_Stop(void) { SDA_L(); SCL_H(); delay_us(5); SDA_H(); delay_us(5); }然后是一个关键函数发送一个字节并等待从设备应答。它由两部分组成循环8次逐位发送以及第9个时钟周期读取ACK。uint8_t IIC_WriteByte(uint8_t data) { uint8_t i; for (i 0; i 8; i) { if (data 0x80) SDA_H(); else SDA_L(); data 1; delay_us(2); SCL_H(); delay_us(2); SCL_L(); } SDA_IN(); // 释放SDA改为输入准备读取ACK delay_us(2); SCL_H(); delay_us(2); if (SDA_READ()) // 读取SDA电平高则NACK低则ACK { SCL_L(); return 1; // 返回1表示未收到应答 } SCL_L(); return 0; }这段代码里有个很多教材不会强调的细节发送完8位数据后主设备必须释放SDA总线也就是把SDA配置成输入状态或输出高阻这样从设备才能把SDA拉低来产生ACK。如果主设备一直占着SDA输出高电平从设备根本拉不动ACK永远读不到。这个细节在GPIO开漏输出模式下尤其重要后面我讲上拉电阻时还会再提。寄存器操作方面例程通常采用库函数或寄存器直操作方式。我个人更推荐把GPIO底层的拉高拉低、输入输出切换封装成宏比如#define SDA_IN() { GPIOB-CRL 0xFF0FFFFF; GPIOB-CRL | 0x80000000; } #define SDA_OUT() { GPIOB-CRL 0xFF0FFFFF; GPIOB-CRL | 0x30000000; }这里用的是STM32F103的CRL寄存器PB7是16位端口中的第7脚对应CRL的bit28到bit31。第一次看这个宏的人可能会懵但理解了“GPIO配置模式本质就是写控制寄存器”之后反而觉得比库函数清爽。接下来是AT24C02的字节写函数void AT24C02_WriteByte(uint8_t addr, uint8_t data) { IIC_Start(); IIC_WriteByte(0xA0); // 设备写地址 IIC_WriteByte(addr); // 芯片内部存储地址 IIC_WriteByte(data); // 要写入的数据 IIC_Stop(); delay_ms(10); // 等待写周期结束实测10ms比较稳 }随机读函数uint8_t AT24C02_ReadByte(uint8_t addr) { uint8_t data; IIC_Start(); IIC_WriteByte(0xA0); // 先发送写地址把存储地址写进去 IIC_WriteByte(addr); IIC_Start(); // 重复起始条件 IIC_WriteByte(0xA1); // 再发送读地址 data IIC_ReadByte(); // 读一个字节 IIC_Stop(); return data; }读操作比写操作多一个“重复起始条件”的过程这是IIC协议里的经典用法先告诉芯片我要读哪个地址然后用重复起始条件切换读写方向。AT24C02收到读地址后会把当前指针指向的字节放到SDA上主设备在第9个时钟周期不产生ACK而直接停止芯片就知道读完了。这里的ACK处理同样关键读最后一个字节时主机要回复NACK否则芯片会继续往总线上送下一个字节。主流程就很简单了先写一个字节到地址0x00再读回来通过串口或OLED打印结果int main(void) { uint8_t write_val 0x5A; uint8_t read_val 0x00; SystemInit(); GPIO_Config(); USART_Config(); AT24C02_WriteByte(0x00, write_val); delay_ms(10); read_val AT24C02_ReadByte(0x00); printf(Write:0x%02X Read:0x%02X\r\n, write_val, read_val); while (1); }如果串口打印出来写读一致说明整个IIC链路已经通了。接下来再往地址、多字节、页写方向扩展基础就牢了。3.3 直接把例程跑起来需要准备什么准备工作不复杂一块麒麟座开发板或其他STM32F103RET6板子一个AT24C02模块4根杜邦线一个ST-Link或J-Link下载器一个USB转串口模块用于看打印最好再有一台逻辑分析仪。没有逻辑分析仪也能调但排查时序问题会吃力很多。接线方面SDA接例程定义的SDA引脚SCL接例程定义的SCL引脚VCC接3.3VGND接GND。AT24C02模块上的WP写保护引脚一定要拉低或接地否则写操作会全部无效读出来始终是0xFF。这个我之前粗心大意吃过亏,单独列出来。4. 调试中遇到的坑上拉电阻、示波器和常见报错4.1 SDA拉不高、读回0xFF先别怀疑程序IIC总线是开漏结构这是协议本身的硬件要求所以SCL和SDA必须有上拉电阻。没有上拉电阻总线空闲时SDA就浮空读回来的数据全是0xFF很多人debug一整天查代码最后发现是模块上没焊上拉电阻。上拉电阻取多大没有唯一标准要综合考虑总线电容、通信速率和从设备数量。低速100kHz、板内短走线、单设备场景用10k或4.7k都行400kHz或设备多的时候建议用2.2k到4.7k之间。如果模块上自带贴片上拉电阻一般不需要额外接。判断方法很简单用万用表测SDA对地电压空闲时接近3.3V说明上拉正常如果悬空或接近0V就得查上拉了。我用麒麟座调试时还遇到一个特殊现象SDA和SCL都接了上拉程序看起来也对但读取一直是0xFF。后来查到是例程引用的GPIO初始化函数里把SDA配成了推挽复用输出而不是开漏输出。推挽输出会把SDA强行拉高或拉低从设备根本没法在ACK时把SDA拉低总线自然就“死”了。解决办法是配成开漏输出或者在软件模拟IIC时在收发之间切换输入输出模式保证SDA是一个“可被外部拉低”的状态。4.2 常见报错与排查方法速查表我把调试中容易遇到的问题整理成一张表方便对照排查现象可能原因排查方法读回全是0xFFSDA/SCL无上拉、WP写保护拉高、地址不对、接线松测空闲电平查WP引脚核对器件地址重新插杜邦线读回全0x00从设备未应答主设备读到低电平查供电查SCL/SDA是否接反用逻辑分析仪看ACK写数据后再读不一致写周期延时不够、跨页写覆盖每字节写后延时至少10ms避免跨页连续写IIC_WriteByte返回1NACK设备地址错、从机不存在、总线被占用计算7位地址用IIC扫描程序探测在线设备卡在等待标志位不动硬件I2CF1系列I2C外设busy位异常复位外设或改用软件模拟IIC下载时报no stm32 target found仿真器连接问题、目标板供电不足检查接线和BOOT引脚确认Debug配置为SWD重新上电后再连接表中第一行和第二行是两极全0xFF和全0x00代表的是完全不同的故障方向排查思路也不一样。全0xFF基本可以确定主设备发出的数据从设备压根没正确接收重点查硬件连接和上拉全0x00说明主设备在总线上读到的电平是低重点查是否被某设备拉死或接错线。用逻辑分析仪抓一下总线波形这两种情况一眼就能区分。4.3 关于硬件I2C和软件IIC的选择建议聊到这里还是绕不开硬件I2C和软件IIC的选择问题。F103的硬件I2C在工程圈子里口碑两极有人觉得挺好用有人被坑得直接放弃。我的实际体会是硬件I2C适合做产品节省CPU时间配合中断和DMA效率高软件IIC适合学习、调试和快速验证代码可控性强出问题容易定位。如果是新项目、需求简单、只有一两个从设备我建议直接软件IIC稳定压倒一切。如果从设备数量多、数据量大、对实时性要求高那就硬着头皮啃硬件I2C但要特别注意超时处理任何等待标志位的循环都要加超时退出不然一旦busy标志卡死程序就僵在那边了。话说回来IIC调通之后带来的成就感是非常实在的因为协议本身简单又通用很多传感器、存储芯片、显示屏都用它整个嵌入式领域绕不开。5. 从测试代码到实用功能把EEPROM变成工程的一部分5.1 参数存储、标志位和数据校验例程测通了很多人就扔一边了其实AT24C02这种芯片在真实项目里最大的价值是掉电保存。麒麟座这种板子如果做联网设备经常需要保存WiFi配置、设备编号、校准参数、报警阈值这些数据。EE PROM就是干这个的。我习惯把要保存的数据定义成一个结构体然后用memcpy整体写入。比如typedef struct { uint8_t magic; uint16_t device_id; int16_t temp_offset; uint8_t alarm_enable; uint8_t reserve[3]; } SystemParam; void Param_Save(SystemParam *param) { AT24C02_WriteBytes(0x00, (uint8_t *)param, sizeof(SystemParam)); } void Param_Load(SystemParam *param) { AT24C02_ReadBytes(0x00, (uint8_t *)param, sizeof(SystemParam)); }结构体的好处是扩展方便新增字段时注意保持偏移量稳定。建议在结构体开头放一个固定的magic标志比如0xA5A5加载时先判断这个标志如果读到的是0xFFFF或乱码说明芯片里还没有有效数据就用默认参数初始化一遍再写回EEPROM。这样既能避免首次上电读到垃圾数据也能在数据损坏时自动恢复。EEPROM还有寿命问题AT24C02擦写次数典型值100万次看起来很多但如果哪天掉电保存逻辑写得不严谨在循环里反复写时间长了照样会磨穿。写之前先比较一下当前值和目标值只有不同才执行写操作这是一种性价比很高的保护习惯。5.2 多设备共存的地址规划麒麟座板上不止AT24C02一个IIC设备如果同时接OLED、温湿度传感器就得做地址规划。IIC总线上每个从设备必须有唯一地址否则会冲突。AT24C02的A0/A1/A2引脚允许8种地址组合所以同一颗芯片最多能挂8片但实际使用时要注意有的模块把这几个引脚已经通过电阻固定到地了想改地址就得动板子。我自己常用一个IIC总线扫描的小函数在初始化阶段遍历所有可能地址看哪些设备在线并打印出它们的地址。这样能第一时间发现地址冲突省去翻手册的麻烦。扫描逻辑很简单对每个7位地址发起写请求能收到ACK就说明这个地址有设备响应。多设备挂在同一条总线上时上拉电阻要重新评估。设备越多总线电容越大上升沿变缓速率过高时波形会有明显畸变。如果挂了三四个设备还要跑400kHz建议用2.2k上拉或干脆降速到100kHz稳定优先。我个人在实际操作中的体会是IIC是所有通信协议里最适合作为入门教学的一类因为它足够基础、足够通用、出问题也足够直观。麒麟座这个例程包的价值不仅仅是让你点亮一颗EEPROM而是给你一套完整的观察思路从原理图到GPIO配置从时序函数到示波器波形每一步都有明确的验证方法。调试IIC时最忌讳的就是瞎改代码碰运气把逻辑分析仪挂上SCL数脉搏SDA看数据一切都会变得非常清晰。新手阶段把软件IIC跑明白后面再接触那些带IIC接口的传感器、电源管理芯片、音频编解码器你会发现都是老朋友了。本文还有配套的精品资源点击获取