第2章 目标硬件平台分析:CPU架构、内存映射与中断控制器
做操作系统移植,说白了就是让内核认识你的硬件。我见过不少团队,代码写得挺漂亮,一上板就崩——问题往往出在对硬件平台的理解不够深。这一章,我们就把目标硬件扒开来看清楚。
2.1 CPU架构:ARM、MIPS与RISC-V
CPU架构决定了内核怎么跑、中断怎么处理、内存怎么管。目前主流的三种架构,各有各的脾气。
2.1.1 ARM架构
ARM在嵌入式领域几乎无处不在。Cortex-A系列跑Linux,Cortex-M系列跑RTOS,Cortex-R系列跑实时控制。我个人习惯把ARM的异常模型分成两类:
- A/R系列:使用Vector Table,支持多个异常等级(EL0~EL3)
- M系列:使用中断向量表,硬件自动压栈,响应极快
移植时最头疼的是MMU配置。ARM的页表格式从V7到V8变了一次,V8的4级页表翻译过程,我当年调了整整三天才跑通。
关键点:ARM的异常向量表基地址通过VBAR寄存器设置。移植时一定要确认VBAR指向正确的内存区域,否则中断一进来就飞了。
2.1.2 MIPS架构
MIPS现在用得少了,但存量市场不小。它的特点是:
- 异常入口固定:0x80000180(或0xBFC00180)
- CP0协处理器管一切:Status、Cause、EPC寄存器
- Cache管理全靠软件,没有硬件自动维护
嗯,这里要注意。MIPS的Cache问题是个大坑。我曾经在一个项目中,数据写进去了,读出来还是旧的——就是因为D-Cache没有刷。移植时务必实现好Cache操作函数。
2.1.3 RISC-V架构
RISC-V是后起之秀,指令集精简,扩展灵活。它的异常处理机制很有意思:
- 机器模式(M-mode)是必选的,管所有异常
- 监管者模式(S-mode)跑操作系统
- 用户模式(U-mode)跑应用程序
移植RISC-V时,mtvec和stvec这两个寄存器是关键。它们分别指向M-mode和S-mode的异常入口。我建议你先把M-mode的trap handler写好,再考虑S-mode的。
| 特性 | ARM | MIPS | RISC-V |
|---|---|---|---|
| 异常入口 | VBAR指向向量表 | 固定地址0x80000180 | mtvec/stvec寄存器 |
| 特权级 | EL0~EL3 | Kernel/User | M/S/U三模式 |
| MMU | VMSA,多级页表 | TLB软件管理 | S-mode页表 |
| Cache | 硬件维护为主 | 全软件管理 | 平台相关 |
2.2 内存映射与布局
内存映射是移植的基石。内核不知道外设在哪、内存多大,什么都干不了。我一般拿到开发板,第一件事就是看数据手册的Memory Map章节。
2.2.1 典型内存布局
一个典型的嵌入式系统,内存布局大概长这样:
0x00000000 - 0x0000FFFF : Boot ROM
0x00010000 - 0x0001FFFF : SRAM
0x20000000 - 0x2003FFFF : 片上SRAM(Cortex-M常见)
0x40000000 - 0x4FFFFFFF : 外设寄存器
0x80000000 - 0x8FFFFFFF : DDR内存(ARM Linux常见)
0xE0000000 - 0xE00FFFFF : 系统控制块(SCB)
移植时,你需要定义几个关键地址:
- ROM起始地址:代码从哪里开始执行
- RAM起始地址和大小:堆栈、数据段放哪
- 外设基地址:UART、GPIO、定时器都在哪
我的经验:在链接脚本里,把.text、.data、.bss的地址写清楚。我习惯在链接脚本开头加一大段注释,把内存映射图画出来,方便以后查。
2.2.2 外设内存映射
外设寄存器通常映射到特定的地址范围。比如STM32的USART1,基地址是0x40011000。移植驱动时,你得用指针直接访问这些地址:
#define USART1_BASE 0x40011000
#define USART1_SR (*(volatile uint32_t *)(USART1_BASE + 0x00))
#define USART1_DR (*(volatile uint32_t *)(USART1_BASE + 0x04))
这里有个坑:volatile关键字不能少。编译器优化会把你坑惨——我见过有人忘了加volatile,结果读状态寄存器永远返回同一个值。
2.3 中断控制器:GIC与CLINT
中断控制器是CPU和外设之间的桥梁。ARM用GIC,RISC-V用CLINT+PLIC,MIPS用CP0中断加外部中断控制器。
2.3.1 ARM GIC(Generic Interrupt Controller)
GIC从V2发展到V4,移植最常用的是GIC V2和V3。GIC的核心组件:
- Distributor:管中断的使能、优先级、目标CPU
- CPU Interface:每个CPU核一个,管中断的应答和结束
GIC的初始化流程,我总结成四步:
- 关中断(设置GICD_CTLR为0)
- 配置每个中断的优先级和目标CPU
- 使能中断(设置GICD_ISENABLER)
- 开中断(设置GICD_CTLR和GICC_CTLR)
注意:GIC V3的寄存器地址和V2不一样。移植时一定要确认GIC版本,否则写寄存器写错位置,中断根本不会来。
2.3.2 RISC-V CLINT与PLIC
RISC-V的中断控制分两部分:
- CLINT:管核内中断(软件中断、定时器中断)
- PLIC:管核外中断(外设中断)
CLINT的寄存器很简单:
// CLINT寄存器定义
#define CLINT_BASE 0x02000000
#define MSIP (CLINT_BASE + 0x0000) // 软件中断
#define MTIMECMP (CLINT_BASE + 0x4000) // 定时器比较值
#define MTIME (CLINT_BASE + 0xBFF8) // 定时器当前值
PLIC稍微复杂一点。它支持最多1023个外部中断源,优先级可配。我建议你先把PLIC的使能位和优先级寄存器搞清楚,再写中断处理函数。
2.4 定时器与时钟系统
操作系统离不开时钟。任务调度、时间片、延时,全得靠定时器。不同架构的定时器实现方式不一样。
2.4.1 ARM Generic Timer
ARM从Cortex-A5开始集成了Generic Timer。它包含:
- 系统计数器:一个64位的递增计数器,频率固定
- 比较器:每个CPU核一个,用于产生中断
移植时,你需要读取CNTFRQ寄存器获取计数器频率,然后设置CNTP_TVAL寄存器产生周期性中断。我习惯把系统时钟频率定义成一个宏,方便调整。
2.4.2 RISC-V Timer
RISC-V的定时器通过CLINT实现。MTIME寄存器每周期递增,当MTIME等于MTIMECMP时,触发定时器中断。移植代码很简单:
void timer_init(uint64_t interval) {
uint64_t current = read_csr(mtime);
write_csr(mtimecmp, current + interval);
set_csr(mie, MIP_MTI); // 使能定时器中断
}
嗯,这里有个细节。MTIME和MTIMECMP是64位的,但有些32位平台只实现了低32位。移植时一定要确认硬件实现,否则高32位写不进去,定时器永远不触发。
2.4.3 时钟频率的确定
时钟频率决定了定时器的精度。我一般这样确定:
- 读数据手册,找到系统时钟频率
- 用示波器量一下时钟引脚,确认实际频率
- 在代码里打印出来,验证一致性
避坑指南:我曾经在一个项目里,数据手册写的是50MHz,实际量出来只有48MHz。差了4%,定时器跑一天就偏了快一个小时。所以,别信手册,信示波器。
2.5 知识结构总览
这一章的内容比较多,我画了一张图帮你理清思路:
这张图把本章的四个核心模块串起来了。你移植的时候,按这个顺序走,基本不会漏东西。
2.6 本章小结
目标硬件平台分析,是操作系统移植的第一步,也是最关键的一步。CPU架构决定了你怎么处理异常和中断,内存映射决定了代码和数据放哪,中断控制器决定了外设怎么和CPU通信,定时器决定了系统的心跳。
我个人建议,拿到一块新板子,先花一天时间把数据手册啃透。把Memory Map画出来,把中断号列出来,把时钟树理清楚。这些基础工作做好了,后面的移植会顺畅很多。