diff options
Diffstat (limited to 'arch')
| -rw-r--r-- | arch/arm64/kernel/start.S | 27 | ||||
| -rw-r--r-- | arch/arm64/lib/gic.c | 105 |
2 files changed, 132 insertions, 0 deletions
diff --git a/arch/arm64/kernel/start.S b/arch/arm64/kernel/start.S index 6ee9941..3cf58d2 100644 --- a/arch/arm64/kernel/start.S +++ b/arch/arm64/kernel/start.S @@ -222,6 +222,24 @@ park: wfe b 1b 2: + /* interrupts masked at release, the manual's boot state */ + msr daifset, #0xf + /* + * every PE must read the same virtual counter. whatever + * ran before this loader could have left a per cpu offset + * in the virtual counter view, the kernel has no way to + * repair that itself. CNTVOFF_EL2 is writable at EL2 and + * the write holds for the EL1 virtual timer the kernel + * runs on. below EL2 it is out of reach, the reset value + * is the best a lower EL can do. + */ + mrs x4, CurrentEL + lsr x4, x4, #2 + cmp x4, #2 + b.lt 3f + msr cntvoff_el2, xzr + isb +3: mov x0, xzr /* secondaries enter with x0-x3 zero */ mov x1, xzr mov x2, xzr @@ -396,6 +414,15 @@ exc_serr: mov x1, #0 mov x2, lr bl exc_report + /* + * an SError while this loader runs means the machine is + * broken. handing the kernel a cpu that already lost is + * worse than stopping: report, then drive the reset domain + * the same way PSCI SYSTEM_RESET does. the reset call does + * not return, the park below is the fallback if a reset + * domain ignores the request. + */ + bl tb_system_reset ldp x29, x30, [sp], #16 b park diff --git a/arch/arm64/lib/gic.c b/arch/arm64/lib/gic.c new file mode 100644 index 0000000..647e987 --- /dev/null +++ b/arch/arm64/lib/gic.c @@ -0,0 +1,105 @@ +/* + * gic.c - the interrupt controller state a bootloader owns. the + * kernel programs the gic itself for the running system, but it + * trusts the state it inherits: on real hardware the secure + * world configures which interrupts are visible to non-secure, + * and a bootloader that leaves random enables or secure group + * bits set hands the kernel a half-configured distributor that + * can fire before the kernel's irqchip driver is up. + * + * this is the gicv2 sequence from the TRM, the same shape + * u-boot leaves the machine in: distributor off, every + * interrupt in the non-secure group, all per interrupt enables + * cleared, pending state cleared, cpu interfaces off. defined + * state, nothing firing, the kernel starts from zero. + * + * GICv1 shows the same register map minus the security + * extension registers, the writes below are harmless there. + * + * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org> + */ + +#include <string.h> +#include <stdint.h> +#include <boot.h> +#include <reg.h> + +/* distributor registers, offsets from the GICD base */ +#define GICD_CTLR 0x000 +#define GICD_TYPER 0x004 +#define GICD_ISENABLER(n) (0x100 + (n) * 4) +#define GICD_ICENABLER(n) (0x180 + (n) * 4) +#define GICD_ICPENDR(n) (0x280 + (n) * 4) +#define GICD_ICACTIVER(n) (0x380 + (n) * 4) + +/* cpu interface registers, offsets from the GICC base */ +#define GICC_CTLR 0x000 +#define GICC_PMR 0x004 + +/* GICD_CTLR bits */ +#define GICD_CTLR_ENABLE_GRP1 (1 << 0) +#define GICD_CTLR_ENABLE_GRP0 (1 << 1) + +/* GICC_CTLR bits */ +#define GICC_CTLR_ENABLE (1 << 0) + +#define GICD_TYPER_ITLINES_MASK 0x1f + +/* + * how many 32-irq lines the distributor carries, TYPER.ITLines + * holds count of (irqs / 32) - 1, clamped per the spec because + * the field is 5 bits and caps at 1020 irqs. + */ +static int gicd_irq_lines(uintptr_t gicd) +{ + uint32_t typer = readl(REG32(gicd + GICD_TYPER)); + + return ((typer & GICD_TYPER_ITLINES_MASK) + 1); +} + +/* + * leave the gic in the defined state the kernel expects. the + * addresses come from the devicetree the caller walked, qemu + * virt carries a gicv2 at 0x08000000 with the cpu interface at + * +0x10000. + */ +int tb_gic_init(uintptr_t gicd, uintptr_t gicc) +{ + int lines; + int n; + + if (!gicd || !gicc) + return -1; + + /* the distributor is off while it is reconfigured */ + writel(0, REG32(gicd + GICD_CTLR)); + writel(0, REG32(gicc + GICC_CTLR)); + + lines = gicd_irq_lines(gicd); + + /* + * the group routing is deliberately untouched. the group + * registers are the secure world's, a non-secure loader's + * writes are dropped on hardware that implements the + * security extension, and on emulators that accept them + * the timer's per cpu interrupts stop reaching the + * kernel. group config belongs to the EL3 monitor, this + * loader runs without one. + */ + + /* no per interrupt enables, nothing pending */ + for (n = 0; n < lines; n++) { + writel(0xffffffff, REG32(gicd + GICD_ICENABLER(n))); + writel(0xffffffff, REG32(gicd + GICD_ICPENDR(n))); + } + + /* + * the cpu interface stays off with the priority mask at + * the lowest priority, the kernel raises it when it + * brings its own irq handling up. off is the defined + * state, the enable is the kernel's decision to make. + */ + writel(0, REG32(gicc + GICC_PMR)); + + return 0; +} |
