简介:本资源是一套专为STM32F103系列单片机设计的FM25CL64铁电存储器SPI接口驱动代码,面向嵌入式开发工程师及电子类专业学生,解决铁电存储器在实时性要求高、频繁读写场景下的可靠存取问题。压缩包共含2个核心文件(1个C源文件实现SPI初始化与字节级读写逻辑,1个H头文件定义接口函数与寄存器操作宏),总大小仅2KB,轻量易集成,适用于工业控制、数据黑匣子等需非易失性高速存储的应用场景。已有1684人学习下载,代码结构清晰、注释完整,包含SPI1外设与GPIOA引脚初始化配置、写使能/等待忙状态等关键时序处理,并兼容FM25系列其他型号,可直接移植到Keil或STM32CubeIDE工程中使用。
1. 项目概述与核心价值
最近在整理一个老项目的资料,翻出来一个基于STM32F103读写FM25CL64铁电存储器的驱动代码包。这个项目虽然不大,但麻雀虽小五脏俱全,从硬件选型、SPI协议理解到驱动编写、稳定性调试,整个过程踩了不少坑,也积累了一些非常实用的经验。铁电存储器(FRAM)对于很多需要频繁、快速、低功耗读写非易失性数据的场景来说,是个“神器”,它兼具了RAM的速度和EEPROM/Flash的非易失性。而STM32F103作为经典的“国民MCU”,其SPI外设的灵活性与复杂性并存。把这两者结合起来,看似只是简单的“读”和“写”,但要想写出稳定、高效、可靠的驱动,里头的门道可不少。
这个驱动代码包,就是针对STM32F103的硬件SPI,实现对FM25CL64铁电存储器的完整操作,包括单字节读写、多字节连续读写、状态寄存器操作以及写保护控制。它不仅仅是一堆函数,更包含了硬件连接参考、时序匹配技巧、中断与DMA的应用考量,以及在实际产品中验证过的抗干扰和异常处理策略。无论你是刚开始接触SPI通信的新手,还是正在为某个数据记录项目寻找可靠存储方案的老手,这份从实战中打磨出来的代码和背后的设计思路,或许都能给你带来直接的帮助和启发。
2. 硬件平台与核心器件解析
2.1 主角一:STM32F103的SPI外设
STM32F103系列通常有多个SPI外设(如SPI1、SPI2),我们这里以SPI1为例,因为它通常挂在高速APB2总线上,性能更好。STM32的SPI支持主/从模式,我们当然配置为主模式。有几个关键点需要特别注意:
时钟极性(CPOL)与时钟相位(CPHA):这是SPI通信的基石,必须与从设备(FM25CL64)严格匹配。CPOL定义了SCK时钟线在空闲时的电平状态(0为低电平,1为高电平),CPHA定义了数据在时钟的第几个边沿被采样(0为第一个边沿,1为第二个边沿)。常见的模式有Mode 0(CPOL=0, CPHA=0)和Mode 3(CPOL=1, CPHA=1)。我们需要根据FM25CL64的数据手册来确定。
数据帧格式:STM32的SPI可以配置数据大小为4位到16位。FM25CL64的指令和数据都是8位的,因此我们需要设置数据大小为8位。同时要注意MSB(最高位)先发送还是LSB(最低位)先发送,FM25CL64通常是MSB first。
NSS引脚管理(片选):这是一个容易混淆的点。STM32的SPI硬件提供了NSS引脚,可以配置为硬件模式或软件模式。
- 硬件NSS模式:由硬件自动控制片选信号的电平。在多主机系统中或需要严格时序时可能用到,但对于单个从设备,我通常不推荐在此处使用,因为其行为有时不如软件控制灵活。
- 软件NSS模式(更常用):将NSS引脚配置为普通的GPIO输出(比如PA4),然后我们在驱动函数中手动拉低(选中)和拉高(取消选中)这个引脚。这种方式控制权完全在程序员手中,非常清晰。
时钟频率(Baud Rate):STM32F103的SPI时钟由PCLK分频得到。FM25CL64支持最高20MHz的SPI时钟。我们需要根据系统时钟(SystemCoreClock)和APB2时钟(PCLK2)来计算分频系数,确保最终SCK频率不超过20MHz,并留有一定余量以保证稳定性。
2.2 主角二:FM25CL64铁电存储器
FM25CL64是64Kbit(即8KB)的串行FRAM。它的核心优势在于:
- 近乎无限的读写耐久度:读写次数可达10^14次,远超EEPROM(约10^5次)和Flash(约10^5次),可以像RAM一样随意读写,无需担心寿命。
- 高速写入:写入速度就是SPI总线的速度,没有页编程或擦除等待时间。写一个字节和写整个存储器阵列的时间几乎只取决于数据传输时间。
- 低功耗:读写电流和待机电流都非常低。
引脚与连接:FM25CL64是8引脚SOIC封装,常用引脚如下:
/CS(片选):接STM32的某个GPIO(如PA4)。SO(串行数据输出):接STM32 SPI的MISO(主设备输入,PA6 for SPI1)。SI(串行数据输入):接STM32 SPI的MOSI(主设备输出,PA7 for SPI1)。SCK(串行时钟):接STM32 SPI的SCK(PA5 for SPI1)。/WP(写保护):接STM32的某个GPIO。拉低时禁止写入状态寄存器,但对存储器阵列的写操作是否保护取决于状态寄存器的设置。拉高时允许写状态寄存器。VDD,GND:电源,通常接3.3V。NC:空脚。
关键操作指令:FM25CL64通过SPI接收指令字节来执行操作,所有指令都是8位。核心指令包括:
WREN(0x06): 写使能指令。任何写操作(包括写状态寄存器)之前,必须发送此指令。这是一个非常关键的细节,容易遗漏导致写入失败。WRDI(0x04): 写禁止指令。RDSR(0x05): 读状态寄存器。WRSR(0x01): 写状态寄存器。READ(0x03): 读存储器数据。WRITE(0x02): 写存储器数据。FSTRD(0x0B): 快速读指令(支持更高时钟频率,但时序略有不同)。
状态寄存器(Status Register):这是一个8位的寄存器,我们主要关注其中两位:
WPEN(位7):写保护使能位。当此位为1,且外部/WP引脚为低电平时,状态寄存器被写保护。通常,为了简化,我们可以将此位设为0,这样写保护就完全由外部/WP引脚控制。BP1, BP0(位3,位2):块保护位。这两位定义了受保护的存储器区域范围(从阵列顶部开始)。例如,BP1=1, BP0=1时,整个8KB阵列都被写保护,任何写操作都会被忽略(除非先解除保护)。在项目初期,建议将这两位都设为0,即禁用块保护,避免不必要的麻烦。
注意:FM25CL64的地址是16位的,因为它有8K字节(2^13 = 8192),但地址线是A0-A12共13位。在SPI通信中,我们发送两个字节(16位)的地址,其中高3位是无效的(或必须为0)。发送地址时,同样是MSB先发,即先发地址的高8位,再发低8位。
3. 驱动程序设计思路与代码架构
一个好的驱动程序,应该是分层清晰、接口明确、易于移植和调试的。我设计的这个驱动主要分为三层:
3.1 硬件抽象层(HAL)
这一层直接与STM32的硬件库(如标准外设库或HAL库)打交道,封装最底层的SPI收发、GPIO控制、延时函数。它的目的是将硬件操作细节隐藏起来,为上层的逻辑驱动提供统一的接口。例如:
SPIx_Init(): 初始化SPI外设,配置模式、速率、数据格式等。SPIx_ReadWriteByte(uint8_t data): 发送一个字节并接收一个字节。这是SPI全双工通信的核心函数。FRAM_CS_LOW()/FRAM_CS_HIGH(): 控制片选引脚的宏定义。FRAM_WP_LOW()/FRAM_WP_HIGH(): 控制写保护引脚的宏定义。
使用宏定义来控制引脚,方便后续如果更换引脚,只需修改宏定义即可,而不需要改动上层逻辑代码。
3.2 逻辑驱动层(Driver)
这是核心层,实现了FM25CL64的所有功能指令。它调用硬件抽象层的函数,按照FM25CL64的时序要求组织数据流。这一层的函数是面向“器件”的。
关键函数设计:
FRAM_WriteEnable(void)/FRAM_WriteDisable(void): 发送WREN和WRDI指令。FRAM_ReadStatusReg(uint8_t *status): 读取状态寄存器。FRAM_WriteStatusReg(uint8_t status): 写入状态寄存器。注意:此函数内部必须先调用FRAM_WriteEnable()。FRAM_ReadByte(uint16_t addr, uint8_t *pData): 从指定地址读取一个字节。FRAM_WriteByte(uint16_t addr, uint8_t data): 向指定地址写入一个字节。注意:此函数内部必须先调用FRAM_WriteEnable()。FRAM_ReadBuffer(uint16_t addr, uint8_t *pBuffer, uint16_t len): 连续读取多个字节。FM25CL64支持地址自动递增,发送起始地址后,可以连续读取。FRAM_WriteBuffer(uint16_t addr, uint8_t *pBuffer, uint16_t len): 连续写入多个字节。同样支持地址自动递增。
连续读写的实现技巧:对于连续读写,最直观的方法是循环调用单字节读写函数。但这样效率较低,因为每次读写都要操作片选、发送指令和地址。更高效的方式是:
- 连续读:拉低片选 → 发送
READ指令(0x03) → 发送16位地址 →连续地调用SPIx_ReadWriteByte(0xFF)来读取数据(主机发送哑元数据以产生时钟)→ 拉高片选。 - 连续写:拉低片选 → 发送
WRITE指令(0x02) → 发送16位地址 →连续地调用SPIx_ReadWriteByte(data)来发送数据 → 拉高片选。
实操心得:在编写
FRAM_WriteBuffer函数时,务必注意长度len的处理。FM25CL64的地址是16位,从0x0000到0x1FFF。如果addr + len超过了0x1FFF,地址会回绕到0x0000。驱动代码里可以不做回绕处理,但上层应用必须保证不越界,或者驱动内加入越界检查并返回错误码。
3.3 应用接口层(API)
这一层根据具体项目需求,对逻辑驱动层进行二次封装,提供更业务化的接口。例如,如果你的项目需要存储一系列传感器数据,你可以定义一个结构体,然后提供SaveSensorData()和LoadSensorData()这样的函数,内部调用FRAM_WriteBuffer和FRAM_ReadBuffer。这一层使驱动与具体业务解耦,提高了代码的复用性。
4. 核心驱动代码实现与详解
下面,我将摘取驱动层中最关键的几个函数进行详细代码实现和讲解。这里假设我们使用STM32标准外设库,SPI1,软件控制片选(PA4)和写保护(PA3)。
4.1 初始化函数
初始化分为两部分:GPIO初始化和SPI初始化。
/** * @brief 初始化FRAM相关的GPIO和SPI * @param 无 * @retval 无 */ void FRAM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; /* 1. 使能时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* 2. 配置片选CS(/CS)和写保护WP(/WP)引脚为推挽输出 */ // 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); FRAM_CS_HIGH(); // 初始状态不选中 // WP - PA3 (可选,如果不用写保护功能可以不初始化) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_Init(GPIOA, &GPIO_InitStructure); FRAM_WP_HIGH(); // 默认拉高,允许写状态寄存器 /* 3. 配置SPI引脚: SCK(PA5), MISO(PA6), 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作为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); /* 4. 配置SPI1工作模式 */ SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主模式 // 根据FM25CL64数据手册设置!假设为Mode 0 (CPOL=0, CPHA=0) SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟空闲低电平 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 数据在第一个边沿采样 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS // 设置波特率预分频器,系统时钟72MHz, APB2为72MHz, 8分频后为9MHz < 20MHz SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // MSB先发 // 根据FM25CL64, CRCPolynomial不用 SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); /* 5. 使能SPI1 */ SPI_Cmd(SPI1, ENABLE); /* 6. (可选)初始化后,读取状态寄存器确认通信正常 */ uint8_t status = 0; if(FRAM_ReadStatusReg(&status) == FRAM_OK) { // 可以打印或处理状态值,默认上电后应为0x00 printf("FRAM Status Reg: 0x%02X\r\n", status); } }关键点解析:
- SPI模式:
SPI_CPOL_Low和SPI_CPHA_1Edge对应Mode 0。你必须根据你手中的FM25CL64数据手册确认这一点!不同批次或型号可能有差异。 - 波特率:
SPI_BaudRatePrescaler_8在72MHz系统时钟下产生9MHz的SCK,对于20MHz上限的FM25CL64是安全的。如果你想追求极限速度,可以尝试_4(18MHz),但务必在面包板或最终PCB上做好信号完整性测试。 - 软件NSS:设置为
SPI_NSS_Soft后,SPI硬件将不再管理NSS引脚(PA4),我们完全用GPIO来控制它。
4.2 基础SPI收发函数
这是硬件抽象层的核心,所有上层通信都基于它。
/** * @brief 通过SPI1读写一个字节 * @param txdata: 要发送的字节 * @retval 接收到的字节 */ static uint8_t SPI1_ReadWriteByte(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); }这个函数利用全双工特性,发送的同时也在接收。当我们需要只发送不关心接收的数据时(如发送指令),可以忽略返回值;当我们需要读取数据时,可以发送一个哑元数据(如0xFF)来产生时钟。
4.3 写使能与单字节读写函数
让我们看看最常用的单字节读写是如何实现的,特别是写使能这个关键步骤。
/** * @brief 向FRAM发送写使能指令 * @param 无 * @retval 操作状态: FRAM_OK / FRAM_ERROR */ FRAM_StatusTypeDef FRAM_WriteEnable(void) { FRAM_CS_LOW(); SPI1_ReadWriteByte(FRAM_CMD_WREN); // 发送0x06 FRAM_CS_HIGH(); // 写使能后,可以插入一个微小延时,确保器件内部状态稳定 // delay_us(1); return FRAM_OK; } /** * @brief 向FRAM指定地址写入一个字节 * @param addr: 16位地址 * @param data: 要写入的数据 * @retval 操作状态 */ FRAM_StatusTypeDef FRAM_WriteByte(uint16_t addr, uint8_t data) { FRAM_StatusTypeDef status = FRAM_ERROR; /* 1. 发送写使能指令 */ if(FRAM_WriteEnable() != FRAM_OK) { return FRAM_ERROR; } /* 2. 发送写指令和地址、数据 */ FRAM_CS_LOW(); SPI1_ReadWriteByte(FRAM_CMD_WRITE); // 发送0x02 SPI1_ReadWriteByte((uint8_t)(addr >> 8)); // 发送地址高8位 SPI1_ReadWriteByte((uint8_t)(addr & 0xFF)); // 发送地址低8位 SPI1_ReadWriteByte(data); // 发送数据 FRAM_CS_HIGH(); /* 3. 等待写入完成 (可选, FRAM写入极快,通常不需要) */ // 对于FRAM,写入是瞬间完成的,无需像Flash那样轮询状态位。 // 但为了代码健壮性,可以添加一个短延时或检查写使能锁存位(通过读状态寄存器)。 // delay_us(1); status = FRAM_OK; return status; } /** * @brief 从FRAM指定地址读取一个字节 * @param addr: 16位地址 * @param pData: 指向存储读取数据的变量的指针 * @retval 操作状态 */ FRAM_StatusTypeDef FRAM_ReadByte(uint16_t addr, uint8_t *pData) { if(pData == NULL) return FRAM_ERROR; FRAM_CS_LOW(); SPI1_ReadWriteByte(FRAM_CMD_READ); // 发送0x03 SPI1_ReadWriteByte((uint8_t)(addr >> 8)); // 发送地址高8位 SPI1_ReadWriteByte((uint8_t)(addr & 0xFF)); // 发送地址低8位 *pData = SPI1_ReadWriteByte(0xFF); // 发送哑元数据(0xFF)以读取数据 FRAM_CS_HIGH(); return FRAM_OK; }注意事项:
- 写使能的时效性:
WREN指令执行后,写使能锁存器(WEL)被置位。但一旦发生一次有效的写操作(包括WRSR和WRITE),或者断电,WEL位会被自动清零。这意味着每次写操作前都必须重新发送WREN。不能发送一次WREN然后连续写多个地址。 - 片选时序:在发送指令字节前拉低片选,在完成整个指令序列(包括所有数据)后拉高片选。片选信号是帧同步信号。
- 地址发送顺序:先发送高字节(
addr >> 8),再发送低字节(addr & 0xFF)。这是MSB First原则在地址上的体现。
4.4 多字节连续读写函数
连续读写能极大提高效率,下面是其实现:
/** * @brief 从FRAM连续读取多个字节 * @param addr: 起始地址 * @param pBuffer: 数据存储缓冲区指针 * @param len: 要读取的字节数 * @retval 操作状态 */ FRAM_StatusTypeDef FRAM_ReadBuffer(uint16_t addr, uint8_t *pBuffer, uint16_t len) { uint16_t i; if(pBuffer == NULL || len == 0) return FRAM_ERROR; FRAM_CS_LOW(); SPI1_ReadWriteByte(FRAM_CMD_READ); SPI1_ReadWriteByte((uint8_t)(addr >> 8)); SPI1_ReadWriteByte((uint8_t)(addr & 0xFF)); for(i = 0; i < len; i++) { pBuffer[i] = SPI1_ReadWriteByte(0xFF); // 连续读取 } FRAM_CS_HIGH(); return FRAM_OK; } /** * @brief 向FRAM连续写入多个字节 * @param addr: 起始地址 * @param pBuffer: 数据源缓冲区指针 * @param len: 要写入的字节数 * @retval 操作状态 */ FRAM_StatusTypeDef FRAM_WriteBuffer(uint16_t addr, uint8_t *pBuffer, uint16_t len) { uint16_t i; if(pBuffer == NULL || len == 0) return FRAM_ERROR; /* 注意:对于连续写,只需要在开始前发送一次WREN */ if(FRAM_WriteEnable() != FRAM_OK) { return FRAM_ERROR; } FRAM_CS_LOW(); SPI1_ReadWriteByte(FRAM_CMD_WRITE); SPI1_ReadWriteByte((uint8_t)(addr >> 8)); SPI1_ReadWriteByte((uint8_t)(addr & 0xFF)); for(i = 0; i < len; i++) { SPI1_ReadWriteByte(pBuffer[i]); // 连续写入 } FRAM_CS_HIGH(); // 连续写入完成后,WEL位也会被自动清除 return FRAM_OK; }实操心得:连续写的“原子性”在连续写操作中,从拉低
/CS到拉高/CS之间的所有数据传输,对于FM25CL64来说是一个不可分割的“写周期”。在此期间,如果因为任何原因(如电源干扰、程序跑飞)导致/CS被意外拉高,可能会导致只有部分数据被正确写入,造成数据损坏。因此,确保写操作期间系统的稳定性非常重要。在一些高可靠性场合,可以在写入关键数据后,立刻读回验证。
5. 高级功能与稳定性优化
5.1 状态寄存器操作与写保护策略
合理使用状态寄存器和/WP引脚,可以有效防止数据被意外篡改。
/** * @brief 配置FRAM的写保护 * @param blockProtect: 块保护设置 (0:无保护, 1:保护1/4, 2:保护1/2, 3:全保护) * @param enableWPPinControl: 是否使能/WP引脚控制 (1:使能, 0:禁用) * @retval 操作状态 */ FRAM_StatusTypeDef FRAM_ConfigWriteProtect(uint8_t blockProtect, uint8_t enableWPPinControl) { uint8_t status_reg = 0; FRAM_StatusTypeDef ret; // 首先读取当前状态寄存器 ret = FRAM_ReadStatusReg(&status_reg); if(ret != FRAM_OK) return ret; // 清除BP1,BP0位和WPEN位 status_reg &= ~(FRAM_STATUS_BP1 | FRAM_STATUS_BP0 | FRAM_STATUS_WPEN); // 设置块保护位 blockProtect &= 0x03; // 确保只取低2位 status_reg |= ((blockProtect << 2) & (FRAM_STATUS_BP1 | FRAM_STATUS_BP0)); // 设置WPEN位 if(enableWPPinControl) { status_reg |= FRAM_STATUS_WPEN; // 如果使能了WPEN,那么外部/WP引脚为低时,状态寄存器不可写。 // 此时,我们可以通过控制/WP引脚电平来锁定/解锁状态寄存器。 // 默认我们让/WP为高(解锁),允许本次配置。 FRAM_WP_HIGH(); } else { // 禁用WPEN,则/WP引脚状态不影响状态寄存器写保护。 // 写保护完全由BP1,BP0位决定。 } // 写入新的状态寄存器 ret = FRAM_WriteStatusReg(status_reg); return ret; }使用建议:
- 项目开发阶段:建议将
blockProtect设为0,enableWPPinControl设为0。即禁用所有保护,方便调试。 - 产品发布阶段:根据需求设置保护。例如,如果有一段存放校准参数或序列号的区域需要永久保护,可以将这部分数据放在存储器的末尾(高地址),然后设置相应的
blockProtect值来保护顶部区域。同时,可以将/WP引脚连接到MCU的一个GPIO或直接上拉,在需要更新时由MCU控制。
5.2 使用DMA提升大数据量传输效率
当需要读写大量数据(如存储一幅小图片、一段音频采样)时,使用SPI的DMA功能可以解放CPU,大幅提高效率并降低系统延迟。
思路:
- 配置SPI的DMA请求:使能SPI的Tx和Rx DMA请求。
- 配置DMA通道:为SPI_Tx和SPI_Rx分别配置DMA通道(如DMA1_Channel3和Channel2 for SPI1)。设置内存到外设(Tx)和外设到内存(Rx)的传输方向、数据宽度、地址递增等。
- 修改读写函数:对于连续读写函数,不再使用
for循环配合SPI1_ReadWriteByte,而是启动DMA传输,并等待DMA传输完成标志或使用中断通知。 - 注意片选控制:DMA传输开始前拉低
/CS,在DMA传输完成中断或轮询到完成标志后拉高/CS。
DMA写示例片段:
// 假设已配置好SPI1_Tx的DMA通道(DMA1_Channel3) void FRAM_WriteBuffer_DMA(uint16_t addr, uint8_t *pBuffer, uint16_t len) { // 1. 写使能 FRAM_WriteEnable(); // 2. 拉低片选,并发送指令和地址(这部分仍需CPU参与) FRAM_CS_LOW(); SPI1_ReadWriteByte(FRAM_CMD_WRITE); SPI1_ReadWriteByte((uint8_t)(addr >> 8)); SPI1_ReadWriteByte((uint8_t)(addr & 0xFF)); // 3. 配置DMA:源地址=pBuffer, 目标地址=&SPI1->DR, 传输长度=len DMA_InitTypeDef DMA_InitStructure; // ... 省略具体的DMA配置代码 ... DMA_Cmd(DMA1_Channel3, ENABLE); // 4. 使能SPI的Tx DMA请求 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); // 5. 等待DMA传输完成(可以通过中断更优雅) while(DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET); // 6. 传输完成,清理标志,失能DMA DMA_ClearFlag(DMA1_FLAG_TC3); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, DISABLE); DMA_Cmd(DMA1_Channel3, DISABLE); // 7. 拉高片选 FRAM_CS_HIGH(); }注意:DMA传输期间,SPI的时钟由DMA请求自动控制。对于读操作,还需要同时配置Rx DMA来接收数据,并注意发送哑元数据(0xFF)来产生时钟。这通常需要设置一个全为0xFF的发送缓冲区,或者使用SPI的“双缓冲”或“循环”模式,实现起来比写操作稍复杂。
5.3 驱动程序的健壮性设计
- 返回值检查:所有驱动函数都应返回状态(如
FRAM_OK,FRAM_ERROR),上层应用应检查这些返回值。 - 超时机制:在轮询标志位(如SPI的TXE/RXNE, DMA的TC)时,应加入超时判断,防止因硬件故障导致程序死锁。
#define FRAM_TIMEOUT_MAX 10000 // 定义一个超时最大值 uint32_t timeout = 0; while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) { if(timeout++ > FRAM_TIMEOUT_MAX) return FRAM_TIMEOUT; } - 数据校验:对于关键数据,可以采用写入后立即读回校验(Write-Verify)的方法,或者使用CRC校验码与数据一同存储。
- 临界区保护:如果驱动程序在中断和主循环中都会被调用,需要使用信号量或关中断等方式保护共享资源(如SPI总线),防止访问冲突。
6. 常见问题排查与调试技巧
在实际使用中,你可能会遇到以下问题。这里提供一个快速排查指南:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 完全无法通信,读回的数据全是0xFF或0x00 | 1. 硬件连接错误(线接反、虚焊) 2. 电源问题 3. SPI模式不匹配 4. 片选信号控制错误 | 1.检查硬件:用万用表或示波器检查VDD、GND、/CS、SCK、MOSI、MISO连接。确保/CS和/WP引脚初始状态正确(/CS高,/WP高)。2.检查电源:测量FM25CL64的VDD引脚电压是否为稳定的3.3V。 3.验证SPI模式:这是最常见的问题!用逻辑分析仪或示波器抓取 SCK,MOSI,/CS波形。对照FM25CL64数据手册的时序图,检查空闲电平和采样边沿。将STM32的CPOL/CPHA与器件要求逐一匹配。4.检查片选时序:确保在发送指令前拉低 /CS,并在整个指令序列完成后拉高。 |
| 可以读取状态寄存器,但无法写入数据 | 1. 忘记发送WREN(写使能)指令2. 写保护( /WP引脚或状态寄存器)被启用3. 写入的地址在保护区内 | 1.检查代码:在每次WRSR或WRITE操作前,是否调用了FRAM_WriteEnable()?2.检查 /WP引脚电平:用万用表测量,是否为高电平?如果使能了WPEN,低电平会锁死状态寄存器。3.读取状态寄存器:调用 FRAM_ReadStatusReg,检查BP1,BP0和WPEN位的值。确认你要写的地址是否在受保护的区块内。4.简化测试:先尝试向一个低地址(如0x0000)写入一个已知值,然后读回验证。确保块保护是关闭的。 |
| 连续读写时数据错位或丢失 | 1. 时钟频率过高,信号质量差 2. 连续读写时地址计算错误,导致回绕 3. 缓冲区指针或长度传递错误 4. 没有处理好片选信号在连续传输中的状态 | 1.降低SPI时钟频率(如降到1MHz)测试,看问题是否消失。检查PCB布线,SCK和MOSI/MISO线是否过长,是否靠近干扰源。 2.检查 FRAM_Read/WriteBuffer函数:确认addr和len的计算不会导致地址超过0x1FFF。可以在函数入口添加断言或检查。3.使用调试器:单步跟踪连续读写函数,观察 pBuffer指针和len的值是否正确。4.逻辑分析仪抓包:这是最直观的方法,可以看到从 /CS拉低到拉高之间,每一个字节的传输是否正确。 |
| 偶尔出现数据损坏 | 1. 电源噪声干扰 2. SPI总线受到其他高速信号干扰 3. 在写操作过程中发生复位或断电 4. 多任务/中断环境下SPI总线访问冲突 | 1.加强电源滤波:在FM25CL64的VDD和GND引脚就近放置一个0.1uF和一个10uF的电容。 2.优化PCB布局:让SPI信号线远离时钟线、PWM输出等噪声源。如果线长超过10cm,考虑串联小电阻(如22Ω-100Ω)进行阻抗匹配,减少振铃。 3.实施写验证:对关键数据,写入后立刻读回比较。或采用“影子存储”策略,同一份数据存两份,读取时进行校验和恢复。 4.增加互斥锁:如果SPI驱动在中断和主程序中共用,必须增加临界区保护,确保同一时间只有一个上下文在访问SPI总线。 |
调试利器:逻辑分析仪一个几十块钱的USB逻辑分析仪(配合PulseView或Saleae Logic软件)是调试SPI、I2C等串行协议的必备工具。它能清晰地展示SCK,MOSI,MISO,/CS每条线上的时序和电平,并能将数据包解析成十六进制或指令助记符。当通信不正常时,抓一包波形,比任何打印信息都管用。
7. 项目集成与实战建议
当你把驱动调试通后,集成到实际项目中时,还有一些工程上的考虑:
内存映射:为你的8KB FRAM规划一个清晰的“内存地图”。例如:
0x0000 - 0x00FF: 系统配置参数(设备ID, 校准值等)0x0100 - 0x03FF: 运行日志缓冲区0x0400 - 0x1FFF: 用户数据区 定义成宏或枚举,方便管理。
磨损均衡:虽然FRAM寿命极长,但如果你有某个变量需要每秒更新100次,十年下来对同一个地址的写入次数也将非常可观。对于这种“热点”数据,可以考虑简单的磨损均衡策略,比如在几个相邻地址间轮换写入。
掉电保护:FRAM虽然不怕掉电,但你的MCU在掉电时可能无法完成一次完整的写操作。对于关键数据,可以考虑:
- 使用外部监控芯片(如MAX706)在电压跌落时给MCU提供早期预警,让MCU有时间完成最后一次关键数据的保存。
- 采用“预写日志”或“双副本”机制,确保即使写操作中断,也能从备份中恢复出一个有效版本。
驱动封装成库:将你的驱动代码(
.c和.h文件)整理成一个独立的模块。在头文件(fram.h)中清晰定义初始化、读写、控制接口,并做好详细的注释。这样,在未来任何STM32F103的项目中,你都可以直接复制粘贴这个模块,快速实现FRAM存储功能。
这个驱动代码包,从最初的原理验证到最终稳定运行在产品中,前后迭代了好几个版本。最大的体会就是,嵌入式开发中“通信”是基础,而“稳定”是生命。每一行代码背后的硬件时序、每一个函数调用的前提条件、每一次异常情况的处理,都需要深思熟虑。希望这份详细的解读和附带的代码,能帮你绕过我当年踩过的那些坑,更顺畅地把铁电存储器的强大能力应用到你的项目中去。如果遇到其他具体问题,欢迎一起交流探讨。
本文还有配套的精品资源,点击获取