《ARM Cortex-M启动代码阅读与最小系统运行实战》

2、ARM Cortex-M处理器基础:内核架构、寄存器组、工作模式与异常中断

启动代码(Startup Code)是嵌入式系统上电后执行的第一段程序,其核心任务包括设置栈指针、初始化中断向量表、拷贝数据段、清零BSS段,并最终跳转到C语言的main函数。要真正读懂并编写启动代码,必须首先理解Cortex-M处理器的内核架构。本章将围绕Cortex-M3/M4的内核结构、寄存器组、工作模式、特权级与用户级,以及异常与中断的基本概念展开,为后续阅读启动代码和实现最小系统运行打下坚实的理论基础。

Cortex-M3/M4 内核架构总览 处理器核心 • 寄存器组 (R0-R15, xPSR) • 内核外设 (SysTick, NVIC) • 调试单元 (DWT, ITM, FPB) • 总线接口 (I-Bus, D-Bus, S-Bus) • 工作模式: Thread / Handler • 特权级 / 用户级 存储器映射 • Code区 (0x00000000-0x1FFFFFFF) • SRAM区 (0x20000000-0x3FFFFFFF) • 外设区 (0x40000000-0x5FFFFFFF) • 私有外设区 (0xE0000000-0xFFFFFFFF) • 位带操作区 (0x22000000) • 向量表位于Flash起始地址 NVIC 嵌套向量中断控制器 • 支持1-240个外部中断 • 可编程优先级 (0-255) • 尾链 (Tail-Chaining) 优化 • 晚到 (Late-Arriving) 优化 • 中断向量表查找 • SysTick 异常 (编号15) 异常与中断模型 编号1: Reset 编号2: NMI 编号3: HardFault 编号4: MemManage 编号5: BusFault 编号6: UsageFault 编号11: SVCall 编号14: PendSV 编号15: SysTick 编号16+: 外部中断 (IRQ0-239) 启动代码执行流程 1. 从向量表取初始SP 2. 从向量表取Reset_Handler 3. 初始化时钟/存储器 4. 拷贝数据段/清零BSS 5. 调用SystemInit() 6. 跳转main() 7. 进入无限循环

2.1 内核架构:从冯·诺依曼到哈佛的演进

Cortex-M3/M4 处理器采用哈佛架构,指令总线(I-Bus)和数据总线(D-Bus)分离,这使得处理器可以在同一个时钟周期内同时取指和访问数据,显著提升了代码执行效率。与经典的冯·诺依曼架构相比,这种分离的总线设计减少了指令预取与数据访问之间的冲突。在启动代码中,向量表通常存放在Flash(Code区)的起始地址,而栈指针(SP)的初始值则从向量表的第一个字加载,这正是哈佛架构下存储器映射的直接体现。

从系统层面看,Cortex-M3/M4 的存储器映射被划分为几个固定的区域。Code区(0x00000000-0x1FFFFFFF)存放程序代码和只读数据,SRAM区(0x20000000-0x3FFFFFFF)存放可读写的变量和栈,外设区(0x40000000-0x5FFFFFFF)映射所有片上外设寄存器,而私有外设区(0xE0000000-0xFFFFFFFF)则包含NVIC、SysTick、调试组件等内核级外设。启动代码中常见的__initial_sp符号,实际上就是链接脚本将栈顶地址定位到SRAM区末尾的结果。

核心原理:Cortex-M3/M4 的存储器映射是固定的,这不同于传统ARM7的Banked寄存器方式。固定映射意味着无论使用哪家厂商的芯片,NVIC、SysTick等内核外设的地址都是统一的,这大大提高了软件的可移植性。启动代码中链接脚本的MEMORY命令正是基于这一固定映射来分配Flash和RAM的地址范围。

2.2 寄存器组:通用寄存器与特殊寄存器

Cortex-M3/M4 拥有16个32位通用寄存器(R0-R12为通用,R13为栈指针SP,R14为链接寄存器LR,R15为程序计数器PC),外加一个程序状态寄存器xPSR。与ARM7TDMI相比,Cortex-M3 的寄存器组没有Banked寄存器,而是通过硬件压栈(PUSH)和出栈(POP)来自动保存和恢复上下文,这极大简化了中断处理流程。

寄存器别名功能说明启动代码中的典型用途
R0-R12通用寄存器数据操作、地址计算传递参数、临时变量
R13SP (MSP/PSP)栈指针,MSP用于Handler模式,PSP用于Thread模式初始化栈顶地址
R14LR链接寄存器,保存函数返回地址跳转main前保存返回地址
R15PC程序计数器,指向当前指令地址复位后从向量表加载
xPSR程序状态字包含条件标志、中断号、执行状态异常返回时恢复

在启动代码中,Reset_Handler 的第一条指令通常是 LDR SP, =__initial_sp,这直接将栈顶地址加载到SP寄存器。随后通过 BL main 指令跳转到C语言的入口点,此时LR寄存器会自动保存返回地址。如果main函数意外返回,LR中的值将导致处理器进入HardFault异常,因此启动代码通常在main后放置一个无限循环 B . 来防止这种情况。

