原创 ✍️ | 新手零门槛,全程新建工程,手把手不踩坑!

文章标签:#stm32 #stm32cubeide #I2C #EEPROM #AT24C256 #模块化编程 #嵌入式存储 #HAL库

系列前置博客(必看!否则跟不上哦😜):

  1. 第1章:零基础必看,从认知到实战全解析
  2. 第2章:STM32CubeIDE 使用+点亮LED
  3. 第3章:从新建工程到LED闪烁
  4. 第4章:GPIO输入+外部中断 实现按键控制LED
  5. 第5章:GPIO内部结构 + 8种模式详解
  6. 第6章:定时器中断原理 + 精准LED闪烁
  7. 第7章:串口通信 + printf重定向打印调试
  8. 第8章:PT100高精度测温实战 + ADS1232驱动 + 24位ADC数据解析
  9. 第9章:工业PID温度控制实战 + PWM加热驱动 + 串口实时调参(本章续篇)

💡 一、前言(为什么学I2C和EEPROM?数据保存的必修课!)

大家好,我是BackCatK Chen!

前面9章我们从点亮LED一路干到了PID温度控制,单片机已经能做很多事了。但接下来有一个尴尬的问题:你辛辛苦苦调好的PID参数(Kp=30.0、Ki=2.0、Kd=8.0),一断电就全丢了,下次开机又得重新输入!

这就像你写了一天的文档,下班时忘记点保存——心态爆炸😱!

今天咱们就学嵌入式必备的"数据存储技术"——I2C总线 + EEPROM。通过两根线(SDA/SCL),就能把数据永久保存在芯片里,断电也不丢。学完这一章,你的PID参数、传感器校准值、设备配置、运行日志统统可以存起来,上电自动加载,单片机真正有了"记忆能力"!

重点说明💥:本章全程从0新建工程,手把手教你配置I2C、驱动AT24C256(256Kbit = 32KB的大容量EEPROM),并采用模块化编程封装成 24c256.c24c256.h 文件——这是你在工作中一定会用到的工程技能。本章基于 STM32CubeIDE 1.19.0 + STM32H723ZGT6 编写(由于前面的某种原因,芯片升级了🐶),但 I2C 协议是通用的,F1/F4/H7 系列都能直接抄!

本章我把I2C协议原理、开漏输出与上拉电阻、设备地址计算、AT24C256页写入限制、模块化封装、跨页安全写入全部讲到最细,全程用"大白话+表情包+一步一截图",新手看完不仅能学会,还能直接移植到自己的项目里,爽到飞起!

🎯 二、本章核心功能目标(清晰明确,学完不迷茫)

  1. 搞懂 ✅ I2C总线协议的本质(两根线怎么传数据?SDA和SCL各干什么?)
  2. 掌握 ✅ 开漏输出 + 上拉电阻的硬件原理(为什么必须外接4.7kΩ电阻?)
  3. 学会 ✅ I2C的设备地址概念(一条总线上挂多个芯片,怎么区分?)
  4. 掌握 ✅ AT24C256的操作时序(单字节写、页写入、随机读取、连续读取)
  5. 学会 ✅ CubeIDE 配置 I2C 的方法(H723 的 I2C4,PD14/PD15)
  6. 掌握 ✅ 模块化编程(把 EEPROM 驱动封装成 24c256.c/h,方便移植)
  7. 实现 ✅ :
    • 单字节写入与读出验证(写入0xAB,读出0xAB)
    • 连续多字节页写入(最多 64 字节/页)
    • 跨页安全写入(自动分页,数据不覆盖)
    • 上电自动读取 EEPROM 并校验数据
    • LED 指示读写状态
  8. 理解 ✅ 为什么 I2C 总线上必须外加 4.7kΩ 上拉电阻(开漏输出的精髓)

🔌 三、硬件接口定义(仅提供我自己的开发版接口,大家根据自己的原理图配置)

本章不深入讲解原理图,只给出最简明的接线关系。

3.1 STM32 ↔ AT24C256 接线表

STM32H723ZGT6 引脚 AT24C256 引脚 功能说明
PD14 SDA (Pin 5) I2C 数据线(必须外接 4.7kΩ 上拉电阻到 3.3V)
PD15 SCL (Pin 6) I2C 时钟线(必须外接 4.7kΩ 上拉电阻到 3.3V)
3.3V VCC (Pin 8) 电源正
GND GND (Pin 4) 共地
GND A0 (Pin 1) 设备地址引脚(接地 → 地址位=0)
GND A1 (Pin 2) 设备地址引脚(接地 → 地址位=0)
GND A2 (Pin 3) 设备地址引脚(接地 → 地址位=0)
GND WP (Pin 7) 写保护(接地 = 允许写入,接 3.3V = 只读保护)

3.2 设备地址计算

AT24C256 的 7 位设备地址由两部分组成:

  • 高 4 位固定:1010(所有 AT24 系列 EEPROM 都一样)
  • 低 3 位由 A2/A1/A0 引脚电平决定
7位设备地址 = 1 0 1 0  A2 A1 A0
            = 1 0 1 0  0  0  0   (全部接地)
            = 0x50

HAL 库的地址使用规则:

HAL 库的 I2C 函数要求 DevAddress 参数传入 7 位设备地址左移 1 位后的值,且 HAL_I2C_Master_TransmitHAL_I2C_Master_Receive 应使用相同的地址参数。HAL 库内部会根据调用的是发送函数还是接收函数,自动设置最低位(0=写,1=读),用户无需手动区分读写地址。

