4. 硬件平台与启动流程

多系统协同的第一道门槛并不在操作系统内部,而在上电后的最初几百毫秒。CPU 从复位向量取指开始,到 QNX 与 Linux 各自拥有可运行的内核、页表和中断控制器,中间要经过 Bootloader、IPL、镜像加载、核间唤醒与共享内存建立等多个阶段。这一节把 ARM 与 x86 两类多核平台的启动链路拆开讲清楚,并给出多系统启动顺序与共享内存初始化的工程做法。

一、核心原理:两级引导与多核启动

无论 ARM 还是 x86,多系统平台的启动都可以抽象为“两级引导 + 多核释放”模型。第一级是固化在芯片内或板级的小型引导程序,负责最基础的时钟、DDR、串口初始化,并把第二级引导程序搬入内存;第二级才是功能完整的 Bootloader,负责加载操作系统镜像、准备设备树或 ACPI 表、建立启动参数,并把控制权交给内核。

在 ARM 平台上,第一级通常称为 IPL(Initial Program Loader),QNX 侧常写作 QNX IPL,它往往与 QNX 的启动镜像一起被打包,承担“把 QNX 内核和启动脚本从 Flash 搬到 RAM 并跳转”的职责。第二级可以是 U-Boot,也可以是 QNX 自带的启动镜像。在 x86 平台上,第一级是芯片内的 Boot ROM 或 UEFI 固件,第二级通常是 U-Boot、GRUB 或 UEFI 应用,QNX 在 x86 上更多依赖 UEFI/BIOS 完成早期初始化后再由 QNX IPL 接管。

多核释放是这一阶段最容易被忽视的细节。上电时通常只有一个核(ARM 的 CPU0、x86 的 BSP)在运行,其余核处于等待事件或自旋状态。Bootloader 需要把从核的入口地址、栈指针、启动参数写入约定内存位置,再通过 SGI(ARM)或 INIT-SIPI-SIPI(x86)唤醒从核。如果从核释放顺序与操作系统 SMP 初始化不匹配,就会出现“内核已启动但只有单核在线”的现象。

ARM / x86 多核平台多系统启动链路 上电复位 CPU0 / BSP 取指 一级引导 ARM: QNX IPL / Boot ROM x86: UEFI / BIOS 二级引导 U-Boot / QNX IFS 加载镜像 + 设备树 多核释放 SGI / SIPI 唤醒从核 入口地址 + 栈指针 QNX 侧启动 QNX IPL 解压 IFS 到 RAM 启动 procnto-smp 内核 挂载共享内存 / 启动资源管理器 QNX 先就绪,作为协同主控 Linux 侧启动 U-Boot 加载 Image + DTB 内核解压、建立页表 SMP 启动从核、初始化 rpmsg Linux 后启动,作为协同从端 共享内存初始化与核间通信 保留内存区 memreserve / reserved-memory 0x8000_0000 起 64MB 控制区 状态字 / 门铃 / 版本 双方只读或只写 数据环 TX / RX ring buffer 生产者-消费者指针 初始化顺序:QNX 先写控制区 → 置 ready 标志 → Linux 读 ready → 建立 rpmsg 通道 → 双向门铃 若 Linux 先启动,则需等待 QNX 的 ready 标志,避免访问未初始化共享内存

二、方法流程:多系统启动顺序设计

多系统启动顺序没有唯一答案,但工程上通常遵循“先主控、后从端,先共享内存、后通信”的原则。以 QNX 作为实时主控、Linux 作为计算从端的典型场景为例,推荐顺序如下:

  1. Bootloader 完成 DDR、时钟、串口初始化,并把 QNX IFS 与 Linux 镜像分别放到约定地址。
  2. Bootloader 释放 QNX 所在核,QNX IPL 解压 IFS,启动 procnto-smp。
  3. QNX 启动脚本初始化共享内存控制区,写入版本号、缓冲区大小、ring 指针,并置 ready 标志。
  4. QNX 通过 SGI 或寄存器写触发 Linux 核的释放,或由 Bootloader 在更早阶段已释放 Linux 核。
  5. Linux 内核启动,解析设备树中的 reserved-memory 节点,映射共享内存,读取 ready 标志。
  6. Linux 初始化 rpmsg / virtio 通道,与 QNX 建立双向门铃,进入数据交换状态。

如果 Linux 必须先启动(例如需要先跑文件系统或网络),则 QNX 侧必须在启动脚本中轮询 Linux 的 ready 标志,不能假设共享内存已可用。这种“谁先谁后”的差异,必须在启动参数和共享内存协议中显式约定,而不是靠延时碰运气。

三、案例与参数:共享内存初始化清单

下面是一段典型的共享内存保留区设备树片段与 QNX 侧初始化伪代码,用于说明双方如何对齐地址与状态。

/* Linux 设备树:保留共享内存区 */
reserved-memory {
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;
    shm_region: shm@80000000 {
        reg = <0x0 0x80000000 0x0 0x04000000>;
        no-map;
    };
};

/* QNX 侧初始化共享内存控制区 */
shm = mmap_device_memory(0x80000000, 0x04000000,
                         PROT_READ|PROT_WRITE,
                         0, 0);
shm->magic   = 0x514E5853;   /* "QNXS" */
shm->version = 0x00010002;
shm->tx_head = 0; shm->tx_tail = 0;
shm->rx_head = 0; shm->rx_tail = 0;
atomic_clr(&shm->flags, SHM_READY);
/* 初始化完成后置位 */
atomic_set(&shm->flags, SHM_READY);
阶段ARM 平台x86 平台关键检查点
一级引导QNX IPL / Boot ROMUEFI / BIOSDDR 训练、串口可用
二级引导U-Boot / QNX IFSU-Boot / GRUB镜像地址、设备树/ACPI
多核释放SGI 唤醒从核INIT-SIPI-SIPI入口地址、栈指针
共享内存reserved-memorye820 / memmap地址对齐、no-map
通信建立rpmsg / 门铃rpmsg / 门铃ready 标志、版本号

四、误区与复盘

误区一:把 Bootloader 当成“只加载镜像”。在多系统平台,Bootloader 还承担核释放、内存保留、启动参数传递等职责。若 reserved-memory 未正确声明,Linux 可能把共享内存当作普通页分配出去,导致 QNX 侧数据被覆盖。

误区二:共享内存初始化顺序靠延时保证。用 sleep 等待对方就绪在实验室可行,在现场会因启动时间波动而失败。正确做法是使用 magic + version + ready 标志,双方轮询或中断通知,而不是固定延时。

复盘要点:启动问题优先看串口日志的时间戳顺序,确认“谁先释放、谁先写共享内存、谁先置 ready”。其次检查设备树或 ACPI 中的保留内存是否与 QNX 侧 mmap 地址一致,最后核对多核释放的入口地址与栈指针是否被后续镜像覆盖。把这三项做成启动自检清单,可以覆盖大多数多系统启动故障。

总结来看,ARM 与 x86 的启动链路在细节上差异明显,但工程抽象是一致的:一级引导负责“活下来”,二级引导负责“装得下”,多核释放负责“叫得醒”,共享内存初始化负责“说得上话”。把这四步的顺序、地址和状态标志在启动阶段就固定下来,后续的 QNX 与 Linux 协同才有稳定的地基。