尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
深入剖析Nor Flash:从存储单元到裸机驱动调试实战
裸机开发这摊事绕不开的一个器件就是 Nor Flash。你可能帮它干过最简单的活把编译好的代码烧进去、上电启动、跑起来。但你有没有想过它内部到底是个什么结构为什么写之前非得先擦除为什么命令序列要按固定顺序发我当年刚接触ARM裸板的时候光是把“读ID”调通就折腾了整整一个下午后来才发现是时序模式没配对。这篇文章就把我这些年在Nor Flash上攒下的基础知识、编程套路和调试教训一次讲清楚配合可复现的裸机代码示例希望能让刚入门的同学少走点弯路也让已经写过驱动的人能回头把底层的“Why”补一补。先给个共识Nor Flash 是嵌入式系统里常见的一种非易失性存储器掉电不丢数据支持按字节随机读取所以很多ARM处理器可以直接从它里面执行代码。它区别于另一个大家族 Nand Flash 的点本质上来自存储单元的电路结构这个结构决定了读写擦三种操作的行为差异也决定了你写驱动的时候必须遵循那些看似“奇怪”的规则。1. 从存储单元的底层结构说起1.1 NOR 结构的“并联”特性决定了什么Nor Flash 每个存储单元的核心是一只浮栅晶体管。这个管子的栅极悬浮在绝缘层中间编程的时候往浮栅里注入电荷相当于把阈值电压抬得很高擦除的时候把电荷抽走阈值电压又掉回低状态。关键是阵列的接法所有存储单元的漏极或者位线并在一根线上源极公共接地所以每一位都能被独立选中、快速读出来——这一点和 CPU 访问内存的方式很像。这种并联结构带来的直接优势就是读速度快、随机性好。地址信号过去片选一拉数据位上电信号很快稳定下来完全能接在 SoC 的静态存储控制器上像读内存一样直接读。但代价是想写一个位只能从 1 改成 0想从 0 改回 1 必须对整个块做擦除把块里所有位全部恢复到 1。所以业界有个非常形象的总结编程就是把 1 改成 0擦除就是把整块恢复成全 1。很多人刚接触的时候会问为什么不能像 SRAM 那样直接覆盖写原因就是物理结构决定的浮栅注入电荷这个动作是按“位”单向进行的。你可以在已擦除全 1的区域里任意把若干位写成 0但如果你想把这个 0 再变回 1只能把整块区域擦掉重来。明白了这一点你再看编程示例里的“先擦除再写”逻辑就一点都不会觉得多余。1.2 Nor Flash 和 Nand Flash不是容量和价格那么简单很多资料喜欢直接列对比表Nor 容量小、价格贵、读快写慢Nand 容量大、便宜、写快读慢。这些结论背后的原因恰好也是“结构决定行为”的延伸。Nand 的单元是串在一条位线上的好处是面积小、密度高坏处是没法按字节随机访问只能按页读、按页写擦除按块。Nor 的并联结构面积大但随机访问性好所以 4MB、8MB 甚至 16MB 就差不多了再做大成本压不住。从系统设计角度看Nor 最大的价值是XIP片内执行。代码直接放在 Nor 里处理器取指就能跑不需要先搬运到 RAM。很多老式 ARM 板卡、单片机的小系统、BIOS ROM 都是这么干的。Nand 就不行坏块管理、ECC 校验、页读取机制这些因素导致它不适合直接映射地址空间执行代码一般要引导代码先把自己拷到内存里。选型的时候参考这个对比会更清楚维度Nor FlashNand Flash存储单元连接并联串联随机读取支持按字节不支持按页XIP 执行可以不可以编程粒度字节/页编程页编程擦除粒度扇区通常 4KB块通常 128KB容量/成本小/高大/低寿命通常 10 万次通常 1 万~10 万次不同工艺差异大常见用途启动代码、固件、参数存储文件系统、大容量数据2. 开发板上的 Nor Flash 是怎么接入 ARM 的2.1 并行接口与内存映射式访问早期的 ARM 裸板Nor Flash 基本都是并口接法。地址线、数据线、片选、读信号、写信号一一对到处理器的外部存储控制器上。有些 SoC 支持“内存映射”模式上电后 Nor 的内容直接被映射到 CPU 地址空间的某个区域比如 0x00000000 或者 0x10000000代码直接跳过去就能执行。这种模式下读操作就是普通的 load 指令处理器在总线上拉低片选和读使能Flash 再把对应地址的数据放上数据总线。写操作则要小心了。你不能直接对一个普通地址执行 store 指令就期望把数据写进 Flash因为 Flash 有自己的状态机。你需要先往特定地址写入命令字、地址、数据让内部状态机进入编程或擦除流程然后再通过查询状态寄存器判断是否完成。芯片手册里会给很详细的命令序列表比如“读 JEDEC ID”要先往地址 0x000000 写入命令 0x90再读取 ID 数据。这种时序本质上是把 Flash 当成一个“带有隐藏控制线的外围设备”来访问。并行接口的好处是速度上限高、访问逻辑简单坏处是引脚占用太多。一个 16 位并口 Nor 动不动就占几十个引脚小封装芯片根本放不下所以现代小型嵌入式系统里并口 Nor 越来越少反而是 SPI Nor 大行其道。2.2 SPI Nor Flash 的接线和访问方式现在的常见做法是 SPI Nor Flash 挂在 SoC 的 SPI 控制器或者 QSPI上。四根线时钟 SCK、主出从入 MOSI、主入从出 MISO、片选 CS。地址不再存在总线上而是你用指令发给它比如发送“读数据”指令 0x03后面跟 3 个字节地址然后从 MISO 上把内容读回来。这种串行访问自然比并口慢一些但对大多数 BootLoader、配置文件、固件存储场景完全够用。我在实际裸板项目里接 SPI Nor 的时候一定会先确认三件事SCK 空闲电平和采样沿也就是 SPI Mode。Nor Flash 一般支持 Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1所以初始化 SPI 控制器时别拍脑袋写代码查 arm 芯片手册或者直接看 Flash 手册的时序图。CS 低电平持续整个命令序列中间不能抬。很多人调试时发现读出来的数偶尔对偶尔错就是因为 GPIO 模拟 CS 的时候在字节之间拉了高电平把器件状态机搞乱了。时钟频率别拉到芯片不支持的值。某些杂牌 Nor Flash 芯片最大时钟没标清楚你按 66MHz 跑数据错乱了好几天最后发现降频到 33MHz 一切正常。2.3 内存映射模式下访问 SPI Nor也可以部分 SoC 设计了 QSPI 控制器支持把 SPI Nor 映射到内存空间。原理是初始化时往 QSPI 控制器里下发一条“读数据”指令并配置好地址模式然后控制器硬件自动产生时钟、在总线上形成存储映射访问。CPU 对这种区域执行普通读指令控制器就会按时序去摸 Flash读到数据后从数据总线返回。这种模式下写操作就麻烦一点要么切回指令模式要么用控制器的特殊寄存器下发写命令。裸机编程如果只是想把上电引导代码放进去我一般建议退出映射模式、用标准的 SPI 指令操作来写写完了再重新映射读回来验证避免被控制器的状态机细节坑到。3. 指令集、状态寄存器和基本操作原理3.1 为什么 Nor Flash 需要一套“命令序列”你可能会想既然是个存储器件为什么不设计成“给地址就出数据、给数据和地址就写进去”因为要平衡成本、可靠性和可编程性内部状态机必须区分普通读操作和控制操作还要控制高压编程/擦除的开关。所以 Nor Flash 规定了一些特定地址上特定数据值的组合作为命令入口比如写使能指令 WREN 是 0x06 单字节命令不需要地址。芯片检测到这个命令后会把内部锁存位置位允许后续编程/擦除操作否则直接发编程命令会被忽略。这套命令设计也带来一个常见问题裸板程序里如果误把一个普通数据写到了命令地址等于给 Flash 下了一条奇怪的指令。所以代码里操作 Flash 的地址和数据缓冲一定要严格分开管理别用同一块 buffer 又存命令地址又存有效载荷数据。我在工程实践中见过不止一次某人改了个共用缓冲区的代码结果“擦除正常、写入的数据变成指令码”排错排到怀疑人生。3.2 最常用的几条指令和状态寄存器位以大多数 SPI Nor Flash 为例几条核心指令像呼吸一样常用指令命令码地址/数据用途WREN0x06无写使能编程/擦除前必须发RDSR0x05无地址读1字节读状态寄存器WRSR0x01写1字节配置保护位/状态位READ0x033字节地址连续读普通读取数据PP0x023字节地址数据页编程通常每页256字节SE0x203字节地址扇区擦除常见4KB扇区BE0xC7无地址整片擦除RDID0x9F无地址读多字节读厂商ID和设备ID状态寄存器最关键的就是bit0 BUSY1 表示 Flash 正在执行擦除或编程0 表示空闲还有bit1 WEL写使能锁存位1 表示允许写操作0 表示写操作被禁止。所以在每个编程或擦除指令发出后要循环读 RDSR 直到 BUSY0这个“轮询等待”是裸机代码里最常见的忙碌循环。有的同学在编程里加了一些奇怪延时代替轮询结果不是等太短没写完就是等太长拖慢速度最稳的做法就是老老实实轮询。3.3 为什么擦除一定是“扇区”或“整片”前面已经说了存储单元的物理结构决定了 0 到 1 必须靠擦除。但擦除动作本身是对一大片区域施加高压脉冲内部电路设计决定了最小粒度为扇区Sector通常 4KB 或 64KB更细粒度做不到。所以如果你只想改 1 个字节也得先把所在的 4KB 扇区整个读出来放到 RAM把那个字节改好再整个扇区擦掉最后把整块内容写回去。这个“读-改-写”流程在参数存储场景里非常常见。还有一个容易忽略的点扇区擦除是有时间参数的不同芯片差别很大典型值在 50ms 到几百毫秒之间。轮询 BUSY 的时候要有超时保护不能死循环无限等。我习惯在等待函数里加一个超时计数超过比如 5 倍典型擦除时间就报错退出避免 Flash 芯片异常挂起时整个系统卡死。3.4 多字节写入要小心“页”边界SPI Nor Flash 的编程指令是页编程Page Program一页通常是 256 字节。你可以在一个 PP 指令里连续写入最多一页的数据但地址超过页边界时会自动回卷到页首覆盖掉页开头的旧数据这绝对是裸机新手最容易踩的坑。比如你想往地址 0x10 开始写 0x120 字节数据直接发一个 PP 命令地址 0x10 会写数据的前 0xF0 字节然后地址计数器回卷到页首后面 0x30 字节会覆盖页头 0x10~0x3F 的内容数据全乱了。所以“跨页写入”必须在上层代码里手动分段每段长度不超过当前页剩余空间目标地址每段递增每段发一次 PP 命令。下面代码会给出这个分段逻辑的完整实现。4. 裸机编程示例一个可复现的 SPI Nor Flash 驱动4.1 先说清楚开发环境和我用的抽象方式下面代码面向一个典型的 ARM 处理器SPI 控制器寄存器和 GPIO 寄存器我用宏定义代替方便移植到不同芯片。实际项目中你要替换成自己 SoC 手册里的寄存器定义。为了表达清楚我假设有以下基础设施spi_cs_low()/spi_cs_high()拉低/拉高片选线spi_read_write_byte()通过 SPI 数据寄存器收发一个字节返回收到的数据spi_set_mode()设置时钟极性和相位spi_set_speed()设置 SCK 频率udelay()微秒延时这种抽象方式在裸机项目里很常见你可以直接抄到自己的 BSP 层。4.2 初始化 SPI 控制器和 Nor Flash初始化的步骤是先配置 GPIO 复用功能把 SCK/MOSI/MISO/CS 脚设为 SPI 模式然后设置 SPI 控制器为主模式、时钟极性相位为 Mode 0 或 Mode 3Nor Flash 手册通常会推荐其中一个同时把时钟频率控制在一个安全范围内。如果频率太高板子的走线寄生电容或信号完整性问题就会出现导致误码。void nor_flash_init(void) { // 1. 配置 GPIO 复用具体寄存器以 SoC 手册为准 gpio_set_alt_function(PIN_SCK, ALT_SPI); gpio_set_alt_function(PIN_MOSI, ALT_SPI); gpio_set_alt_function(PIN_MISO, ALT_SPI); gpio_set_alt_function(PIN_CS, ALT_SPI); // 也可以用普通 GPIO 控制灵活起见很多人喜欢 GPIO 模拟 // 2. 配置 SPI 主模式Mode 0CPOL0CPHA0 spi_set_mode(SPI_MODE_0); // 3. 工作频率初始设保守一点比如 20MHz spi_set_speed(20000000); // 4. 片选默认高电平 spi_cs_high(); }这里我特意把 CS 拉高放在初始化最后一步。上电的时候 Flash 的状态机要求 CS 从高往低跳变才算一帧命令的开始所以默认保持 CS 高电平很重要。如果你在自己板子上发现“复位后第一条命令发不出去”多半是初始化过程中 CS 有过一段低电平的毛刺Flash 状态机被弄糊涂了。初始化完成后可以马上读一次 JEDEC ID 验证通信链路。读 ID 的指令是 0x9F指令后不需要地址直接连续读厂商 ID 和设备 ID。这是裸机上硬件调试的“Hello World”void nor_flash_read_id(uint8_t *id_buf, uint32_t len) { uint32_t i; spi_cs_low(); spi_read_write_byte(0x9F); for (i 0; i len; i) { id_buf[i] spi_read_write_byte(0x00); } spi_cs_high(); }如果读回来的全是 0x00 或者 0xFF先别急着怀疑 Flash 芯片坏了。很大概率是 SPI 模式反了应该用 Mode 3 你用了 Mode 0或者 MISO/MOSI 接反了或者芯片供电没起来。4.3 写使能、等待空闲和状态寄存器操作Nor Flash 的规矩是编程和擦除之前必须先发写使能命令 0x06把状态寄存器的 WEL 位置 1。这个命令非常简单但我建议把它封装成一个函数因为每次编程/擦除前都要调用static void nor_flash_write_enable(void) { spi_cs_low(); spi_read_write_byte(0x06); spi_cs_high(); } static uint8_t nor_flash_read_status(void) { uint8_t status; spi_cs_low(); spi_read_write_byte(0x05); status spi_read_write_byte(0x00); spi_cs_high(); return status; } static void nor_flash_wait_busy(uint32_t timeout_ms) { uint32_t count 0; while (nor_flash_read_status() 0x01) { udelay(1000); count; if (count timeout_ms) { // 这里可以返回错误码实际代码里建议用返回值上报 break; } } }等待循环里我用的是毫秒级计数。如果你做的是对时间敏感的场景比如把擦除操作放在中断上下文里那就得改造成非阻塞方式依靠状态轮询回调推进状态机。不过裸机单线程项目里阻塞等待通常完全够用关键是超时上限设好。4.4 读取数据、页编程和扇区擦除的实现读数据指令 0x03 后面跟 3 字节地址然后从 MISO 连续读数据。地址是大端顺序高字节、中字节、低字节。代码实现void nor_flash_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; spi_cs_low(); spi_read_write_byte(0x03); spi_read_write_byte((addr 16) 0xFF); spi_read_write_byte((addr 8) 0xFF); spi_read_write_byte(addr 0xFF); for (i 0; i len; i) { buf[i] spi_read_write_byte(0x00); } spi_cs_high(); }页编程的代码要点有两个第一编程前要写使能第二数据长度不能超过当前页剩余空间。下面这个函数实现“向任意地址写入连续数据自动分段成多次页编程”这也是我实际项目里直接用的一版void nor_flash_program(uint32_t addr, const uint8_t *data, uint32_t len) { uint32_t page_size 256; uint32_t written 0; while (written len) { uint32_t remain_in_page page_size - (addr % page_size); uint32_t chunk (len - written) remain_in_page ? (len - written) : remain_in_page; uint32_t i; nor_flash_write_enable(); spi_cs_low(); spi_read_write_byte(0x02); spi_read_write_byte((addr 16) 0xFF); spi_read_write_byte((addr 8) 0xFF); spi_read_write_byte(addr 0xFF); for (i 0; i chunk; i) { spi_read_write_byte(data[written i]); } spi_cs_high(); nor_flash_wait_busy(100); written chunk; addr chunk; } }扇区擦除的命令是 0x20后面跟 3 字节目标扇区地址。注意地址得落在本扇区内的任意位置芯片内部会按照扇区对齐来擦除。我通常习惯把地址低位清零再发命令逻辑更严谨一些void nor_flash_erase_sector(uint32_t addr) { addr ~(4096UL - 1); // 对齐到 4K 扇区边界 nor_flash_write_enable(); spi_cs_low(); spi_read_write_byte(0x20); spi_read_write_byte((addr 16) 0xFF); spi_read_write_byte((addr 8) 0xFF); spi_read_write_byte(addr 0xFF); spi_cs_high(); nor_flash_wait_busy(1000); }整片擦除就更简单了一条 0xC7 命令随后等待。注意整片擦除时间可能长达几秒超时时间必须给足我习惯给 5 秒以上。整片擦除是一次性把全部扇区恢复成 1适合量产时清空芯片或者调试时快速擦掉整个固件区域。4.5 写一个简单的验证流程代码写到这一步我强烈建议你写一个自检函数把读写擦串起来验证免得后面 BootLoader 调不通时无法定位是 Flash 还是之前的内存问题。最简单的验证流程是擦除某个扇区比如地址 0。读回整个扇区全部应为 0xFF。往扇区里写一组递增模式的数据比如 0x00、0x01、0x02……读回并逐字节比对。对于跨页测试可以从 0xF0 地址开始写 0x40 字节数据专门考验跨页分段逻辑。比如uint8_t read_buf[256]; uint8_t write_buf[256]; uint32_t i; nor_flash_erase_sector(0); nor_flash_read(0, read_buf, 256); for (i 0; i 256; i) { if (read_buf[i] ! 0xFF) { /* 擦除验证失败 */ } } for (i 0; i 256; i) { write_buf[i] (uint8_t)i; } nor_flash_program(0xF0, write_buf, 256); nor_flash_read(0xF0, read_buf, 256); for (i 0; i 256; i) { if (read_buf[i] ! write_buf[i]) { /* 数据不一致 */ } }这套流程每次移植驱动后我会跑一遍确认过了再继续写上层逻辑。4.6 更可靠一点尝试状态码和坏块意识无论并行还是 SPI Nor理论上都有坏块只是坏块比例通常很低。Nor Flash 不像 Nand 那样要求强制坏块管理但严谨一点也可以在擦除或编程后读回校验发现异常就把扇区标记为坏。这个开销对引导代码场景基本可以接受。量产烧写的时候校验不仅是防止芯片坏还可以发现夹持不稳、电压跌落等问题所以我建议烧写器或者裸机烧录引导程序里都要做读回校验。5. 实际调试中的坑与排查方法5.1 写不进去、擦不掉最先查什么遇到这种问题我会按以下顺序排查这个顺序是踩坑踩出来的经验写使能漏了。编程/擦除前不发 0x06Flash 直接忽略操作状态寄存器 WEL 一直为 0。代码里用nor_flash_write_enable()的地方漏掉一个现象就是别的扇区能写、某个扇区不能。CS 时序不正确。CS 必须在完整命令序列期间保持低电平指令和地址/数据的字节之间不能断。如果是 GPIO 模拟 CS查一下有没有在spi_read_write_byte()中间意外抬高了 CS。BP 位保护。状态寄存器 bit2~bit5 可能定义了区域写保护如果之前有人往状态寄存器写过一个值保护了全部空间你的指令都会被忽略。解决办法是读 RDSR 看保护位必要时用 WRSR 指令把状态寄存器设为 0。硬件写保护引脚。很多板子把 Flash 的 WP 引脚写保护引脚拉到地或者做成了可配置电阻如果没拉高写入功能压根不使能。这是硬件原理图层面最隐蔽的坑。5.2 读出来的数据不对大概率是模式或接线问题如果你能读到 ID 但数据错乱先检查 SPI Mode。Nor Flash 全系列通常工作于 Mode 0 或 Mode 3就两种选择挨个试就知道。重点在于你必须确认SCK 空闲电平和数据采样沿之间的组合有些芯片手册画得含含糊糊直接看时序图比看文字准。另外 MSB 先行还是 LSB 先行也特别坑人。SPI Nor Flash 几乎都是 MSB first但某个 ARM 的 SPI 控制器可能有个“LSB first”位默认值有时候不是 0。一看到数据位序完全反了先把这位置 0 试一下。如果读出来的数据是“隔一个字节错位”之类那基本可以怀疑 MOSI/MISO 在 PCB 上走线交叉或者 GPIO 复用搞错了。用逻辑分析仪抓一拍就能快速确认示波器其实未必比逻辑分析仪好用因为 SPI 是数字协议看波形不如直接看解码结果。5.3 时钟太快引起偶发错误偶发性错误最烦人。一个典型场景是刚焊好的板子测试一切正常跑了半小时偶尔读错一个字节。这种十有八九跟信号完整性相关。排查手段是降频如果降频后问题消失就取当前速度的一半作为量产默认值这是最实在的解决办法。SCK 走线过长或过孔过多时频率越高边沿越钝数据采样点越不稳定不一定要给系统极限值。5.4 擦除时间太长导致的超时误判我以前犯过一个错把扇区擦除超时时间设成 100ms结果有些牌的 Flash 芯片典型擦除时间就是 100ms 多一点导致偶尔报错。后来我把超时设置成“典型值的 5~10 倍”也就是扇区擦除 1~2 秒、整片擦除 5 秒问题就消失了。嵌入式里也有很多类似场景超时值不是越短越好关键是要区分“操作失败”和“操作慢”。5.5 怎么快速验证通信链路一定要学会用读 ID 作为第一条调试手段而不是直接读写数据。读 ID 只需要发送一个字节指令不需要地址和数据出错概率小更容易定位是 SPI 层的问题还是 Flash 内部逻辑的问题。如果你手头有逻辑分析仪抓 CS、SCK、MOSI、MISO 四个信号看一眼帧格式就能判断大部分问题。我调试驱动的时候永远先看逻辑分析仪上的帧结构确认第一个字节命令码、地址字节顺序都对再去追查数据内容。6. 再补充几个工程实操的细节6.1 地址空间划分和“分区表”思想如果你用 Nor Flash 同时存 BootLoader、应用固件和配置参数那最好在程序设计之初就规划好分区哪个地址区间放 BootLoader哪个区间放应用程序哪个区间放参数。Nor Flash 擦除粒度是扇区所以分区大小尽量按扇区整倍数来画不然到了集成阶段你会发现一个扇区里既有配置参数又有固件尾巴升级固件时把配置也给擦没了。我常用的做法是定义一张分区表结构体包含名称、起始地址、长度、属性只读/可擦写。裸机程序里做成 const 数组放在代码里方便所有模块引用typedef struct { const char *name; uint32_t start; uint32_t size; } flash_partition_t; const flash_partition_t g_partitions[] { { bootloader, 0x000000, 0x010000 }, { app, 0x010000, 0x100000 }, { config, 0x110000, 0x010000 }, };这样无论是写烧录脚本、做固件升级还是调试时查看指定位置的数据都有一份统一依据不容易出错。6.2 启动代码里的 Nor Flash 操作注意什么如果 Nor Flash 用来放启动代码那上电瞬间的行为就得仔细想清楚。芯片复位后处理器从启动地址取指令如果你的 BootLoader 就放在 Nor 里而被映射到了 0 地址那么第一条指令会被当成普通读这个没问题。但你绝不能在上电初期就对 Nor 做擦写操作因为此时可能是只读映射写操作会导致处理器抓取完全混乱的指令流比死机还难查。我见过一种稳妥的方案BootLoader 前 4KB 是第一阶段引导代码只做最基本的硬件初始化和 DDR 初始化然后把完整的第二阶段代码从 Nor 复制到 DDR 执行第二阶段才具备擦写 Nor 的能力。这样即使升级固件失败第一阶段还在 Nor 里不会把板子“变砖”。至少对复杂一点的系统这种分层引导模式很值得参考。6.3 固件升级场景下的断点续写稍微提一下固件升级升级程序写 Nor 的时候如果中途断电Flash 里可能出现一个“半写”的固件。为了减少变砖风险可以加一个升级标志区域升级前先把标志位写成“升级中”写完新固件并校验成功后再把标志改成“完成”。启动代码看到“升级中”就自动进入恢复模式从备份区或外部通道重新获取固件。这个方案不复杂但对实际产品的可靠性提升非常明显。标志区本身最好固定在一个小扇区单独划分别跟固件混在一起。因为标志位会频繁改写每次改写都要整扇区擦除重写放独立小扇区可以避免频繁擦写主固件区。6.4 底层的性能优化思路如果你对 Nor Flash 的写入性能有要求能优化最明显的一点就是合并连续页编程。一页数据能一次写完就不要拆成多次因为每次编程前都有写使能和轮询等待。我做固件烧录时会在内存里把数据分批重组到 256 字节页对齐然后一次页编程写一整页速率比逐字节编程高好几倍。读取侧也有类似空间如果 Flash 支持双倍速率或四线模式比如 QSPI在初始化时直接启用可以显著提高吞吐。不过这涉及 QSPI 指令集和 IO 配置代码复杂度会高一些一般到需要给显示 framebuffer 或音频缓冲供数据时才值得上。7. 最后分享一点个人习惯跟 Nor Flash 打交道久了我养成了几个比较“土”但特别有效的习惯。第一个习惯是所有驱动函数都留一个超时返回值的口子哪怕当前调用者不检查也比死循环强。第二个习惯是每个板子到手先写一个“Flash 体检”小程序把读写擦验证跑 10 遍全部通过才把它当做好使的硬件平台要不然后面调试应用Flash 偶尔出错会把锅甩给完全无关的模块。第三个习惯是文档里所有地址、长度、指令码都以 Flash 手册为准但我会在代码注释里再标注“参考手册第几页的 Table”这样三个月后自己回来看代码不用再翻几十页 PDF。Nor Flash 这片领域看起来小但它承载了嵌入式系统里最底层的启动和存储职责。把它的结构、命令、时序和调试手段吃透后面再看 QSPI、XIP、固件升级这些上层话题都会顺畅很多。如果你手头正在调一块板子不妨照着我上面的自检流程走一遍大概率能帮你少熬一个夜。
RELATED

相关推荐

深入uboot第一阶段源码:ARM启动流程与内存初始化实战

深入uboot第一阶段源码:ARM启动流程与内存初始化实战

1. 为什么还要啃uboot第一阶段源码很多人学嵌入式Linux,到了uboot这一关就开始犯怵。网上的教程一抓一大把,但大多数停留在"编译、烧录、能跑就行"的层面,真正翻开源码一行行读下去的少之又少。我当初也是这么过来的——板子能启动…

📅 2026/10/12 13:23:53
车载终端开发实战:同时满足JT/T808与GBT19056的完整指南

车载终端开发实战:同时满足JT/T808与GBT19056的完整指南

项目一开始,我其实是被“行车记录仪”五个字带偏了的。客户说要做一台能对标平台、能导出数据、又能装车的终端,我第一反应还是老式行业记录仪的老路子——本地存曲线、锁文件、接上位机。后来翻到项目标题里的两个标准编号才反应过来,这是要…

📅 2026/10/12 13:23:53
SHAP力图Matplotlib标签重叠的解决方案与批量出图指南

SHAP力图Matplotlib标签重叠的解决方案与批量出图指南

做模型解释的时候,SHAP力图几乎是绕不开的标配,但每次调用Matplotlib渲染,我都会被同一个问题折腾一番:明明模型训练得很顺,一到出图,特征名和数值标签就挤成一团,红的蓝的色块互相叠盖&#xf…

📅 2026/10/12 13:23:53
MORE NEWS

更多资讯

📰

sim-use 3分钟上手:observe-act-verify循环让AI验证自己写的移动App

【免费下载链接】sim-use Give your AI agent eyes and hands on iOS Simulator and Android emulator/devices. 项目地址: https://gitcode.com/gh_mirrors/si/sim-use 点击查看 免费下载 sim-use 是一个跨平台 CLI 工具,它为 AI Agent 提供在 iOS 模拟…

📰

智谱刚把 GLM-5.3 搬上 Amazon Bedrock,股价应声涨近 5%:国产大模型出海怎么突然就火了

智谱刚把 GLM-5.3 搬上 Amazon Bedrock,股价应声涨近 5%:国产大模型出海怎么突然就火了 【免费下载链接】GLM-5.3 GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升…

📰

C++手写CSV解析器:从状态机到流式处理实战

1. 从一次数据清洗任务说起:为什么CSV值得单独写一篇文章前阵子帮一个做实验数据分析的朋友处理一批传感器日志,对方甩过来一个压缩包,解压后是几十个后缀为.csv的文件,每个文件大概几万行,列数从十几列到三十几列不等…

📰

Pomotroid 土耳其语本地化实践:基于 Paraglide 的 8 语言架构与新增 locale 全流程

【免费下载链接】pomotroid :tomato: Simple and visually-pleasing Pomodoro timer 项目地址: https://gitcode.com/gh_mirrors/po/pomotroid 点击查看 免费下载 本文以 Pomotroid 新增土耳其语(tr)支持这一真实变更为主线,讲解…

📰

openJiuwen DeepSearch 研究规划提示模板(planner/user.md)深度解析:从 Jinja 渲染到计划生成的全链路实现

人工智能大模型AI AgentRAG深度研究搜索引擎后端代码智能体 【免费下载链接】deepsearch openJiuwen DeepSearch是一款知识增强的深度搜索与研究框架,有业界领先的片段级引用和溯源推理能力,提供精准Agentic搜索与研究能力 项目地址: https:/…

📰

【宠粉赠书】分布式系统性能优化:方法与实践——用 TaoToken 统一 Key 打通压测与调优链路

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

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