
接触裸板bare metal开发之后你会发现很多在操作系统下面被封装得严严实实的东西终归要自己亲手掀开一次。Nor Flash就是其中一个典型的例子。它在Linux里的驱动几乎没人会直接碰但在ARM裸板的世界里上电第一行代码就躺在Nor Flash里等着被CPU取走。这篇内容基于我折腾某款ARM920T核心处理器的裸板经验把Nor Flash的基础知识、访问原理、编程示例和调试时的常见坑一次讲清楚。适合正在学ARM裸机开发、或者被Flash写不进去/读出来不对折磨的同学参考。1. 不要只把它当存储芯片裸机启动链中的第一块拼图1.1 掉电不丢数据只是表象XIP才是Nor Flash的灵魂很多人对Nor Flash的第一印象就是掉电不丢数据这当然没说错但只停留在这一层根本没法理解裸板上为什么要把Nor Flash放在那么显眼的位置。裸机系统里CPU上电后要取第一条指令这条指令放在哪最常见的方案就是放在Nor Flash里。处理器复位后内存控制器把地址0x0映射到专门的片选空间而Nor Flash恰好接在这根片选上。于是CPU从0x0开始取指实际上就是从Nor Flash的物理存储单元里拿字节。这个特性叫XIPExecute In Place原地执行属于Nor Flash最重要的能力之一它允许代码直接在Flash上运行而不需要先拷到RAM里。XIP能成立根基在于Nor Flash的读操作极其规矩你给它一个地址经过一个稳定的延时它就在数据总线上吐出对应内容。这个时序和SRAM很接近和普通内存不同的是Nor Flash读速度慢一些、可能要求等待周期但总体上它完全可以挂在CPU的总线上被当成一块慢速内存来访问。裸板启动就是这么玩的上电内存控制器配置好CPU傻乎乎地从0x0开始取指第一段启动代码就在Nor Flash里等着它。这里有一个必须强调的细节XIP只是读可以原地执行并不代表写也能原地进行。Nor Flash的写入需要一组复杂的命令序列写入过程中还可能有一段时间无法响应读请求。所以那种把整个程序放Flash里直接跑、跑的时候顺手改一下自己的想法工程上难度远比你想象的大。后面第五部分我会专门说这个坑。1.2 与NAND Flash的分工谁适合放代码谁适合放大数据做嵌入式这行Nor Flash和NAND Flash的对比是绕不开的基本功我见过不少新手把二者混为一谈以为都是Flash存储结果在选型时踩了大坑。它们的物理结构决定了性格完全不同。Nor Flash的特点是位线独立可以按字节或者按半字随机读取读路径非常直接所以支持XIP适合存放启动代码、Bootloader、固件主体这类必须上电就能跑的内容。它的写入和擦除虽然相对慢但胜在在系统里实现简单裸机驱动几十行代码就能搞定。NAND Flash则完全不是这个思路。它的存储单元是串行访问的读出来不是一个字节地址对应一个字节数据而是一块一块地搬页。物理上NAND天然容易出现坏块出厂就允许有一定比例需要软件做坏块管理和ECC。你没法把它映射到CPU地址空间里去直接取指但它容量大、写入快、单位成本低适合拿来存放文件系统、日志、音视频这种非易失的大块数据。所以很多成熟板子的配置都是一颗小容量Nor Flash放启动代码一颗大容量NAND或eMMC放系统和数据。Bootloader先把自己从Nor Flash里跑起来再把NAND里的东西初始化、搬运。这套分工本质上是拿Nor Flash的随机可读、可XIP换启动可靠性拿NAND的大容量、低成本换数据空间。1.3 处理器视角Nor Flash凭什么能被当作普通内存来读要理解裸板上怎么访问Nor Flash先得理解一颗Nor Flash在硬件上到底怎么接进系统的。以我用的某款ARM920T核心处理器为例处理器有多个片选空间比如Bank0到Bank6每个Bank对应一段CPU地址范围。Nor Flash通常接在Bank0基地址从0x00000000开始。硬件连接是CPU的地址总线从A1开始因为Flash是16位的接到Flash的地址引脚CPU的数据总线接到Flash的D15~D0CPU的nOE接到Flash的输出使能OECPU的nWE接到Flash的写使能WECPU的片选信号nGCS0接到Flash的片选CE。这套连接决定了CPU访问Nor Flash的方式非常简单只要配置好内存控制器的时序参数CPU执行一条普通的读指令地址总线就会输出目标地址nOE拉低Flash把数据放到数据总线上CPU拿走。整个过程对软件来说和访问外部SRAM几乎一样。你在裸机代码里定义一个volatile unsigned short *指针指向0x0然后解引用硬件层面就已经完成了一次Nor Flash读。这也是为什么裸机环境下读写Nor Flash的驱动可以写得非常轻量根本不涉及复杂的控制器协议全部核心都压在命令序列的正确性上。反过来如果你的Nor Flash读出来是乱的问题往往也不是出在C语言代码上而是内存控制器的时序参数、地址线连接或者总线宽度配置不对。这一点在排查问题时会反复遇到。2. 看命令集之前先看懂Flash的状态机擦、写、读为什么不对称2.1 物理基础为什么写1要交给擦除来完成Nor Flash的命令集看起来很绕但背后其实是它物理存储单元的工作方式决定的。Nor Flash的存储单元在出厂状态或者说擦除后的状态下内容全为逻辑1也就是每个位都是0xFF。为什么是0xFF而不是0x00因为Flash的写入操作本质上只能把1变成0而把0变回1必须靠擦除动作来完成。擦除会把一整块区域全部恢复成0xFF这个动作是物理层面的、按扇区或者整片进行的速度较慢。而写入则是把某个具体地址上的某些位从1改成0速度相对快一些。这就解释了一个经典现象你往Nor Flash某个地址反复写数据发现只能越写越小或者一旦写过0就再也变不回1了。有人以为再写一次相同地址就能覆盖成新值实际上是错的。新数值里的任何一位如果是1而旧数据对应位已经是0那么这一位根本翻不回去。唯二的办法是先擦除整个扇区或者换一个新的空白区域写。这个先擦后写的规矩是所有Nor Flash应用和驱动设计的地基。驱动里的扇区擦除函数和页编程函数严格分开写流程永远是擦除目标扇区 - 逐地址编程 - 读回校验顺序不能反过来。很多人第一次上手写驱动失败就是直接把写操作当成普通内存赋值没擦就写结果校验时发现一整块全是错位数据。2.2 解锁命令、命令序列与复位命令藏在命令字背后的设计逻辑为什么Nor Flash写一个数据要发那么多条神秘命令很多新手第一次看到0xAA、0x55、0xA0这些数字会很懵这其实是芯片为了防误操作故意设计的。设想一下Flash是挂在总线上的任何一次普通的写指令都会在地址和数据总线上产生一次物理写入动作。如果Flash把写当作常态访问那CPU在初始化内存、配置外设时不小心往Flash地址空间写了个随机值存储内容就被破坏了这绝对不可接受。所以Nor Flash把真正起作用的写操作设计成一个状态机必须按顺序输入特定的解锁命令才会进入可编程或者可擦除状态。如果某一步缺了、错了整个命令序列作废Flash继续当普通存储器使用不会发生数据破坏。以最常见的标准命令集为例。它有一个根深蒂固的约定向特定地址0x555写入0xAA、向0x2AA写入0x55这是两道基本的解锁钥匙。在此基础上进入读ID模式解锁后向0x555写0x90之后读0x0得到厂商ID读0x2得到设备ID。写单个数据解锁后向0x555写0xA0然后向目标地址写数据。擦除扇区解锁后向0x555写0x80再解锁一次最后向目标扇区地址写0x30。整片擦除解锁后向0x555写0x80再解锁一次最后向0x555写0x10。这个命令集是Nor Flash行业事实上的标准几乎所有兼容芯片都支持。我在代码里写命令序列时会严格按顺序执行并且每一条都加上volatile和内存屏障防止编译器乱序优化。还有一个很重要的补充0xF0是软件复位命令可以把Flash从读ID模式或者其他异常状态拉回来。这个命令在调试中救过我很多次后面会展开。2.3 命令地址偏移与总线宽度这里错一位全都白写Nor Flash命令集里的0x555、0x2AA到底是对应CPU地址还是要再乘个什么系数这是新手最容易写错的地方。关键在于总线宽度和CPU地址线的连接方式。假设一颗Nor Flash按16位组织硬件上CPU的A1接到Flash的A0CPU的A2接到Flash的A1依次类推。这意味着CPU端看到的一个半字地址到了Flash内部会被当成Flash的地址总线值来解析。命令集手册里写的0x555是Flash内部的地址偏移也就是以16位单元为单位的偏移。那么CPU侧实际要访问的字节地址就是0x555 1 0xAAA同理0x2AA要写成0x554。很多人的驱动一上来就原样照抄手册地址直接向地址0x555写命令结果硬件上这个地址送到Flash引脚后变成Flash内部地址0x2AA相当于第一个解锁命令就写错了地方后续命令自然全部石沉大海。更隐蔽的问题是指针类型带来的自动换算。如果用unsigned char *去写Flash编译器按字节算偏移0x555 1又会被翻倍一次等于把地址错到十万八千里。我写驱动时固定在宏定义里按16位指针处理((volatile unsigned short *)(NOR_BASE 0xAAA))然后把0xAAA当成字节偏移直接算好代码里不再做任何隐式换算。每次换板子、换Flash位宽第一件事都是重新确认CPU地址线怎么接的、手册里的命令地址到底要不要移位这个确认成本比以后查半天bug低太多。2.4 等待完成的三种姿势轮询、状态标志和超时Nor Flash擦除和编程都不是瞬间完成的扇区擦除可能需要几百毫秒写一个半字也需要几十微秒。驱动必须在操作完成后才知道可以继续这就有几种等法。最简单粗暴的等法是延时读目标地址判断。在裸机里很多人直接delay()几百毫秒完事这在小规模烧录时能凑合但如果做Bootloader在线升级、一个扇区一个扇区地擦写固定延时浪费大量时间而且Flash老化后速度会变慢固定延时还可能不够。更专业的方法是查询状态位。Nor Flash在擦除/编程过程中如果你去读那个正在被操作的目标地址返回的数据最高位DQ7会不断翻转和最终要写入的数据不一致操作完成后DQ7会稳定地等于预期数据的最高位。所以驱动里常见的轮询写法是在读取值中检查最高位是否等于期望值不等就继续读直到相等或者检测到超时/错误位。主流Nor Flash还有额外的状态标志比如DQ5表示操作是否超时或异常DQ6在擦写过程中持续翻转。我通常在轮询里加一个循环计数上限比如等待超过100万次读还没完成就判定失败并触发复位命令而不是让程序死循环卡死。在裸板上死循环的代价不只是程序不走了看门狗可能早就把整个系统复位了。无论用哪种等待方式都要记住一个前提等待期间去读的地址必须是正在擦除/编程的目标地址而不能随便读其他地址。这是Nor Flash状态机的工作特性读其他地址拿不到状态信息。3. ARM裸板Nor Flash驱动编码实录3.1 硬件假设与内存映射确认先把地址算对再写代码进入驱动编码前先把硬件环境固定下来不然代码写出来也没法复现。我这里假设某款以ARM920T为核心的处理区Nor Flash挂在Bank0基地址0x00000000Flash容量2MB按16位组织扇区大小8KB使用常见的兼容命令集。初始化第一步是配内存控制器。Bank0的时序参数包括地址建立时间、片选建立时间、访问周期长度等我通常把访问周期设置得稍微宽松一些因为Nor Flash本身比较慢不能按SRAM的高速参数硬套。在裸板早期调试阶段我会把时序参数调到比数据手册典型值更宽松一档先保证能稳定跑起来等驱动验证通过后再优化更紧的时序。紧接着要确认一件事CPU复位后Bank0是被默认使能的但不同处理器的默认时序未必匹配你的Flash所以启动代码里必须在访问Flash的任何一个地址之前先把内存控制器配置好。否则CPU一上电就去取第一条指令这条指令虽然是从Nor Flash里取的但配置动作本身又要在Flash上取指执行这个先有鸡还是先有蛋的问题依赖的是处理器复位后的默认配置能凑合着读Flash。绝大多数处理器的默认时序都比较宽松能让你跑过早期启动阶段但访问速度慢。真正的稳定运行还是要靠启动代码里尽早重配内存控制器。3.2 驱动原语实现读ID、扇区擦除、字节编程、内容校验我把驱动拆成四个基本原语读ID、扇区擦除、编程、校验。它们各自独立组合起来就能完成完整的烧录流程。先看读ID和复位命令的实现。进入读ID模式后读厂商ID和设备ID。读完之后必须发0xF0复位命令否则Flash一直停在ID模式之后的所有普通访问都会读出奇怪的值。#define NOR_BASE 0x00000000 #define NOR_CMD_1 (*(volatile unsigned short *)(NOR_BASE 0xAAA)) #define NOR_CMD_2 (*(volatile unsigned short *)(NOR_BASE 0x554)) #define NOR_BASE_WORD ((volatile unsigned short *)(NOR_BASE)) static void nor_reset(void) { NOR_CMD_1 0xF0; } unsigned int nor_read_id(unsigned int which) { unsigned int id; NOR_CMD_1 0xAA; NOR_CMD_2 0x55; NOR_CMD_1 0x90; id NOR_BASE_WORD[which]; nor_reset(); return id; }注意宏定义里0xAAA和0x554都是已经左移过的CPU地址不是Flash手册里的原始地址。这样写的好处是代码可读性强每个人看了都知道这是CPU地址。nor_read_id(0)拿厂商IDnor_read_id(1)拿设备ID具体数值不同厂家不同拿到之后拿去和数据手册对比。扇区擦除的代码是接下来要用的核心。擦除前必须确保目标地址落在扇区边界上本驱动假设扇区大小8KB所以扇区地址必须是0x2000的整数倍。void nor_erase_sector(unsigned long sector_base) { volatile unsigned short *dst; if (sector_base 0x1FFF) { /* 扇区地址未对齐按错误处理 */ return; } dst (volatile unsigned short *)(NOR_BASE sector_base); NOR_CMD_1 0xAA; NOR_CMD_2 0x55; NOR_CMD_1 0x80; NOR_CMD_1 0xAA; NOR_CMD_2 0x55; *dst 0x30; while ((*dst 0x80) ! 0x80) { /* 循环等待建议加超时 */ } }等待条件(*dst 0x80) ! 0x80的意思是擦除结束后目标地址的内容应该恢复成0xFF最高位也为1。所以循环会一直等到DQ7稳定为1。如果Flash异常这里会死循环工程上一定要加超时计数代码里我用注释标记。再看编程单个半字的函数。写入前不需要检查扇区是否已擦除但我强烈建议调用方自行保证目标扇区已经擦除干净否则校验必然失败。void nor_program_word(unsigned long byte_addr, unsigned short data) { volatile unsigned short *dst (volatile unsigned short *)(NOR_BASE byte_addr); NOR_CMD_1 0xAA; NOR_CMD_2 0x55; NOR_CMD_1 0xA0; *dst data; while ((*dst 0x80) ! (data 0x80)) { /* 等待DQ7直到和目标数据最高位一致 */ } }DQ7轮询的过程可以这样理解编程过程中目标地址读出来的最高位会跳来跳去一旦编程真正完成最高位就稳定等于你写入数据的最高位。这个等待逻辑比固定延时可靠得多。3.3 一个完整裸机示例从全片擦除到写入和读回比对把上面的原语组合起来就可以写一个完整的验证流程了。我常用的自测方法是选一个扇区擦除它写入一组固定图案再逐个读回比对。如果比对全对驱动的基础读写链路就算通了。static void nor_flash_self_test(void) { const unsigned short pattern[8] { 0x1234, 0x5678, 0xABCD, 0xEF01, 0x2000, 0x3000, 0x4000, 0x5000 }; unsigned long base 0x00080000; unsigned int i; int failed 0; nor_erase_sector(base); for (i 0; i 8; i) { nor_program_word(base i * 2, pattern[i]); } for (i 0; i 8; i) { volatile unsigned short *p (volatile unsigned short *)(NOR_BASE base i * 2); if (*p ! pattern[i]) { failed 1; } } if (failed) { /* 自检失败上报错误 */ } else { /* 自检通过 */ } }这里我特意避开了0x00000000地址做测试因为那一段放了启动代码和中断向量表。测试扇区选在0x80000也就是2MB Flash的中间位置属于比较安全的区域。注意指针偏移量i * 2因为我们要按16位单元连续写第i个半字对应的CPU字节地址就是base i * 2。自测过程中有一点很容易踩如果你是从Nor Flash执行这段测试代码的擦除到一半时Flash暂时无法被读而CPU可能正试图从同样这块Flash里取指令轻则卡顿重则直接跑飞。正确的做法是把整个自测函数和依赖的驱动原语都拷贝到RAM里去执行确保擦写过程中取指令不依赖Nor Flash。这一点在第五部分还会展开。3.4 裸板下最容易忽略的两层防护缓存与编译器行为裸机驱动写起来不难难的是在各种优化下保持稳定。第一个大坑是CPU缓存。如果处理器开了指令缓存或者数据缓存访问Nor Flash时就会出问题。读命令、读ID可能命中缓存拿到旧数据甚至写命令本身被缓存机制缓冲没有真正落到硬件引脚上。更麻烦的是从Nor Flash上执行代码时开了指令缓存CPU可能一直执行缓存里的老指令而你刚刚修改的Flash内容根本没有生效。所以做Flash擦写的关键路径上要么关掉缓存要么在操作前做一次缓存无效化invalidate操作后再刷回。裸板启动阶段我通常直接关闭Cache等整体系统稳定了再开并且开Cache后也绝不在Flash操作期间依赖Cache的一致性。第二个大坑是编译器的指令重排和读写合并。Nor Flash命令序列要求严格的顺序和特定的写宽度。编译器并不知道你在操作一个状态机它可能把两个相邻的16位写合并成一次32位写或者把命令顺序重新排列结果Flash完全不认这套命令。解决办法有两个所有指向Flash地址的指针都声明为volatile unsigned short *禁止编译器做合并和重排。每次写命令后插入一个内存屏障例如__asm__ __volatile__( : : : memory)。具体来说有些编译器优化后还会把NOR_CMD_1 0xAA; NOR_CMD_1 0x80;合并成一次写入这会让Flash状态机彻底乱掉。加了volatile通常能防止合并但重排依然可能发生稳妥做法是每次写命令后都加屏障。虽然性能差点但Flash操作本来就慢不差这几个空指令。4. 现场排障写不进、总变FF、命令超时的真实排查链路4.1 读ID全FF或者全00先从CPU根本没看到Flash查起驱动跑起来第一件事通常是读ID。如果读出来全0xFF或者全0x00不要急着怀疑代码多半是CPU压根没有正确定位到这片Flash。全0xFF有两种可能一是Nor Flash本身就被擦除得很干净二是CPU的读时序根本没成功驱动Flash数据总线保持在高电平于是读到全1。我遇到过很多次根源是内存控制器Bank0的时序配置不对。比如访问周期设置太短Nor Flash还没把数据推到总线上CPU就采样了自然读不到有效内容。这时候把访问周期拉长通常立刻见效。复位之后检查复位引脚。Nor Flash的RESET引脚如果被拉低芯片会一直处于复位状态不响应任何命令读出来要么全FF要么全00。有些板子这个引脚接了一个RC复位电路上电瞬间可能不正常要等它稳定后才能访问Flash。我在某块板子上就遇到过RESET引脚虚焊现象就是读ID偶尔成功偶尔全FF最后是拿示波器量引脚电平才定位到。地址线问题也很隐蔽。如果CPU地址线A1没有接到Flash的A0而是从A0开始接那么CPU读取地址0x2时Flash看到的是地址1整个地址映射就错位了。这种情况读ID可能能读到内容但设备ID值不对读普通数据时数据内容是错位的但看起来有规律。排查手段是把已知的固定数据烧进Flash然后从内存地址视角读出来看是不是发生了整体平移。4.2 命令发出去了Flash没反应时序、零散指令与缓存的合谋如果读普通数据正常但发命令序列后Flash纹丝不动问题往往集中在三处命令地址换算、缓存、时序。命令地址换算是最高频的错误。如前所述16位Flash时解锁地址是0xAAA和0x554而且必须按16位访问。如果代码里用了unsigned char *去实现命令物理上一次8位写Flash收到的是半字节破解的命令根本进不了解锁状态。我排查的习惯是先把代码里所有Flash地址相关的指针类型统一检查一遍确认全是volatile unsigned short *。缓存导致的假性无反应也很常见。开启数据缓存后循环等待while ((*dst 0x80) ! ...)永远读的是缓存里的旧值外面看着像Flash没有执行命令其实Flash早就完成了。这种情况只要关掉缓存或者做无效化就能解决但现场的表现很像驱动逻辑错误极其迷惑人。时序参数也会造成命令发不出去。如果内存控制器的写时序太紧Flash可能压根没有识别到有效的WE脉冲宽度命令字没有锁存进去状态机永远停留在默认态。遇到这种问题我通常把内存控制器的写脉冲宽度调大同时把整个时序参数都放宽一档再跑一遍命令流程。数据手册上给的时序参数是最低要求驱动里稍微放大不会错。4.3 数据写对了但校验失败擦除、位宽、编译器优化三大黑手驱动本身能读ID、能擦除但写完数据校验不通过这是最让人烦躁的场景。我总结出三大黑手按概率排是没擦干净、位宽操作不对、编译器优化搞鬼。没擦干净是最高发的原因。Nor Flash写入的本质是把1变0如果目标扇区没有先擦除里面已有随机数据写入的数据和旧数据叠加在一起校验自然失败。还有更隐蔽的情况你以为擦除了但擦除命令根本没生效驱动里的等待循环又写得不对提前返回了。所以我在校验失败时第一步永远是重新读目标地址看是不是全0xFF。如果不是回头查擦除函数。位宽操作不对的典型表现是写入一半对一半错。比如你想写16位数据但代码里用8位指针赋值CPU会生成两个8位写周期。对Nor Flash来说两次8位写可能分别触发两次半字节编程Flash被人为地塞进了两个半个命令结果数据写到一半被截断甚至完全写不进去。解决方法是严格使用16位指针访问一次写一个完整的半字。编译器优化的黑手则更难察觉。两个相邻的半字写入可能被合并成一个32位写周期对Nor Flash来说一个32位写周期等于往连续地址发了一次异常宽度的编程某些芯片会忽略它某些芯片会写出乱七八糟的结果。这种现象在Release优化级别下才出现Debug下一切正常极其坑人。排查方法很简单在关键写操作之间加上内存屏障或者干脆把优化关掉对比现象。4.4 恢复手段与预防设计复位命令、诊断工具与供电考量命令序列一旦中间断掉Flash会停留在某个不明确的状态后续所有命令都可能被它当成数据解释。这时最有效的恢复手段是发0xF0复位命令或者直接对Flash硬件复位。很多新手不知道0xF0的存在遇到一次写失败后整个驱动越调越乱其实一个复位命令就能回到原点。我在驱动里固定提供nor_reset()函数并且在任何驱动原语的入口处都可以安全调用把它当作Flash操作的保险丝。无论是读ID异常、擦除超时还是编程失败第一步永远是把Flash复位到默认读状态再开始新一轮命令序列。这样能避免状态机的混乱状态持续影响多个扇区。供电问题也要提一嘴。如果Flash供电不稳写操作时会随机丢数据。现象往往很规律小数据量写入正常连续写入大块数据时某个扇区莫名其妙全是0xFF或者0x00再写一次又正常。麻的是模板化和偶然复现的bug最容易浪费时间。我后来专门做了一个诊断小工具循环读写所有扇区并在每轮写完后立刻读回同时记录供电电压变化这才定位到供电纹波过大。5. Bootloader联动从驱动函数到分区烧录的工程化扩展5.1 分区规划boot、环境变量和运行镜像按需切分驱动原语调通只是第一步真要放到Bootloader和产品固件里去用还得考虑分区规划。裸板上把整个Nor Flash当一块大空地随便写是典型的原型阶段思路上了产品线必然出问题。我比较常用的分区方案是将2MB Nor Flash划分为四个区域boot区、环境变量区、固件区、数据备份区。boot区放启动代码和中断向量表固定从0x0开始大小256KB环境变量区紧随其后128KB用来存启动参数、IP地址、校准值等小型持久数据特点是频繁改写所以要给它单独留扇区避免和代码区混在一起造成互相污染再往上是固件区512KB放主程序镜像剩余空间留给数据备份或者日志记录。这个划分不是死的可以根据实际容量调整但原则是启动代码必须放在最前频繁改写的数据必须和只读代码分开并且每个分区都对齐到扇区边界。为什么频繁改写的区域要单独划分因为擦除是一个扇区级的动作如果你把运行镜像和环境变量放在同一个扇区每次修改环境变量都要先擦除整个扇区运行镜像也跟着遭殃一旦擦除中途掉电镜像就毁了。分区隔离后即使环境变量区写坏了最多影响配置数据Bootloader还能正常启动。5.2 自编程的雷区从Nor Flash运行程序时擦写同一片Nor讲实话这是裸板Nor Flash开发里最危险的一个雷区也是我踩得最疼的一次。当你从Nor Flash上执行Bootloader代码然后Bootloader又要去擦写另一段Nor Flash区域这个操作表面看没什么问题但实际执行到一半CPU取指令时极有可能正好访问到正在擦除的扇区或者访问到被命令序列临时占用状态的地址区域结果CPU读到无效数据执行流直接跑飞。更麻烦的是缓存。启动阶段Bootloader如果开了指令缓存Flash擦写完成后缓存里可能还留着旧的指令和数据即使Flash内容已经更新CPU执行的还是旧指令。这就导致烧录明明显示成功复位后却还是老版本固件这种诡异现象。我的处理方式是把凡是涉及Flash擦写命令的代码和数据在运行时就全部拷贝到RAM里去然后让CPU从RAM执行这些擦写函数。中断向量表、栈指针也都要放在RAM中确保整个擦写过程中没有任何一条指令或一个数据访问会落到Nor Flash地址空间。代码架构上就是把Flash驱动做成一个独立模块它的运行位置通过链接脚本固定在RAM段。这块能提前设计就不要临时改。5.3 实操建议先小步验证再动完整镜像按我这几年的实操经验凡是直接上来就做整片擦除、整片写入大镜像的基本都会翻车。稳妥做法永远是先小步验证再逐步扩大范围。我在验证一套新的Flash烧录流程时顺序是读ID确认芯片正常擦除单独一个扇区读回确认全0xFF写入16字节测试数据读回比对写入一整个扇区的随机图案逐字节比对最后才跑完整镜像烧录。每一个步骤都独立打印状态信息任何一个环节不对都先停下来查而不是等到最后镜像校验失败才去大海捞针。烧录过程中我还坚持做进度回显和超时保护。每个扇区擦除前记录起始时间超过数据手册标称时间很多倍仍没有完成就报错并尝试复位Flash。这个设计在面对老化芯片或者供电不稳的板子时特别有用至少不会让整个烧录过程卡死在一个坏扇区上。另外如果产品要支持在线升级给“升级标志”单独留一个扇区也非常值得。Bootloader启动时先读这个标志如果标志显示有完整的新镜像就跳到新镜像启动如果标志显示升级中断了就自动回滚到旧镜像。双镜像加标志位这套玩法依赖的就是Nor Flash“可随机读、分区管理、擦写可控”这些特性从底层逻辑上保证升级失败了还能救回来。我建议每个做裸机Bootloader的人都把这套机制纳入设计哪怕第一次用不上也远比出事之后后悔强。