ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MR25H40CDF与PIC18LF46K22的SPI MRAM嵌入式数据存储实战

MR25H40CDF与PIC18LF46K22的SPI MRAM嵌入式数据存储实战 做了好几年工业现场设备凡是要断电保存数据、频繁记录运行参数的东西我基本都会优先考虑 MRAM 加 MCU 这个组合。MR25H40CDF 这块 4Mbit SPI MRAM配 PIC18LF46K22 这款低功耗 8 位单片机在嵌入式领域做数据存储和读取属于我在实际项目里反复验证过的稳定搭配。这次把整个设计思路、硬件连接、命令时序、固件驱动、以及工业环境下最容易翻车的可靠性问题一起写出来给正准备在项目里用这套方案的朋友做个参考。1. 为什么是这个组合选型拆解与整体思路1.1 先想清楚需求再定方案做嵌入式存储方案第一步不是看芯片手册而是先问自己三件事数据多久写一次、掉电以后数据丢不丢得起、系统工作在什么环境。很多设备跑起来以后才发现问题电表每小时要记录电量曲线如果断电丢半小时数据用户就投诉伺服驱动器每次调整参数可能要写几千字节到存储芯片如果用 Flash宣传寿命十万次擦写实际现场频繁升级参数两三年以后写保护就开始不稳定还有一些设备要在 -40 度的户外启动普通 EEPROM 虽然也能用但写一个字节要等 5 毫秒到 10 毫秒的写周期系统掉电瞬间根本来不及把关键数据存完。我选择 MR25H40CDF 加 PIC18LF46K22核心原因是这三条MRAM 写入速度极快不需要擦除像写 SRAM 一样直接写没有写周期等待写入耐久性基本无限工业场景下频繁记录运行日志完全不用担心寿命PIC18LF46K22 支持 1.8V 到 3.6V 供电和 MRAM 的 2.7V 到 3.6V 工作电压自然匹配不需要额外电平转换如果是做消费类小批量或者原型验证也许 25LC256 或者 W25Q64 也能凑合。一旦进入工业现场写入次数、掉电保存、可靠性这些字眼就会变成硬指标选 MRAM 反而是性价比高的决定。1.2 MRAM 和 Flash、EEPROM 的本质差异MR25H40CDF 属于 STT-MRAM自旋转移矩磁随机存储器它存储数据的介质不是电荷而是磁化方向。这个物理层面的差异带来几个嵌入式工程师最喜欢的特性掉电不丢数据不需要考虑 Flash 的块擦除数据写入立刻生效写入耐久性极高逻辑上无限次甚至可以当非易失 SRAM 用访问时序和普通 SRAM/SPI 存储完全一致没有擦写均衡、垃圾回收这些复杂管理逻辑对比一下常见方案的参数就很直观方案写入前是否需要擦除典型写入速率耐久性掉电保存能力传统 EEPROMI2C/SPI不需要慢ms 级写周期一般百万次有但写入速度受限SPI NOR Flash需要先擦除扇区快但要管理等开销典型 10 万次有但掉电时可能损坏一页SPI MRAM不需要快SPI 时钟直接写基本无限有且写入随机地址无需额外处理实际项目里我遇到过很多次这样的情况用 Flash 做故障录波掉电瞬间数据写到一半正好撞上擦除过程上电一读发现一条记录半截是旧的半截是新的。换成 MRAM 以后这种问题彻底消失因为每一个字节的写入都是独立、立即完成的。1.3 MCU 选择与整体系统框架PIC18LF46K22 属于 Microchip 的 XLP 低功耗系列内置 MSSP 外设可以直接跑 SPI 主模式硬件 SPI 模块带发送和接收缓冲区省去很多软 SPI 的时序折腾。它的工作电压范围大还能直接由 3.3V 或电池供电非常适合做低功耗数据采集设备。整体框架其实很简洁MCU 作为 SPI 主设备通过 4 根线连接 MR25H40CDFMRAM 保存系统配置参数、运行日志、掉电前的状态快照MCU 内部 Flash 保存固件程序数据存储全部交给 MRAM这样的分工能避免一个问题频繁读写数据时总去擦写 MCU 内部 Flash一旦把代码区搞坏了整个设备就变砖了。把存储独立到 MRAM 上即使调试过程写废了数据区最多重新初始化一下存储固件本身不受影响。2. 硬件电路别看就这么几根线坑都在细节里2.1 MR25H40CDF 引脚功能与接线定义MR25H40CDF 是标准 8 脚封装典型 SOP-8 或者 DFN-8引脚定义基本沿袭 SPI EEPROM 的习惯。我把每个引脚的作用和注意事项整理成一个表方便对照画原理图MR25H40CDF 引脚号引脚名功能说明接线建议1CS#片选低有效接任意 GPIO建议单独引脚控制2SO/SIO1串行数据输出MISO接 MCU 的 SPI 输入引脚SDI3WP#写保护输入低有效如果不做外部硬件写保护必须接 VCC4VSS地接 GND5SI/SIO0串行数据输入MOSI接 MCU 的 SPI 输出引脚SDO6SCK串行时钟接 MCU 的 SPI 时钟引脚SCK7HOLD#保持输入低有效必须接 VCC不能悬空8VCC电源接 3.3V就近放去耦电容两个最容易出问题的引脚是 WP# 和 HOLD#。HOLD# 一旦悬空外面有一点电磁干扰把它拉低通信就会莫名其妙被暂停主设备以为还在正常发送实际从设备早就停止采样了读回来的数据全是乱的。WP# 悬空也有风险虽然默认状态寄存器里写保护没使能但工业环境下的毛刺可能导致状态寄存器误置位整片存储突然写不进去。所以我的习惯是这两个引脚不搞任何花活原理图上直接拉到 VCC。2.2 与 PIC18LF46K22 的接口连接PIC18LF46K22 的 MSSP1 外设提供了 SPI 主机功能最常用的引脚分配是 SCK1 在 RC3、SDO1 在 RC5、SDI1 在 RC4。CS# 我习惯用普通 GPIO比如 RA2 或者 RB0不依赖 MSSP 的硬件从选择功能这样软件控制更灵活也能避开一些器件上 CS 自动控制的坑。一个很关键的细节CS# 必须由软件精确控制不能随便选一个引脚就完事。因为 MRAM 的每个命令帧都以 CS# 拉低开始、拉高结束如果 CS# 时序不干净比如拉低以后经过很长的延时才开始发时钟或者最后一个字节还没发完就拉高读出来的数据就会出现位移。连线表可以这样设计信号PIC18LF46K22 引脚MR25H40CDF 引脚SPI 时钟RC3/SCK16 (SCK)SPI 输出RC5/SDO15 (SI/SIO0)SPI 输入RC4/SDI12 (SO/SIO1)片选RA21 (CS#)电源3.3V8 (VCC)地GND4 (VSS)2.3 电源、去耦和 PCB 布局要点MRAM 虽然能耗不高但工业现场电源环境往往很脏。VCC 引脚上我习惯放两个电容一个 1uF 的陶瓷电容负责中低频纹波一个 0.1uF 的小电容负责高频噪声两个电容都要尽量靠近芯片电源引脚。如果板子上有电机、继电器这类感性负载建议在电源输入端再加一个 TVS 管防止反电动势打坏存储芯片。电源进入 MRAM 的路径也要单独走线不要和继电器驱动、指示灯这些大电流回路共用一小段细走线。实测过一版板子MRAM 和继电器共地共电源继电器吸合的瞬间 SPI 读数据偶尔出错把存储芯片的电源走线加宽、就近加去耦电容之后问题就没再出现过。还有一个大家容易忽略的点PCB 上 CS#、SCK、SI、SO 这四根 SPI 信号线尽量保持等长且不要跨分割平面。对于工业应用来说SPI 时钟通常不超过 10MHz等长要求不强但绝对不能把 SCK 线和电机驱动线平行走太长距离。如果条件允许在 SCK 上串联一个 22 欧姆的小电阻可以减少振铃。3. 核心命令与时序搞定 MRAM 的读写流程3.1 命令集速览MR25H40CDF 的命令集很清爽一共就那么几条和常见的 SPI EEPROM 类似。核心的命令如下表命令名操作码功能使用频率WREN0x06设置写使能锁存位WEL每次写入前必须WRDI0x04清除写使能锁存位一般可不主动用RDSR0x05读状态寄存器调试和状态检查WRSR0x01写状态寄存器配置写保护初始化时用READ0x03从指定地址连续读数据日常读取WRITE0x02从指定地址连续写数据日常写入SLEEP0xB9进入深度睡眠模式低功耗产品使用WAKE0xAB唤醒睡眠低功耗产品使用这里要多说一句MRAM 写入前仍然需要发 WREN 指令这是芯片内部对写使能锁存器的要求不是 Flash 那种“先擦后写”。每次写命令结束后 WEL 位会被自动清除所以想连续写多个独立地址每个地址都要重新发 WREN。如果一次命令能写完的数据比如一条超过 100 字节的日志WREN 只需要发一次一次 WRITE 命令里连续写完即可效率很高。3.2 SPI 模式与时序参数MR25H40CDF 支持标准 SPI Mode 0 和 Mode 3绝大多数情况下我用 Mode 0CPOL 0CPHA 0也就是 SCK 空闲时为低电平数据在 SCK 上升沿被采样。这个模式对 PIC 的 MSSP 外设来说比较容易配置。一个重要的概念区分PIC 的 MSSP 寄存器里的 CKP 和 CKE 组合和我们通常说的 SPI Mode 0、3 对应关系容易搞混。我的经验是在 PIC18LF46K22 上配置 SPI Mode 0需要把 CKP 清 0同时把 CKE 置 1。如果用的是硬件 SPI 模块配置完先不要连接任何负载用示波器看 SCK 空闲电平是不是低再决定要不要调整。时序上需要关注的几个典型参数最大 SCK 频率MR25H40CDF 我没记错的话可以跑到 40MHz 左右但 PIC18LF46K22 的系统时钟根据电压不同一般最高 32MHz 或 64MHzSPI 外设分频后通常用 4MHz 到 16MHz完全够用CS# 建立时间CS# 拉低后要等一小段时间再发 SCKPIC 的 IO 翻转速度够快但程序里建议在拉低 CS 后插入几个空周期CS# 保持时间最后一个字节发完要等 SCK 完全停止后再拉高 CS#对于 16MHz 的 SPI 时钟一个字节只需要 8 个 SCK 周期也就是 0.5 微秒写 512 字节的数据也只要 256 微秒。这个速度项目里绰绰有余。3.3 状态寄存器与写保护逻辑MR25H40CDF 的状态寄存器里有一个大家都熟悉的 WEL 位发送 WREN 之后它会被置 1每次写操作完成或者执行 WRDI 后会清 0。通过 RDSR 可以读出来。还有 BP0、BP1 这些块保护位可以设置地址范围写保护防止程序跑飞时意外改掉关键配置区。我一般不建议在系统里长期打开块保护因为工业现场经常需要远程更新参数一旦忘记关闭保护设备就会出现“能读不能写”的诡异故障。如果确实需要保护最好由 MCU 在启动初始化时主动设置状态寄存器并留一个调试命令关闭保护。这里有个细节MR25H40CDF 在写入状态寄存器和写入存储阵列之前都需要 WREN。很多人只记得写数据要 WREN忘了改状态寄存器也要结果想调整写保护位的时候发现根本写不进去排查半天才发现是少了 WREN。4. 固件实现从 SPI 初始化到完整读写驱动4.1 SPI 主模式初始化下面这段代码是我在 PIC18LF46K22 上实际使用的初始化函数做了简化把最关键的部分保留。这里假设你在使用 MPLAB X IDE 和 XC8 编译器// MR25H40CDF 驱动 #include xc.h #define MRAM_CS_LAT LATAbits.LATA2 #define MRAM_CS_TRIS TRISAbits.TRISA2 #define MRAM_CS_LOW() do { MRAM_CS_LAT 0; NOP(); NOP(); } while (0) #define MRAM_CS_HIGH() do { NOP(); NOP(); MRAM_CS_LAT 1; NOP(); NOP(); } while (0) void SPI1_Init(void) { // 引脚方向SCK(SCK1)输出SDO1输出SDI1输入 TRISCbits.TRISC3 0; TRISCbits.TRISC5 0; TRISCbits.TRISC4 1; // CS 引脚方向输出默认高电平 MRAM_CS_TRIS 0; MRAM_CS_LAT 1; // MSSP1 配置为主模式时钟 FOSC/16 // 这里配置成 SPI Mode 0CKP0CKE1 SSP1STATbits.CKE 1; SSP1CON1bits.CKP 0; SSP1CON1bits.SSPM 0b0010; // SPI Master modeFOSC/16 SSP1CON1bits.SSPEN 1; } uint8_t SPI1_Transfer(uint8_t data) { PIR1bits.SSP1IF 0; SSP1BUF data; while (!PIR1bits.SSP1IF) { // 等待发送和接收完成 } return SSP1BUF; }初始化里面值得注意的地方是引脚方向的设置。SCK 和 SDO 是 MCU 输出SDI 是 MCU 输入这个方向如果设反了SPI 模块即使工作正常也收不到数据。很多新手在这上面栽跟头明明寄存器配置看起来没问题但示波器测量发现 SO 引脚根本没波形。4.2 底层命令函数封装接下来是几个基础函数。命令操作码和地址发送都要严格遵守先发最高位的顺序SPI 本身就是 MSB first代码里不用特殊处理但地址要拆成三段发送#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_WRSR 0x01 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_SLEEP 0xB9 #define MRAM_CMD_WAKE 0xAB #define MRAM_SIZE 0x80000 // 512 KByte void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI1_Transfer(MRAM_CMD_WREN); MRAM_CS_HIGH(); } void MRAM_WriteDisable(void) { MRAM_CS_LOW(); SPI1_Transfer(MRAM_CMD_WRDI); MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI1_Transfer(MRAM_CMD_RDSR); status SPI1_Transfer(0x00); MRAM_CS_HIGH(); return status; } void MRAM_WriteStatus(uint8_t value) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI1_Transfer(MRAM_CMD_WRSR); SPI1_Transfer(value); MRAM_CS_HIGH(); }注意 MRAM_CS_LOW 和 MRAM_CS_HIGH 宏里的 NOP 空操作目的是保证 IO 翻转后有足够的建立和保持时间。实际项目中如果系统时钟很快IO 翻转本身只要几十纳秒但加几个 NOP 能让时序更稳妥尤其是高速 SPI 下不建议省略。4.3 数据读取与写入函数数据读写函数是整个驱动的核心。写入前需要检查地址范围再检查 WEL 位是否真的置位了。读取不需要 WREN直接发 READ 命令即可。完整代码如下uint8_t MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; // 参数检查 if ((addr len) MRAM_SIZE) { return 0; // 越界 } MRAM_CS_LOW(); SPI1_Transfer(MRAM_CMD_READ); SPI1_Transfer((addr 16) 0xFF); SPI1_Transfer((addr 8) 0xFF); SPI1_Transfer(addr 0xFF); for (i 0; i len; i) { buf[i] SPI1_Transfer(0x00); } MRAM_CS_HIGH(); return 1; } uint8_t MRAM_WriteBytes(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; if ((addr len) MRAM_SIZE) { return 0; } // 写操作前必须先使能写锁存 MRAM_WriteEnable(); // 校验 WEL 是否置位没置位则返回失败 if (!(MRAM_ReadStatus() 0x02)) { MRAM_CS_HIGH(); return 0; } MRAM_CS_LOW(); SPI1_Transfer(MRAM_CMD_WRITE); SPI1_Transfer((addr 16) 0xFF); SPI1_Transfer((addr 8) 0xFF); SPI1_Transfer(addr 0xFF); for (i 0; i len; i) { SPI1_Transfer(buf[i]); } MRAM_CS_HIGH(); return 1; }这里有几个容易忽略的细节。第一执行 WREN 之后要立刻发 WRITE 命令中间不要夹其他 CS 操作。第二读取函数里每读一个字节都要发一个 0x00 作为空时钟因为 SPI 是全双工的主机不给时钟从机就没法移出数据。第三所有命令帧发送完毕后必须等最后一个 SCK 边沿结束再拉高 CS否则最后一字节可能丢失。有一个我经常用的调试技巧在写完一串数据之后马上调用 MRAM_ReadBytes 把同一块地址读回来用 memcmp 或者逐字节比较。如果发现不一致优先检查 CS# 的时序而不是怀疑 MRAM 坏了。4.4 一个完整的读写验证示例下面是放到 main 函数里的简单验证流程。我一般会先在空板上跑一遍这个流程确保硬件和驱动都正常再往业务代码里集成#include string.h #define TEST_ADDR 0x000100 // 测试地址避开头部的配置区 #define TEST_LEN 32 uint8_t test_write[TEST_LEN]; uint8_t test_read[TEST_LEN]; void MRAM_Test(void) { uint8_t i; uint8_t ok; // 构造测试数据0xA0, 0xA1, 0xA2 ... for (i 0; i TEST_LEN; i) { test_write[i] 0xA0 i; } // 写入并回读 ok MRAM_WriteBytes(TEST_ADDR, test_write, TEST_LEN); if (!ok) { // 写入失败处理 } memset(test_read, 0, TEST_LEN); ok MRAM_ReadBytes(TEST_ADDR, test_read, TEST_LEN); if (!ok) { // 读失败处理 } // 比较 if (memcmp(test_write, test_read, TEST_LEN) 0) { // 验证通过 } else { // 数据不一致定位时序问题 } }把测试地址放在 0x000100 而不是 0x000000是因为很多产品会把 0 地址当作系统启动标志区用来做固件升级标记或者设备序列号测试代码不要占用这些关键区域。5. 工业现场才关心的可靠性设计5.1 掉电保存的时间窗口分析工业设备最典型的故障场景是突然掉电。操作员拉了闸刀设备还没来得及把当前状态保存到存储介质系统已经断了一半的电。EEPROM 和 Flash 掉电瞬间能不能保住数据取决于写操作是不是恰好卡在擦除或写入的中间。MRAM 没有这个过程但我们也得保证 MCU 有足够的“抢救时间”把数据送出去。简单计算一下假设系统主电容是 1000uF负载电流大约 20mA工作电压从 5V 掉到 3.3V 这个区间能支撑的时间大约是电容储能E 0.5 * C * U^25V 到 3.3V 释放的能量差约等于 0.5 * 1000uF * (25 - 10.89) 7.055mJ功率P 5V * 20mA 0.1W时间大约 70 毫秒70 毫秒对 MRAM 来说非常充足。即使要写入 512 字节SPI 时钟 10MHz 时只需要 0.4 毫秒加上地址解析等开销也不到 1 毫秒。真正需要花时间的是 MCU 把数据从运行变量里整理出来、计算 CRC、然后逐字节发送的过程这个通常也能在几毫秒内完成。所以掉电保存方案里瓶颈从来不是存储芯片而是你怎么在掉电瞬间把“重要数据快照”组织好。5.2 掉电检测和重要数据快照策略PIC18LF46K22 芯片内部有 BOR 欠压复位模块可以设置一个电压阈值比如 2.0V 或 2.7V当 VDD 低于这个阈值时产生复位。很多应用可以依赖 BOR 做掉电保护但如果想在掉电瞬间执行一段保存代码我的做法是加一个外部电压监控器比如 Microchip 的 MCP101 或者 TLV809。具体思路是这样的系统检测到掉电电源电压低于某个阈值但 MCU 供电还能维持几十毫秒MCU 收到外部中断停止正常业务进入紧急保存流程把运行状态、累计运行时间、最后一条操作记录、以及相关的校验信息写入 MRAM写完以后可以通过一个 GPIO 拉低信号让电源管理电路完成剩余断电动作这个流程里一个重要技巧是在正常运行时把待保存的数据不断更新到一个全局结构体里掉电中断里只需要对这个结构体做一次整块写入不要在中断里实时收集散落的数据。我见过不少代码在掉电中断里还去做传感器采集、计算平均值这些操作结果时间根本不够用。5.3 日志型存储的循环缓冲区设计工业设备另一个常见需求是保存历史日志。比如记录最近 1000 条报警记录或者最近一个月的温度变化曲线。如果用线性地址从头写到尾写满以后就没办法继续必须擦掉旧数据重写这在 MRAM 上虽然可行但不够优雅。更好的做法是做一个环形缓冲区把存储空间划分为 N 个固定大小的槽位每个槽位存放一条记录记录开头放一个魔数Magic Number、序号、CRC每次写入时从当前写指针取下一条记录读历史数据时从写指针向前回溯 N 条MRAM 好处就在这里不需要像 Flash 那样先擦除整个扇区才能覆盖旧记录直接在旧的槽位上覆盖写即可而且写入单位就是字节不用考虑页对齐。用 MRAM 做环形日志代码会简单很多也不容易出现“写一页只有 64 字节但日志一条要 100 字节”这种尴尬的分页问题。5.4 校验机制CRC 回读与冗余备份工业数据存储不能只往 MRAM 里写数据就完事。我强烈建议所有关键配置保存两份一份在 A 区一份在 B 区。读取时先比较两区数据如果一致才使用如果不一致说明可能发生过写了一半或者干扰优先以 CRC 正确的那份为准然后用正确的覆盖错误的。写入时也要做回读校验。虽然 MRAM 本身可靠性很高但 SPI 线路在恶劣电磁环境下可能发生误码。实际项目中我遇到过这样的问题MRAM 芯片本身没坏但 SPI 数据线旁边走了一根 220V 交流线上电瞬间产生噪声干扰导致写入的数据偶尔错一个字节。回读校验配合写入重试机制能在应用层抵御这种偶发错误。常见的 CRC 校验有 CRC16 或 CRC32。如果保存的是重要参数建议用 CRC32如果只是日志且想节省计算时间CRC16 也够用。校验值可以放在记录末尾读出来以后先算一遍 CRC 再决定是否采用。6. 常见问题排查与踩坑实录6.1 SPI 读回全是 0xFF 或者全 0这个问题在 SPI 存储器调试里出现频率最高。读回全 0xFF通常不是存储芯片坏了而是主机根本没有和从机建立正确通信检查 CS# 是不是真的拉低了示波器探头直接点芯片引脚而不是 MCU 引脚因为中间可能有断路检查 SCK 有没有波形时钟模式是不是配成了 Mode 3 而芯片需要 Mode 0检查 SDI 方向MCU 的 SDI 引脚必须配置成输入有些库函数初始化时会把整个 TRISx 一起改掉我之前调试一块板子SPI 读回来一直是 0xFF排查了半天最后发现 CS# 接错到另一个器件的片选输出上被别的设备拉高了。原理图上看着没错实际 layout 走线时网络名搞乱了。这里提醒一句波形不对先量物理引脚不要盯着寄存器一个一个猜。6.2 写操作执行了但数据读回来没变如果读取正常但写入不生效优先级最高的嫌疑是 WEL 位没有被正确置位。原因通常是 WREN 命令被漏发或者 WREN 命令发送的帧格式不对。MRAM 的 WREN 只有操作码一个字节CS# 必须在整个操作码发射完后才拉高提前拉高会导致命令无效。另一种可能是状态寄存器里的块保护位被意外置位。此时读状态寄存器检查 BP0、BP1 和 WPEN如果配置区落在保护范围内写操作会被芯片拒绝。这种故障在多次重启以后才会暴露因为厂商默认状态寄存器值可能受环境影响发生变化尤其是 WP# 引脚悬空时更容易发生。6.3 系统正常运行时偶尔读错一个字节工业现场最讨厌的就是这种间歇性故障。偶发读错字节常见原因有三个电源纹波过大MRAM 供电不稳SPI 时钟线受到耦合干扰软件上在往返命令之间没有严格遵守 CS# 时序这类问题排查思路是先把 SPI 时钟降到 1MHz 左右如果故障消失多半是信号完整性问题而不是芯片问题。然后再在靠近芯片的电源引脚加 0.1uF 电容看有没有好转。如果还不行试着降低代码中的中断频率因为 SPI 命令发到一半被中断打断会导致 CS# 保持时间过长。这里特别提醒MRAM 命令传输过程中尽量避免在同一个字节的时钟周期里被高优先级中断打断。MCU 的 SPI 硬件不会因为中断而停止 SCK但如果你使用的是软件 SPI中断延时会导致 SCK 停止破坏时序。用硬件 SPI 时尽量把中断优先级调低或者关闭中断直到命令帧发送完成。6.4 HOLD 引脚悬空引发的诡异暂停HOLD# 拉低时芯片会暂停当前通信忽略 SCK 信号数据输出进入高阻状态。如果 HOLD# 悬空遇到静电或者触摸等干扰芯片可能随时进入暂停模式表现就是 SPI 通信偶尔超时、数据丢失且完全复现不出来。我在原理图上强制把 HOLD# 接到 VCC 后这类问题再也没出现过。同样WP# 也不应该悬空。6.5 写入地址越界导致数据覆盖MRAM 的地址计数器在连续写超过地址上限时会回卷到 0。如果你的代码没有做地址边界检查写入 0x07FFFF 之后再继续写数据会悄悄写到 0x000000破坏了芯片头部的配置信息。这个错误非常隐蔽平时写小数据量不会触发一旦日志记录长度累计到某个值就出问题。所以我在驱动函数里强制加入了地址和长度检查越界直接返回失败而不是让硬件悄悄回卷。这是一个成本很低但能救命的习惯。7. 低功耗、睡眠模式与其他扩展点7.1 如何把整机功耗做下来如果设备靠电池供电MR25H40CDF 的低功耗特性就能派上用场。它支持 SLEEP 命令未访问时可以让芯片进入深度睡眠模式典型待机电流可以降到很低。需要访问时发送 WAKE 命令唤醒然后正常读写。配合 PIC18LF46K22 的 XLP 低功耗技术整机待机状态可以做到极低。我的实际做法是平时 MCU 进入 Sleep 模式MRAM 进入 Sleep 模式只有外部事件触发时唤醒做完读写操作后再同时进入睡眠。实测下来在 3.3V 供电下这套方案的待机电流能控制在微安级别完全满足电池供电的产品需求。需要注意的一点是从 SLEEP 唤醒到可以回应命令之间需要一定的恢复时间。代码里最好在 WAKE 之后插入几个空周期再去访问状态寄存器避免唤醒后立即通信失败。7.2 多设备共享 SPI 总线的扩展一个系统中可能有多个 SPI 从设备比如 MRAM、温度芯片、ADC 等。CS# 引脚一定要每个设备独立控制这样才能保证 MRAM 的命令帧不会被其他设备的通信干扰。共享 SPI 总线时另外一个容易忽视的问题是 MISO 引脚的信号可能被多个设备同时驱动。MRAM 在没有被选中时SO 引脚是高阻状态但如果某个设备设计不当未选中时仍然驱动数据线就会产生冲突。设计时最好确认所有同总线的从设备都支持三态输出。7.3 从这套驱动继续扩展的思路这套驱动写完以后可以很自然地扩展成一套通用非易失存储接口。比如在应用层封装 PutParam、GetParam 函数参数按 ID 存取或者把日志记录做成带时间戳的定长块。MRAM 的无限次擦写特性也会让调试时反复擦写参数区的体验变好很多不像 Flash 那样每次大规模测试都担心寿命用尽。如果你正在为一个频繁读写、数据不能丢的嵌入式项目选型MR25H40CDF 加 PIC18LF46K22 这套组合可以少走不少弯路。
返回列表