news 2026/10/4 14:29:23

MRAM与PIC18F86K90工业存储方案:SPI驱动与掉电保护实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MRAM与PIC18F86K90工业存储方案:SPI驱动与掉电保护实战

1. 项目缘起与方案选型:为什么是 MRAM 加 PIC18F86K90

工业现场的数据记录仪、智能电表、PLC 扩展模块、医疗泵控制器,这类设备有一个共同特征:它们需要频繁记录关键状态,掉电不能丢,而且现场环境往往伴随高温、振动和电磁干扰。传统方案里,EEPROM 擦写寿命只有百万次量级,写入速度慢到毫秒级;铁电存储器 FRAM 速度快、寿命高,但容量普遍偏小、价格偏高;带后备电池的 SRAM 容量大、速度快,可电池本身就是个隐患,低温失效、高温漏液、长期不换电池导致数据丢失,维护成本极高。

MR25H40CDF 这类 MRAM 器件正好卡在中间位置。它的核心存储单元是磁性隧道结,靠电子自旋方向存储数据,没有电荷泄漏问题,所以掉电后数据能保持二十年以上,不需要任何后备电源。写入不需要先擦除,字节级随机写入,没有写延迟等待,擦写次数在 10^14 量级,基本可以理解为“随便写”。SPI 接口,40MHz 时钟,4Mbit 容量,工业级温度范围,SOP8 封装,这些参数放在一起,对于需要“高频写入 + 掉电保持 + 免维护”的工业场景来说,是一个非常务实的选择。

主控这边选 PIC18F86K90,理由也很直接。它是 8 位机里外设比较全的一颗,自带 MSSP 模块可以配置成 SPI 主模式,硬件 SPI 比软件模拟稳定得多,尤其是在 40MHz 这种较高时钟下,软件翻转 IO 根本跟不上。它还有 4KB 的片上 SRAM 和 128KB Flash,足够跑一个轻量的数据记录状态机,不需要上 RTOS。工作电压 1.8V 到 5.5V,和 MR25H40CDF 的 2.7V 到 3.6V 供电区间有交集,3.3V 系统里两者可以直接对接,省掉电平转换芯片。

我个人的判断是,这套组合最适合的场景是:写入频率高(每秒几次到几十次)、单次写入数据量不大(几十字节到几百字节)、要求掉电后数据完整、设备部署后基本不拆开维护。如果你做的是消费类产品,成本敏感,那 EEPROM 或者 Flash 更合适;但如果是工业设备,返修一次的人工成本就超过芯片差价了,MRAM 的溢价是值得的。

提示:MR25H40CDF 是 4Mbit 容量,换算成字节是 512KB。注意区分 bit 和 Byte,很多新手看手册时会把 4Mbit 当成 4MB,实际可用空间只有 512KB,规划存储结构时要按 512KB 来算。

2. 硬件连接与 SPI 时序关键细节

2.1 引脚连接与片选处理

MR25H40CDF 是标准 SPI 从器件,引脚定义很清晰:SCK、SI(MOSI)、SO(MISO)、CS#、VDD、VSS,加上两个写保护相关的引脚。PIC18F86K90 这边用 MSSP 模块的 SPI 主模式,映射到 RC3(SCK)、RC5(SDO)、RC4(SDI),片选用一个普通 GPIO 控制。

片选这块我要多说一句。很多教程图省事,直接把 CS# 接地,让器件一直选中。单器件系统里这样确实能跑,但有两个隐患:一是上电瞬间 SCK 线上如果有毛刺,器件可能误触发;二是后续如果要加第二个 SPI 从器件,就得改板子。所以我的习惯是始终用 GPIO 控制片选,初始化时先拉高,通信前拉低,通信完拉高,养成习惯。

// PIC18F86K90 片选引脚定义,假设用 RB0 #define MRAM_CS_LAT LATBbits.LATB0 #define MRAM_CS_TRIS TRISBbits.TRISB0 void MRAM_CS_Init(void) { MRAM_CS_TRIS = 0; // 输出 MRAM_CS_LAT = 1; // 默认拉高,不选中 }

