☰
RISC-V开发踩坑:大容量NOR Flash 4字节地址模式原理与驱动实现
2026/10/17 2:04:10 网站建设 项目流程

简介:面向嵌入式开发者的 SPI Nor Flash 4 字节地址模式参考实现,基于芯来科技 RISC-V 平台,解决 MX25 系列 Nor Flash 在容量超过 16MB 时需切换到 4 字节地址模式的问题。资源内容精简,仅含 4 个 C 文件,总计约 20KB,重点呈现地址切换与读写控制的核心思路,适合需要快速理解并移植到自有 BSP 的工程师参考。代码按功能拆分为独立模块:底层 SPI 驱动负责时序与命令收发,QSPI 控制器初始化配置四线模式,Nor Flash 操作层封装字节地址切换、读、写与擦除流程,另附自检程序用于验证实际读写效果。通过这套精简实现,读者可快速掌握 4 字节模式的关键寄存器操作与命令序列,进而适配 GD25Q32、MX25L 等常见 Nor Flash。目前已有 199 人学习,适合有一定嵌入式基础、希望绕过复杂驱动框架直接阅读核心逻辑的开发者。资源包体积小、模块划分清晰,便于按需抽取整合到自己的工程中。 最近在调一块芯来RISC-V内核的板子,NOR Flash用的是MX25L25645G,容量32MB。前期代码和数据都在低地址区,一切正常,直到我把一个超过16MB的文件系统镜像写进去,读回来发现数据在0x1000000附近开始循环重复。第一反应以为是SPI时序不稳,逻辑分析仪抓了半天,指令和地址都是对的,但Flash返回的数据就是从低地址镜像出来的。后来才意识到,问题出在Flash的地址模式:这颗芯片默认工作在3字节地址模式,最大只能寻址16MB,超过部分会自动回卷。这篇文章就把整个排查过程和最终方案完整复盘一遍,重点讲清楚4Byte地址模式的原理、MX25系列的相关指令、以及芯来CPU的SPI控制器怎么配合实现,最后附上我整理好的驱动代码和排查清单。适合正在做RISC-V开发、或者用大容量NOR Flash存代码和数据的嵌入式工程师。

1. 背景与需求拆解:16MB边界上为什么会翻车

1.1 3字节地址的极限:16MB是怎么卡住的

NOR Flash在SPI协议里的经典寻址方式是命令字后面跟3字节地址,也就是24位地址,最大寻址范围2的24次方等于16MB。这个设计在1MB、2MB、4MB、8MB时代完全够用,但Flash容量发展到128Mbit(16MB)时正好顶到天花板,再往上256Mbit(32MB)、512Mbit(64MB)就必须要扩展地址位宽了。

MX25系列是旺宏Macronix的NOR Flash产品线,常见的有MX25L12835F(128Mbit/16MB)、MX25L25645G(256Mbit/32MB)、MX25U51245G(512Mbit/64MB)等。16MB容量用3字节地址刚好覆盖所有地址空间,但32MB和64MB版本就不行了。这也是很多工程师第一次在项目里用上32MB Flash时会踩坑的直接原因:数据手册习惯性往后翻,没注意到地址模式这个细节。

芯来科技的RISC-V SoC在嵌入式里用得越来越多,不少开发板板载Flash就是MX25系列。CPU访问外部Flash时,往往是内存映射方式(XIP,程序直接在Flash上执行),地址总线是32位的,理论上可以访问4GB空间,但底层SPI控制器往Flash发地址时,如果只发3字节,那超过16MB的部分就彻底没招了。

1.2 回卷现象的真正原因

所谓回卷(wrap-around),本质就是地址被截断。3字节地址模式下发出去的地址只有低24位有效,bit24及以上的位被丢弃。所以访问0x01000000时,实际发到Flash的地址是0x000000,等于又从头开始了;0x01000001对应0x000001,依此类推。

我在逻辑分析仪上确认的波形就是这样:命令字0x03(读)、地址0x00 0x00 0x00(因为控制器只发了3字节),然后Flash返回了低地址的数据。直观表现就是,文件系统里超过16MB区域的目录项和低地址区域一模一样,像是"幽灵数据"。

这个问题影响的不只是数据读写,还有程序执行。如果代码用XIP方式直接在Flash上运行,一旦跳转到超过16MB的函数,CPU产生的地址是0x0100xxxx,但Flash收到的是0x0000xxxx,结果拿到的是完全错误的指令,程序直接跑飞或进HardFault。所以这类问题有时候表现得很隐蔽,容易误判成栈溢出或者中断配置错误,排查起来很费时间。

2. 芯来CPU的SPI控制器与MX25的硬件基础

