简介:这份资源是面向嵌入式开发初学者与电子工程师的EEPROM 25LC256读写源码包,采用C++实现SPI模拟读写操作,帮助读者理解非易失性存储器的通信原理与驱动编写方法。源码围绕SPI接口初始化、片选控制、命令与地址传输、读写使能、事务管理及错误处理等关键环节展开,可作为配置参数保存、设备固件存储等场景的参考模板。压缩包共20个文件,约38KB,包含c与h源文件、obj目标文件、lst列表文件、hex烧录文件以及Keil工程相关配置,覆盖从源码到编译产物的完整链路,便于直接导入调试。目前已有1259人学习下载,适合希望掌握SPI协议实践、积累底层驱动经验的开发者参考借鉴。
1. 拿到一份 25LC256 读写源码,先别急着编译
如果你手头有一块带 25LC256 的板子,或者正在调一个需要掉电保存参数的嵌入式项目,这份 C++ 源码大概率能让你少熬两个晚上。25LC256 是 Microchip 那类 256Kbit 的 SPI EEPROM,32KB 容量、页写 64 字节、支持 10MHz 时钟,工控板、采集器、老设备参数区里到处都是它。这份源码的核心价值在于:它不依赖芯片厂商的 HAL 库,而是用普通 GPIO 模拟 SPI 时序,把读写、擦除、状态轮询全流程都手写了一遍。这意味着你把它移植到任何一块能点灯的 MCU 上,改几个引脚宏就能跑。适合谁?适合正在用模拟 SPI 驱动 EEPROM、又不想被硬件 SPI 外设坑到的嵌入式开发者,也适合想搞懂 SPI 时序底层逻辑的新手。
2. 项目定位与技术选型:为什么用模拟 SPI 而不是硬件外设
2.1 模拟 SPI 的适用场景
硬件 SPI 外设当然快,但嵌入式项目里经常遇到几个尴尬:目标引脚被其他外设占用了、MCU 的 SPI 数量不够、或者硬件 SPI 的时序配置在某些老芯片上有玄学问题。模拟 SPI 的好处是引脚随便挑,时序完全可控,调试时拿逻辑分析仪一抓就能看到每一位的翻转。这份源码选择模拟 SPI,本质上是为了可移植性和可调试性。代价是速率会低一些,但对于 25LC256 这种参数存储场景,一次写一页 64 字节,慢个几十微秒根本不影响业务。
2.2 25LC256 的关键特性
25LC256 的存储组织是 32KB,分成 512 页,每页 64 字节。它支持字节写和页写,页写一次最多 64 字节,跨页写会回卷覆盖,这是新手最容易翻车的地方。指令集包括 READ、WRITE、WREN、WRDI、RDSR、WRSR 等,所有操作都围绕状态寄存器(SR)展开。状态寄存器的 WIP 位表示写进行中,WEL 位表示写使能锁存。源码里必须有一个轮询 WIP 的等待函数,否则连续写会丢数据。
2.3 源码的整体结构
这份源码通常包含三个部分:底层 GPIO 操作、SPI 时序模拟、25LC256 指令封装。底层 GPIO 负责拉高拉低 CS、SCK、MOSI,读 MISO;SPI 时序层实现一个字节的收发;指令层则组合出读状态、写使能、读数据、写数据、擦除等操作。下面是一个典型的目录结构:
eeprom_25lc256/ ├── inc/ │ ├── soft_spi.h # 模拟 SPI 引脚定义与函数声明 │ └── eeprom_25lc256.h # EEPROM 指令与 API 声明 ├── src/ │ ├── soft_spi.cpp # SPI 时序模拟实现 │ ├── eeprom_25lc256.cpp# EEPROM 读写实现 │ └── main.cpp # 测试用例 └── README.md这种分层的好处是,换 MCU 时只需要改 soft_spi.cpp 里的 GPIO 操作,EEPROM 逻辑一行不用动。
3. 核心模块拆解:从 GPIO 翻转到一页数据写入
3.1 模拟 SPI 的字节收发
模拟 SPI 的本质就是按 CPOL 和 CPHA 约定,在 SCK 的边沿采样或输出数据。25LC256 支持 SPI 模式 0(CPOL=0,CPHA=0),即空闲时 SCK 低电平,数据在 SCK 上升沿采样。下面是一个典型的字节收发函数:
// soft_spi.cpp uint8_t SoftSpi_TransferByte(uint8_t txData) { uint8_t rxData = 0; for (int i = 7; i >= 0; i--) { // 在 SCK 低电平时准备 MOSI if (txData & (1 << i)) { MOSI_HIGH(); } else { MOSI_LOW(); } // 短暂延时,满足建立时间 SoftSpi_Delay(); SCK_HIGH(); // 上升沿,从机采样 MOSI SoftSpi_Delay(); // 主机在上升沿后采样 MISO if (MISO_READ()) { rxData |= (1 << i); } SCK_LOW(); // 下降沿,准备下一位 SoftSpi_Delay(); } return rxData; }逻辑说明:循环从最高位开始,先设置 MOSI,再拉高 SCK,此时从机采样 MOSI,同时主机读取 MISO。参数说明:txData 是要发送的字节,返回值是从机返回的字节。SoftSpi_Delay 的延时长度取决于 MCU 主频和 25LC256 的时序要求,通常几十纳秒到几百纳秒即可,太快可能导致从机来不及响应。
3.2 25LC256 的指令封装
25LC256 的所有操作都以拉低 CS 开始,发送指令码,然后跟地址或数据。下面是一个读状态寄存器的函数:
// eeprom_25lc256.cpp uint8_t EEPROM_ReadStatus(void) { uint8_t status; CS_LOW(); // 选中芯片 SoftSpi_TransferByte(0x05); // RDSR 指令 status = SoftSpi_TransferByte(0xFF); // 读回状态 CS_HIGH(); // 释放芯片 return status; }逻辑说明:0x05 是 RDSR 指令码,发送后从机返回状态寄存器值。参数说明:无入参,返回状态字节。注意 CS 拉高后必须给一个短暂延时,让芯片完成内部操作。
3.3 写使能与写等待
25LC256 在每次写操作前必须先发 WREN 指令,否则写操作会被忽略。写完之后要轮询 WIP 位,直到写完成。这是最容易被忽略的一步:
void EEPROM_WriteEnable(void) { CS_LOW(); SoftSpi_TransferByte(0x06); // WREN 指令 CS_HIGH(); } void EEPROM_WaitWriteComplete(void) { while (EEPROM_ReadStatus() & 0x01) { // WIP 位为 1 表示写进行中,等待 SoftSpi_Delay(); } }逻辑说明:WREN 指令置位 WEL,允许后续写操作。WaitWriteComplete 轮询状态寄存器最低位,直到 WIP 清零。参数说明:无。常见做法是在每次写字节或写页之后都调用一次等待函数,否则连续写会丢数据。
3.4 页写与地址对齐
页写是 25LC256 效率最高的写入方式,一次最多 64 字节。但地址必须对齐到页边界,否则会回卷覆盖。下面是一个页写函数:
void EEPROM_WritePage(uint16_t addr, const uint8_t *data, uint16_t len) { if (len > 64) len = 64; // 单页最多 64 字节 EEPROM_WriteEnable(); CS_LOW(); SoftSpi_TransferByte(0x02); // WRITE 指令 SoftSpi_TransferByte((addr >> 8) & 0xFF); // 地址高字节 SoftSpi_TransferByte(addr & 0xFF); // 地址低字节 for (uint16_t i = 0; i < len; i++) { SoftSpi_TransferByte(data[i]); } CS_HIGH(); EEPROM_WaitWriteComplete(); }逻辑说明:先发 WREN,再发 WRITE 指令和 16 位地址,然后连续发送数据。参数说明:addr 是起始地址,data 是数据指针,len 是长度。注意如果 addr 不是 64 的倍数,且 len 跨页,数据会从页尾回卷到页首,覆盖原有数据。我一般会在上层做地址对齐检查。
4. 编译与运行:从裸机到 Linux 模拟测试
4.1 裸机环境下的移植步骤
如果你在 STM32 或类似 MCU 上跑,移植步骤很简单:把 soft_spi.h 里的引脚宏改成你的 GPIO 操作,比如 HAL_GPIO_WritePin。然后把 soft_spi.cpp 和 eeprom_25lc256.cpp 加入工程,在 main 里调用测试函数。常见做法是先用一个已知数据写入,再读出来比对,确认时序正确。
4.2 Linux 下的模拟测试
没有硬件也能验证逻辑。你可以用文件模拟 EEPROM 存储,把 SoftSpi_TransferByte 替换成对文件的读写,这样就能在 PC 上跑单元测试。下面是一个简单的模拟层:
// 模拟 EEPROM 存储 static uint8_t eeprom_mem[32768]; uint8_t SoftSpi_TransferByte(uint8_t txData) { static uint16_t addr = 0; // 简化模拟:根据指令返回数据 // 实际测试时需要维护状态机 return eeprom_mem[addr++]; }逻辑说明:这只是一个示意,真实模拟需要维护指令状态机。参数说明:txData 是主机发送的字节,返回值是从机响应。这种方式适合在 CI 里跑回归测试,确保 EEPROM 逻辑没有回归。
4.3 编译命令与依赖
这份源码通常只依赖标准 C++ 库,不需要额外依赖。裸机下用你的 IDE 或 Makefile 编译即可。Linux 下可以用 g++ 直接编译:
g++ -std=c++11 -Iinc src/*.cpp -o eeprom_test ./eeprom_test注意:如果用到 printf 调试,需要包含 cstdio。裸机下可能需要重定向 printf 到串口。
5. 避坑与常见问题排查
5.1 读出来全是 0xFF 或 0x00
现象:上电后读 EEPROM,数据全是 0xFF 或 0x00。原因:CS 引脚没有正确拉低,或者 SPI 模式不匹配。25LC256 是模式 0,如果主机用模式 3,采样边沿反了,数据全错。解决:用逻辑分析仪抓 CS、SCK、MOSI 三根线,确认 CS 在传输期间保持低电平,SCK 空闲为低,数据在上升沿稳定。
5.2 写入后读出来没变
现象:调用写函数后,读回数据还是旧值。原因:没有发 WREN 指令,或者写完之后没有等待 WIP 清零。25LC256 要求每次写之前必须 WREN,写完之后芯片内部需要几毫秒擦写,期间不响应新指令。解决:在写函数里强制加 WREN 和 WaitWriteComplete,不要依赖上层调用。
5.3 跨页写导致数据覆盖
现象:写 100 字节数据,前 64 字节正确,后面数据覆盖了页首。原因:25LC256 页写不能跨页,地址到页尾后回卷到页首。解决:上层按页拆分,每 64 字节调用一次页写,或者改用字节写(效率低但安全)。
5.4 延时不够导致时序错误
现象:低速 MCU 上正常,高速 MCU 上读写不稳定。原因:模拟 SPI 的延时太短,25LC256 的建立时间和保持时间不满足。解决:在 SCK 翻转前后加延时,具体值查 25LC256 数据手册的时序参数,通常 tSU 和 tHD 在几十纳秒级别。
5.5 电源波动导致写失败
现象:偶尔写失败,重启后数据丢失。原因:EEPROM 写操作期间电源波动,导致内部电荷泵异常。解决:在 VCC 和 GND 之间加 0.1uF 去耦电容,写操作期间避免大电流外设动作。
6. 二次开发:把这份源码改造成你的参数管理模块
这份源码最值钱的地方不是它实现了 25LC256 的读写,而是它给你提供了一个可移植的模拟 SPI 框架。你可以在此基础上做几件事:第一,加一层参数管理,把结构体序列化后写入 EEPROM,上电时反序列化;第二,加掉电保护,在写之前先写一个标志位,写完清除,上电时检查标志位判断数据是否完整;第三,加磨损均衡,虽然 25LC256 擦写寿命有 100 万次,但频繁写同一地址还是会提前报废,可以做一个简单的轮询写入。
下面是一个参数管理的示例:
struct DeviceConfig { uint32_t magic; uint16_t sample_rate; uint8_t mode; uint8_t reserved; uint16_t crc; }; void SaveConfig(const DeviceConfig &cfg) { // 先写标志位,表示正在写入 uint8_t flag = 0xAA; EEPROM_WritePage(0x0000, &flag, 1); // 写入配置数据 EEPROM_WritePage(0x0001, (uint8_t*)&cfg, sizeof(cfg)); // 清除标志位 flag = 0x00; EEPROM_WritePage(0x0000, &flag, 1); } bool LoadConfig(DeviceConfig &cfg) { uint8_t flag; EEPROM_Read(0x0000, &flag, 1); if (flag != 0x00) { return false; // 上次写入未完成 } EEPROM_Read(0x0001, (uint8_t*)&cfg, sizeof(cfg)); // 校验 magic 和 crc if (cfg.magic != 0xDEADBEEF) return false; // 计算 crc 并比对 return true; }逻辑说明:SaveConfig 先写标志位,再写数据,最后清标志位。LoadConfig 先检查标志位,再读数据并校验。参数说明:DeviceConfig 是自定义结构体,magic 用于识别有效数据,crc 用于校验完整性。注意结构体要紧凑,避免编译器填充导致大小不一致。
验证方法:写一个测试用例,反复写入不同配置,断电重启后读取,确认数据一致。我一般会跑 1000 次随机写入,观察是否有失败。从那以后我每次改 EEPROM 相关代码,都强制走一遍「写标志位 → 写数据 → 清标志位」的流程,再也不敢省这一步。希望帮到你。
本文还有配套的精品资源,点击获取