写保护引脚 WC# 建议接 GPIO 或者直接上拉。如果接 GPIO,可以在固件里做一层保护:只有在明确要写入时才拉低 WC#,其他时间保持高电平,防止程序跑飞时误写数据。这个细节在工业设备里很重要,我见过因为没做写保护,程序异常后把整个存储区写乱的案例。

2.2 SPI 模式选择与时钟计算

MR25H40CDF 支持 SPI 模式 0 和模式 3,也就是 CPOL=0/CPHA=0 或者 CPOL=1/CPHA=1。PIC18F86K90 的 MSSP 模块配置成 SPI 主模式时,通过 SSPCON1 寄存器的 CKP 位和 SSPSTAT 的 CKE 位来设置。我一般用模式 0,因为大多数逻辑分析仪和调试工具默认就是模式 0,抓波形方便。

时钟频率的计算要结合 PIC18F86K90 的系统时钟。假设系统时钟 Fosc 是 64MHz,MSSP 的 SPI 时钟来源是 Fosc/4,也就是 16MHz,然后通过 SSPADD 或者 SSPCON1 的分频位再分频。MR25H40CDF 最高支持 40MHz,所以 16MHz 完全在范围内,不需要额外分频。

void SPI_Init(void) { // 配置 MSSP 为 SPI 主模式,时钟 = Fosc/4 SSPCON1 = 0x00; SSPSTAT = 0x00; // 模式 0:CKP=0, CKE=0 SSPCON1bits.SSPM = 0b0000; // SPI Master, Fosc/4 SSPCON1bits.CKP = 0; // 空闲低电平 SSPSTATbits.CKE = 0; // 数据在时钟上升沿采样 TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC5 = 0; // SDO 输出 TRISCbits.TRISC4 = 1; // SDI 输入 SSPCON1bits.SSPEN = 1; // 使能 MSSP }

这里有个容易踩的坑:PIC18F86K90 的 MSSP 在 SPI 主模式下,写 SSPBUF 之后要等 BF 位(SSPSTATbits.BF)或者 SSPIF 中断标志,才能读接收到的数据。如果不等标志位直接读,读到的可能是上一次的残留值。我在早期调试时因为这个原因,读回来的状态寄存器值总是差一位,排查了半天才发现是时序没等够。

2.3 上电初始化与器件识别

MRAM 上电后不需要像 Flash 那样等待就绪,但稳妥起见,初始化流程里还是加一段延时,然后读器件 ID 确认通信正常。MR25H40CDF 的器件 ID 读取命令是 0x9F,返回 3 个字节,第一个是制造商 ID,后面是产品 ID。

uint8_t MRAM_ReadID(uint8_t *id_buf) { MRAM_CS_LAT = 0; SPI_Transfer(0x9F); // 读 ID 命令 id_buf[0] = SPI_Transfer(0xFF); id_buf[1] = SPI_Transfer(0xFF); id_buf[2] = SPI_Transfer(0xFF); MRAM_CS_LAT = 1; return 0; }

实测下来,制造商 ID 应该是 0x0E,产品 ID 的高字节是 0x22 左右。如果读回来全是 0xFF 或者 0x00,基本可以判断是接线问题或者片选没拉低。这个 ID 检查步骤建议写进产品自检流程,设备出厂前跑一遍,能挡掉不少焊接不良的板子。

3. 存储结构设计与读写实现

3.1 地址空间规划

512KB 的空间看着不大,但如果没有规划,写到后面会乱成一锅粥。我的做法是把它分成几个固定区域:头部信息区、配置参数区、循环记录区、故障快照区。

区域起始地址大小用途
头部信息区0x000004KB设备序列号、固件版本、存储格式版本
配置参数区0x010004KB校准系数、报警阈值、通信参数
循环记录区0x02000480KB按时间顺序循环写入的运行数据
故障快照区0x7A00024KB故障发生前后的关键变量快照

头部信息区放存储格式版本号这个设计,是我踩过坑之后加的。早期产品升级固件时改了数据结构,结果旧数据读出来全是乱的,现场设备又没法返厂格式化。后来加了版本号,固件启动时先读版本,不匹配就走兼容解析或者提示需要迁移,问题就解决了。

循环记录区的设计要点是“环形缓冲 + 序号标记”。每条记录固定长度,比如 64 字节,包含一个递增的序号、时间戳、数据载荷和 CRC 校验。写满之后回到区域起始位置覆盖最旧的记录。读取时通过序号判断哪些是最新数据,不需要额外的指针管理。

3.2 单条记录的写入流程

MRAM 的写入命令是 0x02,后面跟 24 位地址,然后连续写入数据。地址是 3 字节,高位在前。写完之后 CS# 拉高,数据就真正写入了,不需要等待写周期,这是 MRAM 相比 EEPROM 最大的优势。

void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len) { MRAM_CS_LAT = 0; SPI_Transfer(0x02); // 写命令 SPI_Transfer((addr >> 16) & 0xFF); // 地址高字节 SPI_Transfer((addr >> 8) & 0xFF); // 地址中字节 SPI_Transfer(addr & 0xFF); // 地址低字节 for (uint16_t i = 0; i < len; i++) { SPI_Transfer(data[i]); } MRAM_CS_LAT = 1; }

这里有个细节:MRAM 支持跨页写入,不像 EEPROM 那样有页边界限制。EEPROM 写跨页时会回卷到页首,导致数据写错位置,MRAM 没有这个问题,连续写多少字节都行,只要不超出芯片容量。这个特性简化了驱动代码,不需要做页对齐处理。

读取命令是 0x03,格式和写入一样,只是数据方向反过来。

void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LAT = 0; SPI_Transfer(0x03); SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = SPI_Transfer(0xFF); } MRAM_CS_LAT = 1; }

