STM32保姆级入门教程|第10章:I2C总线协议详解 + EEPROM读写实战(手把手避坑,模块化封装)
**摘要:STM32 I2C驱动EEPROM实战指南 本文详细讲解如何使用STM32CubeIDE通过I2C总线驱动AT24C256 EEPROM芯片,实现数据断电保存功能。内容涵盖: I2C协议原理与硬件接线要点(必须外接4.7kΩ上拉电阻) AT24C256设备地址计算(固定0x50)与操作时序 STM32CubeIDE配置I2C外设(PD14/PD15引脚) 模块化编程实现(封装为24c25
原创 ✍️ | 新手零门槛,全程新建工程,手把手不踩坑!
文章标签:#stm32 #stm32cubeide #I2C #EEPROM #AT24C256 #模块化编程 #嵌入式存储 #HAL库
系列前置博客(必看!否则跟不上哦😜):
💡 一、前言(为什么学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.c 和 24c256.h 文件——这是你在工作中一定会用到的工程技能。本章基于 STM32CubeIDE 1.19.0 + STM32H723ZGT6 编写(由于前面的某种原因,芯片升级了🐶),但 I2C 协议是通用的,F1/F4/H7 系列都能直接抄!
本章我把I2C协议原理、开漏输出与上拉电阻、设备地址计算、AT24C256页写入限制、模块化封装、跨页安全写入全部讲到最细,全程用"大白话+表情包+一步一截图",新手看完不仅能学会,还能直接移植到自己的项目里,爽到飞起!
🎯 二、本章核心功能目标(清晰明确,学完不迷茫)
- 搞懂 ✅ I2C总线协议的本质(两根线怎么传数据?SDA和SCL各干什么?)
- 掌握 ✅ 开漏输出 + 上拉电阻的硬件原理(为什么必须外接4.7kΩ电阻?)
- 学会 ✅ I2C的设备地址概念(一条总线上挂多个芯片,怎么区分?)
- 掌握 ✅ AT24C256的操作时序(单字节写、页写入、随机读取、连续读取)
- 学会 ✅ CubeIDE 配置 I2C 的方法(H723 的 I2C4,PD14/PD15)
- 掌握 ✅ 模块化编程(把 EEPROM 驱动封装成
24c256.c/h,方便移植) - 实现 ✅ :
- 单字节写入与读出验证(写入0xAB,读出0xAB)
- 连续多字节页写入(最多 64 字节/页)
- 跨页安全写入(自动分页,数据不覆盖)
- 上电自动读取 EEPROM 并校验数据
- LED 指示读写状态
- 理解 ✅ 为什么 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_Transmit 和 HAL_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:新建工程
- 打开 STM32CubeIDE,点击
Start a new STM32 project - 搜索芯片型号
STM32H723ZGT6,选中后点击Next - 工程名称:
I2C_EEPROM_Demo→Finish
步骤2:配置 SCL 和 SDA 引脚(通用 GPIO 输出模式)
PD15 作为 SCL(时钟线),PD14 作为 SDA(数据线),都配置为开漏输出:
- 在右侧芯片引脚图中,找到 PD15
- 右键点击 PD15 → 选择
GPIO_Output - 找到 PD14
- 右键点击 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
- 左侧点击
Connectivity→USART1 - Mode 选择
Asynchronous - 参数设置:Baud Rate = 115200,Word Length = 8 Bits,Parity = None,Stop Bits = 1
- 确认引脚:PA9 = USART1_TX,PA10 = USART1_RX
如果引脚不对可以在引脚处,重新选择串口1 如下图。

步骤5:配置 LED 引脚(用于状态指示)
- 在引脚图中找到 PA0
- 配置为
GPIO_Output - 参数:推挽输出、无上下拉、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:新建工程
- 打开 STM32CubeIDE,点击
Start a new STM32 project - 搜索芯片型号
STM32H723ZGT6,选中后点击Next - 填写工程名称:
I2C_EEPROM_Demo,选择保存路径,点击Finish
步骤2:配置 SCL 和 SDA 引脚(通用 GPIO 输出模式)
PD15 作为 SCL(时钟线),PD14 作为 SDA(数据线),都配置为开漏输出:
- 在右侧芯片引脚图中,找到 PD15
- 右键点击 PD15 → 选择
GPIO_Output - 找到 PD14
- 右键点击 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
- 左侧点击
Connectivity→USART1 - Mode 选择
Asynchronous - 参数设置:Baud Rate = 115200,Word Length = 8 Bits,Parity = None,Stop Bits = 1
- 确认引脚:PA9 = USART1_TX,PA10 = USART1_RX
步骤5:配置 LED 引脚(用于状态指示)
- 在引脚图中找到 PA0
- 配置为
GPIO_Output - 参数:推挽输出、无上下拉、Low Speed、别名设为
LED
步骤6:生成代码
点击右上角 GENERATE CODE。
💻 七、代码功能详解(逐行拆解,新手也能懂)
✅ 本章重点学习软件模拟 I2C 时序和模块化编程。我们将 I2C 底层时序和 AT24C256 驱动都封装成独立的
24c256.c/h文件。引脚定义全部放在24c256.c中,做到模块自包含——main.c只需包含头文件,不用关心引脚细节。
7.1 新建 EEPROM 驱动模块
在 STM32CubeIDE 中:
- 右键
Core/Src→New→Source File→ 文件名:24c256.c - 右键
Core/Inc→New→Header 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 */
✨ 八、下载验证(见证奇迹的时刻!)
操作步骤:
- 连接 ST-Link 到单片机和电脑(下载程序用)
- 连接 USB 转 TTL 模块:PA9 → RX,PA10 → TX,GND → GND
- 确认 AT24C256 模块已正确接线(SDA/SCL 有 4.7kΩ 上拉电阻!)
- 打开串口调试助手,设置:波特率 115200,数据位 8,停止位 1,无校验
- 点击下载按钮,将程序下载到单片机
- 观察串口助手输出
预期输出:
===== 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 读写方案!数据断电不丢失,以后再也不用担心参数丢失了!
📝 九、关键功能总结(新手必背,避免踩坑!)
- I2C 本质:两根线(SDA + SCL),主从通信,通过设备地址区分不同芯片
- 必须外接上拉电阻:4.7kΩ,STM32 内部 40kΩ 上拉不够,波形会严重变形
- GPIO 配置:必须用开漏输出,不能用推挽输出,否则多设备时会短路
- 软件 I2C 时序:起始信号(SCL 高时 SDA 变低)、停止信号(SCL 高时 SDA 变高)
- ACK 应答:每发一个字节,第 9 个时钟必须释放 SDA 让从设备应答
- AT24C256 设备地址:A2/A1/A0 接地 = 写 0xA0,读 0xA1
- 页写入限制:每页 64 字节,不可跨页。跨页写入需使用
AT24C256_Write自动分页 - 写入后延时:每次写入后必须等待 5ms(EEPROM 内部编程时间)
- 随机读取原理:先"假写"发送内存地址(设置内部地址指针),再启动读操作
- 模块化编程:引脚定义、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相关学习资料包

更多推荐





所有评论(0)