PIC18F97J60外扩MRAM实现工业级非易失数据存储方案

📅 发布时间:2026/10/4 2:01:39
PIC18F97J60外扩MRAM实现工业级非易失数据存储方案
几个月前接手一个老项目改造主角是 PIC18F97J60一颗自带以太网 MAC 和 PHY 的 8 位单片机。原来它只管把传感器数据打包成 Modbus TCP 送上位机跑了几年一直很稳。结果甲方今年提了新需求所有原始波形和运行日志必须本地留存断电不能丢而且要能追溯至少一年。这就尴尬了——芯片内 Flash 才 128KB程序空间都快占满了RAM 只有 3.8KB 级别想在本地存几十万条带时间戳的数据记录纯粹天方夜谭。于是重新翻了一圈外部存储方案。最后定下来的是 Everspin 的 MR25H40CDF一颗 4Mbit 的 SPI 接口 MRAM配 PIC18F97J60 的 MSSP1 模块。这套组合在工业现场已经稳定跑了两个多月期间经历了多次随机断电和一次雷击导致的电压跌落数据一条没丢。这篇博文就把这套存储方案的选型逻辑、硬件接线、驱动代码、数据布局以及实际调试中踩过的坑完整分享一下给准备在 8 位 MCU 上做工业非易失存储的同行一个参考。1. 从需求倒推选型为什么最终锁定 MR25H40CDF1.1 MRAM 和 EEPROM/Flash 的本质差异先说需求本质工业现场的数据记录写入频率远比家用场景高。一台设备每隔几秒就要追加一条日志如果按照十年使用寿命估算存储器的擦写次数会轻松突破百万级。这个数字对普通 SPI EEPROM 来说已经是极限附近了对 SPI NOR Flash 更是灾难——NOR Flash 写入前必须先擦除整个扇区而且典型擦写寿命只有十万次量级。有人可能会说SPI NOR Flash 不是也有磨损均衡算法吗对消费级 SD 卡和 U 盘里就是这么干的但那是靠主控芯片里的 FTL 层。在 PIC18F97J60 这种 8 位 MCU 上自己实现一套磨损均衡既要管理块映射表又要处理擦除失败和坏块替换代码量轻松多出上千行而且每次写入前都要做读-改-擦-写四步操作实时性完全没法保证。MRAM磁阻式随机存取存储器则完全是另一种脾气它的存储单元是磁隧道结写入靠电流改变磁化方向不依赖电荷保持所以写入前不需要擦除直接覆盖写就完事。这就意味着写寿命几乎无限标称可达 10 的 14 次方以上工业现场随便造写入速度和读速度一样快不存在先擦除再编程的等待时间掉电后数据保持靠磁性状态不需要后备电池也不存在 EEPROM 那种写周期中途掉电导致数据半新半旧的问题。这正是工业数据记录最需要的东西。实测中我用 10MHz 的 SPI 时钟连续写 1KB 数据从调用驱动到返回整个过程不到 1ms这在 NOR Flash 上简直不敢想象。1.2 MR25H40CDF 这颗芯片的关键规格MR25H40CDF 是 Everspin 的 MR25H40 系列中的 DFN-8 封装型号容量 4Mbit也就是 512KB按字节组织。下面把它和常见的 SPI EEPROM、SPI NOR Flash 放一起对比差异一目了然。参数MR25H40CDF典型 SPI EEPROM如 25AA256典型 SPI NOR Flash如 W25Q64容量4Mbit / 512KB256Kbit / 32KB64Mbit / 8MB写入方式直接覆盖写直接覆盖写先擦除扇区再写入写寿命大于 10 的 14 次方约 100 万次约 10 万次擦除单位无无通常 4KB 扇区SPI 时钟最高 40MHz保守可按 10MHz 使用通常 10MHz 左右通常 50-133MHz但受擦除拖累数据保持大于 20 年约 100 年约 20 年典型写一个字节耗时一个 SPI 时钟周期 极短的内部写入约 5ms 内部写入页编程约 1ms但之前要擦除工业温度范围-40 到 85 摄氏度-40 到 85 摄氏度-40 到 85 摄氏度注意那个写一个字节的对比差距是数量级的。EEPROM 一个字节写入要等待内部的电荷泵完成通常 5ms 左右NOR Flash 则是先擦除几百毫秒再编程几毫秒MR25H40CDF 的写入在 SPI 层面就是普通的数据传输芯片内部几乎没有额外等待最多看一眼状态寄存器里的 WIP 位确认上一笔写完成。对频繁写日志的场景这种差异直接决定了整个系统的实时性。1.3 为什么用 PIC18F97J60 而不是换平台既然数据量这么大为什么不干脆换一颗带大 Flash 的 Cortex-M 系列 MCU这里有个很现实的问题原产品已经量产多年PCB、上位机协议、测试工装都是围绕 PIC18F97J60 搭的。PIC18F97J60 的优势在于集成度——一颗 80 引脚的片子把 MCU、以太网 MAC、以太网 PHY 全包了BOM 成本低布板简单外围只需一个网络变压器就能上网。对工业网关、远程 IO、协议转换器这类应用这颗片子的算力完全够用。换平台意味着整个软件栈重写协议栈、启动流程、看门狗逻辑、老客户现场的升级包全部要跟着动风险和成本都不可控。所以最终方案是保留 PIC18F97J60 作为主控外扩一片 MR25H40CDF 专门做非易失数据存储两者用 SPI 连接。这个思路也符合工业产品升级的一贯做法——不动成熟的逻辑处理部分只在外围存储上做加法。2. 硬件连接低成本把 MR25H40CDF 接上 PIC18F97J602.1 SPI 引脚连接和端口分配MR25H40CDF 是标准 SPI 从设备8 个引脚里真正要接主控的就是六根CS、SCK、SI、SO、VDD、VSS。另外两根 HOLD 和 WP 是功能控制脚下面的小节专门说。我这边用的是 PIC18F97J60 的 MSSP1 模块配置成 SPI 主模式。连接方式如下具体引脚编号不同封装有差异以你自己板子的数据手册为准PIC18F97J60 侧MR25H40CDF 侧说明SCK1SCKSPI 时钟由主控输出SDO1SI主出从入MRAM 的数据输入SDI1SO主入从出MRAM 的数据输出RA0任意 GPIOCS片选低电平有效由软件控制RA1任意 GPIOWP写保护低电平有效RA2任意 GPIOHOLD暂停通讯低电平有效3.3VVDD供电需去耦电容GNDVSS接地这里 CS 和 HOLD/WP 都用普通 GPIO 控制没有把 CS 接到 MSSP 硬件片选上。原因很简单MSSP 的硬件 CS 有时序约束不适合多字节交易的软件裁剪。我们自己用 GPIO 拉低拉高可以精确控制每笔读写的边界代码也更直观。2.2 电源去耦和引脚处理供电方面MR25H40CDF 工作在 3.3V和 PIC18F97J60 同电压域不需要电平转换。但 MRAM 写操作瞬间电流变化比较快VDD 引脚旁边必须有 100nF 陶瓷电容紧贴芯片放置我习惯再并一个 10uF 钽电容做低频缓冲。千万别省这个小电容我见过有人把去耦电容省掉SPI 线上稍微有点干扰读回来的数据就偶发错位。HOLD 和 WP 这两根脚一定要显式接主控 GPIO 或直接拉高不要悬空。HOLD 是通讯暂停脚一旦被拉低芯片会忽略 SCK 上的边沿数据传一半就卡住。WP 是写保护脚低电平时写使令会被屏蔽。MR25H40CDF 内部虽然有一定的弱上拉但工业现场电磁环境复杂我还是把这两根脚接到了 GPIO 上初始化时默认输出高电平既实现了功能可控也避免悬空引入噪声。2.3 DFN 封装焊接和 PCB 注意点MR25H40CDF 是 DFN-8 封装底面有一块裸露的散热焊盘。好消息是这颗芯片功耗不大散热焊盘的主要作用其实是机械固定和接地。PCB Layout 上要注意散热焊盘直接用过孔连到地平面不要把散热焊盘只走一条细线否则芯片焊好后容易虚焊手工样板焊接时先在焊盘上均匀涂一层薄焊膏芯片放正后用热风枪 300 到 320 度吹看到焊料融化润湿即可。不要用烙铁直接怼引脚DFN 引脚间距小容易连锡连锡了也不用慌上助焊剂后用烙铁头在引脚侧面轻轻一拖多余的锡会被带走。这些细节看似不起眼但 DFN 封装如果焊不好会出现一种极难排查的故障模式——常温下好使温度一变化接触电阻漂移读回来数据时好时坏。直接用 SOP-8 封装的 MR25H40CDC 能避开这个问题但体积略大看产品结构怎么取舍。3. 指令集与时序看懂 MR25H40CDF 在 SPI 线上怎么工作3.1 指令速查与状态寄存器MR25H40CDF 的指令集和 SPI NOR Flash 很像但少了一大堆和擦除、保护相关的操作。实际应用中最常用的就这么几条指令操作码功能注意事项WREN0x06写使能置位状态寄存器中的 WEL 位每次写数据前必须先发这条CS 必须有一次完整的低到高边沿WRDI0x04写禁用清除 WEL 位上电默认 WEL 是 0可以不用RDSR0x05读状态寄存器关注 bit0 WIP 和 bit1 WELWRSR0x01写状态寄存器常规场景可以完全不碰READ0x03读数据操作码后跟 3 字节地址WRITE0x02写数据操作码后跟 3 字节地址再跟要写入的数据SLEEP0xB9进入休眠降低待机电流唤醒用 0xABWAKE0xAB唤醒芯片唤醒后需要等待一小段时间再操作状态寄存器里最重要的两位是 bit0WIP和 bit1WEL。WIP 是写入进行中标志MRAM 虽然写入很快但连续高频写时还是建议查一下这个位WEL 是写使能锁存标志它必须由 WREN 指令置位。发送 WREN 之后芯片会在 CS 从低拉高的那一刻把 WEL 置 1如果 CS 时序不对WEL 就置不上后面的 WRITE 会被芯片直接忽略。3.2 读出时序拆解READ 指令的线上格式是先拉低 CS发送操作码 0x03紧接着发送 3 字节地址高位在前然后主控持续输出 SCK芯片就会从地址处开始一个字节一个字节地把数据送到 SO 线上。地址发完后的第一个 SCK 上升沿SO 上就出现数据的最高位。这里有个关键点MR25H40CDF 的地址是 24 位但实际容量只有 512KB0x80000所以高 5 位地址必须填 0否则行为未定义。我代码里直接用(addr 16) 0xFF取出地址最高字节高位自然就是 0不会有问题。连续读模式不需要反复发送指令——只要 CS 保持低电平地址会自动递增。这个特性非常适合一次性把一整块配置数据搬回 RAM。64KB 的参数区按 10MHz 的 SPI 时钟算全部读完也就 50ms 左右。3.3 写入时序拆解写入比读取多一道前置手续。完整流程是拉低 CS发送 0x06WREN立刻拉高 CS让 WEL 锁存读状态寄存器确认 bit1 是 1这一步相当于握手校验拉低 CS发送 0x02WRITE再发 3 字节地址随后把数据一个字节一个字节地写进 SI 线全部数据发完后拉高 CS此时芯片开始内部写入读状态寄存器等 WIP 变成 0。WRITE 指令同样支持连续写地址自动递增没有 NOR Flash 那种页边界限制。也就是说你完全可以一次性写 32KB 数据只要 CS 不放芯片就会一路写下去。这个特性对日志记录太友好了逻辑上可以当一块无限次擦写的 RAM 用完全不用顾虑这页擦过了没。4. 驱动代码在 PIC18F97J60 上把 MRAM 跑起来4.1 MSSP1 初始化PIC18F97J60 的 MSSP1 模块可以配置成多种模式我们这里只要 SPI 主模式。初始化代码很短但每个位都要理解。我最终用的是 SPI Mode 0即空闲时钟为低、第一个边沿采样数据。#define MRAM_SIZE (512UL * 1024UL) void spi1_init(void) { // SSP1STAT: SMP0 (中间采样), CKE0 (空闲低,下降沿发送) SPI Mode 0 SSP1STAT 0x00; // SSP1CON1: WCOL0, SSPOV0, SSPEN1, CKP0, SSPM0001 (SPI Master, FOSC/16) SSP1CON1 0x31; // 初始化 CS、HOLD、WP 三个 GPIO默认高电平 MRAM_HOLD_TRIS 0; MRAM_WP_TRIS 0; MRAM_CS_TRIS 0; MRAM_HOLD_LAT 1; MRAM_WP_LAT 1; MRAM_CS_LAT 1; }这里SSP1CON1 0x31指的是 SSPM 段设为 0001也就是 SPI 主模式时钟为 FOSC/16。我在项目里主时钟是 40MHz所以 SPI 时钟实际是 2.5MHz。MR25H40CDF 最高支持 40MHz2.5MHz 是相当保守的速度换来的是极稳的时序冗余。如果你要追求吞吐量可以逐步把分频调到 FOSC/4但至少先按保守参数跑通。4.2 基础读写函数SPI 单字节交换是基础MSSP 的 BF 位Buffer Full在发送完成且接收缓冲有数据时被硬件置 1读 SSP1BUF 会同时清零。代码如下unsigned char spi1_transfer(unsigned char data) { SSP1BUF data; while (!SSP1STATbits.BF) { // 等待传输完成 } return SSP1BUF; }注意这里千万不要在循环里加什么delaySPI 传输由硬件完成BF 置位就是确凿的完成标志。加延时反而可能在数据未真正出完时写坏下一步时序。MRAM 基础指令的实现如下#define MRAM_CS_LOW() MRAM_CS_LAT 0 #define MRAM_CS_HIGH() MRAM_CS_LAT 1 unsigned char mram_read_status(void) { unsigned char status; MRAM_CS_LOW(); spi1_transfer(0x05); // RDSR status spi1_transfer(0x00); // 时钟驱动数据输出 MRAM_CS_HIGH(); return status; } void mram_write_enable(void) { MRAM_CS_LOW(); spi1_transfer(0x06); // WREN MRAM_CS_HIGH(); // CS 上升沿锁存 WEL } void mram_wait_busy(void) { while (mram_read_status() 0x01) { // WIP 为 1 时继续等待 } }4.3 业务层 API 封装有了基础函数读写任意地址就顺理成章了。我封装了mram_read和mram_write调用方只需要传入起始地址、缓冲区指针和长度完全不用关心指令细节void mram_read(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_CS_LOW(); spi1_transfer(0x03); // READ spi1_transfer((unsigned char)(addr 16)); // 地址高位 spi1_transfer((unsigned char)(addr 8)); spi1_transfer((unsigned char)addr); for (i 0; i len; i) { buf[i] spi1_transfer(0x00); } MRAM_CS_HIGH(); } unsigned char mram_write(unsigned long addr, const unsigned char *buf, unsigned int len) { unsigned int i; if ((addr len) MRAM_SIZE) { return 0xFF; // 地址越界 } mram_write_enable(); if ((mram_read_status() 0x02) 0) { return 0xFE; // WEL 未置位写入拒绝 } MRAM_CS_LOW(); spi1_transfer(0x02); // WRITE spi1_transfer((unsigned char)(addr 16)); spi1_transfer((unsigned char)(addr 8)); spi1_transfer((unsigned char)addr); for (i 0; i len; i) { spi1_transfer(buf[i]); } MRAM_CS_HIGH(); mram_wait_busy(); return 0; // 成功 }这个写函数做了两件防御性的事。第一件是地址越界检查MRAM 连续写会地址自动递增如果写指针越过 0x7FFFF 会回卷到 0x00000可能把配置区覆盖掉必须在入口拦住第二件是写完 WREN 后立刻读状态寄存器确认 WEL 已经置位如果没置位说明硬件连接或者 CS 时序有问题提前返回错误码而不是带着错误状态继续写数据。4.4 关于 3 字节地址和自动递增的处理代码里地址被拆成三个字节发送这个细节非常重要。MR25H40CDF 使用 24 位地址虽然只用到低 19 位。拆地址时高位在后和大多数 SPI 存储芯片一致。自动递增的坑在于跨越 512KB 边界时地址会回卷而且很多 SPI 存储芯片在地址回卷时并不会给任何警告。所以驱动层必须由自己维护一个当前写指针 本次长度的合法性判断。我在mram_write里已经加了检查上层调用时还要注意如果连续写多个不连续的数据块每个块都要保证 addr len 在合法范围内。5. 工业数据布局把 512KB 用明白5.1 分区规划表512KB 说大不大说小也不小但完全不规划就裸奔肯定出问题。我按照工业数据的特点把整个地址空间手工划分成五个区起始地址大小用途0x0000064KB引导和版本信息几乎不写0x1000064KB配置参数区 A主用0x2000064KB配置参数区 B备份0x30000192KB运行日志环形区0x6000064KB采集数据暂存区0x7000064KB出厂校准区生产时一次写入这样分区的逻辑很直接引导区只读参数区双备份日志区使用最频繁也最大数据区给波形这类相对比较大的记录块校准区在生产时一次性烧录。每个区之间还留了一点地址空间作为隔离带万一日志区指针写飞了不至于立刻撞上参数区。5.2 参数区双备份和校验工业设备的配置参数包括设备地址、IP 配置、量程、报警阈值等丢失会导致现场设备失联。参数区的写入策略是双备份加 CRC16 校验。写流程如下把新参数序列化到内存缓冲区计算整个缓冲区的 CRC16追加在末尾先写参数区 B备份区写完做一次读回校验再写参数区 A主用区同样读回校验如果中途掉电导致某个区数据不完整下次上电时用另一个区恢复。读配置时优先读 A 区CRC16 通过就直接用不通过就回退读 B 区如果两个区都坏了才恢复出厂默认值。这片 MRAM 的写寿命远超产品寿命双备份的目的不是为了抗磨损而是为了对付写入过程中可能发生的异常掉电。虽然 MRAM 本身不会像 EEPROM 那样写一半丢数据但应用层的多字段更新依然需要原子性保障双备份是最简单可靠的手段。CRC16 可以采用标准 CRC-CCITT 多项式 0x1021代码很成熟这里给一个常用的逐字节实现unsigned short crc16_update(unsigned short crc, unsigned char byte) { unsigned char i; crc ^ (unsigned short)byte 8; for (i 0; i 8; i) { if (crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } return crc; }5.3 日志区环形覆盖和写指针日志区的设计是这套方案里最沾光的。日志需要高频追加写MR25H40CDF 的无限写寿命让我们可以彻底抛弃磨损均衡这个包袱。环形队列的实现思路在参数区里固定保存两个 4 字节变量日志写指针和日志读指针每次追加日志时从写指针地址开始写一条记录记录头是时间戳记录尾是长度字段然后更新写指针写指针走到日志区末尾就回卷到区首日志读走时用读指针读一条走一格读追到写指针说明日志已经消费完。如果用 NOR Flash 做同样的事每覆盖一条旧日志都要先把整个扇区擦掉不仅慢反复擦写还会快速耗尽寿命。而 MRAM 的覆盖写就是直接往地址上写新数据完全不需要关心旧数据是什么。日志区在实测中最繁忙的时候每两秒写入一条 64 字节的记录跑满一年也就写入 1500 万次对 MRAM 来说连热身都算不上。5.4 掉电场景的一致性处理工业设备最怕的就是写数据时突然掉电。对 MRAM 来说单次写在物理层是即时完成的但业务层的多步操作依然需要设计一致性。我的做法是给每条日志记录加一个固定 magic 头记录头写入成功后再把长度字段写进去。其他逻辑判断记录有效性时必须同时校验 magic 头和长度字段缺一个就视为无效记录读指针直接跳过。配置参数区的双备份已经覆盖了多步更新的一致性日志区用 magic 头加长度字段覆盖单条记录的完整性整个数据布局在面对随机断电时就有了基本保证。实测中我们用继电器随机切断设备电源反复断电十几轮重启后配置参数均能正确恢复日志区最多丢最后一条不完整的记录后面自动续写。6. 实测中踩过的坑和排查链路6.1 写入读回全是 0xFFWEL 没置位第一次上板调试我的排查链路是这样的先发 WREN再写一个字节到地址 0x00000然后读回来结果全是 0xFF。写操作完全没生效。按顺序查了三样东西。第一用逻辑分析仪看 SI 线上的数据确认 0x06 真的发出去了。第二检查 CS 时序发现我当时是在spi1_transfer返回后立刻拉高 CS表面上没问题但实际翻代码发现第一版驱动在 WREN 发送后没有读 SSP1BUF 清 BF导致 MSSP 模块状态机还停留在上一轮传输的尾巴上CS 被过早拉高WEL 没有锁存成功。第三读状态寄存器确认确实 WEL 一直是 0。这个坑的教训是WREN 指令对 CS 上升沿的时序极其敏感发送完 0x06 后必须确保该字节已经完整移出才能拉高 CS。老实用spi1_transfer并且等待 BF 置位再拉 CS问题自然消失。6.2 HOLD 引脚悬空导致偶发抖动系统跑了一阵子后出现一个非常恶心的现象大多数时间读写都正常但偶尔读回来的一整块数据里会有一两个字节是旧的位置不固定。一开始怀疑 SPI 时钟太快降到 FOSC/64 依然偶发。后来用示波器同时抓 CS、SCK 和 SO 三根线蹲了十几分钟终于抓到一次异常SO 线上本该连续输出的数据中间出现了一个格外长的电平保持段就像芯片突然暂停了移位输出。排查到最后发现 HOLD 引脚一直是悬空的PCB 布线时它旁边恰好跑了一根 24V 继电器驱动线继电器吸合瞬间的电场耦合把 HOLD 电平瞬间压到低电平芯片暂停工作等干扰消失后继续输出数据已经错位了。把 HOLD 引脚焊根飞线接到 3.3V 之后这个现象再也没出现过。这个案例说明MRAM 的 HOLD 和 WP 引脚绝不是可有可无的工业环境里必须明确接高电平或由 GPIO 驱动悬空等于把可靠性交给运气。6.3 连续写越界回卷覆盖参数区日志环形队列刚写完时测试一切正常。直到某次模拟日志写满后继续覆盖的边界场景发现配置参数被改写了。追查原因环形队列的写指针在回卷计算时有个 off-by-one 错误回卷后的新地址没有判断是否越过日志区的末尾而是直接当作区内地址写入。MR25H40CDF 连续写地址自动递增一旦越过 0x7FFFF 就回卷到 0x00000恰好就是引导区。这次写越界覆盖的不是配置区但配置区的风险同样存在。修复方式就是我在驱动里加的那个越界判断每次写入前用addr len MRAM_SIZE拦截。但更重要的教训是上层业务逻辑也必须严格审计自己的地址计算不能指望驱动层兜底。驱动层挡住的是物理地址越界业务层挡住的是逻辑分区越界两层都要查。6.4 SPI 模式配置与示波器验证MR25H40CDF 支持 SPI Mode 0 和 Mode 3代码注释里写清楚了。但实际调试中发现把 MSSP 配成 Mode 0 后读状态寄存器返回的值是 0xFF问题出在初始化代码里SSP1STAT和SSP1CON1的写入顺序上——MSSP 模块必须先设置模式再使能SSPEN如果反过来部分位会被模块状态机重置。正确顺序是先写SSP1CON1的模式段和SSP1STAT的极性段最后再置SSPEN。这种寄存器顺序问题在数据手册里写得很隐蔽但踩过一次后就不会再犯。验证方法就是拿逻辑分析仪看 SCK 空闲电平Mode 0 空闲为低Mode 3 空闲为高一眼就能分辨。6.5 一个小经验上电先等一下再初始化 MRAM芯片手册上写了上电到第一个有效指令之间需要一小段稳定时间。我刚开始没当回事上电后立刻发 WREN 和读状态偶尔出现第一笔读取失败。后来在初始化函数入口加了 5ms 延时问题消失。对于 PIC18F97J60 这种 8 位 MCU5ms 延时在系统启动流程里几乎可以忽略但换来了存储系统全生命周期内的稳定可靠。我在实际测试中还发现如果设备启动阶段以太网 PHY 也在初始化两者同时上电可能因为共用 3.3V 电源纹波偏大而导致 MRAM 首次读取异常。解决方式是先初始化以太网等电源平缓后再做 MRAM 的首次读写自检。这个顺序安排对整机启动成功率有明显帮助值得在系统设计初期就考虑进去。回头再看这套方案MR25H40CDF 加 PIC18F97J60 的组合其实没什么高深技术核心就是存储器件选型选对了驱动写得扎实数据布局想清楚了。工业应用里存储这件事不是看芯片参数表多漂亮而是看它在现场连续跑几个月、经历无数次断电后还靠不靠谱。就目前的运行数据来看这套组合的表现完全达到预期而且因为 MRAM 的写寿命和写入速度代码里连磨损均衡都不用写省下了一大块需要长期维护的复杂逻辑。