AT24C256 在本章的地址宏定义:

  • 7 位设备地址 = 0x50(A2/A1/A0 全部接地)
  • HAL 库 DevAddress 参数 = 0x50 << 1 = 0xA0
  • 无论读还是写,统一传入 0xA0 即可

3.3 为什么必须外接 4.7kΩ 上拉电阻?(新手最易忽略的坑!)

I2C 总线的 SDA 和 SCL 引脚是开漏输出(Open-Drain)结构:

  • 引脚只能主动拉低到 GND
  • 引脚不能主动输出高电平
  • 高电平全靠外部上拉电阻拉到 VCC
没有上拉电阻 → SDA/SCL 悬空,电平不确定 → 通信失败 ❌
有上拉电阻 → 空闲时被拉到高电平 → 通信正常 ✅

STM32 内部有约 40kΩ 的弱上拉,但阻值太大,驱动能力不足,波形上升沿会严重变形(RC 充电太慢)。必须外接 4.7kΩ 上拉电阻,这是 I2C 通信的标准配置。

3.4 串口打印接线(本章使用 USART2)

STM32H723ZGT6 USB 转 TTL
PD5 (TX) RX
PD6 (RX) TX
GND GND

🏗️ 四、I2C 总线协议通俗讲解(大白话,不烧脑!)

4.1 I2C 是什么?

I2C = Inter-Integrated Circuit(芯片间通信总线)

大白话:I2C 就是一条"公共电话线"🔊,所有设备都挂在这条线上。主设备(STM32)通过"呼叫设备地址"的方式,点名要找哪个从设备(EEPROM),然后开始对话。其他设备听到不是叫自己,就保持沉默。

核心特点

  • 只需 两根线:SDA(数据)+ SCL(时钟)
  • 一条总线上最多挂 127 个设备(7 位地址模式)
  • 主设备发起通信,从设备响应
  • 速度:标准模式 100kHz,快速模式 400kHz

4.2 总线结构图

VCC (3.3V)
  │
  ├── 4.7kΩ ──┬── SDA ──┬──────────┬──────────
  │            │         │          │
  │          STM32    AT24C256   其他I2C设备
  │            │         │          │
  ├── 4.7kΩ ──┴── SCL ──┴──────────┴──────────
  │
 GND ─────────────── GND ──────── GND

所有设备并联在同一对总线上,通过不同的设备地址区分。

4.3 I2C 通信流程(一次完整的"对话")

STM32 要向 EEPROM 写入一个字节,通信过程如下:

① 起始信号(Start Condition)

SCL 保持高电平时,SDA 从高变低
→ "大家注意,通话开始!"
SCL: ──────┐     ┌─────
           └─────┘
SDA: ──┐     ┌───────
       └─────┘
       ↑ 起始信号

② 发送设备地址 + 读写位

发送 8 位数据:7 位设备地址 + 1 位读写方向
0xA0 = 1010 0000 → 设备地址0x50,写操作(最低位=0)

③ 等待应答(ACK)

从设备收到自己的地址后,在第9个时钟拉低 SDA
→ "我在!"
如果 SDA 没有被拉低 → 从设备不在线 → 通信失败

④ 发送内存地址(高8位 + 低8位)

T24C256 容量 32KB(32768 字节 = 2^15),需要 15 位地址来寻址。实际传输时占用 2 个字节,最高位(bit 15)恒为 0。有效地址范围 0x0000 ~ 0x7FFF。
先发高8位,再发低8位
每个字节后面都要等一个 ACK

⑤ 发送数据

发送要写入的数据字节,等待 ACK

⑥ 停止信号(Stop Condition)

SCL 保持高电平时,SDA 从低变高
→ "通话结束!"
SCL: ──────┐     ┌─────
           └─────┘
SDA: ────────┐     ┌──
             └─────┘
             ↑ 停止信号

4.4 为什么每个字节后面都要等 ACK?

ACK 是 I2C 协议的握手机制

  • 主设备每发完 8 位数据,释放 SDA(让上拉电阻拉到高电平)
  • 从设备如果收到数据,拉低 SDA(ACK)
  • 如果从设备没有拉低 SDA(NACK),说明出问题了:可能地址不对、设备不在线、内部正忙

这就是为什么 I2C 通信稳定可靠——每一步都有确认。

🔬 五、AT24C256 芯片详解(新手必知)

5.1 芯片特性速览

参数 说明
容量 256 Kbit = 32 KB(32768 字节)
页大小 64 字节/页(一次最多连续写 64 字节)
页数量 512 页
接口 I2C(100kHz 标准 / 400kHz 快速)
写入周期 最大 5ms(页写入需要内部编程时间)
擦写寿命 100 万次
设备地址 0x50(A2/A1/A0 全部接地)
地址宽度 15 位有效地址(传输占 2 字节,最高位恒为 0)

5.2 页写入的限制(必须搞懂!最容易翻车的地方)

AT24C256 内部按64 字节一页组织,共 512 页。单次写入时:

  • 最多写入 1 ~ 64 字节
  • 不能跨页! 如果从第 60 字节开始写 10 字节,前 4 字节写入 60~63,然后地址"绕回"到同一页的开头写入剩下的 6 字节,覆盖了前面 0~5 字节的数据!
  • 跨页写入需要分页处理

5.3 内存地址表示

AT24C256 需要 15 位地址来寻址(数据手册原文:“Random word addressing requires a 15‑bit data word address”)。实际传输时占用 2 个字节,且最高位(bit 15)始终为 0。

  • 发送顺序:先发高 8 位(其中最高位恒为 0),再发低 8 位

  • 有效地址范围:0x0000 ~ 0x7FFF(0 ~ 32767)