2.1 先搞清楚SoC里有两类SPI

芯来的SoC(比如N100、N200、N300系列)通常带两类SPI接口。一类是挂在APB总线上的通用SPI外设,用来接传感器、LCD屏幕、SD卡这些普通设备;另一类是专门接启动Flash的QSPI控制器,它内存映射到CPU地址空间,CPU读某个Flash地址时,控制器自动把地址翻译成SPI时序。

做NOR Flash驱动之前,一定要先确认自己操作的是哪一个控制器。通用SPI接口需要手动拉CS、手动收发每个字节,适合数据量小的场合;QSPI Flash控制器支持XIP,硬件自动管理片选和时序,读取效率高得多,但配置寄存器也更复杂。MX25这类启动Flash默认接在QSPI控制器上,如果你拿通用SPI去连,不是不行,但无法享受XIP的内存映射便利,性能也差不少。

另一个重点是确认控制器支不支持4字节地址。有些老型号的QSPI控制器地址位宽是写死的24位,那就只能靠Flash自身的B7指令全局切换;较新的控制器会有"地址位宽"或"4B指令配置"这类寄存器,可以让控制器在每次读操作时自动发送4字节地址,配合13h这样的专用读指令。具体寄存器名字每个系列不一样,但思路是通用的。

2.2 时钟极性与相位:Mode 0还是Mode 3

MX25手册明确写了支持SPI Mode 0(CPOL=0, CPHA=0)和Mode 3(CPOL=1, CPHA=1),也就是时钟空闲电平和采样边沿有两种组合。实际用哪个取决于控制器上电默认配置。芯来系SoC的QSPI控制器通常默认Mode 0,和MX25的默认状态是匹配的,但这不是绝对的,上电后第一件事应该是读JEDEC ID验证链路。

如果读ID发现数据全是错位或乱码,优先检查CPOL和CPHA的配置。这里有个简单的判断方法:用示波器看CLK空闲时是低还是高,低就是Mode 0,高就是Mode 3;再配合数据手册里波形图对比采样边沿,基本就能对上。

片选时序也要注意。每一次完整操作(命令+地址+数据)CS#要拉低,操作结束拉高,CS#高电平持续时间必须满足数据手册里的tSH参数。尤其是B7这种单字节命令,发完立刻做下一步操作,CS高电平时间不够,Flash可能根本不会执行切换。

2.3 硬件片选和软件片选怎么选

有相当一部分开发者喜欢用GPIO手动拉CS,觉得灵活可控,但实际做下来,硬件自动片选在连续读写和DMA场景下优势明显。硬件片选由控制器根据总线状态自动控制,指令间隙的CS#时序是确定的,不会因为中断抢占产生一个多余的上升沿或下降沿;软件片选一旦在操作过程中被中断打断,CS#还拉着低,Flash会以为后面的数据还是本指令的一部分,命令解析直接错乱。

4字节地址模式下,一个读操作要发1字节命令加4字节地址,中间如果被高优先级中断打断,后面几个字节就可能被Flash当成新命令。我遇到过几次偶发数据错乱,最后都是关闭中断或者改用硬件片选解决的。所以我的建议是:能用控制器片选就别用GPIO,尤其是在XIP和DMA场景下。

3. 4Byte地址模式的两种正确姿势

3.1 全局切换模式:B7进入、E9退出

MX25系列支持通过指令切换全局地址模式。发送0xB7(Enter 4-Byte Address Mode)之后,Flash内部状态变为4字节地址模式,此后所有命令的地址字段都按4字节来解析;要退回3字节模式,发送0xE9(Exit 4-Byte Address Mode)即可。

这个方式的好处是命令集不用变,原来用的03h读、02h页编程、20h扇区擦除等指令照常工作,只是地址多了一个字节。对已有驱动来说改动最小。

但有一个关键点必须记住:B7切换是易失性的,掉电或复位后Flash会恢复到默认的3字节模式。所以如果你在应用层切到4字节模式,运行中一旦发生复位,bootloader启动时又按3字节地址访问高地址区域,就会出错。很多项目里bootloader和应用程序是分开烧录的,bootloader里没做4字节模式初始化,而应用程序用了大容量Flash的高地址区域,复位后就会起不来。每次上电都要在最早阶段重新执行B7,这个顺序不能漏。

3.2 4字节专用指令:不切换状态直接访问

MX25系列针对大容量场景专门定义了一组4字节地址专用指令:13h是4字节地址读,12h是4字节地址页编程,21h是4字节地址扇区擦除(4KB),DCh是4字节地址块擦除(64KB)。这类指令的特点是不依赖Flash当前的全局模式状态,命令本身自带4字节地址字段,哪怕Flash还在3字节模式下也能正确访问全部地址空间。