3.3 数据完整性保障

工业设备最怕的是“写了一半掉电”,记录区里出现半条脏数据。我的做法是每条记录末尾加两个字节的 CRC16,读取时先校验 CRC,不通过就跳过这条记录。同时记录头里放一个“有效标志”,写入时先写数据区,最后写标志字节,这样即使掉电,标志字节没写成功,这条记录就被视为无效,不会污染数据链。

CRC16 的计算用查表法,在 PIC18F86K90 上跑一次 64 字节的 CRC 大概几十微秒,对整体性能影响可以忽略。查表放在 Flash 里,占 512 字节空间,换来的是计算速度提升好几倍。

uint16_t CRC16_Calc(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc = (crc >> 8) ^ crc16_table[(crc ^ data[i]) & 0xFF]; } return crc; }

注意:CRC 校验的多项式要固定,不要中途换。我见过一个项目,两个工程师分别写了写入端和读取端的 CRC 代码,用的多项式不一样,结果数据死活读不出来,查了两天才发现是这个问题。建议把 CRC 算法封装成一个公共函数,两边都调用同一个。

4. 性能实测与优化经验

4.1 写入速度实测

在 16MHz SPI 时钟下,我实测连续写入 64 字节记录,从拉低 CS# 到拉高 CS#,示波器抓下来大约 36 微秒。其中命令和地址占 4 字节,数据 64 字节,总共 68 字节,68 × 8 / 16MHz = 34 微秒,加上片选和函数调用开销,36 微秒是合理的。

对比一下 EEPROM:同样 64 字节,EEPROM 需要先发写命令,然后等待 5ms 左右的写周期,期间芯片不响应任何命令。也就是说 EEPROM 写一次要 5ms,MRAM 只要 36 微秒,差了将近 140 倍。如果设备每秒记录 10 次,EEPROM 方案里 50ms 都花在等待写完成上,CPU 什么都干不了;MRAM 方案里这 360 微秒几乎无感。

4.2 读取速度与随机访问

读取速度更快,因为没有写周期等待。16MHz 时钟下读 64 字节大约 34 微秒。MRAM 的随机访问延迟是固定的,不像 Flash 有读取缓存命中率的问题,读任何地址都是同样的速度。这个特性对于需要频繁查询历史数据的应用很友好。

