第2章:芯片架构与启动流程

各位同学,咱们今天聊点硬核的。ARM Cortex-M、R、A这三个系列,我这些年几乎都摸过。说实话,刚开始接触时我也觉得它们差不多,都是ARM嘛。但真正做RTOS移植时才发现——嗯,差别大了去了。

2.1 ARM Cortex-M/R/A架构对比

先说说我个人的理解。这三个系列,说白了就是ARM针对不同场景设计的三个“性格”完全不同的处理器。

特性 Cortex-M Cortex-R Cortex-A
典型应用 MCU、IoT、工业控制 实时控制、汽车、存储 应用处理器、Linux、Android
中断延迟 极低(12周期左右) 低(带硬件中断嵌套) 较高(依赖软件处理)
MMU MPU(内存保护) MMU(虚拟内存)
指令集 Thumb/Thumb-2 ARM + Thumb ARM + Thumb + AArch64
RTOS移植难度 简单 中等 复杂

我在一个工业自动化项目中,客户要求用Cortex-R做运动控制。当时我心想,M系列不也能做吗?结果一查资料才发现,R系列有硬件中断嵌套和紧耦合内存(TCM),这对实时性要求极高的场景简直是救命稻草。而M系列虽然也能跑RTOS,但中断响应时间在极端情况下会抖动——你想想看,一个伺服电机每100微秒就要响应一次,抖动个几微秒可能就出问题了。

核心差异一句话总结:

  • Cortex-M:为深度嵌入式而生,简单、低功耗、中断快
  • Cortex-R:为实时控制而生,确定性高、带硬件容错
  • Cortex-A:为复杂应用而生,性能强、跑Linux

做RTOS移植时,我个人习惯先看芯片属于哪个系列。如果是M系列,直接上FreeRTOS或RT-Thread,基本不用改什么。R系列就得小心了——我曾经在Cortex-R5上移植RTOS时,发现它的异常向量表和M系列完全不同,折腾了两天才搞定。

2.2 芯片内存映射与外设总线

内存映射,说白了就是芯片厂商给各个模块分配地址。你想想看,一个芯片里CPU、SRAM、Flash、外设寄存器都挤在一起,总得有个地址规则吧?

以STM32F4为例,它的内存映射大概是这样的:

0x0000 0000 - 0x000F FFFF: 代码区(Flash映射)
0x2000 0000 - 0x2001 FFFF: SRAM
0x4000 0000 - 0x400F FFFF: 外设总线(APB1)
0x5000 0000 - 0x500F FFFF: 外设总线(APB2)
0xE000 0000 - 0xE00F FFFF: 系统控制块(SCB、NVIC等)

这里有个坑,我当年踩过。外设总线的地址,不同厂商甚至同一厂商不同系列都可能不一样。比如STM32F1和STM32F4的GPIO地址就不同。所以移植RTOS时,千万别硬编码地址——用寄存器定义头文件才是正道。

避坑指南:我曾经在项目里直接写了外设地址的宏定义,结果换芯片型号时改得想哭。后来我学乖了,一律用芯片厂商提供的CMSIS头文件,或者自己封装一层地址映射函数。

外设总线这块,ARM Cortex-M用的是AHB/APB总线矩阵。AHB负责高速传输(比如DMA、Flash),APB负责低速外设(比如UART、I2C)。你想想看,如果UART挂在AHB上,那纯属浪费带宽。芯片厂商这么设计是有道理的。

2.3 启动文件与向量表分析

启动文件,很多初学者觉得它神秘。其实说白了,它就是芯片上电后执行的第一段代码。我刚开始做嵌入式时,总觉得启动文件是芯片厂商写好的,不用管。直到有一次自己画板子、自己写启动代码——嗯,那才叫酸爽。

以Cortex-M3为例,它的向量表长这样:

; 向量表起始地址(通常为0x00000000)
__Vectors       DCD     __initial_sp          ; 栈顶指针
                DCD     Reset_Handler         ; 复位中断
                DCD     NMI_Handler           ; NMI中断
                DCD     HardFault_Handler     ; 硬错误
                DCD     MemManage_Handler     ; 内存管理错误
                DCD     BusFault_Handler      ; 总线错误
                DCD     UsageFault_Handler    ; 用法错误
                ...

注意看,Cortex-M的向量表第一个条目是栈顶指针,第二个才是复位中断。这和Cortex-A完全不同——A系列的向量表第一个是复位中断,栈指针需要软件初始化。我记得第一次在Cortex-A上写裸机程序时,按M系列的习惯写了向量表,结果芯片死活跑不起来。查了半天才发现,A系列压根不自动加载栈指针。

个人经验:做RTOS移植时,启动文件里最关键的其实是中断处理。我习惯在启动文件中把所有的中断都指向一个默认处理函数,然后在RTOS初始化时再重新注册。这样即使某个中断没被处理,也不会导致系统崩溃。

启动流程大致是这样的:

  1. 芯片上电,从0x00000000读取栈顶指针
  2. 从0x00000004读取复位中断地址
  3. 跳转到复位中断处理函数
  4. 复位中断里初始化时钟、内存、外设
  5. 调用main函数

嗯,这里要注意。Cortex-M的复位中断里,通常会调用一个叫SystemInit的函数,然后才跳转到main。SystemInit是芯片厂商提供的,负责配置系统时钟。我建议你在移植RTOS时,不要动SystemInit,除非你非常清楚自己在做什么。

2.4 链接脚本(.ld)深度解析

链接脚本,很多人觉得它比启动文件还神秘。其实它就是告诉链接器:你的代码放哪,数据放哪,堆栈放哪。我见过不少工程师,代码写得很溜,但一碰到链接脚本就头大。

一个典型的Cortex-M链接脚本长这样:

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .text :
    {
        *(.isr_vector)    ; 向量表
        *(.text)          ; 代码
        *(.rodata)        ; 只读数据
        _etext = .;
    } > FLASH

    .data : AT (ADDR(.text) + SIZEOF(.text))
    {
        _sdata = .;
        *(.data)
        _edata = .;
    } > RAM

    .bss :
    {
        _sbss = .;
        *(.bss)
        _ebss = .;
    } > RAM
}

这里有几个关键点,我当年踩过坑:

  • ORIGIN和LENGTH:必须和芯片手册一致。我曾经把Flash地址写错了,结果程序死活下载不进去。
  • .isr_vector:向量表必须放在最前面,而且地址要对齐。Cortex-M要求向量表按256字节对齐。
  • .data段的加载地址和运行地址:加载地址在Flash,运行地址在RAM。启动文件里需要把.data从Flash拷贝到RAM。
  • _sdata、_edata这些符号:启动文件里会用它们来初始化数据段。如果你改了链接脚本,记得同步修改启动文件。

我个人的链接脚本调试技巧:

先用arm-none-eabi-objdump -h查看各段的地址,确认是否和预期一致。再用arm-none-eabi-nm查看符号地址。如果发现某个全局变量地址不对,八成是链接脚本的问题。

做RTOS移植时,链接脚本里还需要注意堆栈大小。FreeRTOS默认的堆栈是configTOTAL_HEAP_SIZE,这个值需要在链接脚本里预留足够的RAM空间。我习惯在链接脚本里定义一个_heap_size符号,然后在RTOS配置里引用它。

好了,这一章的内容就到这。芯片架构和启动流程是RTOS移植的地基,地基打不牢,后面全是坑。下一章咱们聊聊中断管理和上下文切换——那才是RTOS移植的真正核心。


公众号:蓝海资料掘金营,微信 deep3321