第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的初始化流程,我总结成四步:

  1. 关中断(设置GICD_CTLR为0)
  2. 配置每个中断的优先级和目标CPU
  3. 使能中断(设置GICD_ISENABLER)
  4. 开中断(设置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 知识结构总览

这一章的内容比较多,我画了一张图帮你理清思路:

目标硬件平台分析知识结构 CPU架构分析 ARM MIPS RISC-V 内存映射与布局 ROM/RAM布局 外设映射 中断控制器 GIC CLINT/PLIC 定时器与时钟系统 Generic Timer CLINT Timer 移植核心要点 1. 确认CPU架构的异常模型和特权级 2. 理清内存映射,特别是外设基地址 3. 中断控制器初始化顺序不能错

这张图把本章的四个核心模块串起来了。你移植的时候,按这个顺序走,基本不会漏东西。

2.6 本章小结

目标硬件平台分析,是操作系统移植的第一步,也是最关键的一步。CPU架构决定了你怎么处理异常和中断,内存映射决定了代码和数据放哪,中断控制器决定了外设怎么和CPU通信,定时器决定了系统的心跳。

我个人建议,拿到一块新板子,先花一天时间把数据手册啃透。把Memory Map画出来,把中断号列出来,把时钟树理清楚。这些基础工作做好了,后面的移植会顺畅很多。