基于 QEMU 的 RISC-V AMP 双系统仿真平台研发
第一章:RISC-V 体系结构基础
1.1 三种特权级——谁跑在哪一层
所有 8 个 hart 上电后在 MROM 从 0x00000000 开始执行,此时处于 M-mode。整个启动链的每个组件,在哪个特权级运行是严格分层的:
M-mode (最高权限)
├── MROM reset vector (0x00000000)
├── LowLevelBoot (pflash 0x20000000)
├── OpenSBI × 2 (Linux: 0xAFF00000, RTOS: 0xAFF80000)
│ ↕ SBI ecall (特权级切换)
S-mode (监管态)
├── U-Boot (0xB0100000)
├── Linux Kernel (跳入后接管)
│ └── 创建虚拟内存、页表
└── FreeRTOS (0xB0C00000, hart7独占)
└── vTaskStartScheduler
U-mode (用户态 最低)
└── BusyBox ~ # (initramfs 启动 /bin/sh)
关键设计决策:FreeRTOS 本应跑在 M-mode(大多数嵌入式 RTOS 的默认模式),但在我们的 AMP 架构中 hart7 必须先经过 OpenSBI,由 OpenSBI 从 M-mode 降权到 S-mode 再执行 FreeRTOS。这就是 Phase 3 移植最核心的工作——把 FreeRTOS 的汇编上下文从操作 M-mode CSR 改成 S-mode CSR。
1.2 CSR 寄存器——TwinV-RV 实际用到的 12 个关键 CSR
| CSR名 | 作用 | 项目中在哪里用 |
|---|---|---|
| mhartid | 读取当前 hart ID | LowLevelBoot startup.S 第一行;FreeRTOS startup.S |
| mstatus/sstatus | 全局中断使能 + 特权级状态 | FreeRTOS portASM.S 上下文切换 |
| mie/sie | 中断使能寄存器 | FreeRTOS port.c: csrs sie, 0x220 |
| mip/sip | 待处理中断寄存器 | FreeRTOS startup.S 清零 |
| mtvec/stvec | 中断向量表基址 | 整个中断处理链的入口 |
| mcause/scause | 异常/中断原因码 | FreeRTOS trap handler 区分定时器中断(5) vs 外部中断(9) |
| mepc/sepc | 异常返回地址 | 上下文切换恢复 |
| mtval/stval | 异常附加信息 | debug 用 |
| mscratch/sscratch | 临时寄存器 | 上下文切换保存栈指针 |
1.3 中断异常路径——完整流程
以 FreeRTOS 定时器中断为例,走一遍全路径:
1. 硬件 mtime 计数器达到 mtimecmp 值
↓
2. CLINT 触发 MTI (Machine Timer Interrupt)
↓
3. 硬件自动: mcause ← 7, mepc ← PC, mstatus.MIE ← 0, PC ← mtvec
↓
4. RTOS OpenSBI (M-mode) 捕获 MTI
↓ 清除 mtimecmp,处理硬件侧
↓ 通过 SBI 机制注入 STI (Supervisor Timer Interrupt)
↓
5. 硬件自动: scause ← 5, sepc ← FreeRTOS PC, sstatus.SIE ← 0, PC ← stvec
↓
6. FreeRTOS portASM.S trap_handler:
- 保存上下文 (x1-x31, sstatus, sepc)
- 读取 scause,判断是中断(bit 63=1)还是异常(bit 63=0)
- 是中断 → 读 scause 低 12 位 = 5 → STI → 调用 xTaskIncrementTick
- 检查是否需要任务切换
- 恢复上下文 → sret (回到被中断的任务)
1.4 PMP 物理内存保护
PMP 是 RISC-V M-mode 控制物理内存访问的硬件机制。OpenSBI 用它实现 Domain 隔离:
OpenSBI Domain 配置 (twinv_sbi.dts):
tdomain (hart7, 可信域):
tmem0 (RTOS 代码) : 0xB0C00000, R/W/X = 0x7
tmem1 (RTOS 数据) : 0xB0E00000, R/W/X = 0x7
shmem (共享内存) : 0xB0800000, R/W/X = 0x7
tuart (独占串口) : 0x10002000, R/W/X = 0x7
udomain (hart0-6, 非可信域):
tmem0/tmem1 : 权限 = 0x0 (完全禁止!)
tuart : 权限 = 0x0 (Linux 看不到 UART2)
shmem : 权限 = 0x7 (仅此区域可访问)
PMP 的控制通过两组 CSR:pmpcfg(配置权限)和 pmpaddr(配置地址边界)。OpenSBI 在 sbi_domain_register 中解析 DTB 的 opensbi-domains 节点并编程 PMP 寄存器——这部分由 OpenSBI 自动完成,我们的代码不需要手写。
硬件强制隔离意味着:即使 Linux 内核被攻破,恶意代码写 0xB0C00000 这个物理地址也会被 PMP 直接拒绝,FreeRTOS 不受影响。这就是 AMP 架构相比 SMP 的安全优势。
1.5 原子指令与 fence——项目中的实际使用
原子操作:我们实际没用 amoswap/lr/sc。LowLevelBoot 的多核同步用的是软件 _pen 变量 + 轮询(lw → bnez 循环),配合 fence 保证内存序。
fence 指令:在 LowLevelBoot startup.S 中有两处关键 fence:
/* 主核写 _pen = 1 后 fence —— 确保从核能看到这个写 */
sw t1, 0(t0)
fence /* ← 写屏障 */
/* 从核释放 _pen 后 fence —— 确保后续代码看到最新的内存 */
sw zero, 0(t0)
fence /* ← 写屏障 */
/* 跳转 OpenSBI 前 fence.i —— 确保 I-cache 同步 */
fence.i
fence 保证数据内存序,fence.i 保证指令缓存与数据缓存一致(因为 lowlevelboot 把固件从 pflash 复制到了 DDR)。
1.6 要点
| 问题 | 答案要点 |
|---|---|
| M-mode 和 S-mode 的边界在哪? | OpenSBI 是最后一道 M-mode 代码,之后通过 mret→sret 降权到 S-mode |
| 为什么 FreeRTOS 不直接跑 M-mode? | AMP 架构下所有的 M-mode 资源(中断、定时器、HSM)必须由 OpenSBI 统一管理,否则两个 OS 会争抢硬件 |
| fence vs fence.i 区别? | fence 是数据内存序屏障,fence.i 是指令缓存同步——固件搬运后必须 fence.i 否则 CPU 可能执行旧的缓存指令 |
| 从 mret 到 sret 的修改主要在哪? | portASM.S 上下文恢复路径的最后一条指令,原本用 mret 返回 M-mode 任务,S-mode 下改用 sret |
第二章:QEMU 虚拟 SoC 定制
TwinV-RV 整个项目的第一行代码不是固件,而是在 QEMU 里创建一颗虚拟芯片。打开两个核心文件讲:
- patches/qemu/twinv.h — SoC 的"规格书"(定义有什么外设、多少个核)
- patches/qemu/twinv.c — SoC 的"电路实现"(实际创建这些硬件)
2.1 QEMU 是怎么"造芯片"的
QEMU 有一个对象模型叫 QOM (QEMU Object Model)。创建一个新的虚拟机器分三步走,所有三步都在 twinv.c 最底部:
// 第一步:定义这个类型的"身份证"
static const TypeInfo twinv_machine_typeinfo = {
.name = TYPE_RISCV_TWINV_MACHINE, // 类型名 "twinv"
.parent = TYPE_MACHINE, // 父类:所有机器的基类
.class_init = twinv_machine_class_init, // 第二步:类初始化
.instance_init = twinv_machine_instance_init, // 对象创建时回调
.instance_size = sizeof(TwinVState), // 这个机器需要多大内存存状态
};
// 第三步:注册到 QEMU 的类型系统
type_init(twinv_machine_init_register_types);
type_init 是 QEMU 的注册宏——QEMU 启动时自动执行,把你的 twinv 机器加入它认识的机器列表。之后 -M twinv 就能用了。
2.2 class_init:定义这台机器的"出厂规格"
static void twinv_machine_class_init(ObjectClass *oc, const void *data)
{
MachineClass *mc = MACHINE_CLASS(oc);
mc->desc = "TwinV-RV AMP Platform";
mc->init = twinv_machine_init; // ← 最关键的:机器启动入口
mc->max_cpus = 8; // 最多 8 核
mc->min_cpus = 8; // 最少 8 核(强制,不许减少)
mc->default_cpus = 8; // 默认 8 核
mc->default_cpu_type = TYPE_RISCV_CPU_BASE; // CPU 类型:RISC-V
}
要点:
- mc->init 是 QEMU 启动时第一个被调用的函数,等价于真实芯片上电后的"硬件初始化"
- 这里把 min_cpus = max_cpus = 8,强制必须是 8 核——因为 AMP 架构依赖"7 Linux + 1 RTOS"这个固定划分
- TYPE_RISCV_CPU_BASE 告诉 QEMU 所有 8 个核都是 RISC-V 64 位
2.3 machine_init:一帧一帧"画"出这颗芯片
twinv_machine_init 就是整颗芯片的"原理图"。它按顺序创建每个硬件模块:
static void twinv_machine_init(MachineState *machine)
{
twinv_cpu_create(machine); // 1. 创建 8 个 RISC-V CPU 核心
twinv_interrupt_controller_create(machine); // 2. 内核中断CLINT + 外设中断控制器PLIC 中断控制器
twinv_memory_create(machine); // 3. MROM + SRAM + DDR 内存
twinv_flash_create(machine); // 4. pflash 固件存储
twinv_syscon_create(machine); // 5. 系统控制器(重启)
twinv_rtc_create(machine); // 6. 实时时钟
twinv_serial_create(machine); // 7. 三个 UART 串口
twinv_virtio_mmio_create(machine); // 8. 八个 VirtIO-MMIO 设备
twinv_fw_cfg_create(machine); // 9. 固件配置接口
}
2.4 CPU 是怎么创建的——两簇分离
static void twinv_cpu_create(MachineState *machine)
{
// c-cluster: hart 0-6,跑 Linux
object_initialize_child(OBJECT(machine), "c-cluster",
&s->c_cluster, TYPE_CPU_CLUSTER);
qdev_prop_set_uint32(DEVICE(&s->c_cluster), "cluster-id", 0);
object_initialize_child(OBJECT(&s->c_cluster), "c-cpus",
&s->c_cpus, TYPE_RISCV_HART_ARRAY);
object_property_set_int(OBJECT(&s->c_cpus), "hartid-base", 0);
object_property_set_int(OBJECT(&s->c_cpus), "num-harts", 7);
// r-cluster: hart 7,跑 FreeRTOS
object_initialize_child(OBJECT(machine), "r-cluster",
&s->r_cluster, TYPE_CPU_CLUSTER);
qdev_prop_set_uint32(DEVICE(&s->r_cluster), "cluster-id", 1);
object_initialize_child(OBJECT(&s->r_cluster), "r-cpus",
&s->r_cpus, TYPE_RISCV_HART_ARRAY);
object_property_set_int(OBJECT(&s->r_cpus), "hartid-base", 7);
object_property_set_int(OBJECT(&s->r_cpus), "num-harts", 1);
}
要点:
- 两个 CPU_CLUSTER 把 8 核拆成两组——这是 AMP 架构的硬件基础
- hartid-base=0, num-harts=7 → hart 0-6 属于 c-cluster
- hartid-base=7, num-harts=1 → hart 7 属于 r-cluster
- 为什么要分簇?QEMU 的 CPU_CLUSTER 可以给不同簇配置不同的属性(时钟频率、复位地址等),虽然我们暂时没用到差异化配置,但这个结构为后续扩展留了空间
- resetvec 属性指定了上电后所有 hart 的第一条指令地址:0x00000000(MROM)
2.5 内存是怎么创建的——三层物理内存
static void twinv_memory_create(MachineState *machine)
{
// MROM (32KB @ 0x00000000) — ROM,不可写,存复位向量
memory_region_init_rom(mrom, NULL, "twinv.mrom",
twinv_memmap[TWINV_MROM].size, &error_fatal);
memory_region_add_subregion(system_memory,
twinv_memmap[TWINV_MROM].base, mrom);
// SRAM (32KB @ 0x00008000) — RAM,可读写,早期启动暂存
memory_region_init_ram(sram, NULL, "twinv.sram",
twinv_memmap[TWINV_SRAM].size, &error_fatal);
memory_region_add_subregion(system_memory,
twinv_memmap[TWINV_SRAM].base, sram);
// DDR (1GB @ 0x80000000) — RAM,主内存,大小由 -m 参数决定
memory_region_init_ram(dram, NULL, "twinv.dram",
machine->ram_size, &error_fatal);
memory_region_add_subregion(system_memory,
twinv_memmap[TWINV_DRAM].base, dram);
}
要点:
- memory_region_init_rom vs memory_region_init_ram:ROM 内容由 QEMU 管理,CPU 不可写;RAM 可读写
- machine->ram_size 是用户命令行 -m 1G 传进来的值——我们对用户暴露了可配置的内存大小
- system_memory 是 QEMU 的全局物理地址空间,所有 memory_region_add_subregion 往里面添加区域
2.6 MROM 复位向量——芯片上电后跑的第一段代码
static void twinv_setup_rom_reset_vec(MachineState *machine,
RISCVHartArrayState *harts,
hwaddr start_addr,
hwaddr rom_base, hwaddr rom_size,
uint64_t kernel_entry,
uint32_t fdt_load_addr)
{
uint32_t reset_vec[12] = {
0x00000297, // auipc t0, 0 # t0 = PC = 0
0xf1402573, // csrr a0, mhartid # a0 = hart ID
0x0202b583, // ld a1, 32(t0) # a1 = DTB 地址
0x0282b603, // ld a2, 40(t0) # a2 = fw_dynamic_info 地址
0x0182b283, // ld t0, 24(t0) # t0 = 固件入口地址
0x00028067, // jr t0 # 跳到固件!
... // 后面是数据:start_addr, fdt_load_addr
};
// 把这个小数组烧进 MROM 的 0x00000000 位置
rom_add_blob_fixed_as("mrom.reset", reset_vec, sizeof(reset_vec),
rom_base, &address_space_memory);
}
这是整个项目最先执行的一段代码——10 条指令,手写机器码(不能依赖编译器),烧在 MROM 的 0x00000000。
流程:
- auipc t0, 0 → t0 = 0 (PC 此时为 0)
- csrr a0, mhartid → a0 = 当前核的 ID
- ld a1, 32(t0) → 从 MROM 偏移 32 读取 DTB 地址 → a1
- ld a2, 40(t0) → 从 MROM 偏移 40 读取 fw_dynamic_info → a2
- ld t0, 24(t0) → 从 MROM 偏移 24 读取固件入口 → t0 = 0x20000000
- jr t0 → 跳到 pflash 0x20000000 执行 LowLevelBoot!
要点:为什么是手写机器码而不是汇编?因为 MROM 里的代码必须绝对位置无关且不能依赖任何链接器——此时还没有栈、没有 BSS、没有任何初始化。
2.7 PLIC 中断控制器的连接逻辑
static void twinv_interrupt_controller_create(MachineState *machine)
{
// CLINT: 核内中断 (软件中断 + 定时器)
riscv_aclint_swi_create(0x02000000, 0, 8, false);
riscv_aclint_mtimer_create(0x02004000, ..., 0, 8, ...);
// PLIC: 平台级中断 (UART, VirtIO 等外设)
char *cfg = riscv_plic_hart_config_string(8); // 8 核
s->plic = sifive_plic_create(0x0C000000, cfg, 8, 0, 128, ...);
}
要点:
- CLINT 是核内的,每个 hart 有自己的软件中断和定时器
- PLIC 是全局的,所有外设的中断先到 PLIC,PLIC 再路由到目标 hart
- hart_config_string 格式是 "M,S,M,S,..."(每核两个 context:M-mode 和 S-mode)
- TwinV-RV 的中断号分配:UART0=10, UART1=11, UART2=12, VirtIO=1到8
2.8 VirtIO-MMIO——虚拟设备的"万能接口"
static void twinv_virtio_mmio_create(MachineState *machine)
{
for (int i = 0; i < 8; i++) {
sysbus_create_simple("virtio-mmio",
twinv_memmap[TWINV_VIRTIO0 + i].base,
qdev_get_gpio_in(DEVICE(s->plic), TWINV_VIRTIO0_IRQ + i));
}
}
8 个 VirtIO-MMIO 槽位,每个占 4KB 地址空间。QEMU 命令行可以往这些槽位插设备:
-device virtio-blk-device,drive=rootfs → 插一个虚拟磁盘到槽位 0
-device virtio-net-device,netdev=net0 → 插一个虚拟网卡到槽位 1
要点:VirtIO 是半虚拟化标准——Guest OS 知道自己在虚拟机里,通过 VirtIO 驱动直接跟 QEMU 后端通信,比模拟真实硬件(如 e1000 网卡)快得多。MMIO 是传输层(通过内存映射 IO 地址),PCI 是另一种传输层(通过 PCI 总线发现设备)。
总览
TwinV-RV SoC 内部结构:
┌─────────────────────────────────────────────────────┐
│ c-cluster (hart0-6) │ r-cluster (hart7) │
│ ┌───┐ ┌───┐ ┌───┐ │ ┌───┐ │
│ │H0 │ │H1 │... │H6 │ │ │H7 │ │
│ └───┘ └───┘ └───┘ │ └───┘ │
├─────────────────────────┴───────────────────────────┤
│ CLINT (定时器+软中断) PLIC (外设中断路由) │
├─────────────────────────────────────────────────────┤
│ UART0 UART1 UART2 RTC syscon fw_cfg │
├─────────────────────────────────────────────────────┤
│ VirtIO-MMIO × 8 (磁盘/网络/GPU) │
├─────────────────────────────────────────────────────┤
│ 内存: MROM(0x0) SRAM(0x8000) pflash(0x20000000) │
│ DDR(0x80000000, 1GB) │
└─────────────────────────────────────────────────────┘
第三章:LowLevelBoot — 手写汇编固件。
3.1 LowLevelBoot 是什么?
它是 整个系统上电后执行的第一段"有意义"的代码。
上电 → MROM reset_vec (12条机器码) → jr t0 → LowLevelBoot (pflash 0x20000000)
为什么必须手写汇编? 因为此时:
- 没有 C 运行时(没有栈、没有 .bss 清零)
- 没有 MMU
- 8 个 hart 同时涌进来,必须同步
- 固件还在 pflash(慢速 NOR Flash),需要搬到 DDR 才能执行
MROM reset_vec (0x00000000)
│
▼
LowLevelBoot _start
│
csrr a0, mhartid
│
┌─────── hart0? ────────┐
│ 是 │ 否 (hart 1-7)
▼ ▼
master_init slave_spin
│ │
1. 设 _pen=1 loop 等待
2. 检测运行环境 poll _pen
3. copy_fw × 6 │
pflash→DDR _pen==0?
4. 设 _pen=0 │ 是
│ ▼
│ 判 hart ID
│ ┌──────┴──────┐
│ hart1-6 hart7
▼ │ │
判 hart ID ▼ ▼
│ Linux OpenSBI RTOS OpenSBI
│ @ 0xAFF00000 @ 0xAFF80000
▼
Linux OpenSBI
@ 0xAFF00000
3.2 核心机制
3.2.1 多核同步:_pen 变量
8 个核同时涌进 _start,只有 hart0 负责搬固件,其余 7 个必须原地等待。
// _pen 在 .bss 段,初始值 = 0
_pen:
.word 0
master_init:
sw t1, 0(t0) // hart0: _pen = 1 (关门)
...
sw zero, 0(t0) // hart0: _pen = 0 (开门!大家走!)
slave_spin:
lw t1, 0(t0)
bnez t1, 1b // _pen==1 → 继续等
fence // _pen==0 → 内存屏障,走出循环
这是一个软件自旋锁——不需要硬件支持,pure software。
考点:为什么需要 fence?因为 hart0 在另一个核上写了 _pen=0,没有 fence 的话,从核的 store buffer 可能看到的是旧值。fence 强制刷新 load/store 顺序。
3.2.2 环境检测:我在哪里执行?
auipc t0, 0 // t0 = 当前 PC
li t1, 0x20000000
bgeu t0, t1, in_pflash // PC ≥ pflash → 在 pflash 里
li t1, 0x80000000
bgeu t0, t1, probe_done // PC ≥ DDR → 已在 DDR(调试模式)
j probe_done // PC < pflash → MROM/SRAM(不应该)
同一种代码适配三种运行场景。如果已经烧在 pflash 里就从 pflash 运行搬代码,如果用 JTAG 加载到 DDR 调试就跳过搬运。
- copy_fw 宏:pflash → DDR 搬运工
.macro copy_fw, src_val, dst_val, size_val
li a0, \src_val // 源地址
li a1, \dst_val // 目标地址
li a2, \dst_val + \size_val // 结束地址
bgeu a1, a2, 2f // 空范围直接跳过
1:
lw t3, 0(a0) // 读 4 字节
sw t3, 0(a1) // 写 4 字节
addi a0, a0, 4 // 源指针+4
addi a1, a1, 4 // 目标指针+4
bltu a1, a2, 1b // 没搬完继续
fence
2:
.endm
调用 6 次,搬 6 个固件:
| 源 (pflash) | 目标 (DDR) | 内容 |
|---|---|---|
| 0x20080000 | 0xAFF00000 | Linux OpenSBI |
| 0x20200000 | 0xAFF80000 | RTOS OpenSBI |
| 0x20040000 | 0xB0000000 | twinv_sbi.dtb |
| 0x20100000 | 0xB0C00000 | FreeRTOS (trusted_fw) |
| 0x20060000 | 0xB0008000 | twinv_uboot.dtb |
| 0x20280000 | 0xB0100000 | U-Boot |
- AMP 路由:分流
csrr a0, mhartid
// hart 7 → RTOS OpenSBI
li t1, 7
beq a0, t1, jump_rtos
// hart 0-6 → Linux OpenSBI
li a1, SBI_DTB_DST // a1 = DTB 地址 (OpenSBI 参数)
li t0, OPENSBI_DST
fence.i // 指令缓存刷新
jr t0 // 跳!
jump_rtos:
li a1, SBI_DTB_DST
li t0, RTOS_SBI_DST
fence.i
jr t0
为什么要 fence.i?因为 copy_fw 把代码写到 DDR 用的是 sw(store word),CPU 的指令缓存还以为是旧数据。fence.i = 刷新指令缓存,让 CPU 取指时看到新代码。
3.3 配套文件
| 文件 | 作用 |
|---|---|
| src/lowlevelboot/startup.S | 上面讲的所有逻辑 |
| src/lowlevelboot/uart.c | NS16550 串口驱动(打印 banner) |
| src/lowlevelboot/link.lds | 链接脚本:代码放 pflash 0x20000000,BSS 放 SRAM 0x00008000 |
3.4 要点
为什么 LowLevelBoot 必须用汇编写? → 没有 C 运行时,栈/BSS 都没初始化
多核同步怎么做的? → _pen 软件自旋锁 + fence 内存屏障
为什么固件要从 pflash 搬到 DDR? → NOR Flash 读取慢,DDR 里执行更快;部分固件(OpenSBI/U-Boot)链接地址就在 DDR,不在 pflash
fence.i 干什么的? → 指令缓存刷新,保证 sw 写进去的代码能被 CPU 取指
怎么区分 hart 走哪条路? → mhartid 读 CSR,硬编码判断:hart0-6→Linux,hart7→FreeRTOS
UART 怎么输出第一个字符? → 直接写 MMIO 地址 0x10000000(NS16550 THR 寄存器),轮询 LSR 的 THRE 位
第四章:DTS 设备树
| 文件 | 用途 |
|---|---|
| src/dts/twinv.dtsi | SoC 基础定义(被其他两个 include) |
| src/dts/twinv_sbi.dts | 给 OpenSBI 用(含 domain/PMP 配置) |
| src/dts/twinv_uboot.dts | 给 U-Boot/Linux 用(不含 RTOS 配置) |
twinv.dtsi ← SoC 基础定义(CPU、内存、CLINT、PLIC、串口、VirtIO)
│
├── #include "twinv.dtsi"
│ │
│ twinv_sbi.dts ← OpenSBI 阶段使用
│ 加了: domain/PMP 隔离、memory region、chosen、alias
│
└── #include "twinv.dtsi"
│
twinv_uboot.dts ← U-Boot/Linux 阶段使用
加了: 禁用 cpu7、CMA 池、bootargs、reserved-memory
4.1 twinv.dtsi — SoC"规格书"
4.1.1 CPU 节点
cpu0: cpu@0 {
device_type = "cpu";
reg = <0>; // hart ID = 0
compatible = "riscv";
riscv,isa = "rv64imafdc"; // 指令集: I+MAFD+C
mmu-type = "riscv,sv48"; // 48位虚拟地址(支持 256TB)
cpu0_intc: interrupt-controller { // 每个 hart 自带的中断控制器
compatible = "riscv,cpu-intc";
interrupt-controller;
};
};
- cpu0: 是 label(标签),其他地方用 &cpu0 引用
- compatible 是驱动的匹配字符串——OS 读到 "riscv" 就知道用 RISC-V 的 CPU 驱动
- cpu0_intc 是 CPU 内部中断控制器(处理 M/S-mode 的 timer/software/external 中断)
- 8 个核都写了一遍,只有 hart ID 不同
要点:为什么 sv48? RISC-V 的虚拟地址宽度分 sv32(32位)/sv39(39位)/sv48(48位)/sv57(57位)。sv48 是服务器级 Linux 的标配,支持 256TB 用户地址空间。
4.1.2 memory 节点
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>; // 起始=0x80000000, 大小=1GB
};
reg 的格式:
4.1.3 CLINT — 核内中断器
clint@2000000 {
compatible = "riscv,clint0";
reg = <0x0 0x02000000 0x0 0x00010000>; // 地址 0x02000000, 大小 64KB
interrupts-extended = <&cpu0_intc 3 &cpu0_intc 7 // 每个 hart 两条线
&cpu1_intc 3 &cpu1_intc 7 // intc3=软件中断
... // intc7=定时器中断
&cpu7_intc 3 &cpu7_intc 7>;
};
要点:CLINT 的 intc 3 和 7 是什么意思?
- 3 = MSIP (Machine Software Interrupt Pending) — 核间 IPI 用的软件中断
- 7 = MTIP (Machine Timer Interrupt Pending) — mtime 定时器的中断
4.1.4 PLIC — 平台级中断控制器
plic: plic@c000000 {
compatible = "sifive,plic-1.0.0"; // 用 SiFive 的 PLIC 驱动
reg = <0x0 0x0c000000 0x0 0x04000000>; // 0x0C000000, 64MB
interrupt-controller; // 标记:这是一个中断控制器
interrupts-extended = <&cpu0_intc 11 &cpu0_intc 9 // 每个 hart 两条线
... // intc11=外部中断
&cpu7_intc 11 &cpu7_intc 9>; // intc9=???
riscv,ndev = <128>; // 最多 128 个中断源
};
要点:PLIC 的 intc 11 和 9 是什么意思?
- 11 = MEIP (Machine External Interrupt Pending) — PLIC 把外设中断发给 CPU 用这条线
- 9 = SEIP (Supervisor External Interrupt Pending) — S-mode 的外部中断 (给 Linux 用)
4.1.5 UART 节点
uart0: serial@10000000 {
compatible = "ns16550a"; // 用 NS16550 驱动
reg = <0x0 0x10000000 0x0 0x1000>;
clock-frequency = <1843200>; // ★ 必须能整除 115200
interrupt-parent = <&plic>; // 中断号由 PLIC 管理
interrupts = <10>; // PLIC 中断号 = 10
};
interrupt-parent vs interrupts:
- interrupt-parent = <&plic> → "我的中断连到 PLIC 上"
- interrupts = <10> → "我在 PLIC 上的中断号是 10"
- Linux 驱动通过这两个属性自动调用 request_irq(10, ...)
这个 clock-frequency = 1843200 就是之前 PORTING.md 记录的坑——399193 Hz 算不出 115200 波特率,串口沉默。1843200 ÷ 115200 = 16,整数。
4.1.6 VirtIO-MMIO 节点
virtio_mmio@10100000 {
compatible = "virtio,mmio";
reg = <0x0 0x10100000 0x0 0x1000>; // 每个 4KB MMIO 空间
interrupt-parent = <&plic>;
interrupts = <1>; // PLIC 中断号 1-8
};
8 个槽,地址 0x10100000 到 0x10107000,间隔 0x1000(4KB)。Linux 的 VirtIO 驱动扫到 compatible="virtio,mmio" 就初始化对应的 virtio 设备。-device virtio-blk-device,drive=rootfs 命令会把块设备"插入"第一个空闲槽。
4.2 twinv_sbi.dts — OpenSBI domain 配置
这是整个项目最体现 AMP 设计的地方。
tdomain: trusted-domain {
compatible = "opensbi,domain,instance";
possible-harts = <&cpu7>; // ★ 只有 hart7 属于这个 domain
next-addr = <0x0 0xB0C00000>; // OpenSBI 初始化完跳到 FreeRTOS
next-mode = <0x1>; // 0x1 = S-mode
regions-map = <&tmem0 0x7>, // FreeRTOS 代码区: R/W/X
<&tmem1 0x7>, // FreeRTOS 数据区: R/W/X
<&shmem 0x7>, // 共享内存: R/W/X (IPC 用)
<&tuart 0x7>, // UART2: R/W/X
<&allmem 0x7>; // 其余全部: R/W/X (调试宽松)
};
udomain: untrusted-domain {
possible-harts = <&cpu0 &cpu1 ... &cpu6>; // ★ hart0-6
next-addr = <0x0 0xB0100000>; // 跳到 U-Boot
regions-map = <&tmem0 0x0>, // FreeRTOS 代码区: 禁止访问! (PMP 锁)
<&tmem1 0x0>, // FreeRTOS 数据区: 禁止访问!
<&shmem 0x7>, // 共享内存: 允许访问 (IPC 唯一通道)
<&tuart 0x0>, // UART2: 禁止访问
<&allmem 0x7>; // 其余全部: 允许访问
};
regions-map 的权限值:0x0=无权限, 0x1=R, 0x3=RW, 0x5=RX, 0x7=RWX。
关键设计:Linux domain 的 &tmem0 0x0 和 &tmem1 0x0——即使用 Linux 的 root 权限也访问不了 FreeRTOS 的内存,因为 PMP 在 M-mode 层拦截,比操作系统权限更高。
这是跟 quard_star_tutorial 的核心差异之一——用 OpenSBI domain + PMP 实现硬件级的隔离,而不是依赖软件约定。
4.3 twinv_uboot.dts — Linux 视角
/* 禁用 RTOS 核 — Linux 看不到它就不会尝试调度任务到 hart7 */
cpu7: cpu@7 {
status = "disabled";
};
/* Linux 可见内存缩小,把 RTOS 独占区挖掉 */
memory@80000000 {
reg = <0x0 0x80000000 0x0 0x3F800000>; // 1016MB (1GB - 8MB)
};
/* CMA:Linux 驱动做 DMA 时用的连续内存池 */
cma: linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x0 0x20000000>; // 512MB
linux,cma-default;
};
chosen {
bootargs = "earlycon=sbi console=ttyS0,115200n8 root=/dev/vda rw";
};
cpu7 disabled + memory 缩小 = Linux 完全不知道 hart7 的存在。这是 AMP 隔离的标准做法——不是让 Linux"配合",而是让它根本看不到。
4.4 DTS 生成流程图
twinv.dtsi ─────────┐
(SOC 基础定义) │
├── include ──→ twinv_sbi.dts ──→ dtc → twinv_sbi.dtb
│ (OpenSBI 用) (烧进 pflash)
│
└── include ──→ twinv_uboot.dts ──→ dtc → twinv_uboot.dtb
(U-Boot/Linux 用) (烧进 pflash)
两份 DTB 都写进 pflash,LowLevelBoot 会把 twinv_sbi.dtb 搬到 DDR 0xB0000000,OpenSBI 读它。OpenSBI 读完后,把 twinv_uboot.dtb 传给 U-Boot,U-Boot 再传给 Linux。
4.5 要点
| 问题 | 答案 |
|---|---|
| 为什么有三份 DTS? | SoC 基础 1 份 (dtsi),OpenSBI 阶段 1 份 (含 domain/PMP),Linux 阶段 1 份 (隐藏 RTOS 核和内存) |
| compatible 是干嘛的? | OS 用它匹配驱动,类似 "设备型号" 字符串 |
| #address-cells 和 #size-cells | 定义 reg 里地址 / 大小各占几个 32-bit 字。2/2 = 64 位地址 |
| PLIC 和 CLINT 有什么区别? | CLINT = 核内 (定时器 + IPI),PLIC = 片级 (外部设备→CPU 的中断路由) |
| status = "disabled" 的效果 | Linux 不会为这个节点创建 device,不会 probe 对应驱动 |
| OpenSBI domain regions-map 怎么实现隔离? | 底层调用 PMP 硬件,配置物理内存保护寄存器,M-mode 层面拦截非法访问 |
第五章:OpenSBI 固件
5.1 在启动流程中的位置
hart 0-6: LowLevelBoot ──jr──→ Linux OpenSBI ──→ U-Boot ──→ Linux
hart 7: LowLevelBoot ──jr──→ RTOS OpenSBI ──→ FreeRTOS
↑
都是 fw_jump 类型
只是跳的目标地址不同
5.2 OpenSBI 有三种固件类型
| 类型 | 机制 | 我们用的 |
|---|---|---|
| fw_dynamic | 上一个固件通过 a2 传 fw_dynamic_info 结构体,包含跳转地址 | ❌ QEMU 11 有问题 |
| fw_jump | 编译时写死跳转地址 (FW_JUMP_ADDR),不需要传参 | ✅ 我们用的 |
| fw_payload | OpenSBI 和下面的固件打包成一个文件 | ❌ 不适合 AMP |
我们选 fw_jump 是因为 PORTING.md 第 7/10 号问题——QEMU 11 的 fw_dynamic_info 传参有问题,debug 很久后换成了更简单粗暴的 fw_jump。
"为什么用 fw_jump":因为我们有 LowLevelBoot 做 AMP 路由,已经知道 hart7 该跳哪、hart0-6 该跳哪,不需要 OpenSBI 再动态判断。fw_jump 编译时固定地址反而更可控。
5.3 双实例怎么编译的
# Linux OpenSBI: 跳到 U-Boot
make PLATFORM=twinv CROSS_COMPILE=riscv64-linux-gnu- \
FW_JUMP=y FW_JUMP_ADDR=0xB0100000
cp build/.../fw_jump.bin output/fw/linux_sbi.bin
# RTOS OpenSBI: 跳到 FreeRTOS ★ 只改 FW_JUMP_ADDR!
make clean
make PLATFORM=twinv CROSS_COMPILE=riscv64-linux-gnu- \
FW_JUMP=y FW_JUMP_ADDR=0xB0C00000
cp build/.../fw_jump.bin output/fw/rtos_sbi.bin
同一个源码,同一个 PLATFORM=twinv,只改 FW_JUMP_ADDR。编译出两个功能完全不同的固件。
5.4 我们写的 4 个文件
extern/opensbi/platform/twinv/
├── Kconfig ← 声明依赖哪些 OpenSBI 功能模块
├── objects.mk ← 告诉 Makefile 编译 platform.c
├── configs/
│ └── defconfig ← 默认配置: FW_JUMP=y, FW_JUMP_ADDR=0xB0100000
└── platform.c ← ★ 核心: 平台初始化逻辑
5.4.1 Kconfig — 声明依赖
config PLATFORM_TWINV
bool
select FDT_DOMAIN # ← 关键! 支持 domain/PMP 隔离
select FDT_IRQCHIP_PLIC # ← PLIC 中断控制器驱动
select FDT_SERIAL_NS16550 # ← NS16550 串口驱动
select FDT_TIMER_MTIMER # ← CLINT mtime 定时器
select FDT_IPI_MSWI # ← CLINT msip 核间中断
select FDT_PMU # ← 性能计数器
default y
OpenSBI 和 Linux 内核一样用 Kconfig。select FDT_DOMAIN 的含义是"编译时把 domain 框架链接进来",这样 OpenSBI 启动时才能解析 twinv_sbi.dts 里的 opensbi-domains 节点。
5.4.2 objects.mk — 编译规则
platform-objs-y += platform.o
5.4.3 defconfig — 默认配置
FW_JUMP=y
FW_JUMP_ADDR=0xB0100000 ← 默认跳 U-Boot
PLATFORM_RISCV_XLEN=64
PLATFORM_RISCV_ISA=rv64imafdc
这个 FW_JUMP_ADDR 是 Linux SBI 的默认值。编译 RTOS SBI 时命令行 FW_JUMP_ADDR=0xB0C00000 会覆盖它。
5.4.4 platform.c — 核心逻辑
// ① 早期初始化: OpenSBI 启动时第一个调用的函数
unsigned long fw_platform_init(...) {
void *fdt = (void *)arg1; // arg1 = DTB 地址 (LowLevelBoot 传的)
// 读 /cpus 节点, 遍历所有 cpu@N, 收集 hart ID
fdt_for_each_subnode(cpu_offset, fdt, cpus_offset) {
fdt_parse_hart_id(fdt, cpu_offset, &hartid);
twinv_hart_index2id[hart_count++] = hartid; // 建立索引→hartID 映射
}
platform.hart_count = hart_count; // 告诉 OpenSBI 有 8 个核
return arg1; // 原样返回 DTB 指针
}
// ② 最终初始化: 冷启动时调用, 做 FDT fixup
static int twinv_final_init(bool cold_boot) {
fdt = sbi_scratch_thishart_arg1_ptr();
fdt_cpu_fixup(fdt); // 修正 CPU 节点
fdt_fixups(fdt); // 通用修正 (plic, clint 地址等)
fdt_domain_fixup(fdt); // ★ 根据 DTS 里的 opensbi-domains 配置 PMP!
return 0;
}
// ③ Domain 初始化: 解析 DTS, 设置 PMP 寄存器
static int twinv_domains_init(void) {
return fdt_domains_populate(fdt_get_address());
// 内部流程:
// 1. 解析 twinv_sbi.dts 里的 opensbi-domains 节点
// 2. 创建 tdomain (hart7) 和 udomain (hart0-6)
// 3. 根据 regions-map 配置 PMP 寄存器
// 4. 让 tmem0/tmem1 对 udomain 不可访问 (权限=0x0)
}
// ④ 操作表: 所有回调函数注册
const struct sbi_platform_operations platform_ops = {
.early_init = twinv_early_init, // 早期初始化
.final_init = twinv_final_init, // 最终初始化 (做 DT fixup)
.domains_init = twinv_domains_init, // Domain/PMP 初始化
.timer_init = fdt_timer_init, // 从 DTS 读 timer, 自动初始化
.irqchip_init = fdt_irqchip_init, // 从 DTS 读 PLIC, 自动初始化
...
};
5.5 核心流程
OpenSBI 启动
│
├─ fw_platform_init() ← 读 DTS, 收集 hart 信息
│
├─ twinv_early_init() ← 早期初始化 (空)
│
├─ fdt_timer_init() ← 从 DTS 找到 CLINT, 初始化定时器
├─ fdt_irqchip_init() ← 从 DTS 找到 PLIC, 初始化中断
├─ fdt_serial_init() ← 从 DTS 找到 UART0, 初始化串口
│
├─ twinv_final_init() ← DT fixup (修正节点)
│ └─ fdt_domain_fixup() ← 根据 DTS 的 PMP 配置, 写入硬件
│
├─ twinv_domains_init() ← 创建 domain, 配置 PMP 寄存器
│ └─ fdt_domains_populate()
│
└─ 跳到 next-addr ← hart0-6 → 0xB0100000 (U-Boot)
← hart7 → 0xB0C00000 (FreeRTOS)
5.6 要点
| 问题 | 答案 |
|---|---|
| OpenSBI 跑在什么特权级? | M-mode,最高权限 |
| 为什么 Linux 不自己写 mtimecmp? | S-mode 无权写 M-mode CSR,必须通过 ecall 委托 OpenSBI |
| fw_jump vs fw_dynamic 区别? | fw_jump 编译时写死跳转地址,fw_dynamic 运行时从上一级固件接收 |
| 为什么用双 OpenSBI 而不是单实例? | AMP 架构下两个域要跳不同地址,单实例无法同时跳 U-Boot 和 FreeRTOS |
| PMP 隔离是在哪里配置的? | twinv_domains_init () → fdt_domains_populate () → 写 PMP CSR |
| DTS 里 regions-map 的 0x7/0x0 最终变成什么? | 变成 PMP 寄存器的 R/W/X 位,M-mode 硬件拦截违规访问 |
第六章:U-Boot — Linux 的"接生婆"
U-Boot 的工作是:初始化硬件 → 从磁盘/Flash 找到 Linux 内核 → 加载到内存 → 跳过去。它在 OpenSBI 之后运行,跑在 S-mode。
OpenSBI (M-mode)
│
│ 初始化完 → 切 S-mode → 跳 next-addr
│
▼
U-Boot (S-mode) ← 我们现在在这里
│
│ virtio_init() → 扫描 VirtIO 设备,找到磁盘
│ load virtio 0:1 → 从磁盘把 Linux Image 读到 0x80400000
│ load virtio 0:1 → 从磁盘把 DTB 读到 0x82800000
│ booti → 跳到 Linux!
│
▼
Linux Kernel (S-mode)
extern/u-boot/
├── board/emulation/qemu-twinv/
│ ├── Kconfig ← 注册 board,声明依赖
│ └── qemu-twinv.c ← board_init() — 就 27 行
├── include/configs/
│ └── qemu-twinv.h ← 配置头文件 — bootcmd 在这里
├── configs/
│ └── qemu-twinv_smode_defconfig ← U-Boot 配置
└── arch/riscv/
├── Kconfig ← 加了 TARGET_QEMU_TWINV 入口
└── dts/twinv.dts ← U-Boot 自己的设备树
6.1逐文件讲解
6.1.1 arch/riscv/Kconfig — 注册新板子
config TARGET_QEMU_TWINV ← 新增的配置项
bool "Support QEMU TwinV-RV Platform"
source "board/emulation/qemu-twinv/Kconfig" ← 引入子目录的 Kconfig
6.1.2 board Kconfig — 板的编译配置
config SYS_BOARD → "qemu-twinv" ← 板子名称,对应目录名
config SYS_CONFIG_NAME → "qemu-twinv" ← 配置头文件名 (qemu-twinv.h)
config TEXT_BASE → 0xB0100000 ← ★ U-Boot 的运行地址!
config BOARD_SPECIFIC_OPTIONS
select SYS_NS16550 ← 串口驱动
select VIRTIO_MMIO ← VirtIO-MMIO 传输
select VIRTIO_BLK ← VirtIO 块设备 (磁盘)
select VIRTIO_NET ← VirtIO 网络
TEXT_BASE = 0xB0100000 就是 LowLevelBoot 把 U-Boot 搬到的地方。OpenSBI 跳 0xB0100000 时,U-Boot 的第一条指令就在那里。
6.1.3 qemu-twinv.c — board 初始化(只有 27 行)
phys_size_t get_effective_memsize(void)
{
return 64 * 1024 * 1024; // ★ 告诉 U-Boot: 只用 64MB!
}
int board_init(void)
{
virtio_init(); // 扫描 VirtIO-MMIO 总线, 找到磁盘/网卡
return 0;
}
为什么 get_effective_memsize 返回 64MB?
U-Boot 启动后会把自己"重定位"到内存顶端。如果告诉它 1GB,它会把自己搬到 1GB 附近。但我们现在只给了 U-Boot 64MB 的视野(安全起见,防止它踩到 FreeRTOS 区域),所以返回 64MB。
要点:这个 64MB 只影响 U-Boot 自己的运行,不影响 Linux——Linux 用 DTS 里的 memory 节点,不受此限制。
6.1.4 qemu-twinv.h — 配置头文件(bootcmd 在这里)
#define CFG_SYS_SDRAM_BASE 0x80000000UL // DDR 起始地址
#define RISCV_MMODE_TIMERBASE 0x2000000 // CLINT 地址
#define RISCV_SMODE_TIMER_FREQ 1000000 // 定时器频率 1MHz
// ★ 环境变量 — 启动脚本!
#define CFG_EXTRA_ENV_SETTINGS
"kernel_addr_r=0x80400000\0" // Linux Image 加载到哪
"fdt_addr_r=0x82800000\0" // DTB 加载到哪
"bootcmd=load virtio 0:1 ${kernel_addr_r} /Image && " // ★ 自动执行!
"load virtio 0:1 ${fdt_addr_r} /twinv.dtb && "
"booti ${kernel_addr_r} - ${fdt_addr_r}\0"
bootcmd 是 U-Boot 启动后自动执行的命令:
load virtio 0:1 0x80400000 /Image ← 从第一个 VirtIO 磁盘读 /Image 到 0x80400000
load virtio 0:1 0x82800000 /twinv.dtb ← 从第一个 VirtIO 磁盘读 /twinv.dtb 到 0x82800000
booti 0x80400000 - 0x82800000 ← 以 Image 格式启动 (地址: Image, -, DTB)
这就是为什么 README 里说在 U-Boot 提示符下手动加载内核——但有了这个 bootcmd,实际上 U-Boot 会自动执行,倒计时结束就启动 Linux。
6.1.5 qemu-twinv_smode_defconfig — 编译配置
关键几项:
CONFIG_TARGET_QEMU_TWINV=y ← 选中我们的板子
CONFIG_RISCV_SMODE=y ← ★ S-mode U-Boot (不是 M-mode!)
CONFIG_SYS_LOAD_ADDR=0x80200000 ← 默认加载地址
CONFIG_SYS_FLASH_BASE=0x20000000 ← pflash 基址
CONFIG_VIRTIO_MMIO=y ← VirtIO-MMIO 传输
CONFIG_VIRTIO_BLK=y ← VirtIO 块设备支持
CONFIG_DEFAULT_DEVICE_TREE="twinv" ← 内置 DTS 名称
RISCV_SMODE=y 是关键——告诉 U-Boot"你跑在 S-mode"。U-Boot 刚从 OpenSBI 接手时已经处于 S-mode,它就不需要再做 M-mode 的初始化,直接使用 OpenSBI 提供的 SBI 服务(定时器、IPI 等)。
6.2 启动流程回顾
hart0:
MROM reset_vec → LowLevelBoot → Linux OpenSBI → U-Boot → Linux
↑
bootcmd 自动执行:
1. virtio_init() 扫磁盘
2. load virtio 0:1 ... /Image
3. load virtio 0:1 ... /twinv.dtb
4. booti → 跳到 Linux
6.3 要点
| 问题 | 答案 |
|---|---|
| U-Boot 跑在什么特权级? | S-mode(RISCV_SMODE=y) |
| U-Boot 怎么加载 Linux? | 从 VirtIO 块设备读 /Image 到内存,booti 跳转 |
| bootcmd 是什么? | U-Boot 自动执行的启动脚本,存在环境变量里 |
| get_effective_memsize 为什么返回 64MB? | 限制 U-Boot 自身的可见内存范围,防止重定位时踩到 RTOS 区 |
| U-Boot 和 OpenSBI 的关系? | OpenSBI 是 M-mode 运行时,U-Boot 是 S-mode 引导器。U-Boot 的定时器 / 串口通过 SBI ecall 使用 OpenSBI 服务 |
| TEXT_BASE 为什么是 0xB0100000? | U-Boot 编译时写死运行地址,LowLevelBoot 把它搬到这个地址,OpenSBI 跳到这个地址 |
第七章:Linux 内核 — 从 U-Boot 接手到 Shell 提示符
7.1 Linux 是怎么被拉起来的
交接过程(U-Boot → Linux)
U-Boot 执行 bootcmd:
load virtio 0:1 0x80400000 /Image ← 内核镜像放 0x80400000
load virtio 0:1 0x82800000 /twinv.dtb ← 设备树放 0x82800000
booti 0x80400000 - 0x82800000 ← 跳!
booti 做了什么:
1. 把 DTB 地址写入 a1 寄存器
2. 把 a0 写为当前 hart ID
3. 禁用 MMU
4. 跳转到 0x80400000 (Linux 的 _start)
Linux 内核启动流程(RISC-V 版):
_start (arch/riscv/kernel/head.S)
│
├─ 每个 hart 独立进入, 只有 hart0 走全流程
│
├─ setup_vm() ← 建立临时页表 (identity mapping)
├─ mmu_on() ← 开启 MMU!
├─ start_kernel() ← C 语言入口 (init/main.c)
│ ├─ setup_arch() ← 解析 DTB, 发现 CPU/内存/外设
│ ├─ init_IRQ() ← 初始化中断 (PLIC via DTB)
│ ├─ time_init() ← 初始化定时器 (CLINT via SBI)
│ ├─ rest_init() ← 创建 init 进程
│ │ └─ kernel_init()
│ │ └─ 尝试执行 /init (BusyBox shell!)
│ │
│ └─ cpu_startup_entry() ← idle 循环
│
└─ ~ # ← 你看到的 shell 提示符!
关键:Linux 怎么知道硬件在哪?
U-Boot → a1 寄存器 → DTB 地址 0x82800000
│
▼
setup_arch() 读 DTB:
/cpus/cpu@0 → 注册 CPU 0-6 (cpu7 被 disabled, 跳过!)
/memory@80000000 → 物理内存: 0x80000000, 1016MB
/serial@10000000 → compatible="ns16550a" → 加载 8250 串口驱动
/plic@c000000 → compatible="sifive,plic-1.0.0" → 加载 PLIC 驱动
/virtio_mmio@10100000 → compatible="virtio,mmio" → 扫描 VirtIO 设备
7.2 我们改了哪些 Linux 配置
基于 defconfig,额外启用的关键选项:
| 配置 | 作用 |
|---|---|
| CONFIG_VIRTIO_BLK=y | VirtIO 块设备驱动 → 能找到 /dev/vda 磁盘 |
| CONFIG_VIRTIO_MMIO=y | VirtIO-MMIO 传输层 → 能扫描 8 个 VirtIO 槽位 |
| CONFIG_VIRTIO_NET=y | VirtIO 网卡 → 后续可以加网络 |
| CONFIG_DEVTMPFS=y | 自动创建 /dev/ 下的设备节点 |
| CONFIG_EXT4_FS=y | ext4 文件系统支持 |
| CONFIG_NET=y | 网络栈 |
| CONFIG_PACKET=y | AF_PACKET socket |
| CONFIG_UNIX=y | Unix domain socket |
| CONFIG_INET=y | TCP/IP |
这些在 build.sh 里用 scripts/config --enable 自动设好。
7.3 根文件系统 — Linux 的"家"
Linux 启动后第一个做的事:挂载根文件系统,执行 /init。
我们用了 initramfs(内存里的根文件系统):
output/rootfs/
├── init ← ★ Linux 启动的第一个用户态程序
├── bin/
│ └── busybox ← sh, ls, cat, echo... 全在这里
├── sbin/
├── dev/ ← udev/devtmpfs 自动填充
├── etc/
│ ├── inittab ← BusyBox init 配置
│ └── init.d/rcS ← 启动脚本 (挂载 /proc, /sys, /dev)
├── proc/
├── sys/
├── tmp/
├── modules/
│ └── amp_mailbox.ko ← ★ 我们的 IPC 驱动模块!
└── lib/ ← C 运行时库
/init 脚本:
#!/bin/sh
mount -t proc proc /proc ← 挂载 proc (ps, cpuinfo 依赖它)
mount -t sysfs sysfs /sys ← 挂载 sysfs (设备信息)
mount -t devtmpfs devtmpfs /dev ← 挂载 devtmpfs (自动创建设备节点)
insmod /modules/amp_mailbox.ko ← ★ 加载 IPC 驱动!
cat /dev/twinv_mbox & ← 后台读 FreeRTOS 发来的心跳
echo "Hello-RTOS" > /dev/twinv_mbox ← 发消息给 FreeRTOS
exec /bin/sh ← 启动 shell
7.4 AMP 视角:Linux 怎么看这个系统
Linux 视角 (hart 0-6):
┌────────────────────────────────────────┐
│ CPU: 7 个核 (cpu7=disabled, 看不到) │
│ RAM: 1016MB (FreeRTOS 的 8MB 被挖掉) │
│ 中断: PLIC 管理, 通过 SBI 接收 │
│ 串口: UART0 (UART1/2 不用) │
│ 磁盘: /dev/vda (VirtIO) │
│ IPC: /dev/twinv_mbox ← 读写共享内存 │
│ │
│ ★ Linux 完全不知道 hart7 在并行运行! │
└────────────────────────────────────────┘
7.5 要点
| 问题 | 答案 |
|---|---|
| Linux 怎么知道 CPU 有几个? | 解析 DTB 的 /cpus/cpu@N 节点,status != "disabled" 的才算 |
| 为什么 Linux 看不到 hart7? | twinv_uboot.dts 里 &cpu7 |
| Linux 物理内存为什么少了 8MB? | memory reg 被缩小到 0x3F800000,挖掉了 RTOS 独占区 |
| initramfs vs ext4 rootfs? | initramfs 在内存里,简单快速;ext4 镜像在磁盘上,持久化。我们两套都支持 |
| 内核模块怎么加载? | insmod /modules/amp_mailbox.ko,Linux 调用模块的 module_init () |
| 用户态怎么用 IPC 驱动? | cat /dev/twinv_mbox 读,echo "msg" > /dev/twinv_mbox 写 |
第八章:FreeRTOS S-mode 移植
8.1 架构全景
┌─────────────────────────────────────────┐
│ FreeRTOS 内核 (hart7, S-mode) │
│ ├─ tasks.c (调度器) │
│ ├─ port.c (定时器/中断 — 改了) │
│ ├─ portASM.S (上下文切换 — 大改) │
│ ├─ sbi.c (ecall 封装 — 自写) │
│ ├─ ipc.c (IPC 驱动 — 自写) │
│ └─ main.c (应用 — 自写) │
│ │
│ 定时器: ecall → OpenSBI │
│ 中断: OpenSBI 转发 → S-mode trap │
│ 串口: 直接写 MMIO (UART0) │
│ IPC: 直接读写共享内存 0xB0800000 │
└─────────────────────────────────────────┘
修改涉及的文件:
| 文件 | 类型 | 改了什么 |
|---|---|---|
| portASM.S | 核心修改 | CSR 全替换、位域值修正、中断号修正 |
| port.c | 核心修改 | sie 替代 mie、vPortSetupTimerInterrupt 空实现 |
| chip_specific_extensions.h | 新建 | portasmHAS_MTIME=0,空宏 |
| startup.S | 自写 | S-mode 入口、.data/.bss 初始化 |
| main.c | 自写 | FreeRTOS 11.x 钩子、心跳任务 |
| sbi.c | 自写 | SBI ecall 封装 |
| riscv_encoding.h | 自写 | CSR 位域定义 |
| FreeRTOSConfig.h | 自写 | configMTIME=0,静态分配 |
| link.lds | 自写 | 单区链接脚本 |
8.2 6 个关键修改(PORTING.md 第 13-18 号问题)
8.2.1 所有 M-mode CSR → S-mode CSR
为什么? S-mode 下访问 M-mode CSR(如 mstatus、mcause、mepc)会触发 illegal instruction 异常。
portASM.S 里的替换:
mstatus → sstatus (状态寄存器)
mcause → scause (陷阱原因)
mepc → sepc (异常返回地址)
mtvec → stvec (陷阱向量表基址)
mie → sie (中断使能)
mip → sip (中断待处理)
mret → sret (从陷阱返回)
8.2.2 位域值修正 — 0x188 → 0x120
// 原版 M-mode:
// andi t0, t0, ~0x8 ← 清除 MIE (bit3 in mstatus)
// addi t1, x0, 0x188 ← MPIE=bit7, MPP=bits[12:11]
// 我们 S-mode:
// andi t0, t0, ~0x2 ← 清除 SIE (bit1 in sstatus)
// addi t1, x0, 0x120 ← SPIE=bit5, SPP=bit8
为什么值不一样? RISC-V 规范里 M-mode 和 S-mode 的 status 寄存器布局不同——相同功能的 bit 在不同位置。这是最容易出错的细节。
8.2.3 中断号修正 — 7 → 5
// 原版: MTI (Machine Timer Interrupt) = cause 7
// 改成: STI (Supervisor Timer Interrupt) = cause 5
// portASM.S trap handler:
// addi t1, t0, 5 ← 原来是 7, 改成 S-mode 的定时器中断号 5
// bne a0, t1, ...
8.2.4 vPortSetupTimerInterrupt NULL 崩溃
// 问题: 该函数声明为 weak, configMTIME=0 时没有被覆盖,
// 链接器把它解析为 NULL 地址 → 一调用就崩溃
// 解决: 在 #else 分支加一个强定义的空实现
#else
void vPortSetupTimerInterrupt( void ) { /* no-op for SBI timer */ }
#endif
我们用 SBI ecall 设定时器,不需要写 MTIME 寄存器——所以这个函数空着就行。
8.2.5 chip_specific_extensions.h — 新建
#define portasmHAS_MTIME 0 // ★ 禁用 MTIME 直写, 走 SBI 路径
#define portasmADDITIONAL_CONTEXT_SIZE 0 // 无芯片特有寄存器
这个文件是 FreeRTOS RISC-V 端口的"芯片适配层"。
8.2.6 FreeRTOS 11.x 静态分配要求
// FreeRTOS 11.x 要求应用提供 Idle 和 Timer 任务的栈内存
// 如果不存在这些函数 → 链接失败
StaticTask_t xIdleTCB, xTimerTCB;
StackType_t uxIdleStack[512], uxTimerStack[256];
void vApplicationGetIdleTaskMemory(...) { ... }
void vApplicationGetTimerTaskMemory(...) { ... }
char __freertos_irq_stack_top[2048]; // 中断栈
8.3 自写代码详解
startup.S — S-mode 入口
_start:
li t0, '@'
sb t1, 0(t0) // ★ 调试标记: 串口输出 '@' 证明启动了
li a1, PRIM_HART // PRIM_HART = 7
bne a0, a1, secondary // 不是 hart7 → 进入 WFI 休眠
csrw sie, 0 // 关 S-mode 中断
csrw sip, 0 // 清中断标志
// 拷贝 .data 段: ROM → RAM
// 清零 .bss 段
// 跳转 main()
sbi.c — 让 FreeRTOS 能呼叫 OpenSBI
// 设定时器: FreeRTOS 想要 1ms 后中断
void sbi_set_timer(uint64_t stime_value) {
sbi_ecall(SBI_EXT_TIME, SBI_TIME_SET_TIMER, stime_value, ...);
// → ecall → M-mode → OpenSBI 帮你写 mtimecmp
}
// 发 IPI: Linux 通知 FreeRTOS "有消息"
void sbi_send_ipi(unsigned long hart_mask, unsigned long hart_mask_base) {
sbi_ecall(SBI_EXT_IPI, SBI_IPI_SEND_IPI, hart_mask, ...);
}
main.c — FreeRTOS 应用
int main(void) {
uart_puts("[TwinV-RV] FreeRTOS v11.2 on hart 7\n");
ipc_init(); // 初始化共享内存
xTaskCreate(vHeartbeat, "Heartbeat", ...); // 创建心跳任务
vTaskStartScheduler(); // 启动调度器!
}
心跳任务做的事:每秒递增计数器 → 打印到 UART → 通过 ipc_send() 发给 Linux → 检查 Linux 有没有发消息过来。
FreeRTOSConfig.h — 关键配置
#define configCPU_CLOCK_HZ 10000000UL // 10MHz (CLINT 频率)
#define configTICK_RATE_HZ 1000 // tick 频率 1000Hz → 1ms 一个 tick
#define configMTIME_BASE_ADDRESS 0 // ★ 不用 MTIME 直写, 用 SBI
#define configMTIMECMP_BASE_ADDRESS 0 // ★ 同理
#define configSUPPORT_STATIC_ALLOCATION 1 // ★ 静态分配 (11.x 要求)
8.4 要点
| 问题 | 答案 |
|---|---|
| 为什么 FreeRTOS 跑在 S-mode? | M-mode 已被 OpenSBI 占用;PMP 隔离需要 FreeRTOS 处在 OpenSBI 管辖范围内 |
| M→S 移植改了什么? | CSR 名字 (mstatus→sstatus)、位域值 (~0x8 →~0x2)、中断号 (7→5)、返回指令 (mret→sret) |
| 定时器怎么工作? | FreeRTOS 调 sbi_set_timer () → ecall → OpenSBI 写 mtimecmp |
| 为什么 configMTIME=0? | 我们不直接写硬件 MTIME 寄存器,通过 SBI ecall 让 OpenSBI 代劳 |
| 0x188→0x120 是什么? | M-mode 的 MPIE/MPP 位域在 sstatus 中变成了 SPIE/SPP,物理位置不同 |
| FreeRTOS 11.x 为什么需要静态内存钩子? | 11.x 开始要求应用显式提供 Idle/Timer task 的 TCB 和栈,提高灵活性 |
第九章:IPC 核间通信
9.1 物理链路层:它们怎么连在一起?
hart 0-6 (Linux) hart 7 (FreeRTOS)
┌─────────────────┐ ┌─────────────────┐
│ 用户态: │ │ 心跳任务: │
│ echo "hi" > │ │ ipc_send("HB=X") │
│ /dev/twinv_mbox│ │ ipc_recv() │
├─────────────────┤ ├─────────────────┤
│ 内核态: │ │ S-mode 裸机: │
│ amp_mailbox.ko │ │ ipc.c │
│ ├─ 写 L2R 缓冲区│ │ ├─ 写 R2L 缓冲区│
│ └─ 读 R2L 缓冲区│ │ └─ 读 L2R 缓冲区│
├─────────────────┤ ├─────────────────┤
│ S-mode (Linux) │ │ S-mode (FreeRTOS)│
│ ↓ ecall │ │ ↓ ecall │
├─────────────────┤ ├─────────────────┤
│ M-mode (OpenSBI) │ ←──────── │ M-mode (OpenSBI) │
│ 收到 IPI 请求 → 写 msip │ 收到 IPI 中断 │
└─────────────────┘ └─────────────────┘
│ 共享内存 │
└──── 0xB0800000 ──────┘
三条并行的数据通路:
| 通路 | 方向 | 机制 |
|---|---|---|
| 数据 | 双向 | 共享内存 0xB0800000,无需经过 OpenSBI |
| 通知 | Linux → RTOS | SBI ecall → OpenSBI 写 CLINT msip → hart7 收中断 |
| 通知 | RTOS → Linux | (TODO) 同样走 SBI IPI |
9.2 共享内存布局(协议层)
0xB0800000 ┌────────────────────────────┐
│ Header (64 字节) │
│ magic: 0x5456494E │ ← "TVIN"
│ version: 1 │
│ l2r_write: 0 │ ← Linux→RTOS 写指针
│ l2r_read: 0 │ ← RTOS 读指针 (Linux 只读)
│ r2l_write: 0 │ ← RTOS→Linux 写指针
│ r2l_read: 0 │ ← Linux 读指针 (RTOS 只读)
│ reserved[10] │ ← 对齐到 64B
0xB0800040 ├────────────────────────────┤
│ L2R 环形缓冲区 │
│ [4B长度+数据][4B长度+数据]..│
0xB0810040 ├────────────────────────────┤
│ R2L 环形缓冲区 │
│ [4B长度+数据][4B长度+数据]..│
0xB0820040 └────────────────────────────┘
协议规则:
每条消息格式: [4字节长度 (little-endian)] [数据 (变长)]
写入过程:
1. 检查空间: avail = (read - write - 1) & (ring_size - 1)
★ 留 1 字节空隙,防止 write==read 时分不清空满
2. 写长度: buf[write+0] = len & 0xFF
buf[write+1] = (len>>8) & 0xFF
...
3. 写数据: buf[write+4+i] = data[i]
4. 更新 write = (write + 4 + len) & (ring_size - 1)
5. 发 IPI 通知对方
读取过程:
1. read == write → 空,没有消息
2. 读长度: len = buf[read+0] | buf[read+1]<<8 | ...
3. 读数据: data[i] = buf[read+4+i]
4. 更新 read = (read + 4 + len) & (ring_size - 1)
为什么指针由各自独占写?
l2r_write: 只由 Linux 写 (RTOS 只读) → 无竞争
l2r_read: 只由 FreeRTOS 写 (Linux 只读) → 无竞争
r2l_write: 只由 FreeRTOS 写
r2l_read: 只由 Linux 写
单生产者+单消费者 → 不需要锁。
9.3 完整数据流(以 Linux→RTOS 为例)
步骤1: Linux 用户态
echo "Hello-RTOS" > /dev/twinv_mbox
│
步骤2: Linux 内核 (amp_mailbox.ko)
amp_mbox_write()
├─ copy_from_user("Hello-RTOS", 11)
├─ 写 4 字节长度头: 0x0B 0x00 0x00 0x00
├─ 写 11 字节数据: "Hello-RTOS"
├─ iowrite32 更新 l2r_write 指针
└─ send_ipi_to_hart7()
│
步骤3: SBI ecall (M-mode)
ecall(a7=SBI_EXT_IPI, a0=1<<7)
→ OpenSBI IPI handler
→ 写 hart7 的 CLINT msip 寄存器
│
步骤4: FreeRTOS 接收中断
hart7 的 stvec → freertos_risc_v_trap_handler()
→ 识别为 supervisor timer/software 中断
→ 最终 vHeartbeat 任务被唤醒
│
步骤5: FreeRTOS 读消息
ipc_recv(buf, 128)
├─ 读 l2r_read 和 l2r_write
├─ 不相等 → 有数据!
├─ 读 4 字节长度: 11
├─ 读 11 字节: "Hello-RTOS"
├─ 更新 l2r_read
└─ 返回 "Hello-RTOS"
│
步骤6: FreeRTOS 应用层
uart_puts(" RX:Hello-RTOS\n")
→ 串口输出: "[RTOS] HB=XXXXXXXX TX RX:Hello-RTOS"
9.4 Linux 侧 vs FreeRTOS 侧的对称实现
Linux (amp_mailbox.c) FreeRTOS (ipc.c)
───────────────────── ─────────────────
ioremap(0xB0800000, 4MB) 直接指针 (裸机无 MMU)
iowrite32() / writeb() 直接 *ptr = val
ioread32() / readb() 直接 val = *ptr
misc_register() → /dev/twinv_mbox 无设备模型 (裸机)
copy_from_user() → 用户空间传参 直接调用
send_ipi_to_hart7() (ecall) ipc_send 后无 IPI (TODO)
9.5 要点
| 问题 | 答案 |
|---|---|
| Linux 和 FreeRTOS 怎么通信? | 共享内存 (0xB0800000) + 环形缓冲区 + SBI IPI 通知 |
| 为什么不需要锁? | 单生产者单消费者:write 只由写方写,read 只由读方写,无竞争 |
| 怎么防止环形缓冲满了覆盖? | avail = (r - w - 1) & mask,留 1 字节空隙区分空 / 满 |
| Linux 怎么知道共享内存地址? | amp_mailbox.c 硬编码 SHMEM_PHYS = 0xB0800000ULL |
| FreeRTOS 怎么知道的? | ipc.c 硬编码 IPC_BASE = 0xB0800000ULL(裸机无地址翻译) |
| IPI 中断路径是什么? | Linux ecall → OpenSBI → 写 CLINT msip → hart7 trap → FreeRTOS 中断处理 |
| 消息有最大长度吗? | 取决于环形缓冲区空间。ipc_recv (buf, 128) 限制最大 128 字节 |
第十章:构建系统 — 把所有东西拼在一起
10.1 整体构建流程
./build.sh all
│
┌──────────────┼──────────────┐
▼ ▼ ▼
① 工具链 ② 源码下载+打补丁 ③ 交叉编译
│
┌──────────┼──────────┬──────────┬──────────┐
▼ ▼ ▼ ▼ ▼
QEMU OpenSBI U-Boot Linux FreeRTOS
twinv机器 ×2双实例 qemu-twinv 内核+模块 trusted_fw
│
▼
④ 固件打包
pflash.img (32MB)
│
▼
⑤ ./run.sh
10.2 两大工具链
Linux 工具链 (系统包安装): 裸机工具链 (xPack 自动下载):
riscv64-linux-gnu-gcc riscv-none-elf-gcc
├─ 有 Linux 系统调用 ├─ 无 OS (bare-metal)
├─ 有 glibc 运行时 ├─ 有 newlib (简化版 libc)
├─ 链接到 Linux 的 ELF ├─ 链接成裸机 ELF / 二进制
│ │
用于: 用于:
├─ OpenSBI ├─ LowLevelBoot (汇编固件)
├─ U-Boot └─ FreeRTOS (trusted_fw)
└─ Linux 内核 + BusyBox
为什么需要两套工具链? 因为 Linux 程序调用 write()/mmap() 等系统调用,需要链接 glibc;LowLevelBoot/FreeRTOS 跑在裸机/S-mode,没有 Linux 系统调用,只能用 newlib 或完全不用 libc。
10.3 config.mk — 中央配置
# 项目路径
PROJECT_ROOT := $(dir $(lastword $(MAKEFILE_LIST))) # Makefile 所在目录
# 两个工具链
CROSS_COMPILE_LINUX ?= riscv64-linux-gnu- # 有 OS 的
CROSS_COMPILE_ELF ?= riscv-none-elf- # 裸机的
# 版本号——改一处全局生效
QEMU_VER ?= 11.0.0
LINUX_VER ?= 6.12.0
OPENSBI_VER ?= 1.6.0 # (实际用了 1.9)
FREERTOS_VER ?= 11.2.0
之前有个坑 (PORTING.md 没写但对话里有):PROJECT_ROOT 定义在 ELF_TOOLCHAIN_DIR 之后——后者用了 $(PROJECT_ROOT),结果展开成空字符串。改了顺序才修好。
10.4 各组件构建命令
回顾一下每个组件的关键构建参数:
┌─────────────────────────────────────────────────────────────┐
│ LowLevelBoot │
│ make -C src/lowlevelboot │
│ 工具链: riscv-none-elf-gcc │
│ ARCH: rv64imafdc_zifencei ABI: lp64d │
│ 模型: medany (代码可在任意 64-bit 地址运行) │
│ 链接: link.lds → 代码在 0x20000000 (pflash) │
├─────────────────────────────────────────────────────────────┤
│ Linux OpenSBI │
│ make -C extern/opensbi PLATFORM=twinv │
│ CROSS_COMPILE=riscv64-linux-gnu- │
│ FW_JUMP=y FW_JUMP_ADDR=0xB0100000 ← 跳到 U-Boot │
├─────────────────────────────────────────────────────────────┤
│ RTOS OpenSBI │
│ 同源码, 改 FW_JUMP_ADDR=0xB0C00000 ← 跳到 FreeRTOS │
├─────────────────────────────────────────────────────────────┤
│ U-Boot │
│ make -C extern/u-boot qemu-twinv_smode_defconfig │
│ make -C extern/u-boot CROSS_COMPILE=riscv64-linux-gnu- │
│ TEXT_BASE=0xB0100000 │
├─────────────────────────────────────────────────────────────┤
│ Linux Kernel │
│ make -C extern/linux ARCH=riscv │
│ CROSS_COMPILE=riscv64-linux-gnu- defconfig │
│ + 启用 VIRTIO_BLK, VIRTIO_MMIO, DEVTMPFS, EXT4 等 │
│ → Image (原始内核镜像, 非 vmlinux) │
├─────────────────────────────────────────────────────────────┤
│ BusyBox │
│ make -C extern/busybox ARCH=riscv │
│ CROSS_COMPILE=riscv64-linux-gnu- defconfig │
│ + CONFIG_STATIC=y (静态链接, 不依赖 libc.so) │
│ → _install → output/rootfs │
├─────────────────────────────────────────────────────────────┤
│ FreeRTOS (trusted_fw) │
│ make -C src/trusted_domain │
│ 工具链: riscv-none-elf-gcc │
│ ARCH: rv64imafdc ABI: lp64d │
│ 链接: link.lds → 代码在 0xB0C00000 │
├─────────────────────────────────────────────────────────────┤
│ amp_mailbox.ko │
│ make -C extern/linux M=src/driver/amp_mailbox │
│ ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- modules │
└─────────────────────────────────────────────────────────────┘
10.5 固件打包 — pflash.img 的布局
这是最关键的一步。build_firmware() 组装出 QEMU 启动要用的文件:
pflash.img (32MB) 布局:
Offset 内容 由谁加载
──────────────────────────────────────────────────
0x00000000 lowlevelboot.bin QEMU 直接执行 (via MROM jr)
0x00040000 twinv_sbi.dtb LowLevelBoot → DDR 0xB0000000
0x00060000 twinv_uboot.dtb LowLevelBoot → DDR 0xB0008000
0x00080000 linux_sbi.bin LowLevelBoot → DDR 0xAFF00000
0x00100000 trusted_fw.bin LowLevelBoot → DDR 0xB0C00000
0x00200000 rtos_sbi.bin LowLevelBoot → DDR 0xAFF80000
0x00280000 u-boot.bin LowLevelBoot → DDR 0xB0100000
──────────────────────────────────────────────────
剩余空间填 0xFF (NOR Flash 擦除态)
对应 LowLevelBoot startup.S 里的 6 次 copy_fw 调用。pflash 里的偏移量必须和 startup.S 里定义的宏完全一致,否则搬到 DDR 后全是乱码。
10.6 根文件系统 — initramfs.cpio.gz
output/rootfs/
├── init ← Linux 启动的第一个用户态程序
├── bin/
│ └── busybox (静态链接, ~2MB)
├── modules/
│ └── amp_mailbox.ko
└── etc/
├── inittab
└── init.d/rcS
打包: find . | cpio -o | gzip → initramfs.cpio.gz
QEMU: -initrd output/initramfs.cpio.gz
initramfs 比 ext4 简单——不需要磁盘驱动、不需要文件系统挂载、不需要分区表。内核启动时直接把 cpio 包解压到内存。
10.7 要点
| 问题 | 答案 |
|---|---|
| 为什么用两套工具链? | Linux 程序需要 glibc + 系统调用,裸机程序只需要 newlib |
| -mcmodel=medany 什么意思? | 代码可以在任意 64-bit 地址运行,不限于 ±2GB。用于 DDR 高地址 |
| TEXT_BASE 是什么? | U-Boot 编译时写死的运行地址。链接器把所有符号偏移基于此 |
| 为什么 pflash 偏移量不能随便改? | 改了必须同步改 startup.S 的 copy_fw 宏和 DTS 的 next-addr |
| initramfs vs ext4 rootfs 区别? | initramfs 在内存里 (快,不持久化),ext4 在磁盘上 (慢,可保存数据) |
| FW_JUMP_ADDR 指什么? | OpenSBI 初始化完成后跳到的地址 —— 双实例靠改它实现 AMP 路由 |
第十一章:开源组件的处理
11.1 QEMU — 注入一个新机器类型
我们没有修改 QEMU 的任何现有代码。
我们是"加了一个新的机器型号"。
QEMU 源码 我们做的事
───────── ──────────
hw/riscv/virt.c ← 已有的 patches/qemu/twinv.c ← 我们新写的
hw/riscv/Kconfig ← 已有的 追加了 config TWINV
hw/riscv/meson.build ← 已有的 追加了 twinv.c 的编译规则
结果: 编译后的 QEMU 多认识了一种机器:
qemu-system-riscv64 -M virt ← QEMU 自带的
qemu-system-riscv64 -M twinv ← ★ 我们注入的!
11.2 OpenSBI — 注入一个新平台
OpenSBI 源码 我们做的事
──────────── ──────────
platform/generic/ ← 已有的 platform/twinv/ ← 我们新写的
├── platform.c (启动逻辑)
├── Kconfig (依赖声明)
└── configs/defconfig
结果: 编译后的 OpenSBI 认识我们的硬件:
make PLATFORM=twinv ← 根据我们的 DTS 初始化
11.3 U-Boot — 注入一个新板子
U-Boot 源码 我们做的事
─────────── ──────────
board/emulation/qemu-riscv/ ← 已有的
board/emulation/qemu-twinv/ ← ★ 我们新写的 (27行)
include/configs/qemu-twinv.h ← ★ bootcmd 启动脚本
configs/qemu-twinv_smode_defconfig ← ★ 编译配置
11.4 FreeRTOS — 改了它的汇编文件
FreeRTOS 源码 我们做的事
─────────── ──────────
portable/GCC/RISC-V/portASM.S ← 原有的 我们直接修改了!
mstatus → sstatus ★ CSR 全替换
mret → sret ★ 返回指令替换
0x188 → 0x120 ★ 位域值修正
portable/GCC/RISC-V/port.c ← 原有的 我们也在 #else 加了空函数
FreeRTOSConfig.h ← 我们新写的 (configMTIME=0 等)
11.5 Linux — 只改配置,不改代码
Linux 源码 我们做的事
─────────── ──────────
整个内核: 一行没改 只改了 .config:
CONFIG_VIRTIO_BLK=y
CONFIG_DEVTMPFS=y
...
新增: src/driver/amp_mailbox/ ← ★ 唯一自写的内核代码
外部模块, 编译成 .ko
通过 insmod 加载
11.6 BusyBox — 纯配置
BusyBox 源码: 一行没改 只改了 .config:
CONFIG_STATIC=y (静态链接)
CONFIG_SHA1_HWACCEL=n
11.7 组件之间的通信
它们之间怎么"通信"?
QEMU 生成一个 DTB (设备树二进制)
QEMU 把固件入口地址写在 MROM 数据区
OpenSBI 启动后自己读 DTB 发现硬件
OpenSBI 初始化完直接 jr 到 U-Boot 的地址
U-Boot 通过 SBI ecall 反过来调 OpenSBI 的服务
U-Boot 通过 booti 指令把 Linux Image 加载到
内存, 设好 a0/a1 寄存器, 直接跳过去
Linux 写共享内存 0xB0800000
Linux 发 SBI ecall → OpenSBI 写 msip
FreeRTOS 收到中断, 自己读共享内存
| 接口 | 形式 |
|---|---|
| QEMU → LowLevelBoot | jr t0 跳到 pflash,a0=hartid, a1=DTB 地址 |
| LowLevelBoot → OpenSBI | jr t0 跳到 DDR,a0=hartid, a1=DTB 地址 |
| OpenSBI → U-Boot/FreeRTOS | FW_JUMP_ADDR 编译时写死 |
| U-Boot → Linux | booti 指令,a1=DTB 地址 |
| Linux ↔ FreeRTOS | 共享内存 + 环形缓冲 + SBI IPI |
| 所有层 → 硬件信息 | DTB (设备树二进制) |
| S-mode → M-mode 服务 | SBI ecall |
第十一章:总结
| 章节 | 层级 | 特权级 | 我们做了什么 |
|---|---|---|---|
| 1. RISC-V | ISA | 全部 | 理解指令 / CSR / 特权级 |
| 2. QEMU | 虚拟硬件 | Host | 写 twinv 机器类型 (560 行 C) |
| 3. LowLevelBoot | 固件 | M-mode | 手写汇编固件 (290 行) |
| 4. DTS | 数据 | 无 | 三份设备树描述硬件 |
| 5. OpenSBI | 固件 | M-mode | 双实例 + domain/PMP 隔离 |
| 6. U-Boot | 引导器 | S-mode | qemu-twinv 板适配 |
| 7. Linux | 操作系统 | S-mode | 标准内核 + 自写驱动模块 |
| 8. FreeRTOS | RTOS | S-mode | M→S 大移植 (6 个关键修改) |
| 9. IPC | 跨层通信 | 全部 | 共享内存 + 环形缓冲 + IPI |
| 10. 构建系统 | 元 | Host | 两套工具链 + 多组件编排 |

浙公网安备 33010602011771号