|
| 1 | +@page page_kernel_smp_boot QEMU virt64 AArch64 SMP Boot Flow |
| 2 | + |
| 3 | +# QEMU virt64 AArch64 SMP Boot Flow |
| 4 | + |
| 5 | +This guide walks through the multi-core boot path of RT-Thread on AArch64 using `bsp/qemu-virt64-aarch64` as the concrete reference. It is written to be beginner-friendly and mirrors the current BSP implementation: from `_start` assembly, early MMU bring-up, `rtthread_startup()`, PSCI wakeup of secondary cores, to all CPUs entering the scheduler. The original PlantUML diagram is replaced by Mermaid so it renders directly on GitHub. |
| 6 | + |
| 7 | +- Target setup: QEMU `-machine virt`, `-cpu cortex-a57`, `-smp >=2`, `RT_USING_SMP` enabled, device tree contains `enable-method = "psci"`. |
| 8 | +- Goal: Know who does what, where the code lives, and what to check when SMP does not come up. |
| 9 | + |
| 10 | +## Big Picture First |
| 11 | + |
| 12 | +```mermaid |
| 13 | +flowchart TD |
| 14 | + ROM[BootROM/BL1<br/>QEMU firmware] --> START[_start<br/>(entry_point.S)] |
| 15 | + START --> MMU[init_mmu_early<br/>enable_mmu_early] |
| 16 | + MMU --> CBOOT[rtthread_startup()] |
| 17 | + CBOOT --> BOARD[rt_hw_board_init<br/>-> rt_hw_common_setup] |
| 18 | + BOARD --> MAIN[main_thread_entry] |
| 19 | + MAIN --> PSCI[rt_hw_secondary_cpu_up<br/>(PSCI CPU_ON)] |
| 20 | + PSCI --> SECASM[_secondary_cpu_entry<br/>(ASM)] |
| 21 | + SECASM --> SECC[rt_hw_secondary_cpu_bsp_start] |
| 22 | + SECC --> SCHED[rt_system_scheduler_start] |
| 23 | + SCHED --> RUN[SMP scheduling] |
| 24 | +``` |
| 25 | + |
| 26 | +## Boot CPU: from `_start` to MMU on |
| 27 | + |
| 28 | +**Input registers**: QEMU firmware loads the image and jumps to `_start` at `libcpu/aarch64/cortex-a/entry_point.S`, passing the DTB physical address in `x0` (with `x1~x3` reserved). |
| 29 | + |
| 30 | +**What `_start` does (short version)** |
| 31 | + |
| 32 | +1. Clear thread pointers: zero `tpidr_el1/tpidrro_el0` to avoid stale per-cpu state. |
| 33 | +2. Unify exception level: `init_cpu_el` drops to EL1h, enables timer access, masks unwanted traps. |
| 34 | +3. Clear BSS: `init_kernel_bss` fills `__bss` with zeros so globals start clean. |
| 35 | +4. Prepare stack: `init_cpu_stack_early` switches to SP_EL1 and uses `.boot_cpu_stack_top` as the early stack. |
| 36 | +5. Remember the FDT: `rt_hw_fdt_install_early(x0)` stores DTB address/size before MMU is enabled. |
| 37 | +6. Early MMU mapping: `init_mmu_early`/`enable_mmu_early` build a 0~1G identity map, set TTBR0/TTBR1 and SCTLR_EL1, flush I/D cache and TLB, then branch to `rtthread_startup()` (address in x8). |
| 38 | + |
| 39 | +> Tip: the early page table only covers minimal kernel space; the C phase will remap a fuller layout. |
| 40 | +
|
| 41 | +## C-side startup backbone |
| 42 | + |
| 43 | +`rtthread_startup()` (in `src/components.c`) is the spine of the sequence: |
| 44 | + |
| 45 | +- **Interrupts off + spinlock ready**: `rt_hw_local_irq_disable()` followed by `_cpus_lock` init to keep early steps non-preemptible. |
| 46 | +- **Board init**: `rt_hw_board_init()` directly calls the BSP hook `rt_hw_common_setup()` (`libcpu/aarch64/common/setup.c`) to: |
| 47 | + - set VBAR, build kernel address space, copy DTB to a safe region and pre-parse it; |
| 48 | + - configure MMU mappings; init memblock/page allocator/system heap; |
| 49 | + - parse DT for console, memory, initrd; |
| 50 | + - init GIC (and GICv3 Redistributor if enabled), UART, global GTIMER; |
| 51 | + - install SMP IPIs (`RT_SCHEDULE_IPI`, `RT_STOP_IPI`, `RT_SMP_CALL_IPI`) and unmask them; |
| 52 | + - set idle hook `rt_hw_idle_wfi` so idle CPUs enter low-power wait. |
| 53 | +- **Kernel subsystems**: init system timer, scheduler, signals, and create main/timer/idle threads. |
| 54 | +- **Start scheduling**: `rt_system_scheduler_start()` runs `main_thread_entry()` first. |
| 55 | + |
| 56 | +## How secondary cores are brought up |
| 57 | + |
| 58 | +`main_thread_entry()` calls `rt_hw_secondary_cpu_up()` before invoking user `main()`, so all CPUs join scheduling. |
| 59 | + |
| 60 | +### What `rt_hw_secondary_cpu_up()` does |
| 61 | + |
| 62 | +1. Convert `_secondary_cpu_entry` to a physical address via `rt_kmem_v2p()`—the real entry the firmware jumps to. |
| 63 | +2. Walk CPU nodes recorded at boot (`cpu_info_init()` stored DTB info in `cpu_np[]` and `rt_cpu_mpidr_table[]`). |
| 64 | +3. Read `enable-method`: |
| 65 | + - QEMU virt64: `"psci"` → use `cpu_psci_ops.cpu_boot()` to issue `CPU_ON(target, entry)` to firmware. |
| 66 | + - Legacy compatibility: `"spin-table"` → write `cpu-release-addr` and `sev` to wake. |
| 67 | +4. Any failure prints a warning but does not halt the boot flow, making diagnosis easier. |
| 68 | + |
| 69 | +### What happens on a secondary core |
| 70 | + |
| 71 | +- **Assembly entry `_secondary_cpu_entry`**: |
| 72 | + - Read `mpidr_el1`, compare with `rt_cpu_mpidr_table` to find the logical CPU id, store it back, and write it into `TPIDR` for per-cpu access. |
| 73 | + - Allocate its own stack by offsetting `ARCH_SECONDARY_CPU_STACK_SIZE` per core. |
| 74 | + - Re-run `init_cpu_el`/`init_cpu_stack_early`, reuse the same early MMU path, then branch to `rt_hw_secondary_cpu_bsp_start()`. |
| 75 | + |
| 76 | +- **C-side handoff `rt_hw_secondary_cpu_bsp_start()`** (`libcpu/aarch64/common/setup.c`): |
| 77 | + - Reset VBAR and synchronize with the boot CPU via `_cpus_lock`. |
| 78 | + - Update this core's MPIDR entry and bind the shared `MMUTable`. |
| 79 | + - Init local vector table, GIC CPU interface (and GICv3 Redistributor if present), enable the local GTIMER. |
| 80 | + - Unmask the three SMP IPIs; re-calibrate `loops_per_tick` for microsecond delay if needed. |
| 81 | + - Call `rt_dm_secondary_cpu_init()` to register the CPU device, then enter the scheduler via `rt_system_scheduler_start()`. |
| 82 | + |
| 83 | +### Timeline (Mermaid) |
| 84 | + |
| 85 | +```mermaid |
| 86 | +sequenceDiagram |
| 87 | + participant ROM as BootROM/BL1 |
| 88 | + participant START as _start (ASM) |
| 89 | + participant CBOOT as rtthread_startup |
| 90 | + participant MAIN as main_thread_entry |
| 91 | + participant FW as PSCI firmware |
| 92 | + participant SECASM as _secondary_cpu_entry |
| 93 | + participant SECC as rt_hw_secondary_cpu_bsp_start |
| 94 | + participant SCHED as Scheduler (all CPUs) |
| 95 | +
|
| 96 | + ROM->>START: x0=DTB, jump to _start |
| 97 | + START->>START: init_cpu_el / clear BSS / set stack |
| 98 | + START->>START: init_mmu_early + enable_mmu_early |
| 99 | + START-->>CBOOT: branch to rtthread_startup() |
| 100 | + CBOOT->>CBOOT: rt_hw_board_init -> rt_hw_common_setup |
| 101 | + CBOOT-->>SCHED: rt_system_scheduler_start() |
| 102 | + SCHED-->>MAIN: run main_thread_entry |
| 103 | + MAIN->>FW: rt_hw_secondary_cpu_up (CPU_ON) |
| 104 | + FW-->>SECASM: entry = _secondary_cpu_entry |
| 105 | + SECASM->>SECASM: stack/TPIDR/EL setup |
| 106 | + SECASM-->>SECC: enable_mmu_early -> rt_hw_secondary_cpu_bsp_start |
| 107 | + SECC->>SECC: local GIC/Timer/IPI init |
| 108 | + SECC-->>SCHED: rt_system_scheduler_start() |
| 109 | + SCHED-->>MAIN: continue main() |
| 110 | + SCHED-->>Others: SMP scheduling |
| 111 | +``` |
| 112 | + |
| 113 | +## Source map (where to read the code) |
| 114 | + |
| 115 | +| Stage | File | Role | |
| 116 | +| --- | --- | --- | |
| 117 | +| Boot assembly | `libcpu/aarch64/cortex-a/entry_point.S` | `_start`, `_secondary_cpu_entry`, early MMU enable | |
| 118 | +| BSP hook | `bsp/qemu-virt64-aarch64/drivers/board.c` | Wires `rt_hw_board_init()` to `rt_hw_common_setup()` | |
| 119 | +| Memory/GIC/IPI init | `libcpu/aarch64/common/setup.c` | `rt_hw_common_setup()`, `rt_hw_secondary_cpu_up()`, `rt_hw_secondary_cpu_bsp_start()` | |
| 120 | +| C entry skeleton | `src/components.c` | `rtthread_startup()`, `main_thread_entry()` | |
| 121 | + |
| 122 | +## Quick checks when SMP fails to come up |
| 123 | + |
| 124 | +- Device tree: contains `enable-method = "psci"` and QEMU is started with `-machine virt` (PSCI firmware included). |
| 125 | +- `_secondary_cpu_entry` physical address: `rt_kmem_v2p()` must not return 0, otherwise a check fails. |
| 126 | +- Init order: GIC/Timer must be ready before calling `rt_hw_secondary_cpu_up()`; if you fork a custom BSP, do these first. |
| 127 | +- UART logs: look for `Call cpu X on success/failed`; add extra prints in `_secondary_cpu_entry` if needed, and use QEMU `-d cpu_reset -smp N` to debug. |
| 128 | + |
| 129 | +## AArch64 pocket notes (just enough) |
| 130 | + |
| 131 | +- **Exception levels**: startup may be at EL3/EL2; `init_cpu_el` descends to EL1h where the kernel runs. |
| 132 | +- **Two stack pointers**: `spsel #1` selects `SP_EL1` so user mode cannot touch the kernel stack. |
| 133 | +- **MMU bring-up order**: build page tables → configure TCR/TTBR → flush cache/TLB → set `SCTLR_EL1.M/C/I` → `isb`. |
| 134 | +- **MPIDR**: unique core affinity; stored in `rt_cpu_mpidr_table[]` to map logical CPU ids and IPI targets. |
| 135 | + |
| 136 | +With these in place, the QEMU virt64 AArch64 BSP SMP path is clear: the boot CPU prepares memory and shared peripherals, `main_thread_entry()` issues PSCI wakeups, secondary cores land with the same MMU/EL setup, and all CPUs join the scheduler. |
0 commit comments