Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
808e1d66fd | ||
|
|
8c09ccef66 | ||
|
|
8b6379fb7e |
No files matched your search
@@ -6,12 +6,21 @@
|
||||
archives/isos/
|
||||
montaukos.org/downloads/*.iso
|
||||
*.hdd
|
||||
*.img
|
||||
rpi4-firmware/
|
||||
qemu.log
|
||||
toolchain/local/
|
||||
toolchain/build/
|
||||
toolchain/src/
|
||||
toolchain/sysroot/
|
||||
toolchain/sysroot-*/
|
||||
programs/bin/
|
||||
# Non-x86 userspace builds (programs/mk/arch.mk)
|
||||
programs/bin-*/
|
||||
programs/obj-*/
|
||||
programs/src/*/obj-*/
|
||||
programs/lib/*/obj-*/
|
||||
programs/lib/*/build-*/
|
||||
programs/obj/
|
||||
programs/src/*/obj/
|
||||
programs/libs/*/obj/
|
||||
|
||||
+113
-20
@@ -10,6 +10,32 @@ QEMUFLAGS := -smp 4 -m 2G -d int -D qemu.log -no-reboot
|
||||
|
||||
override IMAGE_NAME := montauk-$(ARCH)
|
||||
|
||||
# KERNEL_ONLY=1 builds a ramdisk holding just the boot files (no
|
||||
# userspace), for bringing up a kernel on a new architecture.
|
||||
KERNEL_ONLY ?= 0
|
||||
|
||||
# programs/ builds x86_64 into bin/, other architectures into bin-<arch>/.
|
||||
ifeq ($(ARCH),x86_64)
|
||||
PROGRAMS_BIN := programs/bin
|
||||
else
|
||||
PROGRAMS_BIN := programs/bin-$(ARCH)
|
||||
endif
|
||||
|
||||
override EFI_LOADER_x86_64 := BOOTX64.EFI
|
||||
override EFI_LOADER_aarch64 := BOOTAA64.EFI
|
||||
override EFI_LOADER_riscv64 := BOOTRISCV64.EFI
|
||||
override EFI_LOADER_loongarch64 := BOOTLOONGARCH64.EFI
|
||||
|
||||
# The host strip handles x86_64 images; other targets use their toolchain's.
|
||||
ifeq ($(ARCH),x86_64)
|
||||
STRIP := strip
|
||||
else
|
||||
STRIP := toolchain/local/bin/$(ARCH)-montauk-strip
|
||||
endif
|
||||
|
||||
# AArch64 UEFI firmware for QEMU (Debian/Ubuntu: qemu-efi-aarch64).
|
||||
AARCH64_EFI_FIRMWARE ?= /usr/share/qemu-efi-aarch64/QEMU_EFI.fd
|
||||
|
||||
# Toolchain for building the 'limine' executable for the host.
|
||||
HOST_CC := cc
|
||||
HOST_CFLAGS := -g -O2 -pipe
|
||||
@@ -51,29 +77,72 @@ run-hdd-x86_64: $(IMAGE_NAME).hdd
|
||||
$(QEMUFLAGS)
|
||||
|
||||
.PHONY: run-aarch64
|
||||
# The machine mirrors the Raspberry Pi 4/400: Cortex-A72 cores, a GICv2, and
|
||||
# keyboard and mouse behind a hub (every USB 2 device on the Pi is: the
|
||||
# VL805's one USB 2 port carries an on-board hub). QEMU's hub is full-speed,
|
||||
# so the transaction-translator path is only exercised on the real board.
|
||||
# The kernel console is mirrored to the PL011 (-serial stdio).
|
||||
run-aarch64: $(IMAGE_NAME).iso
|
||||
qemu-system-$(ARCH) \
|
||||
-M virt \
|
||||
-M virt,gic-version=2 \
|
||||
-cpu cortex-a72 \
|
||||
-device ramfb \
|
||||
-device qemu-xhci,id=xhci \
|
||||
-device usb-kbd,bus=xhci.0 \
|
||||
-device usb-mouse,bus=xhci.0 \
|
||||
-bios /usr/share/ovmf/OVMF.fd \
|
||||
-device usb-hub,bus=xhci.0,port=1 \
|
||||
-device usb-kbd,bus=xhci.0,port=1.1 \
|
||||
-device usb-mouse,bus=xhci.0,port=1.2 \
|
||||
-bios $(AARCH64_EFI_FIRMWARE) \
|
||||
-cdrom $(IMAGE_NAME).iso \
|
||||
-serial stdio \
|
||||
$(QEMUFLAGS)
|
||||
|
||||
.PHONY: run-hdd-aarch64
|
||||
run-hdd-aarch64: $(IMAGE_NAME).hdd
|
||||
qemu-system-$(ARCH) \
|
||||
-M virt \
|
||||
-M virt,gic-version=2 \
|
||||
-cpu cortex-a72 \
|
||||
-device ramfb \
|
||||
-device qemu-xhci,id=xhci \
|
||||
-device usb-kbd,bus=xhci.0 \
|
||||
-device usb-mouse,bus=xhci.0 \
|
||||
-bios /usr/share/ovmf/OVMF.fd \
|
||||
-hda $(IMAGE_NAME).hdd \
|
||||
-device usb-hub,bus=xhci.0,port=1 \
|
||||
-device usb-kbd,bus=xhci.0,port=1.1 \
|
||||
-device usb-mouse,bus=xhci.0,port=1.2 \
|
||||
-bios $(AARCH64_EFI_FIRMWARE) \
|
||||
-drive if=none,id=hd0,format=raw,file=$(IMAGE_NAME).hdd \
|
||||
-device virtio-blk-pci,drive=hd0 \
|
||||
-serial stdio \
|
||||
$(QEMUFLAGS)
|
||||
|
||||
# The SD card image on QEMU, with the root read through the SDHCI driver:
|
||||
# the card goes on sdhci-pci as a padded copy (QEMU wants a power-of-two card;
|
||||
# RPI4_QEMU_CARD_MIB, default 4 GiB, leaves the first-boot grow room to work
|
||||
# with; every run starts from a fresh copy, so every run is a first boot),
|
||||
# and since QEMU's UEFI has no SD driver, the card's ESP is also
|
||||
# attached alone (the card's first partition, as its own disk) over virtio
|
||||
# for the firmware to boot Limine from.
|
||||
RPI4_QEMU_CARD_MIB ?= 4096
|
||||
.PHONY: run-rpi4-sd
|
||||
run-rpi4-sd: montauk-rpi4.img
|
||||
cp --sparse=always montauk-rpi4.img rpi4-sd-qemu.img
|
||||
size=$$(stat -c %s rpi4-sd-qemu.img); p=$$(($(RPI4_QEMU_CARD_MIB) * 1048576)); \
|
||||
while [ $$p -lt $$size ]; do p=$$((p * 2)); done; truncate -s $$p rpi4-sd-qemu.img
|
||||
end=$$(PATH=$$PATH:/usr/sbin:/sbin sfdisk -d montauk-rpi4.img | awk -F'[=,]' '/montauk-rpi4.img1/ {print $$2 + $$4}'); \
|
||||
dd if=montauk-rpi4.img of=rpi4-esp-qemu.img bs=512 count=$$end status=none
|
||||
PATH=$$PATH:/usr/sbin:/sbin sfdisk --quiet --delete rpi4-esp-qemu.img 2 2>/dev/null || true
|
||||
qemu-system-aarch64 \
|
||||
-M virt,gic-version=2 \
|
||||
-cpu cortex-a72 \
|
||||
-device ramfb \
|
||||
-device qemu-xhci,id=xhci \
|
||||
-device usb-hub,bus=xhci.0,port=1 \
|
||||
-device usb-kbd,bus=xhci.0,port=1.1 \
|
||||
-device usb-mouse,bus=xhci.0,port=1.2 \
|
||||
-bios $(AARCH64_EFI_FIRMWARE) \
|
||||
-drive if=none,id=esp,format=raw,file=rpi4-esp-qemu.img \
|
||||
-device virtio-blk-pci,drive=esp \
|
||||
-device sdhci-pci \
|
||||
-drive if=none,id=sd,format=raw,file=rpi4-sd-qemu.img \
|
||||
-device sd-card,drive=sd \
|
||||
-serial stdio \
|
||||
$(QEMUFLAGS)
|
||||
|
||||
.PHONY: run-riscv64
|
||||
@@ -196,13 +265,23 @@ ports: programs
|
||||
fi
|
||||
|
||||
.PHONY: ramdisk
|
||||
ifeq ($(KERNEL_ONLY),1)
|
||||
ramdisk: limine/limine kernel
|
||||
rm -rf .kernel-only-root
|
||||
mkdir -p .kernel-only-root/boot/limine
|
||||
cp -v limine/$(EFI_LOADER_$(ARCH)) limine.conf .kernel-only-root/boot/limine/
|
||||
cp -v kernel/bin-$(ARCH)/kernel .kernel-only-root/boot/
|
||||
./scripts/mkramdisk.sh .kernel-only-root ramdisk.tar
|
||||
rm -rf .kernel-only-root
|
||||
else
|
||||
ramdisk: limine/limine kernel programs
|
||||
mkdir -p programs/bin/boot/limine
|
||||
cp -v limine/BOOTX64.EFI programs/bin/boot/limine/
|
||||
cp -v limine.conf programs/bin/boot/limine/
|
||||
cp -v kernel/bin-$(ARCH)/kernel programs/bin/boot/
|
||||
rm -f programs/bin/boot/ramdisk.tar
|
||||
./scripts/mkramdisk.sh programs/bin ramdisk.tar
|
||||
mkdir -p $(PROGRAMS_BIN)/boot/limine
|
||||
cp -v limine/$(EFI_LOADER_$(ARCH)) $(PROGRAMS_BIN)/boot/limine/
|
||||
cp -v limine.conf $(PROGRAMS_BIN)/boot/limine/
|
||||
cp -v kernel/bin-$(ARCH)/kernel $(PROGRAMS_BIN)/boot/
|
||||
rm -f $(PROGRAMS_BIN)/boot/ramdisk.tar
|
||||
./scripts/mkramdisk.sh $(PROGRAMS_BIN) ramdisk.tar
|
||||
endif
|
||||
|
||||
$(IMAGE_NAME).iso: limine/limine kernel ramdisk
|
||||
rm -rf iso_root
|
||||
@@ -261,8 +340,8 @@ release: limine/limine kernel ramdisk
|
||||
rm -rf iso_root .release-tmp
|
||||
mkdir -p iso_root/boot
|
||||
cp kernel/bin-$(ARCH)/kernel iso_root/boot/
|
||||
strip iso_root/boot/kernel
|
||||
cp -r programs/bin .release-tmp
|
||||
$(STRIP) iso_root/boot/kernel
|
||||
cp -r $(PROGRAMS_BIN) .release-tmp
|
||||
# A release ISO ships only in-tree apps, so drop any out-of-tree port bundles
|
||||
# that happen to be staged in programs/bin/apps (each carries a .port marker).
|
||||
# Pass RELEASE_WITH_PORTS=1 to deliberately cut a ports-included image.
|
||||
@@ -286,11 +365,11 @@ ifeq ($(RELEASE_WITHOUT_SDK),1)
|
||||
.release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk
|
||||
endif
|
||||
rm -f $(addprefix .release-tmp/os/,$(RELEASE_EXCLUDE_PROGRAMS))
|
||||
find .release-tmp -name '*.elf' -exec strip {} +
|
||||
find .release-tmp -name '*.elf' -exec $(STRIP) {} +
|
||||
# The ramdisk carries its own copy of the kernel at 0:/boot/kernel -- that is
|
||||
# the image the Installer lays down on the target disk -- and it is not named
|
||||
# *.elf, so the find above misses it. Unstripped it is ~6 MB against ~0.7 MB.
|
||||
strip .release-tmp/boot/kernel
|
||||
$(STRIP) .release-tmp/boot/kernel
|
||||
./scripts/mkramdisk.sh .release-tmp iso_root/boot/ramdisk.tar
|
||||
rm -rf .release-tmp
|
||||
mkdir -p iso_root/boot/limine
|
||||
@@ -336,6 +415,20 @@ ifeq ($(ARCH),loongarch64)
|
||||
endif
|
||||
rm -rf iso_root
|
||||
|
||||
# Raspberry Pi 4/400 SD card image (UEFI firmware + Limine + MontaukOS).
|
||||
# ROOTFS=ext2 (default) puts the system on an ext2 partition read through the
|
||||
# kernel's SD driver; ROOTFS=ramdisk has Limine load ramdisk.tar instead.
|
||||
# Phony so switching ROOTFS always rebuilds the image.
|
||||
ROOTFS ?= ext2
|
||||
.PHONY: rpi4-sd montauk-rpi4.img
|
||||
rpi4-sd: montauk-rpi4.img
|
||||
|
||||
montauk-rpi4.img: limine/limine kernel ramdisk
|
||||
ifneq ($(ARCH),aarch64)
|
||||
$(error rpi4-sd needs ARCH=aarch64)
|
||||
endif
|
||||
./scripts/mk-rpi4-sd.sh kernel/bin-$(ARCH)/kernel ramdisk.tar $@ $(ROOTFS) $(PROGRAMS_BIN)-rpi
|
||||
|
||||
$(IMAGE_NAME).hdd: limine/limine kernel ramdisk
|
||||
rm -f $(IMAGE_NAME).hdd
|
||||
dd if=/dev/zero bs=1M count=0 seek=64 of=$(IMAGE_NAME).hdd
|
||||
@@ -367,7 +460,7 @@ endif
|
||||
clean:
|
||||
$(MAKE) -C kernel clean
|
||||
# $(MAKE) -C programs clean
|
||||
rm -rf iso_root .release-tmp $(IMAGE_NAME).iso $(IMAGE_NAME).hdd ramdisk.tar
|
||||
rm -rf iso_root .release-tmp $(IMAGE_NAME).iso $(IMAGE_NAME).hdd montauk-rpi4.img rpi4-sd-qemu.img rpi4-esp-qemu.img ramdisk.tar
|
||||
|
||||
.PHONY: distclean
|
||||
distclean:
|
||||
|
||||
+103
-13
@@ -36,7 +36,7 @@ kernel/src/Arch/
|
||||
Platform.hpp boot-sequence hooks, platform device list, reboot
|
||||
CpuPower.hpp frequency scaling / thermal governor (may be no-ops)
|
||||
x86_64/ the complete port
|
||||
aarch64/ skeleton port
|
||||
aarch64/ the port in progress (see below)
|
||||
```
|
||||
|
||||
Other interfaces a port implements:
|
||||
@@ -85,25 +85,115 @@ are `Cpu.hpp`, `IoPort.hpp`, `PerCpu.hpp`, `Context.hpp`, `Abi.hpp` and
|
||||
- Interrupts are numbered IRQ slots (`Arch::IRQ_COUNT`); slot numbers for
|
||||
MSI-capable drivers are fixed in the drivers.
|
||||
|
||||
## The aarch64 skeleton
|
||||
## The aarch64 port
|
||||
|
||||
Real: CPU primitives (DAIF, WFI, `CNTVCT_EL0`, TLBI, TTBR0), per-CPU data via
|
||||
`TPIDR_EL1`, the context switch and EL0 entry (`Context.S`), TLS through
|
||||
`TPIDR_EL0`, the ELF/TLS/exit-stub ABI (syscall number in x8), the IRQ slot
|
||||
dispatch table (`ArchIrqDispatch`), panic-frame printing.
|
||||
dispatch table (`ArchIrqDispatch`), panic-frame printing, and:
|
||||
|
||||
Not yet written (each is marked `TODO(aarch64)`):
|
||||
- Exception vectors (`Vectors.S`, installed in `EarlyInitialize`). Every
|
||||
exception saves one 288-byte frame that is both `Arch::PanicFrame` and
|
||||
`Arch::SyscallFrame`. EL0 `svc` goes to `SyscallDispatch`; other
|
||||
synchronous exceptions go to `ArchSyncException` (`Exceptions.cpp`), which
|
||||
grows user stacks and lazy anonymous memory, kills faulting processes with
|
||||
a crash report, and panics on kernel faults.
|
||||
- Translation tables (`Paging.cpp`): 4 KiB granule, 48-bit VAs. The kernel
|
||||
half lives in its own TTBR1 table; `KernelRoot()` is the TTBR0 table of
|
||||
kernel contexts (EFI identity mappings only, so null pointers fault). The
|
||||
HHDM maps only the memory map's RAM and firmware regions, never the holes
|
||||
between them (device space on ARM). MAIR: 0 Normal WB, 1 Normal NC
|
||||
(`MapWC`), 2 Device-nGnRE (`MapMMIO`).
|
||||
- GICv2 (`Interrupts.cpp`, from the MADT): IRQ slots map onto INTIDs --
|
||||
`IRQ_TIMER` onto the generic timer's PPI, the MSI slots (24-45) onto the
|
||||
SPIs of a GICv2m MSI frame (`ComposeMsi`), the IPI slots onto SGIs 0/1,
|
||||
and the platform lines (1-23) onto the SPIs `AssignPlatformLine` gives
|
||||
them (see "Interrupt lines" below).
|
||||
Interrupts are completed (EOI) before the handler runs, as on x86; a
|
||||
level-triggered source drops its line first in an ack hook.
|
||||
- Generic timer tick (`Timer.cpp`): the EL1 virtual timer at
|
||||
`CNTFRQ_EL0`, its PPI from the GTDT, re-armed from the ack hook.
|
||||
- PSCI `SYSTEM_RESET` / `SYSTEM_OFF` (`Arch::Reboot` / `Arch::PowerOff`)
|
||||
through the conduit the FADT's ARM_BOOT_ARCH names. Hardware-reduced
|
||||
ACPI has no PM1 block, so `AcpiShutdown` leaves power-off to the port.
|
||||
- `Arch::SyncInstructionCache` after the ELF loader writes code: ARM
|
||||
instruction fetch does not snoop data writes (QEMU hides this; real
|
||||
cores do not). EL0 may maintain caches itself (`SCTLR_EL1.UCI`).
|
||||
|
||||
- `Paging.cpp`: translation tables and MAIR (every method panics).
|
||||
- Exception vectors (`VBAR_EL1`): synchronous faults, `svc`, IRQs.
|
||||
- GIC driver: SPI routing, SGIs for IPIs, MSIs via ITS/GICv2m
|
||||
(`ComposeMsi` returns false, so drivers fall back to INTx/polling).
|
||||
- Generic timer tick (`Arch::Timer::InitializeBsp` returns false).
|
||||
- Secondary CPU start-up, PSCI reset and suspend.
|
||||
- SMP (`Smp.cpp`): Limine wakes each secondary CPU (PSCI `CPU_ON`
|
||||
underneath) on the bootloader's tables; it programs its own system
|
||||
registers and the kernel translation regime (`Bringup.hpp`), its banked
|
||||
GIC CPU interface and timer PPI, then idles until a reschedule SGI.
|
||||
TLB maintenance is broadcast (`tlbi ...is`).
|
||||
|
||||
Building it needs `clang` and `ld.lld` (`make kernel ARCH=aarch64`;
|
||||
`KERNEL_LD=` points at a different `ld.lld`, e.g. rustup's
|
||||
`lib/rustlib/x86_64-unknown-linux-gnu/bin/gcc-ld/ld.lld`).
|
||||
The generic kernel is compiled `-fsigned-char` for AArch64: it was written
|
||||
for x86's signed plain `char`.
|
||||
|
||||
Building it needs `clang` and `ld.lld`; without a system `ld.lld` the
|
||||
kernel makefile uses the one rustup ships
|
||||
(`~/.rustup/toolchains/*/lib/rustlib/*/bin/gcc-ld/ld.lld`), and `KERNEL_LD=`
|
||||
overrides both. `make run ARCH=aarch64` boots the full image (desktop, apps,
|
||||
USB keyboard and mouse) on QEMU `virt` with a GICv2 and Cortex-A72 cores, the
|
||||
Raspberry Pi 4/400's configuration; it needs `qemu-system-aarch64` and
|
||||
`qemu-efi-aarch64`, and the userspace compiler
|
||||
(`./toolchain/build-montauk-toolchain.sh aarch64`). `KERNEL_ONLY=1` builds an
|
||||
image without userspace.
|
||||
|
||||
The kernel console is mirrored to the serial port the ACPI SPCR table names
|
||||
(PL011, SBSA UART, 16550 or the BCM2835 mini UART; `Drivers/Serial/Uart.cpp`),
|
||||
including the log written before the port was found. On QEMU that is
|
||||
`-serial stdio`.
|
||||
|
||||
## Interrupt lines
|
||||
|
||||
Drivers for PCI functions without MSI read the Interrupt Line register and
|
||||
pass it to `Arch::EnableLegacyIrq`. On x86 the firmware fills that register
|
||||
with the function's I/O APIC input (`Arch::PlatformLinesAreFixed()`). On
|
||||
other platforms `Pci::AssignInterruptLines` (`Pci/PciAcpi.cpp`) fills it
|
||||
instead: it finds each pin's global system interrupt in the root bridge's
|
||||
`_PRT` (swizzling through PCI-to-PCI bridges, resolving PNP0C0F link
|
||||
devices through `_CRS`), binds the GSI to a platform line with
|
||||
`Hal::GsiRouting::LineForGsi` and writes the line number into the register.
|
||||
Devices that ACPI describes directly (platform devices) get their line from
|
||||
`LineForGsi` too.
|
||||
|
||||
## DMA
|
||||
|
||||
`Memory/Dma.hpp` serves devices whose DMA does not snoop the CPU caches
|
||||
(ACPI `_CCA = 0`): pages for rings and descriptors come uncached, and
|
||||
`SyncForDevice` / `SyncForCpu` clean and invalidate streaming buffers
|
||||
around a transfer (`Arch::CleanDataCache` / `InvalidateDataCache`). For
|
||||
coherent devices every call is the plain allocation or a no-op. The xHCI
|
||||
driver uses it; it also orders its TRB writes with `Arch::DmaWriteFence`
|
||||
(the cycle bit last, a control TD's Setup TRB after its Data and Status
|
||||
TRBs) and its event reads with `Arch::DmaReadFence`. Those fences cost
|
||||
nothing on x86 and are required on ARM, where stores to normal memory and
|
||||
device registers may otherwise be observed out of order. The other PCI
|
||||
drivers (AHCI, NVMe, e1000, HDA) do not have them yet: they work on QEMU,
|
||||
which does not reorder, but need them before they meet real ARM hardware.
|
||||
|
||||
## Raspberry Pi 4 / 400
|
||||
|
||||
See `docs/raspberry-pi.md`: `make ARCH=aarch64 rpi4-sd`, the firmware it
|
||||
expects and how the Pi's hardware is reached.
|
||||
|
||||
## Userspace
|
||||
|
||||
The programs are architecture-neutral C/C++ built by `<arch>-montauk-gcc`
|
||||
(see `toolchain/README.md`). `programs/mk/arch.mk` holds everything
|
||||
architecture-specific in the build: the toolchain prefix, FP/SIMD flag sets
|
||||
(`ARCH_FP_FLAGS` for GUI apps, `ARCH_NOFP_FLAGS` for system programs) and the
|
||||
output layout. x86_64 keeps `bin/`, `obj/` and libraries next to their
|
||||
sources; other architectures build beside them into `bin-<arch>/`,
|
||||
`obj-<arch>/` and `lib/<name>/build-<arch>/`, so both can be built from one
|
||||
tree. The few architecture-specific sources are the raw system-call
|
||||
wrappers (`#if defined(__x86_64__)` / `__aarch64__` blocks in
|
||||
`montauk/syscall.h`, `libc/montauk.h`, `lib/libc/libc.c` and
|
||||
`lib/libc/crt/crt1.c`), `lib/libc/arch/setjmp-<arch>.S` and the scanout
|
||||
store fence in `gui/framebuffer.hpp`.
|
||||
|
||||
Not built for AArch64: tcc (it generates x86 code), the on-OS SDK and the
|
||||
Rust programs (their `x86_64-unknown-montauk` target).
|
||||
|
||||
## Adding another architecture
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# Raspberry Pi 4 / 400
|
||||
|
||||
MontaukOS runs on the Raspberry Pi 4 and 400 through the Pi Firmware Task
|
||||
Force's UEFI firmware ([pftf/RPi4](https://github.com/pftf/RPi4), an EDK2
|
||||
build) in ACPI mode, booting Limine like any other UEFI machine.
|
||||
|
||||
## Building and writing the SD card
|
||||
|
||||
```bash
|
||||
./toolchain/build-montauk-toolchain.sh aarch64 # once
|
||||
make ARCH=aarch64 rpi4-sd # -> montauk-rpi4.img
|
||||
sudo dd if=montauk-rpi4.img of=/dev/sdX bs=4M conv=fsync
|
||||
```
|
||||
|
||||
`scripts/mk-rpi4-sd.sh` downloads the firmware release once into
|
||||
`rpi4-firmware/` (`PFTF_VERSION=v1.53` by default) and builds an MBR image:
|
||||
|
||||
| Partition | Type | Contents |
|
||||
|---|---|---|
|
||||
| 1 | 0xEF, FAT32 | the firmware files, Limine (`EFI/BOOT/BOOTAA64.EFI`), `boot/kernel` |
|
||||
| 2 | 0x83, ext2, label `MONTAUKROOT` | the system tree (the contents of `ramdisk.tar`) |
|
||||
|
||||
Limine loads only the kernel; the `limine.conf` on the card has no ramdisk
|
||||
module. Finding no ramdisk, the kernel mounts the partition labelled
|
||||
`MONTAUKROOT` as `0:/` (`Fs/Boot.cpp`), read through the SD driver
|
||||
(`Drivers/Storage/Sdhci`), and the ESP after it. Files load as they are
|
||||
used instead of the firmware reading the whole 80 MB tree before boot.
|
||||
Unlike the ramdisk, the root is persistent: changes to `0:/` survive a
|
||||
reboot, so shut down from the desktop rather than pulling power.
|
||||
The image is sized to its contents (`ROOT_FREE_MIB`, default 64, sets the
|
||||
free space on the root) and grows to fill the card on first boot.
|
||||
|
||||
### First boot: growing the root
|
||||
|
||||
Only the SD image ships `0:/os/firstboot.elf` (`programs/src/firstboot`,
|
||||
built into `programs/bin-aarch64-rpi/`, which the script lays over the
|
||||
system tree), and the script adds its init service, ahead of all others,
|
||||
and its capability grant (`storage_admin`, `system_image`) to the image's
|
||||
`init.toml` and `capabilities.toml`. On first boot it shows a status card
|
||||
in the login screen's style while it calls `SYS_FS_GROW` on drive 0 in a
|
||||
loop:
|
||||
|
||||
1. The kernel grows the MBR entry of the root partition as far as the
|
||||
filesystem can use (to the end of the card, or 1 TiB), on a 1 MiB
|
||||
boundary (`Gpt::ResizeMbrPartition`).
|
||||
2. The ext2 driver grows the mounted volume (`Fs/Ext2.cpp`, "Online
|
||||
grow"): each call writes the bitmaps and zeroed inode tables of a few
|
||||
new block groups past the old end, then the last call commits the
|
||||
bitmaps, descriptor table and superblock. The descriptor table grows
|
||||
into the blocks `mke2fs -O resize_inode -E resize=2^28` reserved; the
|
||||
leftover reservation is freed and `resize_inode` cleared, so the volume
|
||||
grows once.
|
||||
|
||||
The new groups' inode tables are the bulk of the writes (0.1% of the card,
|
||||
one inode per 256 KiB), written as long multi-block runs. Once the volume has grown, `firstboot.elf` deletes
|
||||
itself and init skips its (optional) service from then on. If it fails it
|
||||
stays and the next boot tries again; an interrupted grow is safe, since the
|
||||
new groups are only referenced once the superblock is written, and a
|
||||
partition larger than its filesystem is valid.
|
||||
|
||||
`make ARCH=aarch64 rpi4-sd ROOTFS=ramdisk` builds the old layout instead:
|
||||
one FAT32 partition with `boot/ramdisk.tar`, which Limine loads through the
|
||||
firmware. It boots slowly but does not depend on the kernel's SD driver.
|
||||
|
||||
`make ARCH=aarch64 run-rpi4-sd` boots the ext2 image on QEMU with the card
|
||||
on `sdhci-pci`, so the root comes through the same driver. QEMU's UEFI has
|
||||
no SD driver, so the card's ESP is also attached on its own over virtio for
|
||||
the firmware to load Limine from. The card is padded to
|
||||
`RPI4_QEMU_CARD_MIB` (default 4096), and every run starts from a fresh
|
||||
copy, so every run is a first boot.
|
||||
|
||||
The firmware defaults are what MontaukOS needs; leave them:
|
||||
|
||||
- **System Table Mode: ACPI.** The kernel reads ACPI only.
|
||||
- **Limit RAM to 3 GB: enabled.** The BCM2711's PCIe can only reach the
|
||||
first 3 GB (its tables say so in `_DMA`). The kernel warns at boot when
|
||||
RAM extends past a device's `_DMA` range.
|
||||
|
||||
## Serial console
|
||||
|
||||
The kernel console (the whole boot log, including what was printed before
|
||||
the port was found) is mirrored to the UART the firmware's SPCR table names.
|
||||
The firmware's `config.txt` applies the `miniuart-bt` overlay, which puts the
|
||||
PL011 on GPIO 14/15: header pins 8 (TX) and 10 (RX), ground on pin 6, 115200
|
||||
8N1, 3.3 V levels. On the Pi 400 the header is on the back.
|
||||
|
||||
## How the hardware is reached
|
||||
|
||||
| Part | Source | Driver |
|
||||
|---|---|---|
|
||||
| 4x Cortex-A72 | MADT, PSCI (TF-A, SMC) | `Arch/aarch64` SMP via Limine MP |
|
||||
| GIC-400 (GICv2) | MADT | `Arch/aarch64/Interrupts.cpp` |
|
||||
| Generic timer | GTDT | `Arch/aarch64/Timer.cpp` |
|
||||
| Display | UEFI GOP (VideoCore) | Limine framebuffer |
|
||||
| Keyboard (built in), USB ports | VL805 xHCI as ACPI `\_SB.SCB0.XHC0` (`PNP0D10`) | `Drivers/USB/Xhci.cpp` platform probe |
|
||||
| USB 2 devices (built-in keyboard too) | VIA hub on the VL805's USB 2 root port | `Drivers/USB/Hub.cpp` |
|
||||
| Reset, power-off | PSCI | `Arch::Reboot`, `Arch::PowerOff` |
|
||||
|
||||
The VL805 sits behind the BCM2711's PCIe, whose config space is not ECAM
|
||||
(the root bridge's `_DSD` says `linux-ecam-quirk-id = "bcm2711"`), so the
|
||||
kernel does not enumerate PCI there. The firmware has already brought up the
|
||||
link and the VL805 and describes the controller as a platform device: a
|
||||
register block at `0x600000000`, GSI 175, and `_CCA = 0`, i.e. its DMA does
|
||||
not snoop the CPU caches. `XHC0._INI` (run by the AML interpreter) enables
|
||||
the controller's PCI memory decoding and bus mastering. The xHCI driver then
|
||||
keeps its rings and contexts in uncached memory, bounces control-transfer
|
||||
data through an uncached page, and cleans/invalidates the caller buffers of
|
||||
bulk transfers (`Memory/Dma.hpp`). It also watches the event ring from the
|
||||
timer tick, so input keeps working if the INTx line never fires.
|
||||
|
||||
The VL805 has one USB 2 root port (port 1) and four USB 3 ones. An on-board
|
||||
VIA hub (`2109:3431`) sits on the USB 2 port, and the USB 2 lines of every
|
||||
socket, and the Pi 400's internal keyboard (`04d9:0007`), hang off it: a
|
||||
keyboard or mouse is always a low/full-speed device behind a high-speed hub,
|
||||
reached through that hub's transaction translator.
|
||||
|
||||
## Not supported yet
|
||||
|
||||
- Ethernet (BCM54213 via GENET v5, `BCM6E4E`).
|
||||
- SD high-speed mode: the driver transfers by polled PIO at 25 MHz on a
|
||||
4-bit bus (multi-block CMD18/CMD25 with Auto CMD12), at most 12.5 MB/s;
|
||||
switching the card to 50 MHz (CMD6) would double that.
|
||||
- Wi-Fi and Bluetooth (CYW43455 over SDIO/UART).
|
||||
- Audio (HDMI via VideoCore, the 3.5 mm jack via PWM).
|
||||
|
||||
## Status
|
||||
|
||||
Everything above is verified on QEMU only, including the Pi-specific path:
|
||||
the firmware's own XHC0 table, retargeted at QEMU's xHCI and loaded with the
|
||||
`bcm2711` quirk on QEMU's root bridge, boots to the desktop with non-coherent
|
||||
DMA. Real hardware has not been tried yet. The ext2 root on the SD card boots on QEMU (`run-rpi4-sd`); the
|
||||
first-boot grow has been tested only against the kernel's ext2 code in a
|
||||
host harness (images grown, then checked with `e2fsck`).
|
||||
+6
-2
@@ -153,8 +153,10 @@ ifeq ($(ARCH),aarch64)
|
||||
override CXX += \
|
||||
-target aarch64-unknown-none
|
||||
endif
|
||||
# Plain char is signed on x86; the generic kernel is written for that.
|
||||
override SHARED_FLAGS += \
|
||||
-mgeneral-regs-only
|
||||
-mgeneral-regs-only \
|
||||
-fsigned-char
|
||||
override LDFLAGS += \
|
||||
-Wl,-m,aarch64elf
|
||||
endif
|
||||
@@ -291,7 +293,9 @@ cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/*
|
||||
# so a clang-built non-x86 kernel is linked by ld.lld directly.
|
||||
ifneq ($(ARCH),x86_64)
|
||||
ifeq ($(CXX_IS_CLANG),1)
|
||||
KERNEL_LD ?= ld.lld
|
||||
# Without a system lld, fall back to the one rustup ships.
|
||||
KERNEL_LD ?= $(firstword $(shell command -v ld.lld) \
|
||||
$(wildcard $(HOME)/.rustup/toolchains/*/lib/rustlib/*/bin/gcc-ld/ld.lld))
|
||||
override LD_EMULATION_aarch64 := aarch64elf
|
||||
override LD_EMULATION_riscv64 := elf64lriscv
|
||||
override LD_EMULATION_loongarch64 := elf64loongarch
|
||||
|
||||
@@ -38,8 +38,24 @@ namespace Hal {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map and parse the DSDT to find \_S5_
|
||||
auto* dsdt = (void*)Memory::HHDM(fadt.DsdtAddress);
|
||||
|
||||
AML::InitializeInterpreter(dsdt, xsdt);
|
||||
|
||||
if (AML::GetInterpreter().IsInitialized()) {
|
||||
AcpiDevices::DeviceList devices{};
|
||||
AcpiDevices::EnumerateAll(devices);
|
||||
}
|
||||
|
||||
AcpiSleep::Initialize(xsdt);
|
||||
|
||||
// Hardware-reduced ACPI (ARM) has no PM1 registers: the
|
||||
// architecture powers off instead (Arch::PowerOff).
|
||||
if ((fadt.Flags & FADT::FlagHardwareReduced) || fadt.PM1aControlBlock == 0) {
|
||||
KernelLogStream(INFO, "ACPI") << "No PM1 control block - ACPI S5 shutdown unavailable";
|
||||
return;
|
||||
}
|
||||
|
||||
AML::S5Object s5 = AML::FindS5(dsdt);
|
||||
|
||||
if (!s5.Valid) {
|
||||
@@ -55,23 +71,6 @@ namespace Hal {
|
||||
|
||||
KernelLogStream(OK, "ACPI") << "ACPI shutdown initialized (PM1a=" << base::hex
|
||||
<< (uint64_t)g_pm1aControlBlock << " SLP_TYPa=" << (uint64_t)g_slpTypA << ")";
|
||||
|
||||
// Initialize the full AML interpreter with the DSDT.
|
||||
// This loads the entire DSDT namespace, enabling device enumeration,
|
||||
// method evaluation, and field I/O for the rest of the kernel.
|
||||
AML::InitializeInterpreter(dsdt, xsdt);
|
||||
|
||||
// Enumerate ACPI devices now that the namespace is loaded
|
||||
if (AML::GetInterpreter().IsInitialized()) {
|
||||
AcpiDevices::DeviceList devices{};
|
||||
AcpiDevices::EnumerateAll(devices);
|
||||
}
|
||||
|
||||
// Initialize S3 suspend support (reads FACS and \_S3_ from namespace)
|
||||
AcpiSleep::Initialize(xsdt);
|
||||
|
||||
// Note: ACPI event handling (SCI, power button) is initialized
|
||||
// from Main.cpp after the APIC subsystem is ready.
|
||||
}
|
||||
|
||||
bool IsAvailable() {
|
||||
|
||||
@@ -107,6 +107,8 @@ namespace Hal {
|
||||
result.AcpiEnable = fadt->AcpiEnable;
|
||||
result.AcpiDisable = fadt->AcpiDisable;
|
||||
result.Flags = fadt->Flags;
|
||||
result.ArmBootArchFlags = fadt->Header.Length >= offsetof(Table, ArmBootArchFlags) + 2
|
||||
? fadt->ArmBootArchFlags : 0;
|
||||
result.Valid = true;
|
||||
|
||||
KernelLogStream(INFO, "FADT") << "DSDT at " << base::hex << result.DsdtAddress;
|
||||
|
||||
@@ -69,6 +69,13 @@ namespace Hal {
|
||||
uint8_t X_GPE1Block[12];
|
||||
} __attribute__((packed));
|
||||
|
||||
// FADT Flags
|
||||
constexpr uint32_t FlagHardwareReduced = 1u << 20;
|
||||
|
||||
// ARM_BOOT_ARCH flags (ACPI 5.1+)
|
||||
constexpr uint16_t ArmBootPsciCompliant = 1u << 0;
|
||||
constexpr uint16_t ArmBootPsciUseHvc = 1u << 1;
|
||||
|
||||
struct ParsedFADT {
|
||||
uint64_t DsdtAddress;
|
||||
uint64_t FacsAddress;
|
||||
@@ -91,6 +98,7 @@ namespace Hal {
|
||||
uint8_t AcpiEnable;
|
||||
uint8_t AcpiDisable;
|
||||
uint32_t Flags;
|
||||
uint16_t ArmBootArchFlags; // 0 when the table predates the field
|
||||
bool Valid;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* GsiRouting.cpp
|
||||
* Platform interrupt lines for ACPI global system interrupts
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "GsiRouting.hpp"
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
namespace Hal::GsiRouting {
|
||||
|
||||
static constexpr uint8_t FirstLine = 1;
|
||||
static constexpr uint8_t LineCount = 24;
|
||||
|
||||
static uint32_t g_lineGsi[LineCount] = {};
|
||||
static uint8_t g_nextLine = FirstLine;
|
||||
static kcp::Mutex g_lock;
|
||||
|
||||
uint8_t LineForGsi(uint32_t gsi, bool levelTriggered, bool activeLow) {
|
||||
if (Arch::PlatformLinesAreFixed()) return 0;
|
||||
|
||||
g_lock.Acquire();
|
||||
for (uint8_t line = FirstLine; line < g_nextLine; line++) {
|
||||
if (g_lineGsi[line] == gsi) {
|
||||
g_lock.Release();
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t line = 0;
|
||||
if (g_nextLine < LineCount &&
|
||||
Arch::AssignPlatformLine(g_nextLine, gsi, levelTriggered, activeLow)) {
|
||||
line = g_nextLine++;
|
||||
g_lineGsi[line] = gsi;
|
||||
}
|
||||
g_lock.Release();
|
||||
return line;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* GsiRouting.hpp
|
||||
* Platform interrupt lines for ACPI global system interrupts
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal::GsiRouting {
|
||||
|
||||
// The platform line (1-23) that delivers global system interrupt `gsi`,
|
||||
// assigning one on first use (Arch::AssignPlatformLine); every device on
|
||||
// one GSI shares its line. Drivers register their handler on the line
|
||||
// and call Arch::EnableLegacyIrq. Returns 0 where the platform's lines
|
||||
// are wired by the hardware (x86) or when none is left.
|
||||
uint8_t LineForGsi(uint32_t gsi, bool levelTriggered, bool activeLow);
|
||||
|
||||
}
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 26
|
||||
#define MONTAUK_BUILD_NUMBER 73
|
||||
@@ -248,6 +248,10 @@ namespace montauk::abi {
|
||||
case Drivers::Storage::BLOCK_KIND_USB_MSC:
|
||||
buf->type = 4;
|
||||
break;
|
||||
case Drivers::Storage::BLOCK_KIND_SD:
|
||||
buf->type = 5;
|
||||
buf->rpm = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,9 @@ namespace montauk::abi {
|
||||
Hal::AcpiShutdown::Shutdown();
|
||||
}
|
||||
|
||||
/* Hardware-reduced platforms (PSCI on ARM) */
|
||||
Arch::PowerOff();
|
||||
|
||||
/* Fallback: EFI ResetSystem */
|
||||
if (Efi::g_ResetSystem) {
|
||||
Memory::VMM::ActivateKernelAddressSpace();
|
||||
|
||||
@@ -163,6 +163,45 @@ namespace montauk::abi {
|
||||
return (int64_t)Fs::FsProbe::SyncAndUnmountAll();
|
||||
}
|
||||
|
||||
// Grow a mounted volume to fill the free space after its partition: the
|
||||
// MBR entry first, only as far as the filesystem can use, then the
|
||||
// filesystem a slice per call. Interrupted grows resume from the next
|
||||
// call (or boot), since a partition larger than its filesystem is valid.
|
||||
static int64_t Sys_FsGrow(int drive, FsGrowStatus* out, bool apply) {
|
||||
int partIndex = Fs::FsProbe::GetPartitionForDrive(drive);
|
||||
auto* part = Drivers::Storage::Gpt::GetPartition(partIndex);
|
||||
if (part == nullptr) return UnsupportedOp; // ramdisk, or no such drive
|
||||
|
||||
uint64_t maxSectors = Drivers::Storage::Gpt::GetMbrPartitionMaxSectors(partIndex);
|
||||
if (maxSectors < part->SectorCount) maxSectors = part->SectorCount;
|
||||
|
||||
Fs::Vfs::GrowStatus gs{};
|
||||
int rc = Fs::Vfs::VfsGrow(drive, maxSectors, false, &gs);
|
||||
if (rc == -2) return UnsupportedOp;
|
||||
if (rc < 0) return IoFailure;
|
||||
|
||||
// Leave whatever the filesystem cannot use unpartitioned, with the end
|
||||
// on a 1 MiB boundary like the partitions mk-rpi4-sd.sh makes.
|
||||
uint64_t wanted = (part->StartLba + (gs.targetBytes + 511) / 512 + 2047) / 2048 * 2048
|
||||
- part->StartLba;
|
||||
if (wanted > maxSectors) wanted = maxSectors;
|
||||
|
||||
if (apply && wanted > part->SectorCount) {
|
||||
if (Drivers::Storage::Gpt::ResizeMbrPartition(partIndex, wanted) != 0) return IoFailure;
|
||||
}
|
||||
|
||||
rc = apply ? Fs::Vfs::VfsGrow(drive, part->SectorCount, true, &gs) : 0;
|
||||
if (rc == -2) return UnsupportedOp;
|
||||
if (rc < 0) return IoFailure;
|
||||
|
||||
out->fsBytes = gs.fsBytes;
|
||||
out->partBytes = part->SectorCount * 512;
|
||||
out->targetBytes = gs.targetBytes;
|
||||
out->stepsDone = gs.stepsDone;
|
||||
out->stepsTotal = gs.stepsTotal;
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Format a partition with a filesystem. Returns 0 on success, GenericFailure on error.
|
||||
static int64_t Sys_FsFormat(const FsFormatParams* params) {
|
||||
if (params == nullptr) return InvalidArgument;
|
||||
|
||||
@@ -454,6 +454,10 @@ namespace montauk::abi {
|
||||
case SYS_FS_SYNC:
|
||||
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
||||
return Sys_FsSync();
|
||||
case SYS_FS_GROW:
|
||||
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::Range(frame->arg2, sizeof(FsGrowStatus), true)) return BadPointer;
|
||||
return Sys_FsGrow((int)frame->arg1, (FsGrowStatus*)frame->arg2, frame->arg3 != 0);
|
||||
case SYS_FSFORMAT:
|
||||
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return BadPointer;
|
||||
|
||||
@@ -400,6 +400,11 @@ namespace montauk::abi {
|
||||
static constexpr uint64_t SYS_WIN_DRAG_FORMATS = 206; // (serial, TransferFormatInfo*, max) -> count
|
||||
static constexpr uint64_t SYS_WIN_DRAG_GET_DATA = 207; // (serial, mime, buf, max, uint32_t* len) -> status
|
||||
|
||||
/* Storage.hpp -- grow a mounted volume (and its MBR partition) to fill the
|
||||
free space after it. apply=0 only reports; apply=1 does one bounded slice
|
||||
and returns 1 while more remains, 0 once done. CAP_STORAGE_ADMIN. */
|
||||
static constexpr uint64_t SYS_FS_GROW = 208; // (drive, FsGrowStatus*, apply) -> 1 / 0 / status
|
||||
|
||||
/* Kernel-owned process capabilities. User identities may namespace
|
||||
per-user resources, but never participate in authorization decisions. */
|
||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||
@@ -915,7 +920,7 @@ namespace montauk::abi {
|
||||
|
||||
struct DiskInfo {
|
||||
uint8_t port; // block device index
|
||||
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage
|
||||
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage, 5=SD card
|
||||
uint8_t sataGen; // SATA gen (1/2/3)
|
||||
uint8_t _pad0;
|
||||
uint64_t sectorCount; // Total user-addressable sectors
|
||||
@@ -997,6 +1002,14 @@ namespace montauk::abi {
|
||||
char label[32]; // volume label
|
||||
};
|
||||
|
||||
struct FsGrowStatus {
|
||||
uint64_t fsBytes; // filesystem size now
|
||||
uint64_t partBytes; // partition size now
|
||||
uint64_t targetBytes; // filesystem size once grown; == fsBytes if it cannot grow
|
||||
uint32_t stepsDone; // progress of the grow, out of stepsTotal
|
||||
uint32_t stepsTotal;
|
||||
};
|
||||
|
||||
struct ProcInfo {
|
||||
int32_t pid;
|
||||
int32_t parentPid;
|
||||
|
||||
@@ -36,6 +36,15 @@ namespace Arch {
|
||||
// Full read/write memory barrier.
|
||||
inline void MemoryFence();
|
||||
|
||||
// Order this CPU's earlier writes to DMA memory (descriptors, buffers)
|
||||
// before a later write the device acts on: the descriptor's valid/cycle
|
||||
// bit, or a doorbell register. x86 keeps stores in order already.
|
||||
inline void DmaWriteFence();
|
||||
|
||||
// Order the read of a device-written valid/cycle/status field before the
|
||||
// reads of the data it guards.
|
||||
inline void DmaReadFence();
|
||||
|
||||
// ---- Interrupt state ----
|
||||
|
||||
inline void EnableInterrupts();
|
||||
@@ -79,6 +88,18 @@ namespace Arch {
|
||||
// Write back and invalidate all CPU caches (before power state changes).
|
||||
inline void WritebackInvalidateCaches();
|
||||
|
||||
// Cache maintenance for devices whose DMA does not snoop the CPU caches
|
||||
// (ACPI _CCA = 0). Clean: write dirty lines back so the device reads
|
||||
// what the CPU wrote. Invalidate: write back and discard, so the CPU
|
||||
// then reads what the device wrote. No-ops where DMA is coherent (x86).
|
||||
inline void CleanDataCache(const void* address, uint64_t size);
|
||||
inline void InvalidateDataCache(const void* address, uint64_t size);
|
||||
|
||||
// Make instructions just written to [address, address+size) (any
|
||||
// mapping of them) visible to instruction fetch on every CPU. A no-op
|
||||
// where instruction fetch snoops data writes (x86).
|
||||
inline void SyncInstructionCache(const void* address, uint64_t size);
|
||||
|
||||
// Invalidate this CPU's translation for one virtual page.
|
||||
inline void FlushTlbPage(uint64_t virtualAddress);
|
||||
|
||||
|
||||
@@ -53,6 +53,17 @@ namespace Arch {
|
||||
// Unmask platform interrupt line `irq` (already routed at boot).
|
||||
void EnableLegacyIrq(uint8_t irq);
|
||||
|
||||
// Whether the platform interrupt lines are wired by the hardware (x86:
|
||||
// ISA IRQs and I/O APIC pins, as the firmware programs them into PCI
|
||||
// Interrupt Line registers). Where they are not (ARM), the PCI layer
|
||||
// assigns lines to PCI INTx pins from ACPI _PRT with AssignPlatformLine.
|
||||
bool PlatformLinesAreFixed();
|
||||
|
||||
// Make platform line `irq` (1-23) deliver global system interrupt `gsi`
|
||||
// (ACPI numbering) with the given trigger mode and polarity, so that
|
||||
// EnableLegacyIrq(irq) unmasks it. Only where !PlatformLinesAreFixed().
|
||||
bool AssignPlatformLine(uint8_t irq, uint32_t gsi, bool levelTriggered, bool activeLow);
|
||||
|
||||
// A message-signalled interrupt targeting IRQ slot `irq` on the boot
|
||||
// CPU: the device writes `data` to `address` to raise it. Program these
|
||||
// into a PCI MSI capability or MSI-X table entry. Returns false when the
|
||||
|
||||
@@ -51,4 +51,8 @@ namespace Arch {
|
||||
// ResetSystem). Never returns.
|
||||
[[noreturn]] void Reboot();
|
||||
|
||||
// Switch the machine off where ACPI S5 does not apply (hardware-reduced
|
||||
// ACPI: ARM's PSCI SYSTEM_OFF). Returns if the platform cannot.
|
||||
void PowerOff();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Bringup.hpp
|
||||
* aarch64 per-CPU bring-up shared by the boot CPU and secondaries
|
||||
* (private to the port)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// System registers every CPU programs for itself: SP_EL1 as the stack,
|
||||
// the exception vectors, SCTLR_EL1 policy bits, FP/SIMD access.
|
||||
// Platform.cpp.
|
||||
void InitializeCpuRegisters();
|
||||
|
||||
// Load the kernel's translation regime (MAIR, TCR, TTBR1 kernel half,
|
||||
// TTBR0 kernel-context table) on the calling CPU. Paging::Init builds
|
||||
// the tables and calls it on the boot CPU. Paging.cpp.
|
||||
void ActivateKernelTranslation();
|
||||
|
||||
}
|
||||
@@ -26,6 +26,15 @@ namespace Arch {
|
||||
asm volatile("dsb sy" ::: "memory");
|
||||
}
|
||||
|
||||
// Devices are outer-shareable observers.
|
||||
inline void DmaWriteFence() {
|
||||
asm volatile("dmb oshst" ::: "memory");
|
||||
}
|
||||
|
||||
inline void DmaReadFence() {
|
||||
asm volatile("dmb oshld" ::: "memory");
|
||||
}
|
||||
|
||||
inline void EnableInterrupts() {
|
||||
asm volatile("msr daifclr, #2" ::: "memory");
|
||||
}
|
||||
@@ -92,9 +101,50 @@ namespace Arch {
|
||||
asm volatile("dsb sy\n\tisb" ::: "memory");
|
||||
}
|
||||
|
||||
// The TLBI operand is VA[55:12] in bits [43:0]; the bits above are RES0.
|
||||
inline uint64_t DataCacheLineSize() {
|
||||
uint64_t ctr;
|
||||
asm volatile("mrs %0, ctr_el0" : "=r"(ctr));
|
||||
return 4ULL << ((ctr >> 16) & 0xF); // DminLine
|
||||
}
|
||||
|
||||
// To the point of coherency: where non-snooping device DMA looks.
|
||||
inline void CleanDataCache(const void* address, uint64_t size) {
|
||||
uint64_t line = DataCacheLineSize();
|
||||
uint64_t end = (uint64_t)address + size;
|
||||
for (uint64_t p = (uint64_t)address & ~(line - 1); p < end; p += line) {
|
||||
asm volatile("dc cvac, %0" :: "r"(p) : "memory");
|
||||
}
|
||||
asm volatile("dsb sy" ::: "memory");
|
||||
}
|
||||
|
||||
// Clean too: a partial line at either end may hold CPU data next to the
|
||||
// buffer, which a plain invalidate would throw away.
|
||||
inline void InvalidateDataCache(const void* address, uint64_t size) {
|
||||
uint64_t line = DataCacheLineSize();
|
||||
uint64_t end = (uint64_t)address + size;
|
||||
for (uint64_t p = (uint64_t)address & ~(line - 1); p < end; p += line) {
|
||||
asm volatile("dc civac, %0" :: "r"(p) : "memory");
|
||||
}
|
||||
asm volatile("dsb sy" ::: "memory");
|
||||
}
|
||||
|
||||
// Clean the data cache to the point of unification, then invalidate
|
||||
// the whole instruction cache: invalidating by address would only
|
||||
// reach the alias it names on an aliasing (VIPT) instruction cache.
|
||||
inline void SyncInstructionCache(const void* address, uint64_t size) {
|
||||
uint64_t line = DataCacheLineSize();
|
||||
uint64_t start = (uint64_t)address & ~(line - 1);
|
||||
uint64_t end = (uint64_t)address + size;
|
||||
for (uint64_t p = start; p < end; p += line) {
|
||||
asm volatile("dc cvau, %0" :: "r"(p) : "memory");
|
||||
}
|
||||
asm volatile("dsb ish\n\tic ialluis\n\tdsb ish\n\tisb" ::: "memory");
|
||||
}
|
||||
|
||||
inline void FlushTlbPage(uint64_t virtualAddress) {
|
||||
asm volatile("dsb ishst\n\ttlbi vaae1is, %0\n\tdsb ish\n\tisb"
|
||||
:: "r"(virtualAddress >> 12) : "memory");
|
||||
:: "r"((virtualAddress >> 12) & ((1ULL << 44) - 1)) : "memory");
|
||||
}
|
||||
|
||||
inline void FlushTlbAll() {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Exceptions.cpp
|
||||
* aarch64 synchronous exceptions: faults, user crashes, kernel panics
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/aarch64/InterruptFrame.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Common/Panic.hpp>
|
||||
#include <Common/TerminationCodes.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Sched/CrashReport.hpp>
|
||||
#include <Api/Heap.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace {
|
||||
|
||||
// ESR_EL1.EC exception classes
|
||||
constexpr uint64_t EcUnknown = 0x00;
|
||||
constexpr uint64_t EcIllegalState = 0x0E;
|
||||
constexpr uint64_t EcInstAbortLower = 0x20;
|
||||
constexpr uint64_t EcInstAbortSame = 0x21;
|
||||
constexpr uint64_t EcPcAlignment = 0x22;
|
||||
constexpr uint64_t EcDataAbortLower = 0x24;
|
||||
constexpr uint64_t EcDataAbortSame = 0x25;
|
||||
constexpr uint64_t EcSpAlignment = 0x26;
|
||||
constexpr uint64_t EcFpException = 0x2C;
|
||||
constexpr uint64_t EcBreakpointLower = 0x30;
|
||||
constexpr uint64_t EcBrk = 0x3C;
|
||||
|
||||
constexpr uint64_t UserSpaceEnd = 0x0000800000000000ULL;
|
||||
|
||||
uint64_t ExceptionClass(const Arch::PanicFrame* frame) {
|
||||
return (frame->esr >> 26) & 0x3F;
|
||||
}
|
||||
|
||||
bool IsAbort(uint64_t ec) {
|
||||
return ec == EcInstAbortLower || ec == EcInstAbortSame
|
||||
|| ec == EcDataAbortLower || ec == EcDataAbortSame;
|
||||
}
|
||||
|
||||
// Fault status code (ISS[5:0]) of an abort: translation faults mean
|
||||
// the page is not mapped, permission faults that it is.
|
||||
bool IsTranslationFault(uint64_t esr) {
|
||||
uint64_t fsc = esr & 0x3F;
|
||||
return fsc >= 0x04 && fsc <= 0x07;
|
||||
}
|
||||
|
||||
bool IsPermissionFault(uint64_t esr) {
|
||||
uint64_t fsc = esr & 0x3F;
|
||||
return fsc >= 0x0C && fsc <= 0x0F;
|
||||
}
|
||||
|
||||
const char* ExceptionName(uint64_t ec) {
|
||||
switch (ec) {
|
||||
case EcUnknown: return "Undefined Instruction";
|
||||
case EcIllegalState: return "Illegal Execution State";
|
||||
case EcInstAbortLower:
|
||||
case EcInstAbortSame: return "Instruction Abort";
|
||||
case EcPcAlignment: return "PC Alignment Fault";
|
||||
case EcDataAbortLower:
|
||||
case EcDataAbortSame: return "Data Abort";
|
||||
case EcSpAlignment: return "SP Alignment Fault";
|
||||
case EcFpException: return "Floating-Point Exception";
|
||||
case EcBrk: return "Breakpoint (BRK)";
|
||||
default:
|
||||
if (ec >= EcBreakpointLower && ec <= 0x35) return "Debug Exception";
|
||||
return "Synchronous Exception";
|
||||
}
|
||||
}
|
||||
|
||||
TerminationCodes TerminationFromClass(uint64_t ec) {
|
||||
if (ec == EcUnknown || ec == EcIllegalState) return TerminatedIllegalInstruction;
|
||||
if (ec == EcBrk || (ec >= EcBreakpointLower && ec <= 0x35)) return TerminatedBreakpoint;
|
||||
if (ec == EcFpException) return TerminatedArithmeticError;
|
||||
return TerminatedAccessViolation;
|
||||
}
|
||||
|
||||
template<size_t N>
|
||||
void CopyName(char (&dst)[N], const char* src) {
|
||||
size_t i = 0;
|
||||
for (; i + 1 < N && src[i]; i++) dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// Kill the faulting process with a crash report. Never returns.
|
||||
[[noreturn]] void TerminateUserProcess(Arch::PanicFrame* frame, uint64_t ec) {
|
||||
// Teardown can wait on devices and other CPUs; keep the clock running.
|
||||
Arch::EnableInterrupts();
|
||||
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
const char* name = ExceptionName(ec);
|
||||
KernelLogStream(ERROR, "Exception") << name << " in process \"" << proc->name
|
||||
<< "\" (pid " << proc->pid << ", ESR 0x" << base::hex << frame->esr
|
||||
<< ", PC 0x" << frame->elr << ") - process terminated";
|
||||
|
||||
CrashReport::Report rep{};
|
||||
rep.valid = true;
|
||||
rep.pid = proc->pid;
|
||||
CopyName(rep.processName, proc->name);
|
||||
rep.exceptionVector = (uint8_t)ec;
|
||||
CopyName(rep.exceptionName, name);
|
||||
rep.instructionPointer = frame->elr;
|
||||
rep.flags = frame->spsr;
|
||||
rep.stackPointer = frame->sp;
|
||||
|
||||
constexpr uint64_t snapshotBytes = CrashReport::StackSnapshotWords * sizeof(uint64_t);
|
||||
if (Memory::VMM::Paging::IsUserRangeAccessible(proc->pageTableRoot, frame->sp,
|
||||
snapshotBytes, false)) {
|
||||
auto* userStack = (const volatile uint64_t*)frame->sp;
|
||||
for (int word = 0; word < CrashReport::StackSnapshotWords; word++)
|
||||
rep.stackWords[word] = userStack[word];
|
||||
rep.stackWordCount = CrashReport::StackSnapshotWords;
|
||||
}
|
||||
|
||||
if (IsAbort(ec)) {
|
||||
rep.faultingAddress = frame->far;
|
||||
rep.pfPresent = IsPermissionFault(frame->esr);
|
||||
rep.pfWrite = ec == EcDataAbortLower && (frame->esr & (1ULL << 6)) != 0;
|
||||
rep.pfUser = 1;
|
||||
rep.pfInstructionFetch = ec == EcInstAbortLower;
|
||||
}
|
||||
rep.timestampTick = Timekeeping::GetTicks();
|
||||
|
||||
CrashReport::AddReport(rep);
|
||||
Sched::SpawnCrashPad(proc->pid);
|
||||
|
||||
// As on x86: a sibling thread's fault must end the whole process.
|
||||
int code = TerminationFromClass(ec);
|
||||
if (Sched::GetCurrentThreadPtr() != proc) {
|
||||
Sched::KillProcess(proc->pid, code);
|
||||
} else {
|
||||
Sched::SetProcessExitCode(proc->pid, code);
|
||||
}
|
||||
|
||||
Sched::ExitProcess();
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
// A not-present user page: anonymous memory mapped lazily or the user
|
||||
// stack growing. Also reached from the kernel touching user buffers.
|
||||
bool TryResolveUserFault(const Arch::PanicFrame* frame, uint64_t ec) {
|
||||
if (!IsAbort(ec) || !IsTranslationFault(frame->esr)) return false;
|
||||
if (frame->far >= UserSpaceEnd) return false;
|
||||
|
||||
auto* cpu = Smp::TryGetCurrentCpuData();
|
||||
if (cpu == nullptr || cpu->currentSlot < 0 || Sched::GetCurrentPid() < 0) return false;
|
||||
|
||||
bool isData = ec == EcDataAbortLower || ec == EcDataAbortSame;
|
||||
Memory::VMM::PageFault fault{false, isData && (frame->esr & (1ULL << 6)) != 0, !isData};
|
||||
return montauk::abi::TryHandleAnonymousPageFault(frame->far, fault)
|
||||
|| Sched::TryGrowUserStack(frame->far);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void ArchSyncException(Arch::PanicFrame* frame, bool fromUser) {
|
||||
uint64_t ec = ExceptionClass(frame);
|
||||
|
||||
if (TryResolveUserFault(frame, ec)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromUser && Sched::GetCurrentPid() >= 0) {
|
||||
TerminateUserProcess(frame, ec);
|
||||
}
|
||||
|
||||
Panic(ExceptionName(ec), frame);
|
||||
}
|
||||
|
||||
// FIQs are never routed, SErrors are asynchronous external aborts, and the
|
||||
// kernel neither runs on SP_EL0 nor supports AArch32 user code.
|
||||
extern "C" void ArchUnexpectedException(Arch::PanicFrame* frame, uint64_t kind) {
|
||||
static const char* const names[] = {
|
||||
"Exception on SP_EL0", "Unexpected FIQ", "SError (asynchronous abort)",
|
||||
"AArch32 exception",
|
||||
};
|
||||
Panic(kind < 4 ? names[kind] : "Unexpected exception", frame);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Gic.hpp
|
||||
* aarch64 GICv2 interrupt controller (private to the port)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <ACPI/ACPI.hpp>
|
||||
|
||||
namespace Arch::Gic {
|
||||
|
||||
// Bring up the distributor and the calling CPU's interface from the
|
||||
// ACPI MADT. Returns false when no usable GIC is described.
|
||||
bool Initialize(Hal::ACPI::CommonSDTHeader* xsdt);
|
||||
|
||||
bool IsInitialized();
|
||||
|
||||
// The XSDT Initialize was given (for the other ACPI tables the port reads).
|
||||
Hal::ACPI::CommonSDTHeader* CurrentXsdt();
|
||||
|
||||
// Per-CPU part of Initialize, for secondary CPUs.
|
||||
void InitializeCpuInterface();
|
||||
|
||||
// Map a private peripheral interrupt (16-31) onto an IRQ slot and enable
|
||||
// it on the calling CPU.
|
||||
void RoutePpi(uint32_t intid, uint8_t irq, bool edgeTriggered);
|
||||
|
||||
// Called for an IRQ slot after the interrupt is acknowledged and before
|
||||
// it is completed (EOI): level-triggered sources deassert here.
|
||||
using AckHook = void (*)();
|
||||
void SetAckHook(uint8_t irq, AckHook hook);
|
||||
|
||||
// The calling CPU's target bit, as the distributor sees it.
|
||||
uint8_t CurrentTargetMask();
|
||||
|
||||
// Find an ACPI table in the XSDT by signature.
|
||||
Hal::ACPI::CommonSDTHeader* FindTable(Hal::ACPI::CommonSDTHeader* xsdt, const char* signature);
|
||||
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// What the exception vectors save before calling into C++.
|
||||
// What the exception vectors (Vectors.S) save before calling into C++.
|
||||
struct PanicFrame {
|
||||
uint64_t x[31]; // x0-x30
|
||||
uint64_t sp; // interrupted SP (SP_EL0 for EL0, else SP_EL1)
|
||||
@@ -19,4 +19,6 @@ namespace Arch {
|
||||
uint64_t far; // FAR_EL1: faulting address
|
||||
};
|
||||
|
||||
static_assert(sizeof(PanicFrame) == 288, "must match Vectors.S");
|
||||
|
||||
}
|
||||
@@ -1,52 +1,420 @@
|
||||
/*
|
||||
* Interrupts.cpp
|
||||
* aarch64 IRQ slot dispatch (implements Arch/Interrupts.hpp)
|
||||
* aarch64 GICv2 driver and IRQ slot dispatch (implements Arch/Interrupts.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Arch/aarch64/Gic.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
// IRQ slots are delivered by the interrupt controller driver (GICv2/v3,
|
||||
// not written yet), which maps INTIDs onto slots and calls ArchIrqDispatch
|
||||
// from the IRQ exception vector after acknowledging the interrupt.
|
||||
// IRQ slots map onto GIC interrupt IDs (INTIDs):
|
||||
// IRQ_TIMER the generic timer's PPI (Timer.cpp routes it)
|
||||
// 24-45 (MSI slots) SPIs of the GICv2m MSI frame, one per slot
|
||||
// IRQ_TLB_SHOOTDOWN,
|
||||
// IRQ_RESCHEDULE SGIs 0 and 1
|
||||
// 1-23 (platform lines) SPIs the PCI layer assigns to INTx pins from
|
||||
// ACPI _PRT (AssignPlatformLine)
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// Distributor registers
|
||||
static constexpr uint32_t GICD_CTLR = 0x000;
|
||||
static constexpr uint32_t GICD_TYPER = 0x004;
|
||||
static constexpr uint32_t GICD_ISENABLER = 0x100;
|
||||
static constexpr uint32_t GICD_ICENABLER = 0x180;
|
||||
static constexpr uint32_t GICD_ICPENDR = 0x280;
|
||||
static constexpr uint32_t GICD_ICACTIVER = 0x380;
|
||||
static constexpr uint32_t GICD_IPRIORITYR = 0x400;
|
||||
static constexpr uint32_t GICD_ITARGETSR = 0x800;
|
||||
static constexpr uint32_t GICD_ICFGR = 0xC00;
|
||||
static constexpr uint32_t GICD_SGIR = 0xF00;
|
||||
|
||||
// CPU interface registers
|
||||
static constexpr uint32_t GICC_CTLR = 0x00;
|
||||
static constexpr uint32_t GICC_PMR = 0x04;
|
||||
static constexpr uint32_t GICC_BPR = 0x08;
|
||||
static constexpr uint32_t GICC_IAR = 0x0C;
|
||||
static constexpr uint32_t GICC_EOIR = 0x10;
|
||||
|
||||
// GICv2m MSI frame registers
|
||||
static constexpr uint32_t V2M_MSI_TYPER = 0x008;
|
||||
static constexpr uint32_t V2M_MSI_SETSPI_NS = 0x040;
|
||||
|
||||
static constexpr uint32_t SpuriousIntid = 1020;
|
||||
static constexpr uint32_t MaxIntids = 1020;
|
||||
static constexpr uint8_t DefaultPriority = 0xA0;
|
||||
static constexpr uint8_t NoSlot = 0xFF;
|
||||
|
||||
static constexpr uint8_t FirstMsiSlot = 24;
|
||||
static constexpr uint8_t LastMsiSlot = 45;
|
||||
|
||||
// MADT interrupt-controller structures (ACPI 6.x)
|
||||
static constexpr uint8_t MadtGicc = 0x0B;
|
||||
static constexpr uint8_t MadtGicd = 0x0C;
|
||||
static constexpr uint8_t MadtGicMsiFrame = 0x0D;
|
||||
|
||||
struct MadtEntry {
|
||||
uint8_t Type;
|
||||
uint8_t Length;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MadtGiccEntry {
|
||||
MadtEntry Header;
|
||||
uint16_t Reserved;
|
||||
uint32_t CpuInterfaceNumber;
|
||||
uint32_t AcpiProcessorUid;
|
||||
uint32_t Flags;
|
||||
uint32_t ParkingProtocolVersion;
|
||||
uint32_t PerformanceInterruptGsiv;
|
||||
uint64_t ParkedAddress;
|
||||
uint64_t PhysicalBaseAddress; // GICC
|
||||
uint64_t Gicv;
|
||||
uint64_t Gich;
|
||||
uint32_t VgicMaintenanceInterrupt;
|
||||
uint64_t GicrBaseAddress;
|
||||
uint64_t Mpidr;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MadtGicdEntry {
|
||||
MadtEntry Header;
|
||||
uint16_t Reserved;
|
||||
uint32_t GicId;
|
||||
uint64_t PhysicalBaseAddress;
|
||||
uint32_t SystemVectorBase;
|
||||
uint8_t GicVersion;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MadtMsiFrameEntry {
|
||||
MadtEntry Header;
|
||||
uint16_t Reserved;
|
||||
uint32_t FrameId;
|
||||
uint64_t PhysicalBaseAddress;
|
||||
uint32_t Flags; // bit 0: SpiCount/SpiBase override TYPER
|
||||
uint16_t SpiCount;
|
||||
uint16_t SpiBase;
|
||||
} __attribute__((packed));
|
||||
|
||||
static constexpr uint32_t MadtHeaderSize = 44; // SDT header + LAPIC address + flags
|
||||
|
||||
static IrqHandler g_irqHandlers[IRQ_COUNT] = {};
|
||||
static Gic::AckHook g_ackHooks[IRQ_COUNT] = {};
|
||||
static uint8_t g_intidToSlot[MaxIntids];
|
||||
static uint32_t g_slotToIntid[IRQ_COUNT];
|
||||
|
||||
static volatile uint8_t* g_gicd = nullptr;
|
||||
static volatile uint8_t* g_gicc = nullptr;
|
||||
static uint32_t g_intidCount = 0;
|
||||
|
||||
static Hal::ACPI::CommonSDTHeader* g_xsdt = nullptr;
|
||||
|
||||
struct PlatformLine {
|
||||
uint32_t gsi; // 0: unassigned
|
||||
bool edgeTriggered;
|
||||
};
|
||||
static PlatformLine g_platformLines[24] = {};
|
||||
|
||||
static uint64_t g_msiFramePhys = 0;
|
||||
static uint32_t g_msiSpiBase = 0;
|
||||
static uint32_t g_msiSpiCount = 0;
|
||||
|
||||
static inline uint32_t GicdRead(uint32_t reg) {
|
||||
return *(volatile uint32_t*)(g_gicd + reg);
|
||||
}
|
||||
|
||||
static inline void GicdWrite(uint32_t reg, uint32_t value) {
|
||||
*(volatile uint32_t*)(g_gicd + reg) = value;
|
||||
}
|
||||
|
||||
static inline void GicdWrite8(uint32_t reg, uint8_t value) {
|
||||
*(volatile uint8_t*)(g_gicd + reg) = value;
|
||||
}
|
||||
|
||||
static inline uint8_t GicdRead8(uint32_t reg) {
|
||||
return *(volatile uint8_t*)(g_gicd + reg);
|
||||
}
|
||||
|
||||
static inline uint32_t GiccRead(uint32_t reg) {
|
||||
return *(volatile uint32_t*)(g_gicc + reg);
|
||||
}
|
||||
|
||||
static inline void GiccWrite(uint32_t reg, uint32_t value) {
|
||||
*(volatile uint32_t*)(g_gicc + reg) = value;
|
||||
}
|
||||
|
||||
static volatile uint8_t* MapRegisters(uint64_t phys, uint64_t size) {
|
||||
for (uint64_t offset = 0; offset < size; offset += 0x1000) {
|
||||
Memory::VMM::g_paging->MapMMIO(phys + offset, Memory::HHDM(phys + offset));
|
||||
}
|
||||
return (volatile uint8_t*)Memory::HHDM(phys);
|
||||
}
|
||||
|
||||
static void SetConfig(uint32_t intid, bool edgeTriggered) {
|
||||
uint32_t reg = GICD_ICFGR + (intid / 16) * 4;
|
||||
uint32_t shift = (intid % 16) * 2 + 1;
|
||||
uint32_t value = GicdRead(reg);
|
||||
if (edgeTriggered) value |= 1u << shift;
|
||||
else value &= ~(1u << shift);
|
||||
GicdWrite(reg, value);
|
||||
}
|
||||
|
||||
static void EnableIntid(uint32_t intid) {
|
||||
GicdWrite(GICD_ISENABLER + (intid / 32) * 4, 1u << (intid % 32));
|
||||
}
|
||||
|
||||
static void Bind(uint32_t intid, uint8_t irq) {
|
||||
if (intid >= MaxIntids || irq >= IRQ_COUNT) return;
|
||||
g_intidToSlot[intid] = irq;
|
||||
g_slotToIntid[irq] = intid;
|
||||
}
|
||||
|
||||
// Route a shared peripheral interrupt to the boot CPU and unmask it.
|
||||
static void RouteSpi(uint32_t intid, uint8_t irq, bool edgeTriggered) {
|
||||
if (intid < 32 || intid >= g_intidCount) return;
|
||||
Bind(intid, irq);
|
||||
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
|
||||
GicdWrite8(GICD_ITARGETSR + intid, Smp::GetCpuData(0)->arch.gicTargetMask);
|
||||
SetConfig(intid, edgeTriggered);
|
||||
EnableIntid(intid);
|
||||
}
|
||||
|
||||
namespace Gic {
|
||||
|
||||
Hal::ACPI::CommonSDTHeader* FindTable(Hal::ACPI::CommonSDTHeader* xsdt, const char* signature) {
|
||||
if (xsdt == nullptr) return nullptr;
|
||||
uint32_t count = (xsdt->Length - sizeof(Hal::ACPI::CommonSDTHeader)) / 8;
|
||||
auto* entries = (uint64_t*)((uint64_t)xsdt + sizeof(Hal::ACPI::CommonSDTHeader));
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
auto* header = (Hal::ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
|
||||
if (header->Signature[0] == signature[0] && header->Signature[1] == signature[1]
|
||||
&& header->Signature[2] == signature[2] && header->Signature[3] == signature[3]) {
|
||||
return header;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ITARGETSR0-7 are banked: each byte reads back the reading CPU's
|
||||
// own interface bit.
|
||||
uint8_t CurrentTargetMask() {
|
||||
uint32_t value = GicdRead(GICD_ITARGETSR);
|
||||
return (uint8_t)(value | value >> 8 | value >> 16 | value >> 24);
|
||||
}
|
||||
|
||||
void InitializeCpuInterface() {
|
||||
// SGIs and PPIs are banked per CPU: disable the PPIs until a
|
||||
// driver routes one, keep the SGIs (IPIs) enabled.
|
||||
GicdWrite(GICD_ICENABLER, 0xFFFF0000u);
|
||||
GicdWrite(GICD_ISENABLER, 0x0000FFFFu);
|
||||
GicdWrite(GICD_ICPENDR, 0xFFFFFFFFu);
|
||||
for (uint32_t intid = 0; intid < 32; intid++) {
|
||||
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
|
||||
}
|
||||
|
||||
GiccWrite(GICC_PMR, 0xF0);
|
||||
GiccWrite(GICC_BPR, 0);
|
||||
GiccWrite(GICC_CTLR, 1);
|
||||
|
||||
auto* cpu = Smp::GetCurrentCpuData();
|
||||
cpu->arch.gicTargetMask = CurrentTargetMask();
|
||||
}
|
||||
|
||||
void RoutePpi(uint32_t intid, uint8_t irq, bool edgeTriggered) {
|
||||
if (intid < 16 || intid >= 32) return;
|
||||
Bind(intid, irq);
|
||||
SetConfig(intid, edgeTriggered);
|
||||
EnableIntid(intid);
|
||||
}
|
||||
|
||||
void SetAckHook(uint8_t irq, AckHook hook) {
|
||||
if (irq < IRQ_COUNT) g_ackHooks[irq] = hook;
|
||||
}
|
||||
|
||||
bool IsInitialized() {
|
||||
return g_gicc != nullptr;
|
||||
}
|
||||
|
||||
Hal::ACPI::CommonSDTHeader* CurrentXsdt() {
|
||||
return g_xsdt;
|
||||
}
|
||||
|
||||
bool Initialize(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||
g_xsdt = xsdt;
|
||||
auto* madt = FindTable(xsdt, "APIC");
|
||||
if (madt == nullptr) {
|
||||
KernelLogStream(ERROR, "GIC") << "No MADT";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t gicdPhys = 0, giccPhys = 0;
|
||||
uint8_t version = 0;
|
||||
auto* cursor = (uint8_t*)madt + MadtHeaderSize;
|
||||
auto* end = (uint8_t*)madt + madt->Length;
|
||||
while (cursor + sizeof(MadtEntry) <= end) {
|
||||
auto* entry = (MadtEntry*)cursor;
|
||||
if (entry->Length < sizeof(MadtEntry)) break;
|
||||
|
||||
if (entry->Type == MadtGicd) {
|
||||
auto* gicd = (MadtGicdEntry*)entry;
|
||||
gicdPhys = gicd->PhysicalBaseAddress;
|
||||
version = gicd->GicVersion;
|
||||
} else if (entry->Type == MadtGicc && giccPhys == 0) {
|
||||
giccPhys = ((MadtGiccEntry*)entry)->PhysicalBaseAddress;
|
||||
} else if (entry->Type == MadtGicMsiFrame && g_msiFramePhys == 0) {
|
||||
auto* frame = (MadtMsiFrameEntry*)entry;
|
||||
g_msiFramePhys = frame->PhysicalBaseAddress;
|
||||
if (frame->Flags & 1) {
|
||||
g_msiSpiBase = frame->SpiBase;
|
||||
g_msiSpiCount = frame->SpiCount;
|
||||
}
|
||||
}
|
||||
cursor += entry->Length;
|
||||
}
|
||||
|
||||
// Version 0 means "infer from the hardware"; 1 and 2 share a
|
||||
// programming model.
|
||||
if (gicdPhys == 0 || giccPhys == 0 || version > 2) {
|
||||
KernelLogStream(ERROR, "GIC") << "No GICv2 in the MADT (version "
|
||||
<< base::dec << (uint64_t)version << ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < MaxIntids; i++) g_intidToSlot[i] = NoSlot;
|
||||
for (int i = 0; i < IRQ_COUNT; i++) g_slotToIntid[i] = SpuriousIntid;
|
||||
Bind(0, IRQ_TLB_SHOOTDOWN);
|
||||
Bind(1, IRQ_RESCHEDULE);
|
||||
|
||||
g_gicd = MapRegisters(gicdPhys, 0x1000);
|
||||
g_gicc = MapRegisters(giccPhys, 0x2000);
|
||||
|
||||
g_intidCount = ((GicdRead(GICD_TYPER) & 0x1F) + 1) * 32;
|
||||
if (g_intidCount > MaxIntids) g_intidCount = MaxIntids;
|
||||
|
||||
// Distributor: everything masked, idle, level-triggered and
|
||||
// aimed at no CPU until RouteSpi claims it.
|
||||
GicdWrite(GICD_CTLR, 0);
|
||||
for (uint32_t intid = 32; intid < g_intidCount; intid += 32) {
|
||||
GicdWrite(GICD_ICENABLER + (intid / 32) * 4, 0xFFFFFFFFu);
|
||||
GicdWrite(GICD_ICPENDR + (intid / 32) * 4, 0xFFFFFFFFu);
|
||||
GicdWrite(GICD_ICACTIVER + (intid / 32) * 4, 0xFFFFFFFFu);
|
||||
}
|
||||
for (uint32_t intid = 32; intid < g_intidCount; intid++) {
|
||||
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
|
||||
GicdWrite8(GICD_ITARGETSR + intid, 0);
|
||||
}
|
||||
for (uint32_t intid = 32; intid < g_intidCount; intid += 16) {
|
||||
GicdWrite(GICD_ICFGR + (intid / 16) * 4, 0);
|
||||
}
|
||||
GicdWrite(GICD_CTLR, 1);
|
||||
|
||||
InitializeCpuInterface();
|
||||
|
||||
if (g_msiFramePhys != 0) {
|
||||
volatile uint8_t* frame = MapRegisters(g_msiFramePhys, 0x1000);
|
||||
if (g_msiSpiCount == 0) {
|
||||
uint32_t typer = *(volatile uint32_t*)(frame + V2M_MSI_TYPER);
|
||||
g_msiSpiBase = (typer >> 16) & 0x3FF;
|
||||
g_msiSpiCount = typer & 0x3FF;
|
||||
}
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "GIC") << "GICv2: distributor 0x" << base::hex << gicdPhys
|
||||
<< ", CPU interface 0x" << giccPhys << ", " << base::dec
|
||||
<< (uint64_t)g_intidCount << " interrupt IDs";
|
||||
if (g_msiSpiCount != 0) {
|
||||
KernelLogStream(OK, "GIC") << "GICv2m MSI frame 0x" << base::hex << g_msiFramePhys
|
||||
<< ": SPIs " << base::dec << (uint64_t)g_msiSpiBase << "-"
|
||||
<< (uint64_t)(g_msiSpiBase + g_msiSpiCount - 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
|
||||
if (irq >= IRQ_COUNT) return;
|
||||
g_irqHandlers[irq] = handler;
|
||||
Kt::KernelLogStream(Kt::DEBUG, "IRQ") << "Registered handler for IRQ "
|
||||
KernelLogStream(DEBUG, "IRQ") << "Registered handler for IRQ "
|
||||
<< base::dec << (uint64_t)irq;
|
||||
}
|
||||
|
||||
// TODO(aarch64): GIC SPI routing (ACPI MADT GICD / interrupt overrides).
|
||||
bool PlatformLinesAreFixed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ACPI GSIs are GIC INTIDs. The GIC knows only active-high levels and
|
||||
// rising edges; an active-low line is inverted on its way in.
|
||||
bool AssignPlatformLine(uint8_t irq, uint32_t gsi, bool levelTriggered, bool) {
|
||||
if (irq == 0 || irq >= 24 || gsi < 32 || gsi >= g_intidCount) return false;
|
||||
g_platformLines[irq] = PlatformLine{ gsi, !levelTriggered };
|
||||
return true;
|
||||
}
|
||||
|
||||
void RouteLegacyIrq(uint8_t irq) {
|
||||
Kt::KernelLogStream(Kt::WARNING, "IRQ") << "aarch64: cannot route IRQ "
|
||||
<< base::dec << (uint64_t)irq << " (no GIC driver)";
|
||||
if (irq >= 24 || g_platformLines[irq].gsi == 0) {
|
||||
KernelLogStream(WARNING, "IRQ") << "aarch64: platform line "
|
||||
<< base::dec << (uint64_t)irq << " is not assigned";
|
||||
return;
|
||||
}
|
||||
RouteSpi(g_platformLines[irq].gsi, irq, g_platformLines[irq].edgeTriggered);
|
||||
}
|
||||
|
||||
void EnableLegacyIrq(uint8_t irq) {
|
||||
RouteLegacyIrq(irq);
|
||||
}
|
||||
|
||||
// TODO(aarch64): GICv3 ITS / GICv2m doorbell.
|
||||
bool ComposeMsi(uint8_t, MsiMessage&) {
|
||||
return false;
|
||||
bool ComposeMsi(uint8_t irq, MsiMessage& out) {
|
||||
if (g_gicd == nullptr || irq < FirstMsiSlot || irq > LastMsiSlot) return false;
|
||||
uint32_t index = irq - FirstMsiSlot;
|
||||
if (index >= g_msiSpiCount) return false;
|
||||
|
||||
uint32_t spi = g_msiSpiBase + index;
|
||||
if (g_slotToIntid[irq] != spi) {
|
||||
RouteSpi(spi, irq, true);
|
||||
}
|
||||
out = MsiMessage{ g_msiFramePhys + V2M_MSI_SETSPI_NS, spi };
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO(aarch64): GIC software-generated interrupts.
|
||||
bool SendIpi(int, uint8_t) {
|
||||
return false;
|
||||
bool SendIpi(int cpuIndex, uint8_t irq) {
|
||||
if (g_gicd == nullptr) return false;
|
||||
auto* cpu = Smp::GetCpuData(cpuIndex);
|
||||
if (cpu == nullptr || cpu->arch.gicTargetMask == 0) return false;
|
||||
uint32_t sgi = g_slotToIntid[irq];
|
||||
if (sgi >= 16) return false;
|
||||
|
||||
// Make the IPI's memory effects visible before it is raised.
|
||||
asm volatile("dsb ishst" ::: "memory");
|
||||
GicdWrite(GICD_SGIR, ((uint32_t)cpu->arch.gicTargetMask << 16) | sgi);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void ArchIrqDispatch(uint64_t irq, bool fromUser) {
|
||||
if (irq < Arch::IRQ_COUNT && Arch::g_irqHandlers[irq] != nullptr) {
|
||||
Arch::g_irqHandlers[irq]((uint8_t)irq, fromUser);
|
||||
// IRQ exception vector (Vectors.S). As on x86, the interrupt is completed
|
||||
// BEFORE the handler runs: the handler may context-switch away and not
|
||||
// come back here for a long time, and an un-EOId interrupt would hold this
|
||||
// CPU's running priority up (no further ticks). Level-triggered sources
|
||||
// get the chance to drop their line first through an ack hook.
|
||||
extern "C" void ArchIrqException(void*, bool fromUser) {
|
||||
using namespace Arch;
|
||||
uint32_t iar = GiccRead(GICC_IAR);
|
||||
uint32_t intid = iar & 0x3FF;
|
||||
if (intid >= SpuriousIntid) return;
|
||||
|
||||
uint8_t irq = g_intidToSlot[intid];
|
||||
if (irq != NoSlot && g_ackHooks[irq] != nullptr) {
|
||||
g_ackHooks[irq]();
|
||||
}
|
||||
GiccWrite(GICC_EOIR, iar);
|
||||
|
||||
if (irq != NoSlot && g_irqHandlers[irq] != nullptr) {
|
||||
g_irqHandlers[irq](irq, fromUser);
|
||||
}
|
||||
}
|
||||
@@ -5,29 +5,68 @@
|
||||
*/
|
||||
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Common/Panic.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Arch/aarch64/Bringup.hpp>
|
||||
|
||||
// TODO(aarch64): 4 KiB-granule, 48-bit VA translation tables. The kernel
|
||||
// half lives in TTBR1_EL1 and is never switched; "address space roots"
|
||||
// handed to the generic kernel are TTBR0_EL1 tables, so
|
||||
// CreateUserAddressSpace has no kernel half to copy. MAIR_EL1 needs
|
||||
// Normal-WB, Normal-NC (MapWC) and Device-nGnRE (MapMMIO) indices.
|
||||
// 4 KiB granule, 48-bit virtual addresses, four levels (0-3). The kernel
|
||||
// half is translated through TTBR1_EL1 (g_kernelHalfRoot), which never
|
||||
// changes after Init. The "address space roots" the generic kernel switches
|
||||
// between are TTBR0_EL1 tables: a process's root holds only its own lower
|
||||
// half, and KernelRoot() is the TTBR0 table of kernel contexts, empty apart
|
||||
// from the EFI runtime identity mappings (so null pointers fault).
|
||||
//
|
||||
// The HHDM covers only what the memory map lists as RAM or firmware data:
|
||||
// unlike on x86, the holes between are device space on ARM machines, and a
|
||||
// cacheable mapping of device registers invites speculative accesses.
|
||||
|
||||
namespace Memory::VMM {
|
||||
|
||||
Paging* g_paging = nullptr;
|
||||
|
||||
static std::uint64_t g_hhdmLimit = 0;
|
||||
|
||||
static std::uint64_t g_kernelHalfRoot = 0;
|
||||
|
||||
std::uint64_t Paging::HhdmLimit() { return g_hhdmLimit; }
|
||||
|
||||
// Translation table descriptor bits (VMSAv8-64, stage 1)
|
||||
static constexpr uint64_t DescValid = 1ULL << 0;
|
||||
static constexpr uint64_t DescTable = 1ULL << 1; // table (levels 0-2) or page (level 3)
|
||||
static constexpr uint64_t DescAttrShift = 2; // AttrIndx[2:0], into MAIR_EL1
|
||||
static constexpr uint64_t DescAttrMask = 7ULL << DescAttrShift;
|
||||
static constexpr uint64_t DescApUser = 1ULL << 6; // AP[1]: EL0 may access
|
||||
static constexpr uint64_t DescApReadOnly = 1ULL << 7; // AP[2]
|
||||
static constexpr uint64_t DescShInner = 3ULL << 8;
|
||||
static constexpr uint64_t DescAf = 1ULL << 10; // access flag (no AF faults)
|
||||
static constexpr uint64_t DescNotGlobal = 1ULL << 11;
|
||||
static constexpr uint64_t DescPxn = 1ULL << 53;
|
||||
static constexpr uint64_t DescUxn = 1ULL << 54;
|
||||
static constexpr uint64_t DescAddrMask = 0x0000FFFFFFFFF000ULL;
|
||||
|
||||
// MAIR_EL1 attribute indices
|
||||
static constexpr uint64_t AttrNormal = 0; // write-back, read/write-allocate
|
||||
static constexpr uint64_t AttrNormalNc = 1; // non-cacheable (write-combining)
|
||||
static constexpr uint64_t AttrDevice = 2; // Device-nGnRE
|
||||
static constexpr uint64_t MairValue = 0xFFULL << (8 * AttrNormal)
|
||||
| 0x44ULL << (8 * AttrNormalNc)
|
||||
| 0x04ULL << (8 * AttrDevice);
|
||||
|
||||
static constexpr uint64_t KernelHalfBase = 0xFFFF000000000000ULL;
|
||||
static constexpr uint64_t UserSpaceEnd = 0x0000800000000000ULL;
|
||||
|
||||
// EFI memory types whose runtime regions are device registers
|
||||
static constexpr uint32_t EfiMemoryMappedIo = 11;
|
||||
static constexpr uint32_t EfiMemoryMappedIoPortSpace = 12;
|
||||
|
||||
static kcp::Mutex pagingLock;
|
||||
|
||||
void LockUserPaging() { pagingLock.Acquire(); }
|
||||
void UnlockUserPaging() { pagingLock.Release(); }
|
||||
|
||||
[[noreturn]] static void NotImplemented(const char* what) {
|
||||
Panic(what, nullptr);
|
||||
}
|
||||
|
||||
void ActivateKernelAddressSpace() {
|
||||
Arch::WritePageTableRoot(g_paging->KernelRoot());
|
||||
}
|
||||
@@ -36,75 +75,446 @@ namespace Memory::VMM {
|
||||
Arch::FlushTlbAll();
|
||||
}
|
||||
|
||||
using Descriptor = uint64_t;
|
||||
|
||||
static inline volatile Descriptor* TableAt(uint64_t phys) {
|
||||
return (volatile Descriptor*)Memory::HHDM(phys);
|
||||
}
|
||||
|
||||
static inline uint64_t Index(uint64_t va, int level) {
|
||||
return (va >> (39 - 9 * level)) & 0x1FF;
|
||||
}
|
||||
|
||||
// Order descriptor writes before the table walker (and later
|
||||
// instructions) can observe them.
|
||||
static inline void PublishTables() {
|
||||
asm volatile("dsb ishst\n\tisb" ::: "memory");
|
||||
}
|
||||
|
||||
static inline uint64_t ReadTtbr1() {
|
||||
uint64_t ttbr;
|
||||
asm volatile("mrs %0, ttbr1_el1" : "=r"(ttbr));
|
||||
return ttbr & Arch::TtbrAddressMask;
|
||||
}
|
||||
|
||||
// The level-3 descriptor for `va`, creating intermediate tables.
|
||||
// Returns nullptr when out of memory or when a block mapping covers
|
||||
// `va` (only the bootloader's tables contain blocks).
|
||||
static volatile Descriptor* WalkCreate(uint64_t root, uint64_t va) {
|
||||
uint64_t table = root;
|
||||
for (int level = 0; level < 3; level++) {
|
||||
volatile Descriptor* entry = &TableAt(table)[Index(va, level)];
|
||||
Descriptor desc = *entry;
|
||||
if ((desc & DescValid) == 0) {
|
||||
void* page = Memory::g_pfa->AllocateZeroed();
|
||||
if (page == nullptr) return nullptr;
|
||||
table = Memory::SubHHDM(page);
|
||||
// The zeroed table must be visible before the walker can
|
||||
// reach it.
|
||||
asm volatile("dsb ishst" ::: "memory");
|
||||
*entry = table | DescTable | DescValid;
|
||||
} else if ((desc & DescTable) == 0) {
|
||||
return nullptr;
|
||||
} else {
|
||||
table = desc & DescAddrMask;
|
||||
}
|
||||
}
|
||||
return &TableAt(table)[Index(va, 3)];
|
||||
}
|
||||
|
||||
// The level-3 descriptor for `va`, or nullptr if a table is missing.
|
||||
static volatile Descriptor* WalkExisting(uint64_t root, uint64_t va) {
|
||||
uint64_t table = root;
|
||||
for (int level = 0; level < 3; level++) {
|
||||
Descriptor desc = TableAt(table)[Index(va, level)];
|
||||
if ((desc & (DescValid | DescTable)) != (DescValid | DescTable)) return nullptr;
|
||||
table = desc & DescAddrMask;
|
||||
}
|
||||
return &TableAt(table)[Index(va, 3)];
|
||||
}
|
||||
|
||||
// Install a leaf. Replacing a live translation (other address or
|
||||
// attributes) needs break-before-make: invalidate, flush, then write.
|
||||
static void SetLeaf(volatile Descriptor* entry, Descriptor desc, uint64_t va) {
|
||||
Descriptor old = *entry;
|
||||
if (old == desc) return;
|
||||
if (old & DescValid) {
|
||||
*entry = 0;
|
||||
Arch::FlushTlbPage(va);
|
||||
}
|
||||
*entry = desc;
|
||||
PublishTables();
|
||||
}
|
||||
|
||||
static Descriptor KernelLeaf(uint64_t phys, uint64_t attr) {
|
||||
Descriptor desc = (phys & DescAddrMask) | DescTable | DescValid | DescAf | DescShInner
|
||||
| (attr << DescAttrShift) | DescUxn;
|
||||
// Kernel memory stays executable, as on x86; device memory never is.
|
||||
if (attr != AttrNormal) desc |= DescPxn;
|
||||
return desc;
|
||||
}
|
||||
|
||||
static Descriptor UserLeaf(uint64_t phys, uint64_t attr, bool writable, bool executable) {
|
||||
Descriptor desc = (phys & DescAddrMask) | DescTable | DescValid | DescAf | DescShInner
|
||||
| DescNotGlobal | (attr << DescAttrShift) | DescApUser | DescPxn;
|
||||
if (!writable) desc |= DescApReadOnly;
|
||||
if (!executable) desc |= DescUxn;
|
||||
return desc;
|
||||
}
|
||||
|
||||
static inline bool IsUserLeaf(Descriptor desc) {
|
||||
return (desc & (DescValid | DescTable)) == (DescValid | DescTable) && (desc & DescApUser);
|
||||
}
|
||||
|
||||
// Translate through a table tree that may contain block mappings (the
|
||||
// bootloader's). Returns 0 if unmapped.
|
||||
static uint64_t Translate(uint64_t root, uint64_t va) {
|
||||
uint64_t table = root;
|
||||
for (int level = 0; level <= 3; level++) {
|
||||
Descriptor desc = TableAt(table)[Index(va, level)];
|
||||
if ((desc & DescValid) == 0) return 0;
|
||||
|
||||
uint64_t span = 1ULL << (39 - 9 * level);
|
||||
bool leaf = level == 3 || (desc & DescTable) == 0;
|
||||
if (leaf) {
|
||||
if (level == 0) return 0; // no level-0 blocks with 4 KiB granules
|
||||
return ((desc & DescAddrMask) & ~(span - 1)) | (va & (span - 1));
|
||||
}
|
||||
table = desc & DescAddrMask;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Paging::MapEfiRuntime(const montauk::boot::EfiInfo& efi) {
|
||||
if (efi.memoryMap == nullptr || efi.descriptorSize == 0) return;
|
||||
|
||||
struct EfiMemDesc {
|
||||
uint32_t Type;
|
||||
uint64_t PhysicalStart;
|
||||
uint64_t VirtualStart;
|
||||
uint64_t NumberOfPages;
|
||||
uint64_t Attribute;
|
||||
};
|
||||
|
||||
static constexpr uint64_t EFI_MEMORY_RUNTIME = 0x8000000000000000ULL;
|
||||
|
||||
auto* base = (uint8_t*)efi.memoryMap;
|
||||
uint64_t count = efi.memoryMapSize / efi.descriptorSize;
|
||||
|
||||
for (uint64_t i = 0; i < count; i++) {
|
||||
auto* desc = (EfiMemDesc*)(base + i * efi.descriptorSize);
|
||||
if ((desc->Attribute & EFI_MEMORY_RUNTIME) == 0) continue;
|
||||
|
||||
bool device = desc->Type == EfiMemoryMappedIo || desc->Type == EfiMemoryMappedIoPortSpace;
|
||||
for (uint64_t p = 0; p < desc->NumberOfPages; p++) {
|
||||
uint64_t addr = desc->PhysicalStart + p * 0x1000;
|
||||
// Firmware code runs at its HHDM alias (Efi::Init calls the
|
||||
// services there) and reaches its data at physical addresses.
|
||||
// Runtime regions are reserved memory, outside the HHDM.
|
||||
if (device) {
|
||||
MapMMIO(addr, addr);
|
||||
MapMMIO(addr, Memory::HHDM(addr));
|
||||
} else {
|
||||
Map(addr, addr);
|
||||
Map(addr, Memory::HHDM(addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlushTLB();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// The new tables map the code running here at the same address as the
|
||||
// bootloader's did, so the switch is a register write followed by a
|
||||
// full invalidate.
|
||||
void ActivateKernelTranslation() {
|
||||
using namespace Memory::VMM;
|
||||
|
||||
// 48-bit VAs in both halves, 4 KiB granules, write-back walks of
|
||||
// inner-shareable tables; the physical size is what the CPU has.
|
||||
uint64_t mmfr0;
|
||||
asm volatile("mrs %0, id_aa64mmfr0_el1" : "=r"(mmfr0));
|
||||
uint64_t ips = mmfr0 & 0xF;
|
||||
if (ips > 5) ips = 5; // 52-bit output needs LPA2 descriptors
|
||||
uint64_t tcr = 16ULL // T0SZ
|
||||
| 1ULL << 8 | 1ULL << 10 // IRGN0, ORGN0: write-back
|
||||
| 3ULL << 12 // SH0: inner shareable
|
||||
| 0ULL << 14 // TG0: 4 KiB
|
||||
| 16ULL << 16 // T1SZ
|
||||
| 1ULL << 24 | 1ULL << 26 // IRGN1, ORGN1
|
||||
| 3ULL << 28 // SH1
|
||||
| 2ULL << 30 // TG1: 4 KiB
|
||||
| ips << 32; // IPS
|
||||
|
||||
asm volatile(
|
||||
"dsb ish\n\t"
|
||||
"msr mair_el1, %0\n\t"
|
||||
"msr tcr_el1, %1\n\t"
|
||||
"isb\n\t"
|
||||
"msr ttbr1_el1, %2\n\t"
|
||||
"msr ttbr0_el1, %3\n\t"
|
||||
"isb\n\t"
|
||||
"tlbi vmalle1\n\t"
|
||||
"dsb nsh\n\t"
|
||||
"isb"
|
||||
:: "r"(MairValue), "r"(tcr), "r"(g_kernelHalfRoot), "r"(g_paging->KernelRoot()) : "memory");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Memory::VMM {
|
||||
|
||||
Paging::Paging() {
|
||||
NotImplemented("aarch64: virtual memory not implemented");
|
||||
rootPhys = Memory::SubHHDM(Memory::g_pfa->AllocateZeroed());
|
||||
g_kernelHalfRoot = Memory::SubHHDM(Memory::g_pfa->AllocateZeroed());
|
||||
}
|
||||
|
||||
void Paging::Init(std::uint64_t, std::uint64_t, const montauk::boot::MemoryMap&,
|
||||
const montauk::boot::Framebuffer&) {
|
||||
NotImplemented("aarch64: Paging::Init not implemented");
|
||||
// Kernel mappings of lower-half addresses (EFI identity mappings) go to
|
||||
// the kernel contexts' TTBR0 table.
|
||||
static uint64_t KernelRootFor(uint64_t kernelContextRoot, uint64_t va) {
|
||||
return va >= KernelHalfBase ? g_kernelHalfRoot : kernelContextRoot;
|
||||
}
|
||||
|
||||
std::uint64_t Paging::HhdmLimit() { return 0; }
|
||||
|
||||
void Paging::Map(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::Map not implemented");
|
||||
void Paging::Init(std::uint64_t kernelBaseVirt, std::uint64_t kernelSize, const montauk::boot::MemoryMap& memMap,
|
||||
const montauk::boot::Framebuffer& framebuffer) {
|
||||
if (Memory::HHDMBase < KernelHalfBase) {
|
||||
Panic("aarch64: HHDM outside the 48-bit kernel half", nullptr);
|
||||
}
|
||||
|
||||
void Paging::MapMMIO(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::MapMMIO not implemented");
|
||||
uint64_t bootTtbr1 = ReadTtbr1();
|
||||
uint64_t bootTcr, bootMair;
|
||||
asm volatile("mrs %0, tcr_el1" : "=r"(bootTcr));
|
||||
asm volatile("mrs %0, mair_el1" : "=r"(bootMair));
|
||||
Kt::KernelLogStream(Kt::DEBUG, "VMM") << "Bootloader TCR 0x" << base::hex << bootTcr
|
||||
<< ", MAIR 0x" << bootMair << ", TTBR1 0x" << bootTtbr1;
|
||||
|
||||
// The kernel image, at the physical pages the bootloader loaded it to.
|
||||
for (uint64_t va = kernelBaseVirt; va < kernelBaseVirt + kernelSize; va += 0x1000) {
|
||||
uint64_t phys = Translate(bootTtbr1, va);
|
||||
if (phys == 0) Panic("aarch64: kernel page missing from the bootloader's tables", nullptr);
|
||||
Map(phys, va);
|
||||
}
|
||||
|
||||
void Paging::MapWC(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::MapWC not implemented");
|
||||
// HHDM: every region that is real backing store (see the top of
|
||||
// this file). The limit is published for the AML interpreter.
|
||||
uint64_t maxPhysAddr = 0;
|
||||
for (size_t i = 0; i < memMap.count; i++) {
|
||||
const auto& region = memMap.regions[i];
|
||||
if (region.kind == montauk::boot::MemoryKind::Reserved
|
||||
|| region.kind == montauk::boot::MemoryKind::Unknown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
void Paging::MapUser(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::MapUser not implemented");
|
||||
uint64_t start = region.base & ~0xFFFULL;
|
||||
uint64_t end = (region.base + region.length + 0xFFF) & ~0xFFFULL;
|
||||
bool framebufferRegion = region.kind == montauk::boot::MemoryKind::Framebuffer;
|
||||
for (uint64_t phys = start; phys < end; phys += 0x1000) {
|
||||
if (framebufferRegion) MapWC(phys, Memory::HHDM(phys));
|
||||
else Map(phys, Memory::HHDM(phys));
|
||||
}
|
||||
if (end > maxPhysAddr) maxPhysAddr = end;
|
||||
}
|
||||
g_hhdmLimit = maxPhysAddr;
|
||||
|
||||
// The framebuffer may not be listed in the memory map at all.
|
||||
if (framebuffer.address != 0) {
|
||||
uint64_t fbPhys = Memory::SubHHDM(framebuffer.address) & ~0xFFFULL;
|
||||
uint64_t fbPages = (framebuffer.height * framebuffer.pitch + 0xFFF) / 0x1000;
|
||||
for (uint64_t p = 0; p < fbPages; p++) {
|
||||
uint64_t phys = fbPhys + p * 0x1000;
|
||||
MapWC(phys, Memory::HHDM(phys));
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t Paging::GetPhysAddr(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::GetPhysAddr not implemented");
|
||||
Arch::ActivateKernelTranslation();
|
||||
|
||||
Kt::KernelLogStream(Kt::OK, "VMM") << "Switched to kernel translation tables (HHDM limit 0x"
|
||||
<< base::hex << g_hhdmLimit << ")";
|
||||
}
|
||||
|
||||
static void MapKernelPage(uint64_t root, uint64_t phys, uint64_t va, uint64_t attr, const char* what) {
|
||||
if (va % 0x1000 != 0 || phys % 0x1000 != 0) {
|
||||
Panic(what, nullptr);
|
||||
}
|
||||
volatile Descriptor* entry = WalkCreate(KernelRootFor(root, va), va);
|
||||
if (entry == nullptr) {
|
||||
Panic("OOM in Paging (kernel page table allocation)", nullptr);
|
||||
}
|
||||
SetLeaf(entry, KernelLeaf(phys, attr), va);
|
||||
}
|
||||
|
||||
void Paging::Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrNormal,
|
||||
"Value that isn't page-aligned passed as address to Paging::Map!");
|
||||
}
|
||||
|
||||
void Paging::MapMMIO(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrDevice,
|
||||
"Value that isn't page-aligned passed as address to Paging::MapMMIO!");
|
||||
}
|
||||
|
||||
void Paging::MapWC(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrNormalNc,
|
||||
"Value that isn't page-aligned passed as address to Paging::MapWC!");
|
||||
}
|
||||
|
||||
void Paging::MapUser(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) {
|
||||
Panic("Value that isn't page-aligned passed as address to Paging::MapUser!", nullptr);
|
||||
}
|
||||
volatile Descriptor* entry = WalkCreate(rootPhys, virtualAddress);
|
||||
if (entry == nullptr) {
|
||||
Panic("OOM in Paging::MapUser (page table allocation)", nullptr);
|
||||
}
|
||||
SetLeaf(entry, UserLeaf(physicalAddress, AttrNormal, true, true), virtualAddress);
|
||||
}
|
||||
|
||||
std::uint64_t Paging::GetPhysAddr(std::uint64_t root, std::uint64_t virtualAddress) {
|
||||
if (virtualAddress >= KernelHalfBase) {
|
||||
root = g_kernelHalfRoot;
|
||||
}
|
||||
return Translate(root, virtualAddress);
|
||||
}
|
||||
|
||||
std::uint64_t Paging::GetPhysAddr(std::uint64_t virtualAddress) {
|
||||
return GetPhysAddr(rootPhys, virtualAddress);
|
||||
}
|
||||
|
||||
// The kernel half lives in TTBR1, so a new address space starts empty.
|
||||
std::uint64_t Paging::CreateUserAddressSpace() {
|
||||
NotImplemented("aarch64: Paging::CreateUserAddressSpace not implemented");
|
||||
void* page = Memory::g_pfa->AllocateZeroed();
|
||||
if (page == nullptr) return 0;
|
||||
return Memory::SubHHDM(page);
|
||||
}
|
||||
|
||||
bool Paging::MapUserIn(std::uint64_t, std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::MapUserIn not implemented");
|
||||
bool Paging::MapUserIn(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
return MapUserInPermissions(root, physicalAddress, virtualAddress, true, true);
|
||||
}
|
||||
|
||||
bool Paging::MapUserInPermissions(std::uint64_t, std::uint64_t, std::uint64_t, bool, bool) {
|
||||
NotImplemented("aarch64: Paging::MapUserInPermissions not implemented");
|
||||
static bool MapUserLeaf(uint64_t root, uint64_t phys, uint64_t va, Descriptor desc, const char* what) {
|
||||
pagingLock.Acquire();
|
||||
if (va % 0x1000 != 0 || phys % 0x1000 != 0) {
|
||||
pagingLock.Release();
|
||||
Panic(what, nullptr);
|
||||
}
|
||||
volatile Descriptor* entry = WalkCreate(root, va);
|
||||
if (entry == nullptr) {
|
||||
pagingLock.Release();
|
||||
return false;
|
||||
}
|
||||
SetLeaf(entry, desc, va);
|
||||
pagingLock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Paging::ProtectUserIn(std::uint64_t, std::uint64_t, bool, bool) {
|
||||
NotImplemented("aarch64: Paging::ProtectUserIn not implemented");
|
||||
bool Paging::MapUserInPermissions(std::uint64_t root, std::uint64_t physicalAddress,
|
||||
std::uint64_t virtualAddress, bool writable, bool executable) {
|
||||
return MapUserLeaf(root, physicalAddress, virtualAddress,
|
||||
UserLeaf(physicalAddress, AttrNormal, writable, executable),
|
||||
"Non-aligned address in Paging::MapUserIn!");
|
||||
}
|
||||
|
||||
bool Paging::MapUserInWC(std::uint64_t, std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::MapUserInWC not implemented");
|
||||
bool Paging::MapUserInWC(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
return MapUserLeaf(root, physicalAddress, virtualAddress,
|
||||
UserLeaf(physicalAddress, AttrNormalNc, true, false),
|
||||
"Non-aligned address in Paging::MapUserInWC!");
|
||||
}
|
||||
|
||||
void Paging::UnmapUserIn(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::UnmapUserIn not implemented");
|
||||
// Permission changes need no break-before-make, only the TLB flush.
|
||||
bool Paging::ProtectUserIn(std::uint64_t root, std::uint64_t virtualAddress,
|
||||
bool writable, bool executable) {
|
||||
if ((virtualAddress & 0xFFFULL) != 0) return false;
|
||||
pagingLock.Acquire();
|
||||
volatile Descriptor* entry = WalkExisting(root, virtualAddress);
|
||||
if (entry == nullptr || !IsUserLeaf(*entry)) {
|
||||
pagingLock.Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
void Paging::FreeUserHalf(std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::FreeUserHalf not implemented");
|
||||
Descriptor desc = *entry;
|
||||
if (writable) desc &= ~DescApReadOnly;
|
||||
else desc |= DescApReadOnly;
|
||||
if (executable) desc &= ~DescUxn;
|
||||
else desc |= DescUxn;
|
||||
*entry = desc;
|
||||
Arch::FlushTlbPage(virtualAddress);
|
||||
pagingLock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Paging::IsUserRangeAccessible(std::uint64_t, std::uint64_t, std::uint64_t, bool) {
|
||||
NotImplemented("aarch64: Paging::IsUserRangeAccessible not implemented");
|
||||
void Paging::UnmapUserIn(std::uint64_t root, std::uint64_t virtualAddress) {
|
||||
pagingLock.Acquire();
|
||||
volatile Descriptor* entry = WalkExisting(root, virtualAddress);
|
||||
if (entry != nullptr && (*entry & DescValid)) {
|
||||
*entry = 0;
|
||||
Arch::FlushTlbPage(virtualAddress);
|
||||
}
|
||||
pagingLock.Release();
|
||||
}
|
||||
|
||||
void Paging::MapEfiRuntime(const montauk::boot::EfiInfo&) {
|
||||
NotImplemented("aarch64: Paging::MapEfiRuntime not implemented");
|
||||
void Paging::FreeUserHalf(std::uint64_t root) {
|
||||
// User space is the lower 128 TiB: level-0 entries 0-255.
|
||||
for (int i0 = 0; i0 < 256; i0++) {
|
||||
Descriptor d0 = TableAt(root)[i0];
|
||||
if ((d0 & DescValid) == 0) continue;
|
||||
uint64_t l1 = d0 & DescAddrMask;
|
||||
|
||||
for (int i1 = 0; i1 < 512; i1++) {
|
||||
Descriptor d1 = TableAt(l1)[i1];
|
||||
if ((d1 & DescValid) == 0) continue;
|
||||
uint64_t l2 = d1 & DescAddrMask;
|
||||
|
||||
for (int i2 = 0; i2 < 512; i2++) {
|
||||
Descriptor d2 = TableAt(l2)[i2];
|
||||
if ((d2 & DescValid) == 0) continue;
|
||||
uint64_t l3 = d2 & DescAddrMask;
|
||||
|
||||
for (int i3 = 0; i3 < 512; i3++) {
|
||||
Descriptor leaf = TableAt(l3)[i3];
|
||||
if ((leaf & DescValid) == 0) continue;
|
||||
|
||||
// Write-combining/device pages are not the process's.
|
||||
if (((leaf & DescAttrMask) >> DescAttrShift) != AttrNormal) continue;
|
||||
|
||||
uint64_t pagePhys = leaf & DescAddrMask;
|
||||
if (pagePhys != 0) {
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(pagePhys));
|
||||
}
|
||||
}
|
||||
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(l3));
|
||||
}
|
||||
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(l2));
|
||||
}
|
||||
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(l1));
|
||||
}
|
||||
}
|
||||
|
||||
bool Paging::IsUserRangeAccessible(std::uint64_t root, std::uint64_t virtualAddress,
|
||||
std::uint64_t size, bool requireWrite) {
|
||||
if (size == 0) return true;
|
||||
if (virtualAddress >= UserSpaceEnd) return false;
|
||||
if (virtualAddress + size < virtualAddress) return false;
|
||||
if (virtualAddress + size > UserSpaceEnd) return false;
|
||||
|
||||
uint64_t page = virtualAddress & ~0xFFFULL;
|
||||
uint64_t lastPage = (virtualAddress + size - 1) & ~0xFFFULL;
|
||||
for (;;) {
|
||||
volatile Descriptor* entry = WalkExisting(root, page);
|
||||
if (entry == nullptr) return false;
|
||||
Descriptor desc = *entry;
|
||||
if (!IsUserLeaf(desc)) return false;
|
||||
if (requireWrite && (desc & DescApReadOnly)) return false;
|
||||
if (page == lastPage) break;
|
||||
page += 0x1000;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,8 @@ namespace Arch {
|
||||
|
||||
struct CpuState {
|
||||
uint64_t mpidr; // MPIDR_EL1 affinity of this CPU
|
||||
uint8_t gicTargetMask; // GICv2 CPU-interface bit (SGI/SPI targets)
|
||||
uint64_t timerPeriod; // tick period in counter ticks, 0 = stopped
|
||||
};
|
||||
|
||||
// TPIDR_EL1 holds the CpuData pointer (zeroed in EarlyInitialize until
|
||||
|
||||
@@ -7,16 +7,73 @@
|
||||
#include <Arch/Platform.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Arch/aarch64/Gic.hpp>
|
||||
#include <Arch/aarch64/Bringup.hpp>
|
||||
#include <ACPI/FADT.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
extern "C" char ArchExceptionVectors[]; // Vectors.S
|
||||
|
||||
namespace Arch {
|
||||
|
||||
void EarlyInitialize() {
|
||||
// No per-CPU data yet: Smp::TryGetCurrentCpuData must see null.
|
||||
asm volatile("msr tpidr_el1, xzr");
|
||||
// SCTLR_EL1.WXN: writable memory is execute-never
|
||||
static constexpr uint64_t SctlrWxn = 1ULL << 19;
|
||||
// SCTLR_EL1.UCT/UCI: EL0 may read CTR_EL0 and clean/invalidate caches
|
||||
// by address (what __builtin___clear_cache needs for generated code).
|
||||
static constexpr uint64_t SctlrUct = 1ULL << 15;
|
||||
static constexpr uint64_t SctlrUci = 1ULL << 26;
|
||||
|
||||
// PSCI 0.2+ function IDs (SMC32 calling convention)
|
||||
static constexpr uint64_t PsciSystemOff = 0x84000008;
|
||||
static constexpr uint64_t PsciSystemReset = 0x84000009;
|
||||
|
||||
// How the firmware takes PSCI calls, from the FADT's ARM_BOOT_ARCH.
|
||||
enum class PsciConduit { None, Smc, Hvc };
|
||||
static PsciConduit g_psci = PsciConduit::None;
|
||||
|
||||
static uint64_t PsciCall(uint64_t function) {
|
||||
register uint64_t x0 asm("x0") = function;
|
||||
register uint64_t x1 asm("x1") = 0;
|
||||
register uint64_t x2 asm("x2") = 0;
|
||||
register uint64_t x3 asm("x3") = 0;
|
||||
if (g_psci == PsciConduit::Hvc) {
|
||||
asm volatile("hvc #0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3) :: "memory");
|
||||
} else if (g_psci == PsciConduit::Smc) {
|
||||
asm volatile("smc #0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3) :: "memory");
|
||||
}
|
||||
return x0;
|
||||
}
|
||||
|
||||
static void InitializePsci(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||
Hal::FADT::ParsedFADT fadt{};
|
||||
if (!Hal::FADT::Parse(xsdt, fadt) || !fadt.Valid) return;
|
||||
if (!(fadt.ArmBootArchFlags & Hal::FADT::ArmBootPsciCompliant)) {
|
||||
KernelLogStream(WARNING, "PSCI") << "Firmware is not PSCI compliant: no reset or power-off";
|
||||
return;
|
||||
}
|
||||
g_psci = (fadt.ArmBootArchFlags & Hal::FADT::ArmBootPsciUseHvc) ? PsciConduit::Hvc : PsciConduit::Smc;
|
||||
KernelLogStream(OK, "PSCI") << "PSCI through " << (g_psci == PsciConduit::Hvc ? "HVC" : "SMC");
|
||||
}
|
||||
|
||||
void InitializeCpuRegisters() {
|
||||
// Run on SP_EL1 so exceptions taken from the kernel arrive on the
|
||||
// stack it is already using (Vectors.S has no SP_EL0 handlers).
|
||||
asm volatile("mov x9, sp\n\tmsr spsel, #1\n\tmov sp, x9" ::: "x9", "memory");
|
||||
|
||||
// The vectors need no allocation, so take over from the
|
||||
// bootloader's right away.
|
||||
asm volatile("msr vbar_el1, %0\n\tisb" :: "r"(ArchExceptionVectors) : "memory");
|
||||
|
||||
// Kernel mappings are RWX (Paging.cpp), so writable must not
|
||||
// imply execute-never; user code may maintain its own caches.
|
||||
uint64_t sctlr;
|
||||
asm volatile("mrs %0, sctlr_el1" : "=r"(sctlr));
|
||||
sctlr &= ~SctlrWxn;
|
||||
sctlr |= SctlrUct | SctlrUci;
|
||||
asm volatile("msr sctlr_el1, %0\n\tisb" :: "r"(sctlr));
|
||||
|
||||
// CPACR_EL1.FPEN = 0b11: FP/SIMD usable at EL0 and EL1 (the kernel
|
||||
// itself is built -mgeneral-regs-only; Context.S saves user state).
|
||||
@@ -26,10 +83,17 @@ namespace Arch {
|
||||
asm volatile("msr cpacr_el1, %0\n\tisb" :: "r"(cpacr));
|
||||
}
|
||||
|
||||
// TODO(aarch64): install the VBAR_EL1 vector table (EL1/EL0 synchronous
|
||||
// exceptions -> panic / SyscallDispatch, IRQ -> GIC -> ArchIrqDispatch).
|
||||
void EarlyInitialize() {
|
||||
// No per-CPU data yet: Smp::TryGetCurrentCpuData must see null.
|
||||
asm volatile("msr tpidr_el1, xzr");
|
||||
|
||||
InitializeCpuRegisters();
|
||||
}
|
||||
|
||||
// Installed by EarlyInitialize.
|
||||
void InitializeTraps() {
|
||||
KernelLogStream(WARNING, "Arch") << "aarch64: exception vectors not implemented";
|
||||
KernelLogStream(OK, "Arch") << "Installed exception vectors at 0x" << base::hex
|
||||
<< (uint64_t)ArchExceptionVectors;
|
||||
}
|
||||
|
||||
// MAIR_EL1 attribute indices are chosen together with the page-table
|
||||
@@ -37,10 +101,12 @@ namespace Arch {
|
||||
void InitializeMemoryAttributes() {
|
||||
}
|
||||
|
||||
// TODO(aarch64): GIC distributor/redistributor from the ACPI MADT.
|
||||
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader*) {
|
||||
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||
Smp::InitBsp();
|
||||
KernelLogStream(WARNING, "Arch") << "aarch64: no interrupt controller driver";
|
||||
if (!Gic::Initialize(xsdt)) {
|
||||
KernelLogStream(ERROR, "Arch") << "aarch64: no interrupt controller, running without IRQs";
|
||||
}
|
||||
InitializePsci(xsdt);
|
||||
}
|
||||
|
||||
void InitializePlatformDevices() {
|
||||
@@ -53,12 +119,22 @@ namespace Arch {
|
||||
void LateInitialize() {
|
||||
}
|
||||
|
||||
void ListPlatformDevices(PlatformDeviceSink, void*) {
|
||||
void ListPlatformDevices(PlatformDeviceSink sink, void* context) {
|
||||
if (Gic::IsInitialized()) {
|
||||
sink(context, 1, "GICv2", "ARM Generic Interrupt Controller");
|
||||
sink(context, 2, "Generic Timer", "ARM architected timer (EL1 virtual)");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(aarch64): PSCI SYSTEM_RESET through the conduit the FADT names.
|
||||
void Reboot() {
|
||||
PsciCall(PsciSystemReset);
|
||||
HaltForever();
|
||||
}
|
||||
|
||||
void PowerOff() {
|
||||
if (g_psci != PsciConduit::None) {
|
||||
PsciCall(PsciSystemOff);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
/*
|
||||
* Smp.cpp
|
||||
* aarch64 per-CPU data (implements the arch half of Arch/Smp.hpp)
|
||||
* aarch64 per-CPU data and secondary CPU start-up (implements the arch half of Arch/Smp.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Syscall.hpp>
|
||||
#include <Arch/aarch64/Bringup.hpp>
|
||||
#include <Arch/aarch64/Gic.hpp>
|
||||
#include <Boot/BootInfo.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
@@ -47,14 +53,83 @@ namespace Smp {
|
||||
<< bsp.arch.mpidr << ")";
|
||||
}
|
||||
|
||||
void BootAPs(const montauk::boot::SmpInfo& smp) {
|
||||
if (smp.cpuCount > 1) {
|
||||
g_detectedCpuCount = (int)smp.cpuCount;
|
||||
// A secondary CPU, woken by the bootloader on its own stack and still
|
||||
// on the bootloader's translation tables. The kernel stashed this CPU's
|
||||
// CpuData in extraArgument.
|
||||
static void ApEntry(montauk::boot::BootCpu* bootCpu) {
|
||||
CpuData* cpu = (CpuData*)bootCpu->extraArgument;
|
||||
|
||||
Arch::InitializeCpuRegisters();
|
||||
Arch::ActivateKernelTranslation();
|
||||
asm volatile("msr tpidr_el1, %0" :: "r"(cpu));
|
||||
|
||||
Arch::Gic::InitializeCpuInterface();
|
||||
Arch::InitializeSyscallEntry();
|
||||
Timekeeping::TimerInitializeAP();
|
||||
|
||||
__atomic_store_n(&cpu->started, true, __ATOMIC_RELEASE);
|
||||
Arch::EnableInterrupts();
|
||||
|
||||
// Runnable work arrives as a reschedule IPI; keep the periodic tick
|
||||
// masked while idle.
|
||||
for (;;) {
|
||||
Timekeeping::TimerEnterApIdle();
|
||||
Timekeeping::ServiceDeferredWork();
|
||||
Arch::DisableInterrupts();
|
||||
Arch::HaltWithInterruptsDisabled();
|
||||
Arch::EnableInterrupts();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(aarch64): start secondaries through smp.startCpu once the GIC
|
||||
// (reschedule/TLB-shootdown SGIs) and the per-CPU generic timer work.
|
||||
KernelLogStream(WARNING, "SMP") << "aarch64: secondary CPUs not started ("
|
||||
<< base::dec << smp.cpuCount << " reported)";
|
||||
void BootAPs(const montauk::boot::SmpInfo& smp) {
|
||||
if (smp.cpus == nullptr || smp.startCpu == nullptr || smp.cpuCount <= 1) {
|
||||
KernelLogStream(INFO, "SMP") << "Single CPU system - no secondary CPUs to start";
|
||||
return;
|
||||
}
|
||||
g_detectedCpuCount = (int)smp.cpuCount;
|
||||
|
||||
if (!Arch::Gic::IsInitialized()) {
|
||||
KernelLogStream(WARNING, "SMP") << "No interrupt controller: secondary CPUs left parked";
|
||||
return;
|
||||
}
|
||||
|
||||
int apIndex = 1;
|
||||
for (uint64_t i = 0; i < smp.cpuCount && apIndex < MaxCPUs; i++) {
|
||||
montauk::boot::BootCpu& info = smp.cpus[i];
|
||||
if (info.hardwareId == smp.bspHardwareId) continue;
|
||||
|
||||
CpuData& ap = g_cpus[apIndex];
|
||||
ap.selfPtr = (uint64_t)≈
|
||||
ap.cpuIndex = apIndex;
|
||||
ap.currentSlot = -1;
|
||||
ap.started = false;
|
||||
ap.arch.mpidr = info.hardwareId;
|
||||
|
||||
info.extraArgument = (uint64_t)≈
|
||||
// Publish the CpuData before the CPU that reads it is woken.
|
||||
asm volatile("dsb ish" ::: "memory");
|
||||
smp.startCpu(&info, ApEntry);
|
||||
apIndex++;
|
||||
}
|
||||
g_cpuCount = apIndex;
|
||||
|
||||
int online = 1;
|
||||
for (int i = 1; i < g_cpuCount; i++) {
|
||||
uint64_t start = Arch::ReadTimestamp();
|
||||
uint64_t frequency;
|
||||
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
|
||||
while (!__atomic_load_n(&g_cpus[i].started, __ATOMIC_ACQUIRE) &&
|
||||
Arch::ReadTimestamp() - start < frequency) {
|
||||
Arch::Pause();
|
||||
}
|
||||
if (g_cpus[i].started) {
|
||||
online++;
|
||||
} else {
|
||||
KernelLogStream(ERROR, "SMP") << "CPU " << base::dec << (uint64_t)i
|
||||
<< " (MPIDR " << base::hex << g_cpus[i].arch.mpidr << ") failed to start";
|
||||
}
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "SMP") << base::dec << (uint64_t)online << " CPU(s) online";
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,17 @@
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// Saved by the EL0 synchronous-exception vector for `svc #0`:
|
||||
// x0-x5 carry the arguments and x8 the system-call number.
|
||||
// The exception frame Vectors.S saves for `svc #0` (the same layout as
|
||||
// Arch::PanicFrame): x0-x5 carry the arguments and x8 the system-call
|
||||
// number; the result is written back to x0.
|
||||
struct SyscallFrame {
|
||||
uint64_t arg1, arg2, arg3, arg4, arg5, arg6; // x0-x5
|
||||
uint64_t x6, x7;
|
||||
uint64_t syscall_nr; // x8
|
||||
uint64_t elr, spsr, sp_el0; // user PC, PSTATE, SP
|
||||
uint64_t x19_x30[12]; // callee-saved + LR
|
||||
uint64_t x9_x30[22];
|
||||
uint64_t sp_el0, elr, spsr, esr, far;
|
||||
};
|
||||
|
||||
static_assert(sizeof(SyscallFrame) == 288, "must match Vectors.S");
|
||||
|
||||
}
|
||||
@@ -1,24 +1,116 @@
|
||||
/*
|
||||
* Timer.cpp
|
||||
* aarch64 tick timer (implements Arch/Timer.hpp)
|
||||
* aarch64 tick timer: the EL1 virtual generic timer (implements Arch/Timer.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Timer.hpp>
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Arch/aarch64/Gic.hpp>
|
||||
#include <ACPI/ACPI.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
// TODO(aarch64): drive the EL1 virtual timer (CNTV_TVAL/CNTV_CTL) from
|
||||
// CNTFRQ_EL0, re-arming on each tick, once the GIC delivers its PPI.
|
||||
// The generic timer counts at CNTFRQ_EL0 on every CPU, so it needs no
|
||||
// calibration. Each CPU's virtual timer raises a PPI (the GTDT names it,
|
||||
// normally INTID 27); the tick is re-armed from the interrupt's ack hook,
|
||||
// which also drops the level-triggered line before the EOI.
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Arch::Timer {
|
||||
|
||||
bool InitializeBsp(uint32_t) {
|
||||
return false;
|
||||
// CNTV_CTL_EL0
|
||||
static constexpr uint64_t CtlEnable = 1ULL << 0;
|
||||
static constexpr uint64_t CtlMask = 1ULL << 1;
|
||||
|
||||
// GTDT timer flags
|
||||
static constexpr uint32_t GtdtEdgeTriggered = 1u << 0;
|
||||
|
||||
static constexpr uint32_t DefaultVirtualTimerIntid = 27;
|
||||
|
||||
struct Gtdt {
|
||||
Hal::ACPI::CommonSDTHeader Header;
|
||||
uint64_t CntControlBase;
|
||||
uint32_t Reserved;
|
||||
uint32_t SecureEl1Gsiv;
|
||||
uint32_t SecureEl1Flags;
|
||||
uint32_t NonSecureEl1Gsiv;
|
||||
uint32_t NonSecureEl1Flags;
|
||||
uint32_t VirtualTimerGsiv;
|
||||
uint32_t VirtualTimerFlags;
|
||||
} __attribute__((packed));
|
||||
|
||||
static uint64_t g_ticksPerMs = 0;
|
||||
static uint32_t g_intid = DefaultVirtualTimerIntid;
|
||||
static bool g_edgeTriggered = false;
|
||||
|
||||
static inline void WriteCtl(uint64_t value) {
|
||||
asm volatile("msr cntv_ctl_el0, %0\n\tisb" :: "r"(value) : "memory");
|
||||
}
|
||||
|
||||
void StartPeriodic(uint32_t) {
|
||||
static inline uint64_t ReadCval() {
|
||||
uint64_t value;
|
||||
asm volatile("mrs %0, cntv_cval_el0" : "=r"(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
static inline void WriteCval(uint64_t value) {
|
||||
asm volatile("msr cntv_cval_el0, %0\n\tisb" :: "r"(value) : "memory");
|
||||
}
|
||||
|
||||
// Advance the deadline by one period from the previous one (no drift),
|
||||
// skipping ticks the CPU missed rather than replaying them.
|
||||
static void Rearm() {
|
||||
uint64_t period = Smp::GetCurrentCpuData()->arch.timerPeriod;
|
||||
if (period == 0) {
|
||||
WriteCtl(CtlMask);
|
||||
return;
|
||||
}
|
||||
uint64_t now = ReadTimestamp();
|
||||
uint64_t next = ReadCval() + period;
|
||||
if ((int64_t)(next - now) <= 0) next = now + period;
|
||||
WriteCval(next);
|
||||
}
|
||||
|
||||
static void ReadGtdt() {
|
||||
auto* gtdt = (Gtdt*)Gic::FindTable(Gic::CurrentXsdt(), "GTDT");
|
||||
if (gtdt == nullptr || gtdt->Header.Length < sizeof(Gtdt)) return;
|
||||
if (gtdt->VirtualTimerGsiv >= 16 && gtdt->VirtualTimerGsiv < 32) {
|
||||
g_intid = gtdt->VirtualTimerGsiv;
|
||||
g_edgeTriggered = (gtdt->VirtualTimerFlags & GtdtEdgeTriggered) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool InitializeBsp(uint32_t periodMs) {
|
||||
uint64_t frequency;
|
||||
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
|
||||
if (frequency < 1000 || !Gic::IsInitialized()) return false;
|
||||
g_ticksPerMs = frequency / 1000;
|
||||
|
||||
ReadGtdt();
|
||||
Gic::SetAckHook(IRQ_TIMER, Rearm);
|
||||
StartPeriodic(periodMs);
|
||||
|
||||
KernelLogStream(OK, "Timer") << "Generic timer: " << base::dec << frequency
|
||||
<< " Hz counter, virtual timer INTID " << (uint64_t)g_intid;
|
||||
return true;
|
||||
}
|
||||
|
||||
// PPIs are banked: each CPU enables its own timer interrupt.
|
||||
void StartPeriodic(uint32_t periodMs) {
|
||||
Gic::RoutePpi(g_intid, IRQ_TIMER, g_edgeTriggered);
|
||||
uint64_t period = g_ticksPerMs * periodMs;
|
||||
Smp::GetCurrentCpuData()->arch.timerPeriod = period;
|
||||
WriteCval(ReadTimestamp() + period);
|
||||
WriteCtl(CtlEnable);
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
Smp::GetCurrentCpuData()->arch.timerPeriod = 0;
|
||||
WriteCtl(CtlMask);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
Vectors.S
|
||||
aarch64 exception vector table (VBAR_EL1)
|
||||
Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
/*
|
||||
* Every exception saves the same 288-byte frame on SP_EL1, laid out as
|
||||
* Arch::PanicFrame (InterruptFrame.hpp) and Arch::SyscallFrame:
|
||||
*
|
||||
* 0 x0 .. x30 (x30 at 240)
|
||||
* 248 interrupted SP (SP_EL0 from EL0, the pre-exception SP_EL1 from EL1)
|
||||
* 256 ELR_EL1
|
||||
* 264 SPSR_EL1
|
||||
* 272 ESR_EL1
|
||||
* 280 FAR_EL1
|
||||
*
|
||||
* The kernel runs on SP_EL1 (SPSel = 1, see Platform.cpp). From EL0,
|
||||
* SP_EL1 is the top of the thread's kernel stack: ArchEnterUserMode sets
|
||||
* it before the first ERET and every return to EL0 restores it.
|
||||
*/
|
||||
|
||||
#define FRAME_SIZE 288
|
||||
#define FRAME_SP 248
|
||||
#define FRAME_ELR 256
|
||||
#define FRAME_SPSR 264
|
||||
#define FRAME_ESR 272
|
||||
#define FRAME_FAR 280
|
||||
|
||||
.text
|
||||
|
||||
.macro SAVE_FRAME el
|
||||
sub sp, sp, #FRAME_SIZE
|
||||
stp x0, x1, [sp, #0]
|
||||
stp x2, x3, [sp, #16]
|
||||
stp x4, x5, [sp, #32]
|
||||
stp x6, x7, [sp, #48]
|
||||
stp x8, x9, [sp, #64]
|
||||
stp x10, x11, [sp, #80]
|
||||
stp x12, x13, [sp, #96]
|
||||
stp x14, x15, [sp, #112]
|
||||
stp x16, x17, [sp, #128]
|
||||
stp x18, x19, [sp, #144]
|
||||
stp x20, x21, [sp, #160]
|
||||
stp x22, x23, [sp, #176]
|
||||
stp x24, x25, [sp, #192]
|
||||
stp x26, x27, [sp, #208]
|
||||
stp x28, x29, [sp, #224]
|
||||
.if \el == 0
|
||||
mrs x21, sp_el0
|
||||
.else
|
||||
add x21, sp, #FRAME_SIZE
|
||||
.endif
|
||||
stp x30, x21, [sp, #240]
|
||||
mrs x22, elr_el1
|
||||
mrs x23, spsr_el1
|
||||
stp x22, x23, [sp, #FRAME_ELR]
|
||||
mrs x24, esr_el1
|
||||
mrs x25, far_el1
|
||||
stp x24, x25, [sp, #FRAME_ESR]
|
||||
.endm
|
||||
|
||||
/* Interrupts are masked again before ELR/SPSR are reloaded: an IRQ taken
|
||||
in between would overwrite them. */
|
||||
.macro RESTORE_FRAME el
|
||||
msr daifset, #2
|
||||
ldp x22, x23, [sp, #FRAME_ELR]
|
||||
msr elr_el1, x22
|
||||
msr spsr_el1, x23
|
||||
.if \el == 0
|
||||
ldr x21, [sp, #FRAME_SP]
|
||||
msr sp_el0, x21
|
||||
.endif
|
||||
ldp x0, x1, [sp, #0]
|
||||
ldp x2, x3, [sp, #16]
|
||||
ldp x4, x5, [sp, #32]
|
||||
ldp x6, x7, [sp, #48]
|
||||
ldp x8, x9, [sp, #64]
|
||||
ldp x10, x11, [sp, #80]
|
||||
ldp x12, x13, [sp, #96]
|
||||
ldp x14, x15, [sp, #112]
|
||||
ldp x16, x17, [sp, #128]
|
||||
ldp x18, x19, [sp, #144]
|
||||
ldp x20, x21, [sp, #160]
|
||||
ldp x22, x23, [sp, #176]
|
||||
ldp x24, x25, [sp, #192]
|
||||
ldp x26, x27, [sp, #208]
|
||||
ldp x28, x29, [sp, #224]
|
||||
ldr x30, [sp, #240]
|
||||
add sp, sp, #FRAME_SIZE
|
||||
eret
|
||||
.endm
|
||||
|
||||
/* Each vector slot is 128 bytes; the handlers live outside the table. */
|
||||
.macro VECTOR target
|
||||
.balign 0x80
|
||||
b \target
|
||||
.endm
|
||||
|
||||
.balign 0x800
|
||||
.global ArchExceptionVectors
|
||||
ArchExceptionVectors:
|
||||
/* Current EL with SP_EL0: the kernel never runs on SP_EL0. */
|
||||
VECTOR el1t_unexpected
|
||||
VECTOR el1t_unexpected
|
||||
VECTOR el1t_unexpected
|
||||
VECTOR el1t_unexpected
|
||||
/* Current EL with SP_ELx */
|
||||
VECTOR el1h_sync
|
||||
VECTOR el1h_irq
|
||||
VECTOR el1h_fiq
|
||||
VECTOR el1h_serror
|
||||
/* Lower EL, AArch64 */
|
||||
VECTOR el0_sync
|
||||
VECTOR el0_irq
|
||||
VECTOR el0_fiq
|
||||
VECTOR el0_serror
|
||||
/* Lower EL, AArch32: not supported */
|
||||
VECTOR el0_aarch32
|
||||
VECTOR el0_aarch32
|
||||
VECTOR el0_aarch32
|
||||
VECTOR el0_aarch32
|
||||
|
||||
/* ---- Exceptions from the kernel ---- */
|
||||
|
||||
el1t_unexpected:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #0
|
||||
bl ArchUnexpectedException
|
||||
|
||||
el1h_sync:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #0
|
||||
bl ArchSyncException
|
||||
RESTORE_FRAME 1
|
||||
|
||||
el1h_irq:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #0
|
||||
bl ArchIrqException
|
||||
RESTORE_FRAME 1
|
||||
|
||||
el1h_fiq:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #1
|
||||
bl ArchUnexpectedException
|
||||
|
||||
el1h_serror:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #2
|
||||
bl ArchUnexpectedException
|
||||
|
||||
/* ---- Exceptions from user mode ---- */
|
||||
|
||||
el0_sync:
|
||||
SAVE_FRAME 0
|
||||
lsr x9, x24, #26 /* ESR_EL1.EC */
|
||||
cmp x9, #0x15 /* SVC from AArch64 */
|
||||
b.ne 1f
|
||||
|
||||
msr daifclr, #2
|
||||
mov x0, sp
|
||||
bl SyscallDispatch
|
||||
str x0, [sp, #0]
|
||||
bl SchedExitIfKilled
|
||||
RESTORE_FRAME 0
|
||||
|
||||
1: mov x0, sp
|
||||
mov x1, #1
|
||||
bl ArchSyncException
|
||||
RESTORE_FRAME 0
|
||||
|
||||
el0_irq:
|
||||
SAVE_FRAME 0
|
||||
mov x0, sp
|
||||
mov x1, #1
|
||||
bl ArchIrqException
|
||||
RESTORE_FRAME 0
|
||||
|
||||
el0_fiq:
|
||||
SAVE_FRAME 0
|
||||
mov x0, sp
|
||||
mov x1, #1
|
||||
bl ArchUnexpectedException
|
||||
|
||||
el0_serror:
|
||||
SAVE_FRAME 0
|
||||
mov x0, sp
|
||||
mov x1, #2
|
||||
bl ArchUnexpectedException
|
||||
|
||||
el0_aarch32:
|
||||
SAVE_FRAME 0
|
||||
mov x0, sp
|
||||
mov x1, #3
|
||||
bl ArchUnexpectedException
|
||||
@@ -58,6 +58,14 @@ namespace Arch {
|
||||
Hal::IoApic::UnmaskIrq(Hal::IoApic::GetGsiForIrq(irq));
|
||||
}
|
||||
|
||||
bool PlatformLinesAreFixed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssignPlatformLine(uint8_t, uint32_t, bool, bool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ComposeMsi(uint8_t irq, MsiMessage& out) {
|
||||
// Physical destination mode, fixed delivery, edge triggered, to the
|
||||
// BSP (APIC ID 0 -- the destination field is left zero, as every
|
||||
|
||||
@@ -24,6 +24,15 @@ namespace Arch {
|
||||
asm volatile("mfence" ::: "memory");
|
||||
}
|
||||
|
||||
// Stores are not reordered with other stores, nor loads with loads.
|
||||
inline void DmaWriteFence() {
|
||||
asm volatile("" ::: "memory");
|
||||
}
|
||||
|
||||
inline void DmaReadFence() {
|
||||
asm volatile("" ::: "memory");
|
||||
}
|
||||
|
||||
inline void EnableInterrupts() {
|
||||
asm volatile("sti" ::: "memory");
|
||||
}
|
||||
@@ -79,6 +88,15 @@ namespace Arch {
|
||||
asm volatile("wbinvd" ::: "memory");
|
||||
}
|
||||
|
||||
inline void SyncInstructionCache(const void*, uint64_t) {
|
||||
}
|
||||
|
||||
inline void CleanDataCache(const void*, uint64_t) {
|
||||
}
|
||||
|
||||
inline void InvalidateDataCache(const void*, uint64_t) {
|
||||
}
|
||||
|
||||
inline void FlushTlbPage(uint64_t virtualAddress) {
|
||||
asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory");
|
||||
}
|
||||
|
||||
@@ -77,4 +77,8 @@ namespace Arch {
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
// x86 powers off through ACPI S5 (AcpiShutdown).
|
||||
void PowerOff() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <Drivers/Init.hpp>
|
||||
#include <Drivers/Serial/Uart.hpp>
|
||||
#include <Graphics/Framebuffer.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Platform.hpp>
|
||||
@@ -111,6 +112,8 @@ extern "C" void kmain() {
|
||||
|
||||
Hal::ACPI g_acpi((Hal::ACPI::XSDP*)Memory::HHDM(boot.rsdpPhysical));
|
||||
|
||||
Drivers::Serial::InitializeConsole(g_acpi.GetXSDT());
|
||||
|
||||
if (g_acpi.GetXSDT() != nullptr) {
|
||||
Hal::AcpiShutdown::Initialize(g_acpi.GetXSDT());
|
||||
|
||||
@@ -135,6 +138,8 @@ extern "C" void kmain() {
|
||||
// power control on the answer.
|
||||
Hal::AML::InitializeNamespaceObjects();
|
||||
|
||||
Pci::AssignInterruptLines();
|
||||
|
||||
Drivers::ProbeEarly();
|
||||
Drivers::InitializeGraphics();
|
||||
|
||||
|
||||
@@ -191,7 +191,8 @@ namespace Drivers::Audio::IntelHda {
|
||||
// Write verb to CORB
|
||||
g_corb[wp] = verb;
|
||||
|
||||
// Update write pointer
|
||||
// Update write pointer once the verb is visible
|
||||
Arch::DmaWriteFence();
|
||||
Write16(REG_CORBWP, wp);
|
||||
|
||||
return true;
|
||||
@@ -202,6 +203,7 @@ namespace Drivers::Audio::IntelHda {
|
||||
uint16_t wp = Read16(REG_RIRBWP) & 0xFF;
|
||||
|
||||
if (g_rirbReadPtr != wp) {
|
||||
Arch::DmaReadFence();
|
||||
g_rirbReadPtr = (g_rirbReadPtr + 1) % g_rirbSize;
|
||||
|
||||
uint32_t ex = g_rirb[g_rirbReadPtr].ResponseEx;
|
||||
|
||||
@@ -14,11 +14,16 @@
|
||||
#include <Drivers/USB/Xhci.hpp>
|
||||
#include <Drivers/Storage/Ahci.hpp>
|
||||
#include <Drivers/Storage/Nvme.hpp>
|
||||
#include <Drivers/Storage/Sdhci.hpp>
|
||||
#include <Drivers/Storage/Gpt.hpp>
|
||||
#include <Drivers/Audio/IntelHda.hpp>
|
||||
#include <Graphics/Framebuffer.hpp>
|
||||
#include <Net/Net.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <ACPI/AcpiDevices.hpp>
|
||||
#include <Boot/Boot.hpp>
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <ACPI/AML/AmlResource.hpp>
|
||||
|
||||
namespace Drivers {
|
||||
|
||||
@@ -89,6 +94,10 @@ namespace Drivers {
|
||||
return Storage::Nvme::Probe(dev);
|
||||
}
|
||||
|
||||
static bool ProbeSdhci(const Pci::PciDevice& dev) {
|
||||
return Storage::Sdhci::Probe(dev);
|
||||
}
|
||||
|
||||
static bool ProbeIntelHda(const Pci::PciDevice& dev) {
|
||||
return Audio::IntelHda::Probe(dev);
|
||||
}
|
||||
@@ -189,6 +198,18 @@ namespace Drivers {
|
||||
// also reports 0x01, but it exposes an I/O-space BAR0:
|
||||
// ReadBar0 returns 0 for I/O BARs and Probe rejects it
|
||||
// cleanly, so matching it here is harmless.
|
||||
// SD host controller (standard interface) - class 0x08/0x05
|
||||
{
|
||||
"SDHCI",
|
||||
0,
|
||||
0x08, // ClassCode (Base system peripheral)
|
||||
0x05, // SubClass (SD host controller)
|
||||
0xFF,
|
||||
nullptr,
|
||||
0,
|
||||
Pci::ProbePhase::Normal,
|
||||
ProbeSdhci,
|
||||
},
|
||||
{
|
||||
"IntelHDA",
|
||||
0x8086, // VendorId (Intel)
|
||||
@@ -227,8 +248,117 @@ namespace Drivers {
|
||||
Graphics::DgpuPower::PowerDownUnused();
|
||||
}
|
||||
|
||||
// A device's _DMA (or its parent's) bounds the addresses it can reach.
|
||||
// DMA buffers come from anywhere in RAM, so RAM past the bound is a
|
||||
// configuration error: on the Raspberry Pi 4 the firmware's "Limit RAM
|
||||
// to 3 GB" setting must stay on.
|
||||
static void WarnIfRamBeyondDma(const char* devicePath) {
|
||||
char path[160];
|
||||
int len = 0;
|
||||
while (devicePath[len] != '\0' && len < 120) { path[len] = devicePath[len]; len++; }
|
||||
|
||||
Hal::AML::ResourceList dma;
|
||||
bool found = false;
|
||||
for (int attempt = 0; attempt < 2 && !found; attempt++) {
|
||||
const char suffix[] = "._DMA";
|
||||
for (int i = 0; i < (int)sizeof(suffix); i++) path[len + i] = suffix[i];
|
||||
Hal::AML::Object obj;
|
||||
if (Hal::AML::GetInterpreter().EvaluateObject(path, obj) &&
|
||||
obj.Type == Hal::AML::ObjectType::Buffer &&
|
||||
Hal::AML::ParseResourceTemplate(obj.Buffer.Data, obj.Buffer.Length, dma)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
// Retry on the parent device.
|
||||
while (len > 0 && path[len - 1] != '.') len--;
|
||||
if (len == 0) return;
|
||||
len--;
|
||||
}
|
||||
|
||||
uint64_t dmaBase = 0, dmaLength = 0;
|
||||
if (!found || !dma.GetMemory(dmaBase, dmaLength) || dmaLength == 0) return;
|
||||
uint64_t dmaEnd = dmaBase + dmaLength;
|
||||
|
||||
const auto& map = montauk::boot::Info().memoryMap;
|
||||
for (uint64_t i = 0; i < map.count; i++) {
|
||||
const auto& r = map.regions[i];
|
||||
if (r.kind == montauk::boot::MemoryKind::Usable && r.base + r.length > dmaEnd) {
|
||||
Kt::KernelLogStream(Kt::WARNING, "Drivers") << devicePath << " can only reach RAM below 0x"
|
||||
<< base::hex << dmaEnd << ", but there is RAM up to 0x" << (r.base + r.length)
|
||||
<< ": its DMA may fail (Raspberry Pi 4: keep 'Limit RAM to 3 GB' enabled)";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An xHCI controller ACPI describes as a platform device (PNP0D10)
|
||||
// instead of a PCI function: the Raspberry Pi 4/400's VL805 under UEFI
|
||||
// firmware in ACPI mode, where firmware has set up the non-ECAM PCIe
|
||||
// behind it. Its DMA is non-coherent unless _CCA says otherwise.
|
||||
static void ProbePlatformXhci() {
|
||||
if (USB::Xhci::IsInitialized()) return;
|
||||
|
||||
Hal::AcpiDevices::DeviceInfo info{};
|
||||
if (!Hal::AcpiDevices::FindDeviceByHid("PNP0D10", info) &&
|
||||
!Hal::AcpiDevices::FindDeviceByHid("PNP0D15", info)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Hal::AML::ResourceList resources;
|
||||
uint64_t base = 0, length = 0;
|
||||
if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) ||
|
||||
!resources.GetMemory(base, length) || base == 0) {
|
||||
Kt::KernelLogStream(Kt::WARNING, "Drivers") << "xHCI " << info.Path
|
||||
<< ": no register block in _CRS";
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t gsi = 0;
|
||||
bool level = true, activeLow = false;
|
||||
if (auto* irq = resources.FindFirst(Hal::AML::ResourceType::ExtendedIrq)) {
|
||||
gsi = irq->ExtendedIrq.Interrupt;
|
||||
level = (irq->ExtendedIrq.Flags & 1) == 0;
|
||||
activeLow = (irq->ExtendedIrq.Flags & 2) != 0;
|
||||
}
|
||||
|
||||
bool coherent = false;
|
||||
Hal::AML::Object cca;
|
||||
if (Hal::AML::GetInterpreter().EvaluateChild(info.NodeIndex, "_CCA", nullptr, 0, cca) &&
|
||||
cca.Type == Hal::AML::ObjectType::Integer) {
|
||||
coherent = cca.Integer != 0;
|
||||
}
|
||||
|
||||
Kt::KernelLogStream(Kt::INFO, "Drivers") << "xHCI platform device " << info.Path
|
||||
<< " at 0x" << base::hex << base << ", GSI " << base::dec << (uint64_t)gsi
|
||||
<< (coherent ? ", coherent DMA" : ", non-coherent DMA");
|
||||
WarnIfRamBeyondDma(info.Path);
|
||||
USB::Xhci::ProbePlatform(base, length, gsi, level, activeLow, coherent);
|
||||
}
|
||||
|
||||
// SD host controllers ACPI describes directly: the Raspberry Pi 4's
|
||||
// EMMC2 (BRCME88C, the SD card slot) or a standard PNP0D40.
|
||||
static void ProbePlatformSdhci() {
|
||||
if (Storage::Sdhci::IsInitialized()) return;
|
||||
|
||||
Hal::AcpiDevices::DeviceInfo info{};
|
||||
if (!Hal::AcpiDevices::FindDeviceByHid("BRCME88C", info) &&
|
||||
!Hal::AcpiDevices::FindDeviceByHid("PNP0D40", info)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Hal::AML::ResourceList resources;
|
||||
uint64_t base = 0, length = 0;
|
||||
if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) ||
|
||||
!resources.GetMemory(base, length) || base == 0) {
|
||||
return;
|
||||
}
|
||||
Storage::Sdhci::ProbePlatform(base, info.Path);
|
||||
}
|
||||
|
||||
void ProbeNormal() {
|
||||
Pci::ProbeAll(g_driverTable, g_driverTableCount, Pci::ProbePhase::Normal);
|
||||
ProbePlatformXhci();
|
||||
ProbePlatformSdhci();
|
||||
}
|
||||
|
||||
void InitializeNetwork() {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <atomic>
|
||||
|
||||
using namespace Kt;
|
||||
@@ -491,6 +492,7 @@ namespace Drivers::Net::E1000 {
|
||||
|
||||
// Advance the tail pointer (tells the NIC there's a new packet)
|
||||
g_txTail = (g_txTail + 1) % TX_DESC_COUNT;
|
||||
Arch::DmaWriteFence();
|
||||
WriteReg(REG_TDT, g_txTail);
|
||||
|
||||
g_txPacketCount++;
|
||||
@@ -545,6 +547,7 @@ namespace Drivers::Net::E1000 {
|
||||
uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT;
|
||||
RxDescriptor& desc = g_rxDescs[nextIdx];
|
||||
if (!(desc.Status & RXSTA_DD)) break;
|
||||
Arch::DmaReadFence();
|
||||
|
||||
uint16_t length = desc.Length;
|
||||
g_rxPacketCount++;
|
||||
@@ -554,6 +557,7 @@ namespace Drivers::Net::E1000 {
|
||||
desc.Length = 0;
|
||||
desc.Errors = 0;
|
||||
g_rxTail = nextIdx;
|
||||
Arch::DmaWriteFence();
|
||||
WriteReg(REG_RDT, g_rxTail);
|
||||
processed++;
|
||||
}
|
||||
|
||||
@@ -667,6 +667,7 @@ namespace Drivers::Net::E1000E {
|
||||
if (!(desc.Status & RXSTA_DD)) {
|
||||
break;
|
||||
}
|
||||
Arch::DmaReadFence();
|
||||
|
||||
uint16_t length = desc.Length;
|
||||
g_rxPacketCount++;
|
||||
@@ -680,6 +681,7 @@ namespace Drivers::Net::E1000E {
|
||||
desc.Errors = 0;
|
||||
|
||||
g_rxTail = nextIdx;
|
||||
Arch::DmaWriteFence();
|
||||
WriteReg(REG_RDT, g_rxTail);
|
||||
processed++;
|
||||
}
|
||||
@@ -709,6 +711,7 @@ namespace Drivers::Net::E1000E {
|
||||
desc.Status = 0;
|
||||
|
||||
g_txTail = (g_txTail + 1) % TX_DESC_COUNT;
|
||||
Arch::DmaWriteFence();
|
||||
WriteReg(REG_TDT, g_txTail);
|
||||
|
||||
g_txPacketCount++;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Uart.cpp
|
||||
* Polled serial console from the ACPI SPCR table
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Uart.hpp"
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Serial {
|
||||
|
||||
struct GenericAddress {
|
||||
uint8_t AddressSpaceId; // 0 = system memory, 1 = system I/O
|
||||
uint8_t RegisterBitWidth;
|
||||
uint8_t RegisterBitOffset;
|
||||
uint8_t AccessSize; // 1 = byte, 2 = word, 3 = dword, 4 = qword
|
||||
uint64_t Address;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Spcr {
|
||||
Hal::ACPI::CommonSDTHeader Header;
|
||||
uint8_t InterfaceType;
|
||||
uint8_t Reserved[3];
|
||||
GenericAddress BaseAddress;
|
||||
uint8_t InterruptType;
|
||||
uint8_t Irq;
|
||||
uint32_t GlobalSystemInterrupt;
|
||||
uint8_t BaudRate;
|
||||
uint8_t Parity;
|
||||
uint8_t StopBits;
|
||||
uint8_t FlowControl;
|
||||
uint8_t TerminalType;
|
||||
} __attribute__((packed));
|
||||
|
||||
// SPCR / DBG2 serial interface subtypes
|
||||
static constexpr uint8_t Type16550 = 0x00;
|
||||
static constexpr uint8_t Type16450 = 0x01;
|
||||
static constexpr uint8_t TypePl011 = 0x03;
|
||||
static constexpr uint8_t TypeSbsa32 = 0x0D;
|
||||
static constexpr uint8_t TypeSbsaGeneric = 0x0E;
|
||||
static constexpr uint8_t TypeBcm2835 = 0x10;
|
||||
static constexpr uint8_t Type16550Generic = 0x12;
|
||||
|
||||
// PL011: data register, flag register (TX FIFO full)
|
||||
static constexpr uint64_t Pl011Dr = 0x00;
|
||||
static constexpr uint64_t Pl011Fr = 0x18;
|
||||
static constexpr uint32_t Pl011FrTxff = 1u << 5;
|
||||
|
||||
// 16550: transmit holding register, line status register (THR empty)
|
||||
static constexpr uint64_t Uart16550Thr = 0;
|
||||
static constexpr uint64_t Uart16550Lsr = 5;
|
||||
static constexpr uint8_t Uart16550LsrThre = 1u << 5;
|
||||
|
||||
// Bounded so a wedged or absent UART cannot hang the console.
|
||||
static constexpr int TxSpinLimit = 100000;
|
||||
|
||||
static volatile uint8_t* g_mmio = nullptr;
|
||||
static uint16_t g_port = 0;
|
||||
static uint32_t g_stride = 1;
|
||||
|
||||
static inline uint32_t Read32(uint64_t offset) {
|
||||
return *(volatile uint32_t*)(g_mmio + offset);
|
||||
}
|
||||
|
||||
static inline void Write32(uint64_t offset, uint32_t value) {
|
||||
*(volatile uint32_t*)(g_mmio + offset) = value;
|
||||
}
|
||||
|
||||
static void Pl011Putchar(char c) {
|
||||
for (int i = 0; i < TxSpinLimit && (Read32(Pl011Fr) & Pl011FrTxff); i++) {
|
||||
Arch::Pause();
|
||||
}
|
||||
Write32(Pl011Dr, (uint8_t)c);
|
||||
}
|
||||
|
||||
static uint8_t Mmio16550Read(uint64_t reg) {
|
||||
if (g_stride == 4) return (uint8_t)Read32(reg * 4);
|
||||
return g_mmio[reg];
|
||||
}
|
||||
|
||||
static void Mmio16550Write(uint64_t reg, uint8_t value) {
|
||||
if (g_stride == 4) Write32(reg * 4, value);
|
||||
else g_mmio[reg] = value;
|
||||
}
|
||||
|
||||
static void Mmio16550Putchar(char c) {
|
||||
for (int i = 0; i < TxSpinLimit && !(Mmio16550Read(Uart16550Lsr) & Uart16550LsrThre); i++) {
|
||||
Arch::Pause();
|
||||
}
|
||||
Mmio16550Write(Uart16550Thr, (uint8_t)c);
|
||||
}
|
||||
|
||||
static void Port16550Putchar(char c) {
|
||||
for (int i = 0; i < TxSpinLimit && !(Io::In8(g_port + Uart16550Lsr) & Uart16550LsrThre); i++) {
|
||||
Arch::Pause();
|
||||
}
|
||||
Io::Out8((uint8_t)c, g_port + Uart16550Thr);
|
||||
}
|
||||
|
||||
static Spcr* FindSpcr(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||
uint32_t entryCount = (xsdt->Length - sizeof(Hal::ACPI::CommonSDTHeader)) / 8;
|
||||
uint64_t* entries = (uint64_t*)((uint64_t)xsdt + sizeof(Hal::ACPI::CommonSDTHeader));
|
||||
|
||||
for (uint32_t i = 0; i < entryCount; i++) {
|
||||
auto* header = (Hal::ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
|
||||
const char* sig = header->Signature;
|
||||
if (sig[0] == 'S' && sig[1] == 'P' && sig[2] == 'C' && sig[3] == 'R') {
|
||||
return (Spcr*)header;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static volatile uint8_t* MapRegisters(uint64_t phys) {
|
||||
uint64_t page = phys & ~0xFFFULL;
|
||||
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
|
||||
return (volatile uint8_t*)Memory::HHDM(phys);
|
||||
}
|
||||
|
||||
void InitializeConsole(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||
if (xsdt == nullptr) return;
|
||||
|
||||
Spcr* spcr = FindSpcr(xsdt);
|
||||
if (spcr == nullptr || spcr->Header.Length < sizeof(Spcr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const GenericAddress& reg = spcr->BaseAddress;
|
||||
if (reg.Address == 0) return;
|
||||
|
||||
SerialSink sink = nullptr;
|
||||
const char* name = "";
|
||||
bool isIo = reg.AddressSpaceId == 1;
|
||||
|
||||
switch (spcr->InterfaceType) {
|
||||
case TypePl011:
|
||||
case TypeSbsa32:
|
||||
case TypeSbsaGeneric:
|
||||
if (isIo) return;
|
||||
g_mmio = MapRegisters(reg.Address);
|
||||
sink = Pl011Putchar;
|
||||
name = "PL011";
|
||||
break;
|
||||
|
||||
case Type16550:
|
||||
case Type16450:
|
||||
case Type16550Generic:
|
||||
case TypeBcm2835:
|
||||
if (isIo) {
|
||||
if (!Io::HasPortIo) return;
|
||||
g_port = (uint16_t)reg.Address;
|
||||
sink = Port16550Putchar;
|
||||
} else {
|
||||
// The BCM2835 mini UART is a 16550 with 32-bit registers.
|
||||
g_stride = (reg.RegisterBitWidth == 32 || reg.AccessSize == 3
|
||||
|| spcr->InterfaceType == TypeBcm2835) ? 4 : 1;
|
||||
g_mmio = MapRegisters(reg.Address);
|
||||
sink = Mmio16550Putchar;
|
||||
}
|
||||
name = spcr->InterfaceType == TypeBcm2835 ? "BCM2835 mini UART" : "16550";
|
||||
break;
|
||||
|
||||
default:
|
||||
KernelLogStream(WARNING, "Serial") << "SPCR interface type 0x" << base::hex
|
||||
<< (uint64_t)spcr->InterfaceType << " not supported";
|
||||
return;
|
||||
}
|
||||
|
||||
AttachSerialConsole(sink);
|
||||
KernelLogStream(OK, "Serial") << "Console on " << name << " at " << (isIo ? "port " : "")
|
||||
<< "0x" << base::hex << reg.Address;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Uart.hpp
|
||||
* Polled serial console from the ACPI SPCR table
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <ACPI/ACPI.hpp>
|
||||
|
||||
namespace Drivers::Serial {
|
||||
|
||||
// Find the firmware's serial console (SPCR: PL011, SBSA generic UART,
|
||||
// 16550 or the BCM2835 mini UART) and mirror the kernel console to it.
|
||||
// Uses the baud rate the firmware configured. Needs the kernel page
|
||||
// tables (the registers are mapped uncached through the HHDM).
|
||||
void InitializeConsole(Hal::ACPI::CommonSDTHeader* xsdt);
|
||||
|
||||
}
|
||||
@@ -267,13 +267,15 @@ namespace Drivers::Storage::Ahci {
|
||||
}
|
||||
}
|
||||
|
||||
// Issue the command
|
||||
// Issue the command once its header, table and PRDs are visible
|
||||
Arch::DmaWriteFence();
|
||||
WritePortReg(port, PORT_CI, 1u << slot);
|
||||
|
||||
// Wait for completion
|
||||
for (int i = 0; i < 5000000; i++) {
|
||||
uint32_t ci = ReadPortReg(port, PORT_CI);
|
||||
if (!(ci & (1u << slot))) {
|
||||
Arch::DmaReadFence();
|
||||
// Check for errors
|
||||
uint32_t is = ReadPortReg(port, PORT_IS);
|
||||
if (is & PORT_IS_TFES) {
|
||||
@@ -641,10 +643,19 @@ namespace Drivers::Storage::Ahci {
|
||||
for (int i = 0; i < MAX_PORTS; i++) {
|
||||
if (!(g_portsImplemented & (1u << i))) continue;
|
||||
|
||||
// The signature arrives in a D2H FIS, which an HBA may only
|
||||
// latch into PxSIG once FIS receive is running (QEMU does so):
|
||||
// start every port with a device on its link, then classify.
|
||||
if ((ReadPortReg(i, PORT_SSTS) & SSTS_DET_MASK) != SSTS_DET_PRESENT) continue;
|
||||
InitPort(i);
|
||||
|
||||
PortType type = ClassifyPort(i);
|
||||
g_ports[i].Type = type;
|
||||
|
||||
if (type == PortType::None) continue;
|
||||
if (type == PortType::None) {
|
||||
StopPort(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* typeStr = "Unknown";
|
||||
switch (type) {
|
||||
@@ -658,8 +669,6 @@ namespace Drivers::Storage::Ahci {
|
||||
KernelLogStream(INFO, "AHCI") << "Port " << base::dec << (uint64_t)i
|
||||
<< ": " << typeStr << " device detected";
|
||||
|
||||
InitPort(i);
|
||||
|
||||
if (type == PortType::Sata) {
|
||||
if (IdentifyDevice(i)) {
|
||||
g_ports[i].Active = true;
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Drivers::Storage {
|
||||
BLOCK_KIND_SATAPI = 2,
|
||||
BLOCK_KIND_NVME = 3,
|
||||
BLOCK_KIND_USB_MSC = 4,
|
||||
BLOCK_KIND_SD = 5,
|
||||
};
|
||||
|
||||
struct BlockDevice {
|
||||
|
||||
@@ -107,6 +107,8 @@ namespace Drivers::Storage::Gpt {
|
||||
return GUID_LINUX_SWAP;
|
||||
case 0x83:
|
||||
return GUID_LINUX_FS;
|
||||
case 0xEF:
|
||||
return GUID_EFI_SYSTEM;
|
||||
default:
|
||||
return GUID_BASIC_DATA;
|
||||
}
|
||||
@@ -551,6 +553,78 @@ namespace Drivers::Storage::Gpt {
|
||||
return removed;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MBR partition resize
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Read sector 0 into mbr and return the primary slot that holds `part`, or
|
||||
// -1 when the device is GPT (protective MBR) or `part` is not a primary
|
||||
// entry (a logical partition inside an extended one).
|
||||
static int FindMbrSlot(const PartitionInfo& part, ProtectiveMbr& mbr) {
|
||||
const BlockDevice* dev = GetBlockDevice(part.BlockDevIndex);
|
||||
if (!dev || !dev->ReadSectors(dev->Ctx, 0, 1, &mbr)) return -1;
|
||||
if (mbr.Signature != 0xAA55 || ValidateProtectiveMbr((const uint8_t*)&mbr)) return -1;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
const MbrPartitionEntry& e = mbr.Partitions[i];
|
||||
if (e.Type == 0 || IsExtendedMbrType(e.Type)) continue;
|
||||
if (e.LbaFirst == part.StartLba && e.SectorCount == part.SectorCount) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t GetMbrPartitionMaxSectors(int index) {
|
||||
const PartitionInfo* part = GetPartition(index);
|
||||
if (!part) return 0;
|
||||
|
||||
ProtectiveMbr mbr;
|
||||
if (FindMbrSlot(*part, mbr) < 0) return 0;
|
||||
const BlockDevice* dev = GetBlockDevice(part->BlockDevIndex);
|
||||
|
||||
// Stop at whichever primary entry starts next (an extended entry covers
|
||||
// every logical partition inside it), else at the end of the device.
|
||||
uint64_t end = dev->SectorCount;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
const MbrPartitionEntry& e = mbr.Partitions[i];
|
||||
if (e.Type == 0 || e.SectorCount == 0) continue;
|
||||
if (e.LbaFirst > part->StartLba && e.LbaFirst < end) end = e.LbaFirst;
|
||||
}
|
||||
|
||||
// The entry stores a 32-bit sector count; keep the end addressable too.
|
||||
if (end > 0xFFFFFFFFull) end = 0xFFFFFFFFull;
|
||||
return end > part->StartLba ? end - part->StartLba : 0;
|
||||
}
|
||||
|
||||
int ResizeMbrPartition(int index, uint64_t sectorCount) {
|
||||
if (index < 0 || index >= g_partitionCount) return -1;
|
||||
PartitionInfo& part = g_partitions[index];
|
||||
if (sectorCount < part.SectorCount) return -1;
|
||||
if (sectorCount > GetMbrPartitionMaxSectors(index)) return -1;
|
||||
if (sectorCount == part.SectorCount) return 0;
|
||||
|
||||
ProtectiveMbr mbr;
|
||||
int slot = FindMbrSlot(part, mbr);
|
||||
if (slot < 0) return -1;
|
||||
|
||||
MbrPartitionEntry& e = mbr.Partitions[slot];
|
||||
e.SectorCount = (uint32_t)sectorCount;
|
||||
// CHS cannot address past 8 GiB; FE FF FF tells CHS-era tools to use
|
||||
// the LBA fields instead.
|
||||
e.ChsLast[0] = 0xFE;
|
||||
e.ChsLast[1] = 0xFF;
|
||||
e.ChsLast[2] = 0xFF;
|
||||
|
||||
const BlockDevice* dev = GetBlockDevice(part.BlockDevIndex);
|
||||
if (!dev->WriteSectors(dev->Ctx, 0, 1, &mbr)) return -1;
|
||||
if (dev->Flush) dev->Flush(dev->Ctx);
|
||||
|
||||
KernelLogStream(OK, "MBR") << "Partition " << index << " grown from "
|
||||
<< part.SectorCount << " to " << sectorCount << " sectors";
|
||||
part.SectorCount = sectorCount;
|
||||
part.EndLba = part.StartLba + sectorCount - 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ASCII to UTF-16LE for partition names
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -127,6 +127,18 @@ namespace Drivers::Storage::Gpt {
|
||||
// hot-pluggable block device disappears.
|
||||
int RemovePartitionsForBlockDevice(int blockDevIndex);
|
||||
|
||||
// Largest size, in sectors, that partition `index` could be grown to: up to
|
||||
// the next partition on its device or the end of the device, within the
|
||||
// 32-bit LBA fields of an MBR entry. Returns 0 if the partition is not a
|
||||
// primary entry of a plain MBR disk (GPT disks and logical partitions are
|
||||
// not resized).
|
||||
uint64_t GetMbrPartitionMaxSectors(int index);
|
||||
|
||||
// Grow MBR partition `index` in place to sectorCount sectors, rewriting its
|
||||
// table entry and the in-memory record. Data is untouched. Returns 0 on
|
||||
// success, -1 if the partition cannot be resized to that size.
|
||||
int ResizeMbrPartition(int index, uint64_t sectorCount);
|
||||
|
||||
// Look up the human-readable name for a partition type GUID.
|
||||
const char* GetTypeName(const Guid& typeGuid);
|
||||
|
||||
|
||||
@@ -87,7 +87,9 @@ namespace Drivers::Storage::Nvme {
|
||||
// CQ y Head Doorbell offset = 0x1000 + ((2y+1) * doorbellStride)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// The submission queue entry must be visible before the doorbell.
|
||||
static void WriteSqTailDoorbell(uint16_t queueId, uint16_t value) {
|
||||
Arch::DmaWriteFence();
|
||||
uint32_t offset = 0x1000 + (2 * queueId) * g_doorbellStride;
|
||||
*(volatile uint32_t*)(g_mmioBase + offset) = value;
|
||||
}
|
||||
@@ -152,6 +154,8 @@ namespace Drivers::Storage::Nvme {
|
||||
uint16_t status = cqe->Status;
|
||||
|
||||
if ((status & CQE_PHASE_BIT) == phase) {
|
||||
// The rest of the entry (and the data) before the phase bit.
|
||||
Arch::DmaReadFence();
|
||||
uint16_t consumedId = cqe->CommandId;
|
||||
uint32_t result = cqe->Result;
|
||||
uint16_t sqHead = cqe->SqHead;
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* Sdhci.cpp
|
||||
* SD Host Controller (SDHCI) driver: SD cards as block devices
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Sdhci.hpp"
|
||||
#include "BlockDevice.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
// One slot, polled, programmed I/O through the buffer data port. PIO keeps
|
||||
// the driver free of DMA address translation and cache coherency, which on
|
||||
// the Raspberry Pi 4 depend on the SoC stepping (EMMC2's _DMA).
|
||||
//
|
||||
// Every register access is 32 bits wide: the Broadcom controllers (the Pi's
|
||||
// EMMC2, and the Arasan-derived EMMC before it) do not support narrower
|
||||
// ones. 8/16-bit fields are written by read-modify-write of their dword,
|
||||
// and the transfer-mode/command pair in a single write that issues the
|
||||
// command.
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Storage::Sdhci {
|
||||
|
||||
// Registers (SD Host Controller Simplified Specification 3.00)
|
||||
static constexpr uint32_t REG_BLOCK_SIZE_COUNT = 0x04; // size [15:0], count [31:16]
|
||||
static constexpr uint32_t REG_ARGUMENT = 0x08;
|
||||
static constexpr uint32_t REG_XFER_MODE_CMD = 0x0C; // transfer mode [15:0], command [31:16]
|
||||
static constexpr uint32_t REG_RESPONSE = 0x10; // 4 dwords
|
||||
static constexpr uint32_t REG_BUFFER_DATA = 0x20;
|
||||
static constexpr uint32_t REG_PRESENT_STATE = 0x24;
|
||||
static constexpr uint32_t REG_HOST_CONTROL = 0x28; // host ctl 1 [7:0], power [15:8]
|
||||
static constexpr uint32_t REG_CLOCK_CONTROL = 0x2C; // clock [15:0], timeout [23:16], reset [31:24]
|
||||
static constexpr uint32_t REG_INT_STATUS = 0x30; // normal [15:0], error [31:16]
|
||||
static constexpr uint32_t REG_INT_ENABLE = 0x34; // status enables
|
||||
static constexpr uint32_t REG_INT_SIGNAL = 0x38; // interrupt signal enables
|
||||
static constexpr uint32_t REG_CAPABILITIES = 0x40;
|
||||
static constexpr uint32_t REG_VERSION = 0xFC; // host version [31:16]
|
||||
|
||||
// Present state
|
||||
static constexpr uint32_t PS_CMD_INHIBIT = 1u << 0;
|
||||
static constexpr uint32_t PS_DAT_INHIBIT = 1u << 1;
|
||||
|
||||
// Interrupt status
|
||||
static constexpr uint32_t INT_CMD_COMPLETE = 1u << 0;
|
||||
static constexpr uint32_t INT_XFER_COMPLETE = 1u << 1;
|
||||
static constexpr uint32_t INT_WRITE_READY = 1u << 4;
|
||||
static constexpr uint32_t INT_READ_READY = 1u << 5;
|
||||
static constexpr uint32_t INT_ERROR = 1u << 15;
|
||||
static constexpr uint32_t INT_ERROR_MASK = 0xFFFF0000u;
|
||||
|
||||
// Host control 1 / power control
|
||||
static constexpr uint32_t HC_4BIT = 1u << 1;
|
||||
static constexpr uint32_t PWR_ON = 1u << 8;
|
||||
static constexpr uint32_t PWR_330 = 7u << 9;
|
||||
static constexpr uint32_t PWR_300 = 6u << 9;
|
||||
static constexpr uint32_t PWR_180 = 5u << 9;
|
||||
|
||||
// Clock control
|
||||
static constexpr uint32_t CLK_INT_EN = 1u << 0;
|
||||
static constexpr uint32_t CLK_INT_STABLE = 1u << 1;
|
||||
static constexpr uint32_t CLK_SD_EN = 1u << 2;
|
||||
static constexpr uint32_t TIMEOUT_MAX = 0xEu << 16;
|
||||
|
||||
// Software reset (byte 0x2F)
|
||||
static constexpr uint32_t RESET_ALL = 1u << 24;
|
||||
static constexpr uint32_t RESET_CMD = 1u << 25;
|
||||
static constexpr uint32_t RESET_DAT = 1u << 26;
|
||||
|
||||
// Capabilities
|
||||
static constexpr uint32_t CAP_VOLT_330 = 1u << 24;
|
||||
static constexpr uint32_t CAP_VOLT_300 = 1u << 25;
|
||||
|
||||
// Transfer mode
|
||||
static constexpr uint32_t XM_BLOCK_COUNT_EN = 1u << 1;
|
||||
static constexpr uint32_t XM_AUTO_CMD12 = 1u << 2;
|
||||
static constexpr uint32_t XM_READ = 1u << 4;
|
||||
static constexpr uint32_t XM_MULTI_BLOCK = 1u << 5;
|
||||
|
||||
// Command register flags
|
||||
enum Response : uint32_t {
|
||||
RespNone = 0,
|
||||
Resp136 = 1, // R2
|
||||
Resp48 = 2 | 3u << 3, // R1, R6, R7: CRC and index checked
|
||||
Resp48Busy= 3 | 3u << 3, // R1b
|
||||
Resp48NoCrc = 2, // R3
|
||||
};
|
||||
static constexpr uint32_t CMD_DATA = 1u << 5;
|
||||
|
||||
static constexpr uint32_t BlockSize = 512;
|
||||
// The block count register is 16 bits wide.
|
||||
static constexpr uint32_t MaxBlocksPerCommand = 0xFFFF;
|
||||
static constexpr uint64_t CommandTimeoutMs = 200;
|
||||
static constexpr uint64_t DataTimeoutMs = 1000;
|
||||
|
||||
// Base clock when the capabilities leave it to the platform: the
|
||||
// Raspberry Pi 4's EMMC2 runs from a 100 MHz clock the firmware sets up.
|
||||
static constexpr uint32_t FallbackBaseClockHz = 100000000;
|
||||
|
||||
static volatile uint8_t* g_regs = nullptr;
|
||||
static bool g_initialized = false;
|
||||
static uint32_t g_version = 0; // 0: 1.00, 1: 2.00, 2: 3.00
|
||||
static uint32_t g_baseClockHz = 0;
|
||||
static uint32_t g_rca = 0;
|
||||
static bool g_highCapacity = false;
|
||||
static uint64_t g_sectorCount = 0;
|
||||
static kcp::Mutex g_lock;
|
||||
|
||||
static inline uint32_t Read(uint32_t reg) {
|
||||
return *(volatile uint32_t*)(g_regs + reg);
|
||||
}
|
||||
|
||||
static inline void Write(uint32_t reg, uint32_t value) {
|
||||
*(volatile uint32_t*)(g_regs + reg) = value;
|
||||
}
|
||||
|
||||
static bool WaitClear(uint32_t reg, uint32_t mask, uint64_t timeoutMs) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Read(reg) & mask) {
|
||||
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||
Arch::Pause();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool Reset(uint32_t which) {
|
||||
Write(REG_CLOCK_CONTROL, Read(REG_CLOCK_CONTROL) | which);
|
||||
return WaitClear(REG_CLOCK_CONTROL, which, 100);
|
||||
}
|
||||
|
||||
// Wait for any of `bits` in the normal status, or an error. Clears what
|
||||
// it returns on.
|
||||
static bool WaitStatus(uint32_t bits, uint64_t timeoutMs) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
for (;;) {
|
||||
uint32_t status = Read(REG_INT_STATUS);
|
||||
if (status & (INT_ERROR | INT_ERROR_MASK)) {
|
||||
Write(REG_INT_STATUS, status);
|
||||
Reset(RESET_CMD | RESET_DAT);
|
||||
return false;
|
||||
}
|
||||
if (status & bits) {
|
||||
Write(REG_INT_STATUS, status & bits);
|
||||
return true;
|
||||
}
|
||||
if (Timekeeping::GetMilliseconds() - start > timeoutMs) {
|
||||
Reset(RESET_CMD | RESET_DAT);
|
||||
return false;
|
||||
}
|
||||
Arch::Pause();
|
||||
}
|
||||
}
|
||||
|
||||
static bool SendCommand(uint32_t index, uint32_t argument, uint32_t response,
|
||||
uint32_t transferMode = 0, bool data = false) {
|
||||
uint32_t inhibit = PS_CMD_INHIBIT;
|
||||
if (data || response == Resp48Busy) inhibit |= PS_DAT_INHIBIT;
|
||||
if (!WaitClear(REG_PRESENT_STATE, inhibit, CommandTimeoutMs)) {
|
||||
Reset(RESET_CMD | RESET_DAT);
|
||||
return false;
|
||||
}
|
||||
|
||||
Write(REG_INT_STATUS, 0xFFFFFFFF);
|
||||
Write(REG_ARGUMENT, argument);
|
||||
uint32_t command = index << 8 | response | (data ? CMD_DATA : 0);
|
||||
Write(REG_XFER_MODE_CMD, command << 16 | transferMode);
|
||||
|
||||
if (!WaitStatus(INT_CMD_COMPLETE, CommandTimeoutMs)) return false;
|
||||
if (response == Resp48Busy && !WaitStatus(INT_XFER_COMPLETE, DataTimeoutMs)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// An application command: CMD55 with the card's address first.
|
||||
static bool SendAppCommand(uint32_t index, uint32_t argument, uint32_t response) {
|
||||
return SendCommand(55, g_rca << 16, Resp48) && SendCommand(index, argument, response);
|
||||
}
|
||||
|
||||
// Bits [start, start+length) of a 136-bit response (CID/CSD numbering).
|
||||
// The controller drops the CRC byte, so CSD bit n is response bit n-8.
|
||||
static uint32_t ResponseBits(const uint32_t response[4], int start, int length) {
|
||||
uint32_t value = 0;
|
||||
for (int i = 0; i < length; i++) {
|
||||
int bit = start + i - 8;
|
||||
if (response[bit / 32] & (1u << (bit % 32))) value |= 1u << i;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool SetClock(uint32_t hz) {
|
||||
uint32_t clock = Read(REG_CLOCK_CONTROL);
|
||||
Write(REG_CLOCK_CONTROL, clock & ~(0xFFFFu));
|
||||
|
||||
uint32_t divider = 0; // base clock / (2 * divider); 0 = undivided
|
||||
if (hz < g_baseClockHz) {
|
||||
divider = (g_baseClockHz + 2 * hz - 1) / (2 * hz);
|
||||
uint32_t max = g_version >= 2 ? 0x3FF : 0x80;
|
||||
if (divider > max) divider = max;
|
||||
if (g_version < 2) {
|
||||
// Before 3.00 the divider is a power of two.
|
||||
uint32_t p = 1;
|
||||
while (p < divider) p <<= 1;
|
||||
divider = p;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t field = (divider & 0xFF) << 8 | ((divider >> 8) & 0x3) << 6;
|
||||
clock = (Read(REG_CLOCK_CONTROL) & ~0xFFFFu) | field | CLK_INT_EN;
|
||||
Write(REG_CLOCK_CONTROL, clock);
|
||||
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (!(Read(REG_CLOCK_CONTROL) & CLK_INT_STABLE)) {
|
||||
if (Timekeeping::GetMilliseconds() - start > 20) return false;
|
||||
Arch::Pause();
|
||||
}
|
||||
Write(REG_CLOCK_CONTROL, Read(REG_CLOCK_CONTROL) | CLK_SD_EN);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool InitializeCard() {
|
||||
uint32_t response[4];
|
||||
|
||||
if (!SendCommand(0, 0, RespNone)) return false;
|
||||
|
||||
// CMD8: a 2.00+ card echoes the check pattern.
|
||||
bool v2 = SendCommand(8, 0x1AA, Resp48) && (Read(REG_RESPONSE) & 0xFFF) == 0x1AA;
|
||||
|
||||
// ACMD41 until the card leaves its busy state (up to a second).
|
||||
uint32_t ocr = 0;
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
for (;;) {
|
||||
if (!SendAppCommand(41, (v2 ? 1u << 30 : 0) | 0x00FF8000, Resp48NoCrc)) return false;
|
||||
ocr = Read(REG_RESPONSE);
|
||||
if (ocr & (1u << 31)) break;
|
||||
if (Timekeeping::GetMilliseconds() - start > 1000) return false;
|
||||
}
|
||||
g_highCapacity = (ocr & (1u << 30)) != 0;
|
||||
|
||||
if (!SendCommand(2, 0, Resp136)) return false;
|
||||
if (!SendCommand(3, 0, Resp48)) return false;
|
||||
g_rca = Read(REG_RESPONSE) >> 16;
|
||||
|
||||
if (!SendCommand(9, g_rca << 16, Resp136)) return false;
|
||||
for (int i = 0; i < 4; i++) response[i] = Read(REG_RESPONSE + i * 4);
|
||||
if (ResponseBits(response, 126, 2) == 1) {
|
||||
uint64_t cSize = ResponseBits(response, 48, 22);
|
||||
g_sectorCount = (cSize + 1) * 1024;
|
||||
} else {
|
||||
uint64_t cSize = ResponseBits(response, 62, 12);
|
||||
uint64_t mult = ResponseBits(response, 47, 3);
|
||||
uint64_t readBlockLength = ResponseBits(response, 80, 4);
|
||||
g_sectorCount = ((cSize + 1) << (mult + 2)) * (1ULL << readBlockLength) / BlockSize;
|
||||
}
|
||||
|
||||
if (!SendCommand(7, g_rca << 16, Resp48Busy)) return false;
|
||||
if (!g_highCapacity && !SendCommand(16, BlockSize, Resp48)) return false;
|
||||
|
||||
// 4-bit bus when the card agrees; 1-bit otherwise.
|
||||
if (SendAppCommand(6, 2, Resp48)) {
|
||||
Write(REG_HOST_CONTROL, Read(REG_HOST_CONTROL) | HC_4BIT);
|
||||
}
|
||||
|
||||
SetClock(25000000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Move one block through the buffer data port. Caller buffers are only
|
||||
// byte aligned in general, so unaligned ones go through a bounce copy.
|
||||
static void PioBlock(uint8_t* data, bool write) {
|
||||
uint32_t bounce[BlockSize / 4];
|
||||
bool aligned = ((uintptr_t)data & 3) == 0;
|
||||
uint32_t* words = aligned ? (uint32_t*)data : bounce;
|
||||
if (write && !aligned) memcpy(bounce, data, BlockSize);
|
||||
for (uint32_t i = 0; i < BlockSize / 4; i++) {
|
||||
if (write) Write(REG_BUFFER_DATA, words[i]);
|
||||
else words[i] = Read(REG_BUFFER_DATA);
|
||||
}
|
||||
if (!write && !aligned) memcpy(data, bounce, BlockSize);
|
||||
}
|
||||
|
||||
// One command for `count` consecutive blocks: CMD17/CMD24 for a single
|
||||
// block, else CMD18/CMD25 with the controller sending the closing CMD12
|
||||
// itself (Auto CMD12; the BCM2711's EMMC2 wants it for multi-block reads).
|
||||
// Cards program a multi-block write a flash page at a time; issued one
|
||||
// block per command, each 512 bytes costs a full page program, which made
|
||||
// writes (the first-boot grow above all) take tens of minutes.
|
||||
static bool TransferBlocks(uint64_t lba, uint32_t count, uint8_t* data, bool write) {
|
||||
uint32_t address = g_highCapacity ? (uint32_t)lba : (uint32_t)(lba * BlockSize);
|
||||
bool multi = count > 1;
|
||||
Write(REG_BLOCK_SIZE_COUNT, count << 16 | BlockSize);
|
||||
|
||||
uint32_t mode = write ? 0 : XM_READ;
|
||||
if (multi) mode |= XM_BLOCK_COUNT_EN | XM_AUTO_CMD12 | XM_MULTI_BLOCK;
|
||||
uint32_t command = write ? (multi ? 25 : 24) : (multi ? 18 : 17);
|
||||
|
||||
bool ok = SendCommand(command, address, Resp48, mode, true);
|
||||
for (uint32_t i = 0; ok && i < count; i++) {
|
||||
ok = WaitStatus(write ? INT_WRITE_READY : INT_READ_READY, DataTimeoutMs);
|
||||
if (ok) PioBlock(data + (uint64_t)i * BlockSize, write);
|
||||
}
|
||||
ok = ok && WaitStatus(INT_XFER_COMPLETE, DataTimeoutMs);
|
||||
|
||||
// A failed multi-block transfer leaves the card sending or receiving
|
||||
// data; stop it so the next command finds it in the transfer state.
|
||||
if (!ok && multi) SendCommand(12, 0, Resp48Busy);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool TransferSectors(uint64_t lba, uint32_t count, uint8_t* data, bool write) {
|
||||
if (lba + count > g_sectorCount) return false;
|
||||
g_lock.Acquire();
|
||||
bool ok = true;
|
||||
while (ok && count > 0) {
|
||||
uint32_t run = count > MaxBlocksPerCommand ? MaxBlocksPerCommand : count;
|
||||
ok = TransferBlocks(lba, run, data, write);
|
||||
lba += run;
|
||||
data += (uint64_t)run * BlockSize;
|
||||
count -= run;
|
||||
}
|
||||
g_lock.Release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool ReadSectors(void*, uint64_t lba, uint32_t count, void* buffer) {
|
||||
return TransferSectors(lba, count, (uint8_t*)buffer, false);
|
||||
}
|
||||
|
||||
static bool WriteSectors(void*, uint64_t lba, uint32_t count, const void* buffer) {
|
||||
return TransferSectors(lba, count, (uint8_t*)buffer, true);
|
||||
}
|
||||
|
||||
static bool InitializeController(uint64_t mmioPhys, const char* name) {
|
||||
if (g_initialized) return false;
|
||||
|
||||
Memory::VMM::g_paging->MapMMIO(mmioPhys & ~0xFFFULL, Memory::HHDM(mmioPhys & ~0xFFFULL));
|
||||
g_regs = (volatile uint8_t*)Memory::HHDM(mmioPhys);
|
||||
|
||||
if (!Reset(RESET_ALL)) {
|
||||
KernelLogStream(ERROR, "SDHCI") << name << ": reset timed out";
|
||||
return false;
|
||||
}
|
||||
|
||||
g_version = (Read(REG_VERSION) >> 16) & 0xFF;
|
||||
uint32_t caps = Read(REG_CAPABILITIES);
|
||||
uint32_t baseMhz = g_version >= 2 ? (caps >> 8) & 0xFF : (caps >> 8) & 0x3F;
|
||||
g_baseClockHz = baseMhz != 0 ? baseMhz * 1000000 : FallbackBaseClockHz;
|
||||
|
||||
uint32_t voltage = (caps & CAP_VOLT_330) ? PWR_330 : (caps & CAP_VOLT_300) ? PWR_300 : PWR_180;
|
||||
Write(REG_HOST_CONTROL, (Read(REG_HOST_CONTROL) & ~0xFF00u) | voltage | PWR_ON);
|
||||
|
||||
Write(REG_INT_ENABLE, 0xFFFFFFFF);
|
||||
Write(REG_INT_SIGNAL, 0);
|
||||
Write(REG_CLOCK_CONTROL, (Read(REG_CLOCK_CONTROL) & ~0xFF0000u) | TIMEOUT_MAX);
|
||||
|
||||
if (!SetClock(400000)) {
|
||||
KernelLogStream(ERROR, "SDHCI") << name << ": clock did not stabilize";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The card needs 74 clocks after power-up before its first command.
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < 2) Arch::Pause();
|
||||
|
||||
KernelLogStream(INFO, "SDHCI") << name << ": version " << base::dec
|
||||
<< (uint64_t)(g_version + 1) << ".00, base clock " << (uint64_t)(g_baseClockHz / 1000000)
|
||||
<< " MHz" << (baseMhz == 0 ? " (assumed)" : "");
|
||||
|
||||
if (!InitializeCard()) {
|
||||
KernelLogStream(INFO, "SDHCI") << name << ": no SD card";
|
||||
return false;
|
||||
}
|
||||
|
||||
g_initialized = true;
|
||||
KernelLogStream(OK, "SDHCI") << name << ": " << (g_highCapacity ? "SDHC/SDXC" : "SDSC")
|
||||
<< " card, " << base::dec << (g_sectorCount * BlockSize / (1024 * 1024)) << " MiB";
|
||||
|
||||
BlockDevice dev = {};
|
||||
dev.ReadSectors = ReadSectors;
|
||||
dev.WriteSectors = WriteSectors;
|
||||
dev.SectorCount = g_sectorCount;
|
||||
dev.SectorSize = BlockSize;
|
||||
dev.Kind = BLOCK_KIND_SD;
|
||||
const char model[] = "SD card";
|
||||
memcpy(dev.Model, model, sizeof(model));
|
||||
RegisterBlockDevice(dev);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Probe(const Pci::PciDevice& dev) {
|
||||
uint64_t bar = Pci::ReadBar0(dev.Bus, dev.Device, dev.Function);
|
||||
if (bar == 0) return false;
|
||||
Pci::EnableBusMaster(dev.Bus, dev.Device, dev.Function);
|
||||
return InitializeController(bar, "PCI controller");
|
||||
}
|
||||
|
||||
bool ProbePlatform(uint64_t mmioPhys, const char* name) {
|
||||
return InitializeController(mmioPhys, name);
|
||||
}
|
||||
|
||||
bool IsInitialized() {
|
||||
return g_initialized;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Sdhci.hpp
|
||||
* SD Host Controller (SDHCI) driver: SD cards as block devices
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Pci/Pci.hpp>
|
||||
|
||||
namespace Drivers::Storage::Sdhci {
|
||||
|
||||
// A standard SD host controller on PCI (class 08h/05h).
|
||||
bool Probe(const Pci::PciDevice& dev);
|
||||
|
||||
// One ACPI describes (the Raspberry Pi 4's EMMC2, BRCME88C; a generic
|
||||
// PNP0D40 controller): its register block.
|
||||
bool ProbePlatform(uint64_t mmioPhys, const char* name);
|
||||
|
||||
bool IsInitialized();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* Hub.cpp
|
||||
* USB hub class driver (USB 2.0 and SuperSpeed hubs)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Hub.hpp"
|
||||
#include "Xhci.hpp"
|
||||
#include "UsbDevice.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <atomic>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
static void BusyWaitMs(uint64_t ms) {
|
||||
if (Arch::InterruptsEnabled()) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < ms) {
|
||||
Arch::Pause();
|
||||
}
|
||||
} else {
|
||||
// The tick is not advancing: count ~1 us delays instead
|
||||
for (uint64_t i = 0; i < ms * 1000; i++) {
|
||||
Arch::IoDelay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Drivers::USB::Hub {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hub class requests and port status (USB 2.0 11.24, USB 3.2 10.16)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constexpr uint8_t REQTYPE_HUB_IN = 0xA0; // Dev-to-host, class, device
|
||||
constexpr uint8_t REQTYPE_HUB_OUT = 0x20; // Host-to-dev, class, device
|
||||
constexpr uint8_t REQTYPE_PORT_IN = 0xA3; // Dev-to-host, class, other (port)
|
||||
constexpr uint8_t REQTYPE_PORT_OUT = 0x23; // Host-to-dev, class, other (port)
|
||||
|
||||
constexpr uint8_t REQ_GET_STATUS = 0x00;
|
||||
constexpr uint8_t REQ_CLEAR_FEATURE = 0x01;
|
||||
constexpr uint8_t REQ_SET_FEATURE = 0x03;
|
||||
constexpr uint8_t REQ_GET_DESCRIPTOR = 0x06;
|
||||
constexpr uint8_t REQ_SET_HUB_DEPTH = 0x0C; // SuperSpeed hubs only
|
||||
|
||||
constexpr uint8_t DESC_HUB = 0x29;
|
||||
constexpr uint8_t DESC_SS_HUB = 0x2A;
|
||||
|
||||
// Port feature selectors
|
||||
constexpr uint16_t FEAT_PORT_RESET = 4;
|
||||
constexpr uint16_t FEAT_PORT_POWER = 8;
|
||||
|
||||
// wPortStatus
|
||||
constexpr uint16_t PS_CONNECTION = (1 << 0);
|
||||
constexpr uint16_t PS_ENABLE = (1 << 1);
|
||||
constexpr uint16_t PS_RESET = (1 << 4);
|
||||
constexpr uint16_t PS_LOW_SPEED = (1 << 9); // USB 2 hubs
|
||||
constexpr uint16_t PS_HIGH_SPEED = (1 << 10); // USB 2 hubs
|
||||
|
||||
// wPortChange
|
||||
constexpr uint16_t PC_CONNECTION = (1 << 0);
|
||||
constexpr uint16_t PC_RESET = (1 << 4);
|
||||
|
||||
// The route string holds a 4-bit port number per tier, five tiers.
|
||||
constexpr uint8_t MAX_TIERS = 5;
|
||||
constexpr uint8_t MAX_ROUTABLE_PORT = 15;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// State
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
struct HubState {
|
||||
bool Active;
|
||||
HubDescriptorInfo Info;
|
||||
};
|
||||
|
||||
static HubState g_hubs[Xhci::MAX_SLOTS + 1] = {};
|
||||
|
||||
// Written from the event-ring drain, consumed by ProcessPendingWork.
|
||||
static std::atomic<uint32_t> g_changedPorts[Xhci::MAX_SLOTS + 1] = {}; // bit n = port n
|
||||
static std::atomic<bool> g_rearm[Xhci::MAX_SLOTS + 1] = {};
|
||||
static std::atomic<bool> g_endpointFailed[Xhci::MAX_SLOTS + 1] = {};
|
||||
static std::atomic<bool> g_pending{false};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Requests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static bool Succeeded(uint32_t cc) {
|
||||
return cc == Xhci::CC_SUCCESS || cc == Xhci::CC_SHORT_PACKET;
|
||||
}
|
||||
|
||||
static bool GetPortStatus(uint8_t hubSlot, uint8_t port, uint16_t& status, uint16_t& change) {
|
||||
uint8_t buf[4] = {};
|
||||
uint32_t cc = Xhci::ControlTransfer(hubSlot, REQTYPE_PORT_IN, REQ_GET_STATUS,
|
||||
0, port, sizeof(buf), buf, true);
|
||||
if (!Succeeded(cc)) return false;
|
||||
status = (uint16_t)(buf[0] | (buf[1] << 8));
|
||||
change = (uint16_t)(buf[2] | (buf[3] << 8));
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t SetPortFeature(uint8_t hubSlot, uint8_t port, uint16_t feature) {
|
||||
return Xhci::ControlTransfer(hubSlot, REQTYPE_PORT_OUT, REQ_SET_FEATURE,
|
||||
feature, port, 0, nullptr, false);
|
||||
}
|
||||
|
||||
static uint32_t ClearPortFeature(uint8_t hubSlot, uint8_t port, uint16_t feature) {
|
||||
return Xhci::ControlTransfer(hubSlot, REQTYPE_PORT_OUT, REQ_CLEAR_FEATURE,
|
||||
feature, port, 0, nullptr, false);
|
||||
}
|
||||
|
||||
// Acknowledge every change bit in `change`. Bits 0-4 clear with
|
||||
// C_PORT_CONNECTION..C_PORT_RESET (16-20); SuperSpeed hubs add
|
||||
// C_BH_PORT_RESET, C_PORT_LINK_STATE and C_PORT_CONFIG_ERROR.
|
||||
static void ClearPortChanges(uint8_t hubSlot, uint8_t port, uint16_t change, bool superSpeed) {
|
||||
for (uint16_t bit = 0; bit <= 4; bit++) {
|
||||
if (change & (1u << bit)) ClearPortFeature(hubSlot, port, (uint16_t)(16 + bit));
|
||||
}
|
||||
if (superSpeed) {
|
||||
if (change & (1u << 5)) ClearPortFeature(hubSlot, port, 29);
|
||||
if (change & (1u << 6)) ClearPortFeature(hubSlot, port, 25);
|
||||
if (change & (1u << 7)) ClearPortFeature(hubSlot, port, 26);
|
||||
}
|
||||
}
|
||||
|
||||
// Acknowledge hub-wide changes (local power, over-current): until they
|
||||
// are cleared the hub keeps reporting bit 0 of its change bitmap.
|
||||
static void ClearHubChanges(uint8_t hubSlot) {
|
||||
uint8_t buf[4] = {};
|
||||
uint32_t cc = Xhci::ControlTransfer(hubSlot, REQTYPE_HUB_IN, REQ_GET_STATUS,
|
||||
0, 0, sizeof(buf), buf, true);
|
||||
if (!Succeeded(cc)) return;
|
||||
uint16_t change = (uint16_t)(buf[2] | (buf[3] << 8));
|
||||
for (uint16_t bit = 0; bit <= 1; bit++) { // C_HUB_LOCAL_POWER, C_HUB_OVER_CURRENT
|
||||
if (change & (1u << bit)) {
|
||||
Xhci::ControlTransfer(hubSlot, REQTYPE_HUB_OUT, REQ_CLEAR_FEATURE,
|
||||
bit, 0, 0, nullptr, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ReadDescriptor(uint8_t slotId, uint32_t speed, HubDescriptorInfo& out) {
|
||||
out = {};
|
||||
out.SuperSpeed = speed >= Xhci::SPEED_SUPER;
|
||||
uint8_t type = out.SuperSpeed ? DESC_SS_HUB : DESC_HUB;
|
||||
|
||||
uint8_t buf[16] = {};
|
||||
uint32_t cc = Xhci::ControlTransfer(slotId, REQTYPE_HUB_IN, REQ_GET_DESCRIPTOR,
|
||||
(uint16_t)(type << 8), 0, sizeof(buf), buf, true);
|
||||
if (!Succeeded(cc) || buf[0] < 7 || buf[1] != type || buf[2] == 0) {
|
||||
KernelLogStream(ERROR, "USB/Hub") << "Slot " << base::dec << (uint64_t)slotId
|
||||
<< ": hub descriptor unreadable, cc=" << (uint64_t)cc;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t characteristics = (uint16_t)(buf[3] | (buf[4] << 8));
|
||||
out.Ports = buf[2];
|
||||
out.ThinkTime = out.SuperSpeed ? 0 : (uint8_t)((characteristics >> 5) & 3);
|
||||
out.PowerOnMs = (uint16_t)(buf[5] * 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Ports
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static uint8_t FindChild(uint8_t hubSlot, uint8_t port) {
|
||||
for (uint8_t s = 1; s <= Xhci::MAX_SLOTS; s++) {
|
||||
auto* dev = Xhci::GetDevice(s);
|
||||
if (dev->Active && dev->ParentSlot == hubSlot && dev->ParentPort == port) return s;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reset a connected port and report the speed the device came up at.
|
||||
static bool ResetPort(uint8_t hubSlot, uint8_t port, bool superSpeed, uint32_t& speed) {
|
||||
if (SetPortFeature(hubSlot, port, FEAT_PORT_RESET) != Xhci::CC_SUCCESS) return false;
|
||||
|
||||
uint16_t status = 0, change = 0;
|
||||
bool done = false;
|
||||
for (int attempt = 0; attempt < 50; attempt++) { // ~500 ms
|
||||
BusyWaitMs(10);
|
||||
if (!GetPortStatus(hubSlot, port, status, change)) return false;
|
||||
if ((change & PC_RESET) && !(status & PS_RESET)) { done = true; break; }
|
||||
if (!(status & PS_CONNECTION)) return false;
|
||||
}
|
||||
ClearPortChanges(hubSlot, port, change, superSpeed);
|
||||
if (!done || !(status & PS_ENABLE)) return false;
|
||||
|
||||
if (superSpeed) speed = Xhci::SPEED_SUPER;
|
||||
else if (status & PS_LOW_SPEED) speed = Xhci::SPEED_LOW;
|
||||
else if (status & PS_HIGH_SPEED) speed = Xhci::SPEED_HIGH;
|
||||
else speed = Xhci::SPEED_FULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bring the port's state in line with what is attached to it: remove a
|
||||
// device that left (or was replaced), enumerate one that arrived.
|
||||
static void HandlePort(uint8_t hubSlot, uint8_t port, bool settled) {
|
||||
const HubDescriptorInfo& info = g_hubs[hubSlot].Info;
|
||||
|
||||
uint16_t status = 0, change = 0;
|
||||
if (!GetPortStatus(hubSlot, port, status, change)) return;
|
||||
if (change != 0) ClearPortChanges(hubSlot, port, change, info.SuperSpeed);
|
||||
|
||||
bool connected = (status & PS_CONNECTION) != 0;
|
||||
uint8_t child = FindChild(hubSlot, port);
|
||||
if (child != 0 && (!connected || (change & PC_CONNECTION))) {
|
||||
KernelLogStream(INFO, "USB/Hub") << "Slot " << base::dec << (uint64_t)hubSlot
|
||||
<< " port " << (uint64_t)port << ": device disconnected";
|
||||
Xhci::RemoveDevice(child);
|
||||
child = 0;
|
||||
}
|
||||
if (!connected || child != 0) return;
|
||||
|
||||
if (!settled) {
|
||||
// Connect debounce (USB 2.0 7.1.7.3): let contacts settle first.
|
||||
BusyWaitMs(100);
|
||||
if (!GetPortStatus(hubSlot, port, status, change)) return;
|
||||
if (!(status & PS_CONNECTION)) return;
|
||||
}
|
||||
|
||||
auto* hub = Xhci::GetDevice(hubSlot);
|
||||
if (hub->Depth + 1 > MAX_TIERS || port > MAX_ROUTABLE_PORT) {
|
||||
KernelLogStream(WARNING, "USB/Hub") << "Slot " << base::dec << (uint64_t)hubSlot
|
||||
<< " port " << (uint64_t)port << ": too deep to address, ignored";
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t speed = 0;
|
||||
if (!ResetPort(hubSlot, port, info.SuperSpeed, speed)) {
|
||||
KernelLogStream(WARNING, "USB/Hub") << "Slot " << base::dec << (uint64_t)hubSlot
|
||||
<< " port " << (uint64_t)port << ": reset failed";
|
||||
return;
|
||||
}
|
||||
BusyWaitMs(10); // reset recovery (USB 2.0 7.1.7.5)
|
||||
|
||||
UsbDevice::DeviceLocation location = {};
|
||||
location.RootPort = hub->PortId;
|
||||
location.ParentSlot = hubSlot;
|
||||
location.ParentPort = port;
|
||||
location.Depth = (uint8_t)(hub->Depth + 1);
|
||||
location.RouteString = hub->RouteString | ((uint32_t)port << (4 * hub->Depth));
|
||||
if (hub->Speed == Xhci::SPEED_HIGH &&
|
||||
(speed == Xhci::SPEED_LOW || speed == Xhci::SPEED_FULL)) {
|
||||
// This hub's transaction translator turns the high-speed
|
||||
// traffic into low/full-speed transactions. Single-TT mode:
|
||||
// the configuration's default alternate setting.
|
||||
location.TtHubSlot = hubSlot;
|
||||
location.TtPort = port;
|
||||
location.TtMulti = false;
|
||||
} else {
|
||||
location.TtHubSlot = hub->TtHubSlot;
|
||||
location.TtPort = hub->TtPort;
|
||||
location.TtMulti = hub->TtMulti;
|
||||
}
|
||||
|
||||
UsbDevice::EnumerateDevice(location, speed);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
void RegisterHub(uint8_t slotId, const HubDescriptorInfo& info) {
|
||||
auto* dev = Xhci::GetDevice(slotId);
|
||||
if (dev == nullptr || !dev->Active) return;
|
||||
|
||||
g_hubs[slotId].Info = info;
|
||||
g_hubs[slotId].Active = true;
|
||||
g_changedPorts[slotId].store(0, std::memory_order_relaxed);
|
||||
g_rearm[slotId].store(false, std::memory_order_relaxed);
|
||||
g_endpointFailed[slotId].store(false, std::memory_order_relaxed);
|
||||
|
||||
uint8_t ports = info.Ports;
|
||||
if (ports > 31) ports = 31; // status-change bitmap tracked in 32 bits
|
||||
|
||||
KernelLogStream(OK, "USB/Hub") << "Slot " << base::dec << (uint64_t)slotId << ": "
|
||||
<< (info.SuperSpeed ? "SuperSpeed" : "USB 2.0") << " hub, "
|
||||
<< (uint64_t)info.Ports << " ports, tier " << (uint64_t)(dev->Depth + 1);
|
||||
|
||||
if (info.SuperSpeed) {
|
||||
Xhci::ControlTransfer(slotId, REQTYPE_HUB_OUT, REQ_SET_HUB_DEPTH,
|
||||
dev->Depth, 0, 0, nullptr, false);
|
||||
}
|
||||
|
||||
for (uint8_t port = 1; port <= ports; port++) {
|
||||
SetPortFeature(slotId, port, FEAT_PORT_POWER);
|
||||
}
|
||||
// Power-on-to-power-good, then the connect debounce interval.
|
||||
uint64_t wait = info.PowerOnMs < 100 ? 100 : info.PowerOnMs;
|
||||
BusyWaitMs(wait + 100);
|
||||
|
||||
for (uint8_t port = 1; port <= ports && g_hubs[slotId].Active; port++) {
|
||||
HandlePort(slotId, port, true);
|
||||
}
|
||||
|
||||
if (g_hubs[slotId].Active) Xhci::QueueInterruptTransfer(slotId);
|
||||
}
|
||||
|
||||
void UnregisterHub(uint8_t slotId) {
|
||||
if (slotId == 0 || slotId > Xhci::MAX_SLOTS) return;
|
||||
g_hubs[slotId] = {};
|
||||
g_changedPorts[slotId].store(0, std::memory_order_relaxed);
|
||||
g_rearm[slotId].store(false, std::memory_order_relaxed);
|
||||
g_endpointFailed[slotId].store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void OnStatusChange(uint8_t slotId, const uint8_t* data, uint16_t length,
|
||||
uint32_t completionCode) {
|
||||
if (slotId == 0 || slotId > Xhci::MAX_SLOTS) return;
|
||||
|
||||
if (Succeeded(completionCode)) {
|
||||
uint32_t bits = 0;
|
||||
for (uint16_t i = 0; i < length && i < 4; i++) {
|
||||
bits |= (uint32_t)data[i] << (8 * i);
|
||||
}
|
||||
g_changedPorts[slotId].fetch_or(bits, std::memory_order_relaxed);
|
||||
g_rearm[slotId].store(true, std::memory_order_relaxed);
|
||||
} else {
|
||||
// The endpoint halted; it needs a reset before it reports again,
|
||||
// and changes may have been missed meanwhile.
|
||||
g_endpointFailed[slotId].store(true, std::memory_order_relaxed);
|
||||
}
|
||||
g_pending.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void RequestPortCheck(uint8_t hubSlot, uint8_t port) {
|
||||
if (hubSlot == 0 || hubSlot > Xhci::MAX_SLOTS || port == 0 || port > 31) return;
|
||||
g_changedPorts[hubSlot].fetch_or(1u << port, std::memory_order_relaxed);
|
||||
g_pending.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool HasPendingWork() {
|
||||
return g_pending.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void ProcessPendingWork() {
|
||||
if (!g_pending.exchange(false, std::memory_order_acq_rel)) return;
|
||||
|
||||
for (uint8_t slot = 1; slot <= Xhci::MAX_SLOTS; slot++) {
|
||||
if (!g_hubs[slot].Active) continue;
|
||||
|
||||
bool failed = g_endpointFailed[slot].exchange(false, std::memory_order_acq_rel);
|
||||
bool rearm = g_rearm[slot].exchange(false, std::memory_order_acq_rel);
|
||||
uint32_t changed = g_changedPorts[slot].exchange(0, std::memory_order_acq_rel);
|
||||
if (!failed && !rearm && changed == 0) continue;
|
||||
|
||||
uint8_t ports = g_hubs[slot].Info.Ports;
|
||||
if (ports > 31) ports = 31;
|
||||
|
||||
if (failed) {
|
||||
// Reset Endpoint re-arms the endpoint itself.
|
||||
Xhci::ResetInterruptEndpoint(slot);
|
||||
changed = ((ports == 31) ? 0xFFFFFFFFu : ((1u << (ports + 1)) - 1)) & ~1u;
|
||||
}
|
||||
|
||||
if (changed & 1u) ClearHubChanges(slot);
|
||||
for (uint8_t port = 1; port <= ports && g_hubs[slot].Active; port++) {
|
||||
if (changed & (1u << port)) HandlePort(slot, port, false);
|
||||
}
|
||||
|
||||
if (rearm && !failed && g_hubs[slot].Active) Xhci::QueueInterruptTransfer(slot);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Hub.hpp
|
||||
* USB hub class driver (USB 2.0 and SuperSpeed hubs)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// Devices behind a hub get their own xHCI slots, addressed by route string
|
||||
// (Xhci::UsbDeviceInfo). The hub driver powers and resets the hub's ports
|
||||
// and enumerates what it finds; the hub's interrupt endpoint reports port
|
||||
// changes afterwards, handled from the xHCI deferred-work pass.
|
||||
//
|
||||
// The Raspberry Pi 4/400 needs this for any USB 2 device: the VL805's only
|
||||
// USB 2 root port carries an on-board VIA hub that every USB 2 line (the
|
||||
// Pi 400's built-in keyboard included) hangs off.
|
||||
|
||||
namespace Drivers::USB::Hub {
|
||||
|
||||
struct HubDescriptorInfo {
|
||||
uint8_t Ports;
|
||||
uint8_t ThinkTime; // TT think time code, wHubCharacteristics[6:5]
|
||||
uint16_t PowerOnMs; // bPwrOn2PwrGood, in ms
|
||||
bool SuperSpeed;
|
||||
};
|
||||
|
||||
// Read the hub class descriptor of a configured hub. `speed` is the
|
||||
// hub's xHCI port speed (SuperSpeed hubs have their own descriptor).
|
||||
bool ReadDescriptor(uint8_t slotId, uint32_t speed, HubDescriptorInfo& out);
|
||||
|
||||
// Take over a hub whose slot is configured as a hub with its interrupt
|
||||
// endpoint: power its ports, enumerate the devices on them, then arm the
|
||||
// status-change endpoint. Process context.
|
||||
void RegisterHub(uint8_t slotId, const HubDescriptorInfo& info);
|
||||
|
||||
// Forget a hub (its children are removed by Xhci::RemoveDevice).
|
||||
void UnregisterHub(uint8_t slotId);
|
||||
|
||||
// A completion on the hub's status-change endpoint. Called from the
|
||||
// event ring drain: records the change and nothing else.
|
||||
void OnStatusChange(uint8_t slotId, const uint8_t* data, uint16_t length,
|
||||
uint32_t completionCode);
|
||||
|
||||
// Look at a port again on the next ProcessPendingWork as if it had
|
||||
// reported a change: a device removed from it is reset and enumerated
|
||||
// afresh, as if replugged.
|
||||
void RequestPortCheck(uint8_t hubSlot, uint8_t port);
|
||||
|
||||
// Port changes are waiting for ProcessPendingWork.
|
||||
bool HasPendingWork();
|
||||
|
||||
// Handle recorded port changes and re-arm the endpoints. Runs inside the
|
||||
// xHCI deferred-work pass (process/idle context, single runner).
|
||||
void ProcessPendingWork();
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "HidKeyboard.hpp"
|
||||
#include "HidMouse.hpp"
|
||||
#include "MassStorage.hpp"
|
||||
#include "Hub.hpp"
|
||||
#include "Bluetooth/Bluetooth.hpp"
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
@@ -65,24 +66,28 @@ namespace Drivers::USB::UsbDevice {
|
||||
return speed; // Same encoding
|
||||
}
|
||||
|
||||
// Convert USB endpoint bInterval to xHCI Endpoint Context Interval value.
|
||||
// HS/SS: bInterval is already in 2^(n-1) * 125µs encoding — use directly.
|
||||
// FS/LS: bInterval is in milliseconds (frames) — convert via fls(bInterval * 8).
|
||||
// Convert USB endpoint bInterval to xHCI Endpoint Context Interval value
|
||||
// (a period of 2^Interval * 125 us), as Linux's xhci_get_endpoint_interval.
|
||||
// HS/SS: bInterval encodes a period of 2^(bInterval-1) microframes.
|
||||
// FS/LS: bInterval is in milliseconds (frames); round down to a power of
|
||||
// two microframes, between 1 ms and 128 ms.
|
||||
static uint32_t ConvertInterval(uint32_t speed, uint8_t bInterval) {
|
||||
if (bInterval == 0) return 0;
|
||||
|
||||
if (speed == Xhci::SPEED_HIGH || speed == Xhci::SPEED_SUPER) {
|
||||
return bInterval;
|
||||
uint32_t b = bInterval;
|
||||
if (b < 1) b = 1;
|
||||
if (b > 16) b = 16;
|
||||
return b - 1;
|
||||
}
|
||||
|
||||
// FS/LS: bInterval ms → microframes, then find highest set bit position
|
||||
if (bInterval == 0) return 3;
|
||||
uint32_t microframes = (uint32_t)bInterval * 8;
|
||||
uint32_t interval = 0;
|
||||
while (microframes > 0) {
|
||||
while (microframes > 1) {
|
||||
interval++;
|
||||
microframes >>= 1;
|
||||
}
|
||||
if (interval > 15) interval = 15;
|
||||
if (interval < 3) interval = 3;
|
||||
if (interval > 10) interval = 10;
|
||||
return interval;
|
||||
}
|
||||
|
||||
@@ -101,9 +106,25 @@ namespace Drivers::USB::UsbDevice {
|
||||
// EnumerateDevice
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static bool EnumerateInSlot(uint8_t slotId, const DeviceLocation& location, uint32_t speed,
|
||||
bool& isHub, Hub::HubDescriptorInfo& hubInfo);
|
||||
|
||||
uint8_t EnumerateDevice(uint8_t portId, uint32_t speed) {
|
||||
DeviceLocation location = {};
|
||||
location.RootPort = portId;
|
||||
return EnumerateDevice(location, speed);
|
||||
}
|
||||
|
||||
uint8_t EnumerateDevice(const DeviceLocation& location, uint32_t speed) {
|
||||
if (location.ParentSlot == 0) {
|
||||
KernelLogStream(INFO, "USB") << "Enumerating device on port "
|
||||
<< (uint64_t)portId << " speed=" << SpeedToString(speed);
|
||||
<< (uint64_t)location.RootPort << " speed=" << SpeedToString(speed);
|
||||
} else {
|
||||
KernelLogStream(INFO, "USB") << "Enumerating device on hub slot "
|
||||
<< (uint64_t)location.ParentSlot << " port " << (uint64_t)location.ParentPort
|
||||
<< " (route " << base::hex << (uint64_t)location.RouteString << base::dec
|
||||
<< ", root port " << (uint64_t)location.RootPort << ") speed=" << SpeedToString(speed);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Step 1: Enable Slot
|
||||
@@ -125,6 +146,31 @@ namespace Drivers::USB::UsbDevice {
|
||||
|
||||
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId << " enabled";
|
||||
|
||||
bool isHub = false;
|
||||
Hub::HubDescriptorInfo hubInfo = {};
|
||||
if (!EnumerateInSlot(slotId, location, speed, isHub, hubInfo)) {
|
||||
// Give the slot back, or every failed attempt (a flaky device
|
||||
// re-reported by its hub) would leak one until none are left.
|
||||
Xhci::TRB disableTrb = {};
|
||||
disableTrb.Control = (Xhci::TRB_DISABLE_SLOT << Xhci::TRB_TYPE_SHIFT)
|
||||
| ((uint32_t)slotId << 24);
|
||||
Xhci::SendCommand(disableTrb);
|
||||
Xhci::g_dcbaa[slotId] = 0;
|
||||
*Xhci::GetDevice(slotId) = {};
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A hub enumerates what is behind it now that its own slot is
|
||||
// published (its children name it as their parent).
|
||||
if (isHub) Hub::RegisterHub(slotId, hubInfo);
|
||||
return slotId;
|
||||
}
|
||||
|
||||
static bool EnumerateInSlot(uint8_t slotId, const DeviceLocation& location, uint32_t speed,
|
||||
bool& isHub, Hub::HubDescriptorInfo& hubInfo) {
|
||||
uint8_t portId = location.RootPort;
|
||||
uint32_t cc = 0;
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Step 2: Allocate device output context and set DCBAA entry
|
||||
// -----------------------------------------------------------------
|
||||
@@ -133,9 +179,16 @@ namespace Drivers::USB::UsbDevice {
|
||||
dev->Active = true;
|
||||
dev->PortId = portId;
|
||||
dev->Speed = speed;
|
||||
dev->ParentSlot = location.ParentSlot;
|
||||
dev->ParentPort = location.ParentPort;
|
||||
dev->Depth = location.Depth;
|
||||
dev->RouteString = location.RouteString;
|
||||
dev->TtHubSlot = location.TtHubSlot;
|
||||
dev->TtPort = location.TtPort;
|
||||
dev->TtMulti = location.TtMulti;
|
||||
|
||||
// Allocate a zeroed page for the output DeviceContext
|
||||
auto* outputCtx = (Xhci::DeviceContext*)Memory::g_pfa->AllocateZeroed();
|
||||
auto* outputCtx = (Xhci::DeviceContext*)Xhci::AllocateDmaPage();
|
||||
dev->OutputContext = outputCtx;
|
||||
dev->OutputContextPhys = Memory::SubHHDM(outputCtx);
|
||||
|
||||
@@ -145,7 +198,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
// -----------------------------------------------------------------
|
||||
// Step 3: Build Input Context for Address Device command
|
||||
// -----------------------------------------------------------------
|
||||
auto* inputCtx = (Xhci::InputContext*)Memory::g_pfa->AllocateZeroed();
|
||||
auto* inputCtx = (Xhci::InputContext*)Xhci::AllocateDmaPage();
|
||||
|
||||
// Input Control Context: add Slot Context (bit 0) and EP0 (bit 1)
|
||||
inputCtx->ICC.AddFlags = 0x3;
|
||||
@@ -153,12 +206,18 @@ namespace Drivers::USB::UsbDevice {
|
||||
// Slot Context
|
||||
uint32_t speedVal = SpeedToSlotContextValue(speed);
|
||||
uint32_t ctxEntries = 1; // Context Entries = 1 (Slot + EP0 only)
|
||||
inputCtx->Slot.Field0 = (ctxEntries << 27) | (speedVal << 20);
|
||||
inputCtx->Slot.Field0 = (ctxEntries << 27) | (speedVal << 20)
|
||||
| (location.RouteString & 0xFFFFF)
|
||||
| (location.TtMulti ? Xhci::SLOT_MTT : 0);
|
||||
inputCtx->Slot.Field1 = ((uint32_t)portId << 16); // Root Hub Port Number
|
||||
// Low/full-speed behind a high-speed hub: the hub slot and port
|
||||
// whose transaction translator the controller addresses.
|
||||
inputCtx->Slot.Field2 = (uint32_t)location.TtHubSlot
|
||||
| ((uint32_t)location.TtPort << 8);
|
||||
|
||||
// EP0 Context
|
||||
// Allocate EP0 transfer ring
|
||||
auto* ep0Ring = (Xhci::TRB*)Memory::g_pfa->AllocateZeroed();
|
||||
auto* ep0Ring = (Xhci::TRB*)Xhci::AllocateDmaPage();
|
||||
dev->EP0Ring = ep0Ring;
|
||||
dev->EP0RingPhys = Memory::SubHHDM(ep0Ring);
|
||||
dev->EP0RingEnqueue = 0;
|
||||
@@ -198,8 +257,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
if (cc != Xhci::CC_SUCCESS) {
|
||||
KernelLogStream(ERROR, "USB") << "Address Device (BSR=1) failed, slot="
|
||||
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
|
||||
dev->Active = false;
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId << " initialized (BSR=1)";
|
||||
@@ -213,8 +271,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
partialDesc, true);
|
||||
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
||||
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(8-byte) failed, cc=" << (uint64_t)cc;
|
||||
dev->Active = false;
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t bMaxPacketSize0 = partialDesc[7];
|
||||
@@ -227,7 +284,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
// Step 4c: Evaluate Context — update EP0 max packet size if needed
|
||||
// -----------------------------------------------------------------
|
||||
if (bMaxPacketSize0 != maxPacket) {
|
||||
auto* evalCtx = (Xhci::InputContext*)Memory::g_pfa->AllocateZeroed();
|
||||
auto* evalCtx = (Xhci::InputContext*)Xhci::AllocateDmaPage();
|
||||
|
||||
// Only updating EP0 — set AddFlags bit 1 (EP0), no slot context needed
|
||||
evalCtx->ICC.AddFlags = (1 << 1);
|
||||
@@ -280,8 +337,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
if (cc != Xhci::CC_SUCCESS) {
|
||||
KernelLogStream(ERROR, "USB") << "Address Device failed, slot="
|
||||
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
|
||||
dev->Active = false;
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set-address recovery time (USB spec requires >= 2ms, use 10ms for safety)
|
||||
@@ -298,8 +354,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
&devDesc, true);
|
||||
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
||||
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(Device) failed, cc=" << (uint64_t)cc;
|
||||
dev->Active = false;
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
dev->VendorId = devDesc.idVendor;
|
||||
@@ -319,8 +374,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
&cfgHdr, true);
|
||||
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
||||
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(Config header) failed, cc=" << (uint64_t)cc;
|
||||
dev->Active = false;
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t cfgBuf[256] = {};
|
||||
@@ -332,8 +386,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
cfgBuf, true);
|
||||
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
||||
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(Config full) failed, cc=" << (uint64_t)cc;
|
||||
dev->Active = false;
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
@@ -341,6 +394,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
// -----------------------------------------------------------------
|
||||
uint16_t offset = 0;
|
||||
bool foundHid = false;
|
||||
bool currentHub = false;
|
||||
bool foundBt = false;
|
||||
bool foundMsc = false;
|
||||
bool currentMsc = false;
|
||||
@@ -358,9 +412,23 @@ namespace Drivers::USB::UsbDevice {
|
||||
auto* iface = (InterfaceDescriptor*)&cfgBuf[offset];
|
||||
// Reset at each new interface boundary
|
||||
foundHid = false;
|
||||
currentHub = false;
|
||||
foundBt = false;
|
||||
currentMsc = false;
|
||||
|
||||
// Hub (class 9): its status-change interrupt endpoint. Only
|
||||
// alternate setting 0 (single-TT) is used.
|
||||
if (!foundEp && !isHub &&
|
||||
iface->bInterfaceClass == CLASS_HUB &&
|
||||
iface->bAlternateSetting == 0) {
|
||||
dev->InterfaceClass = iface->bInterfaceClass;
|
||||
dev->InterfaceSubClass = iface->bInterfaceSubClass;
|
||||
dev->InterfaceProtocol = iface->bInterfaceProtocol;
|
||||
dev->InterfaceNumber = iface->bInterfaceNumber;
|
||||
currentHub = true;
|
||||
isHub = true;
|
||||
}
|
||||
|
||||
if (!foundEp &&
|
||||
iface->bInterfaceClass == CLASS_HID &&
|
||||
iface->bInterfaceSubClass == SUBCLASS_BOOT) {
|
||||
@@ -408,8 +476,8 @@ namespace Drivers::USB::UsbDevice {
|
||||
uint8_t xferType = ep->bmAttributes & EP_XFER_TYPE_MASK;
|
||||
bool isIn = (ep->bEndpointAddress & EP_DIR_IN) != 0;
|
||||
|
||||
// HID interrupt IN endpoint
|
||||
if (foundHid && !foundEp && isIn && xferType == EP_XFER_INTERRUPT) {
|
||||
// HID or hub interrupt IN endpoint
|
||||
if ((foundHid || currentHub) && !foundEp && isIn && xferType == EP_XFER_INTERRUPT) {
|
||||
dev->InterruptEpNum = ep->bEndpointAddress & 0x0F;
|
||||
dev->InterruptMaxPacket = ep->wMaxPacketSize & 0x7FF;
|
||||
dev->InterruptInterval = ep->bInterval;
|
||||
@@ -455,7 +523,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
// The xHCI slot model currently stores one interface; a future model
|
||||
// can retain every alternate/interface without changing the userspace
|
||||
// claim ABI, which already names the interface number explicitly.
|
||||
bool knownInterface = foundBt || foundMsc ||
|
||||
bool knownInterface = foundBt || foundMsc || isHub ||
|
||||
dev->InterfaceClass == CLASS_HID;
|
||||
if (!knownInterface) {
|
||||
bool inFirstInterface = false;
|
||||
@@ -502,15 +570,23 @@ namespace Drivers::USB::UsbDevice {
|
||||
cfgHdr.bConfigurationValue, 0, 0, nullptr, false);
|
||||
if (cc != Xhci::CC_SUCCESS) {
|
||||
KernelLogStream(ERROR, "USB") << "SET_CONFIGURATION failed, cc=" << (uint64_t)cc;
|
||||
dev->Active = false;
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Step 8b: Hub descriptor (the slot context must describe the hub)
|
||||
// -----------------------------------------------------------------
|
||||
if (isHub && (!foundEp || !Hub::ReadDescriptor(slotId, speed, hubInfo))) {
|
||||
KernelLogStream(WARNING, "USB") << "Slot " << (uint64_t)slotId
|
||||
<< ": hub left unconfigured";
|
||||
isHub = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Step 9: Configure Endpoints
|
||||
// -----------------------------------------------------------------
|
||||
if (foundEp || foundBulkIn || foundBulkOut) {
|
||||
auto* inputCtx2 = (Xhci::InputContext*)Memory::g_pfa->AllocateZeroed();
|
||||
auto* inputCtx2 = (Xhci::InputContext*)Xhci::AllocateDmaPage();
|
||||
|
||||
// Start with slot context
|
||||
inputCtx2->ICC.AddFlags = (1 << 0);
|
||||
@@ -528,7 +604,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
if (dci > maxDci) maxDci = dci;
|
||||
|
||||
// Allocate interrupt transfer ring
|
||||
auto* intRing = (Xhci::TRB*)Memory::g_pfa->AllocateZeroed();
|
||||
auto* intRing = (Xhci::TRB*)Xhci::AllocateDmaPage();
|
||||
dev->InterruptRing = intRing;
|
||||
dev->InterruptRingPhys = Memory::SubHHDM(intRing);
|
||||
dev->InterruptRingEnqueue = 0;
|
||||
@@ -542,6 +618,10 @@ namespace Drivers::USB::UsbDevice {
|
||||
|
||||
auto& epCtx = inputCtx2->EP[dci - 1];
|
||||
uint32_t xhciInterval = ConvertInterval(speed, dev->InterruptInterval);
|
||||
// Split transactions through a hub's TT: poll at least every
|
||||
// 8 ms. Cheap, and conservative on the VL805, which Linux
|
||||
// gives a quirk for periodic endpoints behind a TT.
|
||||
if (dev->TtHubSlot != 0 && xhciInterval > 6) xhciInterval = 6;
|
||||
epCtx.Field0 = (xhciInterval << 16);
|
||||
epCtx.Field1 = (3 << 1)
|
||||
| (Xhci::EP_TYPE_INTERRUPT_IN << 3)
|
||||
@@ -556,7 +636,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
inputCtx2->ICC.AddFlags |= (1 << dci);
|
||||
if (dci > maxDci) maxDci = dci;
|
||||
|
||||
auto* bulkInRing = (Xhci::TRB*)Memory::g_pfa->AllocateZeroed();
|
||||
auto* bulkInRing = (Xhci::TRB*)Xhci::AllocateDmaPage();
|
||||
dev->BulkInRing = bulkInRing;
|
||||
dev->BulkInRingPhys = Memory::SubHHDM(bulkInRing);
|
||||
dev->BulkInRingEnqueue = 0;
|
||||
@@ -583,7 +663,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
inputCtx2->ICC.AddFlags |= (1 << dci);
|
||||
if (dci > maxDci) maxDci = dci;
|
||||
|
||||
auto* bulkOutRing = (Xhci::TRB*)Memory::g_pfa->AllocateZeroed();
|
||||
auto* bulkOutRing = (Xhci::TRB*)Xhci::AllocateDmaPage();
|
||||
dev->BulkOutRing = bulkOutRing;
|
||||
dev->BulkOutRingPhys = Memory::SubHHDM(bulkOutRing);
|
||||
dev->BulkOutRingEnqueue = 0;
|
||||
@@ -608,6 +688,19 @@ namespace Drivers::USB::UsbDevice {
|
||||
inputCtx2->Slot.Field0 = (inputCtx2->Slot.Field0 & ~(0x1Fu << 27))
|
||||
| (maxDci << 27);
|
||||
|
||||
// A hub: Hub flag, Number of Ports and, for a high-speed hub, its
|
||||
// TT Think Time (xHCI 6.2.2). The controller needs them to
|
||||
// schedule split transactions for what sits behind it.
|
||||
if (isHub) {
|
||||
inputCtx2->Slot.Field0 |= Xhci::SLOT_HUB;
|
||||
inputCtx2->Slot.Field1 = (inputCtx2->Slot.Field1 & 0x00FFFFFF)
|
||||
| ((uint32_t)hubInfo.Ports << 24);
|
||||
if (speed == Xhci::SPEED_HIGH) {
|
||||
inputCtx2->Slot.Field2 = (inputCtx2->Slot.Field2 & ~(3u << 16))
|
||||
| ((uint32_t)hubInfo.ThinkTime << 16);
|
||||
}
|
||||
}
|
||||
|
||||
// Send Configure Endpoint command
|
||||
Xhci::TRB cfgTrb = {};
|
||||
uint64_t inputCtx2Phys = Memory::SubHHDM(inputCtx2);
|
||||
@@ -620,8 +713,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
if (cc != Xhci::CC_SUCCESS) {
|
||||
KernelLogStream(ERROR, "USB") << "Configure Endpoint failed, slot="
|
||||
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
|
||||
dev->Active = false;
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (foundEp) {
|
||||
@@ -713,6 +805,8 @@ namespace Drivers::USB::UsbDevice {
|
||||
KernelLogStream(OK, "USB") << "Slot " << (uint64_t)slotId << ": Bluetooth Adapter"
|
||||
<< " VID:" << base::hex << (uint64_t)dev->VendorId
|
||||
<< " PID:" << (uint64_t)dev->ProductId << base::dec;
|
||||
} else if (isHub) {
|
||||
dev->KernelDriverBound = true;
|
||||
} else if (dev->InterfaceClass == CLASS_MASS_STORAGE &&
|
||||
dev->InterfaceSubClass == SUBCLASS_SCSI &&
|
||||
dev->InterfaceProtocol == PROTOCOL_BULK_ONLY &&
|
||||
@@ -733,7 +827,7 @@ namespace Drivers::USB::UsbDevice {
|
||||
// Publish to userspace only after endpoint configuration and kernel
|
||||
// class-driver binding decisions are complete.
|
||||
dev->Ready = true;
|
||||
return slotId;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -80,6 +80,9 @@ namespace Drivers::USB::UsbDevice {
|
||||
constexpr uint8_t PROTOCOL_KEYBOARD = 0x01;
|
||||
constexpr uint8_t PROTOCOL_MOUSE = 0x02;
|
||||
|
||||
// Hub class
|
||||
constexpr uint8_t CLASS_HUB = 0x09;
|
||||
|
||||
// Wireless Controller class (Bluetooth)
|
||||
constexpr uint8_t CLASS_WIRELESS = 0xE0;
|
||||
constexpr uint8_t SUBCLASS_RF = 0x01;
|
||||
@@ -119,8 +122,24 @@ namespace Drivers::USB::UsbDevice {
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Enumerate a newly connected device on the given port with the given speed.
|
||||
// Where a device is attached: a root hub port, or a port of a hub
|
||||
// (Xhci::UsbDeviceInfo describes the fields).
|
||||
struct DeviceLocation {
|
||||
uint8_t RootPort;
|
||||
uint8_t ParentSlot;
|
||||
uint8_t ParentPort;
|
||||
uint8_t Depth;
|
||||
uint32_t RouteString;
|
||||
uint8_t TtHubSlot;
|
||||
uint8_t TtPort;
|
||||
bool TtMulti;
|
||||
};
|
||||
|
||||
// Enumerate a newly connected device on the given root port with the given speed.
|
||||
// Returns the assigned slot ID, or 0 on failure.
|
||||
uint8_t EnumerateDevice(uint8_t portId, uint32_t speed);
|
||||
|
||||
// Enumerate a device at `location` (a hub port that has been reset).
|
||||
uint8_t EnumerateDevice(const DeviceLocation& location, uint32_t speed);
|
||||
|
||||
};
|
||||
+407
-62
@@ -11,11 +11,14 @@
|
||||
#include "HidKeyboard.hpp"
|
||||
#include "HidMouse.hpp"
|
||||
#include "MassStorage.hpp"
|
||||
#include "Hub.hpp"
|
||||
#include "UserUsb.hpp"
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Dma.hpp>
|
||||
#include <ACPI/GsiRouting.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
@@ -59,6 +62,21 @@ namespace Drivers::USB::Xhci {
|
||||
static std::atomic<bool> g_hotplugProcessing{false};
|
||||
static std::atomic<bool> g_irqDrainPending{false};
|
||||
|
||||
// HID interrupt endpoints that completed with an error. The event drain
|
||||
// does not re-arm them (a halted endpoint takes commands to clear, which
|
||||
// cannot be waited on from there): it records the error, and the
|
||||
// deferred-work pass recovers the endpoint once g_hidRecoverDueMs is
|
||||
// reached. Low/full-speed devices behind a hub's TT (every mouse on a
|
||||
// Raspberry Pi 4) see transaction and split errors often enough that
|
||||
// without this the device goes dead until replugged.
|
||||
// g_hidErrorStreak counts errors since the last good report; it sets the
|
||||
// backoff, and at HID_REENUMERATE_STREAK the device is reset and
|
||||
// enumerated again, as a replug would.
|
||||
static constexpr uint32_t HID_REENUMERATE_STREAK = 6;
|
||||
static std::atomic<uint32_t> g_hidErrorCc[MAX_SLOTS + 1] = {}; // 0 = no recovery due
|
||||
static std::atomic<uint32_t> g_hidErrorStreak[MAX_SLOTS + 1] = {};
|
||||
static std::atomic<uint64_t> g_hidRecoverDueMs[MAX_SLOTS + 1] = {};
|
||||
|
||||
// PollEvents also runs directly from the MSI handler. KernelLogStream uses
|
||||
// a process-context mutex, so printing there can deadlock if the interrupt
|
||||
// preempts a CPU that already owns the terminal lock. Capture the small
|
||||
@@ -220,6 +238,13 @@ namespace Drivers::USB::Xhci {
|
||||
*(volatile uint32_t*)(g_opBase + reg) = value;
|
||||
}
|
||||
|
||||
// PollEvents() acknowledges the Port Status Change event, clearing PRC,
|
||||
// so a controller that resets instantly (QEMU) can complete the reset
|
||||
// between two reads of PRC. A port that left reset enabled is done too.
|
||||
static bool PortResetComplete(uint32_t portsc) {
|
||||
return (portsc & PORTSC_PRC) || ((portsc & PORTSC_PED) && !(portsc & PORTSC_PR));
|
||||
}
|
||||
|
||||
static uint32_t ReadOp(uint32_t reg) {
|
||||
return *(volatile uint32_t*)(g_opBase + reg);
|
||||
}
|
||||
@@ -236,18 +261,44 @@ namespace Drivers::USB::Xhci {
|
||||
return *(volatile uint32_t*)(g_mmioBase + reg);
|
||||
}
|
||||
|
||||
// TRBs (and the buffers they point at) must be visible before the
|
||||
// controller is told to look.
|
||||
static void WriteDoorbell(uint32_t index, uint32_t value) {
|
||||
Arch::DmaWriteFence();
|
||||
*(volatile uint32_t*)(g_dbBase + index * 4) = value;
|
||||
}
|
||||
|
||||
// Hand a TRB to the controller: everything else in it first, then the
|
||||
// control word that carries its cycle bit. A running ring may fetch the
|
||||
// TRB the moment the cycle bit matches.
|
||||
static inline void PublishTrb(TRB& trb, uint32_t control) {
|
||||
Arch::DmaWriteFence();
|
||||
*(volatile uint32_t*)&trb.Control = control;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DMA buffer allocation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Whether the controller's DMA snoops the CPU caches. PCI controllers
|
||||
// are taken to be coherent; a platform controller says so in _CCA.
|
||||
static bool g_dmaCoherent = true;
|
||||
|
||||
uint8_t* AllocateDmaPage(uint64_t& outPhysAddr) {
|
||||
return (uint8_t*)Memory::Dma::AllocatePage(outPhysAddr, g_dmaCoherent);
|
||||
}
|
||||
|
||||
void* AllocateDmaPage() {
|
||||
uint64_t phys;
|
||||
return AllocateDmaPage(phys);
|
||||
}
|
||||
|
||||
void FreeDmaPage(void* page) {
|
||||
Memory::Dma::FreePage(page, g_dmaCoherent);
|
||||
}
|
||||
|
||||
static uint8_t* AllocateDmaBuffer(uint64_t& outPhysAddr) {
|
||||
void* virt = Memory::g_pfa->AllocateZeroed();
|
||||
outPhysAddr = Memory::SubHHDM(virt);
|
||||
return (uint8_t*)virt;
|
||||
return AllocateDmaPage(outPhysAddr);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -412,6 +463,24 @@ namespace Drivers::USB::Xhci {
|
||||
// MSI setup (same pattern as E1000E)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Legacy INTx, for controllers without MSI (or platforms that cannot
|
||||
// receive it): the line the firmware or Pci::AssignInterruptLines put
|
||||
// in the Interrupt Line register. HandleInterrupt suits a level line:
|
||||
// clearing IMAN.IP drops it.
|
||||
static bool SetupIntx(uint8_t bus, uint8_t dev, uint8_t func) {
|
||||
uint8_t line = Pci::LegacyRead8(bus, dev, func, (uint8_t)Pci::PCI_REG_INTERRUPT);
|
||||
if (line == 0 || line >= 24) return false;
|
||||
|
||||
uint16_t pciCmd = Pci::LegacyRead16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND);
|
||||
Pci::LegacyWrite16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND,
|
||||
pciCmd & ~Pci::PCI_CMD_INTX_DISABLE);
|
||||
Arch::RegisterIrqHandler(line, HandleInterrupt);
|
||||
Arch::EnableLegacyIrq(line);
|
||||
|
||||
KernelLogStream(OK, "xHCI") << "Using INTx: platform line " << base::dec << (uint64_t)line;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SetupMsi(uint8_t bus, uint8_t dev, uint8_t func) {
|
||||
uint8_t cap = Pci::FindCapability(bus, dev, func, Pci::PCI_CAP_MSI);
|
||||
if (cap == 0) {
|
||||
@@ -474,6 +543,16 @@ namespace Drivers::USB::Xhci {
|
||||
BudgetExhausted,
|
||||
};
|
||||
|
||||
// Without MSI nothing interrupts: the deferred-work check (timer and
|
||||
// idle loop) looks at the event ring itself instead. This is the only
|
||||
// mode on platforms whose PCIe has no MSI controller (Raspberry Pi 4).
|
||||
static bool g_pollMode = false;
|
||||
|
||||
static bool EventRingHasWork() {
|
||||
const volatile TRB& evt = g_evtRing[g_evtRingDequeue];
|
||||
return ((evt.Control & TRB_CYCLE_BIT) != 0) == g_evtRingCCS;
|
||||
}
|
||||
|
||||
static PollResult PollEventsBounded(uint32_t maxEvents) {
|
||||
// PollEvents runs from synchronous poll loops (ControlTransfer,
|
||||
// SendCommand, firmware download) and idle bottom halves on different
|
||||
@@ -506,6 +585,8 @@ namespace Drivers::USB::Xhci {
|
||||
if (evtCycle != g_evtRingCCS) {
|
||||
break; // No more events
|
||||
}
|
||||
// The rest of the event was written before its cycle bit.
|
||||
Arch::DmaReadFence();
|
||||
|
||||
uint32_t trbType = (evt.Control & TRB_TYPE_MASK) >> TRB_TYPE_SHIFT;
|
||||
lastType = trbType;
|
||||
@@ -528,8 +609,12 @@ namespace Drivers::USB::Xhci {
|
||||
WriteOp(OP_PORTSC_BASE + (portId - 1) * OP_PORTSC_STRIDE,
|
||||
(portsc & PORTSC_PRESERVE) | PORTSC_CHANGE_BITS);
|
||||
|
||||
// Defer enumeration to ProcessDeferredWork outside interrupt context.
|
||||
if (g_bootScanComplete && portId >= 1 && portId <= g_maxPorts) {
|
||||
// Defer enumeration to ProcessDeferredWork outside interrupt
|
||||
// context. Changes during the boot scan are kept too (that
|
||||
// pass skips ports already enumerated): a device that
|
||||
// connects after the scan looked at its port, slower on
|
||||
// real hardware than in QEMU, would otherwise be lost.
|
||||
if (portId >= 1 && portId <= g_maxPorts) {
|
||||
g_hotplugPending[portId - 1].store(true, std::memory_order_release);
|
||||
g_deferredWorkPending.store(true, std::memory_order_release);
|
||||
}
|
||||
@@ -625,12 +710,17 @@ namespace Drivers::USB::Xhci {
|
||||
uint16_t len = (residual < reqLen)
|
||||
? (uint16_t)(reqLen - residual) : 0;
|
||||
|
||||
if (dev.InterfaceClass == UsbDevice::CLASS_HID) {
|
||||
if (dev.InterfaceClass == UsbDevice::CLASS_HUB) {
|
||||
// Re-armed once the change is handled.
|
||||
Hub::OnStatusChange((uint8_t)slotId, g_interruptDataBuf[slotId],
|
||||
len, completionCode);
|
||||
} else if (dev.InterfaceClass == UsbDevice::CLASS_HID) {
|
||||
if (dev.InterfaceProtocol == UsbDevice::PROTOCOL_KEYBOARD) {
|
||||
HidKeyboard::ProcessReport(g_interruptDataBuf[slotId], len);
|
||||
} else if (dev.InterfaceProtocol == UsbDevice::PROTOCOL_MOUSE) {
|
||||
HidMouse::ProcessReport(g_interruptDataBuf[slotId], len);
|
||||
}
|
||||
g_hidErrorStreak[slotId].store(0, std::memory_order_relaxed);
|
||||
// Re-queue for next HID report
|
||||
QueueInterruptTransfer(slotId);
|
||||
} else if (g_transferCallbacks[slotId]) {
|
||||
@@ -655,6 +745,23 @@ namespace Drivers::USB::Xhci {
|
||||
g_diagnosticWorkPending.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
if (dev.InterfaceClass == UsbDevice::CLASS_HUB && dev.InterruptEpNum &&
|
||||
epDci == (uint32_t)(dev.InterruptEpNum * 2 + 1)) {
|
||||
Hub::OnStatusChange((uint8_t)slotId, nullptr, 0, completionCode);
|
||||
}
|
||||
|
||||
if (dev.InterfaceClass == UsbDevice::CLASS_HID && dev.InterruptEpNum &&
|
||||
epDci == (uint32_t)(dev.InterruptEpNum * 2 + 1)) {
|
||||
// Retry at once, then back off 10 ms doubling.
|
||||
uint32_t streak = g_hidErrorStreak[slotId].fetch_add(
|
||||
1, std::memory_order_relaxed) + 1;
|
||||
uint64_t delay = streak <= 1 ? 0 : 10ull << (streak - 2);
|
||||
if (delay > 1000) delay = 1000;
|
||||
g_hidRecoverDueMs[slotId].store(
|
||||
Timekeeping::GetMilliseconds() + delay, std::memory_order_relaxed);
|
||||
g_hidErrorCc[slotId].store(completionCode, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Notify callback of errors too
|
||||
if (g_transferCallbacks[slotId]) {
|
||||
g_transferCallbacks[slotId](slotId, epDci,
|
||||
@@ -770,7 +877,7 @@ namespace Drivers::USB::Xhci {
|
||||
if (g_cmdRingCCS) {
|
||||
control |= TRB_CYCLE_BIT;
|
||||
}
|
||||
slot.Control = control;
|
||||
PublishTrb(slot, control);
|
||||
|
||||
// Advance enqueue pointer
|
||||
g_cmdRingEnqueue++;
|
||||
@@ -819,10 +926,21 @@ namespace Drivers::USB::Xhci {
|
||||
// ControlTransfer - perform a control transfer on EP0
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static uint32_t ControlTransferLocked(uint8_t slotId, uint8_t bmRequestType, uint8_t bRequest,
|
||||
uint16_t wValue, uint16_t wIndex, uint16_t wLength,
|
||||
void* data, uint64_t dataPhys, bool dirIn, bool trueNested);
|
||||
|
||||
// Data stages of a non-coherent controller go through this page (taken
|
||||
// under g_controlXferLock). Callers pass small stack buffers, and cache
|
||||
// maintenance cannot make those safe: a cache line the buffer shares with
|
||||
// live stack data that the CPU dirties while the transfer runs is written
|
||||
// back over what the device wrote.
|
||||
static uint8_t* g_ctrlBounce = nullptr;
|
||||
static uint64_t g_ctrlBouncePhys = 0;
|
||||
|
||||
uint32_t ControlTransfer(uint8_t slotId, uint8_t bmRequestType, uint8_t bRequest,
|
||||
uint16_t wValue, uint16_t wIndex, uint16_t wLength,
|
||||
void* data, bool dirIn) {
|
||||
|
||||
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) {
|
||||
return 0xFF;
|
||||
}
|
||||
@@ -836,6 +954,32 @@ namespace Drivers::USB::Xhci {
|
||||
g_pollOwnerCpu.load(std::memory_order_relaxed) == CurrentCpuIndex();
|
||||
if (!trueNested) g_controlXferLock.Acquire();
|
||||
|
||||
bool hasData = data != nullptr && wLength != 0;
|
||||
bool bounce = hasData && !g_dmaCoherent && !trueNested &&
|
||||
g_ctrlBounce != nullptr && wLength <= 0x1000;
|
||||
uint32_t cc;
|
||||
if (bounce) {
|
||||
// Copied in both directions: bytes a short IN transfer leaves
|
||||
// alone keep the caller's contents, as with a direct transfer.
|
||||
memcpy(g_ctrlBounce, data, wLength);
|
||||
cc = ControlTransferLocked(slotId, bmRequestType, bRequest, wValue, wIndex,
|
||||
wLength, g_ctrlBounce, g_ctrlBouncePhys, dirIn, false);
|
||||
if (dirIn) memcpy(data, g_ctrlBounce, wLength);
|
||||
} else {
|
||||
// The data stage buffer is the caller's (cacheable) memory.
|
||||
if (hasData) Memory::Dma::SyncForDevice(data, wLength, g_dmaCoherent);
|
||||
cc = ControlTransferLocked(slotId, bmRequestType, bRequest, wValue, wIndex, wLength,
|
||||
data, hasData ? Memory::SubHHDM(data) : 0, dirIn, trueNested);
|
||||
if (hasData && dirIn) Memory::Dma::SyncForCpu(data, wLength, g_dmaCoherent);
|
||||
}
|
||||
|
||||
if (!trueNested) g_controlXferLock.Release();
|
||||
return cc;
|
||||
}
|
||||
|
||||
static uint32_t ControlTransferLocked(uint8_t slotId, uint8_t bmRequestType, uint8_t bRequest,
|
||||
uint16_t wValue, uint16_t wIndex, uint16_t wLength,
|
||||
void* data, uint64_t dataPhys, bool dirIn, bool trueNested) {
|
||||
UsbDeviceInfo& dev = g_devices[slotId];
|
||||
|
||||
// --- Setup Stage TRB ---
|
||||
@@ -853,15 +997,15 @@ namespace Drivers::USB::Xhci {
|
||||
if (dev.EP0RingCCS) {
|
||||
setupControl |= TRB_CYCLE_BIT;
|
||||
}
|
||||
setup.Control = setupControl;
|
||||
// The Setup TRB's cycle bit is published only after the Data and
|
||||
// Status TRBs are complete (below): if EP0 is still running, it
|
||||
// would otherwise run into a half-built TD.
|
||||
|
||||
// Advance EP0 enqueue (handles Link TRB wrap)
|
||||
AdvanceEP0Ring(dev);
|
||||
|
||||
// --- Data Stage TRB (if wLength > 0) ---
|
||||
if (wLength > 0 && data != nullptr) {
|
||||
uint64_t dataPhys = Memory::SubHHDM(data);
|
||||
|
||||
TRB& dataTrb = dev.EP0Ring[dev.EP0RingEnqueue];
|
||||
dataTrb.Parameter0 = (uint32_t)(dataPhys & 0xFFFFFFFF);
|
||||
dataTrb.Parameter1 = (uint32_t)(dataPhys >> 32);
|
||||
@@ -874,7 +1018,7 @@ namespace Drivers::USB::Xhci {
|
||||
if (dev.EP0RingCCS) {
|
||||
dataControl |= TRB_CYCLE_BIT;
|
||||
}
|
||||
dataTrb.Control = dataControl;
|
||||
PublishTrb(dataTrb, dataControl);
|
||||
|
||||
AdvanceEP0Ring(dev);
|
||||
}
|
||||
@@ -895,10 +1039,12 @@ namespace Drivers::USB::Xhci {
|
||||
if (dev.EP0RingCCS) {
|
||||
statusControl |= TRB_CYCLE_BIT;
|
||||
}
|
||||
status.Control = statusControl;
|
||||
PublishTrb(status, statusControl);
|
||||
|
||||
AdvanceEP0Ring(dev);
|
||||
|
||||
PublishTrb(setup, setupControl);
|
||||
|
||||
// Ring doorbell for this slot, target EP0 (DCI 1)
|
||||
g_xferCompleted[slotId] = false;
|
||||
WriteDoorbell(slotId, 1);
|
||||
@@ -922,7 +1068,6 @@ namespace Drivers::USB::Xhci {
|
||||
while (Timekeeping::GetMilliseconds() - xferStart < 2000) {
|
||||
PollEvents();
|
||||
if (g_xferCompleted[slotId]) {
|
||||
g_controlXferLock.Release();
|
||||
return g_xferCompletionCode[slotId];
|
||||
}
|
||||
for (int j = 0; j < 100; j++) {
|
||||
@@ -956,7 +1101,6 @@ namespace Drivers::USB::Xhci {
|
||||
| (1 << 16); // DCI=1 (EP0)
|
||||
SendCommand(deqTrb);
|
||||
|
||||
g_controlXferLock.Release();
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
@@ -986,7 +1130,7 @@ namespace Drivers::USB::Xhci {
|
||||
if (dev.InterruptRingCCS) {
|
||||
control |= TRB_CYCLE_BIT;
|
||||
}
|
||||
trb.Control = control;
|
||||
PublishTrb(trb, control);
|
||||
|
||||
// Advance enqueue (handles Link TRB wrap)
|
||||
AdvanceInterruptRing(dev);
|
||||
@@ -1123,7 +1267,7 @@ namespace Drivers::USB::Xhci {
|
||||
if (dev.BulkInRingCCS) {
|
||||
control |= TRB_CYCLE_BIT;
|
||||
}
|
||||
trb.Control = control;
|
||||
PublishTrb(trb, control);
|
||||
|
||||
AdvanceBulkInRing(dev);
|
||||
|
||||
@@ -1250,6 +1394,9 @@ namespace Drivers::USB::Xhci {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
void QueueBulkOutTransfer(uint8_t slotId, uint8_t* data, uint64_t dataPhys, uint32_t length) {
|
||||
if (data != nullptr && length != 0) {
|
||||
Memory::Dma::SyncForDevice(data, length, g_dmaCoherent);
|
||||
}
|
||||
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
|
||||
|
||||
UsbDeviceInfo& dev = g_devices[slotId];
|
||||
@@ -1264,7 +1411,7 @@ namespace Drivers::USB::Xhci {
|
||||
if (dev.BulkOutRingCCS) {
|
||||
control |= TRB_CYCLE_BIT;
|
||||
}
|
||||
trb.Control = control;
|
||||
PublishTrb(trb, control);
|
||||
|
||||
AdvanceBulkOutRing(dev);
|
||||
|
||||
@@ -1277,8 +1424,23 @@ namespace Drivers::USB::Xhci {
|
||||
// BulkTransfer - blocking bulk IN/OUT transfer
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static uint32_t BulkTransferUnsynced(uint8_t slotId, bool dirIn, void* data, uint64_t dataPhys,
|
||||
uint32_t length, uint32_t* actualLength);
|
||||
|
||||
uint32_t BulkTransfer(uint8_t slotId, bool dirIn, void* data, uint64_t dataPhys,
|
||||
uint32_t length, uint32_t* actualLength) {
|
||||
if (data != nullptr && length != 0) {
|
||||
Memory::Dma::SyncForDevice(data, length, g_dmaCoherent);
|
||||
}
|
||||
uint32_t cc = BulkTransferUnsynced(slotId, dirIn, data, dataPhys, length, actualLength);
|
||||
if (data != nullptr && length != 0 && dirIn) {
|
||||
Memory::Dma::SyncForCpu(data, length, g_dmaCoherent);
|
||||
}
|
||||
return cc;
|
||||
}
|
||||
|
||||
static uint32_t BulkTransferUnsynced(uint8_t slotId, bool dirIn, void* data, uint64_t dataPhys,
|
||||
uint32_t length, uint32_t* actualLength) {
|
||||
if (actualLength) *actualLength = 0;
|
||||
if (length == 0) return CC_SUCCESS;
|
||||
if (length > 0x10000) return 0xFF;
|
||||
@@ -1301,7 +1463,7 @@ namespace Drivers::USB::Xhci {
|
||||
|
||||
uint32_t control = (TRB_NORMAL << TRB_TYPE_SHIFT) | TRB_IOC | TRB_ISP;
|
||||
if (dev.BulkInRingCCS) control |= TRB_CYCLE_BIT;
|
||||
trb.Control = control;
|
||||
PublishTrb(trb, control);
|
||||
|
||||
AdvanceBulkInRing(dev);
|
||||
target = dev.BulkInEpNum * 2 + 1;
|
||||
@@ -1315,7 +1477,7 @@ namespace Drivers::USB::Xhci {
|
||||
|
||||
uint32_t control = (TRB_NORMAL << TRB_TYPE_SHIFT) | TRB_IOC;
|
||||
if (dev.BulkOutRingCCS) control |= TRB_CYCLE_BIT;
|
||||
trb.Control = control;
|
||||
PublishTrb(trb, control);
|
||||
|
||||
AdvanceBulkOutRing(dev);
|
||||
target = dev.BulkOutEpNum * 2;
|
||||
@@ -1395,10 +1557,25 @@ namespace Drivers::USB::Xhci {
|
||||
return g_initialized;
|
||||
}
|
||||
|
||||
// A HID endpoint whose error backoff has run out.
|
||||
static bool HidRecoveryDue() {
|
||||
uint64_t now = 0;
|
||||
for (uint8_t s = 1; s <= MAX_SLOTS; s++) {
|
||||
if (g_hidErrorCc[s].load(std::memory_order_acquire) == 0) continue;
|
||||
if (now == 0) now = Timekeeping::GetMilliseconds();
|
||||
if (now >= g_hidRecoverDueMs[s].load(std::memory_order_relaxed)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HasDeferredWork() {
|
||||
if (g_initialized && g_pollMode && EventRingHasWork()) {
|
||||
g_irqDrainPending.store(true, std::memory_order_release);
|
||||
}
|
||||
return g_initialized &&
|
||||
(g_irqDrainPending.load(std::memory_order_acquire) ||
|
||||
g_deferredWorkPending.load(std::memory_order_acquire) ||
|
||||
(g_bootScanComplete && (Hub::HasPendingWork() || HidRecoveryDue())) ||
|
||||
g_diagnosticWorkPending.load(std::memory_order_acquire));
|
||||
}
|
||||
|
||||
@@ -1448,7 +1625,9 @@ namespace Drivers::USB::Xhci {
|
||||
}
|
||||
|
||||
static void UnregisterClassDriver(uint8_t slotId, const UsbDeviceInfo& dev) {
|
||||
if (dev.InterfaceClass == UsbDevice::CLASS_MASS_STORAGE) {
|
||||
if (dev.InterfaceClass == UsbDevice::CLASS_HUB) {
|
||||
Hub::UnregisterHub(slotId);
|
||||
} else if (dev.InterfaceClass == UsbDevice::CLASS_MASS_STORAGE) {
|
||||
MassStorage::UnregisterDevice(slotId);
|
||||
} else if (dev.InterfaceClass == UsbDevice::CLASS_HID &&
|
||||
dev.InterfaceProtocol == UsbDevice::PROTOCOL_KEYBOARD) {
|
||||
@@ -1471,6 +1650,105 @@ namespace Drivers::USB::Xhci {
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveDevice(uint8_t slotId) {
|
||||
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
|
||||
|
||||
// Children first: a hub's slot outlives the devices addressed through it.
|
||||
for (uint8_t s = 1; s <= MAX_SLOTS; s++) {
|
||||
if (s != slotId && g_devices[s].Active && g_devices[s].ParentSlot == slotId) {
|
||||
RemoveDevice(s);
|
||||
}
|
||||
}
|
||||
|
||||
UserUsb::DeviceDisconnected(slotId);
|
||||
UnregisterClassDriver(slotId, g_devices[slotId]);
|
||||
g_devices[slotId].Active = false;
|
||||
g_hidErrorCc[slotId].store(0, std::memory_order_relaxed);
|
||||
g_hidErrorStreak[slotId].store(0, std::memory_order_relaxed);
|
||||
g_transferCallbacks[slotId] = nullptr;
|
||||
DisableSlot(slotId);
|
||||
g_dcbaa[slotId] = 0;
|
||||
g_devices[slotId] = {};
|
||||
KernelLogStream(INFO, "xHCI") << "Slot " << base::dec << (uint64_t)slotId << " deactivated";
|
||||
}
|
||||
|
||||
// Bring a HID device's interrupt endpoint back after a failed transfer.
|
||||
// Reset Endpoint clears a halt (a Context State Error when the error did
|
||||
// not halt it), Set TR Dequeue skips the failed TRB, and CLEAR_FEATURE
|
||||
// (ENDPOINT_HALT) clears a stall on the device and puts its data toggle
|
||||
// back to DATA0 to match the reset host side (USB 2.0 9.4.5). If errors keep coming, the
|
||||
// device is removed and its port reset, as a replug would.
|
||||
static void RecoverHidEndpoint(uint8_t slotId, uint32_t cc) {
|
||||
UsbDeviceInfo& dev = g_devices[slotId];
|
||||
if (!dev.Active || dev.InterfaceClass != UsbDevice::CLASS_HID ||
|
||||
dev.InterruptEpNum == 0 || !dev.InterruptRing) return;
|
||||
|
||||
uint32_t streak = g_hidErrorStreak[slotId].load(std::memory_order_relaxed);
|
||||
if (streak >= HID_REENUMERATE_STREAK) {
|
||||
KernelLogStream(WARNING, "xHCI") << "Slot " << base::dec << (uint64_t)slotId
|
||||
<< ": HID endpoint keeps failing (cc=" << (uint64_t)cc
|
||||
<< "), resetting the device";
|
||||
uint8_t parentSlot = dev.ParentSlot, parentPort = dev.ParentPort;
|
||||
uint8_t rootPort = dev.PortId;
|
||||
RemoveDevice(slotId);
|
||||
if (parentSlot != 0) {
|
||||
Hub::RequestPortCheck(parentSlot, parentPort);
|
||||
} else if (rootPort >= 1 && rootPort <= g_maxPorts) {
|
||||
// Disable the port (PED is write-1-to-clear) so the hot-plug
|
||||
// pass resets it before enumerating.
|
||||
uint32_t reg = OP_PORTSC_BASE + (rootPort - 1) * OP_PORTSC_STRIDE;
|
||||
WriteOp(reg, (ReadOp(reg) & PORTSC_PRESERVE) | PORTSC_PED);
|
||||
g_hotplugPending[rootPort - 1].store(true, std::memory_order_release);
|
||||
g_deferredWorkPending.store(true, std::memory_order_release);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (streak <= 1) {
|
||||
KernelLogStream(WARNING, "xHCI") << "Slot " << base::dec << (uint64_t)slotId
|
||||
<< ": HID transfer failed (cc=" << (uint64_t)cc << "), recovering endpoint";
|
||||
}
|
||||
|
||||
uint8_t dci = dev.InterruptEpNum * 2 + 1;
|
||||
|
||||
TRB resetTrb = {};
|
||||
resetTrb.Control = (TRB_RESET_ENDPOINT << TRB_TYPE_SHIFT)
|
||||
| ((uint32_t)slotId << 24)
|
||||
| ((uint32_t)dci << 16);
|
||||
bool wasHalted = SendCommand(resetTrb) == CC_SUCCESS;
|
||||
|
||||
uint64_t newDeq = dev.InterruptRingPhys
|
||||
+ (uint64_t)dev.InterruptRingEnqueue * sizeof(TRB);
|
||||
if (dev.InterruptRingCCS) newDeq |= 1; // DCS bit
|
||||
|
||||
TRB deqTrb = {};
|
||||
deqTrb.Parameter0 = (uint32_t)(newDeq & 0xFFFFFFFF);
|
||||
deqTrb.Parameter1 = (uint32_t)(newDeq >> 32);
|
||||
deqTrb.Control = (TRB_SET_TR_DEQUEUE << TRB_TYPE_SHIFT)
|
||||
| ((uint32_t)slotId << 24)
|
||||
| ((uint32_t)dci << 16);
|
||||
SendCommand(deqTrb);
|
||||
|
||||
// Only after a real reset: the host toggle is untouched otherwise.
|
||||
if (wasHalted) {
|
||||
ControlTransfer(slotId, 0x02 /* host-to-dev, standard, endpoint */,
|
||||
0x01 /* CLEAR_FEATURE */, 0 /* ENDPOINT_HALT */,
|
||||
(uint16_t)(0x80 | dev.InterruptEpNum), 0, nullptr, false);
|
||||
}
|
||||
|
||||
if (dev.Active) QueueInterruptTransfer(slotId);
|
||||
}
|
||||
|
||||
static void ProcessHidRecovery() {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
for (uint8_t s = 1; s <= MAX_SLOTS; s++) {
|
||||
if (g_hidErrorCc[s].load(std::memory_order_acquire) == 0) continue;
|
||||
if (now < g_hidRecoverDueMs[s].load(std::memory_order_relaxed)) continue;
|
||||
uint32_t cc = g_hidErrorCc[s].exchange(0, std::memory_order_acq_rel);
|
||||
if (cc != 0) RecoverHidEndpoint(s, cc);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ProcessDeferredWork - handle hot-plug outside interrupt context.
|
||||
// Called from the BSP idle path so class-driver probing can wait on timers.
|
||||
@@ -1488,7 +1766,8 @@ namespace Drivers::USB::Xhci {
|
||||
|
||||
bool haveDiagnostics = g_diagnosticWorkPending.load(std::memory_order_acquire);
|
||||
bool haveHotplug = g_bootScanComplete &&
|
||||
g_deferredWorkPending.load(std::memory_order_acquire);
|
||||
(g_deferredWorkPending.load(std::memory_order_acquire) || Hub::HasPendingWork() ||
|
||||
HidRecoveryDue());
|
||||
if (!haveDiagnostics && !haveHotplug) return;
|
||||
|
||||
// CAS claim: any idle core may call this, but only one runs at a time.
|
||||
@@ -1505,6 +1784,9 @@ namespace Drivers::USB::Xhci {
|
||||
return;
|
||||
}
|
||||
|
||||
// First: a device it gives up on is queued for the hot-plug pass below.
|
||||
ProcessHidRecovery();
|
||||
|
||||
g_deferredWorkPending.store(false, std::memory_order_release);
|
||||
|
||||
for (uint32_t port = 0; port < g_maxPorts; port++) {
|
||||
@@ -1537,7 +1819,7 @@ namespace Drivers::USB::Xhci {
|
||||
for (uint32_t i = 0; i < 100000; i++) {
|
||||
PollEvents();
|
||||
uint32_t ps = ReadOp(OP_PORTSC_BASE + port * OP_PORTSC_STRIDE);
|
||||
if (ps & PORTSC_PRC) {
|
||||
if (PortResetComplete(ps)) {
|
||||
resetDone = true;
|
||||
break;
|
||||
}
|
||||
@@ -1565,25 +1847,23 @@ namespace Drivers::USB::Xhci {
|
||||
UsbDevice::EnumerateDevice(port + 1, speed);
|
||||
}
|
||||
} else {
|
||||
// Device disconnected — deactivate its slot
|
||||
// Device disconnected: deactivate its slot (and, for a hub,
|
||||
// the slots of everything behind it)
|
||||
for (uint8_t s = 1; s <= MAX_SLOTS; s++) {
|
||||
if (g_devices[s].Active && g_devices[s].PortId == port + 1) {
|
||||
UserUsb::DeviceDisconnected(s);
|
||||
UnregisterClassDriver(s, g_devices[s]);
|
||||
g_devices[s].Active = false;
|
||||
g_transferCallbacks[s] = nullptr;
|
||||
DisableSlot(s);
|
||||
g_dcbaa[s] = 0;
|
||||
g_devices[s] = {};
|
||||
KernelLogStream(INFO, "xHCI") << "Hot-unplug: slot "
|
||||
<< base::dec << (uint64_t)s << " (port "
|
||||
<< (uint64_t)(port + 1) << ") deactivated";
|
||||
if (g_devices[s].Active && g_devices[s].PortId == port + 1 &&
|
||||
g_devices[s].ParentSlot == 0) {
|
||||
KernelLogStream(INFO, "xHCI") << "Hot-unplug: port "
|
||||
<< base::dec << (uint64_t)(port + 1);
|
||||
RemoveDevice(s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Port changes behind hubs.
|
||||
Hub::ProcessPendingWork();
|
||||
|
||||
g_hotplugProcessing.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
@@ -1591,32 +1871,22 @@ namespace Drivers::USB::Xhci {
|
||||
// Probe (called by PCI driver matching framework)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
bool Probe(const Pci::PciDevice& dev) {
|
||||
if (g_initialized) return false;
|
||||
|
||||
KernelLogStream(OK, "xHCI") << "Found controller at PCI "
|
||||
<< base::hex << (uint64_t)dev.Bus << ":"
|
||||
<< (uint64_t)dev.Device << "." << (uint64_t)dev.Function;
|
||||
|
||||
// Read BAR0 and map MMIO region
|
||||
uint64_t mmioPhys = Pci::ReadBar0(dev.Bus, dev.Device, dev.Function);
|
||||
|
||||
KernelLogStream(INFO, "xHCI") << "BAR0 physical: " << base::hex << mmioPhys;
|
||||
|
||||
constexpr uint64_t MmioSize = 0x10000;
|
||||
for (uint64_t offset = 0; offset < MmioSize; offset += 0x1000) {
|
||||
// Map the register block, reset the controller and set up its
|
||||
// context and command/event rings. Interrupt delivery is the caller's.
|
||||
static void InitializeController(uint64_t mmioPhys, uint64_t mmioSize) {
|
||||
for (uint64_t offset = 0; offset < mmioSize; offset += 0x1000) {
|
||||
Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset));
|
||||
}
|
||||
|
||||
g_mmioBase = (volatile uint8_t*)Memory::HHDM(mmioPhys);
|
||||
|
||||
Pci::EnableBusMaster(dev.Bus, dev.Device, dev.Function);
|
||||
KernelLogStream(OK, "xHCI") << "Bus mastering enabled";
|
||||
// Parse capability registers. CAPLENGTH and HCIVERSION share the
|
||||
// first dword; read it whole, since narrower accesses are not safe
|
||||
// behind every PCIe bridge (Linux does the same).
|
||||
uint32_t capBase = ReadCap(CAP_CAPLENGTH);
|
||||
g_capLength = (uint8_t)(capBase & 0xFF);
|
||||
|
||||
// Parse capability registers
|
||||
g_capLength = *(volatile uint8_t*)(g_mmioBase + CAP_CAPLENGTH);
|
||||
|
||||
uint32_t hciVersion = *(volatile uint16_t*)(g_mmioBase + CAP_HCIVERSION);
|
||||
uint32_t hciVersion = capBase >> 16;
|
||||
KernelLogStream(INFO, "xHCI") << "Version: " << base::hex << (uint64_t)hciVersion
|
||||
<< ", CapLength: " << (uint64_t)g_capLength;
|
||||
|
||||
@@ -1636,6 +1906,16 @@ namespace Drivers::USB::Xhci {
|
||||
g_rtBase = g_mmioBase + rtsOff;
|
||||
g_dbBase = g_mmioBase + dbOff;
|
||||
|
||||
// A firmware-described register block may be smaller than what the
|
||||
// capability registers point into (port registers, interrupter 0,
|
||||
// the doorbells): map those too.
|
||||
uint64_t needed = g_capLength + OP_PORTSC_BASE + (uint64_t)((hcsParams1 >> 24) & 0xFF) * OP_PORTSC_STRIDE;
|
||||
if (rtsOff + 0x40 > needed) needed = rtsOff + 0x40;
|
||||
if (dbOff + 4 * ((hcsParams1 & 0xFF) + 1) > needed) needed = dbOff + 4 * ((hcsParams1 & 0xFF) + 1);
|
||||
for (uint64_t offset = mmioSize; offset < needed; offset += 0x1000) {
|
||||
Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset));
|
||||
}
|
||||
|
||||
KernelLogStream(INFO, "xHCI") << "MaxSlots: " << base::dec << (uint64_t)g_maxSlots
|
||||
<< ", MaxPorts: " << (uint64_t)g_maxPorts
|
||||
<< ", ScratchpadBufs: " << (uint64_t)maxScratchpadBufs;
|
||||
@@ -1681,6 +1961,8 @@ namespace Drivers::USB::Xhci {
|
||||
// Program CONFIG
|
||||
WriteOp(OP_CONFIG, g_maxSlots);
|
||||
|
||||
if (!g_dmaCoherent) g_ctrlBounce = AllocateDmaBuffer(g_ctrlBouncePhys);
|
||||
|
||||
// Allocate DCBAA
|
||||
g_dcbaa = (uint64_t*)AllocateDmaBuffer(g_dcbaaPhys);
|
||||
WriteOp(OP_DCBAAP, (uint32_t)(g_dcbaaPhys & 0xFFFFFFFF));
|
||||
@@ -1728,12 +2010,10 @@ namespace Drivers::USB::Xhci {
|
||||
g_evtRingCCS = true;
|
||||
g_evtRingDequeue = 0;
|
||||
KernelLogStream(OK, "xHCI") << "Event ring at phys " << base::hex << g_evtRingPhys;
|
||||
|
||||
// MSI setup
|
||||
if (!SetupMsi(dev.Bus, dev.Device, dev.Function)) {
|
||||
KernelLogStream(WARNING, "xHCI") << "MSI not available, using poll mode";
|
||||
}
|
||||
|
||||
// Enable the interrupter, run the controller, power and enumerate ports.
|
||||
static bool StartController() {
|
||||
// Enable interrupter 0
|
||||
WriteRt(IR0_IMAN, IMAN_IE);
|
||||
WriteRt(IR0_IMOD, IMOD_INTERVAL_100US);
|
||||
@@ -1772,7 +2052,7 @@ namespace Drivers::USB::Xhci {
|
||||
for (uint32_t i = 0; i < 100000; i++) {
|
||||
PollEvents();
|
||||
uint32_t ps = ReadOp(OP_PORTSC_BASE + port * OP_PORTSC_STRIDE);
|
||||
if (ps & PORTSC_PRC) { resetDone = true; break; }
|
||||
if (PortResetComplete(ps)) { resetDone = true; break; }
|
||||
for (int j = 0; j < 100; j++) Arch::CompilerBarrier();
|
||||
}
|
||||
|
||||
@@ -1801,11 +2081,75 @@ namespace Drivers::USB::Xhci {
|
||||
UsbDevice::EnumerateDevice(port + 1, speed);
|
||||
}
|
||||
|
||||
// Connected ports the scan found nothing on (still training, or the
|
||||
// enumeration failed) get another look from the hot-plug path.
|
||||
for (uint32_t port = 0; port < g_maxPorts; port++) {
|
||||
if (!(ReadOp(OP_PORTSC_BASE + port * OP_PORTSC_STRIDE) & PORTSC_CCS)) continue;
|
||||
bool active = false;
|
||||
for (uint8_t s = 1; s <= MAX_SLOTS; s++) {
|
||||
if (g_devices[s].Active && g_devices[s].PortId == port + 1) active = true;
|
||||
}
|
||||
if (!active) {
|
||||
g_hotplugPending[port].store(true, std::memory_order_release);
|
||||
g_deferredWorkPending.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
g_bootScanComplete = true;
|
||||
KernelLogStream(OK, "xHCI") << "Initialization complete";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProbePlatform(uint64_t mmioPhys, uint64_t mmioSize, uint32_t gsi, bool levelTriggered,
|
||||
bool activeLow, bool dmaCoherent) {
|
||||
if (g_initialized) return false;
|
||||
|
||||
g_dmaCoherent = dmaCoherent;
|
||||
InitializeController(mmioPhys, mmioSize < 0x1000 ? 0x1000 : (mmioSize + 0xFFF) & ~0xFFFULL);
|
||||
|
||||
uint8_t line = gsi != 0 ? Hal::GsiRouting::LineForGsi(gsi, levelTriggered, activeLow) : 0;
|
||||
if (line != 0) {
|
||||
Arch::RegisterIrqHandler(line, HandleInterrupt);
|
||||
Arch::EnableLegacyIrq(line);
|
||||
KernelLogStream(OK, "xHCI") << "Interrupt: GSI " << base::dec << (uint64_t)gsi
|
||||
<< " on platform line " << (uint64_t)line;
|
||||
} else {
|
||||
KernelLogStream(WARNING, "xHCI") << "No interrupt, using poll mode";
|
||||
}
|
||||
// Watch the event ring from the timer as well. The interrupt is a
|
||||
// PCI INTx routed by firmware through a bridge the kernel does not
|
||||
// drive; if it never arrives, input still works (a ring read per
|
||||
// tick costs next to nothing).
|
||||
g_pollMode = true;
|
||||
|
||||
return StartController();
|
||||
}
|
||||
|
||||
bool Probe(const Pci::PciDevice& dev) {
|
||||
if (g_initialized) return false;
|
||||
|
||||
KernelLogStream(OK, "xHCI") << "Found controller at PCI "
|
||||
<< base::hex << (uint64_t)dev.Bus << ":"
|
||||
<< (uint64_t)dev.Device << "." << (uint64_t)dev.Function;
|
||||
|
||||
uint64_t mmioPhys = Pci::ReadBar0(dev.Bus, dev.Device, dev.Function);
|
||||
|
||||
KernelLogStream(INFO, "xHCI") << "BAR0 physical: " << base::hex << mmioPhys;
|
||||
|
||||
Pci::EnableBusMaster(dev.Bus, dev.Device, dev.Function);
|
||||
KernelLogStream(OK, "xHCI") << "Bus mastering enabled";
|
||||
|
||||
InitializeController(mmioPhys, 0x10000);
|
||||
|
||||
// MSI setup
|
||||
if (!SetupMsi(dev.Bus, dev.Device, dev.Function) && !SetupIntx(dev.Bus, dev.Device, dev.Function)) {
|
||||
KernelLogStream(WARNING, "xHCI") << "No MSI or INTx, using poll mode";
|
||||
g_pollMode = true;
|
||||
}
|
||||
|
||||
return StartController();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Initialize (standalone fallback path)
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -2043,8 +2387,9 @@ namespace Drivers::USB::Xhci {
|
||||
// -----------------------------------------------------------------
|
||||
// Step 12: MSI setup
|
||||
// -----------------------------------------------------------------
|
||||
if (!SetupMsi(foundDev->Bus, foundDev->Device, foundDev->Function)) {
|
||||
KernelLogStream(WARNING, "xHCI") << "MSI not available, using poll mode";
|
||||
if (!SetupMsi(foundDev->Bus, foundDev->Device, foundDev->Function) && !SetupIntx(foundDev->Bus, foundDev->Device, foundDev->Function)) {
|
||||
KernelLogStream(WARNING, "xHCI") << "No MSI or INTx, using poll mode";
|
||||
g_pollMode = true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
@@ -177,6 +177,10 @@ namespace Drivers::USB::Xhci {
|
||||
// Device Context structures (xHCI spec section 6.2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Slot Context Field0 flags
|
||||
constexpr uint32_t SLOT_MTT = (1u << 25); // Multi-TT
|
||||
constexpr uint32_t SLOT_HUB = (1u << 26); // the device is a hub
|
||||
|
||||
struct SlotContext {
|
||||
uint32_t Field0; // Route String, Speed, MTT, Hub, Context Entries
|
||||
uint32_t Field1; // Max Exit Latency, Root Hub Port Number, Num Ports
|
||||
@@ -232,8 +236,21 @@ namespace Drivers::USB::Xhci {
|
||||
struct UsbDeviceInfo {
|
||||
bool Active;
|
||||
bool Ready; // descriptors/endpoints and binding are complete
|
||||
uint8_t PortId;
|
||||
uint8_t PortId; // root hub port the device's branch hangs off
|
||||
uint32_t Speed;
|
||||
|
||||
// Topology (xHCI 4.3, 8.9). A device behind a hub records the hub's
|
||||
// slot and port, its route string (one 4-bit port number per hub
|
||||
// tier, tier 1 in bits 3:0) and, for a low/full-speed device behind a
|
||||
// high-speed hub, the hub and port whose transaction translator
|
||||
// carries its traffic.
|
||||
uint8_t ParentSlot; // 0 = attached to a root port
|
||||
uint8_t ParentPort; // port number on the parent hub
|
||||
uint8_t Depth; // hubs between the device and the root port
|
||||
uint32_t RouteString;
|
||||
uint8_t TtHubSlot; // 0 = no transaction translator
|
||||
uint8_t TtPort;
|
||||
bool TtMulti;
|
||||
uint16_t VendorId;
|
||||
uint16_t ProductId;
|
||||
uint8_t InterfaceClass;
|
||||
@@ -295,6 +312,19 @@ namespace Drivers::USB::Xhci {
|
||||
|
||||
void Initialize();
|
||||
bool Probe(const Pci::PciDevice& dev);
|
||||
|
||||
// A controller described by ACPI rather than found on PCI (PNP0D10,
|
||||
// e.g. the Raspberry Pi 4's VL805 behind firmware-configured PCIe):
|
||||
// its register block, its interrupt (an ACPI GSI, 0 = poll) and
|
||||
// whether its DMA snoops the CPU caches (_CCA).
|
||||
bool ProbePlatform(uint64_t mmioPhys, uint64_t mmioSize, uint32_t gsi, bool levelTriggered,
|
||||
bool activeLow, bool dmaCoherent);
|
||||
|
||||
// A zeroed page for rings, contexts and buffers the controller accesses,
|
||||
// mapped to suit the controller's DMA coherency (Memory/Dma.hpp).
|
||||
uint8_t* AllocateDmaPage(uint64_t& outPhysAddr);
|
||||
void* AllocateDmaPage();
|
||||
void FreeDmaPage(void* page);
|
||||
bool IsInitialized();
|
||||
bool HasDeferredWork();
|
||||
|
||||
@@ -374,6 +404,10 @@ namespace Drivers::USB::Xhci {
|
||||
// Access device info
|
||||
UsbDeviceInfo* GetDevice(uint8_t slotId);
|
||||
|
||||
// Tear down a device that went away, and everything behind it if it is a
|
||||
// hub: userspace claims, the class driver, the slot. Process context.
|
||||
void RemoveDevice(uint8_t slotId);
|
||||
|
||||
// Poll the event ring from process/idle context. The hard interrupt only
|
||||
// acknowledges and masks the source, then queues deferred draining.
|
||||
void PollEvents();
|
||||
|
||||
@@ -276,6 +276,13 @@ namespace Efi {
|
||||
inline EFI_RESET_SYSTEM g_ResetSystem = nullptr;
|
||||
|
||||
inline void Init(SystemTable* ST, const montauk::boot::EfiInfo& efi) {
|
||||
/* The system table and the runtime regions are firmware-reserved
|
||||
memory, which the HHDM does not cover on every architecture:
|
||||
map them (identity and HHDM) before the first access. */
|
||||
if (Memory::VMM::g_paging) {
|
||||
Memory::VMM::g_paging->MapEfiRuntime(efi);
|
||||
}
|
||||
|
||||
Kt::KernelLogStream(Kt::OK, "UEFI") << "ST Minor Revision: " << ST->Header.Revision.MinorRevision;
|
||||
Kt::KernelLogStream(Kt::OK, "UEFI") << "ST Major Revision: " << ST->Header.Revision.MajorRevision;
|
||||
|
||||
@@ -284,12 +291,6 @@ namespace Efi {
|
||||
if (ST->RuntimeServices != nullptr) {
|
||||
Kt::KernelLogStream(Kt::OK, "UEFI") << "EFI Runtime Service API is available.";
|
||||
|
||||
/* Identity-map EFI runtime service regions so firmware code
|
||||
can reference its own data at physical addresses */
|
||||
if (Memory::VMM::g_paging) {
|
||||
Memory::VMM::g_paging->MapEfiRuntime(efi);
|
||||
}
|
||||
|
||||
EFI_TIME Time;
|
||||
EFI_TIME_CAPABILITIES TimeCapabilities;
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace Fs {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* RootLabel = "MONTAUKROOT";
|
||||
|
||||
bool StringsEqual(const char* lhs, const char* rhs) {
|
||||
if (lhs == nullptr || rhs == nullptr) return false;
|
||||
|
||||
@@ -118,9 +120,15 @@ namespace Fs {
|
||||
Vfs::RegisterDrive(0, &g_ramdiskDriver);
|
||||
}
|
||||
|
||||
// When no ramdisk, disk partitions start at drive 0 so init.elf is found there.
|
||||
// Without a ramdisk the root is a disk partition: the one labelled
|
||||
// RootLabel (scripts/mk-rpi4-sd.sh makes it), else the first partition
|
||||
// mounted, so init.elf is found at 0:/os either way.
|
||||
Fat32::RegisterProbe();
|
||||
Ext2::RegisterProbe();
|
||||
if (!hasRamdisk && FsProbe::MountRootByLabel(RootLabel) < 0) {
|
||||
Kt::KernelLogStream(Kt::WARNING, "Fs")
|
||||
<< "No ramdisk and no " << RootLabel << " partition; using the first one found";
|
||||
}
|
||||
FsProbe::MountPartitions(hasRamdisk ? 1 : 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,17 @@ namespace Fs::Ext2 {
|
||||
bool isDirectory;
|
||||
};
|
||||
|
||||
// An online grow in progress (see "Online grow" below): the size being
|
||||
// grown to and how far the new block groups have been laid out.
|
||||
struct GrowPlan {
|
||||
bool active;
|
||||
uint64_t partSectors; // partition length the plan was made for
|
||||
uint32_t newBlocks;
|
||||
uint32_t newGroups;
|
||||
uint32_t newGdtBlocks;
|
||||
uint32_t nextGroup; // next new group to write
|
||||
};
|
||||
|
||||
struct Ext2Instance {
|
||||
bool active;
|
||||
int blockDevIndex;
|
||||
@@ -186,6 +197,8 @@ namespace Fs::Ext2 {
|
||||
char (*dirNames)[MaxNameLen];
|
||||
int dirNamesPages;
|
||||
int dirNameCount;
|
||||
|
||||
GrowPlan grow;
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
@@ -1857,6 +1870,407 @@ namespace Fs::Ext2 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Online grow
|
||||
// =========================================================================
|
||||
//
|
||||
// Grows a mounted volume into its partition after the partition has been
|
||||
// enlarged (the Raspberry Pi SD image does this on first boot, from a card
|
||||
// image sized to its contents). The volume stays mounted: each call runs
|
||||
// under the VFS lock and does a bounded slice of work, so other file I/O
|
||||
// interleaves between slices.
|
||||
//
|
||||
// The new block groups are written first, past the old end of the volume
|
||||
// where nothing refers to them, so an interruption there leaves the volume
|
||||
// as it was. The commit then updates the bitmaps that change, the
|
||||
// descriptor table and the superblock, and the superblock is the point the
|
||||
// new size takes effect.
|
||||
//
|
||||
// The descriptor table grows in place, which needs free blocks right after
|
||||
// it in every group holding a superblock copy: mke2fs reserves those for
|
||||
// resize_inode (mk-rpi4-sd.sh asks for room up to 1 TiB). The grow uses
|
||||
// what it needs, releases the rest and clears the feature, so a volume
|
||||
// grows once. Volumes without the reservation can still grow while the
|
||||
// table's last block has room (128 groups of 128 MiB at 4 KiB blocks).
|
||||
|
||||
static constexpr uint32_t FEATURE_COMPAT_RESIZE_INODE = 0x0010;
|
||||
static constexpr uint32_t FEATURE_RO_COMPAT_SPARSE_SUPER = 0x0001;
|
||||
static constexpr uint32_t EXT2_RESIZE_INODE = 7;
|
||||
static constexpr uint32_t SbOffsetReservedGdt = 0xCE; // s_reserved_gdt_blocks, u16
|
||||
// Blocks written per call before handing the VFS lock back (~4 MiB).
|
||||
static constexpr uint32_t GrowSliceBlocks = 1024;
|
||||
// A trailing group this close to its metadata size is left off.
|
||||
static constexpr uint32_t MinGroupDataBlocks = 64;
|
||||
// Write buffer for new groups, in blocks (pages at 4 KiB). A group's
|
||||
// bitmaps and inode table are contiguous and go out in runs this long;
|
||||
// SD cards write long runs far faster than short ones.
|
||||
static constexpr uint32_t GrowRunBlocks = 64;
|
||||
|
||||
static bool GroupHasSuper(uint32_t group, bool sparse) {
|
||||
if (!sparse || group <= 1) return true;
|
||||
for (uint32_t base : {3u, 5u, 7u}) {
|
||||
uint64_t power = base;
|
||||
while (power < group) power *= base;
|
||||
if (power == group) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint32_t GdtBlocksFor(const Ext2Instance& inst, uint32_t groups) {
|
||||
return (uint32_t)(((uint64_t)groups * sizeof(BlockGroupDescriptor)
|
||||
+ inst.blockSize - 1) / inst.blockSize);
|
||||
}
|
||||
|
||||
static uint32_t InodeTableBlocks(const Ext2Instance& inst) {
|
||||
return (uint32_t)(((uint64_t)inst.inodesPerGroup * inst.inodeSize
|
||||
+ inst.blockSize - 1) / inst.blockSize);
|
||||
}
|
||||
|
||||
static uint32_t ReservedGdtBlocks(const uint8_t* sbRaw) {
|
||||
auto* sb = (const Superblock*)sbRaw;
|
||||
if (!(sb->s_feature_compat & FEATURE_COMPAT_RESIZE_INODE)) return 0;
|
||||
uint16_t reserved;
|
||||
memcpy(&reserved, sbRaw + SbOffsetReservedGdt, sizeof(reserved));
|
||||
return reserved;
|
||||
}
|
||||
|
||||
struct GroupLayout {
|
||||
uint32_t start; // first block
|
||||
uint32_t blocks; // blocks in the group (the last may be short)
|
||||
uint32_t blockBitmap;
|
||||
uint32_t inodeBitmap;
|
||||
uint32_t inodeTable;
|
||||
uint32_t overhead; // metadata blocks at the start of the group
|
||||
};
|
||||
|
||||
// Where a group added by the grow keeps its metadata: a superblock copy and
|
||||
// the descriptor table where the group has one, then the two bitmaps and
|
||||
// the inode table, as mke2fs lays out a group without flex_bg.
|
||||
static GroupLayout NewGroupLayout(const Ext2Instance& inst, const GrowPlan& plan,
|
||||
uint32_t group, bool sparse) {
|
||||
GroupLayout l{};
|
||||
l.start = group * inst.blocksPerGroup;
|
||||
l.blocks = plan.newBlocks - l.start;
|
||||
if (l.blocks > inst.blocksPerGroup) l.blocks = inst.blocksPerGroup;
|
||||
uint32_t meta = GroupHasSuper(group, sparse) ? 1 + plan.newGdtBlocks : 0;
|
||||
l.blockBitmap = l.start + meta;
|
||||
l.inodeBitmap = l.blockBitmap + 1;
|
||||
l.inodeTable = l.inodeBitmap + 1;
|
||||
l.overhead = meta + 2 + InodeTableBlocks(inst);
|
||||
return l;
|
||||
}
|
||||
|
||||
// Work out how far the volume can grow into a partition of partSectors.
|
||||
// Returns false when the layout is one this code does not handle; a plan
|
||||
// whose newBlocks equals the current size means there is nothing to do.
|
||||
static bool PlanGrow(const Ext2Instance& inst, const uint8_t* sbRaw,
|
||||
uint64_t partSectors, GrowPlan& plan) {
|
||||
plan = {};
|
||||
plan.partSectors = partSectors;
|
||||
plan.newBlocks = inst.totalBlocks;
|
||||
plan.newGroups = inst.groupCount;
|
||||
plan.newGdtBlocks = GdtBlocksFor(inst, inst.groupCount);
|
||||
plan.nextGroup = inst.groupCount;
|
||||
|
||||
// 1 KiB volumes start at block 1 and size groups differently; the SD
|
||||
// image (and our own mkfs) use 4 KiB blocks.
|
||||
if (inst.firstDataBlock != 0) return false;
|
||||
if (inst.blocksPerGroup != inst.blockSize * 8) return false;
|
||||
if (inst.groupCount != (inst.totalBlocks + inst.blocksPerGroup - 1) / inst.blocksPerGroup)
|
||||
return false;
|
||||
|
||||
auto* sb = (const Superblock*)sbRaw;
|
||||
bool sparse = sb->s_feature_ro_compat & FEATURE_RO_COMPAT_SPARSE_SUPER;
|
||||
uint32_t bpg = inst.blocksPerGroup;
|
||||
uint32_t oldGdt = GdtBlocksFor(inst, inst.groupCount);
|
||||
uint64_t maxGroups = (uint64_t)(oldGdt + ReservedGdtBlocks(sbRaw))
|
||||
* (inst.blockSize / sizeof(BlockGroupDescriptor));
|
||||
// Block and inode numbers are 32-bit.
|
||||
if (maxGroups > 0xFFFFFFFFull / bpg) maxGroups = 0xFFFFFFFFull / bpg;
|
||||
if (maxGroups > 0xFFFFFFFFull / inst.inodesPerGroup)
|
||||
maxGroups = 0xFFFFFFFFull / inst.inodesPerGroup;
|
||||
|
||||
uint64_t blocks = partSectors / SectorsPerBlock(inst);
|
||||
if (blocks > maxGroups * bpg) blocks = maxGroups * bpg;
|
||||
if (blocks <= inst.totalBlocks) return true;
|
||||
|
||||
uint32_t groups = (uint32_t)((blocks + bpg - 1) / bpg);
|
||||
uint32_t last = groups - 1;
|
||||
if (last >= inst.groupCount) {
|
||||
uint32_t lastBlocks = (uint32_t)(blocks - (uint64_t)last * bpg);
|
||||
uint32_t gdt = GdtBlocksFor(inst, groups);
|
||||
uint32_t overhead = (GroupHasSuper(last, sparse) ? 1 + gdt : 0)
|
||||
+ 2 + InodeTableBlocks(inst);
|
||||
if (lastBlocks < overhead + MinGroupDataBlocks) {
|
||||
groups--;
|
||||
blocks = (uint64_t)groups * bpg;
|
||||
if (blocks <= inst.totalBlocks) return true;
|
||||
}
|
||||
}
|
||||
|
||||
plan.newBlocks = (uint32_t)blocks;
|
||||
plan.newGroups = groups;
|
||||
plan.newGdtBlocks = GdtBlocksFor(inst, groups);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void SetBits(uint8_t* bitmap, uint32_t first, uint32_t end) {
|
||||
for (uint32_t bit = first; bit < end; bit++) bitmap[bit / 8] |= (uint8_t)(1 << (bit % 8));
|
||||
}
|
||||
|
||||
// Write blocks past the cache, then refresh any copies the cache holds.
|
||||
// The grow writes through here because the blocks are new: there is
|
||||
// nothing to read back, and pulling them into the LRU would only evict
|
||||
// live ones.
|
||||
static bool WriteGrowBlocks(Ext2Instance& inst, uint32_t first, uint32_t count,
|
||||
const uint8_t* data) {
|
||||
if (!WritePartSectors(inst, BlockToPartSector(inst, first),
|
||||
count * SectorsPerBlock(inst), data)) return false;
|
||||
if (inst.cache) {
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
if (BlockCache::Contains(inst.cache, first + i)) {
|
||||
BlockCache::Write(inst.cache, first + i, data + (uint64_t)i * inst.blockSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Write one new group's bitmaps and zeroed inode table, which NewGroupLayout
|
||||
// places back to back, as runs of up to GrowRunBlocks from `buf`. Its
|
||||
// superblock and descriptor copies are written at commit, once the table
|
||||
// is final.
|
||||
static bool PrepareGroup(Ext2Instance& inst, const GrowPlan& plan, uint32_t group,
|
||||
bool sparse, uint8_t* buf) {
|
||||
GroupLayout l = NewGroupLayout(inst, plan, group, sparse);
|
||||
uint32_t bitsPerBlock = inst.blockSize * 8;
|
||||
uint8_t* blockBitmap = buf;
|
||||
uint8_t* inodeBitmap = buf + inst.blockSize;
|
||||
|
||||
memset(buf, 0, (uint64_t)GrowRunBlocks * inst.blockSize);
|
||||
SetBits(blockBitmap, 0, l.overhead);
|
||||
SetBits(blockBitmap, l.blocks, bitsPerBlock); // past the group's end
|
||||
SetBits(inodeBitmap, inst.inodesPerGroup, bitsPerBlock);
|
||||
|
||||
// e2fsck reads every inode slot, so a table left with the card's old
|
||||
// contents would show up as phantom files.
|
||||
uint32_t total = 2 + InodeTableBlocks(inst);
|
||||
for (uint32_t done = 0; done < total; ) {
|
||||
uint32_t run = total - done;
|
||||
if (run > GrowRunBlocks) run = GrowRunBlocks;
|
||||
if (!WriteGrowBlocks(inst, l.blockBitmap + done, run, buf)) return false;
|
||||
if (done == 0) memset(buf, 0, 2 * (uint64_t)inst.blockSize); // zeros from here on
|
||||
done += run;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clear `count` bits from `first` in an existing group's block bitmap and
|
||||
// credit them to the group's free count (in memory; the caller writes the
|
||||
// descriptor table).
|
||||
static bool ReleaseBlocks(Ext2Instance& inst, uint32_t group, uint32_t first, uint32_t count) {
|
||||
if (count == 0) return true;
|
||||
uint32_t bitmapBlock = inst.bgdt[group].bg_block_bitmap;
|
||||
if (!ReadBlock(inst, bitmapBlock, inst.blockBuf)) return false;
|
||||
uint32_t released = 0;
|
||||
for (uint32_t bit = first; bit < first + count; bit++) {
|
||||
uint8_t mask = (uint8_t)(1 << (bit % 8));
|
||||
if (inst.blockBuf[bit / 8] & mask) {
|
||||
inst.blockBuf[bit / 8] &= (uint8_t)~mask;
|
||||
released++;
|
||||
}
|
||||
}
|
||||
if (!WriteBlock(inst, bitmapBlock, inst.blockBuf)) return false;
|
||||
inst.bgdt[group].bg_free_blocks_count += (uint16_t)released;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Superblock copy for `group` at the start of its first block, followed by
|
||||
// the descriptor table. Group 0's superblock sits at byte 1024 instead and
|
||||
// is written separately, last.
|
||||
static bool WriteBackup(Ext2Instance& inst, uint32_t group, uint8_t* sbRaw,
|
||||
const uint8_t* gdt, uint32_t gdtBlocks) {
|
||||
uint32_t start = group * inst.blocksPerGroup;
|
||||
auto* sb = (Superblock*)sbRaw;
|
||||
sb->s_block_group_nr = (uint16_t)group;
|
||||
memset(inst.blockBuf, 0, inst.blockSize);
|
||||
memcpy(inst.blockBuf, sbRaw, 1024);
|
||||
sb->s_block_group_nr = 0;
|
||||
if (!WriteGrowBlocks(inst, start, 1, inst.blockBuf)) return false;
|
||||
return WriteGrowBlocks(inst, start + 1, gdtBlocks, gdt);
|
||||
}
|
||||
|
||||
static bool CommitGrow(Ext2Instance& inst, uint8_t* sbRaw, const GrowPlan& plan) {
|
||||
auto* sb = (Superblock*)sbRaw;
|
||||
bool sparse = sb->s_feature_ro_compat & FEATURE_RO_COMPAT_SPARSE_SUPER;
|
||||
uint32_t bpg = inst.blocksPerGroup;
|
||||
uint32_t oldGroups = inst.groupCount;
|
||||
uint32_t oldGdt = GdtBlocksFor(inst, oldGroups);
|
||||
uint32_t reserved = ReservedGdtBlocks(sbRaw);
|
||||
|
||||
// ==== Bitmaps of the existing groups ====
|
||||
// The old last group's padding bits become real blocks.
|
||||
uint32_t lastGroup = oldGroups - 1;
|
||||
uint32_t lastStart = lastGroup * bpg;
|
||||
uint32_t oldLastBlocks = inst.totalBlocks - lastStart;
|
||||
uint32_t newLastBlocks = plan.newBlocks - lastStart;
|
||||
if (newLastBlocks > bpg) newLastBlocks = bpg;
|
||||
if (!ReleaseBlocks(inst, lastGroup, oldLastBlocks, newLastBlocks - oldLastBlocks))
|
||||
return false;
|
||||
|
||||
// The table now fills the first newGdt - oldGdt reserved blocks of every
|
||||
// existing superblock group; the rest go back to the free pool, along
|
||||
// with the resize inode's index block, and the feature is dropped.
|
||||
if (reserved > 0) {
|
||||
for (uint32_t g = 0; g < oldGroups; g++) {
|
||||
if (!GroupHasSuper(g, sparse)) continue;
|
||||
uint32_t firstBit = 1 + plan.newGdtBlocks;
|
||||
uint32_t endBit = 1 + oldGdt + reserved;
|
||||
if (!ReleaseBlocks(inst, g, firstBit, endBit - firstBit)) return false;
|
||||
}
|
||||
|
||||
Inode resize;
|
||||
if (!ReadInode(inst, EXT2_RESIZE_INODE, &resize)) return false;
|
||||
uint32_t dind = resize.i_block[13];
|
||||
if (dind != 0 && dind < inst.totalBlocks) {
|
||||
if (!ReleaseBlocks(inst, dind / bpg, dind % bpg, 1)) return false;
|
||||
}
|
||||
memset(&resize, 0, sizeof(resize));
|
||||
if (!WriteInode(inst, EXT2_RESIZE_INODE, &resize)) return false;
|
||||
|
||||
sb->s_feature_compat &= ~FEATURE_COMPAT_RESIZE_INODE;
|
||||
uint16_t zeroReserved = 0;
|
||||
memcpy(sbRaw + SbOffsetReservedGdt, &zeroReserved, sizeof(zeroReserved));
|
||||
}
|
||||
|
||||
// ==== New descriptor table ====
|
||||
uint32_t gdtBytes = plan.newGdtBlocks * inst.blockSize;
|
||||
int gdtPages = (int)((gdtBytes + 0xFFF) / 0x1000);
|
||||
auto* gdt = (BlockGroupDescriptor*)Memory::g_pfa->ReallocConsecutive(nullptr, gdtPages);
|
||||
if (!gdt) return false;
|
||||
memset(gdt, 0, (uint64_t)gdtPages * 0x1000);
|
||||
memcpy(gdt, inst.bgdt, oldGroups * sizeof(BlockGroupDescriptor));
|
||||
|
||||
for (uint32_t g = oldGroups; g < plan.newGroups; g++) {
|
||||
GroupLayout l = NewGroupLayout(inst, plan, g, sparse);
|
||||
gdt[g].bg_block_bitmap = l.blockBitmap;
|
||||
gdt[g].bg_inode_bitmap = l.inodeBitmap;
|
||||
gdt[g].bg_inode_table = l.inodeTable;
|
||||
gdt[g].bg_free_blocks_count = (uint16_t)(l.blocks - l.overhead);
|
||||
gdt[g].bg_free_inodes_count = (uint16_t)inst.inodesPerGroup;
|
||||
gdt[g].bg_used_dirs_count = 0;
|
||||
}
|
||||
|
||||
uint64_t freeBlocks = 0, freeInodes = 0;
|
||||
for (uint32_t g = 0; g < plan.newGroups; g++) {
|
||||
freeBlocks += gdt[g].bg_free_blocks_count;
|
||||
freeInodes += gdt[g].bg_free_inodes_count;
|
||||
}
|
||||
|
||||
// ==== Superblock fields ====
|
||||
uint64_t reservedBlocks = (uint64_t)sb->s_r_blocks_count * plan.newBlocks / inst.totalBlocks;
|
||||
sb->s_blocks_count = plan.newBlocks;
|
||||
sb->s_r_blocks_count = (uint32_t)reservedBlocks;
|
||||
sb->s_inodes_count = plan.newGroups * inst.inodesPerGroup;
|
||||
sb->s_free_blocks_count = (uint32_t)freeBlocks;
|
||||
sb->s_free_inodes_count = (uint32_t)freeInodes;
|
||||
sb->s_block_group_nr = 0;
|
||||
|
||||
// ==== Write: primary table, superblock copies, primary superblock ====
|
||||
bool ok = WriteGrowBlocks(inst, 1, plan.newGdtBlocks, (const uint8_t*)gdt);
|
||||
for (uint32_t g = 1; ok && g < plan.newGroups; g++) {
|
||||
if (GroupHasSuper(g, sparse)) {
|
||||
ok = WriteBackup(inst, g, sbRaw, (const uint8_t*)gdt, plan.newGdtBlocks);
|
||||
}
|
||||
}
|
||||
ok = ok && WritePartSectors(inst, 2, 2, sbRaw);
|
||||
if (!ok) {
|
||||
Memory::g_pfa->Free(gdt, gdtPages);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* dev = Drivers::Storage::GetBlockDevice(inst.blockDevIndex);
|
||||
if (dev && dev->Flush) dev->Flush(dev->Ctx);
|
||||
|
||||
Memory::g_pfa->Free(inst.bgdt, inst.bgdtPages);
|
||||
inst.bgdt = gdt;
|
||||
inst.bgdtPages = gdtPages;
|
||||
inst.groupCount = plan.newGroups;
|
||||
inst.totalBlocks = plan.newBlocks;
|
||||
inst.totalInodes = plan.newGroups * inst.inodesPerGroup;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void FillGrowStatus(const Ext2Instance& inst, const GrowPlan& plan,
|
||||
uint32_t oldGroups, bool committed, Vfs::GrowStatus* out) {
|
||||
if (!out) return;
|
||||
out->fsBytes = (uint64_t)inst.totalBlocks * inst.blockSize;
|
||||
out->targetBytes = (uint64_t)plan.newBlocks * inst.blockSize;
|
||||
// One step per new group, plus the commit.
|
||||
out->stepsTotal = plan.newGroups - oldGroups + 1;
|
||||
out->stepsDone = committed ? out->stepsTotal : plan.nextGroup - oldGroups;
|
||||
}
|
||||
|
||||
static int GrowImpl(int instIndex, uint64_t partSectors, bool apply, Vfs::GrowStatus* out) {
|
||||
Ext2Instance* self = InstanceAt(instIndex);
|
||||
if (self == nullptr) return -1;
|
||||
Ext2Instance& inst = *self;
|
||||
|
||||
uint8_t sbRaw[1024];
|
||||
if (!ReadPartSectors(inst, 2, 2, sbRaw)) return -1;
|
||||
bool sparse = ((const Superblock*)sbRaw)->s_feature_ro_compat & FEATURE_RO_COMPAT_SPARSE_SUPER;
|
||||
|
||||
GrowPlan plan;
|
||||
if (inst.grow.active && inst.grow.partSectors == partSectors) {
|
||||
plan = inst.grow;
|
||||
} else if (!PlanGrow(inst, sbRaw, partSectors, plan)) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
uint32_t oldGroups = inst.groupCount;
|
||||
if (!apply || plan.newBlocks <= inst.totalBlocks) {
|
||||
FillGrowStatus(inst, plan, oldGroups, false, out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!inst.grow.active) {
|
||||
KernelLogStream(INFO, "Ext2") << "Growing \"" << inst.volumeLabel << "\" from "
|
||||
<< inst.totalBlocks << " to " << plan.newBlocks << " blocks ("
|
||||
<< oldGroups << " to " << plan.newGroups << " groups)";
|
||||
}
|
||||
plan.active = true;
|
||||
|
||||
int bufPages = (int)(((uint64_t)GrowRunBlocks * inst.blockSize + 0xFFF) / 0x1000);
|
||||
uint8_t* buf = (uint8_t*)Memory::g_pfa->ReallocConsecutive(nullptr, bufPages);
|
||||
if (!buf) return -1;
|
||||
|
||||
uint32_t written = 0;
|
||||
bool ok = true;
|
||||
while (ok && plan.nextGroup < plan.newGroups && written < GrowSliceBlocks) {
|
||||
ok = PrepareGroup(inst, plan, plan.nextGroup, sparse, buf);
|
||||
written += 2 + InodeTableBlocks(inst);
|
||||
if (ok) plan.nextGroup++;
|
||||
}
|
||||
Memory::g_pfa->Free(buf, bufPages);
|
||||
|
||||
if (ok && plan.nextGroup < plan.newGroups) {
|
||||
inst.grow = plan;
|
||||
FillGrowStatus(inst, plan, oldGroups, false, out);
|
||||
return 1;
|
||||
}
|
||||
|
||||
inst.grow = {};
|
||||
if (!ok || !CommitGrow(inst, sbRaw, plan)) {
|
||||
KernelLogStream(ERROR, "Ext2") << "Grow of \"" << inst.volumeLabel << "\" failed";
|
||||
return -1;
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "Ext2") << "Grew \"" << inst.volumeLabel << "\" to "
|
||||
<< inst.totalBlocks << " blocks (" << inst.groupCount << " groups)";
|
||||
FillGrowStatus(inst, plan, oldGroups, true, out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Template thunks — generate unique function pointers per instance
|
||||
// =========================================================================
|
||||
@@ -1886,6 +2300,7 @@ namespace Fs::Ext2 {
|
||||
static const char* DrvGetFsType(void*) { return "ext2"; }
|
||||
static int DrvStat(void* c, const char* p, Vfs::StatInfo* o) { return StatImpl(CtxToInst(c), p, o); }
|
||||
static int DrvSetTimes(void* c, const char* p, int64_t a, int64_t m) { return SetTimesImpl(CtxToInst(c), p, a, m); }
|
||||
static int DrvGrow(void* c, uint64_t s, bool a, Vfs::GrowStatus* o) { return GrowImpl(CtxToInst(c), s, a, o); }
|
||||
|
||||
// Release everything the mount owns. The driver is either not registered
|
||||
// yet, or the VFS has deactivated its drive and drained dispatches.
|
||||
@@ -2107,6 +2522,7 @@ namespace Fs::Ext2 {
|
||||
.Stat = DrvStat,
|
||||
.SetTimes = DrvSetTimes,
|
||||
.Unmount = DrvUnmount,
|
||||
.Grow = DrvGrow,
|
||||
};
|
||||
|
||||
KernelLogStream(OK, "Ext2") << "Mounted volume \""
|
||||
|
||||
@@ -78,6 +78,42 @@ namespace Fs::FsProbe {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool LabelEquals(const char* label, const char* want) {
|
||||
if (label == nullptr) return false;
|
||||
while (*label != '\0' && *label == *want) { label++; want++; }
|
||||
return *label == *want;
|
||||
}
|
||||
|
||||
int MountRootByLabel(const char* label) {
|
||||
if (!Vfs::IsInitialized() || g_probeCount == 0 || label == nullptr) return -1;
|
||||
if (Vfs::IsDriveRegistered(0)) return -1;
|
||||
|
||||
int partCount = Drivers::Storage::Gpt::GetPartitionCount();
|
||||
for (int i = 0; i < partCount && i < Drivers::Storage::Gpt::MaxPartitions; i++) {
|
||||
auto* part = Drivers::Storage::Gpt::GetPartition(i);
|
||||
if (!part || g_mounted[i] || IsEfiPartition(part)) continue;
|
||||
|
||||
for (int p = 0; p < g_probeCount; p++) {
|
||||
Vfs::FsDriver* driver = g_probes[p](
|
||||
part->BlockDevIndex, part->StartLba, part->SectorCount);
|
||||
if (!driver) continue;
|
||||
|
||||
const char* found = driver->GetLabel ? driver->GetLabel(driver->ctx) : nullptr;
|
||||
if (!LabelEquals(found, label) || Vfs::RegisterDrive(0, driver) != 0) {
|
||||
DiscardUnregisteredDriver(driver);
|
||||
break;
|
||||
}
|
||||
|
||||
g_mounted[i] = true;
|
||||
g_driveForPart[i] = 0;
|
||||
Kt::KernelLogStream(Kt::OK, "FsProbe") << "Mounted root partition "
|
||||
<< i << " (" << label << ") as drive 0";
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void MountPartitions(int firstDrive) {
|
||||
int partCount = Drivers::Storage::Gpt::GetPartitionCount();
|
||||
if (partCount == 0 || g_probeCount == 0) return;
|
||||
|
||||
@@ -18,6 +18,11 @@ namespace Fs::FsProbe {
|
||||
// Register a filesystem probe function (called once per FS driver at init).
|
||||
void Register(ProbeFn fn);
|
||||
|
||||
// Mount the non-EFI partition whose filesystem label is `label` as drive 0
|
||||
// (the system root when booting without a ramdisk). Returns the global
|
||||
// partition index, or -1 if none matched or drive 0 is already taken.
|
||||
int MountRootByLabel(const char* label);
|
||||
|
||||
// Probe all discovered storage partitions and auto-mount recognized filesystems.
|
||||
// Assigns each partition the lowest available VFS drive number at or above firstDrive.
|
||||
void MountPartitions(int firstDrive = 1);
|
||||
|
||||
@@ -392,6 +392,16 @@ namespace Fs::Vfs {
|
||||
return CopyDriveString(driveNumber, &FsDriver::GetFsType, outType, maxLen);
|
||||
}
|
||||
|
||||
int VfsGrow(int driveNumber, uint64_t partSectors, bool apply, GrowStatus* out) {
|
||||
vfsLock.Acquire();
|
||||
FsDriver* driver = DriverForLocked(driveNumber);
|
||||
int result = (driver != nullptr && driver->Grow != nullptr)
|
||||
? driver->Grow(driver->ctx, partSectors, apply, out)
|
||||
: -2;
|
||||
vfsLock.Release();
|
||||
return result;
|
||||
}
|
||||
|
||||
int VfsRename(const char* oldPath, const char* newPath) {
|
||||
int oldDrive, newDrive;
|
||||
const char* oldLocal;
|
||||
|
||||
@@ -43,6 +43,14 @@ namespace Fs::Vfs {
|
||||
bool isDir; // true if the entry is a directory
|
||||
};
|
||||
|
||||
// Progress of an online grow (FsDriver::Grow).
|
||||
struct GrowStatus {
|
||||
uint64_t fsBytes; // filesystem size now
|
||||
uint64_t targetBytes; // size once grown; equals fsBytes if it cannot grow
|
||||
uint32_t stepsDone; // progress through the grow, out of stepsTotal
|
||||
uint32_t stepsTotal;
|
||||
};
|
||||
|
||||
// A mounted filesystem. Every entry point takes the driver's own `ctx` so a
|
||||
// driver can serve any number of concurrent mounts from one set of function
|
||||
// pointers; drivers with a single global mount (e.g. the ramdisk) ignore it.
|
||||
@@ -78,6 +86,13 @@ namespace Fs::Vfs {
|
||||
// and no dispatch can still be in flight. Drivers with statically
|
||||
// allocated state (e.g. the ramdisk) leave this null.
|
||||
void (*Unmount)(void* ctx);
|
||||
// Optional: grow the filesystem to fill a partition of partSectors.
|
||||
// With apply false, only report the size it would grow to. With apply
|
||||
// true, do one bounded slice of the work: return 1 while more remains
|
||||
// and 0 once the new size is committed or there was nothing to do.
|
||||
// -1 is an I/O error, -2 a layout the driver cannot grow. Callers loop,
|
||||
// so other I/O interleaves between slices.
|
||||
int (*Grow)(void* ctx, uint64_t partSectors, bool apply, GrowStatus* out);
|
||||
};
|
||||
|
||||
void Initialize();
|
||||
@@ -108,4 +123,7 @@ namespace Fs::Vfs {
|
||||
int VfsDriveLabel(int driveNumber, char* outLabel, int maxLen);
|
||||
int VfsDriveFsType(int driveNumber, char* outType, int maxLen);
|
||||
|
||||
// Dispatch FsDriver::Grow for a drive; -2 if its driver cannot grow.
|
||||
int VfsGrow(int driveNumber, uint64_t partSectors, bool apply, GrowStatus* out);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Dma.cpp
|
||||
* Memory shared with bus-mastering devices
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Dma.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
|
||||
namespace Memory::Dma {
|
||||
|
||||
void* AllocatePage(uint64_t& physical, bool coherent) {
|
||||
void* page = Memory::g_pfa->AllocateZeroed();
|
||||
if (page == nullptr) return nullptr;
|
||||
physical = Memory::SubHHDM(page);
|
||||
|
||||
if (!coherent) {
|
||||
// Push the zeroes out and drop the lines before the alias
|
||||
// becomes uncached: a dirty line evicted later would overwrite
|
||||
// whatever the device wrote.
|
||||
Arch::InvalidateDataCache(page, 0x1000);
|
||||
Memory::VMM::g_paging->MapWC(physical, (uint64_t)page);
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
void FreePage(void* page, bool coherent) {
|
||||
if (page == nullptr) return;
|
||||
if (!coherent) {
|
||||
uint64_t physical = Memory::SubHHDM(page);
|
||||
Memory::VMM::g_paging->Map(physical, (uint64_t)page);
|
||||
}
|
||||
Memory::g_pfa->Free(page);
|
||||
}
|
||||
|
||||
void SyncForDevice(const void* buffer, uint64_t size, bool coherent) {
|
||||
if (!coherent) Arch::CleanDataCache(buffer, size);
|
||||
}
|
||||
|
||||
void SyncForCpu(const void* buffer, uint64_t size, bool coherent) {
|
||||
if (!coherent) Arch::InvalidateDataCache(buffer, size);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Dma.hpp
|
||||
* Memory shared with bus-mastering devices
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// Most DMA snoops the CPU caches (all of x86, PCIe on most ARM servers,
|
||||
// QEMU). Some does not: the Raspberry Pi 4's VL805 xHCI is described with
|
||||
// ACPI _CCA = 0. A driver learns which from its bus and passes `coherent`
|
||||
// along; for coherent devices every call here is the plain allocation or a
|
||||
// no-op.
|
||||
|
||||
namespace Memory::Dma {
|
||||
|
||||
// One zeroed page for descriptors and rings both sides read and write.
|
||||
// For a non-coherent device it is mapped uncached (its HHDM alias, the
|
||||
// kernel's only mapping of it, is switched to Normal non-cacheable), so
|
||||
// neither side needs cache maintenance for it. Returns its HHDM address
|
||||
// and physical address, or nullptr when out of memory.
|
||||
void* AllocatePage(uint64_t& physical, bool coherent);
|
||||
|
||||
// Return a page from AllocatePage.
|
||||
void FreePage(void* page, bool coherent);
|
||||
|
||||
// Streaming buffers (ordinary cacheable kernel memory a driver points a
|
||||
// transfer at): call ForDevice before the device accesses the buffer,
|
||||
// ForCpu after it wrote into it and before the CPU reads.
|
||||
void SyncForDevice(const void* buffer, uint64_t size, bool coherent);
|
||||
void SyncForCpu(const void* buffer, uint64_t size, bool coherent);
|
||||
|
||||
}
|
||||
@@ -187,7 +187,20 @@ namespace Pci {
|
||||
Io::Out32(value, ConfigDataPort);
|
||||
}
|
||||
|
||||
// Drivers reach config space through the Legacy* calls below. Without
|
||||
// an I/O address space (ARM) there is no CF8/CFC mechanism, so they use
|
||||
// ECAM there, at the access's own width (16/8-bit writes must not
|
||||
// read-modify-write a neighbouring RW1C register).
|
||||
static bool EcamCovers(uint8_t bus);
|
||||
|
||||
static bool UseEcamForLegacy(uint8_t bus) {
|
||||
return !Io::HasPortIo && EcamCovers(bus);
|
||||
}
|
||||
|
||||
uint32_t LegacyRead32(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset) {
|
||||
if (UseEcamForLegacy(bus)) {
|
||||
return EcamRead32(g_ecamBase, bus, device, function, offset & 0xFC);
|
||||
}
|
||||
g_legacyConfigLock.Acquire();
|
||||
uint32_t value = LegacyRead32Unlocked(bus, device, function, offset);
|
||||
g_legacyConfigLock.Release();
|
||||
@@ -205,12 +218,20 @@ namespace Pci {
|
||||
}
|
||||
|
||||
void LegacyWrite32(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint32_t value) {
|
||||
if (UseEcamForLegacy(bus)) {
|
||||
EcamWrite32(g_ecamBase, bus, device, function, offset & 0xFC, value);
|
||||
return;
|
||||
}
|
||||
g_legacyConfigLock.Acquire();
|
||||
LegacyWrite32Unlocked(bus, device, function, offset, value);
|
||||
g_legacyConfigLock.Release();
|
||||
}
|
||||
|
||||
void LegacyWrite16(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint16_t value) {
|
||||
if (UseEcamForLegacy(bus)) {
|
||||
EcamWrite16(g_ecamBase, bus, device, function, offset & 0xFE, value);
|
||||
return;
|
||||
}
|
||||
g_legacyConfigLock.Acquire();
|
||||
uint32_t tmp = LegacyRead32Unlocked(bus, device, function, offset & 0xFC);
|
||||
int shift = (offset & 2) * 8;
|
||||
@@ -221,6 +242,10 @@ namespace Pci {
|
||||
}
|
||||
|
||||
void LegacyWrite8(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint8_t value) {
|
||||
if (UseEcamForLegacy(bus)) {
|
||||
EcamWrite8(g_ecamBase, bus, device, function, offset, value);
|
||||
return;
|
||||
}
|
||||
g_legacyConfigLock.Acquire();
|
||||
uint32_t tmp = LegacyRead32Unlocked(bus, device, function, offset & 0xFC);
|
||||
int shift = (offset & 3) * 8;
|
||||
@@ -665,6 +690,16 @@ namespace Pci {
|
||||
void Initialize(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||
KernelLogStream(INFO, "PCI") << "Initializing PCI subsystem";
|
||||
|
||||
// A plain ECAM walk of a quirky config space reads (and can fault
|
||||
// on) host-bridge registers. Its devices are described by ACPI
|
||||
// instead (Drivers/Init.cpp's platform devices).
|
||||
char quirk[32];
|
||||
if (RootBridgeEcamQuirk(quirk, sizeof(quirk))) {
|
||||
KernelLogStream(WARNING, "PCI") << "Root bridge config space is not ECAM (quirk \""
|
||||
<< quirk << "\"): not enumerating PCI";
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse MCFG for ECAM access; fall back to legacy if unavailable
|
||||
ParseMcfg(xsdt);
|
||||
|
||||
|
||||
@@ -91,6 +91,18 @@ namespace Pci {
|
||||
// xsdt: pointer to the XSDT (already HHDM-mapped)
|
||||
void Initialize(Hal::ACPI::CommonSDTHeader* xsdt);
|
||||
|
||||
// Whether the root bridge's _DSD names an ECAM quirk
|
||||
// ("linux-ecam-quirk-id"): its config space only looks like ECAM (the
|
||||
// Raspberry Pi 4's BCM2711 shows one device at a time through an index
|
||||
// register). Copies the quirk id into `id`.
|
||||
bool RootBridgeEcamQuirk(char* id, int idSize);
|
||||
|
||||
// Where the platform's interrupt lines are not wired by the hardware
|
||||
// (Arch::PlatformLinesAreFixed), route every function's INTx pin from
|
||||
// ACPI _PRT and store the assigned line in its Interrupt Line register.
|
||||
// Needs the AML namespace; run before drivers probe.
|
||||
void AssignInterruptLines();
|
||||
|
||||
// =========================================================================
|
||||
// Shared PCI constants
|
||||
// =========================================================================
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* PciAcpi.cpp
|
||||
* What the ACPI namespace says about PCI: INTx routing from _PRT on
|
||||
* platforms without PC interrupt lines, and config-space quirks
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Pci.hpp"
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <ACPI/AcpiDevices.hpp>
|
||||
#include <ACPI/GsiRouting.hpp>
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <ACPI/AML/AmlResource.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
// On x86 the firmware programs every PCI function's Interrupt Line register
|
||||
// with its I/O APIC input, and drivers hand that number to EnableLegacyIrq.
|
||||
// Elsewhere nothing fills it in, so this pass does: it finds each pin's
|
||||
// global system interrupt in the root bridge's _PRT (through PCI-to-PCI
|
||||
// bridges by the standard swizzle, and through PNP0C0F link devices' _CRS),
|
||||
// binds the GSI to a platform line (GsiRouting), and writes that line into
|
||||
// the register. The drivers then work unchanged.
|
||||
|
||||
using namespace Kt;
|
||||
using namespace Hal;
|
||||
|
||||
namespace Pci {
|
||||
|
||||
static constexpr uint16_t RegInterruptPin = 0x3D;
|
||||
|
||||
// Extended interrupt descriptor flags
|
||||
static constexpr uint8_t IrqFlagEdge = 1u << 0;
|
||||
static constexpr uint8_t IrqFlagActiveLow = 1u << 1;
|
||||
|
||||
struct RoutedGsi {
|
||||
uint32_t gsi;
|
||||
bool levelTriggered;
|
||||
bool activeLow;
|
||||
};
|
||||
|
||||
// Root-bus device and pin (0 = INTA) for a function, following
|
||||
// PCI-to-PCI bridges up: each bridge rotates the pins by the number of
|
||||
// the device behind it.
|
||||
static bool ResolveToRootBus(uint8_t bus, uint8_t& device, uint8_t& pin) {
|
||||
auto& devices = GetDevices();
|
||||
for (int depth = 0; bus != 0 && depth < 32; depth++) {
|
||||
const PciDevice* bridge = nullptr;
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const PciDevice& d = devices[i];
|
||||
if (IsBridge(d) &&
|
||||
LegacyRead8(d.Bus, d.Device, d.Function, (uint8_t)PCI_REG_SECONDARY_BUS) == bus) {
|
||||
bridge = &d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bridge == nullptr) return false;
|
||||
pin = (uint8_t)((pin + device) % 4);
|
||||
device = bridge->Device;
|
||||
bus = bridge->Bus;
|
||||
}
|
||||
return bus == 0;
|
||||
}
|
||||
|
||||
// The GSI a _PRT entry's source names: the source index itself when the
|
||||
// source is 0, else the interrupt of the link device it references.
|
||||
static bool ResolveSource(AML::Interpreter& interp, int32_t rootNode,
|
||||
const AML::Object& source, uint64_t sourceIndex, RoutedGsi& out) {
|
||||
if (source.Type == AML::ObjectType::Integer && source.Integer == 0) {
|
||||
out = RoutedGsi{ (uint32_t)sourceIndex, true, true };
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t link = interp.ResolveReference(source, rootNode);
|
||||
if (link < 0) return false;
|
||||
|
||||
AML::ResourceList resources;
|
||||
if (!AcpiDevices::EvaluateCrs(link, resources)) return false;
|
||||
|
||||
if (auto* ext = resources.FindFirst(AML::ResourceType::ExtendedIrq)) {
|
||||
out = RoutedGsi{ ext->ExtendedIrq.Interrupt,
|
||||
(ext->ExtendedIrq.Flags & IrqFlagEdge) == 0,
|
||||
(ext->ExtendedIrq.Flags & IrqFlagActiveLow) != 0 };
|
||||
return true;
|
||||
}
|
||||
if (auto* irq = resources.FindFirst(AML::ResourceType::Irq)) {
|
||||
out = RoutedGsi{ irq->Irq.Irq, true, true };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool LookupPrt(AML::Interpreter& interp, int32_t rootNode, AML::Object& prt,
|
||||
uint8_t device, uint8_t pin, RoutedGsi& out) {
|
||||
int count = interp.GetPackageCount(prt);
|
||||
for (int i = 0; i < count; i++) {
|
||||
AML::Object entry, address, entryPin, source, sourceIndex;
|
||||
if (!interp.GetPackageElement(prt, i, entry) || entry.Type != AML::ObjectType::Package) continue;
|
||||
if (!interp.GetPackageElement(entry, 0, address) ||
|
||||
!interp.GetPackageElement(entry, 1, entryPin) ||
|
||||
!interp.GetPackageElement(entry, 2, source) ||
|
||||
!interp.GetPackageElement(entry, 3, sourceIndex)) continue;
|
||||
|
||||
// Address: device in the high word, function 0xFFFF (any).
|
||||
if (((address.Integer >> 16) & 0xFFFF) != device || entryPin.Integer != pin) continue;
|
||||
return ResolveSource(interp, rootNode, source, sourceIndex.Integer, out);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool FindRootBridge(AcpiDevices::DeviceInfo& root) {
|
||||
return AcpiDevices::FindDeviceByHid("PNP0A08", root) ||
|
||||
AcpiDevices::FindDeviceByHid("PNP0A03", root);
|
||||
}
|
||||
|
||||
// Device properties (_DSD) come as { UUID, Package { Package { key,
|
||||
// value }, ... } }; only the key/value list is looked at.
|
||||
bool RootBridgeEcamQuirk(char* id, int idSize) {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return false;
|
||||
|
||||
AcpiDevices::DeviceInfo root{};
|
||||
if (!FindRootBridge(root)) return false;
|
||||
|
||||
AML::Object dsd, properties;
|
||||
if (!interp.EvaluateChild(root.NodeIndex, "_DSD", nullptr, 0, dsd) ||
|
||||
dsd.Type != AML::ObjectType::Package ||
|
||||
!interp.GetPackageElement(dsd, 1, properties) ||
|
||||
properties.Type != AML::ObjectType::Package) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char Key[] = "linux-ecam-quirk-id";
|
||||
int count = interp.GetPackageCount(properties);
|
||||
for (int i = 0; i < count; i++) {
|
||||
AML::Object pair, key, value;
|
||||
if (!interp.GetPackageElement(properties, i, pair) || pair.Type != AML::ObjectType::Package ||
|
||||
!interp.GetPackageElement(pair, 0, key) || key.Type != AML::ObjectType::String ||
|
||||
!interp.GetPackageElement(pair, 1, value) || value.Type != AML::ObjectType::String) {
|
||||
continue;
|
||||
}
|
||||
bool match = true;
|
||||
for (int c = 0; c < (int)sizeof(Key); c++) {
|
||||
if (key.String.Data[c] != Key[c]) { match = false; break; }
|
||||
}
|
||||
if (!match) continue;
|
||||
|
||||
int n = 0;
|
||||
for (; n + 1 < idSize && value.String.Data[n] != '\0'; n++) id[n] = value.String.Data[n];
|
||||
id[n] = '\0';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AssignInterruptLines() {
|
||||
if (Arch::PlatformLinesAreFixed()) return;
|
||||
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return;
|
||||
|
||||
AcpiDevices::DeviceInfo root{};
|
||||
if (!FindRootBridge(root)) return;
|
||||
|
||||
AML::Object prt;
|
||||
if (!interp.EvaluateChild(root.NodeIndex, "_PRT", nullptr, 0, prt) ||
|
||||
prt.Type != AML::ObjectType::Package) {
|
||||
KernelLogStream(WARNING, "PCI") << "No _PRT on " << root.Path << ": INTx unrouted";
|
||||
return;
|
||||
}
|
||||
|
||||
int routed = 0;
|
||||
|
||||
auto& devices = GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const PciDevice& dev = devices[i];
|
||||
uint8_t pinReg = LegacyRead8(dev.Bus, dev.Device, dev.Function, (uint8_t)RegInterruptPin);
|
||||
if (pinReg == 0 || pinReg > 4) continue;
|
||||
|
||||
uint8_t device = dev.Device;
|
||||
uint8_t pin = pinReg - 1;
|
||||
RoutedGsi gsi{};
|
||||
if (!ResolveToRootBus(dev.Bus, device, pin) ||
|
||||
!LookupPrt(interp, root.NodeIndex, prt, device, pin, gsi)) {
|
||||
KernelLogStream(WARNING, "PCI") << "No _PRT route for " << base::hex
|
||||
<< (uint64_t)dev.Bus << ":" << (uint64_t)dev.Device << "."
|
||||
<< (uint64_t)dev.Function << " INT" << (char)('A' + pinReg - 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t line = GsiRouting::LineForGsi(gsi.gsi, gsi.levelTriggered, gsi.activeLow);
|
||||
if (line == 0) continue;
|
||||
|
||||
LegacyWrite8(dev.Bus, dev.Device, dev.Function, (uint8_t)PCI_REG_INTERRUPT, line);
|
||||
routed++;
|
||||
KernelLogStream(DEBUG, "PCI") << base::hex << (uint64_t)dev.Bus << ":"
|
||||
<< (uint64_t)dev.Device << "." << (uint64_t)dev.Function << " INT"
|
||||
<< (char)('A' + pinReg - 1) << " -> GSI " << base::dec << (uint64_t)gsi.gsi
|
||||
<< " (line " << (uint64_t)line << ")";
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "PCI") << "Routed INTx for " << base::dec << (uint64_t)routed
|
||||
<< " function(s) from _PRT";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -179,7 +179,9 @@ namespace Sched {
|
||||
}
|
||||
|
||||
static bool WriteUser64(uint64_t pageTableRoot, uint64_t virtualAddress, uint64_t value) {
|
||||
uint64_t physBase = Memory::VMM::Paging::GetPhysAddr(pageTableRoot, virtualAddress);
|
||||
// Translate the page, not the address: aarch64 returns the in-page
|
||||
// offset too, which the line below would then add a second time.
|
||||
uint64_t physBase = Memory::VMM::Paging::GetPhysAddr(pageTableRoot, virtualAddress & ~0xFFFULL);
|
||||
if (physBase == 0) {
|
||||
Kt::KernelLogStream(Kt::ERROR, "ELF") << "Relocation target not mapped";
|
||||
return false;
|
||||
@@ -528,6 +530,7 @@ namespace Sched {
|
||||
Fs::Vfs::CloseBackendFile(file);
|
||||
return 0;
|
||||
}
|
||||
Arch::SyncInstructionCache(page, 0x1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,6 +721,7 @@ namespace Sched {
|
||||
uint8_t* dst = (uint8_t*)Memory::HHDM(physAddr) + pageDataOffset;
|
||||
uint8_t* src = fileData + srcOffset;
|
||||
memcpy(dst, src, copySize);
|
||||
Arch::SyncInstructionCache(dst, copySize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,6 +442,7 @@ namespace Sched {
|
||||
|
||||
// User code that calls SYS_EXIT(0)
|
||||
Arch::WriteExitStub((uint8_t*)stubPage);
|
||||
Arch::SyncInstructionCache(stubPage, 0x1000);
|
||||
}
|
||||
|
||||
// Returning from the entry function lands in the exit stub.
|
||||
|
||||
@@ -37,6 +37,14 @@ namespace Kt {
|
||||
// ANSI escape sequence filter state
|
||||
static bool g_ansiEscape = false;
|
||||
|
||||
static SerialSink g_serialSink = nullptr;
|
||||
|
||||
static void SerialPutchar(char c) {
|
||||
if (g_serialSink == nullptr) return;
|
||||
if (c == '\n') g_serialSink('\r');
|
||||
g_serialSink(c);
|
||||
}
|
||||
|
||||
static void RingBufferAppend(char c) {
|
||||
// Strip ANSI escape sequences
|
||||
if (c == '\033') {
|
||||
@@ -209,18 +217,20 @@ namespace Kt {
|
||||
// framebuffer terminal below; storing it in the ring makes file
|
||||
// consumers treat one logical newline as two line breaks.
|
||||
RingBufferAppend(c);
|
||||
SerialPutchar(c);
|
||||
|
||||
if (g_suppressKernelLog) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Once a graphical app takes over, suppress ALL flanterm writes
|
||||
// (SYS_PRINT from user processes, etc.) to avoid painting text
|
||||
// over the GUI framebuffer.
|
||||
if (g_suppressKernelLog && g_kernelLogDepth == 0) {
|
||||
if (g_suppressKernelLog) {
|
||||
return;
|
||||
}
|
||||
SerialPutchar(c);
|
||||
}
|
||||
|
||||
if (c == '\n') {
|
||||
flanterm_write(ctx, "\r\n", 2);
|
||||
@@ -235,6 +245,18 @@ namespace Kt {
|
||||
}
|
||||
}
|
||||
|
||||
void AttachSerialConsole(SerialSink sink) {
|
||||
g_termLock.Acquire();
|
||||
uint64_t start = g_klogCount < KLOG_BUF_SIZE ? 0 : g_klogHead;
|
||||
for (uint64_t i = 0; i < g_klogCount; i++) {
|
||||
char c = g_klogBuf[(start + i) % KLOG_BUF_SIZE];
|
||||
if (c == '\n') sink('\r');
|
||||
sink(c);
|
||||
}
|
||||
g_serialSink = sink;
|
||||
g_termLock.Release();
|
||||
}
|
||||
|
||||
void SuppressKernelLog() {
|
||||
g_suppressKernelLog = true;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,12 @@ namespace Kt
|
||||
void Putchar(char c);
|
||||
void Print(const char *text);
|
||||
|
||||
// Mirror console output to a polled serial port (Drivers/Serial). The
|
||||
// sink receives the whole kernel log, also once the desktop suppresses
|
||||
// it on screen; what was logged before attaching is replayed first.
|
||||
using SerialSink = void (*)(char c);
|
||||
void AttachSerialConsole(SerialSink sink);
|
||||
|
||||
// Terminal mutex - protects flanterm from concurrent CPU access.
|
||||
// Uses Mutex (not Spinlock) to keep interrupts enabled while held,
|
||||
// preventing dropped PS/2 mouse/keyboard bytes during log output.
|
||||
|
||||
+59
-34
@@ -2,20 +2,19 @@
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
# Target architecture.
|
||||
ARCH := x86_64
|
||||
# Target architecture (ARCH=x86_64 or aarch64) and the cross compiler
|
||||
# from toolchain/local/.
|
||||
include mk/arch.mk
|
||||
|
||||
# Auto-detect cross compiler from toolchain/local/.
|
||||
TOOLCHAIN_PREFIX := $(shell cd .. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
CC = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
CXX = $(error $(TOOLCHAIN_MISSING))
|
||||
CC = $(error $(TOOLCHAIN_MISSING))
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
CC := $(TOOLCHAIN_PREFIX)gcc
|
||||
endif
|
||||
|
||||
# Compiler flags. The x86_64-montauk toolchain supplies the target ABI
|
||||
# Compiler flags. The <arch>-montauk toolchain supplies the target ABI
|
||||
# (static ET_EXEC, 4 KiB pages, no red zone) and header paths; system
|
||||
# programs additionally avoid FP/SIMD and their own runtime (-nostdlib).
|
||||
override CXXFLAGS := \
|
||||
@@ -30,10 +29,7 @@ override CXXFLAGS := \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-mno-80387 \
|
||||
-mno-mmx \
|
||||
-mno-sse \
|
||||
-mno-sse2 \
|
||||
$(ARCH_NOFP_FLAGS) \
|
||||
-I include
|
||||
|
||||
# Linker flags: freestanding static ELF.
|
||||
@@ -43,13 +39,13 @@ override LDFLAGS := \
|
||||
-T link.ld
|
||||
|
||||
# Output directory.
|
||||
BINDIR := bin
|
||||
BINDIR := bin$(ARCH_SUFFIX)
|
||||
|
||||
# Discover all programs (each subdirectory under src/ is a program).
|
||||
PROGRAMS := $(notdir $(wildcard src/*))
|
||||
|
||||
# Programs with custom Makefiles (built separately).
|
||||
CUSTOM_BUILDS := 2048 files settings fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver terminal syslog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad sshd rtlsdr sdr
|
||||
CUSTOM_BUILDS := 2048 files settings fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver terminal syslog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad sshd rtlsdr sdr firstboot
|
||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||
|
||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||
@@ -96,20 +92,42 @@ WPDIR := data/wallpapers
|
||||
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
||||
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
|
||||
|
||||
.PHONY: all clean 2048 files settings fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls rust
|
||||
.PHONY: all clean 2048 files settings fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls rust firstboot
|
||||
|
||||
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) files settings fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad rust $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
|
||||
# The on-OS SDK, tcc (an x86 code generator) and the Rust programs exist
|
||||
# for x86_64 only.
|
||||
ifneq ($(ARCH),x86_64)
|
||||
ARCH_SKIP := devkit tcc rust
|
||||
endif
|
||||
|
||||
ALL_TARGETS := bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) files settings fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad rust $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
|
||||
|
||||
# The Raspberry Pi SD image's first-boot program (built outside bin/, see
|
||||
# src/firstboot/Makefile); the SD image is aarch64 only.
|
||||
ifeq ($(ARCH),aarch64)
|
||||
ALL_TARGETS += firstboot
|
||||
endif
|
||||
|
||||
all: $(filter-out $(ARCH_SKIP),$(ALL_TARGETS))
|
||||
|
||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/$(ARCH)/include -isystem $(abspath include/libc)
|
||||
ifeq ($(ARCH),x86_64)
|
||||
BEARSSL_ARCH_FLAGS := -m64 -march=x86-64 -mno-80387 -mno-mmx -mno-red-zone -mcmodel=small
|
||||
else
|
||||
BEARSSL_ARCH_FLAGS := $(ARCH_CFLAGS)
|
||||
endif
|
||||
BEARSSL_LIB := lib/bearssl/build$(ARCH_SUFFIX)/libbearssl.a
|
||||
|
||||
lib/bearssl/build/libbearssl.a:
|
||||
$(BEARSSL_LIB):
|
||||
$(MAKE) -C lib/bearssl CONF=MontaukOS \
|
||||
BEARSSL_CC="$(CC)" \
|
||||
BEARSSL_AR="$(TOOLCHAIN_PREFIX)ar" \
|
||||
BUILD=build$(ARCH_SUFFIX) \
|
||||
BEARSSL_ARCH_FLAGS="$(BEARSSL_ARCH_FLAGS)" \
|
||||
BEARSSL_INCLUDES="$(BEARSSL_INCLUDES)"
|
||||
|
||||
bearssl: lib/bearssl/build/libbearssl.a
|
||||
bearssl: $(BEARSSL_LIB)
|
||||
|
||||
# Build shared libc static library.
|
||||
#
|
||||
@@ -120,11 +138,12 @@ bearssl: lib/bearssl/build/libbearssl.a
|
||||
# under -j and silently leaves programs linked against a stale libc.
|
||||
# Grouped target (&:, GNU make 4.3+): one sub-make produces all of them.
|
||||
LIBC_DIR := lib/libc
|
||||
LIBC_LIB := $(LIBC_DIR)/liblibc.a
|
||||
LIBC_LIB := $(LIBC_DIR)/$(ARCH_OUT)liblibc.a
|
||||
LIBC_SRCS := $(wildcard $(LIBC_DIR)/*.c $(LIBC_DIR)/*.h $(LIBC_DIR)/*.S \
|
||||
$(LIBC_DIR)/crt/*.c) $(LIBC_DIR)/Makefile
|
||||
LIBC_OUTPUTS := $(LIBC_LIB) $(LIBC_DIR)/liblibc-full.a \
|
||||
$(LIBC_DIR)/crt1.o $(LIBC_DIR)/crti.o $(LIBC_DIR)/crtn.o
|
||||
LIBC_OUTPUTS := $(LIBC_LIB) $(LIBC_DIR)/$(ARCH_OUT)liblibc-full.a \
|
||||
$(LIBC_DIR)/$(ARCH_OUT)crt1.o $(LIBC_DIR)/$(ARCH_OUT)crti.o \
|
||||
$(LIBC_DIR)/$(ARCH_OUT)crtn.o
|
||||
|
||||
$(LIBC_OUTPUTS) &: $(LIBC_SRCS)
|
||||
$(MAKE) -C $(LIBC_DIR)
|
||||
@@ -250,7 +269,7 @@ terminal: libc
|
||||
$(MAKE) -C src/terminal
|
||||
|
||||
# Build kernel log standalone GUI tool (depends on libc).
|
||||
syslog: libc
|
||||
syslog: libc libloader
|
||||
$(MAKE) -C src/syslog
|
||||
|
||||
# Build process manager standalone GUI tool (depends on libc).
|
||||
@@ -281,8 +300,11 @@ timezone: libc
|
||||
login: bearssl libc
|
||||
$(MAKE) -C src/login
|
||||
|
||||
firstboot: libc
|
||||
$(MAKE) -C src/firstboot
|
||||
|
||||
# Build shell via its own Makefile (multi-file build).
|
||||
shell:
|
||||
shell: libc
|
||||
$(MAKE) -C src/shell
|
||||
|
||||
# Build desktop via its own Makefile (depends on libc, libjpeg, bearssl for user.h, dialogs, and registry config files).
|
||||
@@ -291,11 +313,11 @@ desktop: libc libjpeg bearssl libloader dialogs configs
|
||||
|
||||
# Copy SVG icons for the desktop into bin/os/icons/.
|
||||
icons:
|
||||
../scripts/copy_icons.sh
|
||||
PROGRAMS_BIN=$(BINDIR) ../scripts/copy_icons.sh
|
||||
|
||||
# Copy TTF fonts for the desktop into bin/os/fonts/.
|
||||
fonts:
|
||||
../scripts/copy_fonts.sh
|
||||
PROGRAMS_BIN=$(BINDIR) ../scripts/copy_fonts.sh
|
||||
|
||||
# Copy default system config files into bin/config/.
|
||||
configs: $(CONFIGDST)
|
||||
@@ -400,7 +422,7 @@ crashpad: libc
|
||||
|
||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||
install-apps: 2048 files settings paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver terminal syslog procmgr powermgr calculator charmap screenshot texteditor mandelbrot printers timezone crashpad
|
||||
../scripts/install_apps.sh
|
||||
PROGRAMS_BIN=$(BINDIR) ../scripts/install_apps.sh
|
||||
|
||||
# Copy man pages into bin/os/man/ so mkramdisk.sh picks them up.
|
||||
$(BINDIR)/os/man/%: $(MANDIR)/%
|
||||
@@ -446,14 +468,16 @@ $(BINDIR)/os/wallpapers/%: $(WPDIR)/%
|
||||
# (montauk/capabilities.h, Api/Syscall.hpp, ...) rebuilds every program that
|
||||
# uses it. A missing depfile matches the empty rule below, which forces a
|
||||
# rebuild; that is how programs built before this tracking existed catch up.
|
||||
$(BINDIR)/os/%.elf: src/%/main.cpp link.ld GNUmakefile $(LIBC_LIB) obj/%/main.elf.d
|
||||
mkdir -p $(BINDIR)/os obj/$*
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -MT $@ -MF obj/$*/main.elf.d -c src/$*/main.cpp -o obj/$*/main.o
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) obj/$*/main.o $(LIBC_LIB) -o $@
|
||||
OBJ := obj$(ARCH_SUFFIX)
|
||||
|
||||
obj/%/main.elf.d: ;
|
||||
.PRECIOUS: obj/%/main.elf.d
|
||||
-include $(wildcard obj/*/main.elf.d)
|
||||
$(BINDIR)/os/%.elf: src/%/main.cpp link.ld GNUmakefile $(LIBC_LIB) $(OBJ)/%/main.elf.d
|
||||
mkdir -p $(BINDIR)/os $(OBJ)/$*
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -MT $@ -MF $(OBJ)/$*/main.elf.d -c src/$*/main.cpp -o $(OBJ)/$*/main.o
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJ)/$*/main.o $(LIBC_LIB) -o $@
|
||||
|
||||
$(OBJ)/%/main.elf.d: ;
|
||||
.PRECIOUS: $(OBJ)/%/main.elf.d
|
||||
-include $(wildcard $(OBJ)/*/main.elf.d)
|
||||
|
||||
# ============================================================================
|
||||
# Native development kit (Montauk SDK): binutils built for MontaukOS
|
||||
@@ -524,7 +548,7 @@ else
|
||||
endif
|
||||
|
||||
clean:
|
||||
rm -rf $(BINDIR) obj $(RUST_DIR)/target
|
||||
rm -rf $(BINDIR) $(OBJ) $(RUST_DIR)/target
|
||||
$(MAKE) -C lib/bearssl clean
|
||||
$(MAKE) -C lib/libc clean
|
||||
$(MAKE) -C lib/libjpeg clean
|
||||
@@ -532,6 +556,7 @@ clean:
|
||||
$(MAKE) -C lib/tls clean
|
||||
$(MAKE) -C src/fetch clean
|
||||
$(MAKE) -C src/login clean
|
||||
$(MAKE) -C src/firstboot clean
|
||||
$(MAKE) -C src/shell clean
|
||||
$(MAKE) -C src/desktop clean
|
||||
$(MAKE) -C src/wikipedia clean
|
||||
|
||||
@@ -312,6 +312,11 @@ namespace montauk::abi {
|
||||
static constexpr uint64_t SYS_WIN_DRAG_FORMATS = 206; // (serial, TransferFormatInfo*, max) -> count
|
||||
static constexpr uint64_t SYS_WIN_DRAG_GET_DATA = 207; // (serial, mime, buf, max, uint32_t* len) -> status
|
||||
|
||||
/* Storage.hpp -- grow a mounted volume (and its MBR partition) to fill the
|
||||
free space after it. apply=0 only reports; apply=1 does one bounded slice
|
||||
and returns 1 while more remains, 0 once done. CAP_STORAGE_ADMIN. */
|
||||
static constexpr uint64_t SYS_FS_GROW = 208; // (drive, FsGrowStatus*, apply) -> 1 / 0 / status
|
||||
|
||||
/* Kernel-owned process capabilities. User identities may namespace
|
||||
per-user resources, but never participate in authorization decisions. */
|
||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||
@@ -838,7 +843,7 @@ namespace montauk::abi {
|
||||
|
||||
struct DiskInfo {
|
||||
uint8_t port; // block device index
|
||||
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage
|
||||
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage, 5=SD card
|
||||
uint8_t sataGen; // SATA gen (1/2/3)
|
||||
uint8_t _pad0;
|
||||
uint64_t sectorCount; // Total user-addressable sectors
|
||||
@@ -920,6 +925,14 @@ namespace montauk::abi {
|
||||
char label[32]; // volume label
|
||||
};
|
||||
|
||||
struct FsGrowStatus {
|
||||
uint64_t fsBytes; // filesystem size now
|
||||
uint64_t partBytes; // partition size now
|
||||
uint64_t targetBytes; // filesystem size once grown; == fsBytes if it cannot grow
|
||||
uint32_t stepsDone; // progress of the grow, out of stepsTotal
|
||||
uint32_t stepsTotal;
|
||||
};
|
||||
|
||||
// Bluetooth scan result (returned by SYS_BTSCAN)
|
||||
struct BtScanResult {
|
||||
uint8_t bdAddr[6];
|
||||
|
||||
@@ -13,6 +13,18 @@
|
||||
|
||||
namespace gui {
|
||||
|
||||
// Drain write-combining stores to the scanout buffer before it is flipped
|
||||
// or composited by someone else.
|
||||
inline void wc_store_fence() {
|
||||
#if defined(__x86_64__)
|
||||
asm volatile("sfence" ::: "memory");
|
||||
#elif defined(__aarch64__)
|
||||
asm volatile("dsb st" ::: "memory");
|
||||
#else
|
||||
#error "unsupported architecture"
|
||||
#endif
|
||||
}
|
||||
|
||||
class Framebuffer {
|
||||
uint32_t* hw_fb;
|
||||
uint32_t* hw_fb2; // second scanout buffer (page flip), nullptr if unsupported
|
||||
@@ -322,12 +334,12 @@ public:
|
||||
montauk::memcpy(mirror, src, row_bytes);
|
||||
}
|
||||
}
|
||||
if (hw_mirror) asm volatile("sfence" ::: "memory");
|
||||
if (hw_mirror) wc_store_fence();
|
||||
|
||||
if (hw_fb2) {
|
||||
// Wait for the latch (vsync): the buffer we just left becomes
|
||||
// safe to draw into only once the new one is actually scanning.
|
||||
asm volatile("sfence" ::: "memory");
|
||||
wc_store_fence();
|
||||
int64_t presented = montauk::fb_flip(hw_next, 1);
|
||||
if (presented == hw_next) {
|
||||
hw_next ^= 1;
|
||||
@@ -359,7 +371,7 @@ public:
|
||||
montauk::memcpy(mirror, src, row_bytes);
|
||||
}
|
||||
}
|
||||
asm volatile("sfence" ::: "memory");
|
||||
wc_store_fence();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +231,7 @@ extern "C" {
|
||||
#define MTK_SYS_WIN_DRAG_BEGIN 205
|
||||
#define MTK_SYS_WIN_DRAG_FORMATS 206
|
||||
#define MTK_SYS_WIN_DRAG_GET_DATA 207
|
||||
#define MTK_SYS_FS_GROW 208
|
||||
/* @SYSCALLS-END */
|
||||
|
||||
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
||||
@@ -419,6 +420,8 @@ typedef struct {
|
||||
Raw syscall wrappers
|
||||
==================================================================== */
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
static inline long _mtk_syscall0(long nr) {
|
||||
long ret;
|
||||
__asm__ volatile("syscall" : "=a"(ret) : "a"(nr)
|
||||
@@ -491,6 +494,66 @@ static inline long _mtk_syscall5(long nr, long a1, long a2, long a3, long a4, lo
|
||||
return ret;
|
||||
}
|
||||
|
||||
#elif defined(__aarch64__)
|
||||
|
||||
// AArch64: number in x8, arguments in x0-x5, `svc #0`; the kernel
|
||||
// preserves every register except x0, which carries the result.
|
||||
static inline long _mtk_syscall0(long nr) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0");
|
||||
__asm__ volatile("svc #0" : "=r"(x0) : "r"(x8) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _mtk_syscall1(long nr, long a1) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _mtk_syscall2(long nr, long a1, long a2) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
register long x1 __asm__("x1") = a2;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _mtk_syscall3(long nr, long a1, long a2, long a3) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
register long x1 __asm__("x1") = a2;
|
||||
register long x2 __asm__("x2") = a3;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _mtk_syscall4(long nr, long a1, long a2, long a3, long a4) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
register long x1 __asm__("x1") = a2;
|
||||
register long x2 __asm__("x2") = a3;
|
||||
register long x3 __asm__("x3") = a4;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _mtk_syscall5(long nr, long a1, long a2, long a3, long a4, long a5) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
register long x1 __asm__("x1") = a2;
|
||||
register long x2 __asm__("x2") = a3;
|
||||
register long x3 __asm__("x3") = a4;
|
||||
register long x4 __asm__("x4") = a5;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
#else
|
||||
#error "unsupported architecture"
|
||||
#endif
|
||||
|
||||
/* ====================================================================
|
||||
Process management
|
||||
==================================================================== */
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef unsigned long jmp_buf[8];
|
||||
#if defined(__aarch64__)
|
||||
typedef unsigned long jmp_buf[21]; /* x19-x30, sp, d8-d15 */
|
||||
#else
|
||||
typedef unsigned long jmp_buf[8]; /* rbx, rbp, r12-r15, rsp, rip */
|
||||
#endif
|
||||
|
||||
int setjmp(jmp_buf env);
|
||||
void longjmp(jmp_buf env, int val) __attribute__((noreturn));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* syscall.h
|
||||
* MontaukOS program-side syscall wrappers using SYSCALL instruction
|
||||
* MontaukOS program-side syscall wrappers (x86_64 SYSCALL, AArch64 SVC)
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace montauk {
|
||||
|
||||
// ---- Raw SYSCALL wrappers ----
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
// The SYSCALL handler does not restore RDI, RSI, RDX, R10, R8, R9
|
||||
// (they are skipped on the return path). We move arguments into the
|
||||
// correct registers inside the asm block and list ALL argument
|
||||
@@ -108,6 +110,79 @@ namespace montauk {
|
||||
return ret;
|
||||
}
|
||||
|
||||
#elif defined(__aarch64__)
|
||||
|
||||
// AArch64: number in x8, arguments in x0-x5, `svc #0`; the kernel
|
||||
// preserves every register except x0, which carries the result.
|
||||
|
||||
inline int64_t syscall0(uint64_t nr) {
|
||||
register uint64_t x8 asm("x8") = nr;
|
||||
register uint64_t x0 asm("x0");
|
||||
asm volatile("svc #0" : "=r"(x0) : "r"(x8) : "memory");
|
||||
return (int64_t)x0;
|
||||
}
|
||||
|
||||
inline int64_t syscall1(uint64_t nr, uint64_t a1) {
|
||||
register uint64_t x8 asm("x8") = nr;
|
||||
register uint64_t x0 asm("x0") = a1;
|
||||
asm volatile("svc #0" : "+r"(x0) : "r"(x8) : "memory");
|
||||
return (int64_t)x0;
|
||||
}
|
||||
|
||||
inline int64_t syscall2(uint64_t nr, uint64_t a1, uint64_t a2) {
|
||||
register uint64_t x8 asm("x8") = nr;
|
||||
register uint64_t x0 asm("x0") = a1;
|
||||
register uint64_t x1 asm("x1") = a2;
|
||||
asm volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1) : "memory");
|
||||
return (int64_t)x0;
|
||||
}
|
||||
|
||||
inline int64_t syscall3(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3) {
|
||||
register uint64_t x8 asm("x8") = nr;
|
||||
register uint64_t x0 asm("x0") = a1;
|
||||
register uint64_t x1 asm("x1") = a2;
|
||||
register uint64_t x2 asm("x2") = a3;
|
||||
asm volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2) : "memory");
|
||||
return (int64_t)x0;
|
||||
}
|
||||
|
||||
inline int64_t syscall4(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4) {
|
||||
register uint64_t x8 asm("x8") = nr;
|
||||
register uint64_t x0 asm("x0") = a1;
|
||||
register uint64_t x1 asm("x1") = a2;
|
||||
register uint64_t x2 asm("x2") = a3;
|
||||
register uint64_t x3 asm("x3") = a4;
|
||||
asm volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3) : "memory");
|
||||
return (int64_t)x0;
|
||||
}
|
||||
|
||||
inline int64_t syscall5(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) {
|
||||
register uint64_t x8 asm("x8") = nr;
|
||||
register uint64_t x0 asm("x0") = a1;
|
||||
register uint64_t x1 asm("x1") = a2;
|
||||
register uint64_t x2 asm("x2") = a3;
|
||||
register uint64_t x3 asm("x3") = a4;
|
||||
register uint64_t x4 asm("x4") = a5;
|
||||
asm volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4) : "memory");
|
||||
return (int64_t)x0;
|
||||
}
|
||||
|
||||
inline int64_t syscall6(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5, uint64_t a6) {
|
||||
register uint64_t x8 asm("x8") = nr;
|
||||
register uint64_t x0 asm("x0") = a1;
|
||||
register uint64_t x1 asm("x1") = a2;
|
||||
register uint64_t x2 asm("x2") = a3;
|
||||
register uint64_t x3 asm("x3") = a4;
|
||||
register uint64_t x4 asm("x4") = a5;
|
||||
register uint64_t x5 asm("x5") = a6;
|
||||
asm volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5) : "memory");
|
||||
return (int64_t)x0;
|
||||
}
|
||||
|
||||
#else
|
||||
#error "unsupported architecture"
|
||||
#endif
|
||||
|
||||
// ---- Typed wrappers ----
|
||||
|
||||
// Process
|
||||
@@ -579,6 +654,14 @@ namespace montauk {
|
||||
inline int fs_sync() {
|
||||
return (int)syscall0(montauk::abi::SYS_FS_SYNC);
|
||||
}
|
||||
// Grow mounted volume `drive` (and its MBR partition) to fill the free
|
||||
// space after it. apply=false only fills `status`; apply=true does one
|
||||
// slice of the work and returns 1 while more remains, 0 when finished,
|
||||
// or a negative status.
|
||||
inline int fs_grow(int drive, montauk::abi::FsGrowStatus* status, bool apply) {
|
||||
return (int)syscall3(montauk::abi::SYS_FS_GROW, (uint64_t)drive,
|
||||
(uint64_t)status, apply ? 1 : 0);
|
||||
}
|
||||
inline int fs_format(const montauk::abi::FsFormatParams* params) {
|
||||
return (int)syscall1(montauk::abi::SYS_FSFORMAT, (uint64_t)params);
|
||||
}
|
||||
|
||||
+17
-18
@@ -6,7 +6,7 @@ MAKEFLAGS += -rR
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CC := $(TOOLCHAIN_PREFIX)gcc
|
||||
AR := $(TOOLCHAIN_PREFIX)ar
|
||||
@@ -20,7 +20,7 @@ endif
|
||||
# ---- Paths ----
|
||||
|
||||
LIBC_INC := ../../include/libc
|
||||
OBJDIR := obj
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
CRTDIR := crt
|
||||
ASM_OBJS := $(OBJDIR)/setjmp.o
|
||||
|
||||
@@ -38,12 +38,8 @@ CFLAGS := \
|
||||
-fno-PIC \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
$(ARCH_CFLAGS) \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-isystem $(LIBC_INC) \
|
||||
-isystem $(shell $(CC) -print-file-name=include)
|
||||
|
||||
@@ -55,18 +51,16 @@ CRT_CFLAGS := \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-mno-red-zone \
|
||||
$(ARCH_CFLAGS) \
|
||||
-fno-tree-loop-distribute-patterns \
|
||||
-isystem $(LIBC_INC) \
|
||||
-isystem $(shell $(CC) -print-file-name=include)
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := liblibc.a
|
||||
SDK_TARGET := liblibc-full.a
|
||||
CRT_OBJS := crt1.o crti.o crtn.o
|
||||
TARGET := $(ARCH_OUT)liblibc.a
|
||||
SDK_TARGET := $(ARCH_OUT)liblibc-full.a
|
||||
CRT_OBJS := $(ARCH_OUT)crt1.o $(ARCH_OUT)crti.o $(ARCH_OUT)crtn.o
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
@@ -77,6 +71,7 @@ all: $(TARGET) $(SDK_TARGET) $(CRT_OBJS)
|
||||
# a float leave the decimal-conversion and pow/exp/log code behind. Programs
|
||||
# that do need %e/%f/%g must link with -Wl,-u,_pf_putfloat to force it in.
|
||||
$(TARGET): $(OBJDIR)/libc.o $(OBJDIR)/printf_float.o $(ASM_OBJS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(AR) rcs $@ $^
|
||||
|
||||
$(OBJDIR)/libc.o: libc.c printf_internal.h Makefile
|
||||
@@ -87,7 +82,7 @@ $(OBJDIR)/printf_float.o: printf_float.c printf_internal.h Makefile
|
||||
@mkdir -p $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
$(OBJDIR)/setjmp.o: setjmp.S Makefile
|
||||
$(OBJDIR)/setjmp.o: arch/setjmp-$(ARCH).S Makefile
|
||||
@mkdir -p $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
@@ -97,18 +92,22 @@ $(OBJDIR)/setjmp.o: setjmp.S Makefile
|
||||
# compiled ON MontaukOS. Function-level --gc-sections still applies to the
|
||||
# rest; only the float path is pinned. Image size is not a concern here.
|
||||
$(SDK_TARGET): $(OBJDIR)/libc-merged.o $(ASM_OBJS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(AR) rcs $@ $^
|
||||
|
||||
$(OBJDIR)/libc-merged.o: $(OBJDIR)/libc.o $(OBJDIR)/printf_float.o
|
||||
$(LD) -r -o $@ $(OBJDIR)/libc.o $(OBJDIR)/printf_float.o
|
||||
|
||||
crt1.o: $(CRTDIR)/crt1.c Makefile
|
||||
$(ARCH_OUT)crt1.o: $(CRTDIR)/crt1.c Makefile
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(CRT_CFLAGS) -c $< -o $@
|
||||
|
||||
crti.o: $(CRTDIR)/crti.c Makefile
|
||||
$(ARCH_OUT)crti.o: $(CRTDIR)/crti.c Makefile
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(CRT_CFLAGS) -c $< -o $@
|
||||
|
||||
crtn.o: $(CRTDIR)/crtn.c Makefile
|
||||
$(ARCH_OUT)crtn.o: $(CRTDIR)/crtn.c Makefile
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(CRT_CFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* setjmp-aarch64.S
|
||||
* setjmp/longjmp for AArch64 MontaukOS userspace
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
/* jmp_buf: x19-x28, x29 (fp), x30 (lr), sp, d8-d15 -- everything AAPCS64
|
||||
requires a callee to preserve. */
|
||||
|
||||
.text
|
||||
|
||||
.global setjmp
|
||||
.type setjmp, %function
|
||||
setjmp:
|
||||
stp x19, x20, [x0, #0]
|
||||
stp x21, x22, [x0, #16]
|
||||
stp x23, x24, [x0, #32]
|
||||
stp x25, x26, [x0, #48]
|
||||
stp x27, x28, [x0, #64]
|
||||
stp x29, x30, [x0, #80]
|
||||
mov x2, sp
|
||||
str x2, [x0, #96]
|
||||
stp d8, d9, [x0, #104]
|
||||
stp d10, d11, [x0, #120]
|
||||
stp d12, d13, [x0, #136]
|
||||
stp d14, d15, [x0, #152]
|
||||
mov w0, #0
|
||||
ret
|
||||
.size setjmp, . - setjmp
|
||||
|
||||
.global longjmp
|
||||
.type longjmp, %function
|
||||
longjmp:
|
||||
ldp x19, x20, [x0, #0]
|
||||
ldp x21, x22, [x0, #16]
|
||||
ldp x23, x24, [x0, #32]
|
||||
ldp x25, x26, [x0, #48]
|
||||
ldp x27, x28, [x0, #64]
|
||||
ldp x29, x30, [x0, #80]
|
||||
ldr x2, [x0, #96]
|
||||
mov sp, x2
|
||||
ldp d8, d9, [x0, #104]
|
||||
ldp d10, d11, [x0, #120]
|
||||
ldp d12, d13, [x0, #136]
|
||||
ldp d14, d15, [x0, #152]
|
||||
cmp w1, #0
|
||||
csinc w0, w1, wzr, ne
|
||||
ret
|
||||
.size longjmp, . - longjmp
|
||||
File renamed without changes.
@@ -8,6 +8,8 @@
|
||||
* calls main(), then exits with main()'s return code.
|
||||
*/
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
static inline long _sys1(long nr, long a1) {
|
||||
long ret;
|
||||
__asm__ volatile(
|
||||
@@ -31,6 +33,27 @@ static inline long _sys2(long nr, long a1, long a2) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
#elif defined(__aarch64__)
|
||||
|
||||
static inline long _sys1(long nr, long a1) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _sys2(long nr, long a1, long a2) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
register long x1 __asm__("x1") = a2;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
#else
|
||||
#error "unsupported architecture"
|
||||
#endif
|
||||
|
||||
#define SYS_EXIT 0
|
||||
#define SYS_GETARGS 25
|
||||
#define SYS_GETEXECPATH 151
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
Raw syscall wrappers (C versions matching kernel ABI)
|
||||
======================================================================== */
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
static inline long _mtk_syscall0(long nr) {
|
||||
long ret;
|
||||
__asm__ volatile("syscall" : "=a"(ret) : "a"(nr)
|
||||
@@ -89,6 +91,55 @@ static inline long _mtk_syscall4(long nr, long a1, long a2, long a3, long a4) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
#elif defined(__aarch64__)
|
||||
|
||||
// AArch64: number in x8, arguments in x0-x5, `svc #0`; the kernel
|
||||
// preserves every register except x0, which carries the result.
|
||||
static inline long _mtk_syscall0(long nr) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0");
|
||||
__asm__ volatile("svc #0" : "=r"(x0) : "r"(x8) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _mtk_syscall1(long nr, long a1) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _mtk_syscall2(long nr, long a1, long a2) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
register long x1 __asm__("x1") = a2;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _mtk_syscall3(long nr, long a1, long a2, long a3) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
register long x1 __asm__("x1") = a2;
|
||||
register long x2 __asm__("x2") = a3;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long _mtk_syscall4(long nr, long a1, long a2, long a3, long a4) {
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a1;
|
||||
register long x1 __asm__("x1") = a2;
|
||||
register long x2 __asm__("x2") = a3;
|
||||
register long x3 __asm__("x3") = a4;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3) : "memory");
|
||||
return x0;
|
||||
}
|
||||
|
||||
#else
|
||||
#error "unsupported architecture"
|
||||
#endif
|
||||
|
||||
/* Syscall numbers */
|
||||
#define SYS_EXIT 0
|
||||
#define SYS_YIELD 1
|
||||
|
||||
@@ -6,7 +6,7 @@ MAKEFLAGS += -rR
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CC := $(TOOLCHAIN_PREFIX)gcc
|
||||
AR := $(TOOLCHAIN_PREFIX)ar
|
||||
@@ -19,8 +19,7 @@ endif
|
||||
|
||||
LIBC_INC := ../../include/libc
|
||||
PROG_INC := ../../include
|
||||
OBJDIR := obj
|
||||
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
# ---- Compiler flags ----
|
||||
|
||||
CFLAGS := \
|
||||
@@ -36,25 +35,22 @@ CFLAGS := \
|
||||
-fno-PIC \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
$(ARCH_CFLAGS) \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(LIBC_INC) \
|
||||
-isystem $(shell $(CC) -print-file-name=include)
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := libjpeg.a
|
||||
TARGET := $(ARCH_OUT)libjpeg.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJDIR)/stb_image_impl.o
|
||||
@mkdir -p $(dir $@)
|
||||
$(AR) rcs $@ $^
|
||||
|
||||
$(OBJDIR)/stb_image_impl.o: stb_image_impl.c Makefile
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,7 @@ MAKEFLAGS += -rR
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CC := $(TOOLCHAIN_PREFIX)gcc
|
||||
AR := $(TOOLCHAIN_PREFIX)ar
|
||||
@@ -19,8 +19,7 @@ endif
|
||||
|
||||
LIBC_INC := ../../include/libc
|
||||
PROG_INC := ../../include
|
||||
OBJDIR := obj
|
||||
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
# ---- Compiler flags ----
|
||||
|
||||
CFLAGS := \
|
||||
@@ -37,25 +36,22 @@ CFLAGS := \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-fno-tree-loop-distribute-patterns \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
$(ARCH_CFLAGS) \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(LIBC_INC) \
|
||||
-isystem $(shell $(CC) -print-file-name=include)
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := libjpegwrite.a
|
||||
TARGET := $(ARCH_OUT)libjpegwrite.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJDIR)/stb_image_write_impl.o
|
||||
@mkdir -p $(dir $@)
|
||||
$(AR) rcs $@ $^
|
||||
|
||||
$(OBJDIR)/stb_image_write_impl.o: stb_image_write_impl.c Makefile
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,7 @@ MAKEFLAGS += -rR
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
AR := $(TOOLCHAIN_PREFIX)ar
|
||||
@@ -20,8 +20,7 @@ endif
|
||||
BEARSSL := ../bearssl
|
||||
LIBC_INC := ../../include/libc
|
||||
PROG_INC := ../../include
|
||||
OBJDIR := obj
|
||||
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
# ---- Compiler flags ----
|
||||
|
||||
CXXFLAGS := \
|
||||
@@ -39,29 +38,24 @@ CXXFLAGS := \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-mno-80387 \
|
||||
-mno-mmx \
|
||||
-mno-sse \
|
||||
-mno-sse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
$(ARCH_CFLAGS) \
|
||||
$(ARCH_NOFP_FLAGS) \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(LIBC_INC) \
|
||||
-I $(BEARSSL)/inc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/$(ARCH)/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/$(ARCH)/include
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := libtls.a
|
||||
TARGET := $(ARCH_OUT)libtls.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJDIR)/tls.o
|
||||
@mkdir -p $(dir $@)
|
||||
$(AR) rcs $@ $^
|
||||
|
||||
$(OBJDIR)/tls.o: tls.cpp Makefile
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+2
-3
@@ -3,11 +3,10 @@
|
||||
* Linker script for MontaukOS userspace programs
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
*
|
||||
* Programs are loaded at a standard user-space address.
|
||||
* Programs are loaded at a standard user-space address. The output
|
||||
* format is the toolchain's default (x86_64-montauk or aarch64-montauk).
|
||||
*/
|
||||
|
||||
OUTPUT_FORMAT(elf64-x86-64)
|
||||
|
||||
ENTRY(_start)
|
||||
|
||||
SECTIONS
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# arch.mk - Target architecture settings shared by every userspace Makefile
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
#
|
||||
# Included by programs/GNUmakefile and the per-program/per-library
|
||||
# Makefiles. Pass ARCH=aarch64 (the top-level build does) to build for ARM.
|
||||
#
|
||||
# x86_64 keeps the historical layout: objects in obj/, programs in bin/,
|
||||
# libraries next to their sources. Other architectures build side by side:
|
||||
# obj-<arch>/, bin-<arch>/ and lib/<name>/build-<arch>/.
|
||||
|
||||
ARCH ?= x86_64
|
||||
|
||||
ifeq ($(filter $(ARCH),x86_64 aarch64),)
|
||||
$(error Unsupported ARCH '$(ARCH)' for userspace)
|
||||
endif
|
||||
|
||||
MONTAUK_ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..)
|
||||
|
||||
ifeq ($(ARCH),x86_64)
|
||||
ARCH_SUFFIX :=
|
||||
ARCH_OUT :=
|
||||
else
|
||||
ARCH_SUFFIX := -$(ARCH)
|
||||
ARCH_OUT := build-$(ARCH)/
|
||||
endif
|
||||
|
||||
TOOLCHAIN_PREFIX := $(MONTAUK_ROOT)/toolchain/local/bin/$(ARCH)-montauk-
|
||||
TOOLCHAIN_MISSING := $(ARCH)-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh $(ARCH) first
|
||||
|
||||
# ARCH_FP_FLAGS: programs that use floating point / SIMD (GUI apps)
|
||||
# ARCH_NOFP_FLAGS: system programs that must not touch FP/SIMD registers
|
||||
# ARCH_CFLAGS: code-model flags the libraries pass explicitly
|
||||
ifeq ($(ARCH),x86_64)
|
||||
ARCH_FP_FLAGS := -msse -msse2
|
||||
ARCH_NOFP_FLAGS := -mno-80387 -mno-mmx -mno-sse -mno-sse2
|
||||
ARCH_CFLAGS := -m64 -march=x86-64 -mno-red-zone -mcmodel=small
|
||||
ARCH_LD_EMULATION := elf_x86_64
|
||||
else
|
||||
ARCH_FP_FLAGS :=
|
||||
ARCH_NOFP_FLAGS := -mgeneral-regs-only
|
||||
ARCH_CFLAGS := -mcmodel=small
|
||||
ARCH_LD_EMULATION := aarch64elf
|
||||
endif
|
||||
@@ -4,17 +4,17 @@
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
CXX = $(error $(TOOLCHAIN_MISSING))
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
BINDIR := ../../bin$(ARCH_SUFFIX)
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := \
|
||||
@@ -31,8 +31,7 @@ CXXFLAGS := \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-msse \
|
||||
-msse2 \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-MMD -MP \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc
|
||||
@@ -46,8 +45,8 @@ SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/apps/2048/2048.elf
|
||||
LOADER_LIB := $(LIBDIR)/libloader/liblibloader.a
|
||||
LIBC_LIB := $(LIBDIR)/libc/liblibc.a
|
||||
LOADER_LIB := $(LIBDIR)/libloader/$(ARCH_OUT)liblibloader.a
|
||||
LIBC_LIB := $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ MAKEFLAGS += -rR
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
CXX = $(error $(TOOLCHAIN_MISSING))
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
endif
|
||||
@@ -17,8 +17,8 @@ endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
BINDIR := ../../bin$(ARCH_SUFFIX)
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
LIBDIR := ../../lib
|
||||
|
||||
# ---- Compiler flags ----
|
||||
@@ -37,8 +37,7 @@ CXXFLAGS := \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-msse \
|
||||
-msse2 \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc
|
||||
|
||||
@@ -62,9 +61,9 @@ TARGET := $(BINDIR)/apps/audio/audio.elf
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/liblibc.a
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a
|
||||
mkdir -p $(BINDIR)/apps/audio
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
@@ -6,9 +6,9 @@ MAKEFLAGS += -rR
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
CXX = $(error $(TOOLCHAIN_MISSING))
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
endif
|
||||
@@ -17,8 +17,8 @@ endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
BINDIR := ../../bin$(ARCH_SUFFIX)
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
LIBDIR := ../../lib
|
||||
|
||||
# ---- Compiler flags ----
|
||||
@@ -37,8 +37,7 @@ CXXFLAGS := \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-msse \
|
||||
-msse2 \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc
|
||||
|
||||
@@ -62,9 +61,9 @@ TARGET := $(BINDIR)/apps/bluetooth/bluetooth.elf
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/liblibc.a
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a
|
||||
mkdir -p $(BINDIR)/apps/bluetooth
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
@@ -6,9 +6,9 @@ MAKEFLAGS += -rR
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
CXX = $(error $(TOOLCHAIN_MISSING))
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
endif
|
||||
@@ -17,8 +17,8 @@ endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
BINDIR := ../../bin$(ARCH_SUFFIX)
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
LIBDIR := ../../lib
|
||||
|
||||
# ---- Compiler flags ----
|
||||
@@ -37,8 +37,7 @@ CXXFLAGS := \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-msse \
|
||||
-msse2 \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc
|
||||
|
||||
@@ -62,9 +61,9 @@ TARGET := $(BINDIR)/apps/calculator/calculator.elf
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/liblibc.a
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a
|
||||
mkdir -p $(BINDIR)/apps/calculator
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
CXX = $(error $(TOOLCHAIN_MISSING))
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
BINDIR := ../../bin$(ARCH_SUFFIX)
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := \
|
||||
@@ -31,8 +31,7 @@ CXXFLAGS := \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-msse \
|
||||
-msse2 \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-MMD -MP \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc
|
||||
@@ -46,7 +45,7 @@ SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/apps/capabilities/capabilities.elf
|
||||
LIBS := $(LIBDIR)/libc/liblibc.a
|
||||
LIBS := $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ MAKEFLAGS += -rR
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
CXX = $(error $(TOOLCHAIN_MISSING))
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
endif
|
||||
@@ -17,8 +17,8 @@ endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
BINDIR := ../../bin$(ARCH_SUFFIX)
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
LIBDIR := ../../lib
|
||||
|
||||
# ---- Compiler flags ----
|
||||
@@ -37,8 +37,7 @@ CXXFLAGS := \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-msse \
|
||||
-msse2 \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc
|
||||
|
||||
@@ -62,9 +61,9 @@ TARGET := $(BINDIR)/apps/charmap/charmap.elf
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/liblibc.a
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a
|
||||
mkdir -p $(BINDIR)/apps/charmap
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
include ../../mk/arch.mk
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
CXX = $(error $(TOOLCHAIN_MISSING))
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
BINDIR := ../../bin$(ARCH_SUFFIX)
|
||||
OBJDIR := obj$(ARCH_SUFFIX)
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := \
|
||||
@@ -31,8 +31,7 @@ CXXFLAGS := \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-msse \
|
||||
-msse2 \
|
||||
$(ARCH_FP_FLAGS) \
|
||||
-MMD -MP \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
@@ -47,7 +46,7 @@ SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/apps/crashpad/crashpad.elf
|
||||
LIBS := $(LIBDIR)/libc/liblibc.a
|
||||
LIBS := $(LIBDIR)/libc/$(ARCH_OUT)liblibc.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
|
||||
Loaded 100 of 160 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user