🛠️ 六、CubeIDE 1.19.0 新建工程+手把手配置(一步一截图,零踩坑)

重点🔥:全程从 0 新建工程,基于 STM32H723ZGT6。本章采用软件模拟 I2C 方式,不需要配置硬件 I2C 外设。

步骤1:新建工程

  1. 打开 STM32CubeIDE,点击 Start a new STM32 project
  2. 搜索芯片型号 STM32H723ZGT6,选中后点击 Next
  3. 工程名称:I2C_EEPROM_DemoFinish
    在这里插入图片描述

步骤2:配置 SCL 和 SDA 引脚(通用 GPIO 输出模式)

PD15 作为 SCL(时钟线),PD14 作为 SDA(数据线),都配置为开漏输出:

  1. 在右侧芯片引脚图中,找到 PD15
  2. 右键点击 PD15 → 选择 GPIO_Output
  3. 找到 PD14
  4. 右键点击 PD14 → 选择 GPIO_Output
    在这里插入图片描述

步骤3:GPIO 参数配置(关键!)

分别点击 PD14 和 PD15,在下方 GPIO 配置界面中,设置以下参数:

PD14(SDA 数据线)配置:

参数 设置值 说明
GPIO output level High(高电平) I2C 总线空闲时,SCL 和 SDA 都必须处于高电平
GPIO mode Output Open Drain I2C 总线必须用开漏输出,支持"线与"
GPIO Pull-up/Pull-down No pull-up and no pull-down 不加内部上下拉(外部已接 4.7kΩ 上拉电阻)
Maximum output speed Very High 模拟 I2C 时序需要快速翻转 GPIO
User Label EEPROM_SDA 代码中好识别的名字

PD15(SCL 时钟线)配置:

参数 设置值 说明
GPIO output level High(高电平) 同上
GPIO mode Output Open Drain 同上
GPIO Pull-up/Pull-down No pull-up and no pull-down 同上
Maximum output speed Very High 同上
User Label EEPROM_SCL 同上

⚠️ 为什么用开漏输出(Open Drain)?

I2C 总线的核心特性是**“线与”**(Wire-AND):多个设备可以同时挂在一根线上,任何一个设备拉低,整条线就变低。开漏输出完美支持这个特性:

  • 引脚只能主动输出低电平(拉低到 GND)
  • 高电平全靠外部上拉电阻拉到 VCC
  • 多个开漏输出可以并联,不会短路

如果用推挽输出,当两个设备同时输出不同电平时会直接短路烧毁芯片!
这就是为什么 I2C 总线必须是开漏输出 + 外部上拉电阻的结构。
在这里插入图片描述

步骤4:配置串口 USART1(printf 打印)—— 对应 PA9/PA10

  1. 左侧点击 ConnectivityUSART1
  2. Mode 选择 Asynchronous
  3. 参数设置:Baud Rate = 115200,Word Length = 8 Bits,Parity = None,Stop Bits = 1
  4. 确认引脚:PA9 = USART1_TXPA10 = USART1_RX

如果引脚不对可以在引脚处,重新选择串口1 如下图。

在这里插入图片描述

步骤5:配置 LED 引脚(用于状态指示)

  1. 在引脚图中找到 PA0
  2. 配置为 GPIO_Output
  3. 参数:推挽输出、无上下拉、Low Speed、别名设为 LED
    在这里插入图片描述

步骤6:生成代码

点击右上角 GENERATE CODE

🛠️ 六、CubeIDE 1.19.0 新建工程+手把手配置(一步一截图,零踩坑)

重点🔥:全程从 0 新建工程,基于 STM32H723ZGT6。本章采用软件模拟 I2C 方式驱动 EEPROM,串口使用 USART1(PA9/PA10) 进行 printf 打印。

为什么用软件模拟 I2C?

I2C 通信的本质就是在特定时序下控制 SDA 和 SCL 两根线的高低电平。用 GPIO 手动控制这两根线的翻转,完全能实现 I2C 通信,而且有几个好处:

  • 通用性强:不依赖芯片的硬件 I2C 外设,任何单片机都能用
  • 时序可控:可以精确控制 SDA 和 SCL 的翻转时机,方便调试
  • 避开硬件坑:STM32H723 的硬件 I2C 配置复杂,软件 I2C 更简单可靠
  • 便于移植:软件 I2C 只需要 GPIO 操作,换芯片时几乎不用改代码

这和我们在第 8 章用 GPIO 模拟 ADS1232 时序是一个道理——硬件不够用时,软件来凑!

步骤1:新建工程

  1. 打开 STM32CubeIDE,点击 Start a new STM32 project
  2. 搜索芯片型号 STM32H723ZGT6,选中后点击 Next
  3. 填写工程名称:I2C_EEPROM_Demo,选择保存路径,点击 Finish

步骤2:配置 SCL 和 SDA 引脚(通用 GPIO 输出模式)

PD15 作为 SCL(时钟线),PD14 作为 SDA(数据线),都配置为开漏输出:

  1. 在右侧芯片引脚图中,找到 PD15
  2. 右键点击 PD15 → 选择 GPIO_Output
  3. 找到 PD14
  4. 右键点击 PD14 → 选择 GPIO_Output

步骤3:GPIO 参数配置(关键!)

分别点击 PD14 和 PD15,在下方 GPIO 配置界面中,设置以下参数:

PD14(SDA 数据线)配置:

