1. 从零开始理解ARM芯片启动的基石很多刚接触嵌入式开发的朋友一上来就被“启动过程”这几个字给唬住了。看着芯片手册里复杂的启动模式配置、各种存储器的映射关系感觉头大如斗。其实这事儿说复杂也复杂说简单也简单。它的核心本质上就是回答一个问题当一块ARM芯片上电复位它的第一条指令从哪里来又该如何执行今天我就以经典的S3C2440这颗芯片为例掰开了揉碎了带你走一遍这个从“一片空白”到“程序跑起来”的完整旅程。无论你是刚入行的菜鸟还是想巩固基础的老鸟这篇文章都会让你对ARM启动有一个通透的理解。要搞懂启动你脑子里必须先有两张清晰的地图一张是存储器的地图另一张是程序的地图。存储器地图告诉你芯片能“看到”哪些内存空间比如内部的SRAM、外部的NOR Flash、DRAM以及它们的地址在哪里。程序地图则告诉你你写的代码、数据、堆栈最终被编译器、链接器放到了存储器的哪个位置。启动过程就是芯片拿着第一张地图去找到并执行第二张地图里最开始的那些指令。所以如果你对“ROM、RAM、NOR、NAND、DRAM、SRAM”这些概念还模糊或者对“链接脚本”、“入口地址”、“加载地址与运行地址”感到陌生我强烈建议你先补补课。磨刀不误砍柴工这些基础概念清晰了后面的内容你理解起来会事半功倍。S3C2440作为一款经典的ARM9芯片它的启动设计非常具有代表性。它支持两种主要的启动模式由芯片外部两个叫做OM[1:0]的引脚电平状态决定。简单来说就是告诉芯片“老大上电后请去A地点比如NAND Flash或者B地点比如NOR Flash找你的第一条指令。” 这个简单的选择背后却对应着两套完全不同的启动流程和软件设计思路。接下来我们就分别深入这两种模式看看芯片到底是怎么“活”过来的。2. 启动模式的核心OM引脚与存储器映射解析2.1 启动模式的选择开关OM[1:0]引脚S3C2440芯片上电后干的第一件“有意识”的事就是去采样两个特定的GPIO引脚通常是OM1和OM0的电平状态。这个采样发生在复位信号释放的瞬间其状态被锁存到芯片内部决定了整个系统的启动源头。你可以把它想象成电脑的BIOS设置选择从硬盘启动还是从U盘启动。对于S3C2440常见的配置如下OM[1:0] 00设置为从NAND Flash启动。OM[1:0] 01 或 10设置为从16位或32位宽度的NOR Flash启动具体宽度由OM引脚组合决定。OM[1:0] 11设置为从其他总线设备启动如ROM较少使用。这个硬件配置是启动流程的“总纲”软件设计必须严格遵守这个硬件设定。你不可能在硬件配置为NOR启动的情况下写一个只适用于NAND启动的代码那样芯片一上电就会“迷路”。2.2 上电瞬间的地址空间“魔术”芯片内部有一个非常关键的部件叫做内存控制器或者总线控制器。在复位之后、执行第一条指令之前它会根据OM引脚的状态玩一个“地址重映射”的魔术。在ARM体系结构中复位后的异常向量表尤其是复位向量即第一条指令地址固定位于地址0x00000000。也就是说CPU上电后会无条件地跳到0x00000000这个地址去取指令。问题来了这个地址上到底连着什么东西是空是内部SRAM还是外部的Flash这就是内存控制器的工作了当设置为NAND启动时内存控制器会把芯片内部一块4KB大小的静态RAMSteppingstone SRAM映射到地址0x00000000开始的空间。同时它会自动触发一个硬件行为将连接在NAND Flash控制器上的NAND Flash芯片的前4KB数据搬运到这片内部SRAM中。完成之后CPU才开始从0x00000000此时已经是SRAM的内容取指执行。当设置为NOR启动时内存控制器不做这个特殊的重映射。此时地址0x00000000直接对应到外部总线上的Bank0区域nGCS0片选信号选中的设备。通常我们会在这个Bank0上连接NOR Flash芯片。因此CPU直接从NOR Flash的起始物理地址取指。注意这个“映射”是硬件完成的对软件透明。在NAND启动模式下虽然CPU访问的是0x0但实际上它访问的是内部SRAM而不是真的有一条总线通向NAND Flash。理解这一点对后续理解代码的“位置无关”特性至关重要。2.3 两种存储器的本质区别XIP与非XIP为什么启动模式要分NAND和NOR根源在于这两种存储器的访问特性截然不同。NOR Flash支持XIP。XIP是“eXecute In Place”的缩写意思是“芯片内执行”。NOR Flash拥有独立的地址线和数据线可以像RAM一样被CPU直接寻址和随机读取。CPU发一个地址到总线上NOR Flash就能在几个时钟周期后返回该地址的数据指令。因此代码可以直接在NOR Flash中运行无需先拷贝到RAM。NAND Flash不支持XIP。它接口复杂更像硬盘通过命令、地址、数据复用的IO口以“页”为单位进行读写操作。CPU无法直接发一个地址就去NAND里取一条指令。必须通过专门的NAND Flash控制器发送一系列命令才能读出一页数据。因此代码不能直接在NAND中运行。正是这个根本区别导致了两种启动模式流程的巨大差异。NOR启动“省事”但成本高、速度慢NAND启动需要“搬运”但成本低、容量大。下面我们就分别看看这两种模式下软件工程师需要如何配合硬件完成启动。3. NAND Flash启动模式深度剖析3.1 “搬运工”角色Steppingstone SRAM与自动拷贝当OM引脚设置为NAND启动后上电复位流程是这样的硬件复位释放。内存控制器将内部4KB SRAM映射到0x00000000。NAND Flash控制器自动读取NAND Flash第0块Block 0的前4KB数据通常是第0页到第15页假设页大小为256B或512B通过硬件逻辑将其直接、无条件地搬运到内部4KB SRAM中。CPU从0x00000000开始执行指令此时执行的就是刚刚被搬运过来的那4KB代码。这4KB SRAM被称为“垫脚石”Steppingstone。它的作用就是充当一个临时的、快速的执行场地让我们有机会运行一段初始化代码为后续更大程序的执行搭建舞台。实操心得这4KB代码是硬件自动拷贝的你无法通过软件干预其来源固定是NAND前4KB。因此你必须确保你编译生成的可执行文件bin文件的前4KB代码就是完整的、可自举的启动代码。链接脚本的编写要格外小心必须把最开始的汇编启动代码、向量表等绝对定位在这4KB范围内。3.2 4KB限制下的启动代码设计4KB的空间非常有限大约只能容纳1000条左右的ARM指令。在这狭小的空间里我们的启动代码通常称为Bootloader的第一阶段如u-boot的start.S必须完成以下几项最核心的、无法推迟的任务设置异常向量表ARM CPU在复位后处于管理模式但中断、未定义指令等异常向量入口也必须预先设置好。通常向量表就是几条跳转指令占用空间很小。关闭看门狗S3C2440内部看门狗默认是开启的如果不及时关闭几秒钟后系统就会被复位。关闭中断在初始化完成前避免不可预知的中断发生。初始化系统时钟设置PLL将低频的晶振时钟倍频到CPU、总线、外设需要的工作频率。没有正确的时钟后续所有操作时序都会错乱。初始化内存控制器这是最关键的一步。我们需要配置S3C2440的内存控制器一组寄存器告诉它外部接了多大容量、什么型号、什么时序的SDRAMDRAM。只有正确初始化后CPU才能正常访问外部的大容量SDRAM。设置栈指针为当前模式通常是SVC模式设置栈指针SP。栈是函数调用、局部变量、中断上下文保存的基础没有栈就无法使用C语言。代码重定位将存储在NAND Flash中、4KB之后的主体程序可能是Bootloader的第二阶段也可能是整个操作系统内核拷贝到已经初始化好的SDRAM中。因为NAND中的代码无法直接运行必须搬到RAM里。清空BSS段将BSS未初始化的全局变量段所在内存区域清零。跳转到SDRAM最后通过一条绝对跳转指令如ldr pc, main将程序计数器PC指向SDRAM中代码的起始地址从此开始在广阔的内存中驰骋。所有这些代码必须精炼再精炼确保其二进制大小严格控制在4KB以内。通常会用纯汇编语言来编写以追求极致的效率和空间控制。3.3 从SRAM到DRAM的惊险一跃在4KB代码里初始化SDRAM是整个流程中最具挑战性的一环。因为初始化SDRAM的代码本身正在SRAM中运行而它要初始化的目标——SDRAM——却还不可用。这就像一个建筑师站在一块小木板上要去建造他脚下这座大桥的桥墩。这里有一个至关重要的细节S3C2440的内存控制器寄存器本身是挂在CPU内部总线上的访问它们并不需要依赖外部SDRAM是否初始化好。我们通过配置这些寄存器如BWSCON, BANKCON6/7, REFRESH, BANKSIZE, MRSR等来设定SDRAM的位宽、行列地址、刷新周期、时序参数tRCD, tRP, CL等。配置完成后需要向SDRAM发送一个预充电和模式寄存器设置MRS命令序列来激活它。这个命令序列是通过向SDRAM的特定地址进行“伪写”操作来触发的。一旦初始化成功对应Bank的地址空间如S3C2440的Bank6起始地址0x30000000就可以正常读写了。避坑技巧SDRAM初始化失败是新手最常遇到的问题。排查思路如下检查硬件连接数据线、地址线、时钟、片选、电源是否接好。用示波器看时钟和信号质量。核对芯片手册确保配置的时序参数在寄存器中设置完全符合你所用的SDRAM芯片手册要求特别是刷新率Refresh和CAS延迟CL。简化测试先写一个最简单的内存测试函数在初始化后立刻向SDRAM的起始地址写一个已知值如0x12345678然后读回来比较。如果失败可以单步调试汇编检查每一步配置寄存器的值是否正确。注意启动时的时钟初始化SDRAM前必须确保系统时钟FCLK, HCLK, PCLK已经设置到稳定、正确的频率。SDRAM的时序是基于HCLK计算的。成功初始化SDRAM并完成代码搬运后跳转到SDRAM中执行NAND启动最艰难的部分就过去了。从此程序可以在大容量的内存中自由运行包括使用C语言、设置更复杂的数据结构、加载操作系统等。4. NOR Flash启动模式详解4.1 直接执行的便利与限制当OM引脚设置为NOR启动时事情看起来简单多了。CPU直接从0x00000000取指而这个地址就是NOR Flash的物理起始地址。由于NOR支持XIP代码可以原地执行没有4KB的大小限制。理论上只要NOR Flash容量足够S3C2440支持到128Mb你的整个Bootloader甚至小型操作系统都可以直接放在NOR里运行。但是NOR Flash有一个致命缺点写操作非常慢且通常不能按字节随机写入。它需要先擦除Erase通常以扇区为单位耗时几十到几百毫秒再编程Program速度远慢于RAM。这意味着无法在NOR中运行需要修改变量的C程序C语言中的全局变量、静态变量需要被初始化或修改。如果这些变量所在的段如.data段被链接到NOR地址那么每次修改变量都是一次极其缓慢的Flash写操作程序会慢得无法忍受并且频繁擦写会很快损坏NOR Flash。栈空间不能设在NOR中函数调用时的局部变量、返回地址都保存在栈里栈需要频繁的读写。放在NOR里同样不现实。因此在NOR Flash中直接运行的代码必须是一段“纯代码”它只能读取NOR Flash中的内容不能向NOR Flash的地址空间进行写操作。4.2 NOR启动代码的典型流程那么NOR启动的代码该怎么写呢它的第一阶段任务和NAND启动类似但侧重点不同硬件初始化同样需要设置异常向量、关闭看门狗和中断、初始化系统时钟。这部分代码在NOR中运行没问题因为它们只读取指令和读取/写入寄存器寄存器是映射在内存空间的另一块区域不是NOR。初始化内存控制器同样必须初始化外部的SDRAM。因为我们需要一块可读写的RAM来放置变量和栈。设置栈指针将栈指针SP指向SDRAM中的某个地址。从此函数调用、局部变量都发生在高速的SDRAM中。数据段重定位这是NOR启动的关键一步。在链接脚本中我们通常将代码段.text链接到NOR的地址如0x00000000而将需要读写的.data段已初始化全局变量和.bss段链接到SDRAM的地址如0x30000000。但是上电后这些变量的初始值还存储在NOR Flash的某个位置比如紧接着.text段之后。我们需要编写一段代码将这些初始值从NOR中拷贝到SDRAM中.data段的运行时地址并将.bss段清零。这个过程叫做“重定位”。跳转到C入口完成上述步骤后内存SDRAM已经可用栈也已就绪数据变量也搬到了正确的位置。此时就可以安全地跳转到用C语言写的main函数了。可以看出NOR启动的“搬运”工作比NAND启动要轻量。它只需要搬运数据段通常很小而不需要搬运整个代码段。代码段始终在NOR中执行。4.3 NOR启动的优缺点与适用场景优点开发调试方便由于代码在NOR中可直接运行我们可以通过JTAG仿真器直接将程序下载到NOR Flash然后立即单步调试无需经历复杂的“搬运-跳转”过程非常适合前期硬件调试和Bootloader开发。可靠性高代码在非易失的NOR中即使SDRAM初始化失败只要NOR里的代码没问题CPU依然可以执行便于输出错误信息或进入恢复模式。无大小限制启动代码可以做得很大集成更多功能。缺点成本高NOR Flash单位容量价格远高于NAND Flash。执行速度慢NOR Flash的读取速度比SDRAM慢导致代码执行效率较低。通常会在启动后期将关键的性能敏感代码如内核解压、图形界面初始化从NOR拷贝到SDRAM中再执行这就是“代码重定位Remap”。写入困难无法在NOR中更新自身系统升级通常需要另一套机制。适用场景对启动可靠性要求极高、需要方便调试、或代码量不大的工控、通信设备。在很多商业产品中为了成本考虑最终量产时会切换到NAND启动但开发阶段使用NOR启动进行调试。5. 链接脚本连接硬件与软件的桥梁无论是NAND还是NOR启动都离不开一个关键文件链接脚本Linker Script, 通常以.lds或.ld为后缀。这个文件告诉链接器程序的各个部分代码、只读数据、已初始化数据、未初始化数据应该放在存储器的什么地址。5.1 链接脚本的核心概念加载地址Load Address/LMA程序段Section在存储介质Flash无论是NAND还是NOR中存放的物理地址。运行地址Virtual Address/VMA程序段被加载到内存RAM后期望运行的地址。对于NAND启动第一阶段前4KBLMA和VMA必须都是0x00000000即内部SRAM的映射地址。因为硬件自动拷贝后代码就在那里运行。第二阶段后续代码LMA是它在NAND Flash中的存储地址如0x400VMA是SDRAM中的目标地址如0x30000000。启动代码需要自己完成从LMA到VMA的拷贝。对于NOR启动.text段代码LMA和VMA可以相同都是NOR的地址如0x00000000因为XIP。.data段数据LMA是它在NOR中的存储地址紧挨着.textVMA是SDRAM中的地址如0x30000000。需要启动代码拷贝。5.2 一个简化的NOR启动链接脚本示例SECTIONS { /* 指定程序入口点为 _start 符号 */ . 0x00000000; /* 从地址0开始即NOR起始地址 */ .text : { *(.text) /* 所有代码段放在这里 */ *(.rodata) /* 只读数据也放在这里因为NOR只读 */ } /* .data段的加载地址(LMA)紧跟在.text段后面但在NOR里 */ . ALIGN(4); _data_load_addr .; /* 记录.data在NOR中的加载地址 */ /* .data段的运行地址(VMA)在SDRAM中 */ .data 0x30000000 : AT ( _data_load_addr ) { _data_start .; *(.data) _data_end .; } /* .bss段同样在SDRAM中紧挨着.data段 */ .bss : { _bss_start .; *(.bss) *(COMMON) _bss_end .; } }在这个脚本中.data段使用了AT()关键字指定了它的加载地址在NOR中而它本身的地址0x30000000则是运行地址。启动汇编代码需要利用_data_load_addr,_data_start,_data_end这些链接器生成的符号来完成数据拷贝。5.3 启动汇编代码中的重定位操作在启动汇编文件如start.S中我们需要完成重定位。以下是一个简化的示例片段/* 假设链接器提供了这些符号 */ .extern _data_load_addr /* .data在Flash中的源地址 */ .extern _data_start /* .data在RAM中的目标起始地址 */ .extern _data_end /* .data在RAM中的目标结束地址 */ .extern _bss_start /* .bss起始地址 */ .extern _bss_end /* .bss结束地址 */ _start: /* ... 硬件初始化关看门狗、设时钟、初始化SDRAM等 ... */ /* 1. 将.data段从Flash拷贝到RAM */ ldr r0, _data_load_addr /* 源地址Flash中.data的位置 */ ldr r1, _data_start /* 目标地址RAM中.data的位置 */ ldr r2, _data_end cmp r1, r2 beq copy_done copy_loop: ldr r3, [r0], #4 str r3, [r1], #4 cmp r1, r2 blt copy_loop copy_done: /* 2. 清零.bss段 */ ldr r0, _bss_start ldr r1, _bss_end mov r2, #0 cmp r0, r1 beq bss_clear_done bss_clear_loop: str r2, [r0], #4 cmp r0, r1 blt bss_clear_loop bss_clear_done: /* 3. 设置栈指针准备跳入C世界 */ ldr sp, 0x34000000 /* 设置栈到SDRAM顶端 */ bl main /* 跳转到C语言的main函数 */这段汇编代码就是链接脚本意图的执行者它精确地按照链接脚本的规划在内存中搭建好了C语言运行所需的环境。6. 常见问题排查与实战经验分享6.1 启动失败问题速查表现象可能原因NAND启动可能原因NOR启动排查思路上电后毫无反应串口无输出1. 前4KB代码未正确烧录到NAND。2. 前4KB代码大小超过4KB。3. 硬件复位电路或电源异常。1. 代码未正确烧录到NOR。2. NOR Flash型号或数据位宽配置OM引脚错误。3. 硬件复位电路或电源异常。1. 用编程器确认Flash内容。2. 检查编译生成的bin文件大小和内容。3. 用示波器测复位引脚、电源、晶振。串口输出乱码或部分字符后停止1. 系统时钟PLL配置错误导致串口波特率不准。2. 栈指针设置错误导致函数调用或变量访问出错。3. 代码重定位后跳转地址错误。1. 系统时钟PLL配置错误。2. 栈指针指向了未初始化的RAM或非法地址。3. .data段重定位失败C函数访问变量时出错。1. 简化代码先只初始化时钟和串口打印固定字符串测试。2. 检查链接脚本和启动代码中的地址计算。3. 单步调试汇编观察跳转和内存访问。能打印信息但SDRAM测试失败1. SDRAM初始化序列或时序参数错误。2. SDRAM硬件连接问题线虚焊、短路。3. 电源纹波过大SDRAM工作不稳定。同左。1. 逐条核对SDRAM芯片手册与配置寄存器值。2. 用示波器测量SDRAM时钟、控制信号质量。3. 尝试降低系统频率或放宽时序参数测试。C语言函数一调用就死机1. 栈指针SP未设置或设置错误。2. 跳转到C函数前未完成.data和.bss的重定位。1. 栈指针SP指向了NOR Flash地址不可写。2. .data段未从NOR拷贝到RAM。1. 检查启动汇编中SP的设置值确保指向可读写的RAM高端地址。2. 检查重定位代码确认.data和.bss的起始、结束地址符号使用正确。代码在NOR中调试正常烧录到NAND不启动1. NAND启动的链接脚本错误第一阶段代码地址不是0x0。2. 从SRAM跳转到SDRAM的指令地址错误。3. 代码中使用了绝对地址访问而该地址在NAND模式下无效。此问题不适用于NOR启动1. 对比NOR和NAND的链接脚本确保NAND的第一阶段所有代码/数据都在前4KB。2. 检查跳转指令确认目标地址是SDRAM中的正确地址。6.2 调试技巧与心得“灯语”调试法在最初级的硬件调试阶段串口可能还没调通。可以编写一个最简单的汇编程序只操作GPIO点亮或熄灭一个LED。把这个程序分别编译成NOR启动和NAND启动的版本用编程器烧录。如果灯能按预期闪烁说明最底层的CPU、时钟、GPIO和启动流程是通的。这是硬件工程师的“Hello World”。分段测试步步为营不要试图一次性写完整个Bootloader。应该分阶段测试阶段一只初始化时钟和串口打印“123”。确保CPU和基本外设工作。阶段二加入SDRAM初始化代码然后进行简单的内存读写测试如写0xAA55AA55到首地址再读回。阶段三加入重定位代码和栈设置跳转到一个最简单的C函数只打印一句话。阶段四实现完整的NAND读写或NOR擦写功能。善用仿真器如果有JTAG仿真器如J-Link配合IDE如Keil MDK进行单步调试是无价之宝。你可以看到每一条汇编指令的执行效果查看任何寄存器和内存地址的值。这对于排查SDRAM初始化、重定位代码中的错误极其有效。理解“位置无关码PIC”在NAND启动的第一阶段4KB代码中由于后续要拷贝自身到SDRAM这段代码最好编译成位置无关码。这样无论它被硬件拷贝到SRAM的哪个位置实际就是0x0都能正确运行。这通常通过编译选项-fpic和-msingle-pic-base以及避免使用绝对地址长跳转来实现。关注编译工具链不同的交叉编译工具链如arm-linux-gnueabi- 与 arm-none-eabi-可能有不同的默认行为、库和链接脚本。确保你清楚你使用的工具链是针对裸机none还是带操作系统linux的它们的启动文件crt0.o和默认链接脚本可能不同。最稳妥的方式是自己编写链接脚本和启动文件。7. 从启动到系统Bootloader的使命理解了芯片的启动过程其实就理解了Bootloader最核心、最基础的部分。一个完整的Bootloader如U-Boot所做的工作就是在芯片上电完成“自救”之后继续完成更高级的初始化并为加载最终的操作系统内核做好准备。它会初始化更多硬件如网卡、LCD、USB、MMC/SD卡等。建立完整的内存映射。加载操作系统内核从存储设备NAND, NOR, SD卡网络将内核映像读到SDRAM的指定地址。设置启动参数准备一个叫做“ATAGs”或“Device Tree Blob”的数据结构告诉内核内存大小、命令行参数等信息。跳转到内核通常通过theKernel(0, machine_id, atags_pointer)这样的函数调用将CPU控制权彻底交给操作系统。整个启动链条就是这样一环扣一环硬件复位 - OM引脚选择启动源 - 硬件自动初始化/映射 - 第一阶段汇编代码初始化关键硬件、内存、重定位 - 第二阶段C代码初始化复杂外设、加载内核 - 操作系统内核。每一环的失误都会导致启动失败。而最底层、最让人困惑的往往就是芯片上电后到C语言环境运行起来之前的这一段“黑暗时光”。希望这篇以S3C2440为例的详细解析能为你点亮这段路程的灯。