我做过一个测试:在 480KB 循环记录区里随机读 1000 次,每次读 64 字节,总耗时大约 34 毫秒,平均每次 34 微秒,和连续读取没有明显差异。这说明 MRAM 的随机访问性能确实稳定。

4.3 功耗表现

MR25H40CDF 的写入电流典型值在 10mA 左右,待机电流只有几十微安。对于电池供电或者能量采集供电的设备,这个功耗水平是可以接受的。写入 36 微秒消耗的能量大约是 10mA × 3.3V × 36μs = 1.2μJ,非常小。

如果设备用超级电容供电,掉电后需要把关键数据紧急写入 MRAM,这个能量预算完全够用。我算过一笔账:一个 0.1F 的超级电容从 3.3V 放到 2.7V,释放的能量是 0.5 × 0.1 × (3.3² - 2.7²) = 0.18J,足够写几万次 64 字节记录。所以掉电保护方案里,MRAM 是比 EEPROM 更省心的选择。

5. 常见问题排查与避坑指南

5.1 通信失败排查流程

调试 SPI 器件,最怕的是“什么都读不到”。我整理了一个排查顺序,按这个走基本能定位问题:

现象可能原因排查方法
读回全 0xFFMISO 没接好或片选没拉低示波器看 CS# 和 MISO 波形
读回全 0x00器件没供电或时钟没输出万用表量 VDD,示波器看 SCK
读回数据错位SPI 模式不对切换模式 0 和模式 3 对比
偶发读写失败片选时序太紧在 CS# 拉低后加 1μs 延时
高温下不稳定时钟太快或电源纹波大降时钟到 8MHz 测试

我遇到最多的是第一种和第四种。第一种通常是焊接问题,SOP8 封装引脚间距小,手工焊接容易虚焊。第四种是片选和时钟的建立时间不够,MRAM 要求 CS# 拉低到第一个时钟沿至少有 5ns 的建立时间,一般 GPIO 操作都能满足,但如果用中断里直接操作寄存器,可能会太快,加个 NOP 或者微秒延时就能解决。

5.2 数据异常与恢复

如果发现读出来的数据 CRC 校验不通过,先不要急着格式化。我的处理流程是:先统计整个记录区里有效记录的数量和分布,判断是局部损坏还是大面积损坏。局部损坏通常是掉电时机不巧,那条记录作废就行;大面积损坏可能是电源问题或者程序跑飞,需要检查硬件和固件。

有个技巧:在头部信息区里存一个“写入次数计数器”,每次设备启动时加一。如果发现计数器异常增长,说明设备在频繁复位,可能是电源不稳或者看门狗误触发。这个计数器我一般放在 MRAM 里,因为它写入无损耗,放 EEPROM 里反而担心写坏。

5.3 与 PIC18F86K90 配合的注意事项

PIC18F86K90 的 MSSP 模块在 SPI 主模式下,如果连续发送数据,中间不能有太长的间隔,否则从器件可能认为通信结束。我在写批量数据时,每发一个字节都等 BF 标志,确保 SSPBUF 空了再发下一个,这样虽然慢一点,但稳定。如果追求速度,可以用中断或者 DMA,但 PIC18F86K90 没有 DMA,只能用中断方式,代码复杂度会上升。

另外,PIC18F86K90 的 IO 口驱动能力有限,如果 SPI 走线较长(超过 10cm),建议在 SCK 和 MOSI 上串 22Ω 到 33Ω 的电阻,减少反射和过冲。我在一个工控板上没加这个电阻,SPI 时钟跑到 16MHz 时波形振铃严重,读数据偶发错误,加了 33Ω 电阻后波形干净了,问题消失。

6. 固件架构与状态机设计

6.1 分层结构

固件我分成三层:硬件抽象层、MRAM 驱动层、应用逻辑层。硬件抽象层封装 SPI 收发和片选操作,驱动层实现读写擦和 CRC 校验,应用层负责记录管理、掉电检测和数据恢复。这样分层的好处是,如果以后换主控或者换存储芯片,只需要改硬件抽象层和驱动层,应用层代码基本不动。

