嵌入式裸板开发:nor flash 原理、时序与擦写实战

发布时间:2026/10/15 17:59:34
嵌入式裸板开发:nor flash 原理、时序与擦写实战 1. 为什么还要折腾 nor flash 这种老古董1.1 从一块 JZ2440 开发板说起手头这块 JZ2440 开发板用的是三星 S3C2440 这颗 ARM9 芯片主频 400MHz内存 64MB SDRAM存储部分板载了两片一片是 2MB 的 nor flash型号常见为 EN29LV160 或 MX29LV160 这类另一片是 256MB 的 nand flashK9F2G08 之类。很多人拿到板子第一反应是直接上 nand毕竟容量大、便宜但真正想把裸板启动流程吃透nor flash 是绕不过去的一关。原因很直接S3C2440 的启动方式由 OM[1:0] 两个引脚决定当配置为 nor 启动时CPU 上电后第一条指令直接从 nor flash 的 0x00000000 地址取。这意味着 nor flash 是可以直接被 CPU 按地址总线随机访问的不需要任何初始化代码搬运。而 nand flash 不行它必须靠芯片内部一块叫“Steppingstone”的 4KB SRAM 先把前 4KB 搬到内存里才能跑。所以 nor 启动的代码执行模型是最朴素的——PC 指到哪代码就在哪执行没有搬运、没有重映射这对理解“程序到底怎么跑起来的”极其友好。这一课的核心目标就三件事搞清楚 nor flash 的硬件接口和时序、能在 nor 上直接跑裸板程序、以及实现 nor flash 的擦除与烧写。适合已经学过 GPIO、串口、时钟配置想进一步理解存储子系统和启动流程的嵌入式学习者。哪怕你以后不做 nor这套“读 ID → 解锁 → 擦除 → 写 → 校验”的流程和后来玩 SPI flash、EEPROM 是同一套思维。1.2 nor 和 nand 到底差在哪别只会背结论网上到处是“nor 读取快、写入慢、贵、容量小nand 读取慢、写入快、便宜、容量大”这种结论但做裸板开发光背这个没用得知道差异从哪来。nor flash 的存储单元是并行结构每个 bit 通过字线和位线直接寻址所以支持像内存一样的随机字节访问读一个字节和读一万个字节的延迟几乎一样。代价是单元面积大做不大容量。nand flash 是串行结构数据按页page通常 512B 或 2KB读写按块block通常 16KB 或 128KB擦除访问必须走命令序列不能直接寻址。这就带来一个关键区别nor 上可以执行代码XIPeXecute In Placenand 上不行。你在 nor 里写个死循环CPU 就真的在那儿一条条取指执行nand 里你没法这么干必须先搬。所以 nor 常用来放 bootloader 的第一阶段nand 用来放内核和文件系统。还有一个坑nor 的“写”其实叫“编程”program只能把 bit 从 1 变成 0不能从 0 变回 1。想把 0 变 1 必须先擦除而擦除的最小单位是扇区sector通常 64KB或整片。所以 nor 的写操作永远是“先擦后写”而且擦除很慢一个扇区动辄几百毫秒到几秒。这个特性决定了你写 nor 驱动时擦除函数必须带超时轮询不能傻等。2. 硬件连接与时序先看懂原理图再动手2.1 地址线、数据线与片选是怎么接的S3C2440 的存储控制器把地址空间分成了 8 个 bank每个 bank 128MB。nor flash 一般挂在bank0因为 bank0 是启动 bank上电后 CPU 从 bank0 的 0x00000000 取指。bank0 的片选信号 nGCS0 直接连到 nor 的 CE# 引脚。地址线方面nor 是 16 位宽一个字 2 字节所以 CPU 的地址线 A1 接到 nor 的 A0A2 接 A1以此类推。为什么错开一位因为 CPU 按字节编址而 nor 按“字”编址一个 nor 地址对应 2 个字节。比如 CPU 访问 0x00000000 和 0x00000001 这两个字节实际都落在 nor 的同一个字地址 0 上靠字节选择信号nBE0/nBE1 或 nWBE/nUB、nLB区分高低字节。这个“地址错位”是新手最容易懵的点画原理图时如果 A0 直接接 A0读出来的数据就会整体错乱。数据线 D0~D15 直连读写信号 nOE输出使能和 nWE写使能分别接 nor 的 OE# 和 WE#。另外 nor 还有几个控制引脚RESET# 接复位、RY/BY# 是就绪/忙状态输出可选接 GPIO 轮询、WP# 是写保护一般拉高。2.2 时序参数怎么算别照抄别人的值S3C2440 的 bank0 时序由 BWSCON、BANKCON0 两个寄存器配置。BANKCON0 里有 Tacs、Tcos、Tacc、Tcoh、Tcah、Tacp、PMC 这几个字段单位都是 HCLK 周期数。nor 的读写时序必须满足芯片手册里的 tRC、tCE、tOE、tAA 等参数。以常见的 70ns nor比如 MX29LV160为例假设 HCLK 100MHz一个周期 10ns。读访问时间 tAA 最大 70ns那么 Tacc 至少要 7 个周期。Tacc 字段是 3 位取值 0~7 对应 1~8 个周期所以填 6对应 7 周期。Tacs地址建立和 Tcos片选建立一般给 0 或 1 个周期就够Tcoh片选保持给 0Tcah地址保持给 0。这里有个经验宁可时序给宽一点也别卡着极限值。因为 HCLK 可能因为 PLL 配置变化而且 nor 芯片在不同温度下时序会漂移。我一般把 Tacc 多给 1~2 个周期实测下来稳定性明显提升代价只是读速度慢一点点对裸板调试完全无所谓。配置代码大概长这样#define BWSCON (*(volatile unsigned int *)0x48000000) #define BANKCON0 (*(volatile unsigned int *)0x48000004) void nor_init(void) { /* bank0: 16位宽, 使能WAIT(可选) */ BWSCON ~(0xF 0); BWSCON | (0x1 0); /* DW0 01, 16位 */ /* Tacc7周期, Tacs1, Tcos1, Tcoh0, Tcah0 */ BANKCON0 (6 8) | (1 6) | (1 4) | (0 2) | (0 0); }注意BWSCON 的 DW0 字段决定 bank0 数据宽度nor 是 16 位就填 01。如果填错成 8 位读出来的数据会高低字节错位现象是读 ID 时返回 0x00C2 而不是 0x00C2 的正确排列很容易误判成芯片坏了。3. nor flash 的读 ID 与命令序列3.1 为什么读 ID 是第一步拿到一块 nor第一件事不是急着擦写而是读它的制造商 ID 和设备 ID。这一步能验证三件事地址线接对了没、数据线接对了没、时序配对了没。如果 ID 读出来是 0xFFFF 或 0x0000基本就是硬件或时序问题。nor 的读 ID 不是直接读某个地址就完事它需要走一套“命令序列”也就是往特定地址写特定命令字。这是 nor 内部状态机的设计正常模式下它就是一块只读内存只有通过写命令才能切换到“读 ID 模式”“擦除模式”“编程模式”。以 MX29LV160 为例命令序列是步骤地址nor 视角数据10x5550xAA20x2AA0x5530x5550x904读 0x000制造商 ID5读 0x001设备 ID6写 0xF0复位回读模式注意这里的地址是nor 的字地址不是 CPU 的字节地址。因为前面说了地址线错开一位CPU 访问 nor 字地址 0x555 时实际 CPU 地址是 0x555 1 0xAAA。所以代码里写的时候要小心通常我们直接按 CPU 地址写然后让宏来转换。#define NOR_BASE 0x00000000 #define NOR_ADDR(offset) (*(volatile unsigned short *)(NOR_BASE ((offset) 1))) void nor_read_id(unsigned short *man_id, unsigned short *dev_id) { NOR_ADDR(0x555) 0xAA; NOR_ADDR(0x2AA) 0x55; NOR_ADDR(0x555) 0x90; *man_id NOR_ADDR(0x000); *dev_id NOR_ADDR(0x001); NOR_ADDR(0x000) 0xF0; /* 复位 */ }3.2 命令序列背后的状态机逻辑很多人抄完代码能跑就不管了但我想说清楚这套 0xAA/0x55 序列的用意。它是 nor 厂商为了防止误写设计的“软件锁”单个写操作不会触发任何改变必须连续三个特定地址写特定数据内部状态机才认为这是一条合法命令。这就像保险箱要转三圈密码随便拧一下没用。0xAA 和 0x55 是交替的位模式10101010 和 01010101能有效检测地址线或数据线是否有短路、粘连。如果某根地址线断了这个序列可能就写不进去命令自然不生效起到保护作用。读 ID 命令 0x90 之后nor 进入 ID 模式此时读地址 0x000 返回制造商 ID0x001 返回设备 ID。不同厂商的 ID 不同比如 MXIC 是 0x00C2Spansion 是 0x0001AMD 是 0x0001 等。设备 ID 能告诉你容量和扇区结构比如 0x2249 表示 2MB、扇区 64KB。实操心得读 ID 之前一定要先执行一次复位命令 0xF0因为如果上一次操作中途断电或异常退出nor 可能还停在某个中间状态直接读 ID 会失败。养成“上电先复位”的习惯能省掉很多莫名其妙的调试时间。4. 擦除与编程nor 写的完整流程4.1 扇区擦除的时序与超时处理nor 的擦除最小单位是扇区。MX29LV160 的扇区结构是底部有 4 个 8KB 的小扇区SA0~SA3然后 1 个 16KB再 1 个 8KB剩下 31 个 64KB 大扇区。擦除命令序列是步骤地址数据10x5550xAA20x2AA0x5530x5550x8040x5550xAA50x2AA0x556扇区地址0x30第 6 步往目标扇区的任意地址写 0x30就触发该扇区擦除。擦除开始后nor 进入忙状态此时读任何地址返回的数据 DQ7 是 0真实数据取反DQ6 会翻转。判断擦除完成有两种方式轮询 DQ7 或 DQ6或者读 RY/BY# 引脚。我一般用 DQ6 翻转法因为它不依赖具体数据int nor_sector_erase(unsigned int sector_addr) { NOR_ADDR(0x555) 0xAA; NOR_ADDR(0x2AA) 0x55; NOR_ADDR(0x555) 0x80; NOR_ADDR(0x555) 0xAA; NOR_ADDR(0x2AA) 0x55; NOR_ADDR(sector_addr) 0x30; unsigned short dq6_old NOR_ADDR(sector_addr) 0x40; unsigned short dq6_new; unsigned int timeout 0x1000000; while (timeout--) { dq6_new NOR_ADDR(sector_addr) 0x40; if (dq6_new dq6_old) break; dq6_old dq6_new; } if (timeout 0) return -1; /* 超时 */ NOR_ADDR(0x000) 0xF0; /* 复位 */ return 0; }超时值 0x1000000 是我实测出来的经验值。64KB 扇区擦除典型时间 0.5~1 秒按每次轮询几个周期算这个循环次数足够覆盖最坏情况。如果超时了八成是硬件问题或者时序太紧。4.2 编程写操作与数据校验nor 的编程也是按字16 位或按字节写但只能把 1 写成 0。所以写之前必须确保目标区域已经擦除全 0xFF。编程命令序列步骤地址数据10x5550xAA20x2AA0x5530x5550xA04目标地址数据写完一个字后nor 进入忙状态同样用 DQ6 翻转判断完成。注意每次写一个字都要重新走一遍命令序列不能连续写多个字。这是 nor 编程慢的根本原因——每个字都要三次命令开销。int nor_program_word(unsigned int addr, unsigned short data) { NOR_ADDR(0x555) 0xAA; NOR_ADDR(0x2AA) 0x55; NOR_ADDR(0x555) 0xA0; NOR_ADDR(addr) data; unsigned short dq6_old NOR_ADDR(addr) 0x40; unsigned short dq6_new; unsigned int timeout 0x100000; while (timeout--) { dq6_new NOR_ADDR(addr) 0x40; if (dq6_new dq6_old) break; dq6_old dq6_new; } if (timeout 0) return -1; /* 校验 */ if (NOR_ADDR(addr) ! data) return -2; NOR_ADDR(0x000) 0xF0; return 0; }注意编程完成后一定要读回校验。nor 编程失败有两种一种是根本没写进去返回 0xFFFF一种是位翻转某些 bit 没从 1 变 0。校验能立刻发现避免后面跑程序时出现玄学 bug。5. 把程序烧进 nor 并直接运行5.1 链接脚本与代码定位nor 启动时代码从 0x00000000 开始执行。所以链接脚本要把代码段定位到 0x00000000SECTIONS { . 0x00000000; .text : { *(.text) } .rodata : { *(.rodata) } .data : { *(.data) } .bss : { *(.bss) *(COMMON) } }但这里有个问题nor 是只读的写需要特殊命令所以.data段已初始化全局变量不能直接放在 nor 里被修改。标准做法是把.data的初始值存在 nor 里启动时拷贝到 SDRAM.bss清零。不过如果程序很小、全局变量很少也可以把变量都放栈上避免这个麻烦。我这一课为了聚焦 nor 本身程序里几乎不用全局变量所有数据都在栈上这样链接脚本简单启动代码也简单不用做段拷贝。等后面讲 nand 启动时再引入完整的段搬运。5.2 烧写工具与流程把编译出的 bin 烧进 nor通常用 JTAG比如 OpenJTAG配合工具或者用板子自带的 nor 烧写功能。流程是编译生成led.bin用 JTAG 工具连接板子初始化 CPU 和 SDRAM把 bin 下载到 SDRAM 的某个地址比如 0x30000000调用 nor 擦除函数擦除目标扇区调用 nor 编程函数把 SDRAM 里的数据逐字写进 nor复位板子设置 OM 引脚为 nor 启动观察现象这里第 4、5 步的擦除和编程函数就是前面写的那些。实际烧写时我会先擦除整个要用的区域再逐字写最后整体读回比对一遍。比对通过才算烧写成功。实操心得烧写前先用读 ID 确认 nor 在位烧写后再读回校验。我踩过的坑是 JTAG 下载到 SDRAM 时地址没对齐导致写进 nor 的数据整体偏移现象是程序跑飞。后来养成习惯下载地址一定用 4 字节对齐的比如 0x30000000。6. 常见问题与排查速查6.1 读 ID 失败怎么办读 ID 返回 0xFFFF 或 0x0000按这个顺序排查现象可能原因排查方法返回 0xFFFF片选没拉低、nor 没供电万用表量 CE# 和 VCC返回 0x0000数据线全被拉低、短路量数据线对地电阻返回乱码地址线错位、时序太紧检查 A0 是否接 nor A0放宽 Tacc读一次对一次错时序临界增加 Tacc 周期数我遇到最多的是地址线错位。有一次原理图上 A0 直接接了 nor 的 A0结果读 ID 时制造商 ID 和设备 ID 位置互换折腾了半天才发现。记住16 位 norCPU 的 A1 接 nor 的 A0。6.2 擦除或编程超时超时基本三个原因时序太紧、nor 处于写保护、命令序列写错。先检查 WP# 引脚是否被拉低写保护再放宽时序最后用示波器抓命令序列的波形看 0xAA/0x55 有没有正确出现在总线上。还有一个隐蔽的坑擦除和编程期间不能被打断。如果你的程序里有中断而中断服务程序里又去读 nor会干扰 DQ6 轮询。所以擦写 nor 时最好关中断或者确保中断里不碰 nor。6.3 程序烧进去不运行先确认 OM 引脚设置正确nor 启动再确认链接地址是 0x00000000最后确认烧写的数据和 bin 一致。我见过有人把 bin 烧到了 nor 的 0x1000 偏移结果 CPU 从 0x0 取指取到全 0xFF直接跑飞。烧写时目标地址一定要从 0 开始。7. 我个人在 nor 调试上的几个习惯第一个习惯是每次上电先读 ID。不管程序多简单启动代码里加一句读 ID 并打印到串口能立刻确认 nor 硬件通路正常。这一步花不了几毫秒但能省掉大量“程序不跑”的排查时间。第二个习惯是擦写函数都带超时和返回值。裸板开发没有操作系统兜底一个死循环就能让整个系统卡死。所有轮询都必须有超时退出返回错误码上层根据错误码决定重试还是报错。第三个习惯是写 nor 之前先整片读一遍。如果 nor 里已经有数据先读出来存到 SDRAM擦写完成后再比对能发现擦除是否彻底。尤其是调试阶段反复烧写nor 里可能残留旧数据导致新程序行为诡异。nor flash 这套东西看起来老但它是理解“存储介质如何被 CPU 访问”的最佳教材。把这套流程走通后面看 SPI flash、eMMC、甚至 SSD 的 FTL 层都会觉得似曾相识。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询