aarch64
This page documents the aarch64 architecture backend in kernite/src/arch/aarch64/.
Module Structure
| File | Responsibility |
|---|---|
|
Backend entry point, IRQ save/restore (DAIF manipulation), |
|
BSP bootstrap: GICv3, PSCI, generic timer, paging initialization. |
|
AP startup via PSCI |
|
GICv3 interrupt controller: distributor (GICD), redistributor (GICR), CPU interface (ICC system registers). |
|
Power State Coordination Interface — |
|
Generic timer: EL1 physical timer (CNTP), PPI 30, 10 ms tick interval. |
|
4-level page tables under TTBR0 (user) / TTBR1 (kernel), ASID management, TLB invalidation. |
|
Context switch: save/restore |
|
Per-CPU data via |
|
Exception vector table: sync/IRQ/FIQ/SError handlers at EL1 and from EL0. |
|
NEON/FP eager context switch — Q0-Q31 + FPCR + FPSR saved/restored on every TCB switch, |
|
PL011 UART driver for early serial console output. |
Initialization Order
init() follows this sequence:
-
cpu::init_bsp()— per-CPU data viaTPIDR_EL1, BSP CPU ID. -
mm::init()— frame allocator from boot info memory map. -
gic::init()— GICv3 distributor and redistributor, enable CPU interface. -
timer::init()— configure CNTP but leave it masked (timer starts later). -
paging::init()— kernel page tables under TTBR1, direct physical map, user TTBR0 setup. -
exceptions::init()— install exception vector table (VBAR_EL1).
Timer interrupts start via start_timer() after the scheduler is initialized.
Exception Levels
SaltyOS runs at EL1 (kernel) with user code at EL0. The bootloader enters at EL2 (hypervisor) and uses VHE (Virtualization Host Extensions) if available; otherwise drops to EL1 before entering the kernel.
| Level | Usage |
|---|---|
EL0 |
User mode — all userspace processes. |
EL1 |
Kernel mode — kernite runs here. |
EL2 |
Hypervisor — bootloader enters here, drops to EL1. PSCI calls use HVC. |
EL3 |
Secure monitor — firmware, not used by SaltyOS. |
GICv3
The Generic Interrupt Controller version 3 handles all interrupt routing.
Components
- Distributor (GICD)
-
System-wide. Memory-mapped. Routes SPIs (Shared Peripheral Interrupts) to redistributors. Configures trigger mode (level/edge), priority, and target CPU for each SPI.
- Redistributor (GICR)
-
Per-CPU. Memory-mapped. Manages PPIs (Private Peripheral Interrupts, like the timer) and SGIs (Software Generated Interrupts, used for IPI).
- CPU Interface (ICC)
-
Per-CPU. Accessed via system registers (
ICC_*), not memory-mapped.ICC_IAR1_EL1— acknowledge interrupt (returns interrupt ID).ICC_EOIR1_EL1— end of interrupt (EOI).ICC_PMR_EL1— priority mask register.ICC_SRE_EL1— system register enable.
Interrupt Types
| Type | ID Range | Examples |
|---|---|---|
SGI |
0-15 |
Software-generated for IPI (reschedule, TLB shootdown). |
PPI |
16-31 |
Per-CPU private: PPI 30 = EL1 physical timer (CNTP). |
SPI |
32-1019 |
Shared peripherals: virtio devices, UART, etc. |
IRQ Flow
-
Hardware asserts interrupt line.
-
GIC distributor routes to target CPU’s redistributor.
-
CPU takes IRQ exception (vector offset
0x480fromVBAR_EL1). -
Exception handler reads
ICC_IAR1_EL1to acknowledge and get interrupt ID. -
Dispatch to handler: timer tick (PPI 30), IPI (SGI), or device IRQ (SPI →
dispatch_irq()). -
Write
ICC_EOIR1_EL1to signal end of interrupt.
PSCI
The Power State Coordination Interface provides CPU lifecycle management via HVC calls to the EL2 firmware.
| Function | ID | Description |
|---|---|---|
|
|
Start an application processor. Arguments: target MPIDR, entry point, context ID. |
|
|
Power off the calling CPU. |
|
|
Power off the entire system. |
|
|
Reset the system. |
All calls use the SMC64/HVC64 calling convention.
Generic Timer
The EL1 physical timer (CNTP) provides the scheduler tick.
Timer |
CNTP (Counter-timer Physical) |
|---|---|
Interrupt |
PPI 30 |
Tick interval |
10 ms |
Registers |
|
On each tick:
-
Timer fires PPI 30 → GIC → IRQ exception.
-
Handler reads current counter, computes next deadline.
-
Writes
CNTP_TVAL_EL0with the interval for the next tick. -
Calls into the scheduler’s
timer_tick(). -
Writes
ICC_EOIR1_EL1(EOI) before any schedulable code runs.
Paging
Page Table Format
4-level page tables (4 KB granule, 48-bit VA):
Level 0 (TTBR) → Level 1 → Level 2 → Level 3 (leaf) 9 bits 9 bits 9 bits 9 bits + 12-bit page offset
-
TTBR0_EL1: user address space (lower half,
0x0000_*). -
TTBR1_EL1: kernel address space (upper half,
0xFFFF_*).
Descriptor Format
| Bits | Field | Description |
|---|---|---|
1:0 |
Type |
|
47:12 |
Address |
Physical address of next-level table or output page. |
54 |
UXN |
User execute-never. |
53 |
PXN |
Privileged execute-never. |
7 |
AP[2] |
Access permission: 0 = R/W, 1 = read-only. |
6 |
AP[1] |
Access permission: 1 = EL0 accessible, 0 = EL1 only. |
4:2 |
AttrIdx |
Memory attribute index into MAIR_EL1. |
10 |
AF |
Access flag (must be set or HW raises access flag fault). |
SMP
AP Startup
-
BSP discovers CPU topology from the device tree or ACPI MADT and assigns each AP a logical CPU index.
-
For each AP: call
PSCI CPU_ONvia HVC with the AP’s MPIDR, the sharedap_entryentry-point address, and the logical CPU index passed as PSCI’scontext IDargument. -
PSCI delivers the
context IDto the AP inx0at the entry point, so the AP identifies itself without polling an external mailbox. -
The AP boots at EL2, drops to EL1, loads its per-CPU data (including the kernel stack pointer) indexed by the received logical CPU number.
-
AP initializes: GIC redistributor, per-CPU timer,
TPIDR_EL1, exception vectors. -
AP enters the idle loop.
FPU / NEON
Eager context switching:
-
At boot (BSP and each AP),
CPACR_EL1.FPENis programmed to0b11, permanently disabling FPU/NEON traps from EL0 and EL1. -
On every TCB switch,
fpu::switch()callsaarch64_fpsimd_save_stateon the outgoing TCB andaarch64_fpsimd_restore_stateon the incoming TCB unconditionally — there is no trap-and-demand-load path. -
flush_current()/reload_current()keep the live register file in sync afterTCB_COPY_FPUor similar kernel-side mutations.
Save area: XSaveArea — 528 bytes: 32 × Q registers (512 bytes) + FPCR (4) + FPSR (4) + padding.
PL011 UART
The PL011 UART provides early serial console output on aarch64:
-
Memory-mapped at the device tree-specified address (typically
0x0900_0000on QEMUvirt). -
putc(byte): pollUARTFR(flags register) for TX FIFO not full, then write toUARTDR. -
Used by the raw serial output path (
serial_puts_raw) for panic and early boot messages. -
No interrupt-driven I/O — purely polled output.
Syscall Entry
Syscalls use svc #0. The exception vector dispatches EL0 synchronous exceptions:
-
Save all user registers (
x0-x30,sp_el0,elr_el1,spsr_el1) to the kernel stack. -
Read
ESR_EL1to confirmEC = 0x15(SVC from AArch64). -
Syscall number in
x8, arguments inx0-x5. -
Call
syscall_handle_rust(). -
Restore user registers and
eret.
I/O Port Emulation
aarch64 has no I/O port instructions. For code that uses IoPort capabilities (PCI config space access), the aarch64 backend provides MMIO-based PCI I/O window functions:
-
pci_io_read8/16/32(port)— translate port address to MMIO offset within the PCI I/O window. -
pci_io_write8/16/32(port, value)— same for writes.
Direct inb/outb calls on aarch64 are stubs that return 0 or panic.
Related Pages
-
x86_64 — the other supported architecture
-
Architecture — architecture abstraction layer
-
Memory Layout — TTBR0/TTBR1 address space split
-
Boot Sequence — aarch64 PSCI AP bringup during init