参数 设置值 说明
GPIO mode Output Open Drain I2C 总线必须用开漏输出,支持"线与"
GPIO Pull-up/Pull-down No pull-up and no pull-down 不加内部上下拉(外部已接 4.7kΩ 上拉电阻)
Maximum output speed Very High 模拟 I2C 时序需要快速翻转 GPIO
User Label EEPROM_SDA 代码中好识别的名字

PD15(SCL 时钟线)配置:

参数 设置值 说明
GPIO mode Output Open Drain 同上
GPIO Pull-up/Pull-down No pull-up and no pull-down 同上
Maximum output speed Very High 同上
User Label EEPROM_SCL 同上

⚠️ 为什么用开漏输出(Open Drain)?

I2C 总线的核心特性是**“线与”**(Wire-AND):多个设备可以同时挂在一根线上,任何一个设备拉低,整条线就变低。开漏输出完美支持这个特性:

  • 引脚只能主动输出低电平(拉低到 GND)
  • 高电平全靠外部上拉电阻拉到 VCC
  • 多个开漏输出可以并联,不会短路

如果用推挽输出,当两个设备同时输出不同电平时会直接短路烧毁芯片!
这就是为什么 I2C 总线必须是开漏输出 + 外部上拉电阻的结构。

步骤4:配置串口 USART1(printf 打印)—— 对应 PA9/PA10

  1. 左侧点击 ConnectivityUSART1
  2. Mode 选择 Asynchronous
  3. 参数设置:Baud Rate = 115200,Word Length = 8 Bits,Parity = None,Stop Bits = 1
  4. 确认引脚:PA9 = USART1_TXPA10 = USART1_RX

步骤5:配置 LED 引脚(用于状态指示)

  1. 在引脚图中找到 PA0
  2. 配置为 GPIO_Output
  3. 参数:推挽输出、无上下拉、Low Speed、别名设为 LED

步骤6:生成代码

点击右上角 GENERATE CODE

💻 七、代码功能详解(逐行拆解,新手也能懂)

✅ 本章重点学习软件模拟 I2C 时序模块化编程。我们将 I2C 底层时序和 AT24C256 驱动都封装成独立的 24c256.c/h 文件。引脚定义全部放在 24c256.c,做到模块自包含——main.c 只需包含头文件,不用关心引脚细节。

7.1 新建 EEPROM 驱动模块

在 STM32CubeIDE 中:

  1. 右键 Core/SrcNewSource File → 文件名:24c256.c
  2. 右键 Core/IncNewHeader File → 文件名:24c256.h

创建完成后的工程文件结构:

I2C_EEPROM_Demo
├── Core
│   ├── Inc
│   │   ├── main.h
│   │   └── 24c256.h        ← 新建的 EEPROM 头文件
│   └── Src
│       ├── main.c
│       └── 24c256.c         ← 新建的 EEPROM 源文件(内含引脚定义)

7.2 24c256.h —— EEPROM 驱动头文件

#ifndef __24C256_H
#define __24C256_H

#include "stdint.h"
#include "stm32h7xx_hal.h"

// AT24C256 设备地址(A2/A1/A0 全部接地)
#define AT24C256_ADDR_W  0xA0   // 写地址(7位地址 0x50 << 1 | 0)
#define AT24C256_ADDR_R  0xA1   // 读地址(7位地址 0x50 << 1 | 1)

// 芯片参数
#define AT24C256_PAGE_SIZE    64      // 每页 64 字节
#define AT24C256_TOTAL_SIZE   32768   // 总容量 32KB
#define AT24C256_WRITE_DELAY  5       // 写入后等待 5ms(内部编程时间)

// 初始化函数
void AT24C256_Init(void);

// 单字节操作
uint8_t AT24C256_WriteByte(uint16_t addr, uint8_t data);
uint8_t AT24C256_ReadByte(uint16_t addr, uint8_t *data);

// 多字节操作
uint8_t AT24C256_WritePage(uint16_t addr, uint8_t *data, uint16_t len);
uint8_t AT24C256_ReadBytes(uint16_t addr, uint8_t *data, uint16_t len);

// 安全写入(自动处理跨页写入)
uint8_t AT24C256_Write(uint16_t addr, uint8_t *data, uint16_t len);

// EEPROM 自检
uint8_t AT24C256_SelfTest(void);

#endif

7.3 24c256.c —— 软件模拟 I2C + AT24C256 驱动源文件

#include "24c256.h"
#include "main.h"
#include "stdio.h"

// ========== 软件 I2C 引脚定义(模块内部使用,不对外暴露)==========
#define I2C_SCL_Pin        GPIO_PIN_15
#define I2C_SCL_Port       GPIOD
#define I2C_SDA_Pin        GPIO_PIN_14
#define I2C_SDA_Port       GPIOD

#define SCL_H()  HAL_GPIO_WritePin(I2C_SCL_Port, I2C_SCL_Pin, GPIO_PIN_SET)
#define SCL_L()  HAL_GPIO_WritePin(I2C_SCL_Port, I2C_SCL_Pin, GPIO_PIN_RESET)
#define SDA_H()  HAL_GPIO_WritePin(I2C_SDA_Port, I2C_SDA_Pin, GPIO_PIN_SET)
#define SDA_L()  HAL_GPIO_WritePin(I2C_SDA_Port, I2C_SDA_Pin, GPIO_PIN_RESET)
#define SDA_IN() HAL_GPIO_ReadPin(I2C_SDA_Port, I2C_SDA_Pin)