这组指令的设计思路很清晰:对于非XIP的数据读写场景,驱动层不用关心Flash处于什么模式,每次操作都是独立的、幂等的。掉电重启后不需要重新初始化,代码逻辑更简单,也更好排查问题。缺点是如果控制器在XIP取指时只能发固定命令字(比如03h),就没法自动使用13h,取指场景下还是得靠全局切换。

3.3 实际项目里怎么选

我自己的方案是分角色处理。启动阶段(bootloader、XIP取指)优先用B7全局切换,因为CPU从Flash取指是内存映射方式,控制器产生的读命令是固定的,必须让Flash把所有指令的地址都解释成4字节;而应用层的数据读写、文件系统、OTA固件升级这类操作,全部改用13h、12h、21h等4字节专用指令。

这样做的好处是:启动代码只需在很早期执行一次B7,其他所有驱动代码都不依赖Flash的易失状态,随便复位、随便掉电,只要bootloader保证XIP正常,应用层的读写就一定正确。如果有条件,也可以配置控制器直接支持4字节地址位宽,让它对所有读命令自动补充第4个地址字节,这样连B7都不用,但前提是控制器有这个能力。

4. 驱动移植实战:从读ID到跨边界验证

4.1 最小驱动代码骨架

下面这套代码是基于芯来Nuclei SDK风格写的,核心函数抽象成标准接口:读ID、等待空闲、进入4字节模式、4字节地址读、4字节地址页编程、4字节地址扇区擦除。具体寄存器和GPIO操作逻辑请按你所用SoC的SDK替换,函数流程可以直接套用。

static void flash_cs_low(void) { /* 拉低CS# */ } static void flash_cs_high(void) { /* 拉高CS# */ } static uint8_t flash_xfer(uint8_t tx) { /* 通过SPI收发一个字节 */ } static void flash_wait_busy(void) { uint8_t sr; do { flash_cs_low(); flash_xfer(0x05); /* Read Status Register */ sr = flash_xfer(0x00); flash_cs_high(); } while (sr & 0x01); /* WIP位 */ } static void flash_write_enable(void) { flash_cs_low(); flash_xfer(0x06); /* Write Enable */ flash_cs_high(); } void flash_enter_4byte_mode(void) { flash_cs_low(); flash_xfer(0xB7); /* Enter 4-Byte Address Mode */ flash_cs_high(); delay_us(10); /* 保证CS高电平时间足够 */ } uint32_t flash_read_jedec_id(void) { uint32_t id; flash_cs_low(); flash_xfer(0x9F); /* Read JEDEC ID */ id = (uint32_t)flash_xfer(0x00) << 16; id |= (uint32_t)flash_xfer(0x00) << 8; id |= (uint32_t)flash_xfer(0x00); flash_cs_high(); return id; } void flash_read_4b(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; flash_cs_low(); flash_xfer(0x13); /* 4-Byte Read */ flash_xfer((addr >> 24) & 0xFF); flash_xfer((addr >> 16) & 0xFF); flash_xfer((addr >> 8) & 0xFF); flash_xfer(addr & 0xFF); for (i = 0; i < len; i++) { buf[i] = flash_xfer(0x00); } flash_cs_high(); } void flash_page_program_4b(uint32_t addr, const uint8_t *data, uint32_t len) { uint32_t i; flash_write_enable(); flash_cs_low(); flash_xfer(0x12); /* 4-Byte Page Program */ flash_xfer((addr >> 24) & 0xFF); flash_xfer((addr >> 16) & 0xFF); flash_xfer((addr >> 8) & 0xFF); flash_xfer(addr & 0xFF); for (i = 0; i < len; i++) { flash_xfer(data[i]); } flash_cs_high(); flash_wait_busy(); } void flash_sector_erase_4b(uint32_t addr) { flash_write_enable(); flash_cs_low(); flash_xfer(0x21); /* 4-Byte Sector Erase, 4KB */ flash_xfer((addr >> 24) & 0xFF); flash_xfer((addr >> 16) & 0xFF); flash_xfer((addr >> 8) & 0xFF); flash_xfer(addr & 0xFF); flash_cs_high(); flash_wait_busy(); }

这套代码里没有做页边界处理,实际使用时要注意:页编程的地址如果不在256字节边界,或者数据长度跨越页边界,就必须拆成多次12h操作,否则数据会回卷写到页内偏移处,这是NOR Flash的老规矩,换了4字节地址也一样要遵守。

4.2 初始化顺序与验证流程

初始化顺序我建议固定成以下几步:先配置引脚复用和GPIO,再初始化SPI控制器(时钟分频、极性、片选),接着读JEDEC ID确认SPI链路通了,再做4字节模式初始化,最后做跨边界读写验证。

