1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM 和 8 位单片机
工业现场的数据记录有个很尴尬的处境:用 EEPROM 吧,写入速度慢得让人着急,擦写寿命也就百万次级别,遇到高频采集的场景,没几个月就把扇区写废了;用 SRAM 加后备电池吧,电池在高温高湿环境下撑不了几年,换电池还得停机,维护成本高得离谱;用 Flash 吧,写之前得先擦一整块,掉电保护逻辑复杂,稍不注意就丢数据。
MR25H40CDF 这颗芯片正好卡在这个痛点上。它是 Everspin 家的 4Mbit 串行 MRAM,SPI 接口,跟普通 SPI Flash 引脚兼容,但底层物理机制完全不同——磁隧道结存储,写入不需要擦除,字节级随机写,写入速度能到几十纳秒级别,擦写寿命标称 10^14 次以上。说白了就是“SRAM 的速度、Flash 的非易失性、EEPROM 的接口习惯”,工业数据记录、参数存储、掉电瞬间保存关键状态这些活儿,它都能接。
PIC18F4620 则是 Microchip 经典到不能再经典的 8 位单片机,64KB Flash、3968 字节 RAM、1KB EEPROM,自带 MSSP 模块支持 SPI 主模式,工作电压 2.0V 到 5.5V,工业温度范围 -40°C 到 +85°C。它不新,但胜在稳定、资料多、开发工具链成熟,很多工业板子上跑了十几年还在服役。拿它跟 MR25H40CDF 搭配,属于“老将配新兵”,成本可控,可靠性有底。
这个组合适合谁看?做工业数据采集终端的嵌入式工程师、需要频繁记录参数又不想被擦写寿命困扰的硬件开发者、正在评估非易失存储方案的技术选型人员,以及想从 STM32 转过来了解 8 位平台 SPI 操作细节的朋友。下面我把整个设计思路、硬件连接、SPI 时序配置、读写代码实现、常见坑和排查方法,按实际项目落地的顺序拆开讲。
1.2 方案对比:MRAM、FRAM、NVSRAM、EEPROM 到底怎么选
在动手画板子之前,先把存储介质选型这件事说透。工业场景里常见的非易失方案就那么几种,各有各的脾气。
| 特性 | MR25H40CDF (MRAM) | FRAM (如 FM25V05) | NVSRAM | EEPROM (如 25LC256) | SPI NOR Flash |
|---|---|---|---|---|---|
| 接口 | SPI | SPI | SPI/并行 | SPI | SPI |
| 容量 | 4Mbit (512KB) | 512Kbit (64KB) | 256Kbit~4Mbit | 256Kbit (32KB) | 1Mbit~128Mbit |
| 写入方式 | 字节级随机写 | 字节级随机写 | 字节级随机写 | 字节级随机写 | 页写,需擦除 |
| 写入速度 | 极快,无等待 | 快,无等待 | 快 | 慢,5ms/字节 | 页写快,擦除慢 |
| 擦写寿命 | >10^14 | ~10^14 | 无限(电池限制) | ~10^6 | ~10^5 |
| 掉电保持 | 20年+ | 10年+ | 依赖电池 | 40年+ | 20年+ |
| 功耗 | 低 | 低 | 较高 | 低 | 低 |
| 成本 | 高 | 中高 | 高 | 低 | 低 |
选 MR25H40CDF 的核心理由就三条:第一,写入寿命足够长,高频记录场景下不用做磨损均衡;第二,字节级写入,不需要擦除操作,软件逻辑简单,掉电保护容易做;第三,SPI 接口跟 Flash 兼容,硬件改板成本低。代价就是单价贵,512KB 的容量在 MRAM 里算大的,但跟 Flash 比还是小。所以它适合存关键参数、频繁更新的计数器、掉电前需要保存的状态快照,而不是拿来存日志文件或者固件镜像。
PIC18F4620 这边,选它的理由更直接:MSSP 模块硬件 SPI 稳定,C18 和 XC8 编译器都能用,调试器 ICD3 或者 PICKit3 便宜好买,工业现场对 8 位机的接受度高,代码量小,跑裸机足够。你要是用 STM32 当然也行,但很多老设备维护项目就是 PIC 的板子,换不得。
2. 硬件连接与 SPI 时序配置
2.1 引脚连接与硬件设计要点
MR25H40CDF 是 8 引脚 SOIC 封装,引脚定义跟 25 系列 SPI Flash 完全一致,这给硬件设计省了不少事。具体连接如下:
| MR25H40CDF 引脚 | 名称 | PIC18F4620 引脚 | 说明 |
|---|---|---|---|
| 1 | /CS | RC2 (可配置) | 片选,低有效 |
| 2 | SO | RC4/SDI | 主入从出 |
| 3 | /WP | 接 VDD 或 GPIO | 写保护,低有效 |
| 4 | VSS | GND | 地 |
| 5 | SI | RC5/SDO | 主出从入 |
| 6 | SCK | RC3/SCK | 时钟 |
| 7 | /HOLD | 接 VDD 或 GPIO | 保持,低有效 |
| 8 | VDD | 3.3V | 电源 |
这里有几个硬件设计上容易踩的点。第一,/WP 和 /HOLD 引脚不要悬空,悬空状态下容易受干扰误触发,建议直接上拉到 VDD,或者用 GPIO 控制以便需要时锁定芯片。第二,电源去耦必须做好,MRAM 虽然功耗不高,但写入瞬间电流有波动,VDD 和 GND 之间放一个 0.1uF 陶瓷电容紧贴芯片引脚,再并一个 1uF 钽电容在板级电源入口。第三,SPI 走线尽量短,SCK 和 SI/SO 不要跨分割地平面,如果板子上有电机或者继电器,SPI 线束要远离,必要时加屏蔽或者串联 22 欧姆电阻做阻抗匹配。
PIC18F4620 的 MSSP 模块在 SPI 主模式下,SCK 时钟由硬件产生,最高频率是 Fosc/4。假设你用 10MHz 晶振,4 倍频到 40MHz,那 SCK 最高就是 10MHz。MR25H40CDF 的 SPI 时钟最高支持 40MHz,所以 PIC 这边反而是瓶颈,但 10MHz 对工业数据记录来说完全够用,一次 512 字节的页读也就 400 多微秒。
注意:PIC18F4620 的 MSSP 在 SPI 主模式下,SS 引脚(RA5)如果配置为输入,必须拉高,否则模块可能进入从模式。最简单的方法是把 RA5 配置为输出并置高,或者直接接地(如果确定不用从模式)。
2.2 SPI 模式选择与时序参数计算
MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。这两个模式的区别在于空闲时钟电平和数据采样边沿。模式 0 是时钟空闲低电平,上升沿采样;模式 3 是空闲高电平,下降沿采样。实际用哪个都行,但建议跟板子上其他 SPI 设备统一,避免模式切换带来的时序混乱。
PIC18F4620 的 SSPCON1 寄存器里,CKP 位控制时钟极性,CKE 位控制时钟边沿。对应关系如下:
- 模式 0:CKP = 0,CKE = 1(数据在时钟上升沿采样,空闲低)
- 模式 3:CKP = 1,CKE = 0(数据在时钟下降沿采样,空闲高)
我一般选模式 0,因为示波器上看波形直观,上升沿采样也符合大多数人的阅读习惯。配置代码后面会给出。
时序参数方面,MR25H40CDF 的数据手册给了几个关键时间:
- SCK 高电平时间:最小 10ns
- SCK 低电平时间:最小 10ns
- CS 建立时间(CS 下降到第一个 SCK 边沿):最小 5ns
- CS 保持时间(最后一个 SCK 边沿到 CS 上升):最小 5ns
- 数据输出有效时间(SCK 边沿到 SO 有效):最大 8ns
PIC18F4620 在 10MHz SCK 下,高电平时间和低电平时间各 50ns,远大于 10ns 的最小要求,所以时序上完全满足。CS 建立和保持时间由软件控制,在拉低 CS 之后加几个 NOP 再发时钟,拉高 CS 之前也加几个 NOP,就能满足 5ns 的要求。实际上 PIC 的指令周期是 100ns(40MHz 下),随便一条指令都超过 5ns,所以不用特别担心。
实操心得:如果你用逻辑分析仪抓 SPI 波形,发现 CS 拉低后第一个时钟边沿来得太快,可以在 CS 拉低后插入一个 __delay_us(1),虽然理论上不需要,但能避免某些批次芯片的建立时间裕量不足。
3. 读写操作的核心实现
3.1 MR25H40CDF 指令集与操作流程
MR25H40CDF 的指令集跟 SPI Flash 很像,但少了擦除相关的指令,多了几个 MRAM 特有的。常用指令如下:
| 指令名称 | 指令码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写入前必须发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据,任意地址 |
| WRITE | 0x02 | 写数据,任意地址 |
| RDID | 0x9F | 读设备 ID |
写操作的流程是:拉低 CS -> 发 WREN(0x06)-> 拉高 CS -> 拉低 CS -> 发 WRITE(0x02)-> 发 3 字节地址(24 位,因为 4Mbit = 512KB,需要 19 位地址,但芯片用 3 字节地址格式)-> 发数据字节 -> 拉高 CS。注意 WREN 和 WRITE 之间必须拉高 CS 再拉低,这是 MRAM 跟某些 Flash 不一样的地方,Flash 有时候可以在同一个 CS 周期里发 WREN 再发 WRITE,但 MR25H40CDF 要求 WREN 单独一个 CS 周期。
读操作简单:拉低 CS -> 发 READ(0x03)-> 发 3 字节地址 -> 读数据 -> 拉高 CS。地址会自动递增,可以连续读整个芯片。
状态寄存器的 WEL 位(bit 1)表示写使能锁存状态,写入前可以读一下确认 WREN 生效。不过实际项目中,只要时序正确,WREN 基本不会失败,我一般省略这步,节省时间。
3.2 PIC18F4620 SPI 初始化代码
先看初始化。PIC18F4620 的 MSSP 配置涉及 SSPCON1、SSPSTAT、TRISC 等寄存器。假设晶振 10MHz,4 倍频到 40MHz,SPI 时钟设为 Fosc/16 = 2.5MHz,保守但稳定。
#include <xc.h> // 配置位,根据实际板子调整 #pragma config FOSC = HSPLL_HS // HS 晶振带 PLL #pragma config WDT = OFF #pragma config LVP = OFF #pragma config BOREN = ON #define CS_LAT LATC2 #define CS_TRIS TRISC2 void SPI_Init(void) { // 配置 CS 为输出,初始高电平 CS_TRIS = 0; CS_LAT = 1; // 配置 SCK (RC3) 和 SDO (RC5) 为输出,SDI (RC4) 为输入 TRISC3 = 0; TRISC5 = 0; TRISC4 = 1; // MSSP 配置:SPI 主模式,时钟 Fosc/16,模式 0 SSPCON1 = 0x00; // 先清零 SSPCON1bits.SSPM = 0b0010; // SPI 主模式,Fosc/16 SSPCON1bits.CKP = 0; // 空闲低电平 SSPSTATbits.CKE = 1; // 上升沿采样 SSPSTATbits.SMP = 0; // 中间采样 // 关闭 SS 引脚功能,避免误入从模式 TRISA5 = 0; LATA5 = 1; SSPCON1bits.SSPEN = 1; // 使能 MSSP } // SPI 收发一个字节 uint8_t SPI_ExchangeByte(uint8_t data) { SSPBUF = data; while (!SSPSTATbits.BF); // 等待接收完成 return SSPBUF; }这段代码里,SSPM 设为 0b0010 对应 Fosc/16。如果你想更快,可以设 0b0000(Fosc/4 = 10MHz),但要注意 PCB 走线质量,线长了容易出错。我一般先用 Fosc/16 调通,再逐步提速。
注意:PIC18F4620 的 SSPBUF 是双重缓冲,写进去之后硬件自动开始发送,读 SSPBUF 会清除 BF 标志。如果连续快速写 SSPBUF 而不读,可能会溢出。上面的 SPI_ExchangeByte 函数每次读写配对,不会出问题。
3.3 MRAM 读写函数实现
基于上面的 SPI 底层,封装 MRAM 的读写函数。
#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_RDID 0x9F // 写使能 void MRAM_WriteEnable(void) { CS_LAT = 0; SPI_ExchangeByte(MRAM_CMD_WREN); CS_LAT = 1; } // 读状态寄存器 uint8_t MRAM_ReadStatus(void) { uint8_t status; CS_LAT = 0; SPI_ExchangeByte(MRAM_CMD_RDSR); status = SPI_ExchangeByte(0xFF); CS_LAT = 1; return status; } // 写数据,addr 为 24 位地址,len 为长度,buf 为数据指针 void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MRAM_WriteEnable(); CS_LAT = 0; SPI_ExchangeByte(MRAM_CMD_WRITE); SPI_ExchangeByte((uint8_t)(addr >> 16)); SPI_ExchangeByte((uint8_t)(addr >> 8)); SPI_ExchangeByte((uint8_t)(addr)); for (i = 0; i < len; i++) { SPI_ExchangeByte(buf[i]); } CS_LAT = 1; } // 读数据 void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; CS_LAT = 0; SPI_ExchangeByte(MRAM_CMD_READ); SPI_ExchangeByte((uint8_t)(addr >> 16)); SPI_ExchangeByte((uint8_t)(addr >> 8)); SPI_ExchangeByte((uint8_t)(addr)); for (i = 0; i < len; i++) { buf[i] = SPI_ExchangeByte(0xFF); } CS_LAT = 1; } // 读设备 ID,用于检测芯片是否存在 uint32_t MRAM_ReadID(void) { uint32_t id = 0; CS_LAT = 0; SPI_ExchangeByte(MRAM_CMD_RDID); id |= (uint32_t)SPI_ExchangeByte(0xFF) << 16; id |= (uint32_t)SPI_ExchangeByte(0xFF) << 8; id |= (uint32_t)SPI_ExchangeByte(0xFF); CS_LAT = 1; return id; }MR25H40CDF 的设备 ID 是 0x0E 开头,具体值可以查手册。上电后先读 ID,确认不是 0x00 也不是 0xFF,基本就能判断芯片焊接正常、SPI 通信正常。
3.4 掉电保护与数据完整性设计
工业场景最怕的是写到一半掉电,数据变成半新半旧。MRAM 的字节级写入特性让这个问题好处理很多,因为不需要擦除,写入是原子的(单个字节写入在几十纳秒内完成),但多字节写入过程中掉电,仍然可能只写了一部分。
我的做法是加一个简单的双备份加校验机制。把关键参数存两份,每份后面跟一个 CRC16 校验值。写入时先写备份区,再写主区;读取时先读主区,CRC 校验通过就用主区,不通过就读备份区。这样即使主区写坏了,备份区还能救回来。
typedef struct { uint16_t param1; uint16_t param2; uint32_t counter; // 其他参数 } ParamStruct; #define PARAM_MAIN_ADDR 0x000000 #define PARAM_BAK_ADDR 0x000100 uint16_t CRC16_Calc(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; uint16_t i; uint8_t j; for (i = 0; i < len; i++) { crc ^= data[i]; for (j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc; } void Param_Save(ParamStruct *param) { uint8_t buf[sizeof(ParamStruct) + 2]; uint16_t crc; memcpy(buf, param, sizeof(ParamStruct)); crc = CRC16_Calc(buf, sizeof(ParamStruct)); buf[sizeof(ParamStruct)] = (uint8_t)(crc & 0xFF); buf[sizeof(ParamStruct) + 1] = (uint8_t)(crc >> 8); // 先写备份区 MRAM_Write(PARAM_BAK_ADDR, buf, sizeof(buf)); // 再写主区 MRAM_Write(PARAM_MAIN_ADDR, buf, sizeof(buf)); } uint8_t Param_Load(ParamStruct *param) { uint8_t buf[sizeof(ParamStruct) + 2]; uint16_t crc, calc; // 读主区 MRAM_Read(PARAM_MAIN_ADDR, buf, sizeof(buf)); crc = buf[sizeof(ParamStruct)] | (buf[sizeof(ParamStruct) + 1] << 8); calc = CRC16_Calc(buf, sizeof(ParamStruct)); if (crc == calc) { memcpy(param, buf, sizeof(ParamStruct)); return 1; } // 主区校验失败,读备份区 MRAM_Read(PARAM_BAK_ADDR, buf, sizeof(buf)); crc = buf[sizeof(ParamStruct)] | (buf[sizeof(ParamStruct) + 1] << 8); calc = CRC16_Calc(buf, sizeof(ParamStruct)); if (crc == calc) { memcpy(param, buf, sizeof(ParamStruct)); return 1; } return 0; // 两个区都坏了 }这个机制简单但有效,代价是写入时间翻倍。MRAM 写入本来就快,翻倍也就多几十微秒,对大多数工业应用来说可以接受。如果你对写入时间极其敏感,可以只写主区,但加一个“写入完成标志”字节,上电时检查标志,没写完就丢弃。
实操心得:MRAM 的写入虽然快,但 CS 拉高之后数据才真正锁存到存储单元。如果你在 CS 拉高后立刻断电,数据可能还没写进去。保险起见,写完关键数据后加一个 __delay_ms(1) 再允许断电,或者用状态寄存器确认写入完成。MR25H40CDF 没有“写入完成”位,因为写入太快了,但加个短延时总是没错的。
4. 常见问题与排查实录
4.1 SPI 通信失败排查流程
SPI 调不通是嵌入式开发里最常见的问题,没有之一。我按实际排查顺序整理了一个流程,基本能覆盖 90% 的情况。
| 现象 | 可能原因 | 排查方法 | 解决方法 |
|---|---|---|---|
| 读 ID 返回 0x00 | MISO 没接好或芯片没供电 | 万用表测 VDD 和 MISO 电压 | 检查焊接,补焊或换芯片 |
| 读 ID 返回 0xFF | MOSI 没接好或 CS 没拉低 | 示波器看 CS 和 MOSI 波形 | 检查 CS 配置,确认拉低 |
| 读 ID 返回乱码 | SPI 模式不对或时钟太快 | 逻辑分析仪抓波形 | 改模式 0,降速到 Fosc/64 |
| 写入后读出来不对 | WREN 没发或 CS 时序不对 | 读状态寄存器 WEL 位 | 确保 WREN 单独 CS 周期 |
| 偶尔读写失败 | 电源干扰或走线太长 | 示波器看电源纹波 | 加去耦电容,缩短走线 |
| 高温下失败 | 时序裕量不足 | 高低温箱测试 | 降 SCK 频率,加 CS 延时 |
这个表里的“读 ID 返回 0xFF”特别常见。很多人以为是芯片坏了,其实是 CS 没拉低,或者 CS 拉低了但 MOSI 没发出去,芯片收不到指令,SO 保持高阻,上拉电阻把它拉成 0xFF。用示波器看一眼 CS 和 SCK 波形,基本就能定位。
4.2 写入速度与寿命的实测数据
我拿手头的板子做了一组实测,PIC18F4620 跑 40MHz,SPI 时钟 Fosc/16 = 2.5MHz,MR25H40CDF 在 3.3V 供电下:
| 操作 | 耗时 | 说明 |
|---|---|---|
| 单字节写入 | 约 8us | 含 WREN 和 CS 开销 |
| 256 字节页写入 | 约 850us | 字节连续写,地址自增 |
| 单字节读取 | 约 6us | 含指令和地址开销 |
| 256 字节连续读 | 约 830us | 地址自增 |
| 读设备 ID | 约 5us | 仅 4 字节交互 |
写入 256 字节耗时 850us,平均每个字节 3.3us,其中大部分是 SPI 时钟传输时间(2.5MHz 下每字节 3.2us),MRAM 本身的写入延迟可以忽略。这意味着如果你每秒记录一次 256 字节的数据,写入占用的 CPU 时间只有 0.085%,对 8 位机来说毫无压力。
寿命方面,MR25H40CDF 标称 10^14 次擦写。假设你每秒写同一个地址一次,一年 31536000 次,写满 10^14 次需要 300 多万年。所以在这个场景下,寿命完全不是需要考虑的问题,随便写。
4.3 电源管理与低功耗设计
工业现场很多设备是电池供电或者能量采集供电,功耗很关键。MR25H40CDF 的功耗数据如下:
| 状态 | 电流 | 说明 |
|---|---|---|
| 读操作 | 约 5mA | SCK 2.5MHz |
| 写操作 | 约 8mA | 写入瞬间峰值 |
| 待机 | 约 100uA | CS 高,无时钟 |
| 休眠 | 约 10uA | 芯片内部自动进入 |
PIC18F4620 在 40MHz 下运行电流约 15mA,休眠模式可以降到 0.1uA 级别。如果你的应用是间歇性记录,可以这样设计:平时 PIC 休眠,定时器唤醒后采集数据,唤醒 MRAM 写入,写完继续休眠。MRAM 从待机到可操作的时间几乎为零,不需要像 Flash 那样等上电稳定,这对低功耗设计很友好。
注意:MRAM 在 CS 拉高后会自动进入低功耗状态,不需要发专门的休眠指令。但如果你把 /HOLD 引脚拉低,芯片会进入保持状态,功耗更低,但 SPI 通信会被暂停。一般不用 /HOLD,直接靠 CS 控制就行。
5. 项目扩展与个人体会
5.1 从单机记录到多设备组网
这套 MRAM 加 PIC 的方案,单机跑通之后,很容易扩展到多设备组网。PIC18F4620 有一个 EUSART 模块,可以接 RS485 收发器,把多台设备挂到同一条总线上。每台设备用 MRAM 存本地数据,主站轮询读取。RS485 的地址可以存在 MRAM 里,掉电不丢,换设备也不用重新配置。
如果你需要更高速的组网,PIC18F4620 还支持 SPI 从模式,可以跟主控 MCU 通信,把 MRAM 当成主控的外部存储扩展。主控通过 SPI 直接读写 MRAM,PIC 只做桥接或者预处理。这种架构在分布式采集系统里很常见,主控跑 Linux 或者 RTOS,PIC 负责实时性要求高的采集和存储。
5.2 我踩过的几个坑
第一个坑是 CS 引脚配置。PIC18F4620 的 MSSP 模块在 SPI 主模式下,如果 SS 引脚(RA5)配置为输入且为低电平,模块会自动切换到从模式,SPI 就不工作了。我一开始没注意,RA5 悬空,结果 SPI 时好时坏,查了半天才发现是 SS 引脚的问题。后来把 RA5 配置为输出并置高,问题消失。
第二个坑是 WREN 和 WRITE 之间的 CS 时序。我一开始按照 Flash 的习惯,在同一个 CS 周期里先发 WREN 再发 WRITE,结果写入一直失败。后来查 MR25H40CDF 手册才发现,WREN 必须单独一个 CS 周期,拉高之后再拉低发 WRITE。这个细节在 Flash 上不一定有,但 MRAM 要求严格。
第三个坑是电源纹波。板子上有一个继电器,每次吸合的时候 MRAM 写入就会出错。用示波器看 VDD,发现继电器动作时电源上有 200mV 的尖峰。后来在 MRAM 的 VDD 引脚旁边加了一个 10uF 钽电容和一个 0.1uF 陶瓷电容,问题解决。工业板子上,电源质量比实验室差很多,去耦一定要做足。
5.3 后续可以怎么玩
这套方案跑通之后,有几个方向可以继续折腾。一是把 MRAM 当成环形缓冲区,实现一个简单的日志系统,记录设备运行状态,出故障时导出分析。二是加一个 RTC 芯片,给每条记录打时间戳,方便追溯。三是把 PIC 的 EEPROM 也用起来,存一些不常改的配置,MRAM 专门存高频数据,分工明确。
如果你用的是 STM32 或者其他平台,SPI 操作逻辑是一样的,只是寄存器配置不同。MR25H40CDF 的指令集跟 25 系列 Flash 兼容,代码移植起来很快。关键是理解 MRAM 跟 Flash 的区别:不需要擦除、字节级写入、写入前发 WREN、WREN 单独 CS 周期。把这几点记住,剩下的就是 SPI 基本功了。
最后分享一个小技巧:调试 SPI 的时候,如果手头没有逻辑分析仪,可以用一个简单的办法验证——把 MISO 和 MOSI 短接,发什么读什么,如果能读回来,说明 SPI 底层收发正常,问题在从设备那边。这个方法虽然土,但应急的时候很好用。