实战经验:在调试启动代码时,可以通过IDE的寄存器窗口观察SP和PC的变化。复位后,PC应等于向量表中第二个字(Reset_Handler的地址),SP应等于向量表中第一个字(栈顶地址)。如果这两个值不正确,说明向量表配置或链接脚本有误。

2.3 工作模式与特权级:Thread模式与Handler模式

Cortex-M3/M4 支持两种工作模式:Thread模式(线程模式)和Handler模式(处理模式)。Thread模式用于执行普通应用程序代码,而Handler模式用于执行异常或中断处理程序。复位后,处理器默认进入Thread模式,并以特权级运行。通过修改CONTROL寄存器的nPRIV位,可以将Thread模式切换为用户级,从而限制对某些系统资源的访问。

特权级与用户级的区别在于:特权级可以访问所有系统资源,包括修改NVIC配置、执行CPS指令(如关中断的CPSID i)等;而用户级则禁止访问这些关键资源,任何非法访问都会触发UsageFault异常。在启动代码中,通常保持特权级运行,直到RTOS初始化完成后再切换到用户级。对于裸机系统,一般不需要切换,但理解这一机制有助于排查系统级故障。

风险提示:如果在用户级下尝试执行 MRS 读取CONTROL寄存器或修改NVIC,将触发UsageFault。在启动代码中,如果需要在main之前配置中断,务必确认当前处于特权级。否则,中断配置将静默失败,导致系统无法响应中断。

2.4 异常与中断:向量表与优先级机制

异常(Exception)是处理器内部或外部事件触发的特殊事件,中断(Interrupt)是异常的一种,特指来自外部设备或片上外设的请求。Cortex-M3/M4 的异常系统由NVIC统一管理,支持嵌套中断、尾链优化和晚到优化。向量表是异常处理的核心数据结构,它存储了每个异常处理函数的入口地址。向量表的起始地址由VTOR寄存器决定,复位后默认指向0x00000000。

向量表的布局是启动代码的关键部分。第一个字(偏移0x00)存放初始栈指针,第二个字(偏移0x04)存放Reset_Handler的地址,后续每个异常占用4字节。对于Cortex-M3/M4,编号1-15为系统异常,编号16及以上为外部中断。启动代码中的 __Vectors 段正是按照这一布局定义的。

__Vectors:
    .word __initial_sp          ; 0x00: 栈顶地址
    .word Reset_Handler         ; 0x04: 复位处理
    .word NMI_Handler           ; 0x08: NMI
    .word HardFault_Handler     ; 0x0C: 硬错误
    .word MemManage_Handler     ; 0x10: 存储管理
    .word BusFault_Handler      ; 0x14: 总线错误
    .word UsageFault_Handler    ; 0x18: 用法错误
    .word 0, 0, 0, 0            ; 保留
    .word SVC_Handler           ; 0x2C: 系统服务调用
    .word DebugMon_Handler      ; 0x30: 调试监视
    .word 0                     ; 保留
    .word PendSV_Handler        ; 0x38: 可挂起系统服务
    .word SysTick_Handler       ; 0x3C: 系统节拍定时器
    ; 外部中断从0x40开始

优先级机制方面,Cortex-M3/M4 支持可编程优先级,优先级数值越小,优先级越高。NVIC支持3位或4位优先级位宽(由芯片厂商决定),通常分为抢占优先级和子优先级。在启动代码中,通过 NVIC_SetPriority 函数或直接操作NVIC_IPR寄存器来配置中断优先级。对于系统异常,可以通过SHPR寄存器设置优先级。

案例拆解:假设一个系统使用Cortex-M4内核,外设UART中断(IRQ编号为20)和定时器中断(IRQ编号为24)。如果UART中断的抢占优先级设为1,定时器中断的抢占优先级设为2,那么当定时器中断正在执行时,UART中断可以抢占它。但如果两者优先级相同,则按中断编号小的先执行。启动代码中,需要在main函数之前调用 NVIC_EnableIRQ(UART_IRQn) 和 NVIC_EnableIRQ(TIMER_IRQn) 来使能这些中断。

2.5 常见误区与总结复盘

在学习和调试Cortex-M启动代码时,初学者常犯以下错误:一是混淆MSP和PSP的使用场景,导致栈指针错误;二是忽略向量表对齐要求(Cortex-M3/M4要求向量表按2的整数次幂对齐,通常为256字节或更大);三是在用户级下尝试配置中断,导致配置无效;四是错误理解优先级数值与优先级高低的关系。

总结本章要点:Cortex-M3/M4 的哈佛架构和固定存储器映射为启动代码提供了清晰的地址空间;寄存器组中的SP、LR、PC在启动流程中扮演关键角色;Thread/Handler模式与特权级/用户级的组合决定了系统资源的访问权限;异常与中断的向量表和优先级机制是启动代码配置的核心。掌握这些基础概念后,下一章将深入分析启动代码的每一行汇编指令,并搭建最小系统运行环境。

复盘建议:在阅读启动代码时,建议对照芯片参考手册的向量表章节,逐一核对每个异常处理函数的地址。同时,使用调试器的存储器窗口查看向量表内容,确认第一个字为栈顶地址,第二个字为Reset_Handler地址。这种"代码-手册-硬件"三方对照的方法,是快速掌握启动代码的有效途径。