1. 从零拆解JZ2440裸板开发中的NOR Flash
搞嵌入式裸板开发的朋友,尤其是玩过ARM9那一代经典开发板的人,对“NOR Flash”这个词一定不陌生。我最初接触JZ2440这块板子的时候,满脑子都是“裸板”两个字——没有操作系统、没有文件系统、没有驱动框架,所有东西都得自己从寄存器层面一点点抠出来。而NOR Flash恰恰是这块板子上最值得深挖的存储器件之一,因为它直接关系到你的程序能不能被正确烧录、能不能在断电后依然保存、能不能在启动时被CPU正确读取并执行。
这篇文章要聊的就是JZ2440裸板开发中关于NOR Flash的完整实践。我会从NOR Flash的基本特性讲起,拆解它在JZ2440这块板子上的硬件连接方式、控制器配置逻辑、读写擦除操作的底层实现,以及在实际调试中容易踩的坑。适合已经有一定ARM裸板基础、正在学习存储器件驱动开发、或者想搞清楚“程序到底是怎么从Flash跑到内存里执行”的嵌入式学习者。如果你之前只会在IDE里点“下载”按钮,对底层发生了什么一无所知,那这篇内容应该能帮你把这块知识补上。
我个人的习惯是,学任何一块外设之前,先搞清楚三件事:它是什么、它接在哪里、CPU怎么跟它说话。NOR Flash也不例外。下面我就按这个思路,把JZ2440上NOR Flash的开发过程完整梳理一遍。
2. NOR Flash的核心特性与选型逻辑
2.1 NOR Flash为什么适合存启动代码
NOR Flash和NAND Flash虽然都叫Flash,但两者的脾气完全不同。NOR Flash最大的特点是支持随机访问,你可以像读内存一样直接读取任意地址的数据,不需要先发命令再读页。这个特性决定了它非常适合存储启动代码,因为CPU上电后需要立刻从某个固定地址取指令执行,如果存储器件不支持随机访问,CPU就没法直接运行里面的代码。
JZ2440上用的NOR Flash型号是SST39VF1601或者兼容型号,容量通常是2MB。这块芯片的接口是并行接口,地址线和数据线分开,CPU通过总线直接访问。你可以把它理解成一块“只读的内存”,读操作几乎和读SDRAM一样简单,但写操作就麻烦得多,需要先解锁、再发命令序列、然后才能写入数据。
注意:NOR Flash的“写”和“擦除”是两个不同的操作。写入只能把1变成0,擦除才能把0变回1。所以每次写入新数据之前,必须先擦除对应扇区。
2.2 JZ2440上NOR Flash的硬件连接
JZ2440的NOR Flash挂在Bank0上,也就是片选信号nGCS0对应的地址空间。S3C2440这颗CPU的Bank0比较特殊,它是启动Bank,上电后CPU会从Bank0的0地址开始取第一条指令。如果从NOR Flash启动,那么NOR Flash的基地址就是0x00000000;如果从NAND Flash启动,NOR Flash会被映射到0x40000000。
硬件连接上,NOR Flash的地址线A0~A20接到CPU的地址总线,数据线D0~D15接到CPU的数据总线,片选nGCS0接到NOR Flash的nCE,读信号nOE和写信号nWE分别接到NOR Flash的nOE和nWE。这些连接方式决定了我们在写代码时,访问NOR Flash就像访问内存一样,直接用指针操作即可。
这里有个细节值得注意:JZ2440的NOR Flash是16位宽的,也就是说每次读写操作是2个字节。这意味着地址线的最低位A0在16位模式下实际上对应的是字节偏移的bit1。很多新手在计算地址偏移时容易搞错,把字节地址直接当成16位字的地址用,结果读写的数据总是错位。
2.3 为什么不用NAND Flash替代NOR Flash
有人可能会问,NAND Flash容量更大、价格更便宜,为什么还要用NOR Flash?答案就在“启动”这两个字上。NAND Flash不支持随机访问,CPU没法直接从NAND Flash取指令执行。S3C2440虽然支持NAND启动,但它的做法是先把NAND Flash前4KB的内容复制到片内SRAM(Steppingstone)里,然后从SRAM执行。这4KB的代码必须负责把整个程序搬到SDRAM里,过程比较复杂。
而NOR Flash可以直接在片内执行代码(XIP,eXecute In Place),启动流程简单直接。对于裸板开发来说,用NOR Flash启动能省去很多搬运代码的麻烦,调试起来也更直观。当然,代价就是容量小、成本高。所以实际产品中常见的是NOR Flash存启动代码、NAND Flash存大容量数据,两者配合使用。
3. NOR Flash控制器配置与底层操作原理
3.1 S3C2440的Bank0时序配置
虽然NOR Flash的访问看起来像内存,但CPU和Flash之间的时序并不是天生就匹配的。S3C2440的每个Bank都有对应的BWSCON(总线宽度和等待控制寄存器)和BANKCON(Bank控制寄存器),需要根据NOR Flash的时序参数来配置。
BWSCON中Bank0对应的位是[2:0],需要设置数据宽度为16位。BANKCON0中需要设置Tacs(片选建立时间)、Tcos(片选到读/写建立时间)、Tacc(访问周期)、Tcoh(片选保持时间)、Tcah(地址保持时间)等参数。这些参数的单位是HCLK周期,需要查NOR Flash的数据手册来确定。
以SST39VF1601为例,它的读取访问时间通常是70ns或90ns。假设HCLK是100MHz,一个周期是10ns,那么Tacc至少需要设置为7个周期才能满足70ns的访问时间。实际配置时通常会留一些余量,设置成8或9个周期。如果Tacc设置得太小,读出来的数据会不稳定;设置得太大,虽然能正常工作,但会降低访问速度。
实操心得:调试NOR Flash时,如果读出来的数据总是随机错误,优先检查BANKCON0的Tacc参数。我遇到过好几次因为Tacc设小了导致读取数据偶尔出错的情况,用示波器看时序才发现访问周期不够。
3.2 NOR Flash的命令序列机制
NOR Flash的读操作很简单,直接读地址就行。但写操作和擦除操作需要按照特定的命令序列来执行。以SST39VF1601为例,它的命令序列是这样的:
- 写入0xAA到地址0x5555
- 写入0x55到地址0x2AAA
- 写入0xA0到地址0x5555
- 写入数据到目标地址
这套命令序列看起来很奇怪,为什么要往特定地址写特定数据?这其实是NOR Flash内部的状态机设计。Flash内部有一个命令寄存器,只有按照特定顺序写入特定数据,才能让状态机进入编程模式。这种设计是为了防止误操作,毕竟Flash的擦写次数有限,不能随便乱写。
擦除操作也是类似的命令序列,只是最后一步写入的是0x80表示擦除,然后再发一次命令序列加上0x10表示擦除整个芯片,或者加上扇区地址表示擦除特定扇区。
3.3 扇区擦除与整片擦除的取舍
NOR Flash的擦除操作有两种:扇区擦除和整片擦除。扇区擦除一次擦除一个扇区,SST39VF1601的扇区大小通常是2KB或4KB;整片擦除则是一次把整个芯片擦干净。
选择哪种擦除方式取决于你的应用场景。如果只是更新一小部分数据,用扇区擦除更合适,因为擦除次数少、速度快。如果需要重新烧录整个程序,整片擦除更省事。但要注意,整片擦除的时间比较长,通常需要几百毫秒甚至几秒,期间CPU不能访问NOR Flash,否则会读到无效数据。
注意:擦除操作执行期间,NOR Flash会进入忙状态,此时读取任何地址都会返回状态寄存器的值,而不是实际数据。需要通过读取状态寄存器的DQ7位或DQ6位来判断擦除是否完成。
4. 裸板环境下NOR Flash读写擦除的完整实现
4.1 地址映射与指针操作
在裸板环境下,我们没有文件系统、没有驱动框架,所有操作都要通过指针直接访问物理地址。JZ2440从NOR Flash启动时,NOR Flash的基地址是0x00000000。我们可以定义一个宏来表示这个基地址:
#define NOR_FLASH_BASE 0x00000000然后通过volatile指针来读写:
typedef volatile unsigned short nor_data_t; nor_data_t *nor_addr = (nor_data_t *)NOR_FLASH_BASE;这里用unsigned short是因为NOR Flash是16位宽的,每次读写2个字节。用volatile关键字是为了防止编译器优化掉看似“无用”的读写操作,因为Flash的读写有副作用,不能被优化。
4.2 解锁与命令写入函数
根据前面讲的命令序列,我们可以封装几个基础函数:
#define NOR_CMD_ADDR1 0x5555 #define NOR_CMD_ADDR2 0x2AAA #define NOR_CMD_UNLOCK1 0xAA #define NOR_CMD_UNLOCK2 0x55 #define NOR_CMD_PROGRAM 0xA0 #define NOR_CMD_ERASE 0x80 #define NOR_CMD_CHIP_ERASE 0x10 #define NOR_CMD_SECTOR_ERASE 0x30 void nor_write_cmd(unsigned int addr, unsigned short cmd) { *(volatile unsigned short *)(NOR_FLASH_BASE + (addr << 1)) = cmd; }注意这里的addr << 1,因为NOR Flash是16位宽,地址线的最低位对应字节偏移的bit1。比如要访问字节地址0x5555,实际上要往16位地址0x2AAA写入数据。这个移位操作是很多新手容易忽略的地方。
4.3 扇区擦除的完整流程
扇区擦除的流程可以拆解为以下步骤:
- 发送解锁命令序列
- 发送擦除命令0x80
- 再次发送解锁命令序列
- 发送扇区擦除命令0x30和目标扇区地址
- 轮询状态寄存器等待擦除完成
对应的代码实现:
int nor_erase_sector(unsigned int sector_addr) { nor_write_cmd(NOR_CMD_ADDR1, NOR_CMD_UNLOCK1); nor_write_cmd(NOR_CMD_ADDR2, NOR_CMD_UNLOCK2); nor_write_cmd(NOR_CMD_ADDR1, NOR_CMD_ERASE); nor_write_cmd(NOR_CMD_ADDR1, NOR_CMD_UNLOCK1); nor_write_cmd(NOR_CMD_ADDR2, NOR_CMD_UNLOCK2); nor_write_cmd(sector_addr, NOR_CMD_SECTOR_ERASE); return nor_wait_ready(); }nor_wait_ready函数通过读取状态寄存器的DQ7位来判断操作是否完成。DQ7位在擦除期间会翻转,擦除完成后会稳定为1。也可以通过DQ6位判断,DQ6在操作期间会不断翻转,完成后停止翻转。
4.4 数据写入与校验
写入数据之前必须先擦除对应扇区,因为写入只能把1变成0,如果目标地址已经有数据(0),写入新数据时那些位会保持0,导致写入结果错误。
写入流程:
- 发送解锁命令序列
- 发送编程命令0xA0
- 写入目标地址和数据
- 轮询状态寄存器等待写入完成
int nor_program_word(unsigned int addr, unsigned short data) { nor_write_cmd(NOR_CMD_ADDR1, NOR_CMD_UNLOCK1); nor_write_cmd(NOR_CMD_ADDR2, NOR_CMD_UNLOCK2); nor_write_cmd(NOR_CMD_ADDR1, NOR_CMD_PROGRAM); *(volatile unsigned short *)(NOR_FLASH_BASE + (addr << 1)) = data; return nor_wait_ready(); }写入完成后,建议读回数据做校验。虽然Flash内部有校验机制,但在裸板环境下,读回校验是最直接的验证手段。
4.5 读取操作的实现与优化
读取操作最简单,直接读指针即可:
unsigned short nor_read_word(unsigned int addr) { return *(volatile unsigned short *)(NOR_FLASH_BASE + (addr << 1)); }但要注意,如果刚刚执行过擦除或写入操作,需要先等待操作完成再读取,否则读到的可能是状态寄存器的值而不是实际数据。
对于需要读取大量数据的场景,可以用循环批量读取,或者用DMA来搬运数据。不过NOR Flash的读取速度本身就不快,DMA的收益有限,通常直接循环读取就够了。
5. 调试NOR Flash时的常见问题与排查技巧
5.1 读取数据全为0xFF或随机值
这是最常见的问题。如果读出来的数据全是0xFF,说明NOR Flash可能没有被正确擦除,或者地址映射有问题。如果读出来是随机值,多半是时序配置不对。
排查步骤:
- 检查BWSCON和BANKCON0的配置是否正确
- 用示波器测量nGCS0、nOE、地址线和数据线的波形
- 确认NOR Flash的基地址是否正确(从NOR启动是0x00000000,从NAND启动是0x40000000)
- 检查地址移位是否正确(16位模式下要左移1位)
5.2 擦除操作一直不完成
如果nor_wait_ready函数一直返回忙状态,可能是以下原因:
- 命令序列写错了,Flash没有进入擦除模式
- 地址移位错误,命令写到了错误的位置
- NOR Flash的写保护引脚被拉低,禁止了擦写操作
- 硬件连接有问题,写信号没有正确到达Flash
实操心得:我遇到过因为写保护引脚悬空导致擦除一直失败的情况。JZ2440的NOR Flash有一个WP引脚,如果悬空或者被拉低,擦写操作会被禁止。检查硬件原理图确认这个引脚的状态。
5.3 写入数据后读回不一致
写入数据后读回不一致,通常是因为:
- 目标扇区没有先擦除
- 写入过程中被打断(比如中断触发)
- 时序参数不满足Flash的写入要求
- 数据宽度设置错误,比如把16位数据当成8位写
排查时可以先擦除整个扇区,然后写入一个已知数据,再读回对比。如果单个数据能正确写入,说明基本流程没问题,问题可能出在批量写入时的时序或中断处理上。
5.4 NOR Flash与NAND Flash启动的切换
JZ2440可以通过拨码开关选择从NOR Flash还是NAND Flash启动。如果拨码开关设置错误,会出现程序跑不起来或者跑起来但行为异常的情况。
从NOR Flash启动时,CPU直接从0x00000000取指令,NOR Flash的基地址就是0x00000000。从NAND Flash启动时,CPU先把NAND前4KB搬到片内SRAM,NOR Flash被映射到0x40000000。如果你的程序里硬编码了NOR Flash的地址,切换启动方式后地址就错了。
注意:在写代码时,尽量用宏定义来表示NOR Flash的基地址,不要硬编码。这样切换启动方式时只需要改一个宏定义。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读取全为0xFF | 未擦除、地址错误、片选未使能 | 检查BANKCON0、测量nGCS0波形 |
| 读取随机值 | 时序配置错误 | 增大Tacc参数、示波器看时序 |
| 擦除不完成 | 命令序列错误、写保护 | 检查命令序列、检查WP引脚 |
| 写入后读回不一致 | 未擦除、中断打断、时序不足 | 先擦除再写、关中断、增大时序余量 |
| 程序跑飞 | 启动方式错误、地址映射错误 | 检查拨码开关、确认基地址 |
6. 从NOR Flash开发延伸出的存储器件学习思路
搞完NOR Flash的裸板开发之后,我对存储器件的学习有了一个比较清晰的套路。不管是NOR Flash、NAND Flash、EEPROM还是SD卡,本质上都是“CPU通过某种接口跟存储介质对话”。区别只在于接口协议、命令格式和操作时序不同。
以NOR Flash为例,它的核心知识点可以归纳为三条:地址映射方式、命令序列机制、状态轮询方法。掌握了这三条,换一块NOR Flash芯片,只需要查数据手册确认命令序列和时序参数,代码框架基本不用大改。
再往深了说,NOR Flash的XIP特性让它成为启动代码的理想载体,但它的擦写寿命有限(通常10万次左右),所以不能频繁写入。如果需要频繁记录数据,应该用EEPROM或者带磨损均衡算法的NAND Flash。这些选型上的考量,在裸板开发阶段可能感受不深,但到了实际产品设计时就会变得非常重要。
我个人在实际操作中的体会是,裸板开发NOR Flash最大的价值不在于学会操作这一块芯片,而在于通过它理解CPU如何与外部存储器件交互这个通用问题。你把NOR Flash的时序配置、命令序列、状态轮询这三块搞透了,后面学NAND Flash、SD卡、甚至PCIe设备,都会觉得有章可循。最后再分享一个小技巧:调试Flash操作时,先把擦除、写入、读取三个操作分别单独测试通过,再组合起来跑完整流程。这样出问题时能快速定位是哪个环节的毛病,比一上来就跑完整流程然后抓瞎要高效得多。