// 硬件抽象层接口 void HAL_SPI_Init(void); uint8_t HAL_SPI_Transfer(uint8_t data); void HAL_CS_Set(uint8_t level); // 驱动层接口 void MRAM_Init(void); void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len); void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len); uint16_t MRAM_GetRecordCount(void); // 应用层接口 void Record_Write(uint8_t *payload, uint16_t len); uint8_t Record_ReadLatest(uint8_t *buf, uint16_t len); void Record_Format(void);

6.2 掉电检测与紧急写入

PIC18F86K90 有内置的欠压复位模块,可以配置成在电源电压降到阈值时触发中断。我在中断里做紧急写入:把当前未保存的关键变量打包,写到故障快照区,然后进入死循环等待电源彻底掉下去。这个过程要在电源电容放电到芯片最低工作电压之前完成,所以代码要尽量精简,不能有浮点运算和复杂逻辑。

实测下来,3.3V 系统里用 100μF 电容,从检测到掉电到电压降到 2.7V,大约有 2ms 的时间窗口。写 64 字节记录只要 36μs,加上打包和 CRC 计算,总共不超过 200μs,时间非常充裕。

6.3 上电恢复流程

上电后第一件事是读头部信息区的版本号和设备序列号,确认存储格式匹配。然后扫描循环记录区,找到最新的有效记录序号,从那里继续写。扫描过程不需要读全部数据,只需要读每条记录的头部几个字节,判断有效标志和序号,这样 480KB 区域扫描一遍也就几毫秒。

如果发现记录区全空或者全无效,说明是首次使用或者存储被擦除了,这时候走格式化流程,重建头部信息和记录区结构。格式化不是真的擦除所有数据,只是重置头部信息区的计数器和指针,旧数据留着不管,后续写入会自然覆盖。

7. 实际项目中的经验沉淀

7.1 关于 MRAM 的选型建议

MR25H40CDF 是 4Mbit 容量,如果项目需要更大容量,同系列还有 8Mbit 和 16Mbit 的型号,引脚兼容,驱动代码只需要改地址位数。如果对成本敏感,可以选容量小一点的型号,比如 1Mbit 的 MR25H10,价格会低一些。

选型时要注意温度等级。工业级是 -40°C 到 +85°C,汽车级是 -40°C 到 +125°C。如果设备部署在户外或者靠近热源,建议选汽车级,虽然贵一点,但可靠性有保障。我有个项目用在锅炉房附近,环境温度经常到 70°C 以上,用工业级芯片跑了两年没出问题,但余量确实不大。

7.2 关于 SPI 布线的经验

SPI 是高速信号,布线要注意几点:SCK 和 MOSI 尽量等长,减少时序偏差;走线远离模拟信号和电源开关节点,避免耦合噪声;如果板上有多片 SPI 器件,片选线要分开走,不要共用。我见过一个板子把两个 SPI 器件的片选接在一起,结果两个器件同时被选中,数据冲突,读回来全是乱的。

7.3 关于固件升级的考虑

如果设备支持固件升级,存储格式的兼容性要提前考虑。我的做法是在头部信息区预留一个“格式版本”字段,每次升级固件时检查这个字段,如果版本不匹配,走数据迁移流程。迁移流程可以很简单:读旧格式数据,转换成新格式,写到新区域,然后更新版本号。这个过程可以在设备启动时自动完成,不需要人工干预。

提示:数据迁移过程中如果掉电,可能导致新旧数据都不完整。稳妥的做法是迁移前先在 MRAM 里写一个“迁移中”标志,迁移完成后清除。如果启动时发现这个标志还在,说明上次迁移没完成,需要重新迁移或者回滚。

7.4 关于测试覆盖

嵌入式存储的测试不能只测正常读写,要覆盖边界条件:写满整个区域、跨页写入、掉电中断、高温低温、电压波动。我一般会写一个测试固件,自动跑这些场景,记录每次的结果。测试固件里加一个“错误注入”功能,随机在写入过程中复位,模拟掉电,然后检查数据完整性。这个测试跑一晚上,基本能把大部分隐患暴露出来。