// 微秒延时函数(来自 main.c,这里用 extern 声明)
extern void delay_us(uint32_t nus);

// ========== 软件 I2C 底层时序函数 ==========

/**
 * @brief I2C 起始信号
 * @note  SCL 高电平时,SDA 从高变低 → "通话开始!"
 */
void I2C_Start(void) {
    SDA_H(); delay_us(5);
    SCL_H(); delay_us(5);
    SDA_L(); delay_us(5);
    SCL_L(); delay_us(5);
}

/**
 * @brief I2C 停止信号
 * @note  SCL 高电平时,SDA 从低变高 → "通话结束!"
 */
void I2C_Stop(void) {
    SDA_L(); delay_us(5);
    SCL_H(); delay_us(5);
    SDA_H(); delay_us(5);
}

/**
 * @brief 发送一个字节,返回 ACK
 * @param data 要发送的 8 位数据
 * @retval 0 = 收到 ACK(从设备应答),1 = 收到 NACK(从设备未应答)
 */
uint8_t I2C_SendByte(uint8_t data) {
    // 发送 8 位数据,从最高位(MSB)开始
    for(uint8_t i = 0; i < 8; i++) {
        if(data & 0x80) SDA_H();   // 当前位为 1 → SDA 高
        else            SDA_L();   // 当前位为 0 → SDA 低
        delay_us(2);
        SCL_H(); delay_us(5);      // 时钟上升沿,从设备读取 SDA
        SCL_L(); delay_us(2);
        data <<= 1;                // 移到下一位
    }

    // 第 9 个时钟:释放 SDA,读取从设备的 ACK
    SDA_H(); delay_us(2);
    SCL_H(); delay_us(5);
    uint8_t ack = SDA_IN();        // SDA 为低 = ACK,为高 = NACK
    SCL_L(); delay_us(2);
    return ack;
}

/**
 * @brief 接收一个字节
 * @param ack_mode 0 = 发送 ACK(继续读),1 = 发送 NACK(停止读)
 * @retval 收到的 8 位数据
 */
uint8_t I2C_RecvByte(uint8_t ack_mode) {
    uint8_t data = 0;
    SDA_H();                       // 释放 SDA

    // 接收 8 位数据
    for(uint8_t i = 0; i < 8; i++) {
        SCL_H(); delay_us(5);      // 时钟上升沿
        data <<= 1;
        if(SDA_IN()) data |= 0x01; // 读取 SDA 电平
        SCL_L(); delay_us(2);
    }

    // 第 9 个时钟:主设备发送 ACK 或 NACK
    if(ack_mode == 0) SDA_L();     // ACK(拉低 SDA)
    else              SDA_H();     // NACK(释放 SDA)
    delay_us(2);
    SCL_H(); delay_us(5);
    SCL_L(); delay_us(2);
    SDA_H();                       // 释放 SDA

    return data;
}

// ========== AT24C256 上层驱动函数 ==========

/**
 * @brief 初始化 AT24C256
 */
void AT24C256_Init(void) {
    SDA_H(); SCL_H();              // 总线空闲状态
    HAL_Delay(10);
    printf("AT24C256 Init OK! (Soft I2C, Addr:0x50, 32KB)\r\n");
}

/**
 * @brief 单字节写入
 * @param addr 内存地址(0x0000 ~ 0x7FFF)
 * @param data 要写入的数据
 * @retval 1 = 成功,0 = 失败
 *
 * 写入流程:
 * START → 设备地址(W) → 内存地址高8位 → 内存地址低8位 → 数据 → STOP
 */
uint8_t AT24C256_WriteByte(uint16_t addr, uint8_t data) {
    I2C_Start();
    if(I2C_SendByte(AT24C256_ADDR_W)) { I2C_Stop(); return 0; }      // 设备地址
    if(I2C_SendByte((uint8_t)(addr >> 8))) { I2C_Stop(); return 0; } // 地址高8位
    if(I2C_SendByte((uint8_t)(addr))) { I2C_Stop(); return 0; }      // 地址低8位
    I2C_SendByte(data);                                                // 数据字节
    I2C_Stop();

    HAL_Delay(AT24C256_WRITE_DELAY);  // 等待 EEPROM 内部编程完成
    return 1;
}

/**
 * @brief 单字节随机读取
 * @param addr 内存地址(0x0000 ~ 0x7FFF)
 * @param data 指向存放读出数据的变量
 * @retval 1 = 成功,0 = 失败
 *
 * 读取流程(两步走):
 * ① 假写:START → 设备地址(W) → 内存地址 → STOP(设置内部地址指针)
 * ② 真读:START → 设备地址(R) → 读取1字节(NACK) → STOP
 */
uint8_t AT24C256_ReadByte(uint16_t addr, uint8_t *data) {
    // 步骤1:假写,设置 EEPROM 内部地址指针
    I2C_Start();
    if(I2C_SendByte(AT24C256_ADDR_W)) { I2C_Stop(); return 0; }
    I2C_SendByte((uint8_t)(addr >> 8));
    I2C_SendByte((uint8_t)(addr));
    I2C_Stop();

    HAL_Delay(1);  // 短暂延时,确保 EEPROM 准备好

    // 步骤2:启动读操作
    I2C_Start();
    if(I2C_SendByte(AT24C256_ADDR_R)) { I2C_Stop(); return 0; }
    *data = I2C_RecvByte(1);     // 只读 1 字节,发送 NACK
    I2C_Stop();
    return 1;
}

