简介这份资源面向使用STM32F103系列单片机进行嵌入式开发的工程师与学习者提供通过SPI总线读写FM25CL64铁电存储器的完整驱动程序代码可解决项目中掉电数据保存、参数存储等场景下的底层驱动移植问题。压缩包内共2个文件包含1个c源文件与1个h头文件分别承载SPI初始化、字节读写等核心函数实现与接口声明整体体积约2KB结构精简便于直接嵌入现有工程。资源源自2013年的实践代码适用于fm25系列铁电存储器的读写操作支持直接读写一个字节代码中已封装SPI_Flash_Init等初始化流程涵盖GPIO与SPI外设时钟配置。目前已有1712人学习下载读者可借此快速理解SPI时序配置与铁电存储器指令交互方式省去从零调试底层通信的时间适合作为嵌入式存储驱动开发的参考范例。1. STM32F103 挂 FM25CL64为什么 SPI 铁电存储器值得你花一个下午调通如果你手头有一块 STM32F103 最小系统板想给它加一块掉电不丢、写入不用等、擦写寿命几乎用不完的小容量存储FM25CL64 这类 SPI 铁电存储器FRAM是很务实的选择。它和 EEPROM、NOR Flash 最大的区别在于写入前不需要擦除字节级写入几乎没有延迟写一次就是一次不用等内部写周期也不存在“擦写次数用光就报废”的焦虑。很多做工业采集、参数保存、掉电计数、黑匣子日志的场景最后都会落到这颗 8KB 的 SPI FRAM 上。这篇笔记围绕“STM32F103 单片机 SPI 读写 FM25CL64 铁电存储器驱动程序代码”这个主题把硬件连接、SPI 模式选择、寄存器命令、读写函数、页写边界、状态轮询和常见翻车点讲清楚。目标很直接你照着接线、改几个宏、把驱动文件加进标准库工程就能在自己的板子上读写 FM25CL64并且知道哪些参数不能乱设、哪些坑我踩过。适合正在用 STM32F103 标准库或 HAL 库做存储扩展的嵌入式工程师也适合刚接触 SPI 时序、想找一个真实器件练手的新手。2. FM25CL64 的 SPI 时序与 STM32F103 硬件 SPI 配置2.1 先看器件手册里真正影响驱动的几个参数FM25CL64 是 64Kbit也就是 8KByte 的 FRAMSPI 接口支持最高 20MHz 时钟。它的命令集不复杂但有几个点如果一开始没注意后面读出来全是 0xFF 或者随机值。核心命令包括WREN0x06写使能、WRDI0x04写禁止、RDSR0x05读状态寄存器、WRSR0x01写状态寄存器、READ0x03读数据、WRITE0x02写数据。状态寄存器里最需要关心的是 WEL 位和 BP 位WEL 表示写使能锁存BP 是块保护。SPI 模式方面FM25CL64 支持 SPI Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1。我一般用 Mode 0因为 STM32F103 的硬件 SPI 在 Mode 0 下最稳示波器抓波形也直观。数据在 SCK 上升沿采样下降沿移出MSB First。片选 CS 必须由软件控制因为每次命令序列前后都要拉低拉高硬件 NSS 在很多标准库工程里反而添乱。注意FM25CL64 的 WRITE 命令后面跟 2 字节地址地址高 3 位是无关位实际有效地址是 A12~A0对应 8KB 空间。地址不要左移直接按 16 位发。2.2 STM32F103 硬件 SPI 初始化标准库版本下面这段代码基于 STM32F103 标准库 V3.5.0SPI1 挂在 PA5SCK、PA6MISO、PA7MOSICS 用 PA4 软件控制。如果你用的是 SPI2改一下 GPIO 和 SPI 宏即可。关键参数预分频先设 8得到 9MHz 左右调通后再往上提时钟极性、相位按 Mode 0数据宽度 8 位MSB First软件 NSS。#include stm32f10x.h #define FM25_CS_PORT GPIOA #define FM25_CS_PIN GPIO_Pin_4 void FM25_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* SCK PA5, MOSI PA7 复用推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* MISO PA6 浮空输入 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); /* CS PA4 推挽输出默认拉高 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(FM25_CS_PORT, FM25_CS_PIN); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; /* Mode 0 */ SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }逻辑说明先开 GPIOA 和 SPI1 时钟配置 SCK、MOSI 为复用推挽MISO 为浮空输入CS 为普通推挽输出。SPI 参数里 CPOLLow、CPHA1Edge 对应 Mode 0预分频 8 在 72MHz 主频下得到 9MHz低于 FM25CL64 的 20MHz 上限留了余量。NSS 设为软件模式片选完全由 FM25_CS 宏控制。参数怎么改如果通信不稳定先把预分频改成 16 或 32降速验证如果示波器看到 SCK 空闲电平不对检查 CPOL如果数据错位检查 CPHA 和 FirstBit。CS 的拉低拉高必须包住整个命令序列不能中间断开。2.3 字节收发函数别小看这个轮询硬件 SPI 收发一个字节核心是等 TXE 和 RXNE。下面这个函数同时用于发命令、发地址、发数据和读数据。注意读的时候也要发一个 dummy 字节0xFF来产生时钟。uint8_t FM25_SPI_RW(uint8_t txData) { /* 等发送缓冲区空 */ while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, txData); /* 等接收缓冲区非空 */ while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); }逻辑说明先等 TXE 置位把数据写进 DR然后等 RXNE 置位读 DR。这个函数没有超时实际工程里建议加一个循环计数防止 SPI 外设异常时死等。参数方面txData 在只读场景下发 0xFF 即可FM25CL64 会忽略 MOSI 上的数据只把内部数据移出到 MISO。3. FM25CL64 驱动代码写使能、读状态、页写与地址边界3.1 写使能、读状态和写禁止每次写之前都要做FRAM 虽然写入不需要擦除但 WRITE 命令之前必须发 WREN否则数据写不进去。这是新手最容易漏的一步。写完之后 WEL 位会自动清零不需要手动 WRDI但显式发一次也没坏处。状态寄存器读取可以用来确认 WEL 是否真的置上了。#define FM25_CMD_WREN 0x06 #define FM25_CMD_WRDI 0x04 #define FM25_CMD_RDSR 0x05 #define FM25_CMD_WRSR 0x01 #define FM25_CMD_READ 0x03 #define FM25_CMD_WRITE 0x02 void FM25_CS_Low(void) { GPIO_ResetBits(FM25_CS_PORT, FM25_CS_PIN); } void FM25_CS_High(void) { GPIO_SetBits(FM25_CS_PORT, FM25_CS_PIN); } void FM25_WriteEnable(void) { FM25_CS_Low(); FM25_SPI_RW(FM25_CMD_WREN); FM25_CS_High(); } uint8_t FM25_ReadStatus(void) { uint8_t status; FM25_CS_Low(); FM25_SPI_RW(FM25_CMD_RDSR); status FM25_SPI_RW(0xFF); FM25_CS_High(); return status; }逻辑说明WREN 是单字节命令CS 拉低、发 0x06、CS 拉高WEL 位就置上了。RDSR 先发 0x05再发 dummy 读回状态。状态寄存器 bit1 是 WELbit0 是 WIPFRAM 没有写等待WIP 基本不置位。参数上WREN 之后不要插入其他命令直接跟 WRITE。3.2 单字节读写和页写地址自动递增但页边界要自己管FM25CL64 的 READ 和 WRITE 命令后面跟 2 字节地址然后数据连续移入移出内部地址自动递增。写操作可以跨页但跨页时如果 SPI 时钟太快或者电源不稳容易在页边界处出错。稳妥做法是按页拆分每页 32 字节FM25CL64 页大小是 32 字节。下面给出单字节写、单字节读和页写函数。void FM25_WriteByte(uint16_t addr, uint8_t data) { FM25_WriteEnable(); FM25_CS_Low(); FM25_SPI_RW(FM25_CMD_WRITE); FM25_SPI_RW((uint8_t)(addr 8)); /* 地址高字节 */ FM25_SPI_RW((uint8_t)(addr 0xFF)); /* 地址低字节 */ FM25_SPI_RW(data); FM25_CS_High(); } uint8_t FM25_ReadByte(uint16_t addr) { uint8_t data; FM25_CS_Low(); FM25_SPI_RW(FM25_CMD_READ); FM25_SPI_RW((uint8_t)(addr 8)); FM25_SPI_RW((uint8_t)(addr 0xFF)); data FM25_SPI_RW(0xFF); FM25_CS_High(); return data; } void FM25_WritePage(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t i; FM25_WriteEnable(); FM25_CS_Low(); FM25_SPI_RW(FM25_CMD_WRITE); FM25_SPI_RW((uint8_t)(addr 8)); FM25_SPI_RW((uint8_t)(addr 0xFF)); for (i 0; i len; i) { FM25_SPI_RW(buf[i]); } FM25_CS_High(); }逻辑说明写字节和页写的区别只是数据长度。地址发 16 位高字节在前。读字节时发完地址后发 0xFF 产生时钟。页写函数没有做页边界检查调用者要保证 addr 和 addrlen 不跨 32 字节页或者自己在函数里拆。参数上len 不要超过 32超过就拆成多次调用。提示如果你要写一个结构体先算好大小按页对齐存放。比如参数区从 0x0000 开始日志区从 0x1000 开始避免互相覆盖。3.3 连续读写和跨页处理一个实用的封装实际项目里更常见的是写一个任意长度的缓冲区。下面这个函数按页拆分自动处理跨页调用者不用关心边界。读函数可以一次读完FRAM 读没有页限制。void FM25_WriteBuffer(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t pageRemain; while (len 0) { pageRemain 32 - (addr % 32); /* 当前页剩余空间 */ if (pageRemain len) pageRemain len; FM25_WritePage(addr, buf, pageRemain); addr pageRemain; buf pageRemain; len - pageRemain; } } void FM25_ReadBuffer(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t i; FM25_CS_Low(); FM25_SPI_RW(FM25_CMD_READ); FM25_SPI_RW((uint8_t)(addr 8)); FM25_SPI_RW((uint8_t)(addr 0xFF)); for (i 0; i len; i) { buf[i] FM25_SPI_RW(0xFF); } FM25_CS_High(); }逻辑说明写缓冲区时先算当前地址在页内还剩多少字节取剩余和 len 的较小值写一页然后地址、指针、长度往前推。这样无论从哪个地址开始写都不会跨页。读缓冲区一次 CS 拉低读完地址自动递增8KB 空间内不会回绕。参数说明addr 范围 0x0000~0x1FFFlen 不要超过 0x2000 - addr。如果传入非法地址函数不会报错但会写到回绕后的位置所以调用前自己检查。4. 避坑与排查FM25CL64 调不通时先看这 5 条4.1 读出来全是 0xFF 或 0x00现象上电后读任意地址返回全是 0xFF 或 0x00写进去再读还是不对。原因CS 没有正确拉低或者 SPI 模式不对或者 MISO 没配置成输入。解决先用示波器看 CS、SCK、MOSI 三根线CS 在命令期间必须是低确认 CPOL/CPHA 是 Mode 0检查 MISO 引脚是不是被其他外设占用STM32F103 的 PA6 默认是 SPI1_MISO但如果重映射了就要改。4.2 写进去读出来是旧值现象调用 FM25_WriteByte 后立刻读数据没变。原因WREN 没发或者 WREN 和 WRITE 之间插了其他 SPI 操作导致 WEL 被清。解决每次写之前单独发 WREN发完立刻发 WRITE中间不要读状态、不要操作其他 SPI 器件。如果总线上挂了多个 SPI 从机CS 必须严格分开。4.3 跨页写数据错乱现象写 40 字节前 32 字节对后面错位或丢失。原因FM25CL64 页大小 32 字节跨页写时内部地址回绕到页首覆盖了前面的数据。解决用 FM25_WriteBuffer 按页拆分或者手动保证每次写不超过页边界。这个坑在写结构体时特别常见因为结构体大小往往不是 32 的整数倍。4.4 SPI 时钟太快导致偶发错误现象低速能读写提高到 18MHz 后偶尔读错。原因杜邦线太长、没有加匹配电阻、电源去耦不足。解决先把预分频降到 16 或 32确认功能正常后再逐步提高SCK 走线尽量短FM25CL64 的 VCC 旁边放 0.1uF 去耦电容如果板子上有其他 SPI 器件降低整体时钟。4.5 状态寄存器 BP 位被误写导致写保护现象某次写状态寄存器后所有写操作都失效。原因WRSR 命令把 BP 位设上了块保护生效。解决读状态寄存器确认 BP 位发 WRSR 写 0x00 清除保护。注意 WRSR 之前也要 WREN。如果不需要块保护初始化时不要碰状态寄存器。5. 进阶技巧用 FM25CL64 做掉电计数和参数双备份调通基本读写之后FM25CL64 真正好用的地方在于掉电保存和频繁写入。我一般会拿它做两件事一是掉电计数每次上电读一个 32 位计数器加一写回FRAM 写次数几乎无限不用做磨损均衡二是参数双备份把同一份参数写两个地址读的时候校验和对比哪个有效用哪个。下面是一个掉电计数的例子计数器放在 0x0000占 4 字节小端存放。每次上电调用 FM25_IncPowerCount返回当前次数。uint32_t FM25_ReadPowerCount(void) { uint8_t buf[4]; FM25_ReadBuffer(0x0000, buf, 4); return ((uint32_t)buf[3] 24) | ((uint32_t)buf[2] 16) | ((uint32_t)buf[1] 8) | (uint32_t)buf[0]; } uint32_t FM25_IncPowerCount(void) { uint32_t cnt FM25_ReadPowerCount(); uint8_t buf[4]; cnt; buf[0] (uint8_t)(cnt 0xFF); buf[1] (uint8_t)((cnt 8) 0xFF); buf[2] (uint8_t)((cnt 16) 0xFF); buf[3] (uint8_t)((cnt 24) 0xFF); FM25_WriteBuffer(0x0000, buf, 4); return cnt; }逻辑说明读计数器用 FM25_ReadBuffer一次 CS 拉低读完 4 字节。写的时候先读再加一然后按小端拆成 4 字节写回。因为 FRAM 写入没有延迟这个函数可以在主循环里频繁调用不用担心寿命。参数上计数器地址 0x0000 到 0x0003后面 0x0004 开始可以放其他参数。验证方法写一个测试循环连续写 10000 次计数器每次读回对比看是否一致。再用示波器看 CS 和 SCK确认每次写都有 WREN 和 WRITE 两个命令序列。如果条件允许做一次快速断电测试上电后读计数器确认数值没有丢失。我自己的习惯是任何用到 FM25CL64 的板子第一版驱动调通后先跑 24 小时连续读写测试每秒钟写一次第二天读回来对比。这个测试能暴露电源、时序和页边界的大部分问题。血泪经验是不要等到产品现场掉电丢参数了才回来查 FRAM 驱动那时候成本就高了。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
用OpenClaw和微信打造手机远程控制终端 1. 项目概述:当手机变身专业控制终端在智能设备普及的今天,我们口袋里的智能手机早已不再是简单的通讯工具。作为一名长期混迹极客圈的技术爱好者,最近成功实现了用普通安卓手机通过OpenClaw框架对接微信接口,打造出一个功能完备的… · 2026/11/3 16:50:32
拼多多商品API对接实战:从权限申请到数据采集全流程 1. 项目背景与价值解析 最近在对接电商数据时,我发现很多企业都需要批量获取拼多多平台上的商品信息用于价格监控、竞品分析或库存管理。官方提供的"获取店铺所有商品列表"API正好能满足这个需求,但实际对接过程中发现文档存在不少细节缺失。经… · 2026/11/3 16:50:32
SpringBoot智慧医疗预约平台设计与实践 1. 项目概述与行业背景智慧医疗预约管理平台是当前医疗信息化建设的重要方向之一。作为一名参与过多个医疗信息化项目的开发者,我深刻理解这类系统对提升医疗服务效率的价值。传统的医院挂号系统往往存在预约难、排队时间长、信息不对称等问题,而基于Spr… · 2026/11/3 16:49:30
【java虚拟机】JVM知识框架快速一览 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 18:20:00
【大模型应用开发极简入门】提示工程二:零样本学习、少样本学习与改善提示效果 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 18:20:00
【高等数学】第三章知识点(2)一元函数积分:定积分与反常积分、与几何应用 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 18:20:00
【python虚拟环境管理】【mac m3】使用poetry管理python项目 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 18:20:00
TabPFN-2.6案例研究:在真实世界表格数据上的10个成功应用 TabPFN-2.6案例研究:在真实世界表格数据上的10个成功应用 【免费下载链接】tabpfn_2_6 项目地址: https://ai.gitcode.com/hf_mirrors/Prior-Labs/tabpfn_2_6
TabPFN-2.6是一款革命性的表格预测基础模型,它通过上下文学习在单次前向传播中解决表… · 2026/11/3 18:18:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46