读ID时返回值应该是MX25系列的厂家识别码,以0xC2开头,后续字节对应容量和型号。如果读到0xFFFFFF,说明SPI通信链路没通,优先查CS和CLK接线;读到全0,查MISO是否有短路或上拉问题。

时钟频率建议从低往上试。我先用10MHz左右做基础通信验证,ID读正确后再逐步提高,最后稳定在40MHz左右跑读写。直接上高频一旦出错,很难判断是模式问题还是信号完整性问题。

跨边界验证我习惯这样做:在0x00FFFC00写一组特征值(比如0x5A5A5A5A),然后在0x01000000写一组完全不同的特征值(比如0x12345678),再分别读回来比对。如果0x01000000读回的数据仍是0x5A5A5A5A,说明还在3字节回卷状态,4字节模式没有生效。

4.3 读写擦三大操作的4字节改造要点

读操作最简单,把命令字改成13h,地址改成4字节发送,数据部分和普通读完全一样。页编程要特别注意写使能指令06h必须单独一个CS周期,不能在12h命令前连发,否则Flash不认。扇区擦除推荐用21h(4KB粒度),对文件系统这类频繁小块更新更友好;大规模升级场景再考虑DCh做64KB块擦除以减少擦除次数。

地址发送顺序是大端,先发最高字节再发最低字节,这个顺序搞反了会直接访问到错误地址,且这种错误在低地址区域往往表现正常,一到高地址就乱了,容易被误判成数据总线问题。

5. 高频踩坑:常见问题与排查实录

5.1 高地址读写回卷

这是最常见也最容易被忽视的问题。现象是16MB以上区域读出的数据和低地址区域重复,文件系统挂载时出现大量错误,或者程序运行到某个函数后直接跑飞。根本原因就是地址被截断成24位。排查思路很简单:用上面说的特征值法,先确认是不是回卷,再确认4字节模式有没有真正生效。如果确认模式没生效,就回到初始化顺序排查,优先检查B7是否发出、CS高电平时间是否足够。

5.2 B7模式复位后丢失

B7切换是易失的,掉电、复位后Flash自动回到3字节模式。很多项目里bootloader和app分开开发,bootloader里没加4字节模式初始化,app烧录在高地址区域,复位后bootloader按3字节地址访问高地址区域,自然起不来。解决思路有两种:要么把B7初始化加到bootloader最早期;要么改成配置QSPI控制器支持4字节地址,让控制器在每次取指时都发完整的4字节地址。如果你只做应用层驱动,也要保证每次上电后驱动初始化时重新执行B7,不能假设Flash一直保持在4字节模式。

5.3 高频SPI下偶发数据错误

时钟频率一旦超过40~50MHz,PCB布线、上拉电阻、CS到CLK的时序裕量都可能成为问题。这类错误的特征是偶发性、无明显规律、降频后消失。排查时用示波器看CLK和MISO的边沿关系,确认数据采样点是否在数据稳定区域内;检查CS#的下降沿到CLK第一个上升沿的时间是否满足tSLCH;检查Flash供电引脚附近有没有足够容量的去耦电容。如果板子走线较长,适当降低频率比堆各种补偿方案更实在。

5.4 读ID全FF或型号不对

ID全FF,大概率是MOSI/MISO接反、Flash没供电、CS#没拉下来;ID全00,查MISO是否有对地短路或时钟没起来;如果ID能读到但和期望不符,先确认你用的是不是MX25系列、是不是对应容量的型号,不同型号的ID字节定义不同,别拿A型号的手册去对B型号的值。还有一种情况是焊接问题,虚焊导致偶尔能读ID但高频率下数据错误,用放大镜检查焊点和引脚之间有没有桥连。

另外提一句,如果Flash被误操作进入了非易失4字节模式,而你的控制器不支持4字节地址,板子可能直接变砖。MX25有些型号支持通过写状态寄存器恢复,但不同型号的寄存器位定义不一样,盲目写寄存器有风险。我自己遇到过一次,最后是拿编程器重新擦除配置区才救回来。所以量产固件里我从来不用非易失4字节模式配置,只依赖B7这种易失切换,至少出问题后重新上电就能恢复默认状态。

这套流程跑通之后,我又在MX25L12835F和MX25L25645G上各验证了一遍,逻辑基本通用。现在新项目里,驱动层一律默认用4字节专用指令,B7只放在启动阶段做一次性切换。如果你也在用大容量NOR Flash,建议尽早把4字节地址模式纳入设计,别等到数据错乱或程序跑飞了再回头排查,那时候的成本高得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询