8. 写在最后的一些实操体会

这套 MRAM 加 PIC18F86K90 的方案,我在三个项目里实际用过,累计出货几千台,现场返修率很低。最让我满意的是它的“无感写入”——应用层调用写记录函数,底层 36 微秒就完成了,CPU 几乎不用等待,程序逻辑可以写得很顺。相比之下,之前用 EEPROM 的方案,每次写都要等 5ms,程序里到处是状态机和延时,代码复杂度高很多。

踩过的坑里,最深刻的一次是片选时序问题。当时用了一个新批次的 MRAM,发现偶发读写失败,概率大概千分之一。查了很久,最后用示波器抓波形,发现 CS# 拉低到 SCK 第一个上升沿之间只有 2ns,而手册要求最小 5ns。原因是那批 MRAM 的输入建立时间比之前批次略慢,加上 PCB 走线延迟,刚好卡在边界上。在片选拉低后加了一个 NOP 延时,问题就解决了。这件事让我养成了一个习惯:所有 SPI 器件的片选操作后面都加一点延时,宁可慢一点,也要稳。

另一个体会是,MRAM 虽然写入寿命几乎无限,但也不是可以随便乱写。如果程序有 bug,在循环里疯狂写同一个地址,虽然芯片不会坏,但数据会被覆盖得乱七八糟。所以我在驱动层加了一个简单的写入频率限制:同一个地址在 1ms 内只允许写一次,超过就丢弃并记录错误。这个保护机制在实际运行中触发过几次,帮我发现了应用层的逻辑错误。

最后分享一个小技巧:MRAM 的头部信息区可以存一份“出厂默认配置”的副本,设备恢复出厂设置时直接从副本拷贝到配置区,不需要固件里硬编码默认值。这样如果默认配置需要调整,只需要在出厂前更新副本,不用重新编译固件。这个做法在需要频繁调整参数的场景里特别省事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 14:28:00

眼动追踪中的动态AOI分析:从静态画框到兴趣区跟随的完整实战指南

眼动数据分析里有一个很隐蔽的门槛&#xff1a;静态图片上的AOI分析&#xff0c;随便找个教程就能上手&#xff0c;画个矩形框&#xff0c;统计注视时长&#xff0c;完事。可一旦刺激物动起来——视频广告、驾驶模拟界面、人机交互操作过程&#xff0c;很多人立刻卡壳。为什么&…

作者头像 李华
网站建设 2026/10/4 14:27:57

npm install 安装慢?把 registry 改到 TaoToken 统一通道的配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 14:26:13

Android蓝牙底层开发:从Framework到HAL的13节核心解析

1. 蓝牙更新13节——这套Framework到HAL的内容到底在讲什么先说个背景。我们这个系列一直盯着Android系统底层&#xff0c;从Framework到HAL再到具体平台适配。标题里写"蓝牙更新13节"的时间是2019年6月5日&#xff0c;那个时间点恰好是Android 9已经大面积铺开、And…

作者头像 李华
网站建设 2026/10/4 14:26:08

打字侠邀请码tanjie实测:从40字到90字的提速方法与瓶颈突破

打字侠这个软件&#xff0c;我用邀请码 tanjie 注册了之后&#xff0c;前后练了差不多三个月&#xff0c;从每分钟 40 字左右提升到稳定 90 字&#xff0c;中间踩了不少坑&#xff0c;也摸出了一些门道。这篇就把我的实际使用过程、对邀请码机制的理解、以及练习时总结出来的技…

作者头像 李华
网站建设 2026/10/4 14:24:05

鸿蒙应用中的Flutter堆叠布局:从按钮、徽章到卡片叠加的实战拆解

1. 项目概述与方案选型我最近在做鸿蒙应用时被一类需求折腾得够呛&#xff1a;设计稿里满屏都是“不规矩”的 UI&#xff0c;带图标的渐变按钮、右上角挂红色数字的图标、好几张卡片叠在一起的效果。头两天用 ArkUI 的 Row、Column 去拼&#xff0c;嵌套特别深&#xff0c;页面…

作者头像 李华