基于 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。
流程:

  1. auipc t0, 0 → t0 = 0 (PC 此时为 0)
  2. csrr a0, mhartid → a0 = 当前核的 ID
  3. ld a1, 32(t0) → 从 MROM 偏移 32 读取 DTB 地址 → a1
  4. ld a2, 40(t0) → 从 MROM 偏移 40 读取 fw_dynamic_info → a2
  5. ld t0, 24(t0) → 从 MROM 偏移 24 读取固件入口 → t0 = 0x20000000
  6. 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 调试就跳过搬运。

  1. 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
  1. 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 的格式:。因为 #address-cells=<2>, #size-cells=<2>,每个地址/大小用两个 32 位表示——这是 64 位系统的标准写法。

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 两套工具链 + 多组件编排
posted @ 2026-08-03 17:30  r5ett  阅读(1)  评论(0)    收藏  举报