简介这是一份面向嵌入式开发与通信协议学习者的STM32实测项目包。项目基于中国移动麒麟座开发板以STM32F103作为主控制器通过IIC总线对板载AT24C04 EEPROM进行读写完整覆盖了IIC协议初始化、时序控制、微秒级延时、数据帧收发以及错误处理等关键环节。压缩包大小约161.83MB内部不仅包含项目源码和基础工程框架还提供git版本目录以及触摸屏相关的IIC通信文件便于读者了解多设备共用总线时的适配方式。工程内在逻辑上划分为基本工程、微秒延时模块、核心模块和设备对接抽象层这种分层设计能帮助读者在修改不同存储芯片或传感器时快速替换底层驱动从而加深对IIC从设备寻址与数据交互机制的理解。目前该资源已有167人学习对于正在学习STM32外设驱动或准备调试IIC总线通信的开发者而言是一份颇具参考价值的实例代码集合。1. 麒麟座 IIC 测试 AT24C02一份能少走弯路的 STM32 例程拆解拿到这份名为麒麟座 IIC 测试 AT24C02 的压缩包时我正被手头一块板子的 EEPROM 读写问题折腾得够呛。IIC 协议看起来简单实际调试时却是各种玄学有时能读不能写有时写完再读全是 0xFF还有时候波形对但数据就是不对。这份资源正好是 STM32 平台上针对 AT24C02 的完整测试工程包含初始化、单字节读写、连续读写和串口打印验证。它适合刚接触 IIC 通信的嵌入式新手也适合在 EEPROM 读写上翻过车、想找个干净例程对照排查的熟手。压缩包里没有花哨的东西就是一份能直接烧进板子看效果的实用代码我们一步步把它拆开。2. IIC 与 AT24C02先搞懂时序和地址再动手写代码2.1 IIC 总线的两根线为什么需要上拉电阻IIC 总线只有两根线SCL时钟和 SDA数据都是开漏输出。开漏意味着器件只能把线拉低不能主动拉高所以必须通过外部上拉电阻把线拉到高电平。常见做法是接 4.7kΩ 上拉到 VCC如果总线速率快或者线上设备多可以换 2.2kΩ。这个电阻值直接影响上升沿时间选大了波形边沿变缓选小了功耗增加。STM32 的很多开发板已经自带上拉电阻但有些最小系统板需要自己外接否则 SDA 一直处于低电平总线上压根起不了通信。IIC 地址是 7 位的AT24C02 的地址由硬件引脚 A0、A1、A2 决定默认接地时地址是 0x507 位地址。在 STM32 的 HAL 库中写地址时需要左移一位加上读写位所以实际传入的器件地址是 0xA0写或 0xA1读。这个左移操作是新手最容易搞混的地方后面代码里我会特别标注。2.2 AT24C02 的存储结构和页写入边界AT24C02 容量是 2Kbit也就是 256 字节内部按 8 字节一页分为 32 页。单字节写入没有限制但页写入一次最多写 8 字节超出后会回卷到当前页的首地址把之前写的数据覆盖掉。这是 AT24C02 最容易踩的坑你往地址 0x07 连续写 8 字节实际上最后几个字节会写到 0x00 到 0x03因为页边界回卷了。所以做连续写操作时必须判断剩余字节数是否超过当前页剩余空间分批写入。读操作没有页边界限制可以任意连续读内部地址自动加一。但要注意读操作有三种模式当前地址读、随机读、顺序读。随机读需要先发送一个 dummy 写操作来设置目标地址然后重启起始信号读。HAL 库的HAL_I2C_Mem_Read封装了这个过程但要确保传入的 MemAddress 是正确的否则读到的数据驴唇不对马嘴。2.3 为什么 IIC 调试要用示波器而不是逻辑分析仪我调试 IIC 时习惯同时开示波器两个通道分别抓 SCL 和 SDA。逻辑分析仪虽然也能解码但示波器能看到模拟波形比如边沿是否过缓、ACK 位是否正常拉低。有些 IIC 设备对时序要求严格逻辑分析仪解码成功但设备就是不响应这时候必须看波形。常见波形问题有SCL 频率过高导致从设备跟不上SDA 数据在 SCL 高电平时跳变应该是低电平时变化以及没有 ACK 响应。STM32 的 IIC 外设硬件会生成时序比 IO 口模拟稳定得多但配置错了也会出怪问题。这个例程里用的是 STM32 硬件 IIC 外设配置参数都在 MX 初始化代码里我们接着看。3. 麒麟座板卡上的硬件连接与 CubeMX 初始化配置3.1 板卡引脚映射PB6/PB7 到 AT24C02 的连接这块麒麟座开发板把 AT24C02 接到了 STM32 的 I2C1 外设上对应引脚是 PB6SCL和 PB7SDA。板子上已经贴了 4.7kΩ 上拉电阻A0/A1/A2 全部接地所以设备地址固定是 0x50。如果你用的是其他板子先查原理图确认引脚别直接套用这个 GPIO 配置。打开压缩包里的i2c.c文件能看到完整的初始化函数。这里我提取核心部分void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; // 快速模式 400kHz hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; // 主机模式不需要设置本机地址 hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 0; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(hi2c1); }ClockSpeed 400000是快速模式对应 AT24C02 手册里支持的最高速率。如果你的板子布线比较长或者上拉电阻偏大可以降到 100000 试稳定。OwnAddress1在主机模式下填 0 即可因为 STM32 不响应从机地址匹配。NoStretchMode 必须设为 disable否则从设备拉低时钟延时时主设备不等待会导致读写失败。3.2 GPIO 复用配置AF_AF4 必须选对I2C 引脚功能是复用开漏CubeMX 里配置 PB6/PB7 时要把 GPIO Mode 设为Alternate Function Push-Pull输出速度选 High上拉/下拉选 Pull-up然后同时配置 Alternate Function 为I2C1_SCL和I2C1_SDA。代码生成后对应的是GPIO_AF4_I2C1很多朋友在这里选成了AF0或者AF1结果引脚功能不对IIC 通信无响应。GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_OD; // 复用开漏 GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF4_I2C1; HAL_GPIO_Init(GPIOB, GPIO_InitStruct);留意GPIO_MODE_AF_OD必须用开漏模式不能推挽。因为 IIC 协议要求开漏如果用推挽当器件拉低时会产生短路电流严重时可能损坏引脚或者让波形异常。上拉电阻的选择配合开漏才能让总线在空闲时保持高电平。4. 读写 AT24C02 的核心代码从单字节到连续读写4.1 用 HAL 库函数实现单字节写入AT24C02 写入一个字节需要三个步骤起始信号、发送设备地址写位、发送目标地址、发送数据、停止信号。HAL 库把这封装成了HAL_I2C_Mem_Write但底层时序是硬件生成的。下面这个函数是例程里写的用来向指定地址写一个字节。uint8_t AT24C02_WriteByte(uint16_t addr, uint8_t data) { HAL_StatusTypeDef status; status HAL_I2C_Mem_Write(hi2c1, 0xA0, addr, I2C_MEMADD_SIZE_8BIT, data, 1, 100); if (status ! HAL_OK) { return 0; // 写失败返回0 } HAL_Delay(5); // 等待内部写周期 return 1; }参数说明0xA0是设备写地址addr是 8 位地址因为 AT24C02 只有 256 字节1 字节地址够用I2C_MEMADD_SIZE_8BIT告诉库地址宽度是 8 位data是数据缓冲区指针1是长度100是超时毫秒数。写完后一定要HAL_Delay(5)AT24C02 的数据手册写周期典型值在 5ms 以内不等待就立即读会读出旧数据。这个延时是整个写入流程最容易忽略的部分。4.2 单字节读取随机读流程读单字节比写多一步。因为要先告诉芯片要读哪个地址再重新发起读操作。HAL_I2C_Mem_Read内部封装了这个流程先写设备地址写位发送内存地址然后重启发起设备地址读位读取数据。uint8_t AT24C02_ReadByte(uint16_t addr, uint8_t *buf) { HAL_StatusTypeDef status; status HAL_I2C_Mem_Read(hi2c1, 0xA1, addr, I2C_MEMADD_SIZE_8BIT, buf, 1, 100); if (status ! HAL_OK) { return 0; } return 1; }注意第二个参数变成了0xA1这是读地址。有些教程喜欢写成0xA0 | 0x01效果一样。buf是存放读出数据的指针。如果读操作放在写操作之后且中间没有等待写周期读到的要么是错误数据要么是 0xFF。所以我在读函数调用前如果刚写过建议加一句HAL_Delay(5)或者在写函数内部统一等待。4.3 页写入先算边界再动笔连续写多个字节时不能直接调HAL_I2C_Mem_Write传大长度因为芯片内部页写入最多 8 字节。例程里的连续写函数做了页边界判断uint8_t AT24C02_WriteMulti(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t offset 0; uint8_t page_free; while (len 0) { page_free 8 - (addr % 8); // 当前页剩余可写字节数 if (page_free len) { page_free len; } if (HAL_I2C_Mem_Write(hi2c1, 0xA0, addr offset, I2C_MEMADD_SIZE_8BIT, buf offset, page_free, 100) ! HAL_OK) { return 0; } HAL_Delay(5); offset page_free; addr page_free; len - page_free; } return 1; }addr % 8算出当前地址在页内的偏移8 - (addr % 8)就是这一页还剩多少个字节位置。如果要求写入长度超过页剩余空间就只写当前页的剩余部分然后地址加页剩余量循环继续写下一页。这个做法能避免页回卷覆盖是连续写 EEPROM 的标准写法。4.4 顺序读无需边界限制读操作没有页边界概念可以一次读任意多字节。但要注意HAL_I2C_Mem_Read的len参数不要超过缓冲区大小而且接收缓冲区要够大。例程里的顺序读如下uint8_t AT24C02_ReadMulti(uint16_t addr, uint8_t *buf, uint16_t len) { if (HAL_I2C_Mem_Read(hi2c1, 0xA1, addr, I2C_MEMADD_SIZE_8BIT, buf, len, 200) ! HAL_OK) { return 0; } return 1; }这里len就是想读的字节数HAL 库会连续发送读时序从设备内部地址自动递增。超时设 200ms如果总线上有地址不匹配的设备超时后函数返回 HAL_ERROR。调试时如果发现读返回错误先检查设备地址是否正确再查波形。5. 避坑与常见问题排查我遇到过的五个典型翻车现场5.1 器件地址一直 ACK 失败检查 A0/A1/A2 引脚现象调用写函数返回 HAL_ERROR用示波器抓 ACK 位一直是高电平。原因AT24C02 的地址引脚 A0/A1/A2 没有接地或者被拉高了。部分开发板为了兼容多块 EEPROM用跳线帽控制地址默认开路时内部下拉但有些板子硬件设计是默认拉高。解决先看原理图确认三个引脚电平地址值等于0x50 | (A22) | (A11) | A0。然后用万用表量一下引脚对地电压如果接近 VCC就把跳线帽换到接地状态或者代码里把设备地址改成对应值。5.2 能写不能读读出全是 0xFF现象写一次后读回来全是 0xFF或者写入时返回成功但掉电后数据丢失。原因写周期等待不够。EEPROM 内部写操作需要时间AT24C02 典型写周期是 5ms如果写入后没有延时立即读芯片还在忙读到的就是旧数据或者未定义值。另一个可能是写保护引脚 WP 被拉高芯片只读不写。解决每次写操作后加 5~10msHAL_Delay然后在读之前再延时一次。同时查一下 WP 引脚正常使用要接地。如果板子上 WP 接了 GPIO 控制记得初始化时把它拉低。5.3 页写入回卷前 8 字节被覆盖现象往地址 0x05 开始写 8 字节结果 0x00 到 0x03 地址的数据也变了。原因没有做页边界处理直接一次写入长度大于页剩余空间的字节。AT24C02 内部写指针在写到页末尾时会回卷到页首地址于是多出来的字节被写到了同页的起始位置。解决用上文的AT24C02_WriteMulti分段写入每次写前计算页剩余空间。我在调试时还发现有些 STM32 库函数底层会自动拆分但有些不会所以自己的逻辑模块里必须显式判断不要依赖库。5.4 IIC 总线卡死SCL 正常 SDA 一直低现象程序运行一段时间后SDA 被拉低通信完全停止。复位后第一次读成功第二次就卡死。原因总线死锁。某个时序错误导致从设备处于错误状态SDA 被从设备持续拉低。常见于通信过程中产生了额外的起始或停止信号或者从设备在 ACK 期间异常。解决尝试在初始化时给 IIC 外设发送一个停止信号来复位总线。我通常在AT24C02_Init里加上// 产生一个停止信号复位从设备 hi2c1.Instance-CR1 | I2C_CR1_SWRST; HAL_Delay(1); hi2c1.Instance-CR1 ~I2C_CR1_SWRST; HAL_I2C_Init(hi2c1);或者用 GPIO 手动翻转 SCL 9 个周期释放总线。但最可靠的是检查时序尤其是 ACK 处理。HAL 库的HAL_I2C_Mem_Read在最后会发送 NACK 和停止信号如果这次通信被中断下次就会卡死。所以在超时错误后最好重新初始化 I2C。5.5 引脚配置选错波形异常或者无波形现象SCL 和 SDA 都没有波形或者波形是推挽全幅值没有开漏的低电平特征。原因CubeMX 里 GPIO 模式选成了GPIO_MODE_AF_PP复用推挽而不是GPIO_MODE_AF_OD复用开漏。如果上拉电阻不存在推挽输出空闲时是高电平但总线上同时有其他设备下拉时会产生冲突电流。解决强制改为开漏模式并且使能内部上拉。CubeMX 中GPIO Pull-up/Pull-down选择 Pull-up代码里对应GPIO_PULLUP。改完后用示波器确认空闲状态是高电平设备应答时 SDA 能拉低到 0V。6. 进阶验证用回读对比和串口输出确认每一字节都落对位置写完读写函数你怎么确定数据真的写对了我习惯做一个简单的回读测试向从 0x00 到 0xFF 的每个地址写入地址本身然后读回来比对任何一字节不对都能定位到具体地址。这个测试也顺便验证了所有页边界和页内偏移。void AT24C02_Test(void) { uint8_t write_buf[256]; uint8_t read_buf[256]; uint16_t i; uint8_t ret; // 填充写缓冲区每个地址写入地址值 for (i 0; i 256; i) { write_buf[i] (uint8_t)i; } // 连续写整片 ret AT24C02_WriteMulti(0, write_buf, 256); HAL_Delay(10); // 连续读回 ret AT24C02_ReadMulti(0, read_buf, 256); // 比对 for (i 0; i 256; i) { if (write_buf[i] ! read_buf[i]) { // 用串口打印出错位置 printf(Mismatch at addr %02X: %02X ! %02X\r\n, i, write_buf[i], read_buf[i]); return; } } printf(EEPROM test passed\r\n); }这个测试的关键在于写入内容与地址相同这样打印信息里自带地址一眼就能看到哪里出错。如果是 0xFF 说明没写进去如果是别的值说明写错位了。我在调试里发现页回卷问题时这个测试会报出比如 Mismatch at addr 0x00: 0x05 ! 0x00立刻能联想到刚才往 0x05 起始写 8 字节覆盖了 0x00。另一个进阶点是验证掉电保存。写完数据后给板子断电再上电直接读对应地址看数据是否还在。EEPROM 的写入耐久度是 100 万次正常测试没问题但不要在同一个地址反复写循环否则可能提前损坏。还有一个小习惯每次上电初始化后先读取一次 0x00 地址判断数据是否为 0xFF。如果是说明全新芯片或者上次写入失败这时候才执行写测试如果已经有数据就跳过写入直接读回避免每次上电都写一遍磨损芯片。从那以后我每次调 EEPROM 驱动都会强制走一遍回读对比流程确认页边界处理正确才继续做业务逻辑。这份麒麟座 IIC 测试例程的价值就在这它不是一个玩具 Demo而是能直接用来验证驱动正确性的工具希望帮到你。本文还有配套的精品资源点击获取