第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初始化时再重新注册。这样即使某个中断没被处理,也不会导致系统崩溃。
启动流程大致是这样的:
- 芯片上电,从0x00000000读取栈顶指针
- 从0x00000004读取复位中断地址
- 跳转到复位中断处理函数
- 复位中断里初始化时钟、内存、外设
- 调用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