/**
 * @brief 页写入(不自动分页,调用者必须保证不跨页!)
 * @param addr 内存地址
 * @param data 数据缓冲区指针
 * @param len  数据长度(1~64 字节,不能跨页)
 * @retval 1 = 成功,0 = 失败
 */
uint8_t AT24C256_WritePage(uint16_t addr, uint8_t *data, uint16_t len) {
    if(len > AT24C256_PAGE_SIZE) return 0;  // 超过一页,拒绝写入

    I2C_Start();
    if(I2C_SendByte(AT24C256_ADDR_W)) { I2C_Stop(); return 0; }
    I2C_SendByte((uint8_t)(addr >> 8));
    I2C_SendByte((uint8_t)(addr));

    // 连续发送数据
    for(uint16_t i = 0; i < len; i++) {
        I2C_SendByte(data[i]);
    }
    I2C_Stop();

    HAL_Delay(AT24C256_WRITE_DELAY);
    return 1;
}

/**
 * @brief 连续字节读取
 * @param addr 内存地址
 * @param data 数据缓冲区指针
 * @param len  读取长度
 * @retval 1 = 成功,0 = 失败
 */
uint8_t AT24C256_ReadBytes(uint16_t addr, uint8_t *data, uint16_t len) {
    // 假写,设置内部地址指针
    I2C_Start();
    if(I2C_SendByte(AT24C256_ADDR_W)) { I2C_Stop(); return 0; }
    I2C_SendByte((uint8_t)(addr >> 8));
    I2C_SendByte((uint8_t)(addr));
    I2C_Stop();

    HAL_Delay(1);

    // 连续读取
    I2C_Start();
    if(I2C_SendByte(AT24C256_ADDR_R)) { I2C_Stop(); return 0; }
    for(uint16_t i = 0; i < len; i++) {
        // 最后一字节发 NACK,前面发 ACK
        data[i] = I2C_RecvByte((i == len - 1) ? 1 : 0);
    }
    I2C_Stop();
    return 1;
}

/**
 * @brief 安全写入(自动分页处理跨页写入)
 * @param addr 内存地址
 * @param data 数据缓冲区指针
 * @param len  写入长度(任意长度,自动分页)
 * @retval 1 = 成功,0 = 失败
 *
 * @note  这是推荐使用的写入函数,自动处理跨页问题
 *        工作原理:将大数据分成多个不大于 64 字节的块,逐页写入
 */
uint8_t AT24C256_Write(uint16_t addr, uint8_t *data, uint16_t len) {
    uint16_t remaining = len;
    uint16_t offset = 0;

    while(remaining > 0) {
        // 计算当前页剩余空间
        uint16_t page_offset = addr % AT24C256_PAGE_SIZE;
        uint16_t space_in_page = AT24C256_PAGE_SIZE - page_offset;
        uint16_t chunk = (remaining < space_in_page) ? remaining : space_in_page;

        // 写入当前块
        if(!AT24C256_WritePage(addr, data + offset, chunk)) {
            printf("Write failed at addr 0x%04X\r\n", addr);
            return 0;
        }

        addr += chunk;
        offset += chunk;
        remaining -= chunk;
    }
    return 1;
}

/**
 * @brief EEPROM 自检:写入测试值 → 读出验证
 * @retval 1 = 通过,0 = 失败
 */
uint8_t AT24C256_SelfTest(void) {
    uint8_t test_val = 0xA5;
    uint8_t read_val = 0;

    printf("EEPROM Self Test...\r\n");

    // 写入测试值到地址 0x0000
    if(!AT24C256_WriteByte(0x0000, test_val)) {
        printf("  Write FAIL!\r\n");
        return 0;
    }

    HAL_Delay(10);  // 确保写入完成

    // 读出对比
    if(!AT24C256_ReadByte(0x0000, &read_val)) {
        printf("  Read FAIL!\r\n");
        return 0;
    }

    if(read_val != test_val) {
        printf("  Verify FAIL! (W:0x%02X, R:0x%02X)\r\n", test_val, read_val);
        return 0;
    }

    printf("  PASS!\r\n");
    return 1;
}

7.4 main.c —— 主函数完整代码

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2026 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* USER CODE BEGIN Includes */
#include <stdio.h>    // 解决printf警告
#include <string.h>
#include "24c256.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

UART_HandleTypeDef huart1;

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MPU_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int __io_putchar(int ch) {
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);  // 使用 USART1
    return ch;
}

// 微秒延时函数(基于 SysTick,同第 8 章)
static uint32_t fac_us = 0;
void delay_us(uint32_t nus) {
    uint32_t ticks;
    uint32_t told, tnow, tcnt = 0;
    uint32_t reload = SysTick->LOAD;
    ticks = nus * fac_us;
    told = SysTick->VAL;
    while(1) {
        tnow = SysTick->VAL;
        if(tnow != told) {
            if(tnow < told) tcnt += told - tnow;
            else tcnt += reload - tnow + told;
            told = tnow;
            if(tcnt >= ticks) break;
        }
    }
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MPU Configuration--------------------------------------------------------*/
  MPU_Config();

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN SysInit */
  fac_us = SystemCoreClock / 1000000;
  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */
  fac_us = SystemCoreClock / 1000000;

     // 初始化 EEPROM 驱动
     AT24C256_Init();

     printf("\r\n===== I2C EEPROM (AT24C256) Demo =====\r\n");
     printf("Mode: Software I2C (GPIO Bit-Bang)\r\n");
     printf("SCL: PD15, SDA: PD14\r\n");
     printf("Device Address: 0x50 (A2=A1=A0=GND)\r\n\r\n");

     // ===== 测试1:上电自检 =====
     printf("--- Test 1: Self Test ---\r\n");
     if(AT24C256_SelfTest()) {
         HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
         HAL_Delay(200);
         HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
     }
     printf("\r\n");

     // ===== 测试2:单字节写入 + 读出验证 =====
     printf("--- Test 2: Single Byte Write/Read ---\r\n");
     AT24C256_WriteByte(0x0010, 0xAB);
     uint8_t read_val = 0;
     AT24C256_ReadByte(0x0010, &read_val);
     printf("Write 0xAB @ addr 0x0010, Read: 0x%02X [%s]\r\n\r\n",
            read_val, (read_val == 0xAB) ? "PASS" : "FAIL");

     // ===== 测试3:页写入 + 连续读取 =====
     printf("--- Test 3: Page Write/Read ---\r\n");
     uint8_t wr_data[16], rd_data[16];
     for(int i = 0; i < 16; i++) wr_data[i] = i * 10 + 1;
     AT24C256_WritePage(0x0020, wr_data, 16);
     HAL_Delay(10);
     AT24C256_ReadBytes(0x0020, rd_data, 16);
     printf("Written: ");
     for(int i = 0; i < 16; i++) printf("%d ", wr_data[i]);
     printf("\r\nRead:    ");
     for(int i = 0; i < 16; i++) printf("%d ", rd_data[i]);
     printf("\r\n\r\n");

     // ===== 测试4:安全跨页写入(自动分页) =====
     printf("--- Test 4: Cross-Page Safe Write ---\r\n");
     uint8_t cross_data[100];
     for(int i = 0; i < 100; i++) cross_data[i] = i;
     AT24C256_Write(0x0030, cross_data, 100);
     printf("Write 100 bytes from addr 0x0030 (auto page-split)\r\n");
     uint8_t verify[20];
     AT24C256_ReadBytes(0x0030, verify, 10);
     AT24C256_ReadBytes(0x008E, verify + 10, 10);
     printf("First 10 bytes: ");
     for(int i = 0; i < 10; i++) printf("%d ", verify[i]);
     printf("\r\nLast  10 bytes: ");
     for(int i = 10; i < 20; i++) printf("%d ", verify[i]);
     printf("\r\n\r\n");

     printf("===== All Tests Done =====\r\n");

     while(1) {
         HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
         HAL_Delay(500);
     }

}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Supply configuration update enable
  */
  HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY);

  /** Configure the main internal regulator output voltage
  */
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE0);

  while(!__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY)) {}

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 5;
  RCC_OscInitStruct.PLL.PLLN = 96;
  RCC_OscInitStruct.PLL.PLLP = 2;
  RCC_OscInitStruct.PLL.PLLQ = 2;
  RCC_OscInitStruct.PLL.PLLR = 2;
  RCC_OscInitStruct.PLL.PLLRGE = RCC_PLL1VCIRANGE_2;
  RCC_OscInitStruct.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE;
  RCC_OscInitStruct.PLL.PLLFRACN = 0;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2
                              |RCC_CLOCKTYPE_D3PCLK1|RCC_CLOCKTYPE_D1PCLK1;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2;
  RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_3) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief USART1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
  huart1.Init.ClockPrescaler = UART_PRESCALER_DIV1;
  huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_UARTEx_SetTxFifoThreshold(&huart1, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_UARTEx_SetRxFifoThreshold(&huart1, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_UARTEx_DisableFifoMode(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  /* USER CODE BEGIN MX_GPIO_Init_1 */

  /* USER CODE END MX_GPIO_Init_1 */

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOH_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOD, EEPROM_SDA_Pin|EEPROM_SCL_Pin, GPIO_PIN_SET);

  /*Configure GPIO pin : LED_Pin */
  GPIO_InitStruct.Pin = LED_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LED_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : EEPROM_SDA_Pin EEPROM_SCL_Pin */
  GPIO_InitStruct.Pin = EEPROM_SDA_Pin|EEPROM_SCL_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

  /*AnalogSwitch Config */
  HAL_SYSCFG_AnalogSwitchConfig(SYSCFG_SWITCH_PA0, SYSCFG_SWITCH_PA0_CLOSE);

  /* USER CODE BEGIN MX_GPIO_Init_2 */

  /* USER CODE END MX_GPIO_Init_2 */
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

 /* MPU Configuration */

void MPU_Config(void)
{
  MPU_Region_InitTypeDef MPU_InitStruct = {0};

  /* Disables the MPU */
  HAL_MPU_Disable();

  /** Initializes and configures the Region and the memory to be protected
  */
  MPU_InitStruct.Enable = MPU_REGION_ENABLE;
  MPU_InitStruct.Number = MPU_REGION_NUMBER0;
  MPU_InitStruct.BaseAddress = 0x0;
  MPU_InitStruct.Size = MPU_REGION_SIZE_4GB;
  MPU_InitStruct.SubRegionDisable = 0x87;
  MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
  MPU_InitStruct.AccessPermission = MPU_REGION_NO_ACCESS;
  MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
  MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
  MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
  MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;

  HAL_MPU_ConfigRegion(&MPU_InitStruct);
  /* Enables the MPU */
  HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

}

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

✨ 八、下载验证(见证奇迹的时刻!)

操作步骤:

  1. 连接 ST-Link 到单片机和电脑(下载程序用)
  2. 连接 USB 转 TTL 模块:PA9 → RX,PA10 → TX,GND → GND
  3. 确认 AT24C256 模块已正确接线(SDA/SCL 有 4.7kΩ 上拉电阻!)
  4. 打开串口调试助手,设置:波特率 115200,数据位 8,停止位 1,无校验
  5. 点击下载按钮,将程序下载到单片机
  6. 观察串口助手输出

预期输出:

===== I2C EEPROM (AT24C256) Demo =====
Mode: Software I2C (GPIO Bit-Bang)
SCL: PD15, SDA: PD14
Device Address: 0x50 (A2=A1=A0=GND)

AT24C256 Init OK! (Soft I2C, Addr:0x50, 32KB)

--- Test 1: Self Test ---
EEPROM Self Test...
  PASS!

--- Test 2: Single Byte Write/Read ---
Write 0xAB @ addr 0x0010, Read: 0xAB [PASS]

--- Test 3: Page Write/Read ---
Written: 1 11 21 31 41 51 61 71 81 91 101 111 121 131 141 151
Read:    1 11 21 31 41 51 61 71 81 91 101 111 121 131 141 151

--- Test 4: Cross-Page Safe Write ---
Write 100 bytes from addr 0x0030 (auto page-split)
First 10 bytes: 0 1 2 3 4 5 6 7 8 9
Last  10 bytes: 90 91 92 93 94 95 96 97 98 99

===== All Tests Done =====

在这里插入图片描述

🎉 恭喜!你已经学会了 STM32 + 软件 I2C + AT24C256 的 EEPROM 读写方案!数据断电不丢失,以后再也不用担心参数丢失了!

📝 九、关键功能总结(新手必背,避免踩坑!)

  1. I2C 本质:两根线(SDA + SCL),主从通信,通过设备地址区分不同芯片
  2. 必须外接上拉电阻:4.7kΩ,STM32 内部 40kΩ 上拉不够,波形会严重变形
  3. GPIO 配置:必须用开漏输出,不能用推挽输出,否则多设备时会短路
  4. 软件 I2C 时序:起始信号(SCL 高时 SDA 变低)、停止信号(SCL 高时 SDA 变高)
  5. ACK 应答:每发一个字节,第 9 个时钟必须释放 SDA 让从设备应答
  6. AT24C256 设备地址:A2/A1/A0 接地 = 写 0xA0,读 0xA1
  7. 页写入限制:每页 64 字节,不可跨页。跨页写入需使用 AT24C256_Write 自动分页
  8. 写入后延时:每次写入后必须等待 5ms(EEPROM 内部编程时间)
  9. 随机读取原理:先"假写"发送内存地址(设置内部地址指针),再启动读操作
  10. 模块化编程:引脚定义、I2C 时序、AT24C256 驱动全部封装在 24c256.c 中,main.c 只需包含 24c256.h

❌ 十、常见问题排查(遇到问题不用慌,对照排查!)

现象 可能原因 排查方法
通信失败,读写全部返回 0 ① 未接 4.7kΩ 上拉电阻
② SDA/SCL 接反
③ EEPROM 模块未供电
用万用表测 SDA 空闲时是否为高电平(3.3V)
SDA 一直为低电平 从设备拉死总线(EEPROM 内部状态异常) 断电重启 EEPROM,或手动发送 9 个时钟脉冲恢复总线
写入后读出全是 0xFF ① 写入未等待 5ms
② WP 引脚接了高电平(写保护)
③ 设备地址错误
检查 WP 是否接地,检查 A2/A1/A0 是否全部接地
跨页写入时部分数据被覆盖 页写入超过 64 字节边界,地址绕回到页开头 改用 AT24C256_Write 函数(自动分页)
串口无输出 ① PA9/PA10 配置错误
② printf 重定向未添加
③ 串口助手参数不匹配
检查 CubeMX 中 USART1 的引脚是否是 PA9/PA10
LED 不闪 ① PA0 引脚配置错误
② 程序卡死在某个 I2C 操作中
检查是否卡在等待 ACK(无上拉电阻时最常见)
编译报错 undefined reference to AT24C256_xxx 24c256.c 未添加到编译链 检查文件是否在 Core/Src 目录下
编译报错 undefined reference to delay_us delay_us 函数未定义或未声明 24c256.c 中添加 extern void delay_us(uint32_t nus);

📢 十一、下篇预告(精彩不容错过!新手必追)

STM32保姆级入门教程|第11章:SPI总线协议详解 + W25Q64 Flash读写

手把手教你掌握 SPI 通信、驱动 W25Q64 大容量 Flash(8MB),实现高速数据存储与读取。学完 EEPROM 再学 Flash,存储方案双剑合璧!

原创不易,创作花费大量时间和精力💦,如果本文对你有帮助,欢迎
点赞👍、收藏⭐、关注➕,有任何问题,评论区留言,我会一一回复!你的支持,就是我持续更新的动力~
本文所使用的工程文件已上传至配套资源中,如有需要可自行下载。也可关注博主后留言获取

🎁 欢迎关注公众号,获取更多技术干货!

博主准备的资料包涵盖了从硬件电路设计STM32单片机开发,再到Linux系统学习的全链路内容,适合不同阶段的学习者。

在这里插入图片描述

📂资料包目录
  • 00-STM32单片机环境搭建
  • 01-硬件电路合集
  • 02-硬件设计开发工具包
  • 03-C语言学习资料包
  • 04-STM32单片机开发工具包
  • 05-STM32传感器模块合集
  • 06-STM32项目合集
  • 07-STM32单片机书籍&芯片手册
  • 08-Linux相关学习资料包

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