Compare commits
14
Commits
aae4db01ad
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51a2b3a932 | ||
|
|
8b4a938164 | ||
|
|
be84506fdf | ||
|
|
559c3e2875 | ||
|
|
36291cd1cc | ||
|
|
b781cf8c97 | ||
|
|
7e036c94e7 | ||
|
|
1d00c1c016 | ||
|
|
3310b3d6de | ||
|
|
93d8becce3 | ||
|
|
ffee9002a6 | ||
|
|
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/
|
||||
|
||||
+119
-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,26 @@ 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
|
||||
|
||||
# Graphical imager: write montauk-rpi4.img to an SD card, or update the
|
||||
# installation on one in place (scripts/montauk-imager.py, docs/raspberry-pi.md).
|
||||
.PHONY: rpi4-imager
|
||||
rpi4-imager:
|
||||
./scripts/montauk-imager.py
|
||||
|
||||
$(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 +466,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,344 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
Or use the imager, which also updates a card in place:
|
||||
|
||||
```bash
|
||||
make rpi4-imager # = ./scripts/montauk-imager.py
|
||||
```
|
||||
|
||||
### The imager
|
||||
|
||||
`scripts/montauk-imager.py` is a Tk window (`python3-tk`) that lists the SD
|
||||
cards and USB drives attached (never the host's own disks) and what is on
|
||||
them, and either writes a fresh image or updates the MontaukOS on a card. It
|
||||
runs as the user and does the writing by starting itself again through
|
||||
`pkexec` (it asks for the password each time). The same operations work from
|
||||
a terminal: `list`, and as root `flash`, `update` and `expand`
|
||||
(`--help` for the options). Images can be raw or `.gz`, `.xz` or `.zip`
|
||||
compressed; **Build** runs `make ARCH=aarch64 rpi4-sd`.
|
||||
|
||||
- **Write a fresh image** erases the card, writes the image, clears the last
|
||||
MiB of the card (a backup GPT from an earlier OS) and reads the card back to
|
||||
verify it. *Expand* grows the root to fill the card right away instead of
|
||||
leaving it to `firstboot.elf`.
|
||||
- **Update** leaves the partition table alone and finds both partitions by
|
||||
label, so a root that has grown to fill the card (or was resized by hand)
|
||||
stays as it is. On the ESP every file is written except `RPI_EFI.fd`, which
|
||||
holds the firmware's own settings (*Update the UEFI firmware* replaces it);
|
||||
a changed `config.txt` is kept as `config.txt.bak`. On the root, `os/`,
|
||||
`apps/`, `sdk/`, `boot/` get the image's new and changed files; `config/`
|
||||
and `www/` only get files the card lacks, `users/` and `state/` are not
|
||||
touched. `init.toml` becomes the image's, keeping each service's `enabled`
|
||||
from the card and appending services only the card has; `capabilities.toml`
|
||||
becomes the image's plus grants only the card has (old copies: `.old`).
|
||||
`os/firstboot.elf` is only written if the card still has it, so a grown
|
||||
card does not grow again. *Remove system files the new image no longer
|
||||
ships* prunes `os/`, `apps/`, `sdk/` and `boot/` (keeping `os/btkeys.bin`,
|
||||
the Bluetooth bonds). Unchanged files are skipped and each file is
|
||||
replaced by a rename; the volume is checked with `e2fsck` before and after,
|
||||
and its features against what `Fs/Ext2.cpp` mounts.
|
||||
- **Expand** (a host-side first boot) grows the root partition to the end of
|
||||
the card, at most 1 TiB, and the volume with `resize2fs`; `firstboot.elf`
|
||||
then finds nothing to do and deletes itself.
|
||||
|
||||
While it works, a runtime udev rule keeps the desktop from automounting the
|
||||
card's partitions (Linux writes to an ext2 volume as soon as it mounts it).
|
||||
|
||||
`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.
|
||||
The card can also boot from a USB card reader (or the image from a USB
|
||||
stick): the root then comes through `Drivers/USB/MassStorage.cpp`. A USB
|
||||
root can register after the boot scan (a SuperSpeed link still training, a
|
||||
reader still bringing its card up), so a boot that finds no `MONTAUKROOT`
|
||||
waits up to 10 s for one, servicing USB hot-plug meanwhile.
|
||||
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 image's `RPI_EFI.fd` comes with a **MontaukOS** boot entry first in the
|
||||
firmware's boot order (`scripts/rpi4-efi-bootorder.py` writes it into the
|
||||
release's empty variable store), so the card boots on its own instead of
|
||||
falling behind the PXE/HTTP entries the firmware creates. The entry names the
|
||||
ESP by the image's MBR disk ID and partition number, so it works from the SD
|
||||
slot or a USB drive. A card whose `RPI_EFI.fd` predates it keeps its old
|
||||
boot order until it is updated with *Update the UEFI firmware*
|
||||
(`--firmware`), which also copies the image's disk ID to the card.
|
||||
|
||||
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` |
|
||||
| Bluetooth (CYW43455 on the 4B, CYW43456 on the 400) | `BCM2EA6` (`\_SB.GDV0.BTH0`) on the mini UART (`BCM2836`) | `Drivers/Bluetooth/Broadcom.cpp`, `Drivers/Serial/BtUart.cpp` |
|
||||
| VideoCore firmware mailbox (radio power, clocks, board MAC) | `BCM2849` (`RPIQ`) | `Drivers/Platform/RpiFirmware.cpp` |
|
||||
| GPIO pin functions | `BCM2845` (`GPI0`) | `Drivers/Platform/RpiGpio.cpp` |
|
||||
| Ethernet (GENET v5 MAC, BCM54213PE PHY) | `BCM6E4E` (`\_SB.ETH0`) | `Drivers/Net/Genet.cpp` |
|
||||
| Wi-Fi (the same CYW4345x, SDIO) | Arasan SD controller `BCM2847`, GPIO 34-39 | `Drivers/Net/Wifi/Brcmf*.cpp` |
|
||||
|
||||
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.
|
||||
|
||||
## Bluetooth
|
||||
|
||||
The radio's Bluetooth half speaks HCI over a UART (H4) on GPIO 30-33 (CTS,
|
||||
RTS, TXD, RXD); its power switch, `BT_ON`, is line 0 of the VideoCore
|
||||
firmware's GPIO expander, reached through the mailbox. The firmware tables
|
||||
describe it as `BTH0`, whose `UARTSerialBus` names the UART it uses: with
|
||||
the `miniuart-bt` overlay the PL011 is the console, so Bluetooth has the mini
|
||||
UART. The kernel refuses to take a UART that is the SPCR console.
|
||||
|
||||
At boot the driver probe routes the pins (`ALT5` for the mini UART, `ALT3`
|
||||
for the PL011), starts the UART (interrupt driven, 16 KiB receive and
|
||||
transmit rings) and the H4 transport under the shared HCI layer. Once the
|
||||
root is mounted the idle loop finishes, as for USB adapters:
|
||||
|
||||
1. power-cycle the controller through `BT_ON`, HCI Reset at 115200 baud;
|
||||
2. Read Local Version: the ROM's LMP subversion picks the patch file,
|
||||
`0:/os/firmware/brcm/BCM4345C0.hcd` (0x6119, 4B) or `BCM4345C5.hcd`
|
||||
(0x6606, 400), shipped unmodified from Raspberry Pi OS's bluez-firmware;
|
||||
3. raise both ends to the operating speed (vendor `0xFC18`);
|
||||
4. Download Minidriver (`0xFC2E`), then every record of the `.hcd` file
|
||||
(Write RAM, then Launch RAM), as Linux's `btbcm` does;
|
||||
5. the patched firmware restarts at 115200: reset, raise the speed again.
|
||||
|
||||
The mini UART's FIFO is 8 bytes deep. Its hardware RTS is tried first; if
|
||||
the controller answers with it on, the link runs at 921600 baud, otherwise
|
||||
at 460800 without flow control (what Raspberry Pi OS uses), and at 115200
|
||||
if the UART has no interrupt. The baud rate derives from the VPU core
|
||||
clock, so `mk-rpi4-sd.sh` pins it (`core_freq=500`, `core_freq_min=500` in
|
||||
`config.txt`), as the overlay's documentation requires. A failed fast
|
||||
bring-up is retried entirely at 115200.
|
||||
|
||||
The CYW43456 reports a chip-wide default address (`43:45:C5:00:1F:AC`);
|
||||
the driver replaces it with the board's Ethernet MAC from the firmware.
|
||||
`0:/config/bluetooth.toml` still overrides both (Broadcom `0xFC01`).
|
||||
|
||||
## Ethernet
|
||||
|
||||
The BCM2711's own Gigabit MAC, GENET v5 (a UniMAC behind a DMA engine),
|
||||
talks RGMII to a BCM54213PE PHY at MDIO address 1. The firmware describes
|
||||
it as `BCM6E4E`: a 64 KiB register block, two interrupts (the driver uses
|
||||
the first, which carries the default queue), `_CCA = 0`, and a `_DSD` with
|
||||
`phy-mode` (`rgmii-rxid`: the PHY delays the receive clock, the MAC the
|
||||
transmit clock) and `brcm,max-dma-burst-size`.
|
||||
|
||||
The driver uses only the default queue (ring 16), with all 256 descriptors
|
||||
of each direction. The descriptors live in the MAC's own SRAM, so only the
|
||||
2 KiB packet buffers need cache maintenance. Received frames start two
|
||||
bytes into their buffer (`RBUF_ALIGN_2B`); neither direction uses the
|
||||
64-byte status blocks, so there is no checksum offload. The UniMAC accepts
|
||||
broadcast and its own address only (the MDF filter).
|
||||
|
||||
The interrupt only acknowledges and masks; frames are dispatched from the
|
||||
idle bottom half, which also checks the ring itself, so receive works if
|
||||
the interrupt never arrives. The PHY is polled once a second (its interrupt
|
||||
pin is not described); a link change reprograms the RGMII block and the
|
||||
UniMAC's speed and duplex. A link the firmware already negotiated is kept,
|
||||
otherwise the PHY is reset and advertises 10/100 and 1000 full duplex.
|
||||
|
||||
The MAC address is the board's, read from OTP through the firmware mailbox
|
||||
(the one the Bluetooth driver uses too); the address the firmware's own
|
||||
driver programmed is the fallback. `dhcp.elf` waits up to 10 s for a wired
|
||||
link, since autonegotiation takes about 3 s after the PHY reset.
|
||||
|
||||
## Wi-Fi
|
||||
|
||||
The radio's WLAN half is an SDIO card on the BCM2711's older SD controller,
|
||||
the Arasan-derived EMMC (`BCM2847`, `0x7E300000`); the card slot is on
|
||||
EMMC2, so the two never share a bus. Its pins are GPIO 34-39 (ALT3; clock
|
||||
without a pull, the rest pulled up, as Linux's `sdio_pins`) and its power
|
||||
switch, `WL_ON`, is line 1 of the firmware's GPIO expander. The probe
|
||||
claims the controller only while the slot is on EMMC2 (`BRCME88C` present);
|
||||
if the tables do not describe the Arasan, its address is taken as EMMC2's
|
||||
less 256 KiB.
|
||||
|
||||
It is a FullMAC radio: the firmware does the 802.11 MLME itself and the host
|
||||
sees Ethernet frames. The driver follows Linux's brcmfmac (ISC license,
|
||||
Broadcom; its notice is in `NOTICES.txt`):
|
||||
|
||||
| File | Part |
|
||||
|---|---|
|
||||
| `BrcmfSdio.cpp` | the Arasan as an SDIO host: CMD5/3/7, 4-bit at 25 MHz, CMD52/CMD53, polled PIO, 32-bit register access |
|
||||
| `BrcmfChip.cpp` | backplane window (function 1), clocks, the enumeration ROM's core list, firmware + NVRAM download, function 2, the firmware console |
|
||||
| `BrcmfProto.cpp` | SDPCM framing and transmit credit, CDC ioctls, BDC data, events, the receive pump, the transmit queue |
|
||||
| `BrcmfConnect.cpp` | preinit and CLM, escan, join, keys, link state |
|
||||
|
||||
Once the root is mounted the idle loop:
|
||||
|
||||
1. routes the pins, switches the EMMC clock on, pulses `WL_ON`, enumerates
|
||||
the card (function 1 blocks of 64 bytes, function 2 of 512);
|
||||
2. requests the ALP clock, reads the chip ID: `0x4345` revision 9 is the
|
||||
CYW43456 (Pi 400), any other revision the CYW43455 (Pi 4B), which picks
|
||||
`0:/os/firmware/brcm/brcmfmac43456-sdio.*` or `brcmfmac43455-sdio.*`
|
||||
(shipped unmodified from Raspberry Pi OS's firmware-nonfree);
|
||||
3. halts the ARM Cortex-R4, holds the 802.11 core in reset, sizes the RAM
|
||||
from the CR4's banks, writes the firmware at `0x198000` and the NVRAM
|
||||
(stripped to `key=value\0` pairs and a length token) at the top of RAM,
|
||||
reading both back to verify, puts the reset vector at address 0 and
|
||||
releases the CPU;
|
||||
4. waits for the HT clock, sends the SDPCM protocol version through the
|
||||
mailbox, enables function 2, the F2 watermark and MES busy signalling of
|
||||
Linux's CYW43455 settings, save/restore if the chip has it, and the card
|
||||
interrupt;
|
||||
5. reads `cur_etheraddr` (the NVRAM's shared placeholder address
|
||||
`b8:27:eb:74:f2:6c` is replaced with the board address + 1, as on
|
||||
Raspberry Pi OS), loads the CLM blob (`clmload`, 1400-byte chunks: these
|
||||
firmware builds carry no regulatory data of their own), turns off glom,
|
||||
roaming, ARP/ND offload and power save, enables the events it handles and
|
||||
brings the interface up (`WLC_UP`).
|
||||
|
||||
Nothing uses the controller's interrupt: the bus is swept from the idle
|
||||
loop (and from the timer tick when the cores never idle), every 10 ms while
|
||||
a scan or a connection is active and every 250 ms otherwise, and at once when
|
||||
the controller shows the card interrupt. Every sweep reads the SDIO core's
|
||||
interrupt status, then every frame waiting in function 2 (a 4-byte header
|
||||
read, then the rest). Ioctls wait for transmit credit and their answer by
|
||||
sweeping the bus themselves. Frames are delivered with the bus unlocked, and
|
||||
transmitting never waits for the bus: data frames go into a 32-frame queue
|
||||
that whoever holds the bus, or the next sweep, sends.
|
||||
|
||||
Scans are escans (all channels, active, firmware dwell times); each result
|
||||
event carries a `brcmf_bss_info_le` whose information elements go into the
|
||||
same scan table the Intel driver fills from beacons. A join sets `wpaie`
|
||||
(the RSN element our supplicant builds), `wpa_auth`, `auth` and `wsec`, then
|
||||
`join` with the BSSID and the chanspec escan reported. The 4-way handshake
|
||||
runs in the host's supplicant (`Wpa.cpp`) as for the Intel adapter: EAPOL
|
||||
frames are ordinary Ethernet frames here, and the keys go to the firmware
|
||||
with `wsec_key` once message 4 has left the transmit queue. The radio does
|
||||
AES-CCMP only: GCMP networks are refused, as are WPA3-only (SAE), WEP and
|
||||
TKIP ones.
|
||||
|
||||
Unanswered ioctls (a leaky bucket of three), a halt reported through
|
||||
the mailbox, or repeated bus errors mark the radio failed: the firmware's
|
||||
console and any trap record are printed to the kernel log (`WiFi-FW`), and
|
||||
the facade's recovery powers the radio off and repeats the bring-up (at
|
||||
most three times).
|
||||
|
||||
`tests/wifi/run.sh` drives `BrcmfProto.cpp` and `BrcmfConnect.cpp` with the
|
||||
real supplicant against a simulated firmware: preinit and the CLM chunks,
|
||||
escan parsing, open and WPA2 joins (an independent authenticator checks
|
||||
messages 2 and 4 and the installed keys), the wrong passphrase, refusals,
|
||||
join timeouts and a firmware that stops answering.
|
||||
|
||||
## Not supported yet
|
||||
|
||||
- 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 beyond 802.11 station mode on one interface: no access point mode,
|
||||
no SDIO high speed (50 MHz), no glom, no firmware supplicant.
|
||||
- Audio (HDMI via VideoCore, the 3.5 mm jack via PWM).
|
||||
|
||||
## Status
|
||||
|
||||
Bluetooth works on a Pi 400 (CYW43456), including A2DP music to
|
||||
headphones. Ethernet has not run yet: QEMU has no GENET model. Wi-Fi has
|
||||
not run on hardware yet either (QEMU has no model of the radio); only its
|
||||
protocol and connection layers are tested, in the host harness. It is untested on a Pi 4B (CYW43455), and QEMU has no model of
|
||||
the radio.
|
||||
|
||||
Everything else 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);
|
||||
|
||||
}
|
||||
@@ -11,8 +11,8 @@
|
||||
#include <Drivers/Audio/IntelHda.hpp>
|
||||
#include <Drivers/Audio/Mixer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
||||
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Bluetooth/A2dp.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
@@ -55,10 +55,10 @@ namespace montauk::abi {
|
||||
return Drivers::Audio::Mixer::SetOutput(
|
||||
(Drivers::Audio::Mixer::Output)value);
|
||||
if (cmd == AUDIO_CTL_BT_STATUS) {
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||
if (Drivers::USB::Bluetooth::A2dp::IsReady()) return 2;
|
||||
return Drivers::USB::Bluetooth::A2dp::GetState()
|
||||
== Drivers::USB::Bluetooth::A2dp::State::Idle
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||
if (Drivers::Bluetooth::A2dp::IsReady()) return 2;
|
||||
return Drivers::Bluetooth::A2dp::GetState()
|
||||
== Drivers::Bluetooth::A2dp::State::Idle
|
||||
? 0 : 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/USB/Bluetooth/Hci.hpp>
|
||||
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Bluetooth/Hci.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
@@ -19,14 +19,14 @@ namespace montauk::abi {
|
||||
|
||||
static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
|
||||
if (!buf || maxCount <= 0) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
// Use a stack buffer for kernel-side results (max 16)
|
||||
Drivers::USB::Bluetooth::Hci::InquiryDevice tmpBuf[16];
|
||||
Drivers::Bluetooth::Hci::InquiryDevice tmpBuf[16];
|
||||
int kMax = maxCount;
|
||||
if (kMax > 16) kMax = 16;
|
||||
|
||||
int found = Drivers::USB::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
|
||||
int found = Drivers::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
|
||||
if (found < 0) return found;
|
||||
|
||||
// Convert kernel InquiryDevice to userspace BtScanResult
|
||||
@@ -47,38 +47,38 @@ namespace montauk::abi {
|
||||
|
||||
static int64_t Sys_BtConnect(const uint8_t* bdAddr) {
|
||||
if (!bdAddr) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
return (int64_t)Drivers::USB::Bluetooth::Connect(bdAddr);
|
||||
return (int64_t)Drivers::Bluetooth::Connect(bdAddr);
|
||||
}
|
||||
|
||||
static int64_t Sys_BtDisconnect(const uint8_t* bdAddr) {
|
||||
if (!bdAddr) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
|
||||
<< (uint64_t)Sched::GetCurrentPid()
|
||||
<< " entered Bluetooth disconnect stage";
|
||||
return (int64_t)Drivers::USB::Bluetooth::Disconnect(bdAddr);
|
||||
return (int64_t)Drivers::Bluetooth::Disconnect(bdAddr);
|
||||
}
|
||||
|
||||
static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) {
|
||||
if (!bdAddr) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
// bdAddr is a 6-byte buffer, addr[0] = least-significant octet.
|
||||
return Drivers::USB::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
|
||||
return Drivers::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
|
||||
}
|
||||
|
||||
static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) {
|
||||
if (!buf || maxCount <= 0) return 0;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||
|
||||
Drivers::USB::Bluetooth::Hci::BondInfo tmpBuf[8];
|
||||
Drivers::Bluetooth::Hci::BondInfo tmpBuf[8];
|
||||
int kMax = maxCount;
|
||||
if (kMax > 8) kMax = 8;
|
||||
|
||||
int count = Drivers::USB::Bluetooth::ListBonded(tmpBuf, kMax);
|
||||
int count = Drivers::Bluetooth::ListBonded(tmpBuf, kMax);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6);
|
||||
@@ -91,20 +91,20 @@ namespace montauk::abi {
|
||||
|
||||
static int64_t Sys_BtForget(const uint8_t* bdAddr) {
|
||||
if (!bdAddr) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
return (int64_t)Drivers::USB::Bluetooth::ForgetDevice(bdAddr);
|
||||
return (int64_t)Drivers::Bluetooth::ForgetDevice(bdAddr);
|
||||
}
|
||||
|
||||
static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) {
|
||||
if (!buf || maxCount <= 0) return 0;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||
|
||||
Drivers::USB::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
|
||||
Drivers::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
|
||||
int kMax = maxCount;
|
||||
if (kMax > 4) kMax = 4;
|
||||
|
||||
int count = Drivers::USB::Bluetooth::ListConnected(tmpBuf, kMax);
|
||||
int count = Drivers::Bluetooth::ListConnected(tmpBuf, kMax);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6);
|
||||
@@ -123,14 +123,14 @@ namespace montauk::abi {
|
||||
|
||||
memset(buf, 0, sizeof(BtAdapterInfo));
|
||||
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) {
|
||||
if (!Drivers::Bluetooth::IsInitialized()) {
|
||||
buf->initialized = 0;
|
||||
return NoDevice;
|
||||
}
|
||||
|
||||
buf->initialized = 1;
|
||||
memcpy(buf->bdAddr, Drivers::USB::Bluetooth::GetBdAddr(), 6);
|
||||
buf->scanning = Drivers::USB::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
|
||||
memcpy(buf->bdAddr, Drivers::Bluetooth::GetBdAddr(), 6);
|
||||
buf->scanning = Drivers::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
|
||||
|
||||
// Copy adapter name
|
||||
const char* name = "MontaukOS";
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 26
|
||||
#define MONTAUK_BUILD_NUMBER 123
|
||||
@@ -8,9 +8,10 @@
|
||||
#include <cstdint>
|
||||
#include <Drivers/USB/Xhci.hpp>
|
||||
#include <Drivers/USB/UsbDevice.hpp>
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Genet.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||
#include <Drivers/Storage/Ahci.hpp>
|
||||
@@ -122,11 +123,24 @@ namespace montauk::abi {
|
||||
if (Drivers::Net::E1000E::IsInitialized()) {
|
||||
add(5, "Intel E1000E", "Gigabit Ethernet (82574L)");
|
||||
}
|
||||
if (Drivers::Net::Genet::IsInitialized()) {
|
||||
uint32_t speed = Drivers::Net::Genet::GetLinkSpeed();
|
||||
add(5, "Broadcom GENET", speed == 1000 ? "Gigabit Ethernet (1000 Mbit/s)"
|
||||
: speed == 100 ? "Gigabit Ethernet (100 Mbit/s)"
|
||||
: speed == 10 ? "Gigabit Ethernet (10 Mbit/s)"
|
||||
: "Gigabit Ethernet (no link)");
|
||||
}
|
||||
if (Drivers::Net::Wifi::IsPresent()) {
|
||||
add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp()
|
||||
? "802.11 wireless (connected)"
|
||||
: "802.11 wireless");
|
||||
}
|
||||
// An on-board UART Bluetooth controller (Raspberry Pi) is no USB
|
||||
// device, so it is listed here; USB adapters appear above.
|
||||
if (Drivers::Bluetooth::IsInitialized() &&
|
||||
Drivers::Bluetooth::Hci::GetTransport() == Drivers::Bluetooth::Hci::Transport::Uart) {
|
||||
add(5, "Bluetooth Adapter", "Broadcom, HCI over UART");
|
||||
}
|
||||
|
||||
// Display (category 6)
|
||||
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
|
||||
@@ -148,7 +162,7 @@ namespace montauk::abi {
|
||||
add(9, "Intel HDA", detail);
|
||||
}
|
||||
|
||||
if (Drivers::USB::Bluetooth::IsInitialized()) {
|
||||
if (Drivers::Bluetooth::IsInitialized()) {
|
||||
add(9, "Bluetooth Audio", "A2DP (SBC)");
|
||||
}
|
||||
|
||||
@@ -248,6 +262,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;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <Net/NetIf.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Genet.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
@@ -157,6 +158,10 @@ namespace montauk::abi {
|
||||
out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount();
|
||||
out->txPackets = Drivers::Net::E1000E::GetTxPacketCount();
|
||||
CopyNetStatusDriver(out->driver, "Intel e1000e");
|
||||
} else if (Drivers::Net::Genet::IsInitialized()) {
|
||||
out->rxPackets = Drivers::Net::Genet::GetRxPacketCount();
|
||||
out->txPackets = Drivers::Net::Genet::GetTxPacketCount();
|
||||
CopyNetStatusDriver(out->driver, "Broadcom GENET");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#include "Terminal.hpp" // SYS_PRINT, SYS_PUTCHAR
|
||||
#include "Filesystem.hpp" // SYS_OPEN, SYS_READ, SYS_GETSIZE, SYS_CLOSE, SYS_READDIR, SYS_FWRITE, SYS_FCREATE
|
||||
#include "Heap.hpp" // SYS_ALLOC, SYS_FREE
|
||||
#include "Time.hpp" // SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME
|
||||
#include "Time.hpp" // SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME, SYS_GETUNIXTIME
|
||||
#include "Keyboard.hpp" // SYS_ISKEYAVAILABLE, SYS_GETKEY, SYS_GETCHAR
|
||||
#include "Info.hpp" // SYS_GETINFO
|
||||
#include "Graphics.hpp" // SYS_FBINFO, SYS_FBMAP, SYS_TERMSIZE, SYS_TERMSCALE
|
||||
@@ -245,6 +245,8 @@ namespace montauk::abi {
|
||||
if (!UserMemory::Writable<DateTime>(frame->arg1)) return BadPointer;
|
||||
Sys_GetTime((DateTime*)frame->arg1);
|
||||
return Success;
|
||||
case SYS_GETUNIXTIME:
|
||||
return Sys_GetUnixTime();
|
||||
case SYS_SETUNIXTIME:
|
||||
if (!Sched::Consent::Require(CAP_SET_TIME)) return CapabilityDenied;
|
||||
return Sys_SetUnixTime((int64_t)frame->arg1);
|
||||
@@ -454,6 +456,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,14 @@ 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
|
||||
|
||||
/* Time.hpp -- wall clock as UTC unix seconds (SYS_GETTIME is local time). */
|
||||
static constexpr uint64_t SYS_GETUNIXTIME = 209; // () -> seconds
|
||||
|
||||
/* 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 +923,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 +1005,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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Time.hpp
|
||||
* SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME syscalls
|
||||
* SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME, SYS_GETUNIXTIME syscalls
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
@@ -39,6 +39,10 @@ namespace montauk::abi {
|
||||
return (int64_t)Timekeeping::GetTZOffset();
|
||||
}
|
||||
|
||||
static int64_t Sys_GetUnixTime() {
|
||||
return Timekeeping::GetUnixTimestamp();
|
||||
}
|
||||
|
||||
static int64_t Sys_SetUnixTime(int64_t unixSeconds) {
|
||||
return Timekeeping::SetUnixTimestamp(unixSeconds) ? Success : InvalidArgument;
|
||||
}
|
||||
|
||||
@@ -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,454 @@ 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::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::Map(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::Map 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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// The bootloader wrote the image through the data cache. Push it to
|
||||
// the point of unification and drop every instruction-cache line
|
||||
// before the kernel relies on fetching code it has not run yet: a
|
||||
// line still dirty in L1D is invisible to instruction fetch, which
|
||||
// then reads stale DRAM (an "unknown reason" exception on a valid
|
||||
// instruction).
|
||||
Arch::SyncInstructionCache((const void*)kernelBaseVirt, kernelSize);
|
||||
|
||||
Arch::ActivateKernelTranslation();
|
||||
|
||||
Kt::KernelLogStream(Kt::OK, "VMM") << "Switched to kernel translation tables (HHDM limit 0x"
|
||||
<< base::hex << g_hhdmLimit << ")";
|
||||
}
|
||||
|
||||
void Paging::MapMMIO(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::MapMMIO not implemented");
|
||||
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::MapWC(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::MapWC not implemented");
|
||||
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::MapUser(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::MapUser not implemented");
|
||||
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!");
|
||||
}
|
||||
|
||||
std::uint64_t Paging::GetPhysAddr(std::uint64_t, std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::GetPhysAddr not implemented");
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void Paging::FreeUserHalf(std::uint64_t) {
|
||||
NotImplemented("aarch64: Paging::FreeUserHalf 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();
|
||||
}
|
||||
|
||||
bool Paging::IsUserRangeAccessible(std::uint64_t, std::uint64_t, std::uint64_t, bool) {
|
||||
NotImplemented("aarch64: Paging::IsUserRangeAccessible 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));
|
||||
}
|
||||
}
|
||||
|
||||
void Paging::MapEfiRuntime(const montauk::boot::EfiInfo&) {
|
||||
NotImplemented("aarch64: Paging::MapEfiRuntime not implemented");
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,5 +24,13 @@ namespace Arch {
|
||||
PrintBoxedHex(kerr, "PSTATE (SPSR)", frame->spsr, BoxWidth);
|
||||
PrintBoxedHex(kerr, "Stack Pointer", frame->sp, BoxWidth);
|
||||
PrintBoxedHex(kerr, "Link Register (x30)", frame->x[30], BoxWidth);
|
||||
|
||||
// An "unknown reason" exception on a kernel PC: show the word the
|
||||
// data side sees there. A valid instruction means instruction fetch
|
||||
// saw something else (cache maintenance); garbage means the code
|
||||
// itself was overwritten. The fetch succeeded, so the page is mapped.
|
||||
if ((frame->esr >> 26) == 0 && frame->elr >= 0xFFFF000000000000ULL && (frame->elr & 3) == 0) {
|
||||
PrintBoxedHex(kerr, "Instruction at PC", *(volatile uint32_t*)frame->elr, BoxWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 << ")";
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
void BootAPs(const montauk::boot::SmpInfo& smp) {
|
||||
if (smp.cpuCount > 1) {
|
||||
g_detectedCpuCount = (int)smp.cpuCount;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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)";
|
||||
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,10 +27,12 @@
|
||||
#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>
|
||||
#include <Fs/Boot.hpp>
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Ipc/Ipc.hpp>
|
||||
#include <Api/Syscall.hpp>
|
||||
@@ -111,6 +113,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 +139,8 @@ extern "C" void kmain() {
|
||||
// power control on the answer.
|
||||
Hal::AML::InitializeNamespaceObjects();
|
||||
|
||||
Pci::AssignInterruptLines();
|
||||
|
||||
Drivers::ProbeEarly();
|
||||
Drivers::InitializeGraphics();
|
||||
|
||||
@@ -173,6 +179,11 @@ extern "C" void kmain() {
|
||||
|
||||
Arch::LateInitialize();
|
||||
|
||||
// Without a root nothing can start; say so instead of idling silently.
|
||||
if (!Fs::Vfs::IsDriveRegistered(0)) {
|
||||
Panic("No root filesystem (0:/): cannot start 0:/os/init.elf", nullptr);
|
||||
}
|
||||
|
||||
Kt::SuppressKernelLog();
|
||||
Sched::Spawn("0:/os/init.elf");
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ namespace kcp {
|
||||
Arch::Pause();
|
||||
}
|
||||
}
|
||||
bool TryAcquire() {
|
||||
return !flag.test_and_set(std::memory_order_acquire);
|
||||
}
|
||||
void Release() {
|
||||
flag.clear(std::memory_order_release);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
#include "Mixer.hpp"
|
||||
#include "IntelHda.hpp"
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
||||
#include <Drivers/USB/Bluetooth/Avrcp.hpp>
|
||||
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Bluetooth/A2dp.hpp>
|
||||
#include <Drivers/Bluetooth/Avrcp.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
@@ -146,8 +146,8 @@ namespace Drivers::Audio::Mixer {
|
||||
}
|
||||
|
||||
static bool BluetoothReady() {
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return false;
|
||||
return Drivers::USB::Bluetooth::A2dp::IsReady();
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return false;
|
||||
return Drivers::Bluetooth::A2dp::IsReady();
|
||||
}
|
||||
|
||||
static void SyncHdaMasterMute() {
|
||||
@@ -236,7 +236,7 @@ namespace Drivers::Audio::Mixer {
|
||||
static uint32_t BackendWriteSpace() {
|
||||
if (g_output == Output::Bluetooth) {
|
||||
return g_btOpened
|
||||
? Drivers::USB::Bluetooth::A2dp::GetWriteSpace() : 0;
|
||||
? Drivers::Bluetooth::A2dp::GetWriteSpace() : 0;
|
||||
}
|
||||
return g_hdaOpened ? IntelHda::GetWriteSpace(g_hdaHandle) : 0;
|
||||
}
|
||||
@@ -244,7 +244,7 @@ namespace Drivers::Audio::Mixer {
|
||||
static int BackendWrite(const uint8_t* data, uint32_t size) {
|
||||
if (g_output == Output::Bluetooth) {
|
||||
return g_btOpened
|
||||
? Drivers::USB::Bluetooth::A2dp::WriteAudio(data, size) : -1;
|
||||
? Drivers::Bluetooth::A2dp::WriteAudio(data, size) : -1;
|
||||
}
|
||||
return g_hdaOpened ? IntelHda::Write(g_hdaHandle, data, size) : -1;
|
||||
}
|
||||
@@ -403,14 +403,14 @@ namespace Drivers::Audio::Mixer {
|
||||
if (g_output == Output::Bluetooth && !g_btOpened && BluetoothReady()) {
|
||||
g_switchingOutput = true;
|
||||
g_lock.Release();
|
||||
bool btOk = Drivers::USB::Bluetooth::A2dp::ConfigureStream(
|
||||
bool btOk = Drivers::Bluetooth::A2dp::ConfigureStream(
|
||||
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
||||
Drivers::USB::Bluetooth::A2dp::StartStream();
|
||||
Drivers::Bluetooth::A2dp::StartStream();
|
||||
g_lock.Acquire();
|
||||
g_btOpened = btOk;
|
||||
if (!btOk) g_output = Output::Hda;
|
||||
if (btOk)
|
||||
Drivers::USB::Bluetooth::A2dp::SetMuted(
|
||||
Drivers::Bluetooth::A2dp::SetMuted(
|
||||
g_masterMute.load(std::memory_order_acquire));
|
||||
g_switchingOutput = false;
|
||||
wakeSwitchWaiters = true;
|
||||
@@ -515,7 +515,7 @@ namespace Drivers::Audio::Mixer {
|
||||
if (closeBt) g_btOpened = false;
|
||||
g_lock.Release();
|
||||
if (closeHda) IntelHda::Close(hdaHandle);
|
||||
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
||||
if (closeBt) Drivers::Bluetooth::A2dp::StopStream(true);
|
||||
if (ringToFree) FreeRing(ringToFree);
|
||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
}
|
||||
@@ -535,7 +535,7 @@ namespace Drivers::Audio::Mixer {
|
||||
bool serviceBluetooth = g_output == Output::Bluetooth;
|
||||
g_lock.Release();
|
||||
if (serviceBluetooth)
|
||||
Drivers::USB::Bluetooth::A2dp::ServiceMedia();
|
||||
Drivers::Bluetooth::A2dp::ServiceMedia();
|
||||
return (int)written;
|
||||
}
|
||||
|
||||
@@ -651,7 +651,7 @@ namespace Drivers::Audio::Mixer {
|
||||
if (changed) BumpSerialLocked();
|
||||
g_lock.Release();
|
||||
if (closeHda) IntelHda::Close(hdaHandle);
|
||||
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
||||
if (closeBt) Drivers::Bluetooth::A2dp::StopStream(true);
|
||||
for (int i = 0; i < ringCount; i++) FreeRing(ringsToFree[i]);
|
||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
}
|
||||
@@ -668,7 +668,7 @@ namespace Drivers::Audio::Mixer {
|
||||
g_lock.Release();
|
||||
|
||||
if (changed)
|
||||
Drivers::USB::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
|
||||
Drivers::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
|
||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
}
|
||||
|
||||
@@ -688,7 +688,7 @@ namespace Drivers::Audio::Mixer {
|
||||
|
||||
if (changed) SyncHdaMasterMute();
|
||||
if (changed)
|
||||
Drivers::USB::Bluetooth::A2dp::SetMuted(muted);
|
||||
Drivers::Bluetooth::A2dp::SetMuted(muted);
|
||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
}
|
||||
|
||||
@@ -733,11 +733,11 @@ namespace Drivers::Audio::Mixer {
|
||||
}
|
||||
} else if (needBackend && output == Output::Bluetooth) {
|
||||
prepared = BluetoothReady() &&
|
||||
Drivers::USB::Bluetooth::A2dp::ConfigureStream(
|
||||
Drivers::Bluetooth::A2dp::ConfigureStream(
|
||||
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
||||
Drivers::USB::Bluetooth::A2dp::StartStream();
|
||||
Drivers::Bluetooth::A2dp::StartStream();
|
||||
if (prepared)
|
||||
Drivers::USB::Bluetooth::A2dp::SetMuted(
|
||||
Drivers::Bluetooth::A2dp::SetMuted(
|
||||
g_masterMute.load(std::memory_order_acquire));
|
||||
} else if (output == Output::Hda) {
|
||||
prepared = IntelHda::IsInitialized();
|
||||
@@ -779,7 +779,7 @@ namespace Drivers::Audio::Mixer {
|
||||
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
|
||||
|
||||
if (closeHda) IntelHda::Close(oldHdaHandle);
|
||||
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
||||
if (closeBt) Drivers::Bluetooth::A2dp::StopStream(true);
|
||||
Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
return 0;
|
||||
}
|
||||
@@ -815,7 +815,7 @@ namespace Drivers::Audio::Mixer {
|
||||
wake = true;
|
||||
}
|
||||
g_lock.Release();
|
||||
if (stopBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
||||
if (stopBt) Drivers::Bluetooth::A2dp::StopStream(true);
|
||||
if (wake) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "Sbc.hpp"
|
||||
#include "L2cap.hpp"
|
||||
#include "Hci.hpp"
|
||||
#include <Drivers/USB/Xhci.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
@@ -18,7 +17,7 @@
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::USB::Bluetooth::A2dp {
|
||||
namespace Drivers::Bluetooth::A2dp {
|
||||
|
||||
// =========================================================================
|
||||
// AVDTP constants
|
||||
@@ -222,7 +221,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents(); // AVDTP responses arrive as ACL data
|
||||
if (g_avdtpResponseReady.load(std::memory_order_acquire)) {
|
||||
// Freeze this mailbox before returning to the caller. A late
|
||||
@@ -557,7 +556,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
|
||||
void OnChannelReady(uint16_t l2capCid) {
|
||||
// Invoked from L2CAP inside the ACL receive path -- i.e. NESTED under
|
||||
// Xhci::PollEvents. PollEvents is non-reentrant (re-entry guard), so we
|
||||
// Hci::DrainEvents. That is non-reentrant (re-entry guard), so we
|
||||
// must NOT run the blocking AVDTP signaling chain here; a nested poll
|
||||
// would stall and every WaitAvdtpResponse() would time out. Just record
|
||||
// the channel; StartSource() drives the handshakes from top-level
|
||||
@@ -1161,7 +1160,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
bool answered = false;
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
if (g_sdpRspReady.load(std::memory_order_acquire)) {
|
||||
answered = true;
|
||||
@@ -1196,7 +1195,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
static bool WaitEncrypted(uint32_t timeoutMs) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
Hci::ProcessPendingCommands();
|
||||
auto* c = Hci::GetConnection(g_aclHandle.load(std::memory_order_acquire));
|
||||
@@ -1235,7 +1234,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
uint64_t waitStart = Timekeeping::GetMilliseconds();
|
||||
while (g_sourceSetupActive.load(std::memory_order_acquire)
|
||||
&& Timekeeping::GetMilliseconds() - waitStart < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
for (int j = 0; j < 100; j++) Arch::Pause();
|
||||
}
|
||||
@@ -1265,7 +1264,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
if (g_sigCid.load(std::memory_order_acquire) == 0) {
|
||||
uint64_t lStart = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - lStart < 1000) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
Hci::ProcessPendingCommands();
|
||||
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
|
||||
@@ -1319,7 +1318,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
uint32_t attemptWaitMs = timeoutMs < 1500 ? timeoutMs : 1500;
|
||||
uint32_t waitBudgetMs = attemptWaitMs;
|
||||
while (Timekeeping::GetMilliseconds() - sigStart < waitBudgetMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
|
||||
// PENDING is explicitly non-final. A peer performing service
|
||||
@@ -1363,7 +1362,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
if (sig) L2cap::FreeChannel(sig);
|
||||
uint64_t st = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - st < 400) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
for (int j = 0; j < 200; j++) Arch::Pause();
|
||||
}
|
||||
@@ -1384,7 +1383,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
if (g_peerConfigured.load(std::memory_order_acquire)) {
|
||||
uint64_t peerStart = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - peerStart < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
State s = g_state.load(std::memory_order_acquire);
|
||||
if (s == State::Open || s == State::Streaming) break;
|
||||
@@ -1483,7 +1482,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
uint64_t mStart = Timekeeping::GetMilliseconds();
|
||||
while (g_mediaCid.load(std::memory_order_acquire) == 0
|
||||
&& Timekeeping::GetMilliseconds() - mStart < kMediaWaitMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
auto* ch = media ? L2cap::GetChannel(media) : nullptr;
|
||||
if (ch && ch->Configured) {
|
||||
@@ -1838,6 +1837,36 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// ConfigureStream
|
||||
// =========================================================================
|
||||
|
||||
// A UART transport has a hard byte budget: at 460800 baud it carries
|
||||
// 46 KB/s, and SBC at bitpool 53 / 48 kHz needs ~46 KB/s with its
|
||||
// headers, so the stream fell behind whenever anything else crossed the
|
||||
// link and the sink played concealment. Keep the stream within three
|
||||
// quarters of the line rate by lowering the bitpool (any value in the
|
||||
// negotiated range is valid per frame). Over USB nothing changes.
|
||||
static void FitBitpoolToTransport(uint32_t sampleRate) {
|
||||
uint32_t budget = Hci::TransportBytesPerSecond() / 4 * 3;
|
||||
if (budget == 0) return;
|
||||
uint32_t rate = sampleRate ? sampleRate : 48000;
|
||||
uint8_t minBitpool = g_cfgSbc[2] < 2 ? 2 : g_cfgSbc[2];
|
||||
uint8_t original = g_sbcEncoder.Bitpool;
|
||||
auto bytesPerSecond = [&]() {
|
||||
// H4 + ACL + L2CAP + RTP + SCMS-T + SBC headers (23 bytes) per
|
||||
// packet, spread over the ~4-5 frames bundled into each.
|
||||
uint32_t frameCost = Sbc::GetFrameSize(&g_sbcEncoder) + 6;
|
||||
return (uint64_t)frameCost * rate / Sbc::GetSamplesPerFrame(&g_sbcEncoder);
|
||||
};
|
||||
while (g_sbcEncoder.Bitpool > minBitpool && bytesPerSecond() > budget) {
|
||||
Sbc::Configure(&g_sbcEncoder, g_cfgSbc[0], g_cfgSbc[1],
|
||||
(uint8_t)(g_sbcEncoder.Bitpool - 1));
|
||||
}
|
||||
if (g_sbcEncoder.Bitpool != original) {
|
||||
KernelLogStream(INFO, "BT-A2DP") << "Bitpool " << (uint64_t)original << " -> "
|
||||
<< (uint64_t)g_sbcEncoder.Bitpool << " to fit the transport ("
|
||||
<< bytesPerSecond() << " of " << (uint64_t)Hci::TransportBytesPerSecond()
|
||||
<< " bytes/s)";
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigureStream(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
|
||||
// Encoder configuration and PumpMedia both mutate the SBC encoder.
|
||||
// Device switching normally configures an Open stream, but explicitly
|
||||
@@ -1852,6 +1881,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// agreed to (Init's defaults may differ from the negotiated subset).
|
||||
if (g_cfgSbc[0] || g_cfgSbc[1]) {
|
||||
Sbc::Configure(&g_sbcEncoder, g_cfgSbc[0], g_cfgSbc[1], g_cfgSbc[3]);
|
||||
FitBitpoolToTransport(sampleRate);
|
||||
}
|
||||
g_sbcInitialized = true;
|
||||
g_seqNum = 0;
|
||||
@@ -2136,7 +2166,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
|
||||
void ServiceMedia() {
|
||||
if (!AcquireMediaService()) return;
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
PumpMedia();
|
||||
g_serviceActive.store(false, std::memory_order_release);
|
||||
@@ -8,7 +8,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::USB::Bluetooth::A2dp {
|
||||
namespace Drivers::Bluetooth::A2dp {
|
||||
|
||||
// =========================================================================
|
||||
// A2DP stream states
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
namespace Drivers::Bluetooth::Avrcp {
|
||||
|
||||
// =========================================================================
|
||||
// Constants
|
||||
@@ -7,7 +7,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
namespace Drivers::Bluetooth::Avrcp {
|
||||
|
||||
// Process an AVCTP packet arriving on an AVRCP control channel (PSM 0x0017).
|
||||
// Implements a minimal AVRCP Target: unit/subunit info, honest rejection of
|
||||
+55
-18
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Bluetooth.cpp
|
||||
* Top-level Bluetooth subsystem — adapter registration and Intel BT initialization
|
||||
* Top-level Bluetooth subsystem — adapter registration and controller bring-up
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Hci.hpp"
|
||||
#include "A2dp.hpp"
|
||||
#include "IntelFirmware.hpp"
|
||||
#include "Broadcom.hpp"
|
||||
#include <Drivers/Audio/Mixer.hpp>
|
||||
#include <Drivers/USB/Xhci.hpp>
|
||||
#include <Drivers/USB/UsbDevice.hpp>
|
||||
@@ -22,7 +23,9 @@
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::USB::Bluetooth {
|
||||
namespace Drivers::Bluetooth {
|
||||
|
||||
namespace Xhci = Drivers::USB::Xhci;
|
||||
|
||||
// =========================================================================
|
||||
// State
|
||||
@@ -205,7 +208,9 @@ namespace Drivers::USB::Bluetooth {
|
||||
// =========================================================================
|
||||
|
||||
void RegisterAdapter(uint8_t slotId) {
|
||||
if (g_initialized) {
|
||||
// One adapter at a time, whichever came first (an on-board UART
|
||||
// controller may still be finishing its deferred init).
|
||||
if (g_initialized || Hci::GetTransport() != Hci::Transport::None) {
|
||||
KernelLogStream(WARNING, "BT") << "Bluetooth adapter already registered";
|
||||
return;
|
||||
}
|
||||
@@ -249,17 +254,47 @@ namespace Drivers::USB::Bluetooth {
|
||||
CompleteInit();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ProbePlatformAdapter — an on-board UART controller ACPI describes
|
||||
// =========================================================================
|
||||
|
||||
bool ProbePlatformAdapter() {
|
||||
if (g_initialized || Hci::GetTransport() != Hci::Transport::None) return false;
|
||||
if (!Broadcom::ProbePlatform()) return false;
|
||||
|
||||
// The firmware patch lives on the root filesystem, which the boot
|
||||
// driver probe precedes; finish from the idle loop like USB adapters.
|
||||
if (!Fs::Vfs::IsDriveRegistered(0)) {
|
||||
g_initPending.store(true, std::memory_order_release);
|
||||
KernelLogStream(INFO, "BT") << "Transport up; deferring init until the root is mounted";
|
||||
return true;
|
||||
}
|
||||
CompleteInit();
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CompleteInit — firmware-dependent HCI bring-up (needs VFS/ramdisk)
|
||||
// =========================================================================
|
||||
|
||||
static void CompleteInit() {
|
||||
auto* dev = Xhci::GetDevice(g_slotId);
|
||||
if (!dev) return;
|
||||
bool didReset = false;
|
||||
|
||||
if (Hci::GetTransport() == Hci::Transport::Uart) {
|
||||
// Power cycle, firmware patch and operating baud; ends with a reset.
|
||||
if (!Broadcom::Initialize()) {
|
||||
KernelLogStream(ERROR, "BT") << "Broadcom BT initialization failed";
|
||||
return;
|
||||
}
|
||||
didReset = true;
|
||||
}
|
||||
|
||||
auto* dev = Hci::GetTransport() == Hci::Transport::Usb ? Xhci::GetDevice(g_slotId) : nullptr;
|
||||
if (Hci::GetTransport() == Hci::Transport::Usb && !dev) return;
|
||||
|
||||
// Intel-specific initialization (firmware download + HCI Reset)
|
||||
bool didReset = false;
|
||||
if (IsIntelBt(dev->VendorId, dev->ProductId)) {
|
||||
if (dev && IsIntelBt(dev->VendorId, dev->ProductId)) {
|
||||
Hci::SetVendor(Hci::Vendor::Intel);
|
||||
if (InitIntelBluetooth()) {
|
||||
didReset = true; // InitIntelBluetooth already sent HCI Reset
|
||||
} else {
|
||||
@@ -280,9 +315,11 @@ namespace Drivers::USB::Bluetooth {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a persisted BD_ADDR override (0:/config/bluetooth.toml) now,
|
||||
// after the last reset and before the address is read back, so the
|
||||
// whole bring-up below uses the overridden address.
|
||||
// A Broadcom part still on its chip-wide default address gets the
|
||||
// board's, then a persisted BD_ADDR override (0:/config/bluetooth.toml)
|
||||
// wins over both. Both after the last reset and before the address
|
||||
// is read back, so the whole bring-up below uses the final address.
|
||||
if (Hci::GetVendor() == Hci::Vendor::Broadcom) Broadcom::FixDefaultAddress();
|
||||
ApplyConfiguredAddress();
|
||||
|
||||
// Read BD_ADDR
|
||||
@@ -387,7 +424,7 @@ namespace Drivers::USB::Bluetooth {
|
||||
void ServiceDeferredInit() {
|
||||
if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return;
|
||||
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk still not mounted
|
||||
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
||||
if (Hci::InPumpContext()) return; // never nest under the transport pump
|
||||
|
||||
// Claim the pending init (this runs from the idle loop; make sure only
|
||||
// one pass performs the bring-up). The firmware download inside takes
|
||||
@@ -425,12 +462,12 @@ namespace Drivers::USB::Bluetooth {
|
||||
!g_initialized && Fs::Vfs::IsDriveRegistered(0)) {
|
||||
return true;
|
||||
}
|
||||
return g_initialized && Hci::HasPendingCommands();
|
||||
return g_initialized && (Hci::HasPendingCommands() || Hci::TransportHasRxData());
|
||||
}
|
||||
|
||||
void ServiceEvents() {
|
||||
if (!g_initialized) return;
|
||||
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
||||
if (Hci::InPumpContext()) return; // never nest under the transport pump
|
||||
A2dp::ServiceMedia(); // reap events and feed queued media
|
||||
// DrainEvents may just have queued an accept/auth/encryption reply.
|
||||
// Send it in this same service pass instead of adding a scheduler-turn
|
||||
@@ -492,7 +529,7 @@ namespace Drivers::USB::Bluetooth {
|
||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||
bool anyActive = true;
|
||||
while (anyActive && Timekeeping::GetMilliseconds() - t0 < 2000) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
anyActive = false;
|
||||
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||
@@ -548,7 +585,7 @@ namespace Drivers::USB::Bluetooth {
|
||||
// Poll until inquiry completes or timeout
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Hci::IsInquiryActive() && (Timekeeping::GetMilliseconds() - start < timeoutMs)) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
|
||||
for (int j = 0; j < 200; j++) {
|
||||
@@ -566,7 +603,7 @@ namespace Drivers::USB::Bluetooth {
|
||||
uint64_t cancelStart = Timekeeping::GetMilliseconds();
|
||||
while (Hci::IsInquiryActive()
|
||||
&& Timekeeping::GetMilliseconds() - cancelStart < 250) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
for (int j = 0; j < 100; j++)
|
||||
Arch::Pause();
|
||||
@@ -612,7 +649,7 @@ namespace Drivers::USB::Bluetooth {
|
||||
// Wait for Connection Complete event
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (!target && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
target = FindAclConnection(bdAddr);
|
||||
for (int j = 0; j < 200; j++) {
|
||||
@@ -631,7 +668,7 @@ namespace Drivers::USB::Bluetooth {
|
||||
if (reusedAcl) Hci::AuthenticateLink(target->Handle);
|
||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents();
|
||||
Hci::ProcessPendingCommands();
|
||||
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) break;
|
||||
+16
-9
@@ -8,28 +8,35 @@
|
||||
#include <cstdint>
|
||||
#include "Hci.hpp"
|
||||
|
||||
namespace Drivers::USB::Bluetooth {
|
||||
namespace Drivers::Bluetooth {
|
||||
|
||||
// Called by USB enumeration when a Bluetooth adapter is detected
|
||||
void RegisterAdapter(uint8_t slotId);
|
||||
|
||||
// Called by the boot driver probe: bring up an on-board UART controller
|
||||
// ACPI describes (the Raspberry Pi 4/400's Broadcom radio). Returns true
|
||||
// if one was found; its firmware-dependent init is deferred like USB's.
|
||||
bool ProbePlatformAdapter();
|
||||
|
||||
// Complete any firmware-dependent bring-up that was deferred because the
|
||||
// adapter enumerated during the boot port scan, before the ramdisk (which
|
||||
// holds the firmware images) was mounted. Safe to call unconditionally
|
||||
// once the boot filesystems are up; a no-op if nothing is pending.
|
||||
void ServiceDeferredInit();
|
||||
|
||||
// Steady-state event service: drains the xHCI event ring + Bluetooth ACL
|
||||
// RX ring and flushes queued HCI replies. Called from the idle loop so
|
||||
// Steady-state event service: drains the transport (xHCI event ring or
|
||||
// UART receive ring) + Bluetooth ACL RX ring and flushes queued HCI
|
||||
// replies. Called from the idle loop so
|
||||
// inbound traffic (the headset's SDP/AVRCP queries, AVDTP commands, ACL
|
||||
// credits) is processed even when no syscall happens to be pumping a BT
|
||||
// wait loop. Cheap no-op when the adapter is down; safe from any core
|
||||
// (PollEvents/DrainEvents/ProcessPendingCommands all self-serialize).
|
||||
// (Pump/DrainEvents/ProcessPendingCommands all self-serialize).
|
||||
void ServiceEvents();
|
||||
|
||||
// True when boot-deferred initialization or queued HCI control work needs
|
||||
// an idle-context service pass. USB receive events are signaled separately
|
||||
// by xHCI and cause the dispatcher to service Bluetooth in the same pass.
|
||||
// True when boot-deferred initialization, queued HCI control work or
|
||||
// received UART bytes need an idle-context service pass. USB receive
|
||||
// events are signaled separately by xHCI and cause the dispatcher to
|
||||
// service Bluetooth in the same pass.
|
||||
bool HasDeferredWork();
|
||||
|
||||
// Query adapter state
|
||||
@@ -40,8 +47,8 @@ namespace Drivers::USB::Bluetooth {
|
||||
const uint8_t* GetBdAddr();
|
||||
|
||||
// Change the adapter's BD_ADDR at runtime. Drops any active links (their
|
||||
// pairing/crypto was bound to the old address), issues the Intel vendor
|
||||
// write (0xFC31), re-reads the controller's address into the cache and
|
||||
// pairing/crypto was bound to the old address), issues the vendor write
|
||||
// (Intel 0xFC31, Broadcom 0xFC01), re-reads the controller's address into the cache and
|
||||
// re-asserts scan. Returns true only if the controller reports the new
|
||||
// address. Persisting the choice to 0:/config/bluetooth.toml is the
|
||||
// caller's job (userspace); the override is re-applied on boot by
|
||||
@@ -0,0 +1,555 @@
|
||||
/*
|
||||
* Broadcom.cpp
|
||||
* Broadcom/Cypress UART Bluetooth controller bring-up (Raspberry Pi 4/400)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Broadcom.hpp"
|
||||
#include "Hci.hpp"
|
||||
#include <ACPI/AcpiDevices.hpp>
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Drivers/Platform/RpiFirmware.hpp>
|
||||
#include <Drivers/Platform/RpiGpio.hpp>
|
||||
#include <Drivers/Serial/BtUart.hpp>
|
||||
#include <Drivers/Serial/Uart.hpp>
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Memory/Heap.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
// The Raspberry Pi 4B (CYW43455) and 400 (CYW43456) carry a Cypress radio
|
||||
// whose Bluetooth half speaks HCI over a UART (H4) on GPIO 30-33: CTS, RTS,
|
||||
// TXD, RXD. Its power switch, BT_ON, is line 0 of the VideoCore firmware's
|
||||
// GPIO expander.
|
||||
//
|
||||
// Bring-up follows Linux's hci_bcm + btbcm:
|
||||
// 1. power on, wait ~100 ms, HCI Reset at the controller's 115200 baud;
|
||||
// 2. Read Local Version: the LMP subversion names the chip and so the
|
||||
// patch file (BCM4345C0.hcd, BCM4345C5.hcd);
|
||||
// 3. raise both ends to the operating speed (vendor 0xFC18, answered at
|
||||
// the old speed);
|
||||
// 4. Download Minidriver (0xFC2E), wait 50 ms, then send every record of
|
||||
// the .hcd file -- each one is a complete HCI command, Write RAM
|
||||
// (0xFC4C) and finally Launch RAM (0xFC4E) -- and wait 250 ms;
|
||||
// 5. the patched firmware restarts at 115200: HCI Reset there, and raise
|
||||
// the speed again.
|
||||
//
|
||||
// Raspberry Pi OS's btuart runs the controller at 460800 without flow
|
||||
// control on the mini UART and at 3 Mbaud with it on the PL011. 460800 is
|
||||
// too slow for A2DP here: 8N1 carries 46 KB/s and SBC at bitpool 53 needs
|
||||
// ~46 KB/s with its headers, so music broke up whenever anything else used
|
||||
// the link. The mini UART's auto RTS/CTS allows 921600, so it is tried
|
||||
// first; without flow control (460800) is the fallback, and A2DP lowers its
|
||||
// bitpool to fit (Hci::TransportBytesPerSecond).
|
||||
//
|
||||
// Our RTS must be deasserted while the controller powers on: with it
|
||||
// asserted the controller boots into autobaud mode (see PowerCycle).
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Bluetooth::Broadcom {
|
||||
|
||||
namespace BtUart = Drivers::Serial::BtUart;
|
||||
namespace RpiFirmware = Drivers::Platform::RpiFirmware;
|
||||
namespace RpiGpio = Drivers::Platform::RpiGpio;
|
||||
|
||||
// GPIO 30-33 carry CTS, RTS, TXD, RXD: alternate function 3 routes them
|
||||
// to the PL011, 5 to the mini UART. Pulls as Linux's bcm2711 pinctrl
|
||||
// sets them (uart0_ctsrts_gpio30, uart0_gpio32).
|
||||
static constexpr uint32_t PIN_CTS = 30;
|
||||
static constexpr uint32_t PIN_RTS = 31;
|
||||
static constexpr uint32_t PIN_TXD = 32;
|
||||
static constexpr uint32_t PIN_RXD = 33;
|
||||
|
||||
static constexpr uint32_t InitialBaud = 115200;
|
||||
|
||||
// LMP subversions of the ROM firmware -> patch file (Linux btbcm's
|
||||
// bcm_uart_subver_table).
|
||||
struct ChipName {
|
||||
uint16_t subversion;
|
||||
const char* name;
|
||||
};
|
||||
static constexpr ChipName g_chips[] = {
|
||||
{ 0x6119, "BCM4345C0" }, // CYW43455: Raspberry Pi 3B+, 4B
|
||||
{ 0x6606, "BCM4345C5" }, // CYW43456: Raspberry Pi 400, CM4
|
||||
};
|
||||
|
||||
static bool g_flowControl = false;
|
||||
static bool g_powerSwitch = true;
|
||||
|
||||
// =========================================================================
|
||||
// Platform probe
|
||||
// =========================================================================
|
||||
|
||||
static bool EndsWith(const char* s, const char* suffix) {
|
||||
int sl = 0, xl = 0;
|
||||
while (s[sl]) sl++;
|
||||
while (suffix[xl]) xl++;
|
||||
if (xl > sl) return false;
|
||||
for (int i = 0; i < xl; i++) {
|
||||
if (s[sl - xl + i] != suffix[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The controller's UartSerialBusV2 descriptor (ACPI 6.4, 6.4.3.8.2.3)
|
||||
// names the UART it hangs off in its ResourceSource string.
|
||||
static bool ReadUartSource(int32_t node, char* source, int maxLength) {
|
||||
Hal::AML::Object crs;
|
||||
if (!Hal::AML::GetInterpreter().EvaluateChild(node, "_CRS", nullptr, 0, crs) ||
|
||||
crs.Type != Hal::AML::ObjectType::Buffer) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t* data = crs.Buffer.Data;
|
||||
uint32_t length = crs.Buffer.Length;
|
||||
uint32_t pos = 0;
|
||||
while (pos < length) {
|
||||
uint8_t tag = data[pos];
|
||||
if (!(tag & 0x80)) {
|
||||
if ((tag & 0x78) == 0x78) break; // end tag
|
||||
pos += 1 + (tag & 0x07);
|
||||
continue;
|
||||
}
|
||||
if (pos + 3 > length) break;
|
||||
uint32_t descLength = (uint32_t)data[pos + 1] | ((uint32_t)data[pos + 2] << 8);
|
||||
uint32_t end = pos + 3 + descLength;
|
||||
if (end > length) break;
|
||||
static constexpr uint8_t SerialBusTag = 0x8E, SerialBusUart = 3;
|
||||
if (tag == SerialBusTag && descLength >= 9 && data[pos + 5] == SerialBusUart) {
|
||||
uint32_t typeDataLength = (uint32_t)data[pos + 10] | ((uint32_t)data[pos + 11] << 8);
|
||||
uint32_t s = pos + 12 + typeDataLength;
|
||||
int n = 0;
|
||||
while (s < end && data[s] != 0 && n < maxLength - 1) source[n++] = (char)data[s++];
|
||||
source[n] = '\0';
|
||||
return n > 0;
|
||||
}
|
||||
pos = end;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ReadInteger(int32_t node, const char* name, uint64_t& value) {
|
||||
Hal::AML::Object obj;
|
||||
if (!Hal::AML::GetInterpreter().EvaluateChild(node, name, nullptr, 0, obj) ||
|
||||
obj.Type != Hal::AML::ObjectType::Integer) {
|
||||
return false;
|
||||
}
|
||||
value = obj.Integer;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProbePlatform() {
|
||||
Hal::AcpiDevices::DeviceInfo bt{};
|
||||
if (!Hal::AcpiDevices::FindDeviceByHid("BCM2EA6", bt)) return false;
|
||||
|
||||
// Which UART: the descriptor's source, else the firmware's "UART in
|
||||
// use" flag (0: the PL011 is the console, Bluetooth has the mini UART).
|
||||
BtUart::Kind kind = BtUart::Kind::MiniUart;
|
||||
char source[64] = {};
|
||||
uint64_t uartInUse = 0;
|
||||
if (ReadUartSource(bt.NodeIndex, source, sizeof(source))) {
|
||||
kind = EndsWith(source, "URT0") ? BtUart::Kind::Pl011 : BtUart::Kind::MiniUart;
|
||||
} else if (ReadInteger(bt.NodeIndex, "URIU", uartInUse)) {
|
||||
kind = uartInUse == 0 ? BtUart::Kind::MiniUart : BtUart::Kind::Pl011;
|
||||
}
|
||||
bool mini = kind == BtUart::Kind::MiniUart;
|
||||
|
||||
Hal::AcpiDevices::DeviceInfo uart{};
|
||||
if (!Hal::AcpiDevices::FindDeviceByHid(mini ? "BCM2836" : "BCM2837", uart)) {
|
||||
KernelLogStream(WARNING, "BT-BCM") << bt.Path << ": its UART "
|
||||
<< (source[0] ? source : (mini ? "(mini UART)" : "(PL011)")) << " is not in ACPI";
|
||||
return false;
|
||||
}
|
||||
|
||||
Hal::AML::ResourceList resources;
|
||||
uint64_t base = 0, length = 0;
|
||||
if (!Hal::AcpiDevices::EvaluateCrs(uart.NodeIndex, resources) ||
|
||||
!resources.GetMemory(base, length) || base == 0) {
|
||||
KernelLogStream(WARNING, "BT-BCM") << uart.Path << ": no register block in _CRS";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t console = Drivers::Serial::ConsoleAddress();
|
||||
if (console != 0 && (console & ~0xFFFULL) == (base & ~0xFFFULL)) {
|
||||
KernelLogStream(WARNING, "BT-BCM") << "Bluetooth's UART " << uart.Path
|
||||
<< " is the serial console; leaving Bluetooth off";
|
||||
return false;
|
||||
}
|
||||
|
||||
BtUart::Config config{};
|
||||
config.kind = kind;
|
||||
config.mmioPhys = base;
|
||||
config.levelTriggered = true;
|
||||
if (auto* irq = resources.FindFirst(Hal::AML::ResourceType::ExtendedIrq)) {
|
||||
config.gsi = irq->ExtendedIrq.Interrupt;
|
||||
config.levelTriggered = (irq->ExtendedIrq.Flags & 1) == 0;
|
||||
config.activeLow = (irq->ExtendedIrq.Flags & 2) != 0;
|
||||
}
|
||||
|
||||
// The mini UART's baud generator runs off the VPU core clock: ask the
|
||||
// firmware for its current rate, else take the rate the firmware
|
||||
// patched into the table (MUCR; the placeholder 0x1000000 means it
|
||||
// did not). The PL011's reference clock is fixed (CLCK, 48 MHz).
|
||||
uint64_t tableClock = 0;
|
||||
uint32_t clockHz = 0;
|
||||
const char* clockSource = "firmware";
|
||||
if (mini) {
|
||||
if (!RpiFirmware::GetClockRate(RpiFirmware::CLOCK_CORE, clockHz)) {
|
||||
if (ReadInteger(uart.NodeIndex, "MUCR", tableClock) && tableClock > 0x1000000) {
|
||||
clockHz = (uint32_t)tableClock;
|
||||
clockSource = "ACPI MUCR";
|
||||
} else {
|
||||
clockHz = 500000000;
|
||||
clockSource = "assumed";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clockSource = "PL011 reference";
|
||||
clockHz = ReadInteger(uart.NodeIndex, "CLCK", tableClock) && tableClock != 0
|
||||
? (uint32_t)tableClock : 48000000;
|
||||
}
|
||||
config.clockHz = clockHz;
|
||||
|
||||
RpiGpio::Function function = mini ? RpiGpio::Function::Alt5 : RpiGpio::Function::Alt3;
|
||||
bool pinsOk = RpiGpio::SetFunction(PIN_CTS, function)
|
||||
&& RpiGpio::SetFunction(PIN_RTS, function)
|
||||
&& RpiGpio::SetFunction(PIN_TXD, function)
|
||||
&& RpiGpio::SetFunction(PIN_RXD, function)
|
||||
&& RpiGpio::SetPull(PIN_CTS, RpiGpio::Pull::Up)
|
||||
&& RpiGpio::SetPull(PIN_RTS, RpiGpio::Pull::None)
|
||||
&& RpiGpio::SetPull(PIN_TXD, RpiGpio::Pull::None)
|
||||
&& RpiGpio::SetPull(PIN_RXD, RpiGpio::Pull::Up);
|
||||
if (!pinsOk) {
|
||||
KernelLogStream(WARNING, "BT-BCM")
|
||||
<< "No GPIO controller (BCM2845): UART pins left as the firmware set them";
|
||||
}
|
||||
|
||||
if (!BtUart::Initialize(config)) return false;
|
||||
|
||||
Hci::InitializeUart();
|
||||
Hci::SetVendor(Hci::Vendor::Broadcom);
|
||||
KernelLogStream(OK, "BT-BCM") << "Bluetooth controller " << bt.Path << " on "
|
||||
<< uart.Path << " (GSI " << (uint64_t)config.gsi << ", clock " << clockSource << ")";
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Controller bring-up
|
||||
// =========================================================================
|
||||
|
||||
// BT_ON through the firmware's expander. The line has to be an output
|
||||
// first (hci_bcm's GPIOD_OUT_LOW request does that on Linux); plain
|
||||
// SET_GPIO_STATE is the fallback for firmware without the config tag.
|
||||
static bool SetBtOn(bool on) {
|
||||
return RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_BT_ON, on)
|
||||
|| RpiFirmware::SetGpioState(RpiFirmware::EXPGPIO_BT_ON, on);
|
||||
}
|
||||
|
||||
// The controller samples its CTS (our RTS) as it leaves reset. Asserted,
|
||||
// it boots into autobaud mode, where it answers HCI Reset and silently
|
||||
// drops nearly everything else (on a Pi 400: Read Local Version, Read
|
||||
// PIN Type, Write PIN Type, at any baud rate). hci_bcm deasserts it
|
||||
// around power-on for the same reason; SyncAtInitialBaud asserts it
|
||||
// again once the controller is up.
|
||||
static void PowerCycle() {
|
||||
if (g_powerSwitch) {
|
||||
BtUart::DeassertRts();
|
||||
if (SetBtOn(false)) {
|
||||
Timekeeping::BusyWaitMs(20);
|
||||
bool on = SetBtOn(true);
|
||||
bool state = false;
|
||||
bool readBack = RpiFirmware::GetGpioState(RpiFirmware::EXPGPIO_BT_ON, state);
|
||||
KernelLogStream(on ? INFO : WARNING, "BT-BCM") << "BT_ON "
|
||||
<< (on ? "switched on" : "did not switch on") << " (reads back "
|
||||
<< (readBack ? (state ? "high" : "low") : "unknown") << ")";
|
||||
// hci_bcm allows 100-120 ms to power on and leave reset.
|
||||
Timekeeping::BusyWaitMs(150);
|
||||
} else {
|
||||
g_powerSwitch = false;
|
||||
KernelLogStream(WARNING, "BT-BCM")
|
||||
<< "Cannot switch BT_ON through the firmware; assuming the controller is powered";
|
||||
}
|
||||
}
|
||||
Hci::ResetUartReceiver();
|
||||
}
|
||||
|
||||
// Whether received bytes move by interrupt and whether any were lost:
|
||||
// the first thing to know when the controller seems not to answer.
|
||||
static void LogUartStats(const char* when) {
|
||||
BtUart::Stats stats{};
|
||||
BtUart::GetStats(stats);
|
||||
KernelLogStream(INFO, "BT-BCM") << when << ": UART rx " << stats.rxBytes << " bytes, tx "
|
||||
<< stats.txBytes << ", " << stats.interrupts << " interrupts, " << stats.overruns
|
||||
<< " overruns, " << stats.rxDropped << " dropped, " << (uint64_t)BtUart::GetBaud()
|
||||
<< " baud, " << (BtUart::FlowControl() ? "RTS flow control" : "no flow control");
|
||||
}
|
||||
|
||||
static bool ResetController(uint32_t timeoutMs) {
|
||||
if (!Hci::SendCommand(Hci::OP_RESET, nullptr, 0)) return false;
|
||||
if (!Hci::WaitCommandComplete(Hci::OP_RESET, nullptr, 0, timeoutMs)) return false;
|
||||
// The controller acknowledges the reset before it has finished it
|
||||
// and ignores commands until then; btbcm_reset waits 100 ms.
|
||||
Timekeeping::BusyWaitMs(100);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reach the controller at its power-on 115200 baud and read its ROM
|
||||
// version. A flow-control mode counts only once a second command has
|
||||
// been answered too: a controller in autobaud mode answers HCI Reset
|
||||
// alone, and a flow-control polarity that holds either side back fails
|
||||
// here rather than later.
|
||||
static bool SyncAtInitialBaud(Hci::LocalVersion& rom) {
|
||||
BtUart::SetBaud(InitialBaud);
|
||||
static constexpr bool attempts[] = { true, true, false, false };
|
||||
for (bool flow : attempts) {
|
||||
BtUart::SetFlowControl(flow);
|
||||
// Events the controller held back while it saw RTS deasserted
|
||||
// pour out once it is asserted: let them arrive and drop them,
|
||||
// so they neither overrun the FIFO under the next reply nor
|
||||
// leave the deframer mid-packet.
|
||||
Timekeeping::BusyWaitMs(50);
|
||||
Hci::ResetUartReceiver();
|
||||
if (!ResetController(1000)) {
|
||||
LogUartStats(flow ? "No reset reply with flow control" : "No reset reply");
|
||||
continue;
|
||||
}
|
||||
// A controller slow to finish its reset gets a second chance.
|
||||
bool answered = Hci::ReadLocalVersion(&rom);
|
||||
if (!answered) {
|
||||
Timekeeping::BusyWaitMs(200);
|
||||
answered = Hci::ReadLocalVersion(&rom);
|
||||
}
|
||||
if (answered) {
|
||||
g_flowControl = flow;
|
||||
return true;
|
||||
}
|
||||
LogUartStats(flow ? "Reset answered but not Read Local Version, with flow control"
|
||||
: "Reset answered but not Read Local Version");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint32_t PickOperatingBaud() {
|
||||
uint32_t wanted;
|
||||
if (BtUart::GetKind() == BtUart::Kind::Pl011) {
|
||||
wanted = g_flowControl ? 3000000 : 921600;
|
||||
} else if (!BtUart::HasInterrupt()) {
|
||||
// Polled 8-byte FIFO: stay where nothing overruns.
|
||||
return InitialBaud;
|
||||
} else {
|
||||
wanted = g_flowControl ? 921600 : 460800;
|
||||
}
|
||||
static constexpr uint32_t rates[] = { 3000000, 2000000, 921600, 460800, 230400 };
|
||||
for (uint32_t rate : rates) {
|
||||
if (rate <= wanted && BtUart::BaudReachable(rate)) return rate;
|
||||
}
|
||||
return InitialBaud;
|
||||
}
|
||||
|
||||
// Both ends to `baud`. The controller answers 0xFC18 at the old speed,
|
||||
// then switches. False leaves the host at its old speed; the controller
|
||||
// may or may not have switched (the caller power-cycles to be sure).
|
||||
static bool SwitchBaud(uint32_t baud) {
|
||||
uint32_t oldBaud = BtUart::GetBaud();
|
||||
uint8_t params[6] = {
|
||||
0x00, 0x00,
|
||||
(uint8_t)baud, (uint8_t)(baud >> 8), (uint8_t)(baud >> 16), (uint8_t)(baud >> 24),
|
||||
};
|
||||
if (!Hci::SendCommand(Hci::OP_BCM_UPDATE_BAUD_RATE, params, sizeof(params)) ||
|
||||
!Hci::WaitCommandComplete(Hci::OP_BCM_UPDATE_BAUD_RATE, nullptr, 0, 1000)) {
|
||||
KernelLogStream(WARNING, "BT-BCM") << "Controller refused " << (uint64_t)baud << " baud";
|
||||
return false;
|
||||
}
|
||||
if (!BtUart::SetBaud(baud)) return false;
|
||||
Timekeeping::BusyWaitMs(10);
|
||||
Hci::ResetUartReceiver();
|
||||
|
||||
Hci::LocalVersion ver{};
|
||||
if (Hci::ReadLocalVersion(&ver)) return true;
|
||||
|
||||
KernelLogStream(WARNING, "BT-BCM") << "No answer at " << (uint64_t)baud << " baud";
|
||||
BtUart::SetBaud(oldBaud);
|
||||
Hci::ResetUartReceiver();
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char* ChipFor(uint16_t subversion) {
|
||||
for (const auto& chip : g_chips) {
|
||||
if (chip.subversion == subversion) return chip.name;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Load 0:/os/firmware/brcm/<chip>.hcd. Returns nullptr (and logs) when
|
||||
// the file is missing; the caller frees the image.
|
||||
static uint8_t* LoadPatch(const char* chip, uint64_t& size) {
|
||||
char path[64];
|
||||
const char* prefix = "0:/os/firmware/brcm/";
|
||||
int n = 0;
|
||||
for (const char* p = prefix; *p; p++) path[n++] = *p;
|
||||
for (const char* p = chip; *p && n < 56; p++) path[n++] = *p;
|
||||
for (const char* p = ".hcd"; *p; p++) path[n++] = *p;
|
||||
path[n] = '\0';
|
||||
|
||||
Fs::Vfs::BackendFile file;
|
||||
if (Fs::Vfs::OpenBackendFile(path, file) < 0) {
|
||||
KernelLogStream(WARNING, "BT-BCM") << "Firmware patch not found: " << path;
|
||||
return nullptr;
|
||||
}
|
||||
size = Fs::Vfs::GetBackendFileSize(file);
|
||||
uint8_t* image = size ? (uint8_t*)Memory::g_heap->Request(size) : nullptr;
|
||||
if (image == nullptr) {
|
||||
Fs::Vfs::CloseBackendFile(file);
|
||||
return nullptr;
|
||||
}
|
||||
int got = Fs::Vfs::ReadBackendFile(file, image, 0, size);
|
||||
Fs::Vfs::CloseBackendFile(file);
|
||||
if (got != (int)size) {
|
||||
Memory::g_heap->Free(image);
|
||||
KernelLogStream(ERROR, "BT-BCM") << "Short read of " << path;
|
||||
return nullptr;
|
||||
}
|
||||
KernelLogStream(INFO, "BT-BCM") << "Loading " << path << " (" << size << " bytes)";
|
||||
return image;
|
||||
}
|
||||
|
||||
static bool DownloadPatch(const uint8_t* image, uint64_t size) {
|
||||
if (!Hci::SendCommand(Hci::OP_BCM_DOWNLOAD_MINIDRV, nullptr, 0) ||
|
||||
!Hci::WaitCommandComplete(Hci::OP_BCM_DOWNLOAD_MINIDRV, nullptr, 0, 2000)) {
|
||||
KernelLogStream(ERROR, "BT-BCM") << "Download Minidriver failed";
|
||||
return false;
|
||||
}
|
||||
Timekeeping::BusyWaitMs(50);
|
||||
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
uint64_t off = 0;
|
||||
uint32_t records = 0;
|
||||
while (off + 3 <= size) {
|
||||
uint16_t opcode = (uint16_t)image[off] | ((uint16_t)image[off + 1] << 8);
|
||||
uint8_t paramLength = image[off + 2];
|
||||
off += 3;
|
||||
if (off + paramLength > size) {
|
||||
KernelLogStream(ERROR, "BT-BCM") << "Truncated patch record " << (uint64_t)records;
|
||||
return false;
|
||||
}
|
||||
if (!Hci::SendCommand(opcode, image + off, paramLength)) return false;
|
||||
bool acked = Hci::WaitCommandComplete(opcode, nullptr, 0, 2000);
|
||||
if (!acked && opcode != Hci::OP_BCM_LAUNCH_RAM) {
|
||||
KernelLogStream(ERROR, "BT-BCM") << "Patch record " << (uint64_t)records
|
||||
<< " (opcode " << base::hex << (uint64_t)opcode << base::dec << ") failed";
|
||||
return false;
|
||||
}
|
||||
off += paramLength;
|
||||
records++;
|
||||
}
|
||||
// The patched firmware starts after Launch RAM; btbcm waits 250 ms.
|
||||
Timekeeping::BusyWaitMs(250);
|
||||
KernelLogStream(OK, "BT-BCM") << "Patch downloaded: " << (uint64_t)records << " records in "
|
||||
<< (Timekeeping::GetMilliseconds() - start) << " ms at "
|
||||
<< (uint64_t)BtUart::GetBaud() << " baud";
|
||||
return true;
|
||||
}
|
||||
|
||||
static void LogVersion(const char* chip, const Hci::LocalVersion& ver) {
|
||||
uint16_t sub = ver.LmpSubversion;
|
||||
KernelLogStream(OK, "BT-BCM") << (chip ? chip : "Broadcom controller") << " ("
|
||||
<< (uint64_t)((sub & 0xE000) >> 13) << "." << (uint64_t)((sub & 0x1F00) >> 8)
|
||||
<< "." << (uint64_t)(sub & 0x00FF) << ") build " << (uint64_t)(ver.HciRevision & 0x0FFF)
|
||||
<< ", " << (uint64_t)BtUart::GetBaud() << " baud, "
|
||||
<< (g_flowControl ? "RTS flow control" : "no flow control");
|
||||
}
|
||||
|
||||
// One complete bring-up. `fast` raises the speed for the download and
|
||||
// afterwards; without it everything stays at 115200.
|
||||
static bool Attempt(bool fast) {
|
||||
PowerCycle();
|
||||
Hci::LocalVersion rom{};
|
||||
if (!SyncAtInitialBaud(rom)) {
|
||||
KernelLogStream(ERROR, "BT-BCM") << "Controller does not answer HCI commands"
|
||||
<< (g_powerSwitch ? "" : " (BT_ON was never switched on: it is likely unpowered)");
|
||||
return false;
|
||||
}
|
||||
const char* chip = ChipFor(rom.LmpSubversion);
|
||||
KernelLogStream(INFO, "BT-BCM") << "ROM firmware: LMP subversion " << base::hex
|
||||
<< (uint64_t)rom.LmpSubversion << ", HCI revision " << (uint64_t)rom.HciRevision
|
||||
<< base::dec << (chip ? " -> " : " (unknown chip)") << (chip ? chip : "");
|
||||
|
||||
uint32_t operating = fast ? PickOperatingBaud() : InitialBaud;
|
||||
if (operating != InitialBaud && !SwitchBaud(operating)) return false;
|
||||
|
||||
uint64_t size = 0;
|
||||
uint8_t* image = chip ? LoadPatch(chip, size) : nullptr;
|
||||
if (image != nullptr) {
|
||||
bool ok = DownloadPatch(image, size);
|
||||
Memory::g_heap->Free(image);
|
||||
if (!ok) return false;
|
||||
|
||||
// The patched firmware comes up at 115200 again.
|
||||
BtUart::SetBaud(InitialBaud);
|
||||
Hci::ResetUartReceiver();
|
||||
bool reset = false;
|
||||
for (int i = 0; i < 3 && !reset; i++) reset = ResetController(1000);
|
||||
if (!reset) {
|
||||
KernelLogStream(ERROR, "BT-BCM") << "Patched firmware does not answer HCI Reset";
|
||||
return false;
|
||||
}
|
||||
if (operating != InitialBaud && !SwitchBaud(operating)) return false;
|
||||
} else {
|
||||
KernelLogStream(WARNING, "BT-BCM") << "Running the controller's ROM firmware";
|
||||
}
|
||||
|
||||
Hci::LocalVersion ver{};
|
||||
if (Hci::ReadLocalVersion(&ver)) LogVersion(chip, ver);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Initialize() {
|
||||
if (Hci::GetTransport() != Hci::Transport::Uart) return false;
|
||||
if (Attempt(true)) return true;
|
||||
KernelLogStream(WARNING, "BT-BCM") << "Retrying bring-up at 115200 baud";
|
||||
return Attempt(false);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// BD_ADDR
|
||||
// =========================================================================
|
||||
|
||||
// Addresses are in HCI order (addr[0] = least significant octet).
|
||||
static bool IsDefaultAddress(const uint8_t* addr) {
|
||||
bool allZero = true, allOnes = true;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (addr[i] != 0x00) allZero = false;
|
||||
if (addr[i] != 0xFF) allOnes = false;
|
||||
}
|
||||
if (allZero || allOnes) return true;
|
||||
// The chip-wide defaults Linux's btbcm lists for these parts (the
|
||||
// BCM4345C5's is 43:45:C5:00:1F:AC) start with 0x43, which has the
|
||||
// IEEE group bit set: no assigned device address does.
|
||||
return (addr[5] & 0x01) != 0;
|
||||
}
|
||||
|
||||
void FixDefaultAddress() {
|
||||
uint8_t addr[6];
|
||||
if (!Hci::ReadBdAddr(addr) || !IsDefaultAddress(addr)) return;
|
||||
|
||||
uint8_t mac[6];
|
||||
if (!RpiFirmware::GetBoardMacAddress(mac)) {
|
||||
KernelLogStream(WARNING, "BT-BCM")
|
||||
<< "Controller has its chip-wide default BD_ADDR and no board MAC is available";
|
||||
return;
|
||||
}
|
||||
// The board's Ethernet MAC is unique to it and in the Raspberry Pi's
|
||||
// own address block.
|
||||
uint8_t board[6];
|
||||
for (int i = 0; i < 6; i++) board[i] = mac[5 - i];
|
||||
if (Hci::SetBdAddr(board)) {
|
||||
KernelLogStream(OK, "BT-BCM") << "Replaced the chip-wide default BD_ADDR with the board's";
|
||||
} else {
|
||||
KernelLogStream(WARNING, "BT-BCM") << "Controller rejected the board BD_ADDR";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Broadcom.hpp
|
||||
* Broadcom/Cypress UART Bluetooth controller bring-up (Raspberry Pi 4/400)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Drivers::Bluetooth::Broadcom {
|
||||
|
||||
// Find the on-board controller ACPI describes (BCM2EA6, \_SB.GDV0.BTH0),
|
||||
// route its UART to GPIO 30-33 and start the UART and the H4 HCI
|
||||
// transport. Needs no firmware files, so it runs during the boot driver
|
||||
// probe. Returns false when there is no such controller.
|
||||
bool ProbePlatform();
|
||||
|
||||
// Power-cycle the controller, load its firmware patch from
|
||||
// 0:/os/firmware/brcm/ and raise the UART to its operating speed.
|
||||
// Leaves the controller freshly reset and ready for standard HCI.
|
||||
bool Initialize();
|
||||
|
||||
// After the last HCI Reset: give a controller still on its chip-wide
|
||||
// default BD_ADDR (the CYW43456 ships with one) a board-unique address
|
||||
// from the Pi's Ethernet MAC.
|
||||
void FixDefaultAddress();
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hci.cpp
|
||||
* Bluetooth HCI transport over USB
|
||||
* Bluetooth HCI transport over USB or a UART (H4)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "A2dp.hpp"
|
||||
#include <atomic>
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Drivers/Serial/BtUart.hpp>
|
||||
#include <Drivers/USB/Xhci.hpp>
|
||||
#include <Drivers/USB/UsbDevice.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
@@ -22,12 +23,17 @@
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::USB::Bluetooth::Hci {
|
||||
namespace Drivers::Bluetooth::Hci {
|
||||
|
||||
namespace Xhci = Drivers::USB::Xhci;
|
||||
namespace BtUart = Drivers::Serial::BtUart;
|
||||
|
||||
// =========================================================================
|
||||
// State
|
||||
// =========================================================================
|
||||
|
||||
static Transport g_transport = Transport::None;
|
||||
static Vendor g_vendor = Vendor::Unknown;
|
||||
static uint8_t g_slotId = 0;
|
||||
static bool g_initialized = false;
|
||||
|
||||
@@ -541,6 +547,161 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// UART (H4) receive deframing
|
||||
// =========================================================================
|
||||
|
||||
// The UART delivers one byte stream: [type][header][payload]... Pump()
|
||||
// feeds it here in process context, so complete packets go straight to
|
||||
// the same command mailbox / async event ring / ACL ring the USB
|
||||
// callback fills. A byte that cannot start a packet is dropped, which
|
||||
// resynchronizes after line noise (e.g. while the controller powers up).
|
||||
enum class H4State : uint8_t { Type, Header, Payload, Discard };
|
||||
|
||||
static H4State g_h4State = H4State::Type;
|
||||
static uint8_t g_h4Type = 0;
|
||||
static uint8_t g_h4Packet[ACL_RX_SLOT_SIZE] = {};
|
||||
static uint16_t g_h4Len = 0; // bytes in g_h4Packet
|
||||
static uint16_t g_h4HeaderLen = 0;
|
||||
static uint16_t g_h4Expected = 0; // header + payload
|
||||
static uint32_t g_h4Remaining = 0; // Discard: bytes left to skip
|
||||
static uint32_t g_h4Dropped = 0; // bytes that could not start a packet
|
||||
|
||||
// The UART receive ring and the deframer have one consumer at a time:
|
||||
// whoever holds this (Pump, or a flush).
|
||||
static std::atomic<bool> g_uartConsumer{false};
|
||||
|
||||
void ResetUartReceiver() {
|
||||
bool expected = false;
|
||||
while (!g_uartConsumer.compare_exchange_weak(expected, true,
|
||||
std::memory_order_acquire)) {
|
||||
expected = false;
|
||||
Arch::Pause();
|
||||
}
|
||||
BtUart::FlushRx();
|
||||
g_h4State = H4State::Type;
|
||||
g_h4Len = 0;
|
||||
g_h4Remaining = 0;
|
||||
g_uartConsumer.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
static void CompleteH4Packet() {
|
||||
if (g_h4Type == H4_EVENT) {
|
||||
QueueCompleteHciEvent(g_h4Packet, g_h4Len);
|
||||
} else if (g_h4Type == H4_ACL) {
|
||||
QueueCompleteAclPacket(g_h4Packet, g_h4Len);
|
||||
}
|
||||
}
|
||||
|
||||
static void FeedH4Bytes(const uint8_t* data, uint32_t length) {
|
||||
uint32_t off = 0;
|
||||
while (off < length) {
|
||||
switch (g_h4State) {
|
||||
case H4State::Type: {
|
||||
uint8_t type = data[off++];
|
||||
g_h4Type = type;
|
||||
g_h4Len = 0;
|
||||
if (type == H4_EVENT) g_h4HeaderLen = sizeof(EventHeader);
|
||||
else if (type == H4_ACL) g_h4HeaderLen = sizeof(AclHeader);
|
||||
else if (type == H4_SCO) g_h4HeaderLen = 3;
|
||||
else { g_h4Dropped++; break; }
|
||||
g_h4State = H4State::Header;
|
||||
break;
|
||||
}
|
||||
|
||||
case H4State::Header: {
|
||||
g_h4Packet[g_h4Len++] = data[off++];
|
||||
if (g_h4Len < g_h4HeaderLen) break;
|
||||
uint32_t payload;
|
||||
if (g_h4Type == H4_EVENT) payload = g_h4Packet[1];
|
||||
else if (g_h4Type == H4_ACL) payload = (uint32_t)g_h4Packet[2]
|
||||
| ((uint32_t)g_h4Packet[3] << 8);
|
||||
else payload = g_h4Packet[2];
|
||||
// SCO is not used; an ACL packet beyond the ring slot
|
||||
// cannot be a valid one from a controller that reported
|
||||
// its buffer size. Skip either without losing framing.
|
||||
if (g_h4Type == H4_SCO
|
||||
|| g_h4HeaderLen + payload > sizeof(g_h4Packet)) {
|
||||
g_h4Remaining = payload;
|
||||
g_h4State = payload ? H4State::Discard : H4State::Type;
|
||||
break;
|
||||
}
|
||||
g_h4Expected = (uint16_t)(g_h4HeaderLen + payload);
|
||||
if (payload == 0) {
|
||||
CompleteH4Packet();
|
||||
g_h4State = H4State::Type;
|
||||
} else {
|
||||
g_h4State = H4State::Payload;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case H4State::Payload: {
|
||||
uint32_t take = g_h4Expected - g_h4Len;
|
||||
if (take > length - off) take = length - off;
|
||||
memcpy(&g_h4Packet[g_h4Len], &data[off], take);
|
||||
g_h4Len = (uint16_t)(g_h4Len + take);
|
||||
off += take;
|
||||
if (g_h4Len == g_h4Expected) {
|
||||
CompleteH4Packet();
|
||||
g_h4State = H4State::Type;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case H4State::Discard: {
|
||||
uint32_t take = length - off;
|
||||
if (take > g_h4Remaining) take = g_h4Remaining;
|
||||
off += take;
|
||||
g_h4Remaining -= take;
|
||||
if (g_h4Remaining == 0) g_h4State = H4State::Type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Transport pump
|
||||
// =========================================================================
|
||||
|
||||
void Pump() {
|
||||
if (g_transport == Transport::Usb) {
|
||||
Xhci::PollEvents();
|
||||
return;
|
||||
}
|
||||
if (g_transport != Transport::Uart) return;
|
||||
|
||||
bool expected = false;
|
||||
if (!g_uartConsumer.compare_exchange_strong(expected, true,
|
||||
std::memory_order_acquire)) return;
|
||||
// Also moves bytes by hand, in case the UART interrupt never fires.
|
||||
BtUart::Service();
|
||||
uint8_t chunk[256];
|
||||
int n;
|
||||
while ((n = BtUart::Read(chunk, sizeof(chunk))) > 0) {
|
||||
FeedH4Bytes(chunk, (uint32_t)n);
|
||||
}
|
||||
g_uartConsumer.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool InPumpContext() {
|
||||
// UART packets are only queued by Pump, never handled inside it.
|
||||
return g_transport == Transport::Usb && Xhci::InPollContext();
|
||||
}
|
||||
|
||||
bool TransportHasRxData() {
|
||||
return g_transport == Transport::Uart && BtUart::HasRxData();
|
||||
}
|
||||
|
||||
Transport GetTransport() { return g_transport; }
|
||||
|
||||
uint32_t TransportBytesPerSecond() {
|
||||
return g_transport == Transport::Uart ? BtUart::GetBaud() / 10 : 0;
|
||||
}
|
||||
void SetVendor(Vendor vendor) { g_vendor = vendor; }
|
||||
Vendor GetVendor() { return g_vendor; }
|
||||
|
||||
// =========================================================================
|
||||
// USB transfer callback
|
||||
// =========================================================================
|
||||
@@ -641,7 +802,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
static void PollWait(uint32_t ms) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < ms) {
|
||||
Xhci::PollEvents();
|
||||
Pump();
|
||||
DrainEvents();
|
||||
for (int j = 0; j < 100; j++) {
|
||||
Arch::CompilerBarrier();
|
||||
@@ -672,10 +833,20 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// the configured USB function its settling delay. StartEventPipe then
|
||||
// arms both receive endpoints before the first bootloader command.
|
||||
|
||||
g_transport = Transport::Usb;
|
||||
g_initialized = true;
|
||||
KernelLogStream(OK, "BT-HCI") << "HCI transport initialized on slot " << (uint64_t)slotId;
|
||||
}
|
||||
|
||||
void InitializeUart() {
|
||||
// No DMA buffers: SendCommand/SendAcl copy each H4 packet straight
|
||||
// into the UART's transmit ring.
|
||||
ResetUartReceiver();
|
||||
g_transport = Transport::Uart;
|
||||
g_initialized = true;
|
||||
KernelLogStream(OK, "BT-HCI") << "HCI transport initialized on UART";
|
||||
}
|
||||
|
||||
// Start receiving HCI events and ACL data — call after HCI init sequence.
|
||||
// Arms BOTH the interrupt IN (events) and the bulk IN (ACL). The bulk IN
|
||||
// MUST stay armed across the firmware download: on this controller the device
|
||||
@@ -712,7 +883,8 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// =========================================================================
|
||||
|
||||
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen) {
|
||||
if (!g_initialized || !g_cmdDmaBuf) return false;
|
||||
if (!g_initialized) return false;
|
||||
if (g_transport == Transport::Usb && !g_cmdDmaBuf) return false;
|
||||
|
||||
uint16_t expected = 0;
|
||||
if (!g_commandOpcode.compare_exchange_strong(expected, opcode,
|
||||
@@ -745,6 +917,27 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
<< base::hex << (uint64_t)opcode << base::dec;
|
||||
}
|
||||
|
||||
if (g_transport == Transport::Uart) {
|
||||
// H4: [0x01][opcode][paramLen][params]. The transmit ring is
|
||||
// shared with ACL traffic; wait briefly for room behind it.
|
||||
uint8_t header[4] = {
|
||||
H4_COMMAND, (uint8_t)(opcode & 0xFF), (uint8_t)(opcode >> 8), paramLen
|
||||
};
|
||||
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (!BtUart::Write(header, sizeof(header), params, paramLen)) {
|
||||
if (Timekeeping::GetMilliseconds() - start > 1000) {
|
||||
FinishCommand(opcode);
|
||||
KernelLogStream(WARNING, "BT-HCI") << "SendCommand: UART transmit stalled, opcode="
|
||||
<< base::hex << (uint64_t)opcode << base::dec;
|
||||
return false;
|
||||
}
|
||||
BtUart::Service();
|
||||
Arch::Pause();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use DMA-allocated buffer (not stack) for the command data.
|
||||
// xHCI reads from this buffer via DMA for OUT transfers.
|
||||
memset(g_cmdDmaBuf, 0, 512);
|
||||
@@ -791,7 +984,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// cannot wait -- a nested PollEvents is a no-op, so the Command Complete
|
||||
// is reaped by the active PollEvents after we return. The command was
|
||||
// already submitted (fire-and-forget); report success.
|
||||
if (Xhci::InPollContext()) {
|
||||
if (InPumpContext()) {
|
||||
FinishCommand(opcode);
|
||||
return true;
|
||||
}
|
||||
@@ -799,7 +992,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Pump();
|
||||
DrainEvents();
|
||||
|
||||
uint8_t event[sizeof(g_eventBuf)];
|
||||
@@ -852,7 +1045,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
|
||||
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs) {
|
||||
// See WaitCommandComplete: cannot wait when nested under PollEvents.
|
||||
if (Xhci::InPollContext()) {
|
||||
if (InPumpContext()) {
|
||||
FinishCommand(opcode);
|
||||
return true;
|
||||
}
|
||||
@@ -860,7 +1053,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Pump();
|
||||
DrainEvents();
|
||||
|
||||
uint8_t event[sizeof(g_eventBuf)];
|
||||
@@ -897,10 +1090,40 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// SendAcl — via USB bulk OUT
|
||||
// =========================================================================
|
||||
|
||||
// Room the UART transmit ring must keep for one maximum-size ACL packet.
|
||||
static uint32_t UartAclSlotBytes() {
|
||||
return 1u + sizeof(AclHeader) + (g_aclMaxLen ? g_aclMaxLen : 1021);
|
||||
}
|
||||
|
||||
bool SendAcl(uint16_t handle, uint16_t pbFlag, const uint8_t* data, uint16_t len) {
|
||||
if (!g_initialized || !g_aclTxRing[0] || !GetConnection(handle)) return false;
|
||||
if (!g_initialized || !GetConnection(handle)) return false;
|
||||
if (len + sizeof(AclHeader) > 4096) return false; // Single page DMA buffer
|
||||
|
||||
if (g_transport == Transport::Uart) {
|
||||
g_aclTxLock.Acquire();
|
||||
if (g_aclMaxNum != 0 && AclPendingCount() >= g_aclMaxNum) {
|
||||
g_aclTxLock.Release();
|
||||
return false;
|
||||
}
|
||||
uint16_t handleFlags = (uint16_t)((handle & 0x0FFF) | pbFlag);
|
||||
uint8_t header[1 + sizeof(AclHeader)] = {
|
||||
H4_ACL,
|
||||
(uint8_t)handleFlags, (uint8_t)(handleFlags >> 8),
|
||||
(uint8_t)len, (uint8_t)(len >> 8)
|
||||
};
|
||||
// All or nothing: a partial H4 packet would desynchronize the
|
||||
// controller's deframer. A full ring drops the packet, as a
|
||||
// full xHCI TX ring does.
|
||||
bool queued = BtUart::Write(header, sizeof(header), data, len);
|
||||
if (queued) {
|
||||
g_aclPendingCount.fetch_add(1, std::memory_order_relaxed);
|
||||
g_aclTxCount.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
g_aclTxLock.Release();
|
||||
return queued;
|
||||
}
|
||||
|
||||
if (!g_aclTxRing[0]) return false;
|
||||
g_aclTxLock.Acquire();
|
||||
|
||||
// HCI host flow control is a hard limit, not merely a media-pacing
|
||||
@@ -951,6 +1174,11 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
uint16_t AclMaxPackets() { return g_aclMaxNum; }
|
||||
|
||||
uint32_t AclTxInFlight() {
|
||||
if (g_transport == Transport::Uart) {
|
||||
uint32_t pending = BtUart::TxPending();
|
||||
uint32_t slot = UartAclSlotBytes();
|
||||
return (pending + slot - 1) / slot;
|
||||
}
|
||||
uint32_t tx = g_aclTxCount.load(std::memory_order_relaxed);
|
||||
uint32_t done = g_aclTxDoneCount.load(std::memory_order_relaxed);
|
||||
return (tx >= done) ? (tx - done) : 0;
|
||||
@@ -966,6 +1194,10 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
uint16_t maxOut = g_aclMaxNum ? g_aclMaxNum : 4;
|
||||
uint16_t mediaLimit = maxOut > 1 ? (uint16_t)(maxOut - 1) : maxOut;
|
||||
if (AclPendingCount() >= mediaLimit) return false;
|
||||
if (g_transport == Transport::Uart) {
|
||||
// Room for this packet plus a signaling reply behind it.
|
||||
return BtUart::TxFree() >= 2 * UartAclSlotBytes();
|
||||
}
|
||||
if (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -1420,6 +1652,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
}
|
||||
|
||||
case EVT_VENDOR_SPECIFIC: {
|
||||
if (g_vendor != Vendor::Intel) break;
|
||||
// Intel vendor events carry a sub-opcode in the first byte.
|
||||
// 0x02 = "bootup" (operational firmware booted after 0xFC01)
|
||||
// 0x06 = "secure send result": result(1) opcode(2) status(1)
|
||||
@@ -1625,7 +1858,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// copying past what the controller delivered.
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < 2000) {
|
||||
Xhci::PollEvents();
|
||||
Pump();
|
||||
DrainEvents();
|
||||
|
||||
uint8_t event[sizeof(g_eventBuf)];
|
||||
@@ -1676,7 +1909,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
void ProcessPendingCommands() {
|
||||
// Top-level only: drains queued pairing replies with real, confirmed
|
||||
// control transfers. Must NOT be called from inside PollEvents.
|
||||
if (Xhci::InPollContext()) return;
|
||||
if (InPumpContext()) return;
|
||||
// One consumer at a time: this runs from the Connect wait loop AND the
|
||||
// idle-core service pump, potentially on different cores; two consumers
|
||||
// on the lockless queue would race the tail and interleave commands.
|
||||
@@ -1728,7 +1961,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
bool WaitSecureSendResult(uint32_t timeoutMs, uint8_t* outResult, uint8_t* outStatus) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Pump();
|
||||
DrainEvents();
|
||||
if (g_secureResultValid) {
|
||||
if (outResult) *outResult = g_secureResult;
|
||||
@@ -1763,7 +1996,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
<< " byte " << g_ssBytesSent;
|
||||
return false;
|
||||
}
|
||||
Xhci::PollEvents();
|
||||
Pump();
|
||||
DrainEvents();
|
||||
|
||||
g_ssBytesSent += frag;
|
||||
@@ -1784,7 +2017,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// header is deliberately absent, and the NOCP credit counter is NOT
|
||||
// touched: these are commands, not ACL data).
|
||||
bool IntelSecureSendBulk(uint8_t fragmentType, const uint8_t* data, uint32_t len) {
|
||||
if (!g_initialized || !g_aclTxRing[0]) return false;
|
||||
if (!g_initialized || g_transport != Transport::Usb || !g_aclTxRing[0]) return false;
|
||||
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
|
||||
|
||||
uint32_t off = 0;
|
||||
@@ -1796,7 +2029,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// controller from freezing the boot path.
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) {
|
||||
Xhci::PollEvents();
|
||||
Pump();
|
||||
DrainEvents();
|
||||
if (g_aclTxTransportError.load(std::memory_order_acquire)) {
|
||||
KernelLogStream(ERROR, "BT-HCI")
|
||||
@@ -1843,7 +2076,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
bool DrainBulkTx(uint32_t timeoutMs) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (AclTxInFlight() > 0) {
|
||||
Xhci::PollEvents();
|
||||
Pump();
|
||||
DrainEvents();
|
||||
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
|
||||
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||
@@ -1872,7 +2105,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Pump();
|
||||
DrainEvents();
|
||||
if (g_intelBootup) return true;
|
||||
for (int j = 0; j < 100; j++) Arch::CompilerBarrier();
|
||||
@@ -1953,9 +2186,14 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
}
|
||||
|
||||
bool SetBdAddr(const uint8_t* addr) {
|
||||
// Intel 0xFC31: 6-byte BD_ADDR, little-endian (same order as ReadBdAddr).
|
||||
if (!SendCommand(OP_INTEL_WRITE_BD_ADDR, addr, 6)) return false;
|
||||
return WaitCommandComplete(OP_INTEL_WRITE_BD_ADDR, nullptr, 0, 2000);
|
||||
// Intel 0xFC31 / Broadcom 0xFC01: 6-byte BD_ADDR, little-endian (same
|
||||
// order as ReadBdAddr).
|
||||
uint16_t opcode;
|
||||
if (g_vendor == Vendor::Intel) opcode = OP_INTEL_WRITE_BD_ADDR;
|
||||
else if (g_vendor == Vendor::Broadcom) opcode = OP_BCM_WRITE_BD_ADDR;
|
||||
else return false;
|
||||
if (!SendCommand(opcode, addr, 6)) return false;
|
||||
return WaitCommandComplete(opcode, nullptr, 0, 2000);
|
||||
}
|
||||
|
||||
bool Disconnect(uint16_t handle, uint8_t reason) {
|
||||
@@ -1,23 +1,35 @@
|
||||
/*
|
||||
* Hci.hpp
|
||||
* Bluetooth HCI (Host Controller Interface) layer
|
||||
* HCI transport over USB bulk/interrupt/control endpoints
|
||||
* HCI transport over USB (bulk/interrupt/control endpoints) or a UART (H4)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::USB::Bluetooth::Hci {
|
||||
namespace Drivers::Bluetooth::Hci {
|
||||
|
||||
// =========================================================================
|
||||
// HCI packet types (for USB transport)
|
||||
// HCI transports
|
||||
// =========================================================================
|
||||
|
||||
// USB transport uses different endpoints for each packet type:
|
||||
// Commands -> Control EP0 (class request)
|
||||
// ACL data -> Bulk OUT / Bulk IN
|
||||
// Events -> Interrupt IN
|
||||
//
|
||||
// The UART transport (H4, Bluetooth Core spec Vol 4 Part A) carries all
|
||||
// of them on one byte stream, each packet preceded by its type.
|
||||
constexpr uint8_t H4_COMMAND = 0x01;
|
||||
constexpr uint8_t H4_ACL = 0x02;
|
||||
constexpr uint8_t H4_SCO = 0x03;
|
||||
constexpr uint8_t H4_EVENT = 0x04;
|
||||
|
||||
enum class Transport : uint8_t { None, Usb, Uart };
|
||||
|
||||
// Controller vendor, for the vendor-specific commands (BD_ADDR write).
|
||||
enum class Vendor : uint8_t { Unknown, Intel, Broadcom };
|
||||
|
||||
// =========================================================================
|
||||
// HCI command opcodes (OGF << 10 | OCF)
|
||||
@@ -61,6 +73,13 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
constexpr uint16_t OP_READ_LOCAL_FEATURES = 0x1003;
|
||||
constexpr uint16_t OP_READ_BUFFER_SIZE = 0x1005;
|
||||
|
||||
// Broadcom vendor commands (OGF 0x3F), from Linux btbcm/hci_bcm
|
||||
constexpr uint16_t OP_BCM_WRITE_BD_ADDR = 0xFC01;
|
||||
constexpr uint16_t OP_BCM_UPDATE_BAUD_RATE = 0xFC18;
|
||||
constexpr uint16_t OP_BCM_DOWNLOAD_MINIDRV = 0xFC2E;
|
||||
constexpr uint16_t OP_BCM_WRITE_RAM = 0xFC4C;
|
||||
constexpr uint16_t OP_BCM_LAUNCH_RAM = 0xFC4E;
|
||||
|
||||
// Intel vendor commands (OGF 0x3F)
|
||||
constexpr uint16_t OP_INTEL_READ_VERSION = 0xFC05;
|
||||
constexpr uint16_t OP_INTEL_RESET = 0xFC01;
|
||||
@@ -175,6 +194,37 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// Initialize HCI transport over USB for the given slot
|
||||
void Initialize(uint8_t slotId);
|
||||
|
||||
// Initialize HCI transport over the Bluetooth UART (Serial::BtUart,
|
||||
// already initialized by the caller).
|
||||
void InitializeUart();
|
||||
|
||||
Transport GetTransport();
|
||||
|
||||
// Bytes per second the host-to-controller link can carry: the UART's
|
||||
// line rate (8N1), or 0 when the transport is not the bottleneck (USB).
|
||||
uint32_t TransportBytesPerSecond();
|
||||
|
||||
void SetVendor(Vendor vendor);
|
||||
Vendor GetVendor();
|
||||
|
||||
// Move received bytes from the transport into the HCI queues: the xHCI
|
||||
// event ring for USB, the UART receive ring (H4 deframing) for UART.
|
||||
// Every wait loop calls this before DrainEvents. Non-reentrant; a
|
||||
// nested or concurrent call returns at once.
|
||||
void Pump();
|
||||
|
||||
// True while running nested under Pump (USB transfer callbacks), where
|
||||
// a command cannot be waited for.
|
||||
bool InPumpContext();
|
||||
|
||||
// Received transport bytes are waiting for Pump (UART only; USB signals
|
||||
// through the xHCI deferred-work flag).
|
||||
bool TransportHasRxData();
|
||||
|
||||
// Discard all received UART input, queued bytes and any partial packet
|
||||
// (after a power cycle or a baud-rate change).
|
||||
void ResetUartReceiver();
|
||||
|
||||
// Start receiving HCI events and ACL data (call after HCI init sequence).
|
||||
// Arms both the interrupt IN and the bulk IN; the bulk IN must stay armed
|
||||
// through the firmware download (it absorbs the device's ~635 KB cc=4 glitch
|
||||
@@ -186,7 +236,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// are intentionally ignored and any endpoint error is recovered here.
|
||||
void EnableAclDataReception();
|
||||
|
||||
// Send an HCI command via USB control transfer (EP0)
|
||||
// Send an HCI command (USB: control transfer on EP0; UART: H4 packet)
|
||||
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen);
|
||||
|
||||
// Wait for a Command Complete event matching the given opcode
|
||||
@@ -197,7 +247,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// Wait for a Command Status event matching the given opcode
|
||||
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs = 2000);
|
||||
|
||||
// Send ACL data via USB bulk OUT
|
||||
// Send ACL data (USB: bulk OUT; UART: H4 packet)
|
||||
bool SendAcl(uint16_t handle, uint16_t pbFlag, const uint8_t* data, uint16_t len);
|
||||
|
||||
// Process an HCI event received on the interrupt IN endpoint
|
||||
@@ -323,8 +373,10 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// Drives Link Key Request -> pairing; needed for bonded-device reconnects.
|
||||
bool AuthenticateLink(uint16_t handle);
|
||||
|
||||
// Set the adapter's BD_ADDR (Intel vendor command 0xFC31). Used to dodge a
|
||||
// remote that holds a stale, un-clearable bond to our real address.
|
||||
// Set the adapter's BD_ADDR (vendor command: Intel 0xFC31, Broadcom
|
||||
// 0xFC01). Used to dodge a remote that holds a stale, un-clearable bond
|
||||
// to our real address, and to give Broadcom parts without a programmed
|
||||
// address a unique one.
|
||||
bool SetBdAddr(const uint8_t* addr);
|
||||
|
||||
// Send any queued pairing replies (IO-cap / user-confirm / link-key) with
|
||||
@@ -340,7 +392,8 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
uint16_t AclMaxPackets();
|
||||
|
||||
// ACL packets handed to the xHCI whose bulk OUT completion has not been
|
||||
// reaped yet (each one still owns a TX DMA ring slot).
|
||||
// reaped yet (each one still owns a TX DMA ring slot). On UART, the
|
||||
// transmit ring's backlog in maximum-size packets (0 once drained).
|
||||
uint32_t AclTxInFlight();
|
||||
|
||||
// True when SendAcl can be called without overrunning either the
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::USB::Bluetooth {
|
||||
namespace Drivers::Bluetooth {
|
||||
|
||||
// =========================================================================
|
||||
// .sfi / TLV format constants (from drivers/bluetooth/btintel.{c,h})
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::USB::Bluetooth {
|
||||
namespace Drivers::Bluetooth {
|
||||
|
||||
// Drive the Intel bootloader firmware download sequence for the currently
|
||||
// registered adapter:
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "Hci.hpp"
|
||||
#include "A2dp.hpp"
|
||||
#include "Avrcp.hpp"
|
||||
#include <Drivers/USB/Xhci.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
@@ -17,7 +16,7 @@
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::USB::Bluetooth::L2cap {
|
||||
namespace Drivers::Bluetooth::L2cap {
|
||||
|
||||
// =========================================================================
|
||||
// State
|
||||
@@ -548,7 +547,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::Pump();
|
||||
Hci::DrainEvents(); // process incoming ACL (the Connection Response!)
|
||||
|
||||
auto* ch = GetChannel(localCid);
|
||||
@@ -7,7 +7,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::USB::Bluetooth::L2cap {
|
||||
namespace Drivers::Bluetooth::L2cap {
|
||||
|
||||
// =========================================================================
|
||||
// L2CAP CIDs (Channel Identifiers)
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "Sbc.hpp"
|
||||
#include <Libraries/Memory.hpp>
|
||||
|
||||
namespace Drivers::USB::Bluetooth::Sbc {
|
||||
namespace Drivers::Bluetooth::Sbc {
|
||||
|
||||
// =========================================================================
|
||||
// Analysis filterbank tables
|
||||
@@ -8,7 +8,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::USB::Bluetooth::Sbc {
|
||||
namespace Drivers::Bluetooth::Sbc {
|
||||
|
||||
// =========================================================================
|
||||
// SBC configuration
|
||||
+265
-2
@@ -10,15 +10,22 @@
|
||||
#include <Drivers/Graphics/DgpuPower.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Genet.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
#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 <Drivers/Bluetooth/Bluetooth.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 +96,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 +200,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 +250,243 @@ 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);
|
||||
}
|
||||
|
||||
// A device property (_DSD: { UUID, Package { Package { key, value },
|
||||
// ... } }) by key.
|
||||
static bool FindDsdProperty(uint32_t nodeIndex, const char* key, Hal::AML::Object& value) {
|
||||
auto& interp = Hal::AML::GetInterpreter();
|
||||
Hal::AML::Object dsd, properties;
|
||||
if (!interp.EvaluateChild(nodeIndex, "_DSD", nullptr, 0, dsd) ||
|
||||
dsd.Type != Hal::AML::ObjectType::Package ||
|
||||
!interp.GetPackageElement(dsd, 1, properties) ||
|
||||
properties.Type != Hal::AML::ObjectType::Package) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int count = interp.GetPackageCount(properties);
|
||||
for (int i = 0; i < count; i++) {
|
||||
Hal::AML::Object pair, name;
|
||||
if (!interp.GetPackageElement(properties, i, pair) || pair.Type != Hal::AML::ObjectType::Package ||
|
||||
!interp.GetPackageElement(pair, 0, name) || name.Type != Hal::AML::ObjectType::String) {
|
||||
continue;
|
||||
}
|
||||
int c = 0;
|
||||
while (key[c] != '\0' && name.String.Data[c] == key[c]) c++;
|
||||
if (key[c] != '\0' || name.String.Data[c] != '\0') continue;
|
||||
return interp.GetPackageElement(pair, 1, value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool StringEquals(const char* a, const char* b) {
|
||||
while (*a != '\0' && *a == *b) { a++; b++; }
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
// The Raspberry Pi 4/400's GENET v5 Ethernet MAC (BCM6E4E). The first
|
||||
// of its two interrupts carries the default queue.
|
||||
static void ProbePlatformGenet() {
|
||||
if (Net::Genet::IsInitialized()) return;
|
||||
|
||||
Hal::AcpiDevices::DeviceInfo info{};
|
||||
if (!Hal::AcpiDevices::FindDeviceByHid("BCM6E4E", 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") << "GENET " << 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;
|
||||
}
|
||||
|
||||
// The Pi 4's PHY delays the receive clock and expects the MAC to
|
||||
// delay transmit: "rgmii-rxid", also when the table says nothing.
|
||||
auto phyMode = Net::Genet::PhyMode::RgmiiRxid;
|
||||
Hal::AML::Object mode;
|
||||
if (FindDsdProperty(info.NodeIndex, "phy-mode", mode) && mode.Type == Hal::AML::ObjectType::String) {
|
||||
const char* m = mode.String.Data;
|
||||
if (StringEquals(m, "rgmii")) phyMode = Net::Genet::PhyMode::Rgmii;
|
||||
else if (StringEquals(m, "rgmii-id")) phyMode = Net::Genet::PhyMode::RgmiiId;
|
||||
else if (StringEquals(m, "rgmii-txid")) phyMode = Net::Genet::PhyMode::RgmiiTxid;
|
||||
else if (!StringEquals(m, "rgmii-rxid")) {
|
||||
Kt::KernelLogStream(Kt::WARNING, "Drivers") << "GENET " << info.Path
|
||||
<< ": unsupported phy-mode " << m << ", trying rgmii-rxid";
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t burst = 0;
|
||||
Hal::AML::Object burstObj;
|
||||
if (FindDsdProperty(info.NodeIndex, "brcm,max-dma-burst-size", burstObj) &&
|
||||
burstObj.Type == Hal::AML::ObjectType::Integer) {
|
||||
burst = (uint32_t)burstObj.Integer;
|
||||
}
|
||||
|
||||
Kt::KernelLogStream(Kt::INFO, "Drivers") << "GENET platform device " << info.Path
|
||||
<< " at 0x" << base::hex << base << ", GSI " << base::dec << (uint64_t)gsi;
|
||||
WarnIfRamBeyondDma(info.Path);
|
||||
Net::Genet::ProbePlatform(base, length, gsi, level, activeLow, coherent, phyMode, burst);
|
||||
}
|
||||
|
||||
// The Raspberry Pi 4/400's Wi-Fi: the CYW4345x's SDIO half on the
|
||||
// BCM2711's older Arasan controller (BCM2847, GPIO 34-39). Only while the
|
||||
// card slot is on EMMC2 (BRCME88C): with the firmware's "SD on Arasan"
|
||||
// option the Arasan runs the slot instead, and is not the radio's.
|
||||
static void ProbePlatformBroadcomWifi() {
|
||||
Hal::AcpiDevices::DeviceInfo emmc2{};
|
||||
if (!Hal::AcpiDevices::FindDeviceByHid("BRCME88C", emmc2)) return;
|
||||
|
||||
Hal::AML::ResourceList resources;
|
||||
uint64_t base = 0, length = 0;
|
||||
Hal::AcpiDevices::DeviceInfo info{};
|
||||
const char* name = "BCM2847";
|
||||
if (Hal::AcpiDevices::FindDeviceByHid("BCM2847", info) &&
|
||||
Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) &&
|
||||
resources.GetMemory(base, length) && base != 0) {
|
||||
name = info.Path;
|
||||
} else {
|
||||
// Not described: the Arasan sits 256 KiB below EMMC2 on the
|
||||
// BCM2711 (0x7E300000 / 0x7E340000 bus addresses).
|
||||
Hal::AML::ResourceList emmc2Resources;
|
||||
uint64_t emmc2Base = 0;
|
||||
if (!Hal::AcpiDevices::EvaluateCrs(emmc2.NodeIndex, emmc2Resources) ||
|
||||
!emmc2Resources.GetMemory(emmc2Base, length) || emmc2Base < 0x40000) {
|
||||
return;
|
||||
}
|
||||
base = emmc2Base - 0x40000;
|
||||
name = "the Arasan controller (inferred from EMMC2)";
|
||||
}
|
||||
Net::Wifi::ProbeBroadcomSdio(base, name);
|
||||
}
|
||||
|
||||
void ProbeNormal() {
|
||||
Pci::ProbeAll(g_driverTable, g_driverTableCount, Pci::ProbePhase::Normal);
|
||||
ProbePlatformXhci();
|
||||
ProbePlatformSdhci();
|
||||
ProbePlatformGenet();
|
||||
ProbePlatformBroadcomWifi();
|
||||
Bluetooth::ProbePlatformAdapter();
|
||||
}
|
||||
|
||||
void InitializeNetwork() {
|
||||
@@ -241,10 +499,15 @@ namespace Drivers {
|
||||
Storage::Gpt::ProbeAll();
|
||||
}
|
||||
|
||||
void ServiceStorageHotplug() {
|
||||
if (USB::Xhci::HasDeferredWork()) USB::Xhci::ProcessDeferredWork();
|
||||
}
|
||||
|
||||
void InitializeAudio() {
|
||||
// HDA driver initializes during ProbeNormal().
|
||||
// Bluetooth audio (A2DP) initializes automatically during USB enumeration
|
||||
// when a Bluetooth adapter is detected by the xHCI driver.
|
||||
// Bluetooth audio (A2DP) initializes with its adapter: during USB
|
||||
// enumeration, or for an on-board UART controller (Raspberry Pi) from
|
||||
// the platform probe in ProbeNormal().
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,6 +29,11 @@ namespace Drivers {
|
||||
// Post-probe: register SATA drives with VFS.
|
||||
void InitializeStorage();
|
||||
|
||||
// Before the scheduler runs: enumerate storage that arrived after the boot
|
||||
// scan (a USB device still training, a hub port), which normally waits for
|
||||
// the idle loop. Its partitions are mounted as it registers.
|
||||
void ServiceStorageHotplug();
|
||||
|
||||
// Post-probe: initialize audio subsystem.
|
||||
void InitializeAudio();
|
||||
|
||||
|
||||
@@ -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,713 @@
|
||||
/*
|
||||
* Genet.cpp
|
||||
* Broadcom GENET v5 Ethernet driver (Raspberry Pi 4 / 400)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Genet.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <ACPI/GsiRouting.hpp>
|
||||
#include <Drivers/Platform/RpiFirmware.hpp>
|
||||
#include <Memory/Dma.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <atomic>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Net::Genet {
|
||||
|
||||
// ====================================================================
|
||||
// State
|
||||
// ====================================================================
|
||||
|
||||
static bool g_initialized = false;
|
||||
static bool g_coherent = false;
|
||||
static PhyMode g_phyMode = PhyMode::RgmiiRxid;
|
||||
static volatile uint8_t* g_regs = nullptr;
|
||||
static uint8_t g_mac[6] = {};
|
||||
static uint32_t g_phyAddr = 1;
|
||||
|
||||
// Packet buffers, BUF_LENGTH each, two to a page. Slot n of a ring
|
||||
// always uses buffer n.
|
||||
static uint8_t* g_rxBufs[RX_DESCS] = {};
|
||||
static uint8_t* g_txBufs[TX_DESCS] = {};
|
||||
|
||||
// Software copies of the 16-bit ring counters: the RX consumer index we
|
||||
// last wrote and the TX producer index. Slot = counter % ring size.
|
||||
static std::atomic<uint32_t> g_rxCons{0};
|
||||
static uint32_t g_txProd = 0;
|
||||
static kcp::Spinlock g_txLock;
|
||||
|
||||
static std::atomic<bool> g_rxWorkPending{false};
|
||||
static std::atomic<bool> g_processing{false};
|
||||
|
||||
// The PHY is polled: an external RGMII PHY raises no MAC interrupt on
|
||||
// link changes, and its own interrupt pin is not wired to anything
|
||||
// ACPI describes.
|
||||
static constexpr uint64_t LINK_POLL_MS = 1000;
|
||||
static std::atomic<uint64_t> g_nextLinkPollMs{0};
|
||||
static std::atomic<bool> g_linkUp{false};
|
||||
static uint32_t g_linkSpeed = 0;
|
||||
static bool g_linkFullDuplex = false;
|
||||
|
||||
static uint64_t g_rxPacketCount = 0;
|
||||
static uint64_t g_txPacketCount = 0;
|
||||
static uint64_t g_rxErrorCount = 0;
|
||||
|
||||
static RxCallback g_rxCallback = nullptr;
|
||||
|
||||
// ====================================================================
|
||||
// Register access
|
||||
// ====================================================================
|
||||
|
||||
static uint32_t Read(uint32_t reg) {
|
||||
return *(volatile uint32_t*)(g_regs + reg);
|
||||
}
|
||||
|
||||
static void Write(uint32_t reg, uint32_t value) {
|
||||
*(volatile uint32_t*)(g_regs + reg) = value;
|
||||
}
|
||||
|
||||
static void SetBits(uint32_t reg, uint32_t bits) { Write(reg, Read(reg) | bits); }
|
||||
static void ClearBits(uint32_t reg, uint32_t bits) { Write(reg, Read(reg) & ~bits); }
|
||||
|
||||
static uint32_t DescReg(uint32_t base, uint32_t slot, uint32_t word) {
|
||||
return base + slot * DESC_SIZE + word * 4;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// MDIO (the UniMAC's clause 22 master)
|
||||
// ====================================================================
|
||||
|
||||
// Standard PHY registers and bits.
|
||||
static constexpr uint32_t MII_BMCR = 0x00;
|
||||
static constexpr uint32_t MII_BMSR = 0x01;
|
||||
static constexpr uint32_t MII_PHYSID1 = 0x02;
|
||||
static constexpr uint32_t MII_PHYSID2 = 0x03;
|
||||
static constexpr uint32_t MII_ADVERTISE = 0x04;
|
||||
static constexpr uint32_t MII_LPA = 0x05;
|
||||
static constexpr uint32_t MII_CTRL1000 = 0x09;
|
||||
static constexpr uint32_t MII_STAT1000 = 0x0A;
|
||||
|
||||
static constexpr uint16_t BMCR_SPEED1000 = 1u << 6;
|
||||
static constexpr uint16_t BMCR_FULLDPLX = 1u << 8;
|
||||
static constexpr uint16_t BMCR_ANRESTART = 1u << 9;
|
||||
static constexpr uint16_t BMCR_ANENABLE = 1u << 12;
|
||||
static constexpr uint16_t BMCR_SPEED100 = 1u << 13;
|
||||
static constexpr uint16_t BMCR_RESET = 1u << 15;
|
||||
static constexpr uint16_t BMSR_LSTATUS = 1u << 2;
|
||||
static constexpr uint16_t BMSR_ANEGCOMPLETE = 1u << 5;
|
||||
static constexpr uint16_t ADVERTISE_CSMA = 0x0001;
|
||||
static constexpr uint16_t ADVERTISE_10HALF = 1u << 5;
|
||||
static constexpr uint16_t ADVERTISE_10FULL = 1u << 6;
|
||||
static constexpr uint16_t ADVERTISE_100HALF = 1u << 7;
|
||||
static constexpr uint16_t ADVERTISE_100FULL = 1u << 8;
|
||||
static constexpr uint16_t ADVERTISE_1000HALF = 1u << 8; // MII_CTRL1000
|
||||
static constexpr uint16_t ADVERTISE_1000FULL = 1u << 9; // MII_CTRL1000
|
||||
static constexpr uint16_t LPA_1000HALF = 1u << 10; // MII_STAT1000
|
||||
static constexpr uint16_t LPA_1000FULL = 1u << 11; // MII_STAT1000
|
||||
|
||||
static bool MdioRun(uint32_t command) {
|
||||
Write(MDIO_CMD, command);
|
||||
Write(MDIO_CMD, command | MDIO_START_BUSY);
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Read(MDIO_CMD) & MDIO_START_BUSY) {
|
||||
if (Timekeeping::GetMilliseconds() - start > 20) return false;
|
||||
Arch::Pause();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool MdioRead(uint32_t phy, uint32_t reg, uint16_t& value) {
|
||||
if (!MdioRun(MDIO_RD | (phy << MDIO_PMD_SHIFT) | (reg << MDIO_REG_SHIFT))) return false;
|
||||
uint32_t result = Read(MDIO_CMD);
|
||||
if (result & MDIO_READ_FAIL) return false;
|
||||
value = (uint16_t)result;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool MdioWrite(uint32_t phy, uint32_t reg, uint16_t value) {
|
||||
return MdioRun(MDIO_WR | (phy << MDIO_PMD_SHIFT) | (reg << MDIO_REG_SHIFT) | value);
|
||||
}
|
||||
|
||||
static uint16_t PhyRead(uint32_t reg) {
|
||||
uint16_t value = 0;
|
||||
if (!MdioRead(g_phyAddr, reg, value)) return 0;
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool PhyPresent(uint32_t addr, uint32_t& id) {
|
||||
uint16_t id1 = 0, id2 = 0;
|
||||
if (!MdioRead(addr, MII_PHYSID1, id1) || !MdioRead(addr, MII_PHYSID2, id2)) return false;
|
||||
if ((id1 == 0xFFFF && id2 == 0xFFFF) || (id1 == 0 && id2 == 0)) return false;
|
||||
id = ((uint32_t)id1 << 16) | id2;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The board puts the PHY at address 1; scan the bus if it is not there.
|
||||
static bool FindPhy(uint32_t& id) {
|
||||
if (PhyPresent(1, id)) { g_phyAddr = 1; return true; }
|
||||
for (uint32_t addr = 0; addr < 32; addr++) {
|
||||
if (addr != 1 && PhyPresent(addr, id)) { g_phyAddr = addr; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Link
|
||||
// ====================================================================
|
||||
|
||||
// BMSR latches link failures: the first read reports any loss since the
|
||||
// last one, the second the current state.
|
||||
static bool PhyLinkUp() {
|
||||
(void)PhyRead(MII_BMSR);
|
||||
uint16_t bmsr = PhyRead(MII_BMSR);
|
||||
if (!(bmsr & BMSR_LSTATUS)) return false;
|
||||
uint16_t bmcr = PhyRead(MII_BMCR);
|
||||
return !(bmcr & BMCR_ANENABLE) || (bmsr & BMSR_ANEGCOMPLETE);
|
||||
}
|
||||
|
||||
// The highest mode both ends advertised (or the forced mode).
|
||||
static void ResolveLink(uint32_t& speed, bool& fullDuplex) {
|
||||
uint16_t bmcr = PhyRead(MII_BMCR);
|
||||
if (!(bmcr & BMCR_ANENABLE)) {
|
||||
speed = (bmcr & BMCR_SPEED1000) ? 1000 : (bmcr & BMCR_SPEED100) ? 100 : 10;
|
||||
fullDuplex = (bmcr & BMCR_FULLDPLX) != 0;
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t ctrl1000 = PhyRead(MII_CTRL1000);
|
||||
uint16_t stat1000 = PhyRead(MII_STAT1000);
|
||||
if ((ctrl1000 & ADVERTISE_1000FULL) && (stat1000 & LPA_1000FULL)) {
|
||||
speed = 1000; fullDuplex = true; return;
|
||||
}
|
||||
if ((ctrl1000 & ADVERTISE_1000HALF) && (stat1000 & LPA_1000HALF)) {
|
||||
speed = 1000; fullDuplex = false; return;
|
||||
}
|
||||
|
||||
uint16_t common = PhyRead(MII_ADVERTISE) & PhyRead(MII_LPA);
|
||||
if (common & ADVERTISE_100FULL) { speed = 100; fullDuplex = true; }
|
||||
else if (common & ADVERTISE_100HALF) { speed = 100; fullDuplex = false; }
|
||||
else if (common & ADVERTISE_10FULL) { speed = 10; fullDuplex = true; }
|
||||
else { speed = 10; fullDuplex = false; }
|
||||
}
|
||||
|
||||
// Point the RGMII block and the UniMAC at the negotiated mode and start
|
||||
// both directions.
|
||||
static void ConfigureMac(uint32_t speed, bool fullDuplex) {
|
||||
uint32_t oob = Read(EXT_RGMII_OOB_CTRL);
|
||||
oob &= ~OOB_DISABLE;
|
||||
oob |= RGMII_LINK | RGMII_MODE_EN;
|
||||
if (g_phyMode == PhyMode::Rgmii || g_phyMode == PhyMode::RgmiiId) oob |= ID_MODE_DIS;
|
||||
else oob &= ~ID_MODE_DIS;
|
||||
Write(EXT_RGMII_OOB_CTRL, oob);
|
||||
|
||||
uint32_t cmd = (speed == 1000 ? CMD_SPEED_1000 : speed == 100 ? CMD_SPEED_100 : CMD_SPEED_10)
|
||||
<< CMD_SPEED_SHIFT;
|
||||
if (!fullDuplex) cmd |= CMD_HD_EN;
|
||||
Write(UMAC_CMD, cmd);
|
||||
Write(UMAC_CMD, cmd | CMD_TX_EN | CMD_RX_EN);
|
||||
}
|
||||
|
||||
static void PollLink() {
|
||||
bool up = PhyLinkUp();
|
||||
bool wasUp = g_linkUp.load(std::memory_order_relaxed);
|
||||
|
||||
if (up) {
|
||||
uint32_t speed = 0;
|
||||
bool fullDuplex = false;
|
||||
ResolveLink(speed, fullDuplex);
|
||||
if (wasUp && speed == g_linkSpeed && fullDuplex == g_linkFullDuplex) return;
|
||||
|
||||
ConfigureMac(speed, fullDuplex);
|
||||
g_linkSpeed = speed;
|
||||
g_linkFullDuplex = fullDuplex;
|
||||
g_linkUp.store(true, std::memory_order_release);
|
||||
KernelLogStream(OK, "GENET") << "Link up: " << base::dec << (uint64_t)speed
|
||||
<< " Mbit/s " << (fullDuplex ? "full" : "half") << " duplex";
|
||||
} else if (wasUp) {
|
||||
g_linkUp.store(false, std::memory_order_release);
|
||||
g_linkSpeed = 0;
|
||||
ClearBits(EXT_RGMII_OOB_CTRL, RGMII_LINK);
|
||||
KernelLogStream(INFO, "GENET") << "Link down";
|
||||
}
|
||||
}
|
||||
|
||||
// Leave a link the firmware already negotiated alone (renegotiating
|
||||
// costs seconds); otherwise reset the PHY and advertise 10/100 and
|
||||
// 1000 full duplex (the MAC has no 1000 half duplex).
|
||||
static void StartPhy() {
|
||||
if (PhyLinkUp()) {
|
||||
KernelLogStream(INFO, "GENET") << "PHY already has a link, keeping it";
|
||||
return;
|
||||
}
|
||||
|
||||
MdioWrite(g_phyAddr, MII_BMCR, BMCR_RESET);
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (PhyRead(MII_BMCR) & BMCR_RESET) {
|
||||
if (Timekeeping::GetMilliseconds() - start > 500) {
|
||||
KernelLogStream(WARNING, "GENET") << "PHY reset timed out";
|
||||
break;
|
||||
}
|
||||
Timekeeping::BusyWaitUs(100);
|
||||
}
|
||||
|
||||
MdioWrite(g_phyAddr, MII_ADVERTISE, ADVERTISE_CSMA | ADVERTISE_10HALF | ADVERTISE_10FULL |
|
||||
ADVERTISE_100HALF | ADVERTISE_100FULL);
|
||||
uint16_t ctrl1000 = PhyRead(MII_CTRL1000);
|
||||
ctrl1000 &= ~(ADVERTISE_1000HALF | ADVERTISE_1000FULL);
|
||||
MdioWrite(g_phyAddr, MII_CTRL1000, ctrl1000 | ADVERTISE_1000FULL);
|
||||
MdioWrite(g_phyAddr, MII_BMCR, BMCR_ANENABLE | BMCR_ANRESTART);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// MAC bring-up
|
||||
// ====================================================================
|
||||
|
||||
static bool WaitDmaDisabled(uint32_t engineRegs) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (!(Read(engineRegs + DMA_STATUS) & DMA_DISABLED)) {
|
||||
if (Timekeeping::GetMilliseconds() - start > 5) return false;
|
||||
Arch::Pause();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stop both DMA engines (the firmware's network stack may have left
|
||||
// them running) and drop whatever the buffers still hold.
|
||||
static void StopDma() {
|
||||
Write(TDMA_REGS + DMA_CTRL, 0);
|
||||
Write(RDMA_REGS + DMA_CTRL, 0);
|
||||
if (!WaitDmaDisabled(TDMA_REGS)) KernelLogStream(WARNING, "GENET") << "TX DMA did not stop";
|
||||
if (!WaitDmaDisabled(RDMA_REGS)) KernelLogStream(WARNING, "GENET") << "RX DMA did not stop";
|
||||
|
||||
Write(UMAC_TX_FLUSH, 1);
|
||||
Timekeeping::BusyWaitUs(10);
|
||||
Write(UMAC_TX_FLUSH, 0);
|
||||
|
||||
SetBits(SYS_RBUF_FLUSH_CTRL, 1u << 0);
|
||||
Timekeeping::BusyWaitUs(10);
|
||||
ClearBits(SYS_RBUF_FLUSH_CTRL, 1u << 0);
|
||||
}
|
||||
|
||||
// Reset the UniMAC with RGMII loopback on, so its receive clock stays
|
||||
// stable through the reset, and leave it disabled until the link is up.
|
||||
static void ResetUmac() {
|
||||
SetBits(SYS_RBUF_FLUSH_CTRL, 1u << 1);
|
||||
Timekeeping::BusyWaitUs(10);
|
||||
ClearBits(SYS_RBUF_FLUSH_CTRL, 1u << 1);
|
||||
Timekeeping::BusyWaitUs(10);
|
||||
Write(SYS_RBUF_FLUSH_CTRL, 0);
|
||||
Timekeeping::BusyWaitUs(10);
|
||||
|
||||
Write(UMAC_CMD, 0);
|
||||
Write(UMAC_CMD, CMD_SW_RESET | CMD_LCL_LOOP_EN);
|
||||
Timekeeping::BusyWaitUs(2);
|
||||
Write(UMAC_CMD, 0);
|
||||
|
||||
Write(UMAC_MIB_CTRL, MIB_RESET_RX | MIB_RESET_TX | MIB_RESET_RUNT);
|
||||
Write(UMAC_MIB_CTRL, 0);
|
||||
|
||||
Write(UMAC_MAX_FRAME_LEN, MAX_FRAME_LEN);
|
||||
|
||||
// No status blocks in front of frames; received frames start two
|
||||
// bytes into their buffer, so the IP header lands 4-byte aligned.
|
||||
uint32_t rbuf = Read(RBUF_CTRL);
|
||||
rbuf &= ~RBUF_64B_EN;
|
||||
rbuf |= RBUF_ALIGN_2B;
|
||||
Write(RBUF_CTRL, rbuf);
|
||||
ClearBits(TBUF_CTRL, TBUF_64B_EN);
|
||||
Write(RBUF_TBUF_SIZE_CTRL, 1);
|
||||
}
|
||||
|
||||
static void ReadFirmwareMac(uint8_t mac[6]) {
|
||||
uint32_t mac0 = Read(UMAC_MAC0);
|
||||
uint32_t mac1 = Read(UMAC_MAC1);
|
||||
mac[0] = (uint8_t)(mac0 >> 24);
|
||||
mac[1] = (uint8_t)(mac0 >> 16);
|
||||
mac[2] = (uint8_t)(mac0 >> 8);
|
||||
mac[3] = (uint8_t)mac0;
|
||||
mac[4] = (uint8_t)(mac1 >> 8);
|
||||
mac[5] = (uint8_t)mac1;
|
||||
}
|
||||
|
||||
static bool MacUsable(const uint8_t mac[6]) {
|
||||
bool allZero = true, allOnes = true;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (mac[i] != 0x00) allZero = false;
|
||||
if (mac[i] != 0xFF) allOnes = false;
|
||||
}
|
||||
return !allZero && !allOnes && !(mac[0] & 1);
|
||||
}
|
||||
|
||||
static void SetMdfEntry(uint32_t& index, const uint8_t mac[6]) {
|
||||
Write(UMAC_MDF_ADDR + index * 4, ((uint32_t)mac[0] << 8) | mac[1]);
|
||||
Write(UMAC_MDF_ADDR + (index + 1) * 4, ((uint32_t)mac[2] << 24) | ((uint32_t)mac[3] << 16) |
|
||||
((uint32_t)mac[4] << 8) | mac[5]);
|
||||
index += 2;
|
||||
}
|
||||
|
||||
// Station address, and the destination filter: broadcast and our own
|
||||
// address. Filter n is enabled by bit (16 - n).
|
||||
static void ProgramAddress() {
|
||||
Write(UMAC_MAC0, ((uint32_t)g_mac[0] << 24) | ((uint32_t)g_mac[1] << 16) |
|
||||
((uint32_t)g_mac[2] << 8) | g_mac[3]);
|
||||
Write(UMAC_MAC1, ((uint32_t)g_mac[4] << 8) | g_mac[5]);
|
||||
|
||||
static constexpr uint8_t Broadcast[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
uint32_t index = 0;
|
||||
SetMdfEntry(index, Broadcast);
|
||||
SetMdfEntry(index, g_mac);
|
||||
uint32_t filters = index / 2;
|
||||
uint32_t enable = 0;
|
||||
for (uint32_t i = 0; i < filters; i++) enable |= 1u << (MAX_MDF_FILTER - 1 - i);
|
||||
Write(UMAC_MDF_CTRL, enable);
|
||||
}
|
||||
|
||||
static bool AllocateBuffers(uint8_t** bufs, uint32_t count) {
|
||||
constexpr uint32_t PerPage = 0x1000 / BUF_LENGTH;
|
||||
for (uint32_t i = 0; i < count; i += PerPage) {
|
||||
uint8_t* page = (uint8_t*)Memory::g_pfa->AllocateZeroed();
|
||||
if (page == nullptr) return false;
|
||||
// The zeroes sit dirty in the cache; write them back now so no
|
||||
// eviction lands on top of a frame the MAC wrote later.
|
||||
Memory::Dma::SyncForDevice(page, 0x1000, g_coherent);
|
||||
for (uint32_t j = 0; j < PerPage && i + j < count; j++) bufs[i + j] = page + j * BUF_LENGTH;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ring 16 spans all 256 descriptors (START_ADDR/END_ADDR count 32-bit
|
||||
// words). The producer/consumer counters are zeroed, but if the MAC
|
||||
// keeps an old value the software side follows it instead: the slot is
|
||||
// always counter % 256.
|
||||
static void InitRxRing(uint32_t burst) {
|
||||
Write(RDMA_REGS + DMA_SCB_BURST_SIZE, burst);
|
||||
|
||||
Write(RDMA_RING + RDMA_PROD_INDEX, 0);
|
||||
Write(RDMA_RING + RDMA_CONS_INDEX, 0);
|
||||
Write(RDMA_RING + DMA_START_ADDR, 0);
|
||||
Write(RDMA_RING + RDMA_READ_PTR, 0);
|
||||
Write(RDMA_RING + RDMA_WRITE_PTR, 0);
|
||||
Write(RDMA_RING + DMA_END_ADDR, RX_DESCS * DESC_SIZE / 4 - 1);
|
||||
Write(RDMA_RING + DMA_RING_BUF_SIZE, (RX_DESCS << 16) | BUF_LENGTH);
|
||||
Write(RDMA_RING + RDMA_XON_XOFF_THRESH, (5u << 16) | (RX_DESCS >> 4));
|
||||
Write(RDMA_RING + DMA_MBUF_DONE_THRESH, 1);
|
||||
|
||||
uint32_t cons = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF;
|
||||
Write(RDMA_RING + RDMA_CONS_INDEX, cons);
|
||||
g_rxCons.store(cons, std::memory_order_relaxed);
|
||||
if (cons != 0) {
|
||||
KernelLogStream(INFO, "GENET") << "RX ring resumes at index " << base::dec << (uint64_t)cons;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < RX_DESCS; i++) {
|
||||
uint64_t phys = Memory::SubHHDM(g_rxBufs[i]);
|
||||
Write(DescReg(RX_DESC_BASE, i, 1), (uint32_t)phys);
|
||||
Write(DescReg(RX_DESC_BASE, i, 2), (uint32_t)(phys >> 32));
|
||||
Write(DescReg(RX_DESC_BASE, i, 0), (BUF_LENGTH << DMA_BUFLENGTH_SHIFT) | DMA_OWN);
|
||||
}
|
||||
|
||||
Write(RDMA_REGS + DMA_RING_CFG, 1u << DEFAULT_Q);
|
||||
}
|
||||
|
||||
static void InitTxRing(uint32_t burst) {
|
||||
Write(TDMA_REGS + DMA_SCB_BURST_SIZE, burst);
|
||||
|
||||
Write(TDMA_RING + TDMA_PROD_INDEX, 0);
|
||||
Write(TDMA_RING + TDMA_CONS_INDEX, 0);
|
||||
Write(TDMA_RING + DMA_START_ADDR, 0);
|
||||
Write(TDMA_RING + TDMA_READ_PTR, 0);
|
||||
Write(TDMA_RING + TDMA_WRITE_PTR, 0);
|
||||
Write(TDMA_RING + DMA_END_ADDR, TX_DESCS * DESC_SIZE / 4 - 1);
|
||||
Write(TDMA_RING + DMA_RING_BUF_SIZE, (TX_DESCS << 16) | BUF_LENGTH);
|
||||
Write(TDMA_RING + DMA_MBUF_DONE_THRESH, 1);
|
||||
Write(TDMA_RING + TDMA_FLOW_PERIOD, 0);
|
||||
|
||||
g_txProd = Read(TDMA_RING + TDMA_CONS_INDEX) & 0xFFFF;
|
||||
Write(TDMA_RING + TDMA_PROD_INDEX, g_txProd);
|
||||
if (g_txProd != 0) {
|
||||
KernelLogStream(INFO, "GENET") << "TX ring resumes at index " << base::dec << (uint64_t)g_txProd;
|
||||
}
|
||||
|
||||
Write(TDMA_REGS + DMA_RING_CFG, 1u << DEFAULT_Q);
|
||||
}
|
||||
|
||||
static void StartDma() {
|
||||
uint32_t ctrl = (1u << (DEFAULT_Q + DMA_RING_BUF_EN_SHIFT)) | DMA_EN;
|
||||
Write(TDMA_REGS + DMA_CTRL, ctrl);
|
||||
Write(RDMA_REGS + DMA_CTRL, ctrl);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Interrupt
|
||||
// ====================================================================
|
||||
|
||||
// Acknowledge and mask; the frames are dispatched from the bottom half,
|
||||
// which takes process-context locks.
|
||||
static void HandleInterrupt(uint8_t, bool) {
|
||||
if (!g_initialized) return;
|
||||
uint32_t status = Read(INTRL2_0 + INTRL2_CPU_STAT) & ~Read(INTRL2_0 + INTRL2_CPU_MASK_STATUS);
|
||||
if (status == 0) return;
|
||||
Write(INTRL2_0 + INTRL2_CPU_CLEAR, status);
|
||||
|
||||
if (status & UMAC_IRQ_RXDMA_DONE) {
|
||||
Write(INTRL2_0 + INTRL2_CPU_MASK_SET, UMAC_IRQ_RXDMA_DONE);
|
||||
g_rxWorkPending.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Receive
|
||||
// ====================================================================
|
||||
|
||||
static void PollRx() {
|
||||
uint32_t cons = g_rxCons.load(std::memory_order_relaxed);
|
||||
uint32_t prod = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF;
|
||||
|
||||
// At most one revolution per pass; the bottom half comes back for
|
||||
// the rest.
|
||||
uint32_t processed = 0;
|
||||
while (cons != prod && processed < RX_DESCS) {
|
||||
Arch::DmaReadFence();
|
||||
uint32_t slot = cons % RX_DESCS;
|
||||
uint32_t lenStat = Read(DescReg(RX_DESC_BASE, slot, 0));
|
||||
uint32_t length = (lenStat >> DMA_BUFLENGTH_SHIFT) & DMA_BUFLENGTH_MASK;
|
||||
uint8_t* buffer = g_rxBufs[slot];
|
||||
|
||||
bool whole = (lenStat & (DMA_SOP | DMA_EOP)) == (DMA_SOP | DMA_EOP);
|
||||
if (!whole || (lenStat & DMA_RX_ERRORS) || length <= RX_BUF_OFFSET || length > BUF_LENGTH) {
|
||||
g_rxErrorCount++;
|
||||
} else {
|
||||
Memory::Dma::SyncForCpu(buffer, length, g_coherent);
|
||||
g_rxPacketCount++;
|
||||
if (g_rxCallback != nullptr) {
|
||||
g_rxCallback(buffer + RX_BUF_OFFSET, (uint16_t)(length - RX_BUF_OFFSET));
|
||||
}
|
||||
}
|
||||
|
||||
cons = (cons + 1) & 0xFFFF;
|
||||
g_rxCons.store(cons, std::memory_order_relaxed);
|
||||
Write(RDMA_RING + RDMA_CONS_INDEX, cons);
|
||||
processed++;
|
||||
if (cons == prod) prod = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
static bool RxPending() {
|
||||
uint32_t prod = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF;
|
||||
return prod != g_rxCons.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Public API
|
||||
// ====================================================================
|
||||
|
||||
bool ProbePlatform(uint64_t mmioPhys, uint64_t mmioSize, uint32_t gsi, bool levelTriggered,
|
||||
bool activeLow, bool dmaCoherent, PhyMode phyMode, uint32_t burst) {
|
||||
if (g_initialized) return false;
|
||||
|
||||
if (mmioSize < 0x10000) mmioSize = 0x10000;
|
||||
for (uint64_t offset = 0; offset < mmioSize; offset += 0x1000) {
|
||||
Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset));
|
||||
}
|
||||
g_regs = (volatile uint8_t*)Memory::HHDM(mmioPhys);
|
||||
g_coherent = dmaCoherent;
|
||||
g_phyMode = phyMode;
|
||||
if (burst == 0) burst = 8;
|
||||
|
||||
// GENET v5 reports major revision 6 (v4 reports 5).
|
||||
uint32_t rev = Read(SYS_REV_CTRL);
|
||||
uint32_t major = (rev >> 24) & 0x0F;
|
||||
if (major != 6) {
|
||||
KernelLogStream(WARNING, "GENET") << "Unsupported revision 0x" << base::hex << (uint64_t)rev;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mask everything while the controller is reconfigured.
|
||||
Write(INTRL2_0 + INTRL2_CPU_MASK_SET, 0xFFFFFFFF);
|
||||
Write(INTRL2_0 + INTRL2_CPU_CLEAR, 0xFFFFFFFF);
|
||||
Write(INTRL2_1 + INTRL2_CPU_MASK_SET, 0xFFFFFFFF);
|
||||
Write(INTRL2_1 + INTRL2_CPU_CLEAR, 0xFFFFFFFF);
|
||||
|
||||
// The board's address comes from OTP through the firmware; else
|
||||
// whatever the firmware's own driver programmed.
|
||||
uint8_t firmwareMac[6];
|
||||
ReadFirmwareMac(firmwareMac);
|
||||
const char* macSource = "OTP";
|
||||
if (!Platform::RpiFirmware::GetBoardMacAddress(g_mac) || !MacUsable(g_mac)) {
|
||||
if (MacUsable(firmwareMac)) {
|
||||
for (int i = 0; i < 6; i++) g_mac[i] = firmwareMac[i];
|
||||
macSource = "firmware";
|
||||
} else {
|
||||
// Locally administered, fixed so DHCP leases survive reboots.
|
||||
static constexpr uint8_t Fallback[6] = { 0x02, 0x4D, 0x54, 0x4B, 0x00, 0x01 };
|
||||
for (int i = 0; i < 6; i++) g_mac[i] = Fallback[i];
|
||||
macSource = "fallback";
|
||||
}
|
||||
}
|
||||
|
||||
Write(SYS_PORT_CTRL, PORT_MODE_EXT_GPHY);
|
||||
StopDma();
|
||||
ResetUmac();
|
||||
ProgramAddress();
|
||||
|
||||
if (!AllocateBuffers(g_rxBufs, RX_DESCS) || !AllocateBuffers(g_txBufs, TX_DESCS)) {
|
||||
KernelLogStream(ERROR, "GENET") << "Out of memory for packet buffers";
|
||||
return false;
|
||||
}
|
||||
InitRxRing(burst);
|
||||
InitTxRing(burst);
|
||||
StartDma();
|
||||
|
||||
uint32_t phyId = 0;
|
||||
if (!FindPhy(phyId)) {
|
||||
KernelLogStream(ERROR, "GENET") << "No PHY answers on MDIO";
|
||||
StopDma();
|
||||
return false;
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "GENET") << "GENET v5." << base::dec << (uint64_t)((rev >> 16) & 0x0F)
|
||||
<< " at 0x" << base::hex << mmioPhys << ", PHY 0x" << (uint64_t)phyId
|
||||
<< " at MDIO " << base::dec << (uint64_t)g_phyAddr
|
||||
<< (dmaCoherent ? ", coherent DMA" : ", non-coherent DMA");
|
||||
KernelLogStream(OK, "GENET") << "MAC " << base::hex
|
||||
<< (uint64_t)g_mac[0] << ":" << (uint64_t)g_mac[1] << ":" << (uint64_t)g_mac[2] << ":"
|
||||
<< (uint64_t)g_mac[3] << ":" << (uint64_t)g_mac[4] << ":" << (uint64_t)g_mac[5]
|
||||
<< " (" << macSource << ")";
|
||||
|
||||
// RGMII on with the board's delays; the link (RGMII_LINK, speed,
|
||||
// TX/RX enable) follows from the PHY poll.
|
||||
uint32_t oob = Read(EXT_RGMII_OOB_CTRL);
|
||||
oob &= ~(OOB_DISABLE | RGMII_LINK);
|
||||
oob |= RGMII_MODE_EN;
|
||||
if (phyMode == PhyMode::Rgmii || phyMode == PhyMode::RgmiiId) oob |= ID_MODE_DIS;
|
||||
else oob &= ~ID_MODE_DIS;
|
||||
Write(EXT_RGMII_OOB_CTRL, oob);
|
||||
|
||||
StartPhy();
|
||||
|
||||
uint8_t line = gsi != 0 ? Hal::GsiRouting::LineForGsi(gsi, levelTriggered, activeLow) : 0;
|
||||
if (line != 0) {
|
||||
Arch::RegisterIrqHandler(line, HandleInterrupt);
|
||||
Arch::EnableLegacyIrq(line);
|
||||
KernelLogStream(OK, "GENET") << "Interrupt: GSI " << base::dec << (uint64_t)gsi
|
||||
<< " on platform line " << (uint64_t)line;
|
||||
} else {
|
||||
KernelLogStream(WARNING, "GENET") << "No interrupt, receive is polled";
|
||||
}
|
||||
|
||||
g_initialized = true;
|
||||
Write(INTRL2_0 + INTRL2_CPU_MASK_CLEAR, UMAC_IRQ_RXDMA_DONE);
|
||||
|
||||
PollLink();
|
||||
g_nextLinkPollMs.store(Timekeeping::GetMilliseconds() + LINK_POLL_MS, std::memory_order_relaxed);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Frames are copied into the slot's buffer, so the caller's buffer is
|
||||
// free again on return. Fails while the ring is full or the link down.
|
||||
bool SendPacket(const uint8_t* data, uint16_t length) {
|
||||
if (!g_initialized || data == nullptr || length == 0 || length > MAX_FRAME_LEN) return false;
|
||||
if (!g_linkUp.load(std::memory_order_acquire)) return false;
|
||||
|
||||
g_txLock.Acquire();
|
||||
|
||||
uint32_t cons = Read(TDMA_RING + TDMA_CONS_INDEX) & 0xFFFF;
|
||||
if (((g_txProd - cons) & 0xFFFF) >= TX_DESCS - 1) {
|
||||
g_txLock.Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t slot = g_txProd % TX_DESCS;
|
||||
uint8_t* buffer = g_txBufs[slot];
|
||||
memcpy(buffer, data, length);
|
||||
// The MAC sends short frames as given; pad to the 60-byte minimum.
|
||||
uint32_t frameLength = length;
|
||||
if (frameLength < 60) {
|
||||
memset(buffer + frameLength, 0, 60 - frameLength);
|
||||
frameLength = 60;
|
||||
}
|
||||
Memory::Dma::SyncForDevice(buffer, frameLength, g_coherent);
|
||||
|
||||
uint64_t phys = Memory::SubHHDM(buffer);
|
||||
Write(DescReg(TX_DESC_BASE, slot, 1), (uint32_t)phys);
|
||||
Write(DescReg(TX_DESC_BASE, slot, 2), (uint32_t)(phys >> 32));
|
||||
Write(DescReg(TX_DESC_BASE, slot, 0), (frameLength << DMA_BUFLENGTH_SHIFT) |
|
||||
(DMA_TX_QTAG_MASK << DMA_TX_QTAG_SHIFT) | DMA_TX_APPEND_CRC | DMA_SOP | DMA_EOP);
|
||||
|
||||
g_txProd = (g_txProd + 1) & 0xFFFF;
|
||||
Arch::DmaWriteFence();
|
||||
Write(TDMA_RING + TDMA_PROD_INDEX, g_txProd);
|
||||
g_txPacketCount++;
|
||||
|
||||
g_txLock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t* GetMacAddress() {
|
||||
return g_mac;
|
||||
}
|
||||
|
||||
bool IsInitialized() {
|
||||
return g_initialized;
|
||||
}
|
||||
|
||||
bool IsLinkUp() {
|
||||
return g_initialized && g_linkUp.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
uint32_t GetLinkSpeed() {
|
||||
return IsLinkUp() ? g_linkSpeed : 0;
|
||||
}
|
||||
|
||||
uint64_t GetRxPacketCount() {
|
||||
return g_rxPacketCount;
|
||||
}
|
||||
|
||||
uint64_t GetTxPacketCount() {
|
||||
return g_txPacketCount;
|
||||
}
|
||||
|
||||
void SetRxCallback(RxCallback callback) {
|
||||
g_rxCallback = callback;
|
||||
}
|
||||
|
||||
bool HasDeferredWork() {
|
||||
if (!g_initialized) return false;
|
||||
return g_rxWorkPending.load(std::memory_order_acquire) || RxPending() ||
|
||||
Timekeeping::GetMilliseconds() >= g_nextLinkPollMs.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void ProcessDeferredWork() {
|
||||
if (!g_initialized) return;
|
||||
|
||||
bool expected = false;
|
||||
if (!g_processing.compare_exchange_strong(expected, true,
|
||||
std::memory_order_acquire, std::memory_order_relaxed)) return;
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (now >= g_nextLinkPollMs.load(std::memory_order_relaxed)) {
|
||||
g_nextLinkPollMs.store(now + LINK_POLL_MS, std::memory_order_relaxed);
|
||||
PollLink();
|
||||
}
|
||||
|
||||
g_rxWorkPending.store(false, std::memory_order_release);
|
||||
PollRx();
|
||||
|
||||
// A frame that arrived while masked has latched the status bit, so
|
||||
// unmasking raises the interrupt again rather than losing it.
|
||||
Write(INTRL2_0 + INTRL2_CPU_MASK_CLEAR, UMAC_IRQ_RXDMA_DONE);
|
||||
|
||||
g_processing.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Genet.hpp
|
||||
* Broadcom GENET v5 Ethernet driver (Raspberry Pi 4 / 400)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// The BCM2711's on-chip Gigabit Ethernet MAC (GENET v5, a UniMAC behind a
|
||||
// DMA engine), wired over RGMII to a BCM54213PE PHY at MDIO address 1. The
|
||||
// Pi Firmware Task Force UEFI firmware describes it as BCM6E4E
|
||||
// (\_SB.ETH0): one register block, two interrupts (the first carries the
|
||||
// default queue), _CCA = 0 (buffers do not snoop the CPU caches) and a
|
||||
// _DSD with "phy-mode" and "brcm,max-dma-burst-size".
|
||||
//
|
||||
// Only the default queue (ring 16) is used, with all 256 descriptors of
|
||||
// each direction. The descriptors live in the MAC's own SRAM, so only the
|
||||
// packet buffers need cache maintenance.
|
||||
|
||||
namespace Drivers::Net::Genet {
|
||||
|
||||
// ---- Register map (offsets from the block base) ----
|
||||
|
||||
constexpr uint32_t SYS_REV_CTRL = 0x0000;
|
||||
constexpr uint32_t SYS_PORT_CTRL = 0x0004;
|
||||
constexpr uint32_t SYS_RBUF_FLUSH_CTRL = 0x0008;
|
||||
constexpr uint32_t SYS_TBUF_FLUSH_CTRL = 0x000C;
|
||||
|
||||
constexpr uint32_t EXT_RGMII_OOB_CTRL = 0x008C;
|
||||
|
||||
constexpr uint32_t INTRL2_0 = 0x0200; // default queue, MAC events
|
||||
constexpr uint32_t INTRL2_1 = 0x0240; // priority queues (unused)
|
||||
constexpr uint32_t INTRL2_CPU_STAT = 0x00;
|
||||
constexpr uint32_t INTRL2_CPU_CLEAR = 0x08;
|
||||
constexpr uint32_t INTRL2_CPU_MASK_STATUS = 0x0C;
|
||||
constexpr uint32_t INTRL2_CPU_MASK_SET = 0x10;
|
||||
constexpr uint32_t INTRL2_CPU_MASK_CLEAR = 0x14;
|
||||
|
||||
constexpr uint32_t RBUF_CTRL = 0x0300;
|
||||
constexpr uint32_t RBUF_TBUF_SIZE_CTRL = 0x03B4;
|
||||
constexpr uint32_t TBUF_CTRL = 0x0600;
|
||||
|
||||
constexpr uint32_t UMAC_CMD = 0x0808;
|
||||
constexpr uint32_t UMAC_MAC0 = 0x080C;
|
||||
constexpr uint32_t UMAC_MAC1 = 0x0810;
|
||||
constexpr uint32_t UMAC_MAX_FRAME_LEN = 0x0814;
|
||||
constexpr uint32_t UMAC_TX_FLUSH = 0x0B34;
|
||||
constexpr uint32_t UMAC_MIB_CTRL = 0x0D80;
|
||||
constexpr uint32_t MDIO_CMD = 0x0E14;
|
||||
constexpr uint32_t UMAC_MDF_CTRL = 0x0E50;
|
||||
constexpr uint32_t UMAC_MDF_ADDR = 0x0E54;
|
||||
|
||||
// Descriptor SRAM, then the DMA registers: 17 rings of 0x40, then the
|
||||
// engine's own registers.
|
||||
constexpr uint32_t TOTAL_DESCS = 256;
|
||||
constexpr uint32_t DESC_SIZE = 12; // length/status, address lo, address hi
|
||||
constexpr uint32_t RX_DESC_BASE = 0x2000;
|
||||
constexpr uint32_t TX_DESC_BASE = 0x4000;
|
||||
constexpr uint32_t RDMA_BASE = RX_DESC_BASE + TOTAL_DESCS * DESC_SIZE;
|
||||
constexpr uint32_t TDMA_BASE = TX_DESC_BASE + TOTAL_DESCS * DESC_SIZE;
|
||||
constexpr uint32_t DEFAULT_Q = 16;
|
||||
constexpr uint32_t DMA_RING_SIZE = 0x40;
|
||||
constexpr uint32_t RDMA_RING = RDMA_BASE + DEFAULT_Q * DMA_RING_SIZE;
|
||||
constexpr uint32_t TDMA_RING = TDMA_BASE + DEFAULT_Q * DMA_RING_SIZE;
|
||||
constexpr uint32_t RDMA_REGS = RDMA_BASE + (DEFAULT_Q + 1) * DMA_RING_SIZE;
|
||||
constexpr uint32_t TDMA_REGS = TDMA_BASE + (DEFAULT_Q + 1) * DMA_RING_SIZE;
|
||||
|
||||
// Per-ring registers. RX: the MAC produces, software consumes; TX the
|
||||
// other way round. Both indices are 16-bit counters.
|
||||
constexpr uint32_t RDMA_WRITE_PTR = 0x00;
|
||||
constexpr uint32_t RDMA_PROD_INDEX = 0x08;
|
||||
constexpr uint32_t RDMA_CONS_INDEX = 0x0C;
|
||||
constexpr uint32_t RDMA_XON_XOFF_THRESH = 0x28;
|
||||
constexpr uint32_t RDMA_READ_PTR = 0x2C;
|
||||
constexpr uint32_t TDMA_READ_PTR = 0x00;
|
||||
constexpr uint32_t TDMA_CONS_INDEX = 0x08;
|
||||
constexpr uint32_t TDMA_PROD_INDEX = 0x0C;
|
||||
constexpr uint32_t TDMA_FLOW_PERIOD = 0x28;
|
||||
constexpr uint32_t TDMA_WRITE_PTR = 0x2C;
|
||||
constexpr uint32_t DMA_RING_BUF_SIZE = 0x10;
|
||||
constexpr uint32_t DMA_START_ADDR = 0x14;
|
||||
constexpr uint32_t DMA_END_ADDR = 0x1C;
|
||||
constexpr uint32_t DMA_MBUF_DONE_THRESH = 0x24;
|
||||
|
||||
// Engine registers.
|
||||
constexpr uint32_t DMA_RING_CFG = 0x00;
|
||||
constexpr uint32_t DMA_CTRL = 0x04;
|
||||
constexpr uint32_t DMA_STATUS = 0x08;
|
||||
constexpr uint32_t DMA_SCB_BURST_SIZE = 0x0C;
|
||||
|
||||
// ---- Register bits ----
|
||||
|
||||
constexpr uint32_t PORT_MODE_EXT_GPHY = 3;
|
||||
|
||||
constexpr uint32_t RGMII_LINK = 1u << 4;
|
||||
constexpr uint32_t OOB_DISABLE = 1u << 5;
|
||||
constexpr uint32_t RGMII_MODE_EN = 1u << 6;
|
||||
constexpr uint32_t ID_MODE_DIS = 1u << 16; // no MAC-side TX clock delay
|
||||
|
||||
constexpr uint32_t RBUF_64B_EN = 1u << 0; // receive status block
|
||||
constexpr uint32_t RBUF_ALIGN_2B = 1u << 1; // 2 pad bytes before each frame
|
||||
constexpr uint32_t TBUF_64B_EN = 1u << 0; // transmit status block
|
||||
|
||||
constexpr uint32_t CMD_TX_EN = 1u << 0;
|
||||
constexpr uint32_t CMD_RX_EN = 1u << 1;
|
||||
constexpr uint32_t CMD_SPEED_SHIFT = 2;
|
||||
constexpr uint32_t CMD_SPEED_10 = 0;
|
||||
constexpr uint32_t CMD_SPEED_100 = 1;
|
||||
constexpr uint32_t CMD_SPEED_1000 = 2;
|
||||
constexpr uint32_t CMD_PROMISC = 1u << 4;
|
||||
constexpr uint32_t CMD_HD_EN = 1u << 10;
|
||||
constexpr uint32_t CMD_SW_RESET = 1u << 13;
|
||||
constexpr uint32_t CMD_LCL_LOOP_EN = 1u << 15;
|
||||
|
||||
constexpr uint32_t MIB_RESET_RX = 1u << 0;
|
||||
constexpr uint32_t MIB_RESET_RUNT = 1u << 1;
|
||||
constexpr uint32_t MIB_RESET_TX = 1u << 2;
|
||||
|
||||
constexpr uint32_t MDIO_START_BUSY = 1u << 29;
|
||||
constexpr uint32_t MDIO_READ_FAIL = 1u << 28;
|
||||
constexpr uint32_t MDIO_RD = 2u << 26;
|
||||
constexpr uint32_t MDIO_WR = 1u << 26;
|
||||
constexpr uint32_t MDIO_PMD_SHIFT = 21;
|
||||
constexpr uint32_t MDIO_REG_SHIFT = 16;
|
||||
|
||||
constexpr uint32_t MAX_MDF_FILTER = 17;
|
||||
|
||||
constexpr uint32_t DMA_EN = 1u << 0;
|
||||
constexpr uint32_t DMA_RING_BUF_EN_SHIFT = 1;
|
||||
constexpr uint32_t DMA_DISABLED = 1u << 0; // DMA_STATUS
|
||||
|
||||
constexpr uint32_t UMAC_IRQ_RXDMA_DONE = 1u << 13; // ring 16 reached its done threshold
|
||||
|
||||
// Descriptor length/status word: length in 27:16, flags in 15:0.
|
||||
constexpr uint32_t DMA_BUFLENGTH_SHIFT = 16;
|
||||
constexpr uint32_t DMA_BUFLENGTH_MASK = 0x0FFF;
|
||||
constexpr uint32_t DMA_OWN = 0x8000;
|
||||
constexpr uint32_t DMA_EOP = 0x4000;
|
||||
constexpr uint32_t DMA_SOP = 0x2000;
|
||||
constexpr uint32_t DMA_TX_APPEND_CRC = 0x0040;
|
||||
constexpr uint32_t DMA_TX_QTAG_SHIFT = 7;
|
||||
constexpr uint32_t DMA_TX_QTAG_MASK = 0x3F;
|
||||
constexpr uint32_t DMA_RX_LG = 0x0010;
|
||||
constexpr uint32_t DMA_RX_NO = 0x0008;
|
||||
constexpr uint32_t DMA_RX_RXER = 0x0004;
|
||||
constexpr uint32_t DMA_RX_CRC_ERROR = 0x0002;
|
||||
constexpr uint32_t DMA_RX_OV = 0x0001;
|
||||
constexpr uint32_t DMA_RX_ERRORS = DMA_RX_LG | DMA_RX_NO | DMA_RX_RXER |
|
||||
DMA_RX_CRC_ERROR | DMA_RX_OV;
|
||||
|
||||
// ---- Driver parameters ----
|
||||
|
||||
constexpr uint32_t RX_DESCS = TOTAL_DESCS;
|
||||
constexpr uint32_t TX_DESCS = TOTAL_DESCS;
|
||||
constexpr uint32_t BUF_LENGTH = 2048;
|
||||
constexpr uint32_t RX_BUF_OFFSET = 2; // RBUF_ALIGN_2B padding
|
||||
// Payload 1500 + header 14 + VLAN 4 + Broadcom tag 6 + FCS 4 + pad 8.
|
||||
constexpr uint32_t MAX_FRAME_LEN = 1536;
|
||||
|
||||
// How the board wires RGMII clock delays (ACPI _DSD "phy-mode").
|
||||
enum class PhyMode : uint8_t {
|
||||
Rgmii, // no delays from the PHY or the MAC
|
||||
RgmiiId, // the PHY delays both clocks
|
||||
RgmiiRxid, // the PHY delays RX, the MAC delays TX (Raspberry Pi 4)
|
||||
RgmiiTxid,
|
||||
};
|
||||
|
||||
// Take over the controller at `mmioPhys`. `gsi` is the default-queue
|
||||
// interrupt (0: poll only). `burst` is the DMA burst size in 32-bit
|
||||
// words from "brcm,max-dma-burst-size" (0: 8).
|
||||
bool ProbePlatform(uint64_t mmioPhys, uint64_t mmioSize, uint32_t gsi, bool levelTriggered,
|
||||
bool activeLow, bool dmaCoherent, PhyMode phyMode, uint32_t burst);
|
||||
|
||||
bool SendPacket(const uint8_t* data, uint16_t length);
|
||||
const uint8_t* GetMacAddress();
|
||||
bool IsInitialized();
|
||||
|
||||
// Carrier as of the last PHY poll (once a second from deferred work).
|
||||
bool IsLinkUp();
|
||||
|
||||
// Negotiated speed in Mbit/s, 0 while the link is down.
|
||||
uint32_t GetLinkSpeed();
|
||||
|
||||
uint64_t GetRxPacketCount();
|
||||
uint64_t GetTxPacketCount();
|
||||
|
||||
using RxCallback = void(*)(const uint8_t* data, uint16_t length);
|
||||
void SetRxCallback(RxCallback callback);
|
||||
|
||||
// The interrupt only masks and flags; frames are dispatched and the PHY
|
||||
// polled from the idle bottom half. HasDeferredWork also looks at the
|
||||
// ring itself, so receive keeps working if the interrupt never fires.
|
||||
bool HasDeferredWork();
|
||||
void ProcessDeferredWork();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Brcmf.hpp
|
||||
* Broadcom/Cypress FullMAC Wi-Fi over SDIO (CYW43455 / CYW43456 on the
|
||||
* Raspberry Pi 4 / 400): driver-internal interface
|
||||
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
|
||||
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
// A FullMAC radio runs the 802.11 MLME itself: the host asks for scans and
|
||||
// joins through ioctls and sees Ethernet frames. Layout, after Linux's
|
||||
// brcmfmac:
|
||||
//
|
||||
// BrcmfSdio.cpp the SD controller as an SDIO host (CMD52/CMD53)
|
||||
// BrcmfChip.cpp backplane access, core enumeration (EROM), clocks,
|
||||
// firmware + NVRAM download, F2 bring-up, firmware console
|
||||
// BrcmfProto.cpp SDPCM framing, CDC ioctls, BDC data, the RX pump,
|
||||
// firmware events
|
||||
// BrcmfConnect.cpp preinit, CLM, escan, join, keys, the facade entry points
|
||||
//
|
||||
// The handshake runs on the host (Wpa.cpp), exactly as for the Intel
|
||||
// adapter: EAPOL frames arrive as Ethernet frames and the keys go back to the
|
||||
// firmware with "wsec_key".
|
||||
//
|
||||
// Threading: everything on the bus holds g_brcmf.BusLock for the length of one
|
||||
// SDIO exchange. The RX pump (BrcmfPoll) has a reentrancy guard and releases
|
||||
// the bus lock before dispatching, so the network stack may transmit from
|
||||
// inside a delivered frame. Dispatch must never issue an ioctl -- its answer
|
||||
// is read by the very pump it would be waiting inside. Work needing one is
|
||||
// queued for BrcmfConnectService().
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
|
||||
// Facade-visible adapter state; values are the WIFI_STATE_* ABI ones.
|
||||
enum class BrcmfState : uint8_t {
|
||||
Absent = 0,
|
||||
Detected,
|
||||
Booting,
|
||||
Running,
|
||||
Error,
|
||||
};
|
||||
|
||||
struct BrcmfCore {
|
||||
uint16_t Id = 0;
|
||||
uint8_t Rev = 0;
|
||||
uint32_t Base = 0;
|
||||
uint32_t Wrap = 0;
|
||||
};
|
||||
|
||||
struct BrcmfDevice {
|
||||
BrcmfState State = BrcmfState::Absent;
|
||||
uint64_t ArasanMmio = 0;
|
||||
|
||||
// Chip
|
||||
uint16_t ChipId = 0;
|
||||
uint8_t ChipRev = 0;
|
||||
const char* FwName = nullptr; // "43455" / "43456"
|
||||
BrcmfCore Cores[16];
|
||||
uint32_t CoreCount = 0;
|
||||
uint32_t RamBase = 0;
|
||||
uint32_t RamSize = 0;
|
||||
uint32_t SdioCoreBase = 0;
|
||||
uint32_t PmuBase = 0;
|
||||
bool SrEnabled = false;
|
||||
uint32_t SharedAddr = 0; // sdpcm_shared, 0 = not read yet
|
||||
uint32_t ConsoleIdx = 0; // console bytes already printed
|
||||
|
||||
// Bus
|
||||
kcp::Mutex BusLock;
|
||||
uint32_t Window = 0xFFFFFFFF; // backplane window, 0xFFFFFFFF = unknown
|
||||
uint32_t HostIntMask = 0;
|
||||
|
||||
// SDPCM
|
||||
uint8_t TxSeq = 0;
|
||||
uint8_t TxMax = 1;
|
||||
bool RxPending = false;
|
||||
|
||||
// Firmware
|
||||
uint8_t Mac[6] = {};
|
||||
char FwVersion[32] = {};
|
||||
uint16_t ChannelCount = 0;
|
||||
bool Band24 = true;
|
||||
bool Band5 = false;
|
||||
|
||||
// Health
|
||||
uint32_t FwErrors = 0;
|
||||
uint32_t CmdFailBucket = 0; // leaky bucket of unanswered ioctls
|
||||
uint64_t RxPackets = 0;
|
||||
uint64_t TxPackets = 0;
|
||||
uint64_t LastPollMs = 0;
|
||||
};
|
||||
|
||||
extern BrcmfDevice g_brcmf;
|
||||
|
||||
// =========================================================================
|
||||
// BrcmfChip.cpp
|
||||
// =========================================================================
|
||||
|
||||
bool BrcmfBpRead32(uint32_t addr, uint32_t& value);
|
||||
bool BrcmfBpWrite32(uint32_t addr, uint32_t value);
|
||||
bool BrcmfBpReadMem(uint32_t addr, uint8_t* buffer, uint32_t len);
|
||||
bool BrcmfBpWriteMem(uint32_t addr, const uint8_t* buffer, uint32_t len);
|
||||
// Point the backplane window at `addr` (function 2 transfers want it on
|
||||
// ChipCommon, as Linux keeps it).
|
||||
bool BrcmfBpSelectWindow(uint32_t addr);
|
||||
|
||||
// Function-1 SDIO-core registers in the 0x1000x space.
|
||||
bool BrcmfF1Read8(uint32_t reg, uint8_t& value);
|
||||
bool BrcmfF1Write8(uint32_t reg, uint8_t value);
|
||||
|
||||
// Power the radio, enumerate the SDIO card, identify the chip, download
|
||||
// firmware + NVRAM, start the ARM and bring up function 2. Leaves
|
||||
// g_brcmf ready for SDPCM traffic.
|
||||
bool BrcmfChipBringUp();
|
||||
|
||||
// Hold the radio in reset (WL_ON low).
|
||||
void BrcmfChipPowerOff();
|
||||
|
||||
// Print what the firmware wrote to its console since the last call, and
|
||||
// a trap/assert summary if it has one. Takes the bus lock.
|
||||
void BrcmfDumpConsole();
|
||||
|
||||
// Load 0:/os/firmware/brcm/brcmfmac<chip>-sdio.<ext>. The caller frees.
|
||||
uint8_t* BrcmfLoadFile(const char* ext, uint32_t& size, bool required);
|
||||
|
||||
// =========================================================================
|
||||
// BrcmfProto.cpp
|
||||
// =========================================================================
|
||||
|
||||
void BrcmfProtoReset();
|
||||
|
||||
// Drain the device: interrupt status, mailbox, every pending frame, and
|
||||
// queued transmits. Reentrancy guarded; returns at once if another core
|
||||
// is already pumping.
|
||||
void BrcmfPoll();
|
||||
|
||||
// Whether a poll is due (card interrupt, queued TX, the periodic sweep).
|
||||
bool BrcmfPollDue(uint64_t now, uint64_t intervalMs);
|
||||
|
||||
// Synchronous firmware command. `buf` carries the request (for a query,
|
||||
// its first bytes, e.g. an iovar name) and receives up to `len` bytes of
|
||||
// answer. Returns false on a transport failure, a timeout, or a negative
|
||||
// firmware status (*fwStatus, when given, receives it).
|
||||
bool BrcmfIoctl(uint32_t cmd, bool set, void* buf, uint32_t len, int32_t* fwStatus = nullptr);
|
||||
|
||||
bool BrcmfSetInt(uint32_t cmd, uint32_t value);
|
||||
bool BrcmfGetInt(uint32_t cmd, uint32_t& value);
|
||||
bool BrcmfIovarSet(const char* name, const void* data, uint32_t len, bool quiet = false);
|
||||
bool BrcmfIovarGet(const char* name, void* out, uint32_t len, bool quiet = false);
|
||||
bool BrcmfIovarSetInt(const char* name, uint32_t value, bool quiet = false);
|
||||
bool BrcmfIovarGetInt(const char* name, uint32_t& value, bool quiet = false);
|
||||
|
||||
// Queue an Ethernet frame for transmission (802.3, 14-byte header).
|
||||
bool BrcmfTxEthernet(const uint8_t* frame, uint32_t len);
|
||||
bool BrcmfTxPending();
|
||||
|
||||
// =========================================================================
|
||||
// BrcmfConnect.cpp -- called from the protocol layer
|
||||
// =========================================================================
|
||||
|
||||
// A firmware event. Runs inside the pump: no ioctls.
|
||||
void BrcmfOnEvent(uint32_t type, uint32_t status, uint32_t reason, uint16_t flags,
|
||||
const uint8_t* addr, const uint8_t* data, uint32_t len);
|
||||
|
||||
// A received Ethernet frame. Runs inside the pump.
|
||||
void BrcmfOnRxEthernet(const uint8_t* frame, uint32_t len);
|
||||
|
||||
// The firmware halted or stopped answering.
|
||||
void BrcmfOnFirmwareFailure(const char* why);
|
||||
|
||||
// =========================================================================
|
||||
// BrcmfConnect.cpp -- the facade's backend (Wifi.cpp)
|
||||
// =========================================================================
|
||||
|
||||
bool BrcmfProbe(uint64_t arasanMmio);
|
||||
bool BrcmfCompleteInit();
|
||||
void BrcmfShutdown(); // for recovery: power the radio off
|
||||
|
||||
BrcmfState BrcmfGetState();
|
||||
bool BrcmfHasWork(uint64_t now);
|
||||
void BrcmfService(); // pump + connection service
|
||||
|
||||
bool BrcmfStartScan();
|
||||
void BrcmfAbortScan();
|
||||
bool BrcmfScanActive();
|
||||
|
||||
bool BrcmfConnectStart(const uint8_t* bssid, uint8_t channel, const char* ssid,
|
||||
const char* password, const uint8_t* rsnIe, uint8_t rsnIeLen);
|
||||
void BrcmfConnectAbort();
|
||||
int BrcmfConnectState(); // IwxConnStateId values
|
||||
bool BrcmfConnectRefusedForSecurity();
|
||||
bool BrcmfJoinFailedOnAuth();
|
||||
const char* BrcmfConnectSsid();
|
||||
const uint8_t* BrcmfConnectBssid();
|
||||
uint8_t BrcmfConnectChannel();
|
||||
bool BrcmfLinkUp();
|
||||
bool BrcmfSendEthernet(const uint8_t* frame, uint32_t len);
|
||||
|
||||
bool BrcmfWpaTxEapol(const uint8_t* body, uint32_t len);
|
||||
bool BrcmfWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher);
|
||||
bool BrcmfWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc);
|
||||
|
||||
// =========================================================================
|
||||
// Wifi.cpp -- the facade, called by the backend
|
||||
// =========================================================================
|
||||
|
||||
// One BSS from a scan: its information elements, as in a beacon body
|
||||
// after the fixed fields.
|
||||
void WifiRxBssInfo(const uint8_t* bssid, uint16_t beaconInterval, uint16_t capability,
|
||||
const uint8_t* ies, uint32_t iesLen, uint8_t channel, int8_t rssiDbm);
|
||||
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,849 @@
|
||||
/*
|
||||
* BrcmfChip.cpp
|
||||
* Broadcom FullMAC over SDIO: backplane access, core enumeration, clocks,
|
||||
* firmware and NVRAM download, function 2 bring-up, firmware console
|
||||
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
|
||||
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Brcmf.hpp"
|
||||
#include "BrcmfSdio.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Drivers/Platform/RpiFirmware.hpp>
|
||||
#include <Drivers/Platform/RpiGpio.hpp>
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Memory/Heap.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
// The radio is a small SoC of its own (a Broadcom "AI" interconnect): an
|
||||
// ARM Cortex-R4 with its tightly coupled RAM, the 802.11 MAC/PHY core (D11),
|
||||
// ChipCommon (with the PMU) and an SDIO device core. Function 1 of the SDIO
|
||||
// card is a window onto that backplane: three window registers select the
|
||||
// 32 KiB page, and CMD53 at offset | 0x8000 reaches it.
|
||||
//
|
||||
// Bring-up follows Linux's brcmfmac (sdio.c, chip.c, firmware.c):
|
||||
// 1. route GPIO 34-39 to the Arasan controller, pulse WL_ON (line 1 of the
|
||||
// VideoCore's GPIO expander), enumerate the card;
|
||||
// 2. request the ALP clock, read the chip ID, walk the enumeration ROM for
|
||||
// the cores;
|
||||
// 3. halt the ARM and hold the D11 core in reset, size the RAM from the
|
||||
// CR4's bank registers;
|
||||
// 4. write the firmware at the RAM base and the NVRAM at its top (with the
|
||||
// length token in the last word), write the reset vector to address 0
|
||||
// and release the ARM;
|
||||
// 5. wait for the HT clock the firmware brings up, tell the SDIO core the
|
||||
// protocol version, enable function 2 (frames) and its interrupts.
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
|
||||
namespace RpiFirmware = Drivers::Platform::RpiFirmware;
|
||||
namespace RpiGpio = Drivers::Platform::RpiGpio;
|
||||
|
||||
BrcmfDevice g_brcmf;
|
||||
|
||||
// =========================================================================
|
||||
// Registers
|
||||
// =========================================================================
|
||||
|
||||
// Function 1, SDIO core local space
|
||||
static constexpr uint32_t F1_WATERMARK = 0x10008;
|
||||
static constexpr uint32_t F1_DEVICE_CTL = 0x10009;
|
||||
static constexpr uint32_t F1_SBADDRLOW = 0x1000A;
|
||||
static constexpr uint32_t F1_SBADDRMID = 0x1000B;
|
||||
static constexpr uint32_t F1_SBADDRHIGH = 0x1000C;
|
||||
static constexpr uint32_t F1_CHIPCLKCSR = 0x1000E;
|
||||
static constexpr uint32_t F1_SDIOPULLUP = 0x1000F;
|
||||
static constexpr uint32_t F1_MESBUSYCTRL = 0x1001D;
|
||||
static constexpr uint32_t F1_WAKEUPCTRL = 0x1001E;
|
||||
|
||||
static constexpr uint8_t CLK_FORCE_ALP = 0x01;
|
||||
static constexpr uint8_t CLK_FORCE_HT = 0x02;
|
||||
static constexpr uint8_t CLK_ALP_AVAIL_REQ = 0x08;
|
||||
static constexpr uint8_t CLK_HT_AVAIL_REQ = 0x10;
|
||||
static constexpr uint8_t CLK_FORCE_HW_REQ_OFF= 0x20;
|
||||
static constexpr uint8_t CLK_ALP_AVAIL = 0x40;
|
||||
static constexpr uint8_t CLK_HT_AVAIL = 0x80;
|
||||
|
||||
static constexpr uint8_t DEVCTL_F2WM_ENAB = 0x10;
|
||||
static constexpr uint8_t MESBUSY_ENAB = 0x80;
|
||||
static constexpr uint8_t WAKEUP_HTWAIT = 0x02;
|
||||
|
||||
// CYW43455 values (Linux CY_43455_*): a deeper F2 watermark and
|
||||
// MES busy signalling keep the card's FIFO from overrunning.
|
||||
static constexpr uint8_t F2_WATERMARK_43455 = 0x60;
|
||||
static constexpr uint8_t MES_WATERMARK_43455 = 0x50;
|
||||
|
||||
// Broadcom vendor CCCR
|
||||
static constexpr uint32_t CCCR_BRCM_CARDCAP = 0xF0;
|
||||
static constexpr uint8_t CARDCAP_CMD14 = 0x02 | 0x04; // support | ext
|
||||
static constexpr uint32_t CCCR_BRCM_CARDCTRL = 0xF1;
|
||||
static constexpr uint8_t CARDCTRL_WLANRESET = 0x02;
|
||||
|
||||
// Backplane
|
||||
static constexpr uint32_t SI_ENUM_BASE = 0x18000000; // ChipCommon
|
||||
static constexpr uint32_t CC_CHIPID = 0x000;
|
||||
static constexpr uint32_t CC_CAPS_EXT = 0x0AC;
|
||||
static constexpr uint32_t CAPS_EXT_AOB = 0x40; // PMU in its own "always on" core
|
||||
static constexpr uint32_t CC_EROMPTR = 0x0FC;
|
||||
static constexpr uint32_t CC_PMUCONTROL = 0x600;
|
||||
static constexpr uint32_t CC_CHIPCONTROL_ADDR = 0x650;
|
||||
static constexpr uint32_t CC_CHIPCONTROL_DATA = 0x654;
|
||||
static constexpr uint32_t PMU_CTL_RES_RELOAD = 2u << 13;
|
||||
|
||||
// SDIO device core
|
||||
static constexpr uint32_t SD_INTSTATUS = 0x20;
|
||||
static constexpr uint32_t SD_HOSTINTMASK = 0x24;
|
||||
static constexpr uint32_t SD_TOSBMAILBOXDATA = 0x48;
|
||||
static constexpr uint32_t SDPCM_PROT_VERSION = 4;
|
||||
static constexpr uint32_t HOSTINTMASK = 0x000000F0 | (1u << 29); // HMB SW | CHIPACTIVE
|
||||
|
||||
// Core wrapper (AI)
|
||||
static constexpr uint32_t WRAP_IOCTL = 0x408;
|
||||
static constexpr uint32_t WRAP_RESET_CTL = 0x800;
|
||||
static constexpr uint32_t IOCTL_CLK = 0x0001;
|
||||
static constexpr uint32_t IOCTL_FGC = 0x0002;
|
||||
static constexpr uint32_t RESET_CTL_RESET = 0x0001;
|
||||
|
||||
static constexpr uint32_t CR4_IOCTL_CPUHALT = 0x0020;
|
||||
static constexpr uint32_t D11_IOCTL_PHYCLKEN = 0x0004;
|
||||
static constexpr uint32_t D11_IOCTL_PHYRESET = 0x0008;
|
||||
|
||||
// ARM CR4 core
|
||||
static constexpr uint32_t CR4_CAP = 0x04;
|
||||
static constexpr uint32_t CR4_BANKIDX = 0x40;
|
||||
static constexpr uint32_t CR4_BANKINFO = 0x44;
|
||||
|
||||
// Core IDs
|
||||
static constexpr uint16_t CORE_CHIPCOMMON = 0x800;
|
||||
static constexpr uint16_t CORE_80211 = 0x812;
|
||||
static constexpr uint16_t CORE_PMU = 0x827;
|
||||
static constexpr uint16_t CORE_SDIO_DEV = 0x829;
|
||||
static constexpr uint16_t CORE_ARM_CR4 = 0x83E;
|
||||
static constexpr uint16_t CORE_GCI = 0x840;
|
||||
|
||||
static constexpr uint16_t CHIP_4345 = 0x4345;
|
||||
static constexpr uint32_t RAMBASE_4345 = 0x198000;
|
||||
|
||||
// =========================================================================
|
||||
// Function 1 and the backplane
|
||||
// =========================================================================
|
||||
|
||||
bool BrcmfF1Read8(uint32_t reg, uint8_t& value) { return Sdio::Read8(Sdio::FN1, reg, value); }
|
||||
bool BrcmfF1Write8(uint32_t reg, uint8_t value) { return Sdio::Write8(Sdio::FN1, reg, value); }
|
||||
|
||||
static bool SetWindow(uint32_t addr) {
|
||||
uint32_t window = addr & ~0x7FFFu;
|
||||
if (window == g_brcmf.Window) return true;
|
||||
bool ok = BrcmfF1Write8(F1_SBADDRLOW, (uint8_t)((window >> 8) & 0x80))
|
||||
&& BrcmfF1Write8(F1_SBADDRMID, (uint8_t)(window >> 16))
|
||||
&& BrcmfF1Write8(F1_SBADDRHIGH, (uint8_t)(window >> 24));
|
||||
g_brcmf.Window = ok ? window : 0xFFFFFFFF;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool BrcmfBpSelectWindow(uint32_t addr) { return SetWindow(addr); }
|
||||
|
||||
// 0x8000 in the function 1 address selects a 4-byte backplane access.
|
||||
static constexpr uint32_t SB_ACCESS_4B = 0x8000;
|
||||
|
||||
bool BrcmfBpRead32(uint32_t addr, uint32_t& value) {
|
||||
uint32_t word = 0;
|
||||
if (!SetWindow(addr)) return false;
|
||||
if (!Sdio::ReadBlock(Sdio::FN1, (addr & 0x7FFF) | SB_ACCESS_4B, true, (uint8_t*)&word, 4))
|
||||
return false;
|
||||
value = word;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BrcmfBpWrite32(uint32_t addr, uint32_t value) {
|
||||
if (!SetWindow(addr)) return false;
|
||||
return Sdio::WriteBlock(Sdio::FN1, (addr & 0x7FFF) | SB_ACCESS_4B, true, (const uint8_t*)&value, 4);
|
||||
}
|
||||
|
||||
static bool BpMem(bool write, uint32_t addr, uint8_t* buffer, uint32_t len) {
|
||||
while (len > 0) {
|
||||
uint32_t offset = addr & 0x7FFF;
|
||||
uint32_t run = 0x8000 - offset;
|
||||
if (run > len) run = len;
|
||||
if (!SetWindow(addr)) return false;
|
||||
bool ok = write ? Sdio::WriteBlock(Sdio::FN1, offset, true, buffer, run)
|
||||
: Sdio::ReadBlock(Sdio::FN1, offset, true, buffer, run);
|
||||
if (!ok) return false;
|
||||
addr += run;
|
||||
buffer += run;
|
||||
len -= run;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BrcmfBpReadMem(uint32_t addr, uint8_t* buffer, uint32_t len) {
|
||||
return BpMem(false, addr, buffer, len);
|
||||
}
|
||||
|
||||
bool BrcmfBpWriteMem(uint32_t addr, const uint8_t* buffer, uint32_t len) {
|
||||
return BpMem(true, addr, (uint8_t*)buffer, len);
|
||||
}
|
||||
|
||||
static uint32_t Rd(uint32_t addr) {
|
||||
uint32_t v = 0;
|
||||
BrcmfBpRead32(addr, v);
|
||||
return v;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Cores
|
||||
// =========================================================================
|
||||
|
||||
static const BrcmfCore* FindCore(uint16_t id) {
|
||||
for (uint32_t i = 0; i < g_brcmf.CoreCount; i++)
|
||||
if (g_brcmf.Cores[i].Id == id) return &g_brcmf.Cores[i];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Enumeration ROM (Linux brcmf_chip_dmp_erom_scan).
|
||||
static constexpr uint32_t DESC_TYPE_MSK = 0x0000000F;
|
||||
static constexpr uint32_t DESC_VALID = 0x00000001;
|
||||
static constexpr uint32_t DESC_COMPONENT = 0x00000001;
|
||||
static constexpr uint32_t DESC_MASTER_PORT = 0x00000003;
|
||||
static constexpr uint32_t DESC_ADDRESS = 0x00000005;
|
||||
static constexpr uint32_t DESC_ADDRSIZE_GT32 = 0x00000008;
|
||||
static constexpr uint32_t DESC_EOT = 0x0000000F;
|
||||
static constexpr uint32_t COMP_PARTNUM_MASK = 0x000FFF00, COMP_PARTNUM_SHIFT = 8;
|
||||
static constexpr uint32_t COMP_REV_MASK = 0xFF000000, COMP_REV_SHIFT = 24;
|
||||
static constexpr uint32_t COMP_NSW_MASK = 0x00F80000, COMP_NSW_SHIFT = 19;
|
||||
static constexpr uint32_t COMP_NMW_MASK = 0x0007C000, COMP_NMW_SHIFT = 14;
|
||||
static constexpr uint32_t SLAVE_ADDR_BASE = 0xFFFFF000;
|
||||
static constexpr uint32_t SLAVE_TYPE_MASK = 0x000000C0, SLAVE_TYPE_SHIFT = 6;
|
||||
static constexpr uint32_t SLAVE_TYPE_SLAVE = 0;
|
||||
static constexpr uint32_t SLAVE_TYPE_SWRAP = 2;
|
||||
static constexpr uint32_t SLAVE_TYPE_MWRAP = 3;
|
||||
static constexpr uint32_t SLAVE_SIZE_MASK = 0x00000030, SLAVE_SIZE_SHIFT = 4;
|
||||
static constexpr uint32_t SLAVE_SIZE_4K = 0;
|
||||
static constexpr uint32_t SLAVE_SIZE_8K = 1;
|
||||
static constexpr uint32_t SLAVE_SIZE_DESC = 3;
|
||||
|
||||
static uint32_t EromNext(uint32_t& erom, uint32_t* type) {
|
||||
uint32_t v = Rd(erom);
|
||||
erom += 4;
|
||||
uint32_t t = v & DESC_TYPE_MSK;
|
||||
if ((t & ~DESC_ADDRSIZE_GT32) == DESC_ADDRESS) t = DESC_ADDRESS;
|
||||
if (type) *type = t;
|
||||
return v;
|
||||
}
|
||||
|
||||
static bool EromRegAddr(uint32_t& erom, uint32_t& regBase, uint32_t& wrapBase) {
|
||||
regBase = 0;
|
||||
wrapBase = 0;
|
||||
uint32_t desc = 0, val = 0;
|
||||
uint32_t wrapType;
|
||||
|
||||
// One descriptor decides it, as in Linux: a master port means the
|
||||
// core's wrapper is a master wrapper (any further master ports are
|
||||
// skipped by the address search below); an address straight away
|
||||
// means a slave wrapper. Cores with master ports -- the CR4, D11,
|
||||
// the SDIO core, and ChipCommon on these chips -- must not be
|
||||
// looked up as slave-wrapped: their wrapper is never found, and the
|
||||
// halt/release writes go nowhere.
|
||||
val = EromNext(erom, &desc);
|
||||
if (desc == DESC_MASTER_PORT) {
|
||||
wrapType = SLAVE_TYPE_MWRAP;
|
||||
} else if (desc == DESC_ADDRESS) {
|
||||
erom -= 4;
|
||||
wrapType = SLAVE_TYPE_SWRAP;
|
||||
} else {
|
||||
erom -= 4;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int guard = 0; guard < 64; guard++) {
|
||||
do {
|
||||
val = EromNext(erom, &desc);
|
||||
if (desc == DESC_EOT) { erom -= 4; return false; }
|
||||
} while (desc != DESC_ADDRESS && desc != DESC_COMPONENT);
|
||||
|
||||
if (desc == DESC_COMPONENT) { erom -= 4; return true; }
|
||||
|
||||
if (val & DESC_ADDRSIZE_GT32) EromNext(erom, nullptr);
|
||||
uint32_t sizeType = (val & SLAVE_SIZE_MASK) >> SLAVE_SIZE_SHIFT;
|
||||
if (sizeType == SLAVE_SIZE_DESC) {
|
||||
uint32_t size = EromNext(erom, nullptr);
|
||||
if (size & DESC_ADDRSIZE_GT32) EromNext(erom, nullptr);
|
||||
}
|
||||
if (sizeType != SLAVE_SIZE_4K && sizeType != SLAVE_SIZE_8K) continue;
|
||||
|
||||
uint32_t slaveType = (val & SLAVE_TYPE_MASK) >> SLAVE_TYPE_SHIFT;
|
||||
if (regBase == 0 && slaveType == SLAVE_TYPE_SLAVE) regBase = val & SLAVE_ADDR_BASE;
|
||||
if (wrapBase == 0 && slaveType == wrapType) wrapBase = val & SLAVE_ADDR_BASE;
|
||||
if (regBase && wrapBase) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ScanCores() {
|
||||
g_brcmf.CoreCount = 0;
|
||||
uint32_t erom = Rd(SI_ENUM_BASE + CC_EROMPTR);
|
||||
if (erom == 0 || erom == 0xFFFFFFFF) return false;
|
||||
|
||||
uint32_t desc = 0;
|
||||
for (int guard = 0; guard < 512 && desc != DESC_EOT; guard++) {
|
||||
uint32_t val = EromNext(erom, &desc);
|
||||
if (!(val & DESC_VALID) || desc != DESC_COMPONENT) continue;
|
||||
uint16_t id = (uint16_t)((val & COMP_PARTNUM_MASK) >> COMP_PARTNUM_SHIFT);
|
||||
|
||||
val = EromNext(erom, &desc);
|
||||
if (desc != DESC_COMPONENT) return false;
|
||||
uint32_t nmw = (val & COMP_NMW_MASK) >> COMP_NMW_SHIFT;
|
||||
uint32_t nsw = (val & COMP_NSW_MASK) >> COMP_NSW_SHIFT;
|
||||
uint8_t rev = (uint8_t)((val & COMP_REV_MASK) >> COMP_REV_SHIFT);
|
||||
if (nmw + nsw == 0 && id != CORE_PMU && id != CORE_GCI) continue;
|
||||
|
||||
uint32_t base = 0, wrap = 0;
|
||||
if (!EromRegAddr(erom, base, wrap)) continue;
|
||||
if (g_brcmf.CoreCount < sizeof(g_brcmf.Cores) / sizeof(g_brcmf.Cores[0])) {
|
||||
g_brcmf.Cores[g_brcmf.CoreCount++] = { id, rev, base, wrap };
|
||||
}
|
||||
}
|
||||
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||
return FindCore(CORE_CHIPCOMMON) && FindCore(CORE_SDIO_DEV) && cr4 && cr4->Wrap != 0;
|
||||
}
|
||||
|
||||
static void CoreDisable(const BrcmfCore* core, uint32_t prereset, uint32_t reset) {
|
||||
if (!(Rd(core->Wrap + WRAP_RESET_CTL) & RESET_CTL_RESET)) {
|
||||
BrcmfBpWrite32(core->Wrap + WRAP_IOCTL, prereset | IOCTL_FGC | IOCTL_CLK);
|
||||
Rd(core->Wrap + WRAP_IOCTL);
|
||||
BrcmfBpWrite32(core->Wrap + WRAP_RESET_CTL, RESET_CTL_RESET);
|
||||
Timekeeping::BusyWaitUs(20);
|
||||
for (int i = 0; i < 30 && Rd(core->Wrap + WRAP_RESET_CTL) != RESET_CTL_RESET; i++)
|
||||
Timekeeping::BusyWaitUs(10);
|
||||
}
|
||||
BrcmfBpWrite32(core->Wrap + WRAP_IOCTL, reset | IOCTL_FGC | IOCTL_CLK);
|
||||
Rd(core->Wrap + WRAP_IOCTL);
|
||||
}
|
||||
|
||||
static void CoreReset(const BrcmfCore* core, uint32_t prereset, uint32_t reset, uint32_t postreset) {
|
||||
CoreDisable(core, prereset, reset);
|
||||
for (int i = 0; i < 50 && (Rd(core->Wrap + WRAP_RESET_CTL) & RESET_CTL_RESET); i++) {
|
||||
BrcmfBpWrite32(core->Wrap + WRAP_RESET_CTL, 0);
|
||||
Timekeeping::BusyWaitUs(50);
|
||||
}
|
||||
BrcmfBpWrite32(core->Wrap + WRAP_IOCTL, postreset | IOCTL_CLK);
|
||||
Rd(core->Wrap + WRAP_IOCTL);
|
||||
}
|
||||
|
||||
// The CR4's TCM: ATCM and BTCM bank counts in CAP, each bank's size in
|
||||
// 8 KiB units in BANKINFO.
|
||||
static uint32_t Cr4RamSize(const BrcmfCore* cr4) {
|
||||
uint32_t cap = Rd(cr4->Base + CR4_CAP);
|
||||
uint32_t banks = (cap & 0xF) + ((cap >> 4) & 0xF);
|
||||
uint32_t size = 0;
|
||||
for (uint32_t i = 0; i < banks; i++) {
|
||||
BrcmfBpWrite32(cr4->Base + CR4_BANKIDX, i);
|
||||
uint32_t info = Rd(cr4->Base + CR4_BANKINFO);
|
||||
size += ((info & 0x3F) + 1) * 8192;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Firmware files
|
||||
// =========================================================================
|
||||
|
||||
uint8_t* BrcmfLoadFile(const char* ext, uint32_t& size, bool required) {
|
||||
char path[80];
|
||||
int n = 0;
|
||||
auto add = [&](const char* s) { while (*s && n < 78) path[n++] = *s++; };
|
||||
add("0:/os/firmware/brcm/brcmfmac");
|
||||
add(g_brcmf.FwName);
|
||||
add("-sdio.");
|
||||
add(ext);
|
||||
path[n] = '\0';
|
||||
|
||||
size = 0;
|
||||
Fs::Vfs::BackendFile file;
|
||||
if (Fs::Vfs::OpenBackendFile(path, file) < 0) {
|
||||
if (required) KernelLogStream(ERROR, "WiFi") << "Firmware file not found: " << path;
|
||||
return nullptr;
|
||||
}
|
||||
uint64_t fileSize = Fs::Vfs::GetBackendFileSize(file);
|
||||
if (fileSize == 0 || fileSize > 4 * 1024 * 1024) {
|
||||
Fs::Vfs::CloseBackendFile(file);
|
||||
KernelLogStream(ERROR, "WiFi") << "Unusable firmware file " << path;
|
||||
return nullptr;
|
||||
}
|
||||
// Rounded up and zero padded: everything goes over the bus in dwords.
|
||||
uint32_t padded = ((uint32_t)fileSize + 3) & ~3u;
|
||||
uint8_t* data = (uint8_t*)Memory::g_heap->Request(padded);
|
||||
if (!data) {
|
||||
Fs::Vfs::CloseBackendFile(file);
|
||||
return nullptr;
|
||||
}
|
||||
memset(data, 0, padded);
|
||||
int got = Fs::Vfs::ReadBackendFile(file, data, 0, fileSize);
|
||||
Fs::Vfs::CloseBackendFile(file);
|
||||
if (got != (int)fileSize) {
|
||||
Memory::g_heap->Free(data);
|
||||
KernelLogStream(ERROR, "WiFi") << "Short read of " << path;
|
||||
return nullptr;
|
||||
}
|
||||
size = (uint32_t)fileSize;
|
||||
KernelLogStream(INFO, "WiFi") << "Loaded " << path << " (" << (uint64_t)size << " bytes)";
|
||||
return data;
|
||||
}
|
||||
|
||||
// The NVRAM text ("key=value" lines, '#' comments) becomes the binary
|
||||
// form the firmware reads: NUL-terminated pairs, zero padded to a dword
|
||||
// (at least one extra NUL), then a token word holding the padded length
|
||||
// in dwords and its complement (Linux brcmf_fw_nvram_strip).
|
||||
static uint8_t* BuildNvram(const uint8_t* text, uint32_t len, uint32_t& outLen) {
|
||||
uint8_t* out = (uint8_t*)Memory::g_heap->Request(len + 8);
|
||||
if (!out) return nullptr;
|
||||
uint32_t n = 0;
|
||||
uint32_t i = 0;
|
||||
auto printable = [](uint8_t c) { return c > 0x20 && c < 0x7F && c != '#'; };
|
||||
while (i < len) {
|
||||
while (i < len && (text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n')) i++;
|
||||
if (i >= len) break;
|
||||
if (text[i] == '#') {
|
||||
while (i < len && text[i] != '\n') i++;
|
||||
continue;
|
||||
}
|
||||
uint32_t start = i;
|
||||
bool sawEquals = false;
|
||||
while (i < len && printable(text[i])) {
|
||||
if (text[i] == '=') sawEquals = true;
|
||||
i++;
|
||||
}
|
||||
if (sawEquals && i > start) {
|
||||
memcpy(out + n, text + start, i - start);
|
||||
n += i - start;
|
||||
out[n++] = '\0';
|
||||
}
|
||||
while (i < len && text[i] != '\n') i++;
|
||||
}
|
||||
uint32_t padded = (n + 1 + 3) & ~3u;
|
||||
while (n < padded) out[n++] = 0;
|
||||
uint32_t words = padded / 4;
|
||||
uint32_t token = (~words << 16) | (words & 0xFFFF);
|
||||
memcpy(out + n, &token, 4);
|
||||
outLen = n + 4;
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool DownloadAndVerify(uint32_t addr, const uint8_t* data, uint32_t len) {
|
||||
if (!BrcmfBpWriteMem(addr, data, len)) return false;
|
||||
|
||||
// Read the image back: a marginal SDIO bus shows up here as a clean
|
||||
// error instead of as a firmware that never starts.
|
||||
uint8_t* check = (uint8_t*)Memory::g_heap->Request(4096);
|
||||
if (!check) return true;
|
||||
bool ok = true;
|
||||
for (uint32_t off = 0; off < len && ok; off += 4096) {
|
||||
uint32_t run = len - off < 4096 ? len - off : 4096;
|
||||
if (!BrcmfBpReadMem(addr + off, check, run) || memcmp(check, data + off, run) != 0) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Firmware verify failed at offset 0x"
|
||||
<< base::hex << (uint64_t)off << base::dec;
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
Memory::g_heap->Free(check);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Bring-up
|
||||
// =========================================================================
|
||||
|
||||
static bool WaitClock(uint8_t bit, uint64_t timeoutMs) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
for (;;) {
|
||||
uint8_t v = 0;
|
||||
if (BrcmfF1Read8(F1_CHIPCLKCSR, v) && (v & bit)) return true;
|
||||
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||
Timekeeping::BusyWaitUs(200);
|
||||
}
|
||||
}
|
||||
|
||||
void BrcmfChipPowerOff() {
|
||||
RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_WL_ON, false);
|
||||
}
|
||||
|
||||
static bool PowerOnAndEnumerate() {
|
||||
// GPIO 34-39: CLK, CMD, DAT0-3 on ALT3 (the Arasan), pulled up but
|
||||
// for the clock, as Linux's sdio_pins.
|
||||
for (uint32_t pin = 34; pin <= 39; pin++) {
|
||||
RpiGpio::SetFunction(pin, RpiGpio::Function::Alt3);
|
||||
RpiGpio::SetPull(pin, pin == 34 ? RpiGpio::Pull::None : RpiGpio::Pull::Up);
|
||||
}
|
||||
|
||||
if (!RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_WL_ON, false)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Cannot switch the radio's power (WL_ON)";
|
||||
return false;
|
||||
}
|
||||
Timekeeping::BusyWaitMs(20);
|
||||
RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_WL_ON, true);
|
||||
Timekeeping::BusyWaitMs(250);
|
||||
|
||||
// The legacy EMMC clock feeds the Arasan; nothing on the boot path
|
||||
// uses that controller, so make sure the clock is not gated.
|
||||
if (!RpiFirmware::SetClockState(RpiFirmware::CLOCK_EMMC, true))
|
||||
KernelLogStream(WARNING, "WiFi") << "Could not switch the EMMC clock on";
|
||||
uint32_t clockHz = 0;
|
||||
if (!RpiFirmware::GetClockRate(RpiFirmware::CLOCK_EMMC, clockHz)) clockHz = 0;
|
||||
if (!Sdio::HostInit(g_brcmf.ArasanMmio, clockHz)) return false;
|
||||
return Sdio::CardInit();
|
||||
}
|
||||
|
||||
static bool AttachChip() {
|
||||
g_brcmf.Window = 0xFFFFFFFF;
|
||||
|
||||
// ALP first: the backplane runs from it until the firmware has set
|
||||
// up the PLL.
|
||||
uint8_t req = CLK_FORCE_HW_REQ_OFF | CLK_ALP_AVAIL_REQ;
|
||||
uint8_t v = 0;
|
||||
if (!BrcmfF1Write8(F1_CHIPCLKCSR, req) || !BrcmfF1Read8(F1_CHIPCLKCSR, v) ||
|
||||
(v & ~(CLK_ALP_AVAIL | CLK_HT_AVAIL)) != req) {
|
||||
KernelLogStream(ERROR, "WiFi") << "ChipClkCSR: wrote 0x" << base::hex << (uint64_t)req
|
||||
<< ", read 0x" << (uint64_t)v << base::dec;
|
||||
return false;
|
||||
}
|
||||
if (!WaitClock(CLK_ALP_AVAIL, 100)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "ALP clock never became available";
|
||||
return false;
|
||||
}
|
||||
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_FORCE_HW_REQ_OFF | CLK_FORCE_ALP);
|
||||
Timekeeping::BusyWaitUs(65);
|
||||
BrcmfF1Write8(F1_SDIOPULLUP, 0);
|
||||
|
||||
uint32_t chipId = 0;
|
||||
if (!BrcmfBpRead32(SI_ENUM_BASE + CC_CHIPID, chipId)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Backplane read failed";
|
||||
return false;
|
||||
}
|
||||
g_brcmf.ChipId = (uint16_t)(chipId & 0xFFFF);
|
||||
g_brcmf.ChipRev = (uint8_t)((chipId >> 16) & 0xF);
|
||||
uint32_t socType = chipId >> 28;
|
||||
|
||||
KernelLogStream(INFO, "WiFi") << "Broadcom chip " << base::hex << (uint64_t)g_brcmf.ChipId
|
||||
<< base::dec << " rev " << (uint64_t)g_brcmf.ChipRev;
|
||||
|
||||
if (g_brcmf.ChipId != CHIP_4345 || socType != 1) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Unsupported chip (only the BCM4345 family: "
|
||||
"CYW43455 / CYW43456)";
|
||||
return false;
|
||||
}
|
||||
// Linux's table: revision 9 is the CYW43456 (Pi 400), the others
|
||||
// the CYW43455 (Pi 4B).
|
||||
g_brcmf.FwName = g_brcmf.ChipRev == 9 ? "43456" : "43455";
|
||||
|
||||
if (!ScanCores()) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Enumeration ROM scan found no usable cores";
|
||||
return false;
|
||||
}
|
||||
// Linux brcmf_chip_get_pmu: the PMU registers live in a core of their
|
||||
// own only when ChipCommon (rev 35+) says the always-on block exists;
|
||||
// otherwise they are ChipCommon's, at the same offsets.
|
||||
const BrcmfCore* cc = FindCore(CORE_CHIPCOMMON);
|
||||
const BrcmfCore* pmu = FindCore(CORE_PMU);
|
||||
uint32_t capsExt = cc->Rev >= 35 ? Rd(cc->Base + CC_CAPS_EXT) : 0;
|
||||
g_brcmf.PmuBase = (pmu && (capsExt & CAPS_EXT_AOB)) ? pmu->Base : cc->Base;
|
||||
g_brcmf.SdioCoreBase = FindCore(CORE_SDIO_DEV)->Base;
|
||||
|
||||
for (uint32_t i = 0; i < g_brcmf.CoreCount; i++) {
|
||||
const BrcmfCore& c = g_brcmf.Cores[i];
|
||||
KernelLogStream(INFO, "WiFi") << " core 0x" << base::hex << (uint64_t)c.Id << " rev "
|
||||
<< base::dec << (uint64_t)c.Rev << " at 0x" << base::hex << (uint64_t)c.Base
|
||||
<< ", wrapper 0x" << (uint64_t)c.Wrap << base::dec;
|
||||
}
|
||||
KernelLogStream(INFO, "WiFi") << "ChipCommon rev " << (uint64_t)cc->Rev << ", caps_ext 0x"
|
||||
<< base::hex << (uint64_t)capsExt << ", PMU registers at 0x" << (uint64_t)g_brcmf.PmuBase
|
||||
<< base::dec;
|
||||
|
||||
// Passive: ARM halted, D11 held in reset with its PHY clock on.
|
||||
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||
CoreReset(cr4, CR4_IOCTL_CPUHALT, CR4_IOCTL_CPUHALT, CR4_IOCTL_CPUHALT);
|
||||
if (const BrcmfCore* d11 = FindCore(CORE_80211)) {
|
||||
CoreReset(d11, D11_IOCTL_PHYRESET | D11_IOCTL_PHYCLKEN, D11_IOCTL_PHYCLKEN, D11_IOCTL_PHYCLKEN);
|
||||
}
|
||||
uint32_t haltIoctl = Rd(cr4->Wrap + WRAP_IOCTL);
|
||||
uint32_t haltReset = Rd(cr4->Wrap + WRAP_RESET_CTL);
|
||||
if (haltIoctl != (CR4_IOCTL_CPUHALT | IOCTL_CLK) || (haltReset & RESET_CTL_RESET)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "The CR4 did not take the halt (wrapper 0x" << base::hex
|
||||
<< (uint64_t)cr4->Wrap << ": ioctl 0x" << (uint64_t)haltIoctl << " reset 0x"
|
||||
<< (uint64_t)haltReset << base::dec << ", expected 0x21 / 0x0)";
|
||||
return false;
|
||||
}
|
||||
|
||||
g_brcmf.RamBase = RAMBASE_4345;
|
||||
g_brcmf.RamSize = Cr4RamSize(cr4);
|
||||
if (g_brcmf.RamSize == 0 || g_brcmf.RamSize > 4 * 1024 * 1024) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Implausible RAM size " << (uint64_t)g_brcmf.RamSize;
|
||||
return false;
|
||||
}
|
||||
|
||||
// An SDIO card reset also resets the WLAN backplane, and a backplane
|
||||
// reset reloads the PMU state.
|
||||
uint8_t cardCtrl = 0;
|
||||
if (Sdio::Read8(Sdio::FN0, CCCR_BRCM_CARDCTRL, cardCtrl))
|
||||
Sdio::Write8(Sdio::FN0, CCCR_BRCM_CARDCTRL, cardCtrl | CARDCTRL_WLANRESET);
|
||||
BrcmfBpWrite32(g_brcmf.PmuBase + CC_PMUCONTROL,
|
||||
Rd(g_brcmf.PmuBase + CC_PMUCONTROL) | PMU_CTL_RES_RELOAD);
|
||||
|
||||
KernelLogStream(INFO, "WiFi") << "CYW" << g_brcmf.FwName << ": " << (uint64_t)g_brcmf.CoreCount
|
||||
<< " cores, " << (uint64_t)(g_brcmf.RamSize / 1024) << " KiB RAM at 0x" << base::hex
|
||||
<< (uint64_t)g_brcmf.RamBase << base::dec;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool DownloadFirmware() {
|
||||
uint32_t fwLen = 0, nvLen = 0;
|
||||
uint8_t* fw = BrcmfLoadFile("bin", fwLen, true);
|
||||
if (!fw) return false;
|
||||
uint8_t* nvText = BrcmfLoadFile("txt", nvLen, true);
|
||||
if (!nvText) {
|
||||
Memory::g_heap->Free(fw);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t nvramLen = 0;
|
||||
uint8_t* nvram = BuildNvram(nvText, nvLen, nvramLen);
|
||||
Memory::g_heap->Free(nvText);
|
||||
uint32_t fwPadded = (fwLen + 3) & ~3u;
|
||||
|
||||
bool ok = nvram && fwPadded + nvramLen < g_brcmf.RamSize;
|
||||
if (!ok) KernelLogStream(ERROR, "WiFi") << "Firmware and NVRAM do not fit in the radio's RAM";
|
||||
|
||||
uint32_t rstvec = 0;
|
||||
memcpy(&rstvec, fw, 4);
|
||||
|
||||
// Download under a plain ALP request, as Linux does (alp_only):
|
||||
// the probe's FORCE_HW_REQ_OFF | FORCE_ALP must be gone before the
|
||||
// ARM starts, or the firmware's own clock requests -- the HT clock
|
||||
// its PLL setup asks for above all -- are blocked.
|
||||
if (ok) {
|
||||
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_ALP_AVAIL_REQ);
|
||||
if (!WaitClock(CLK_ALP_AVAIL, 100))
|
||||
KernelLogStream(WARNING, "WiFi") << "ALP clock lost on its plain request";
|
||||
}
|
||||
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
if (ok && !DownloadAndVerify(g_brcmf.RamBase, fw, fwPadded)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Firmware download failed";
|
||||
ok = false;
|
||||
}
|
||||
if (ok && !DownloadAndVerify(g_brcmf.RamBase + g_brcmf.RamSize - nvramLen, nvram, nvramLen)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "NVRAM download failed";
|
||||
ok = false;
|
||||
}
|
||||
if (ok) {
|
||||
KernelLogStream(INFO, "WiFi") << "Firmware and NVRAM written in "
|
||||
<< (Timekeeping::GetMilliseconds() - start) << " ms";
|
||||
}
|
||||
|
||||
Memory::g_heap->Free(fw);
|
||||
if (nvram) Memory::g_heap->Free(nvram);
|
||||
if (!ok) return false;
|
||||
|
||||
// Start the ARM: clear the SDIO core's interrupts, put the reset
|
||||
// vector at address 0 and take the CPU out of halt.
|
||||
BrcmfBpWrite32(g_brcmf.SdioCoreBase + SD_INTSTATUS, 0xFFFFFFFF);
|
||||
uint32_t vecBack = 0;
|
||||
bool vecOk = BrcmfBpWriteMem(0, (const uint8_t*)&rstvec, 4)
|
||||
&& BrcmfBpReadMem(0, (uint8_t*)&vecBack, 4) && vecBack == rstvec;
|
||||
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||
CoreReset(cr4, CR4_IOCTL_CPUHALT, 0, 0);
|
||||
if (!vecOk)
|
||||
KernelLogStream(ERROR, "WiFi") << "Reset vector did not stick: address 0 reads 0x"
|
||||
<< base::hex << (uint64_t)vecBack << base::dec;
|
||||
KernelLogStream(INFO, "WiFi") << "Reset vector 0x" << base::hex << (uint64_t)rstvec
|
||||
<< "; CR4 released: ioctl 0x" << (uint64_t)Rd(cr4->Wrap + WRAP_IOCTL) << " reset 0x"
|
||||
<< (uint64_t)Rd(cr4->Wrap + WRAP_RESET_CTL) << base::dec << " (expect 0x1 / 0x0)";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save/restore: the firmware can power its domains down between
|
||||
// frames; the host then has to allow for the HT clock coming back.
|
||||
static bool SrCapable() {
|
||||
BrcmfBpWrite32(g_brcmf.PmuBase + CC_CHIPCONTROL_ADDR, 3);
|
||||
return (Rd(g_brcmf.PmuBase + CC_CHIPCONTROL_DATA) & (1u << 2)) != 0;
|
||||
}
|
||||
|
||||
static bool StartFrames() {
|
||||
// Clear the ALP force, then ask for HT: the running firmware brings
|
||||
// up the PLL.
|
||||
BrcmfF1Write8(F1_CHIPCLKCSR, 0);
|
||||
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_HT_AVAIL_REQ);
|
||||
bool ht = WaitClock(CLK_HT_AVAIL, 20);
|
||||
if (!ht) {
|
||||
// Not yet: a snapshot 20 ms in tells a CPU that never ran from
|
||||
// one that ran and was reset.
|
||||
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||
uint8_t csr = 0;
|
||||
BrcmfF1Read8(F1_CHIPCLKCSR, csr);
|
||||
KernelLogStream(INFO, "WiFi") << "20 ms after release: ChipClkCSR 0x" << base::hex
|
||||
<< (uint64_t)csr << ", CR4 ioctl 0x" << (uint64_t)Rd(cr4->Wrap + WRAP_IOCTL) << " reset 0x"
|
||||
<< (uint64_t)Rd(cr4->Wrap + WRAP_RESET_CTL) << ", RAM top 0x"
|
||||
<< (uint64_t)Rd(g_brcmf.RamBase + g_brcmf.RamSize - 4) << base::dec;
|
||||
ht = WaitClock(CLK_HT_AVAIL, 2000);
|
||||
}
|
||||
if (!ht) {
|
||||
// What the hardware was doing, for the next attempt: the clock
|
||||
// register (or whether function 1 still answers at all), the
|
||||
// CPU's wrapper state, and whether the firmware ever replaced
|
||||
// the NVRAM token in the last word of RAM with its shared area.
|
||||
uint8_t csr = 0;
|
||||
bool csrOk = BrcmfF1Read8(F1_CHIPCLKCSR, csr);
|
||||
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||
uint32_t ioctl = Rd(cr4->Wrap + WRAP_IOCTL);
|
||||
uint32_t reset = Rd(cr4->Wrap + WRAP_RESET_CTL);
|
||||
uint32_t top = Rd(g_brcmf.RamBase + g_brcmf.RamSize - 4);
|
||||
KernelLogStream(ERROR, "WiFi") << "HT clock never became available (firmware did not start)";
|
||||
KernelLogStream(ERROR, "WiFi") << "ChipClkCSR " << (csrOk ? "0x" : "unreadable")
|
||||
<< base::hex << (uint64_t)(csrOk ? csr : 0) << ", CR4 ioctl 0x" << (uint64_t)ioctl
|
||||
<< " reset 0x" << (uint64_t)reset << ", RAM top word 0x" << (uint64_t)top
|
||||
<< base::dec << (((~top >> 16) & 0xFFFF) == (top & 0xFFFF)
|
||||
? " (still the NVRAM token)" : " (written by the firmware)");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t saveClk = 0;
|
||||
BrcmfF1Read8(F1_CHIPCLKCSR, saveClk);
|
||||
BrcmfF1Write8(F1_CHIPCLKCSR, saveClk | CLK_FORCE_HT);
|
||||
|
||||
BrcmfBpWrite32(g_brcmf.SdioCoreBase + SD_TOSBMAILBOXDATA, SDPCM_PROT_VERSION << 16);
|
||||
if (!Sdio::EnableFunction(Sdio::FN2, true, 3000)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Function 2 did not become ready";
|
||||
Sdio::EnableFunction(Sdio::FN2, false, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_brcmf.HostIntMask = HOSTINTMASK;
|
||||
BrcmfBpWrite32(g_brcmf.SdioCoreBase + SD_HOSTINTMASK, HOSTINTMASK);
|
||||
|
||||
BrcmfF1Write8(F1_WATERMARK, F2_WATERMARK_43455);
|
||||
uint8_t devCtl = 0;
|
||||
if (BrcmfF1Read8(F1_DEVICE_CTL, devCtl))
|
||||
BrcmfF1Write8(F1_DEVICE_CTL, devCtl | DEVCTL_F2WM_ENAB);
|
||||
BrcmfF1Write8(F1_MESBUSYCTRL, MES_WATERMARK_43455 | MESBUSY_ENAB);
|
||||
|
||||
g_brcmf.SrEnabled = SrCapable();
|
||||
if (g_brcmf.SrEnabled) {
|
||||
uint8_t wake = 0;
|
||||
if (BrcmfF1Read8(F1_WAKEUPCTRL, wake)) BrcmfF1Write8(F1_WAKEUPCTRL, wake | WAKEUP_HTWAIT);
|
||||
Sdio::Write8(Sdio::FN0, CCCR_BRCM_CARDCAP, CARDCAP_CMD14);
|
||||
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_FORCE_HT);
|
||||
} else {
|
||||
BrcmfF1Write8(F1_CHIPCLKCSR, saveClk);
|
||||
}
|
||||
|
||||
// Master, function 1 and function 2.
|
||||
Sdio::EnableCardInterrupts(0x07);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BrcmfChipBringUp() {
|
||||
g_brcmf.BusLock.Acquire();
|
||||
g_brcmf.SharedAddr = 0;
|
||||
g_brcmf.ConsoleIdx = 0;
|
||||
g_brcmf.RamSize = 0;
|
||||
bool ok = PowerOnAndEnumerate() && AttachChip() && DownloadFirmware() && StartFrames();
|
||||
g_brcmf.BusLock.Release();
|
||||
if (!ok) {
|
||||
// A firmware that started and then trapped has said why on its
|
||||
// console (no-op when it never got that far).
|
||||
BrcmfDumpConsole();
|
||||
BrcmfChipPowerOff();
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Firmware console and trap report
|
||||
// =========================================================================
|
||||
|
||||
static bool InRam(uint32_t addr, uint32_t len) {
|
||||
return addr >= g_brcmf.RamBase && addr + len <= g_brcmf.RamBase + g_brcmf.RamSize
|
||||
&& addr + len >= addr;
|
||||
}
|
||||
|
||||
static bool ReadShared(uint32_t sh[16]) {
|
||||
if (!g_brcmf.SharedAddr) {
|
||||
uint32_t addr = Rd(g_brcmf.RamBase + g_brcmf.RamSize - 4);
|
||||
// Still the NVRAM token: the firmware has not published it.
|
||||
if (((~addr >> 16) & 0xFFFF) == (addr & 0xFFFF) || !InRam(addr, 64)) return false;
|
||||
g_brcmf.SharedAddr = addr;
|
||||
}
|
||||
return BrcmfBpReadMem(g_brcmf.SharedAddr, (uint8_t*)sh, 64);
|
||||
}
|
||||
|
||||
static void EmitConsole(const char* buf, uint32_t size, uint32_t from, uint32_t to) {
|
||||
char line[161];
|
||||
uint32_t n = 0;
|
||||
for (uint32_t i = from; i != to; i = (i + 1) % size) {
|
||||
char c = buf[i];
|
||||
if (c == '\r') continue;
|
||||
if (c == '\n' || n == sizeof(line) - 1) {
|
||||
line[n] = '\0';
|
||||
if (n) KernelLogStream(INFO, "WiFi-FW") << line;
|
||||
n = 0;
|
||||
if (c == '\n') continue;
|
||||
}
|
||||
line[n++] = (c >= 0x20 && c < 0x7F) ? c : '.';
|
||||
}
|
||||
line[n] = '\0';
|
||||
if (n) KernelLogStream(INFO, "WiFi-FW") << line;
|
||||
}
|
||||
|
||||
void BrcmfDumpConsole() {
|
||||
if (g_brcmf.RamSize == 0) return;
|
||||
g_brcmf.BusLock.Acquire();
|
||||
|
||||
// sdpcm_shared: flags, trap_addr, assert_exp_addr, assert_file_addr,
|
||||
// assert_line, console_addr, msgtrace_addr, tag[32], brpt_addr
|
||||
uint32_t sh[16] = {};
|
||||
if (!ReadShared(sh)) {
|
||||
g_brcmf.BusLock.Release();
|
||||
return;
|
||||
}
|
||||
uint32_t flags = sh[0];
|
||||
|
||||
// rte_console: vcons_in, vcons_out, then the log: buf, buf_size,
|
||||
// idx, out_idx.
|
||||
uint32_t log[4] = {};
|
||||
if (InRam(sh[5], 24) && BrcmfBpReadMem(sh[5] + 8, (uint8_t*)log, 16)
|
||||
&& log[1] > 0 && log[1] <= 16384 && InRam(log[0], log[1]) && log[2] < log[1]) {
|
||||
uint32_t size = log[1];
|
||||
char* buf = (char*)Memory::g_heap->Request((size + 3) & ~3u);
|
||||
if (buf && BrcmfBpReadMem(log[0], (uint8_t*)buf, (size + 3) & ~3u)) {
|
||||
if (g_brcmf.ConsoleIdx >= size) g_brcmf.ConsoleIdx = 0;
|
||||
EmitConsole(buf, size, g_brcmf.ConsoleIdx, log[2]);
|
||||
g_brcmf.ConsoleIdx = log[2];
|
||||
}
|
||||
if (buf) Memory::g_heap->Free(buf);
|
||||
}
|
||||
|
||||
if ((flags & 0x0400) && InRam(sh[1], 72)) {
|
||||
// brcmf_trap_info: type, epc, cpsr, spsr, r0-r12, sp, lr, pc
|
||||
uint32_t trap[20] = {};
|
||||
if (BrcmfBpReadMem(sh[1], (uint8_t*)trap, sizeof(trap))) {
|
||||
KernelLogStream(ERROR, "WiFi-FW") << "Firmware trap: type 0x" << base::hex
|
||||
<< (uint64_t)trap[0] << ", epc 0x" << (uint64_t)trap[1] << ", lr 0x"
|
||||
<< (uint64_t)trap[18] << ", pc 0x" << (uint64_t)trap[19] << base::dec;
|
||||
}
|
||||
}
|
||||
if (flags & 0x0200) {
|
||||
char file[64] = {};
|
||||
if (InRam(sh[3], 60)) BrcmfBpReadMem(sh[3], (uint8_t*)file, 60);
|
||||
file[63] = '\0';
|
||||
KernelLogStream(ERROR, "WiFi-FW") << "Firmware assert at " << file << ":"
|
||||
<< (uint64_t)sh[4];
|
||||
}
|
||||
g_brcmf.BusLock.Release();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,902 @@
|
||||
/*
|
||||
* BrcmfConnect.cpp
|
||||
* Broadcom FullMAC over SDIO: firmware preinit (CLM, events), escan,
|
||||
* join, key installation, link state, and the entry points the Wi-Fi
|
||||
* facade calls
|
||||
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
|
||||
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Brcmf.hpp"
|
||||
#include "BrcmfSdio.hpp"
|
||||
#include "Ieee80211.hpp"
|
||||
#include "Wpa.hpp"
|
||||
#include <Api/Syscall.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Drivers/Platform/RpiFirmware.hpp>
|
||||
#include <Libraries/Crypto.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Memory/Heap.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
// Joining follows brcmf_cfg80211_connect with the supplicant on the host:
|
||||
// "wpaie" the RSN element our supplicant built (the firmware puts it in
|
||||
// the association request, and it must match message 2's copy)
|
||||
// "wpa_auth" WPA2-PSK (0x80) or PSK-SHA256 (0x8000), 0 for open
|
||||
// "auth" open system
|
||||
// "wsec" AES (4) for CCMP, 0 for open
|
||||
// "join" SSID, BSSID and the chanspec escan reported
|
||||
// The firmware answers with E_SET_SSID (status 0 = associated) and E_LINK;
|
||||
// the access point then starts the 4-way handshake, whose EAPOL frames come
|
||||
// and go as ordinary Ethernet frames. The keys it produces go to the
|
||||
// firmware with "wsec_key". The radio does AES-CCMP only: GCMP networks are
|
||||
// refused.
|
||||
|
||||
using namespace Kt;
|
||||
using namespace montauk::abi;
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
|
||||
// WLC ioctls
|
||||
static constexpr uint32_t WLC_UP = 2;
|
||||
static constexpr uint32_t WLC_SET_INFRA = 20;
|
||||
static constexpr uint32_t WLC_SET_SSID = 26;
|
||||
static constexpr uint32_t WLC_SCAN = 50;
|
||||
static constexpr uint32_t WLC_DISASSOC = 52;
|
||||
static constexpr uint32_t WLC_SET_PM = 86;
|
||||
static constexpr uint32_t WLC_GET_VALID_CHANNELS = 217;
|
||||
|
||||
// Events
|
||||
static constexpr uint32_t E_SET_SSID = 0;
|
||||
static constexpr uint32_t E_AUTH = 3;
|
||||
static constexpr uint32_t E_DEAUTH = 5;
|
||||
static constexpr uint32_t E_DEAUTH_IND = 6;
|
||||
static constexpr uint32_t E_ASSOC = 7;
|
||||
static constexpr uint32_t E_REASSOC = 9;
|
||||
static constexpr uint32_t E_DISASSOC = 11;
|
||||
static constexpr uint32_t E_DISASSOC_IND = 12;
|
||||
static constexpr uint32_t E_LINK = 16;
|
||||
static constexpr uint32_t E_ROAM = 19;
|
||||
static constexpr uint32_t E_PSK_SUP = 46;
|
||||
static constexpr uint32_t E_ESCAN_RESULT = 69;
|
||||
|
||||
static constexpr uint32_t STATUS_SUCCESS = 0;
|
||||
static constexpr uint32_t STATUS_PARTIAL = 8;
|
||||
static constexpr uint16_t EVENT_FLAG_LINK = 0x01;
|
||||
|
||||
static constexpr uint32_t WPA_AUTH_DISABLED = 0;
|
||||
static constexpr uint32_t WPA2_AUTH_PSK = 0x80;
|
||||
static constexpr uint32_t WPA2_AUTH_PSK_SHA256= 0x8000;
|
||||
static constexpr uint32_t AES_ENABLED = 4;
|
||||
static constexpr uint32_t CRYPTO_ALGO_AES_CCM = 4;
|
||||
static constexpr uint32_t KEY_FLAG_PRIMARY = 1u << 1;
|
||||
|
||||
|
||||
static constexpr uint64_t JOIN_TIMEOUT_MS = 12000;
|
||||
static constexpr uint64_t SCAN_TIMEOUT_MS = 25000;
|
||||
static constexpr uint64_t POLL_ACTIVE_MS = 10;
|
||||
static constexpr uint64_t POLL_IDLE_MS = 250;
|
||||
|
||||
// The NVRAM files ship one address for every board; the radio's own
|
||||
// OTP normally overrides it.
|
||||
static constexpr uint8_t NVRAM_PLACEHOLDER_MAC[6] = { 0xb8, 0x27, 0xeb, 0x74, 0xf2, 0x6c };
|
||||
|
||||
// =========================================================================
|
||||
// State
|
||||
// =========================================================================
|
||||
|
||||
static volatile uint32_t g_conn = WIFI_CONN_IDLE;
|
||||
static uint8_t g_bssid[6];
|
||||
static char g_ssid[33];
|
||||
static uint8_t g_ssidLen = 0;
|
||||
static uint8_t g_channel = 0;
|
||||
static bool g_secure = false;
|
||||
static bool g_refusedSecurity = false;
|
||||
static bool g_authFailed = false;
|
||||
static bool g_sawEapol = false;
|
||||
static uint64_t g_joinStartMs = 0;
|
||||
static uint64_t g_lastServiceMs = 0;
|
||||
static WpaConfig g_wpaCfg = {};
|
||||
|
||||
// Raised by events inside the pump, consumed by ConnectService().
|
||||
static volatile bool g_evAssoc = false;
|
||||
static volatile bool g_evJoinFailed = false;
|
||||
static volatile bool g_evLinkDown = false;
|
||||
static volatile uint32_t g_evStatus = 0;
|
||||
static volatile uint32_t g_evReason = 0;
|
||||
|
||||
static volatile bool g_scanActive = false;
|
||||
static uint64_t g_scanStartMs = 0;
|
||||
static uint16_t g_syncId = 0;
|
||||
static uint32_t g_scanSeen = 0;
|
||||
|
||||
static volatile bool g_dumpConsole = false;
|
||||
static bool g_failureLogged = false;
|
||||
|
||||
// The chanspec escan reported for each BSS, in the firmware's own
|
||||
// encoding, so the join names the channel exactly.
|
||||
struct BssChan { uint8_t Bssid[6]; uint16_t Chanspec; bool Used; };
|
||||
static BssChan g_chans[64];
|
||||
static kcp::Spinlock g_chanLock;
|
||||
|
||||
struct PendingEapol { uint32_t Len; uint8_t Data[512]; };
|
||||
static constexpr uint32_t EAPOL_DEPTH = 4;
|
||||
static PendingEapol g_eapolQ[EAPOL_DEPTH];
|
||||
static uint32_t g_eapolHead = 0, g_eapolTail = 0;
|
||||
static kcp::Spinlock g_eapolLock;
|
||||
|
||||
static inline uint16_t Le16(const uint8_t* p) { return (uint16_t)(p[0] | p[1] << 8); }
|
||||
static inline uint32_t Le32(const uint8_t* p) {
|
||||
return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
|
||||
}
|
||||
static inline void Put32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); }
|
||||
static inline void Put16(uint8_t* p, uint16_t v) { memcpy(p, &v, 2); }
|
||||
|
||||
static bool Elapsed(uint64_t now, uint64_t since, uint64_t ms) {
|
||||
return now >= since && now - since >= ms;
|
||||
}
|
||||
|
||||
static void PutMac(KernelLogStream& s, const uint8_t* m) {
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
char buf[18];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
buf[i * 3] = hex[m[i] >> 4];
|
||||
buf[i * 3 + 1] = hex[m[i] & 0xF];
|
||||
buf[i * 3 + 2] = i < 5 ? ':' : '\0';
|
||||
}
|
||||
s << buf;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Events (inside the pump: record, never send)
|
||||
// =========================================================================
|
||||
|
||||
static void RememberChanspec(const uint8_t* bssid, uint16_t chanspec) {
|
||||
g_chanLock.Acquire();
|
||||
BssChan* slot = nullptr;
|
||||
for (auto& c : g_chans) {
|
||||
if (c.Used && memcmp(c.Bssid, bssid, 6) == 0) { slot = &c; break; }
|
||||
if (!c.Used && !slot) slot = &c;
|
||||
}
|
||||
if (slot) {
|
||||
memcpy(slot->Bssid, bssid, 6);
|
||||
slot->Chanspec = chanspec;
|
||||
slot->Used = true;
|
||||
}
|
||||
g_chanLock.Release();
|
||||
}
|
||||
|
||||
static uint16_t LookupChanspec(const uint8_t* bssid, uint8_t channel) {
|
||||
uint16_t found = 0;
|
||||
g_chanLock.Acquire();
|
||||
for (auto& c : g_chans) {
|
||||
if (c.Used && memcmp(c.Bssid, bssid, 6) == 0) { found = c.Chanspec; break; }
|
||||
}
|
||||
g_chanLock.Release();
|
||||
if (found) return found;
|
||||
// 802.11ac encoding: channel, 20 MHz, band.
|
||||
return (uint16_t)(channel | 0x1000 | (channel > 14 ? 0xC000 : 0x0000));
|
||||
}
|
||||
|
||||
// brcmf_escan_result_le: buflen, version, sync_id, bss_count, then
|
||||
// brcmf_bss_info_le records (128 bytes fixed, IEs at ie_offset).
|
||||
static void HandleEscanResult(const uint8_t* data, uint32_t len) {
|
||||
if (len < 12) return;
|
||||
uint16_t count = Le16(data + 10);
|
||||
uint32_t off = 12;
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
if (off + 128 > len) break;
|
||||
const uint8_t* bi = data + off;
|
||||
uint32_t biLen = Le32(bi + 4);
|
||||
if (biLen < 128 || biLen > len - off) break;
|
||||
|
||||
const uint8_t* bssid = bi + 8;
|
||||
uint16_t beacon = Le16(bi + 14);
|
||||
uint16_t caps = Le16(bi + 16);
|
||||
uint16_t chanspec = Le16(bi + 72);
|
||||
int16_t rssi = (int16_t)Le16(bi + 78);
|
||||
uint8_t ctlCh = bi[88];
|
||||
uint16_t ieOff = Le16(bi + 116);
|
||||
uint32_t ieLen = Le32(bi + 120);
|
||||
if (ieOff > biLen || ieLen > biLen - ieOff) ieLen = 0;
|
||||
|
||||
uint8_t channel = ctlCh ? ctlCh : (uint8_t)(chanspec & 0xFF);
|
||||
if (rssi < -127) rssi = -127;
|
||||
if (rssi > 0) rssi = 0;
|
||||
RememberChanspec(bssid, chanspec);
|
||||
WifiRxBssInfo(bssid, beacon, caps, ieLen ? bi + ieOff : nullptr, ieLen,
|
||||
channel, (int8_t)rssi);
|
||||
g_scanSeen++;
|
||||
off += biLen;
|
||||
}
|
||||
}
|
||||
|
||||
void BrcmfOnEvent(uint32_t type, uint32_t status, uint32_t reason, uint16_t flags,
|
||||
const uint8_t* addr, const uint8_t* data, uint32_t len) {
|
||||
(void)addr;
|
||||
switch (type) {
|
||||
case E_ESCAN_RESULT:
|
||||
if (status == STATUS_PARTIAL) {
|
||||
HandleEscanResult(data, len);
|
||||
} else if (g_scanActive) {
|
||||
g_scanActive = false;
|
||||
KernelLogStream(INFO, "WiFi") << "Scan " << (status == STATUS_SUCCESS ? "complete" : "ended")
|
||||
<< ": " << (uint64_t)g_scanSeen << " results";
|
||||
}
|
||||
break;
|
||||
|
||||
case E_SET_SSID:
|
||||
if (g_conn != WIFI_CONN_ASSOCIATING) break;
|
||||
if (status == STATUS_SUCCESS) {
|
||||
g_evAssoc = true;
|
||||
} else {
|
||||
g_evStatus = status;
|
||||
g_evJoinFailed = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case E_LINK:
|
||||
if (flags & EVENT_FLAG_LINK) {
|
||||
if (g_conn == WIFI_CONN_ASSOCIATING) g_evAssoc = true;
|
||||
} else if (g_conn >= WIFI_CONN_ASSOCIATED && g_conn != WIFI_CONN_FAILED) {
|
||||
g_evReason = reason;
|
||||
g_evLinkDown = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case E_DEAUTH:
|
||||
case E_DEAUTH_IND:
|
||||
case E_DISASSOC_IND:
|
||||
if (g_conn >= WIFI_CONN_ASSOCIATED && g_conn != WIFI_CONN_FAILED) {
|
||||
g_evReason = reason;
|
||||
g_evLinkDown = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case E_AUTH:
|
||||
if (status != STATUS_SUCCESS && g_conn == WIFI_CONN_ASSOCIATING)
|
||||
KernelLogStream(WARNING, "WiFi") << "Authentication rejected (status "
|
||||
<< (uint64_t)status << ", reason " << (uint64_t)reason << ")";
|
||||
break;
|
||||
|
||||
case E_ASSOC:
|
||||
case E_REASSOC:
|
||||
if (status != STATUS_SUCCESS && g_conn == WIFI_CONN_ASSOCIATING)
|
||||
KernelLogStream(WARNING, "WiFi") << "Association rejected (status "
|
||||
<< (uint64_t)status << ", reason " << (uint64_t)reason << ")";
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void QueueEapol(const uint8_t* body, uint32_t len) {
|
||||
if (len > sizeof(g_eapolQ[0].Data)) return;
|
||||
g_eapolLock.Acquire();
|
||||
uint32_t next = (g_eapolHead + 1) % EAPOL_DEPTH;
|
||||
if (next == g_eapolTail) g_eapolTail = (g_eapolTail + 1) % EAPOL_DEPTH;
|
||||
memcpy(g_eapolQ[g_eapolHead].Data, body, len);
|
||||
g_eapolQ[g_eapolHead].Len = len;
|
||||
g_eapolHead = next;
|
||||
g_eapolLock.Release();
|
||||
}
|
||||
|
||||
static bool EapolPending() {
|
||||
g_eapolLock.Acquire();
|
||||
bool pending = g_eapolHead != g_eapolTail;
|
||||
g_eapolLock.Release();
|
||||
return pending;
|
||||
}
|
||||
|
||||
void BrcmfOnRxEthernet(const uint8_t* frame, uint32_t len) {
|
||||
uint16_t etherType = (uint16_t)(frame[12] << 8 | frame[13]);
|
||||
if (etherType == ETHERTYPE_EAPOL) {
|
||||
if (g_secure && g_conn >= WIFI_CONN_ASSOCIATING && g_conn != WIFI_CONN_FAILED) {
|
||||
QueueEapol(frame + 14, len - 14);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (BrcmfLinkUp()) WifiRxEthernet(frame, len);
|
||||
}
|
||||
|
||||
void BrcmfOnFirmwareFailure(const char* why) {
|
||||
if (g_brcmf.State != BrcmfState::Running) return;
|
||||
g_brcmf.State = BrcmfState::Error;
|
||||
g_brcmf.FwErrors++;
|
||||
g_scanActive = false;
|
||||
g_dumpConsole = true;
|
||||
if (!g_failureLogged) {
|
||||
g_failureLogged = true;
|
||||
KernelLogStream(ERROR, "WiFi") << "Radio failed: " << why;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Bring-up
|
||||
// =========================================================================
|
||||
|
||||
// CLM blob: the regulatory database the firmware ships without
|
||||
// ("clm_min"). Sent in 1400-byte chunks behind brcmf_dload_data_le:
|
||||
// flag (handler version 1 << 12 | begin 2 | end 4), type (2 = CLM),
|
||||
// length, CRC (unused), data.
|
||||
static bool LoadClm() {
|
||||
uint32_t size = 0;
|
||||
uint8_t* blob = BrcmfLoadFile("clm_blob", size, false);
|
||||
if (!blob) {
|
||||
KernelLogStream(WARNING, "WiFi") << "No CLM blob; the firmware's own regulatory data is used";
|
||||
return true;
|
||||
}
|
||||
|
||||
constexpr uint32_t CHUNK = 1400;
|
||||
constexpr uint32_t HDR = 12;
|
||||
uint8_t* chunk = (uint8_t*)Memory::g_heap->Request(HDR + CHUNK + 4);
|
||||
bool ok = chunk != nullptr;
|
||||
uint32_t off = 0;
|
||||
uint16_t flag = 1u << 12 | 0x0002;
|
||||
while (ok && off < size) {
|
||||
uint32_t n = size - off > CHUNK ? CHUNK : size - off;
|
||||
if (off + n == size) flag |= 0x0004;
|
||||
memset(chunk, 0, HDR + CHUNK + 4);
|
||||
Put16(chunk, flag);
|
||||
Put16(chunk + 2, 2);
|
||||
Put32(chunk + 4, n);
|
||||
Put32(chunk + 8, 0);
|
||||
memcpy(chunk + HDR, blob + off, n);
|
||||
// Linux sends sizeof(struct) + len - 1: three bytes of padding.
|
||||
ok = BrcmfIovarSet("clmload", chunk, HDR + n + 3);
|
||||
flag &= (uint16_t)~0x0002;
|
||||
off += n;
|
||||
}
|
||||
if (chunk) Memory::g_heap->Free(chunk);
|
||||
Memory::g_heap->Free(blob);
|
||||
|
||||
uint32_t status = 0;
|
||||
if (ok && BrcmfIovarGetInt("clmload_status", status, true) && status != 0) {
|
||||
KernelLogStream(ERROR, "WiFi") << "The firmware rejected the CLM blob (status "
|
||||
<< (uint64_t)status << ")";
|
||||
ok = false;
|
||||
}
|
||||
if (ok) KernelLogStream(INFO, "WiFi") << "CLM blob loaded (" << (uint64_t)size << " bytes)";
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void SetEventMask() {
|
||||
uint8_t mask[16] = {};
|
||||
const uint32_t events[] = {
|
||||
E_SET_SSID, E_AUTH, E_DEAUTH, E_DEAUTH_IND, E_ASSOC, E_REASSOC, E_DISASSOC,
|
||||
E_DISASSOC_IND, E_LINK, E_ROAM, E_PSK_SUP, E_ESCAN_RESULT,
|
||||
};
|
||||
for (uint32_t e : events) mask[e / 8] |= (uint8_t)(1u << (e % 8));
|
||||
BrcmfIovarSet("event_msgs", mask, sizeof(mask));
|
||||
}
|
||||
|
||||
// "Version: 7.45.265 (28bca26 CY) CRC: ..." -> "7.45.265"
|
||||
static void ParseVersion(const char* ver) {
|
||||
const char* p = ver;
|
||||
const char* key = "version ";
|
||||
while (*p) {
|
||||
const char* a = p;
|
||||
const char* b = key;
|
||||
while (*a && *b && (*a | 0x20) == *b) { a++; b++; }
|
||||
if (!*b) { p = a; break; }
|
||||
p++;
|
||||
}
|
||||
if (!*p) p = ver;
|
||||
int n = 0;
|
||||
while (p[n] && p[n] != ' ' && p[n] != '\n' && n < 31) {
|
||||
g_brcmf.FwVersion[n] = p[n];
|
||||
n++;
|
||||
}
|
||||
g_brcmf.FwVersion[n] = '\0';
|
||||
}
|
||||
|
||||
static void ReadChannels() {
|
||||
uint32_t list[65] = {};
|
||||
list[0] = 64;
|
||||
g_brcmf.ChannelCount = 0;
|
||||
if (!BrcmfIoctl(WLC_GET_VALID_CHANNELS, false, list, sizeof(list))) return;
|
||||
uint32_t count = list[0] > 64 ? 64 : list[0];
|
||||
bool b24 = false, b5 = false;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
if (list[1 + i] > 14) b5 = true;
|
||||
else if (list[1 + i] > 0) b24 = true;
|
||||
}
|
||||
g_brcmf.ChannelCount = (uint16_t)count;
|
||||
g_brcmf.Band24 = b24;
|
||||
g_brcmf.Band5 = b5;
|
||||
}
|
||||
|
||||
static bool Preinit() {
|
||||
uint8_t mac[6] = {};
|
||||
if (!BrcmfIovarGet("cur_etheraddr", mac, 6)) return false;
|
||||
if (memcmp(mac, NVRAM_PLACEHOLDER_MAC, 6) == 0) {
|
||||
// Raspberry Pi convention: wlan0 is the board (Ethernet) address
|
||||
// plus one.
|
||||
uint8_t board[6];
|
||||
if (Drivers::Platform::RpiFirmware::GetBoardMacAddress(board)) {
|
||||
memcpy(mac, board, 6);
|
||||
for (int i = 5; i >= 3; i--) if (++mac[i] != 0) break;
|
||||
BrcmfIovarSet("cur_etheraddr", mac, 6);
|
||||
}
|
||||
}
|
||||
memcpy(g_brcmf.Mac, mac, 6);
|
||||
|
||||
char ver[256] = {};
|
||||
if (BrcmfIovarGet("ver", ver, sizeof(ver) - 1)) {
|
||||
ver[sizeof(ver) - 1] = '\0';
|
||||
for (char& c : ver) if (c == '\n' || c == '\r') c = ' ';
|
||||
KernelLogStream(INFO, "WiFi") << "Firmware: " << ver;
|
||||
ParseVersion(ver);
|
||||
}
|
||||
|
||||
if (!LoadClm()) return false;
|
||||
|
||||
// No glomming in either direction: one SDPCM frame per transfer.
|
||||
BrcmfIovarSetInt("bus:txglom", 0, true);
|
||||
BrcmfIovarSetInt("bus:rxglom", 0, true);
|
||||
|
||||
BrcmfSetInt(WLC_SET_INFRA, 1);
|
||||
// Roaming would re-key without the host's supplicant; ARP/ND offload
|
||||
// would answer for an address the firmware was never told.
|
||||
BrcmfIovarSetInt("roam_off", 1, true);
|
||||
BrcmfIovarSetInt("arpoe", 0, true);
|
||||
BrcmfIovarSetInt("ndoe", 0, true);
|
||||
// Power save off: the radio misses broadcasts (ARP, DHCP offers)
|
||||
// while dozing, and the Pi is not on a battery.
|
||||
BrcmfSetInt(WLC_SET_PM, 0);
|
||||
|
||||
SetEventMask();
|
||||
if (!BrcmfSetInt(WLC_UP, 1)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "The firmware would not bring the interface up";
|
||||
return false;
|
||||
}
|
||||
ReadChannels();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ResetConnection() {
|
||||
g_conn = WIFI_CONN_IDLE;
|
||||
g_evAssoc = false;
|
||||
g_evJoinFailed = false;
|
||||
g_evLinkDown = false;
|
||||
g_secure = false;
|
||||
g_sawEapol = false;
|
||||
g_eapolLock.Acquire();
|
||||
g_eapolHead = g_eapolTail = 0;
|
||||
g_eapolLock.Release();
|
||||
WpaReset();
|
||||
}
|
||||
|
||||
bool BrcmfProbe(uint64_t arasanMmio) {
|
||||
if (g_brcmf.State != BrcmfState::Absent) return false;
|
||||
g_brcmf.ArasanMmio = arasanMmio;
|
||||
g_brcmf.State = BrcmfState::Detected;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BrcmfCompleteInit() {
|
||||
g_brcmf.State = BrcmfState::Booting;
|
||||
g_failureLogged = false;
|
||||
g_dumpConsole = false;
|
||||
g_scanActive = false;
|
||||
g_brcmf.CmdFailBucket = 0;
|
||||
ResetConnection();
|
||||
BrcmfProtoReset();
|
||||
|
||||
// One retry from power-off: the Bluetooth half of the same chip may
|
||||
// be powering up or taking its patch download at the same moment.
|
||||
bool up = BrcmfChipBringUp();
|
||||
if (!up) {
|
||||
KernelLogStream(WARNING, "WiFi") << "Bring-up failed; power-cycling the radio and trying once more";
|
||||
Timekeeping::BusyWaitMs(100);
|
||||
up = BrcmfChipBringUp();
|
||||
}
|
||||
if (!up) {
|
||||
g_brcmf.State = BrcmfState::Error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Running from here: the preinit below is the first SDPCM traffic.
|
||||
g_brcmf.LastPollMs = Timekeeping::GetMilliseconds();
|
||||
g_brcmf.State = BrcmfState::Running;
|
||||
BrcmfDumpConsole();
|
||||
|
||||
if (!Preinit()) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Firmware initialization failed";
|
||||
BrcmfDumpConsole();
|
||||
g_brcmf.State = BrcmfState::Error;
|
||||
BrcmfChipPowerOff();
|
||||
return false;
|
||||
}
|
||||
BrcmfDumpConsole();
|
||||
|
||||
auto log = KernelLogStream(OK, "WiFi");
|
||||
log << "CYW" << g_brcmf.FwName << " ready: firmware " << g_brcmf.FwVersion << ", "
|
||||
<< (uint64_t)g_brcmf.ChannelCount << " channels, MAC ";
|
||||
PutMac(log, g_brcmf.Mac);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BrcmfShutdown() {
|
||||
g_scanActive = false;
|
||||
ResetConnection();
|
||||
BrcmfChipPowerOff();
|
||||
g_brcmf.State = BrcmfState::Detected;
|
||||
}
|
||||
|
||||
BrcmfState BrcmfGetState() { return g_brcmf.State; }
|
||||
|
||||
// =========================================================================
|
||||
// Scanning
|
||||
// =========================================================================
|
||||
|
||||
bool BrcmfScanActive() { return g_scanActive; }
|
||||
|
||||
bool BrcmfStartScan() {
|
||||
if (g_brcmf.State != BrcmfState::Running) return false;
|
||||
if (g_scanActive) return true;
|
||||
|
||||
// brcmf_escan_params_le: version 1, action 1 (start), sync id, then
|
||||
// brcmf_scan_params_le: SSID (any), BSSID (broadcast), BSS type any,
|
||||
// active scan, firmware defaults for every dwell, all channels.
|
||||
uint8_t p[72] = {};
|
||||
Put32(p, 1);
|
||||
Put16(p + 4, 1);
|
||||
Put16(p + 6, ++g_syncId);
|
||||
memset(p + 44, 0xFF, 6);
|
||||
p[50] = 2;
|
||||
p[51] = 0;
|
||||
Put32(p + 52, 0xFFFFFFFF);
|
||||
Put32(p + 56, 0xFFFFFFFF);
|
||||
Put32(p + 60, 0xFFFFFFFF);
|
||||
Put32(p + 64, 0xFFFFFFFF);
|
||||
Put32(p + 68, 0);
|
||||
|
||||
g_scanSeen = 0;
|
||||
g_scanStartMs = Timekeeping::GetMilliseconds();
|
||||
g_scanActive = true; // results may arrive while the ioctl is pumping
|
||||
if (!BrcmfIovarSet("escan", p, sizeof(p))) {
|
||||
g_scanActive = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BrcmfAbortScan() {
|
||||
if (!g_scanActive) return;
|
||||
// A WLC_SCAN naming channel -1 cancels the scan in progress.
|
||||
uint8_t p[68] = {};
|
||||
memset(p + 36, 0xFF, 6);
|
||||
p[42] = 2;
|
||||
Put32(p + 44, 0xFFFFFFFF);
|
||||
Put32(p + 48, 0xFFFFFFFF);
|
||||
Put32(p + 52, 0xFFFFFFFF);
|
||||
Put32(p + 56, 0xFFFFFFFF);
|
||||
Put32(p + 60, 1);
|
||||
Put16(p + 64, 0xFFFF);
|
||||
BrcmfIoctl(WLC_SCAN, true, p, sizeof(p));
|
||||
g_scanActive = false;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Joining
|
||||
// =========================================================================
|
||||
|
||||
int BrcmfConnectState() { return (int)g_conn; }
|
||||
bool BrcmfConnectRefusedForSecurity() { return g_refusedSecurity; }
|
||||
bool BrcmfJoinFailedOnAuth() { return g_authFailed || WpaGetState() == WpaState::Failed; }
|
||||
const char* BrcmfConnectSsid() { return g_ssid; }
|
||||
const uint8_t* BrcmfConnectBssid() { return g_bssid; }
|
||||
uint8_t BrcmfConnectChannel() { return g_channel; }
|
||||
bool BrcmfLinkUp() { return g_brcmf.State == BrcmfState::Running && g_conn == WIFI_CONN_CONNECTED; }
|
||||
|
||||
static void Disassociate() {
|
||||
uint8_t scb[12] = {};
|
||||
Put32(scb, 3); // deauthenticated: leaving
|
||||
memcpy(scb + 4, g_bssid, 6);
|
||||
BrcmfIoctl(WLC_DISASSOC, true, scb, sizeof(scb));
|
||||
}
|
||||
|
||||
void BrcmfConnectAbort() {
|
||||
if (g_conn != WIFI_CONN_IDLE && g_brcmf.State == BrcmfState::Running) Disassociate();
|
||||
ResetConnection();
|
||||
}
|
||||
|
||||
bool BrcmfConnectStart(const uint8_t* bssid, uint8_t channel, const char* ssid,
|
||||
const char* password, const uint8_t* rsnIe, uint8_t rsnIeLen) {
|
||||
if (g_brcmf.State != BrcmfState::Running) return false;
|
||||
if (g_conn != WIFI_CONN_IDLE) BrcmfConnectAbort();
|
||||
g_refusedSecurity = false;
|
||||
g_authFailed = false;
|
||||
|
||||
memcpy(g_bssid, bssid, 6);
|
||||
g_channel = channel;
|
||||
g_ssidLen = 0;
|
||||
while (ssid && g_ssidLen < 32 && ssid[g_ssidLen]) {
|
||||
g_ssid[g_ssidLen] = ssid[g_ssidLen];
|
||||
g_ssidLen++;
|
||||
}
|
||||
g_ssid[g_ssidLen] = '\0';
|
||||
|
||||
bool secure = rsnIe && rsnIeLen > 0;
|
||||
uint8_t ie[64];
|
||||
uint32_t ieLen = 0;
|
||||
if (secure) {
|
||||
memset(&g_wpaCfg, 0, sizeof(g_wpaCfg));
|
||||
if (!WpaParseApRsn(rsnIe, rsnIeLen, g_wpaCfg)) {
|
||||
g_refusedSecurity = true;
|
||||
return false;
|
||||
}
|
||||
if (g_wpaCfg.PairwiseCipher != RSN_CIPHER_CCMP || g_wpaCfg.GroupCipher != RSN_CIPHER_CCMP) {
|
||||
KernelLogStream(WARNING, "WiFi") << "This radio does AES-CCMP only; the network uses GCMP";
|
||||
g_refusedSecurity = true;
|
||||
return false;
|
||||
}
|
||||
uint32_t pl = 0;
|
||||
if (password) while (pl < sizeof(g_wpaCfg.Passphrase) - 1 && password[pl]) pl++;
|
||||
if (pl == 0) {
|
||||
KernelLogStream(WARNING, "WiFi") << "This network is encrypted and needs a passphrase";
|
||||
g_refusedSecurity = true;
|
||||
return false;
|
||||
}
|
||||
memcpy(g_wpaCfg.Passphrase, password, pl);
|
||||
g_wpaCfg.Passphrase[pl] = '\0';
|
||||
g_wpaCfg.PassLen = (uint8_t)pl;
|
||||
memcpy(g_wpaCfg.OwnMac, g_brcmf.Mac, 6);
|
||||
memcpy(g_wpaCfg.Bssid, g_bssid, 6);
|
||||
memcpy(g_wpaCfg.Ssid, g_ssid, g_ssidLen);
|
||||
g_wpaCfg.SsidLen = g_ssidLen;
|
||||
if (!WpaStart(g_wpaCfg)) {
|
||||
g_refusedSecurity = true;
|
||||
return false;
|
||||
}
|
||||
ieLen = WpaBuildRsnIe(ie, sizeof(ie));
|
||||
if (ieLen == 0) return false;
|
||||
}
|
||||
|
||||
if (g_scanActive) BrcmfAbortScan();
|
||||
|
||||
bool sha256 = secure && g_wpaCfg.Akm == RSN_AKM_PSK_SHA256;
|
||||
if (secure) BrcmfIovarSet("wpaie", ie, ieLen);
|
||||
else BrcmfIovarSet("wpaie", nullptr, 0, true);
|
||||
bool ok = BrcmfIovarSetInt("wpa_auth", secure ? (sha256 ? WPA2_AUTH_PSK_SHA256 : WPA2_AUTH_PSK)
|
||||
: WPA_AUTH_DISABLED)
|
||||
&& BrcmfIovarSetInt("auth", 0)
|
||||
&& BrcmfIovarSetInt("wsec", secure ? AES_ENABLED : 0);
|
||||
BrcmfIovarSetInt("mfp", 0, true);
|
||||
BrcmfIovarSetInt("sup_wpa", 0, true);
|
||||
if (!ok) {
|
||||
WpaReset();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t chanspec = LookupChanspec(g_bssid, channel);
|
||||
|
||||
g_secure = secure;
|
||||
g_evAssoc = false;
|
||||
g_evJoinFailed = false;
|
||||
g_evLinkDown = false;
|
||||
g_joinStartMs = Timekeeping::GetMilliseconds();
|
||||
g_conn = WIFI_CONN_ASSOCIATING; // before "join": its events may land while it pumps
|
||||
|
||||
// brcmf_ext_join_params_le: SSID; join scan (type default, probes
|
||||
// and dwells as Linux uses for a known channel); BSSID, one chanspec.
|
||||
uint8_t jp[72] = {};
|
||||
Put32(jp, g_ssidLen);
|
||||
memcpy(jp + 4, g_ssid, g_ssidLen);
|
||||
jp[36] = 0xFF;
|
||||
Put32(jp + 40, 16);
|
||||
Put32(jp + 44, 320);
|
||||
Put32(jp + 48, 400);
|
||||
Put32(jp + 52, 0xFFFFFFFF);
|
||||
memcpy(jp + 56, g_bssid, 6);
|
||||
Put32(jp + 64, 1);
|
||||
Put16(jp + 68, chanspec);
|
||||
|
||||
if (!BrcmfIovarSet("join", jp, sizeof(jp), true)) {
|
||||
// Older firmware: WLC_SET_SSID with brcmf_join_params_le.
|
||||
uint8_t sp[52] = {};
|
||||
Put32(sp, g_ssidLen);
|
||||
memcpy(sp + 4, g_ssid, g_ssidLen);
|
||||
memcpy(sp + 36, g_bssid, 6);
|
||||
Put32(sp + 44, 1);
|
||||
Put16(sp + 48, chanspec);
|
||||
if (!BrcmfIoctl(WLC_SET_SSID, true, sp, sizeof(sp))) {
|
||||
KernelLogStream(ERROR, "WiFi") << "The firmware refused the join request";
|
||||
ResetConnection();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto log = KernelLogStream(INFO, "WiFi");
|
||||
log << "Joining \"" << g_ssid << "\" (";
|
||||
PutMac(log, g_bssid);
|
||||
log << ", channel " << (uint64_t)channel << ", chanspec 0x" << base::hex << (uint64_t)chanspec
|
||||
<< base::dec << (secure ? ", WPA2-PSK)" : ", open)");
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Keys and EAPOL (the supplicant's hooks)
|
||||
// =========================================================================
|
||||
|
||||
bool BrcmfWpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||
uint8_t frame[14 + 512];
|
||||
if (len > 512) return false;
|
||||
memcpy(frame, g_bssid, 6);
|
||||
memcpy(frame + 6, g_brcmf.Mac, 6);
|
||||
frame[12] = ETHERTYPE_EAPOL >> 8;
|
||||
frame[13] = ETHERTYPE_EAPOL & 0xFF;
|
||||
memcpy(frame + 14, body, len);
|
||||
return BrcmfTxEthernet(frame, 14 + len);
|
||||
}
|
||||
|
||||
// brcmf_wsec_key_le: index, len, data[32], pad[18], algo, flags, pad[3],
|
||||
// iv_initialized, pad, rxiv {hi, lo}, pad[2], ea[6] -- 164 bytes.
|
||||
static bool SendKey(uint32_t index, const uint8_t* key, uint32_t keyLen, uint32_t flags,
|
||||
const uint8_t* ea, const uint8_t* rsc) {
|
||||
if (keyLen > 32) return false;
|
||||
uint8_t k[164] = {};
|
||||
Put32(k, index);
|
||||
Put32(k + 4, keyLen);
|
||||
memcpy(k + 8, key, keyLen);
|
||||
Put32(k + 112, CRYPTO_ALGO_AES_CCM);
|
||||
Put32(k + 116, flags);
|
||||
if (rsc) {
|
||||
Put32(k + 132, 1);
|
||||
Put32(k + 140, (uint32_t)rsc[2] | (uint32_t)rsc[3] << 8 | (uint32_t)rsc[4] << 16
|
||||
| (uint32_t)rsc[5] << 24);
|
||||
Put16(k + 144, (uint16_t)(rsc[0] | rsc[1] << 8));
|
||||
}
|
||||
if (ea) memcpy(k + 156, ea, 6);
|
||||
bool ok = BrcmfIovarSet("wsec_key", k, sizeof(k));
|
||||
Crypto::SecureZero(k, sizeof(k));
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Message 4 is still in the transmit queue when the supplicant installs
|
||||
// the pairwise key; it has to reach the radio first or it goes out
|
||||
// encrypted and the access point never sees it (Linux waits for its
|
||||
// pending 802.1X frames the same way).
|
||||
static void DrainTx() {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (BrcmfTxPending() && Timekeeping::GetMilliseconds() - start < 200) {
|
||||
BrcmfPoll();
|
||||
Timekeeping::BusyWaitUs(200);
|
||||
}
|
||||
Timekeeping::BusyWaitMs(10);
|
||||
}
|
||||
|
||||
bool BrcmfWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||
if (cipher != RSN_CIPHER_CCMP) return false;
|
||||
DrainTx();
|
||||
return SendKey(0, tk, tkLen, 0, g_bssid, nullptr);
|
||||
}
|
||||
|
||||
bool BrcmfWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc) {
|
||||
if (cipher != RSN_CIPHER_CCMP) return false;
|
||||
DrainTx();
|
||||
return SendKey(keyIdx, gtk, gtkLen, KEY_FLAG_PRIMARY, nullptr, rsc);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Service (idle loop / syscall waits: may send ioctls)
|
||||
// =========================================================================
|
||||
|
||||
static void ConnectService() {
|
||||
static volatile bool inService = false;
|
||||
if (__atomic_test_and_set(&inService, __ATOMIC_ACQUIRE)) return;
|
||||
struct Guard { ~Guard() { __atomic_clear(&inService, __ATOMIC_RELEASE); } } guard;
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
g_lastServiceMs = now;
|
||||
|
||||
if (g_scanActive && Elapsed(now, g_scanStartMs, SCAN_TIMEOUT_MS)) {
|
||||
KernelLogStream(WARNING, "WiFi") << "Scan never finished; abandoning it";
|
||||
BrcmfAbortScan();
|
||||
}
|
||||
|
||||
if (g_conn == WIFI_CONN_ASSOCIATING) {
|
||||
if (g_evJoinFailed) {
|
||||
g_evJoinFailed = false;
|
||||
KernelLogStream(WARNING, "WiFi") << "Could not join \"" << g_ssid << "\" (status "
|
||||
<< (uint64_t)g_evStatus << (g_evStatus == 3 ? ": network not found)" : ")");
|
||||
g_conn = WIFI_CONN_FAILED;
|
||||
} else if (g_evAssoc) {
|
||||
g_evAssoc = false;
|
||||
if (g_secure) {
|
||||
KernelLogStream(OK, "WiFi") << "Associated with \"" << g_ssid << "\"; starting the key handshake";
|
||||
g_conn = WIFI_CONN_HANDSHAKING;
|
||||
} else {
|
||||
KernelLogStream(OK, "WiFi") << "Connected to \"" << g_ssid << "\" (open network)";
|
||||
g_conn = WIFI_CONN_CONNECTED;
|
||||
}
|
||||
} else if (Elapsed(Timekeeping::GetMilliseconds(), g_joinStartMs, JOIN_TIMEOUT_MS)) {
|
||||
KernelLogStream(WARNING, "WiFi") << "Timed out joining \"" << g_ssid << "\"";
|
||||
g_conn = WIFI_CONN_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_evLinkDown && g_conn >= WIFI_CONN_ASSOCIATED && g_conn != WIFI_CONN_FAILED) {
|
||||
g_evLinkDown = false;
|
||||
if (g_conn == WIFI_CONN_HANDSHAKING) {
|
||||
// The access point gives up on a handshake whose MICs do not
|
||||
// verify: a wrong passphrase, nearly always.
|
||||
KernelLogStream(WARNING, "WiFi") << "The access point dropped the connection during the key "
|
||||
"handshake (reason " << (uint64_t)g_evReason << "); check the passphrase";
|
||||
g_authFailed = g_sawEapol;
|
||||
g_conn = WIFI_CONN_FAILED;
|
||||
} else {
|
||||
KernelLogStream(WARNING, "WiFi") << "Disconnected from \"" << g_ssid << "\" (reason "
|
||||
<< (uint64_t)g_evReason << ")";
|
||||
ResetConnection();
|
||||
}
|
||||
}
|
||||
|
||||
if (g_conn == WIFI_CONN_HANDSHAKING || g_conn == WIFI_CONN_CONNECTED) {
|
||||
uint8_t body[512];
|
||||
for (;;) {
|
||||
uint32_t len = 0;
|
||||
g_eapolLock.Acquire();
|
||||
if (g_eapolHead != g_eapolTail) {
|
||||
len = g_eapolQ[g_eapolTail].Len;
|
||||
memcpy(body, g_eapolQ[g_eapolTail].Data, len);
|
||||
g_eapolTail = (g_eapolTail + 1) % EAPOL_DEPTH;
|
||||
}
|
||||
g_eapolLock.Release();
|
||||
if (!len) break;
|
||||
g_sawEapol = true;
|
||||
WpaOnEapol(body, len);
|
||||
}
|
||||
}
|
||||
|
||||
if (g_conn == WIFI_CONN_HANDSHAKING) {
|
||||
WpaService(Timekeeping::GetMilliseconds());
|
||||
if (WpaIsComplete()) {
|
||||
KernelLogStream(OK, "WiFi") << "Connected to \"" << g_ssid << "\"";
|
||||
g_conn = WIFI_CONN_CONNECTED;
|
||||
} else if (WpaGetState() == WpaState::Failed) {
|
||||
g_authFailed = true;
|
||||
g_conn = WIFI_CONN_FAILED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BrcmfHasWork(uint64_t now) {
|
||||
if (g_brcmf.State == BrcmfState::Error) return g_dumpConsole;
|
||||
if (g_brcmf.State != BrcmfState::Running) return false;
|
||||
|
||||
bool active = g_scanActive || (g_conn != WIFI_CONN_IDLE && g_conn != WIFI_CONN_FAILED);
|
||||
if (BrcmfPollDue(now, active ? POLL_ACTIVE_MS : POLL_IDLE_MS)) return true;
|
||||
if (g_evAssoc || g_evJoinFailed || g_evLinkDown || EapolPending()) return true;
|
||||
// Deadlines and the supplicant's retransmissions, even with the air quiet.
|
||||
if ((g_conn == WIFI_CONN_ASSOCIATING || g_conn == WIFI_CONN_HANDSHAKING || g_scanActive)
|
||||
&& Elapsed(now, g_lastServiceMs, 50))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void BrcmfService() {
|
||||
if (g_brcmf.State == BrcmfState::Error) {
|
||||
if (g_dumpConsole) {
|
||||
g_dumpConsole = false;
|
||||
BrcmfDumpConsole();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (g_brcmf.State != BrcmfState::Running) return;
|
||||
BrcmfPoll();
|
||||
ConnectService();
|
||||
}
|
||||
|
||||
bool BrcmfSendEthernet(const uint8_t* frame, uint32_t len) {
|
||||
if (!BrcmfLinkUp()) return false;
|
||||
return BrcmfTxEthernet(frame, len);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
/*
|
||||
* BrcmfProto.cpp
|
||||
* Broadcom FullMAC over SDIO: SDPCM framing, CDC ioctls, BDC data frames,
|
||||
* firmware events, the RX pump and the transmit queue
|
||||
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
|
||||
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Brcmf.hpp"
|
||||
#include "BrcmfSdio.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <atomic>
|
||||
|
||||
// Every frame on function 2 starts with the SDPCM header (Linux sdio.c):
|
||||
//
|
||||
// hardware u16 len (the whole frame), u16 ~len
|
||||
// software u8 seq, u8 channel (low nibble; bit 7 = glom descriptor),
|
||||
// u8 next length, u8 data offset (from the frame start),
|
||||
// u8 flow control bitmap, u8 max seq (TX credit, inbound),
|
||||
// u16 reserved
|
||||
//
|
||||
// Channel 0 carries ioctls and their answers behind a 16-byte CDC header
|
||||
// (cmd, len, flags: id [31:16] | interface [15:12] | set [1] | error [0],
|
||||
// status). Channels 1 (events) and 2 (data) carry Ethernet frames behind a
|
||||
// 4-byte BDC header (flags: version 2 in [7:4]; priority; flags2: interface;
|
||||
// data offset in dwords to skip). Events are Ethernet frames of type 0x886C
|
||||
// with Broadcom's OUI.
|
||||
//
|
||||
// Transmit credit: the device's max seq bounds what may be sent, every frame
|
||||
// (ioctls included) takes the next sequence number.
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
|
||||
static constexpr uint32_t SDPCM_HDRLEN = 12;
|
||||
static constexpr uint8_t CHAN_CONTROL = 0;
|
||||
static constexpr uint8_t CHAN_EVENT = 1;
|
||||
static constexpr uint8_t CHAN_DATA = 2;
|
||||
static constexpr uint8_t CHAN_GLOM = 3;
|
||||
static constexpr uint8_t GLOM_DESC = 0x80;
|
||||
|
||||
static constexpr uint32_t CDC_HDRLEN = 16;
|
||||
static constexpr uint32_t CDCF_ERROR = 0x01;
|
||||
static constexpr uint32_t CDCF_SET = 0x02;
|
||||
static constexpr uint32_t BDC_HDRLEN = 4;
|
||||
static constexpr uint8_t BDC_VERSION = 2;
|
||||
|
||||
static constexpr uint16_t ETHERTYPE_BRCM = 0x886C;
|
||||
|
||||
// SDIO device core
|
||||
static constexpr uint32_t SD_INTSTATUS = 0x20;
|
||||
static constexpr uint32_t SD_TOSBMAILBOX = 0x40;
|
||||
static constexpr uint32_t SD_TOHOSTMAILBOXDATA = 0x4C;
|
||||
static constexpr uint32_t I_HMB_FRAME_IND = 1u << 6;
|
||||
static constexpr uint32_t I_HMB_HOST_INT = 1u << 7;
|
||||
static constexpr uint32_t SMB_INT_ACK = 1u << 1;
|
||||
static constexpr uint32_t HMB_DATA_FWHALT = 0x10;
|
||||
|
||||
static constexpr uint32_t F1_FRAMECTRL = 0x1000D;
|
||||
static constexpr uint32_t F1_RFRAMEBCLO = 0x1001B;
|
||||
static constexpr uint32_t F1_RFRAMEBCHI = 0x1001C;
|
||||
static constexpr uint8_t SFC_RF_TERM = 0x01;
|
||||
|
||||
// Function 2's FIFO; the address is ChipCommon's window | 4-byte access.
|
||||
static constexpr uint32_t F2_ADDR = 0x8000;
|
||||
static constexpr uint32_t SI_ENUM_BASE = 0x18000000;
|
||||
|
||||
static constexpr uint32_t RX_MAX = 8192;
|
||||
static constexpr uint32_t CTRL_MAX = 2048; // ioctl payload
|
||||
static constexpr uint32_t TX_SLOT = 2048; // padded data frame
|
||||
static constexpr uint32_t TXQ_DEPTH = 32;
|
||||
static constexpr uint64_t CREDIT_TIMEOUT_MS = 1000;
|
||||
static constexpr uint64_t IOCTL_TIMEOUT_MS = 2000;
|
||||
static constexpr uint32_t FAIL_BUCKET_LIMIT = 3;
|
||||
static constexpr uint32_t BUS_ERROR_LIMIT = 16;
|
||||
|
||||
static uint32_t g_rxBuf[RX_MAX / 4 + 16];
|
||||
static volatile bool g_pumping = false;
|
||||
static uint32_t g_busErrors = 0;
|
||||
static bool g_glomWarned = false;
|
||||
|
||||
struct TxSlot {
|
||||
uint32_t Len; // bytes on the bus (padded)
|
||||
uint32_t Data[TX_SLOT / 4];
|
||||
};
|
||||
static TxSlot g_txq[TXQ_DEPTH];
|
||||
static uint32_t g_txHead = 0, g_txTail = 0;
|
||||
static kcp::Spinlock g_txqLock;
|
||||
|
||||
// The outstanding ioctl, if any. The pump on whichever core finds the
|
||||
// answer copies it here under g_respLock; the caller copies it out.
|
||||
static kcp::Mutex g_ctrlLock;
|
||||
static kcp::Spinlock g_respLock;
|
||||
// Room for the largest ioctl padded to whole blocks.
|
||||
static uint32_t g_ctrlTx[(SDPCM_HDRLEN + CDC_HDRLEN + CTRL_MAX + Sdio::FN2_BLOCK - 1)
|
||||
/ Sdio::FN2_BLOCK * Sdio::FN2_BLOCK / 4];
|
||||
static uint8_t g_ctrlResp[CTRL_MAX];
|
||||
static uint32_t g_ctrlRespLen = 0;
|
||||
static uint16_t g_reqId = 0;
|
||||
static uint16_t g_waitId = 0; // 0 = nobody waiting
|
||||
static int32_t g_ctrlStatus = 0;
|
||||
static uint32_t g_ctrlFlags = 0;
|
||||
static std::atomic<bool> g_ctrlDone{false};
|
||||
|
||||
static inline uint16_t Be16(const uint8_t* p) { return (uint16_t)(p[0] << 8 | p[1]); }
|
||||
static inline uint32_t Be32(const uint8_t* p) {
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
|
||||
}
|
||||
static inline void Le32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); }
|
||||
|
||||
void BrcmfProtoReset() {
|
||||
g_brcmf.TxSeq = 0;
|
||||
g_brcmf.TxMax = 1;
|
||||
g_brcmf.RxPending = false;
|
||||
g_txqLock.Acquire();
|
||||
g_txHead = g_txTail = 0;
|
||||
g_txqLock.Release();
|
||||
g_respLock.Acquire();
|
||||
g_waitId = 0;
|
||||
g_respLock.Release();
|
||||
g_busErrors = 0;
|
||||
g_glomWarned = false;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Bus transfers (bus lock held)
|
||||
// =========================================================================
|
||||
|
||||
static bool CanSend() {
|
||||
uint8_t room = (uint8_t)(g_brcmf.TxMax - g_brcmf.TxSeq);
|
||||
return room != 0 && (room & 0x80) == 0;
|
||||
}
|
||||
|
||||
static void NoteBusError() {
|
||||
if (++g_busErrors == BUS_ERROR_LIMIT) BrcmfOnFirmwareFailure("SDIO bus errors");
|
||||
}
|
||||
|
||||
static bool SendFrameLocked(uint8_t* frame, uint32_t busLen) {
|
||||
frame[4] = g_brcmf.TxSeq;
|
||||
if (!BrcmfBpSelectWindow(SI_ENUM_BASE) ||
|
||||
!Sdio::WriteBlock(Sdio::FN2, F2_ADDR, false, frame, busLen)) {
|
||||
NoteBusError();
|
||||
return false;
|
||||
}
|
||||
g_busErrors = 0;
|
||||
g_brcmf.TxSeq++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bytes on the bus for a frame: dwords, and whole blocks once it is
|
||||
// longer than one (the device takes the length from the header).
|
||||
static uint32_t BusLength(uint32_t len) {
|
||||
if (len > Sdio::FN2_BLOCK)
|
||||
return (len + Sdio::FN2_BLOCK - 1) / Sdio::FN2_BLOCK * Sdio::FN2_BLOCK;
|
||||
return (len + 3) & ~3u;
|
||||
}
|
||||
|
||||
static void PutSdpcm(uint8_t* f, uint32_t len, uint8_t channel) {
|
||||
uint16_t l = (uint16_t)len;
|
||||
f[0] = (uint8_t)l; f[1] = (uint8_t)(l >> 8);
|
||||
f[2] = (uint8_t)~l; f[3] = (uint8_t)(~l >> 8);
|
||||
f[4] = 0; // seq, filled in at send time
|
||||
f[5] = channel;
|
||||
f[6] = 0;
|
||||
f[7] = SDPCM_HDRLEN;
|
||||
f[8] = f[9] = f[10] = f[11] = 0;
|
||||
}
|
||||
|
||||
// Abandon a frame half read: abort function 2, have the device drop the
|
||||
// rest of it, and wait for its read FIFO to empty (Linux rxfail).
|
||||
static void RxFailLocked() {
|
||||
Sdio::Abort(Sdio::FN2);
|
||||
BrcmfF1Write8(F1_FRAMECTRL, SFC_RF_TERM);
|
||||
for (int i = 0; i < 100; i++) {
|
||||
uint8_t hi = 0, lo = 0;
|
||||
if (!BrcmfF1Read8(F1_RFRAMEBCHI, hi) || !BrcmfF1Read8(F1_RFRAMEBCLO, lo)) break;
|
||||
if (hi == 0 && lo == 0) break;
|
||||
}
|
||||
NoteBusError();
|
||||
}
|
||||
|
||||
// Read one frame into g_rxBuf. 1 = a frame, 0 = none waiting, -1 = error.
|
||||
static int ReadFrameLocked(uint32_t& len) {
|
||||
uint8_t* buf = (uint8_t*)g_rxBuf;
|
||||
if (!BrcmfBpSelectWindow(SI_ENUM_BASE) ||
|
||||
!Sdio::ReadBlock(Sdio::FN2, F2_ADDR, false, buf, 4)) {
|
||||
RxFailLocked();
|
||||
return -1;
|
||||
}
|
||||
uint16_t l = (uint16_t)(buf[0] | buf[1] << 8);
|
||||
uint16_t check = (uint16_t)(buf[2] | buf[3] << 8);
|
||||
if ((l | check) == 0) return 0;
|
||||
if ((uint16_t)(l ^ check) != 0xFFFF || l < SDPCM_HDRLEN || l > RX_MAX) {
|
||||
KernelLogStream(WARNING, "WiFi") << "Bad SDPCM header (len 0x" << base::hex
|
||||
<< (uint64_t)l << ", check 0x" << (uint64_t)check << base::dec << ")";
|
||||
RxFailLocked();
|
||||
return -1;
|
||||
}
|
||||
uint32_t rest = ((l + 3u) & ~3u) - 4;
|
||||
if (rest && !Sdio::ReadBlock(Sdio::FN2, F2_ADDR, false, buf + 4, rest)) {
|
||||
RxFailLocked();
|
||||
return -1;
|
||||
}
|
||||
g_busErrors = 0;
|
||||
len = l;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void FlushTxLocked() {
|
||||
for (;;) {
|
||||
g_txqLock.Acquire();
|
||||
bool empty = g_txHead == g_txTail;
|
||||
g_txqLock.Release();
|
||||
if (empty || !CanSend()) return;
|
||||
|
||||
TxSlot& slot = g_txq[g_txTail % TXQ_DEPTH];
|
||||
if (SendFrameLocked((uint8_t*)slot.Data, slot.Len)) g_brcmf.TxPackets++;
|
||||
|
||||
g_txqLock.Acquire();
|
||||
g_txTail++;
|
||||
g_txqLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Receive dispatch (bus lock released)
|
||||
// =========================================================================
|
||||
|
||||
static void HandleControl(const uint8_t* p, uint32_t len) {
|
||||
if (len < CDC_HDRLEN) return;
|
||||
uint32_t cmd, dlen, flags, status;
|
||||
memcpy(&cmd, p, 4);
|
||||
memcpy(&dlen, p + 4, 4);
|
||||
memcpy(&flags, p + 8, 4);
|
||||
memcpy(&status, p + 12, 4);
|
||||
(void)cmd;
|
||||
(void)dlen;
|
||||
uint16_t id = (uint16_t)(flags >> 16);
|
||||
|
||||
g_respLock.Acquire();
|
||||
if (g_waitId != 0 && id == g_waitId) {
|
||||
uint32_t n = len - CDC_HDRLEN;
|
||||
if (n > CTRL_MAX) n = CTRL_MAX;
|
||||
memcpy(g_ctrlResp, p + CDC_HDRLEN, n);
|
||||
g_ctrlRespLen = n;
|
||||
g_ctrlStatus = (int32_t)status;
|
||||
g_ctrlFlags = flags;
|
||||
g_waitId = 0;
|
||||
g_ctrlDone.store(true, std::memory_order_release);
|
||||
}
|
||||
g_respLock.Release();
|
||||
}
|
||||
|
||||
static void HandleEvent(const uint8_t* eth, uint32_t len) {
|
||||
// Ethernet (14), bcm_ethhdr (10): subtype 0x8001, length, version 1,
|
||||
// OUI 00:10:18, user subtype 1; then the 48-byte event message.
|
||||
constexpr uint32_t MSG = 24;
|
||||
constexpr uint32_t DATA = MSG + 48;
|
||||
if (len < DATA) return;
|
||||
const uint8_t* h = eth + 14;
|
||||
if (Be16(h) != 0x8001 || h[5] != 0x00 || h[6] != 0x10 || h[7] != 0x18 || Be16(h + 8) != 1)
|
||||
return;
|
||||
|
||||
const uint8_t* m = eth + MSG;
|
||||
uint16_t flags = Be16(m + 2);
|
||||
uint32_t type = Be32(m + 4);
|
||||
uint32_t status = Be32(m + 8);
|
||||
uint32_t reason = Be32(m + 12);
|
||||
uint32_t dataLen = Be32(m + 20);
|
||||
const uint8_t* addr = m + 24;
|
||||
uint8_t ifidx = m[46];
|
||||
if (ifidx != 0) return;
|
||||
if (dataLen > len - DATA) dataLen = len - DATA;
|
||||
BrcmfOnEvent(type, status, reason, flags, addr, eth + DATA, dataLen);
|
||||
}
|
||||
|
||||
static void Dispatch(const uint8_t* f, uint32_t len) {
|
||||
const uint8_t* sw = f + 4;
|
||||
uint8_t channel = sw[1] & 0x0F;
|
||||
uint8_t offset = sw[3];
|
||||
|
||||
// Credit: the device's max seq, sanity checked as Linux does.
|
||||
uint8_t max = sw[5];
|
||||
if ((uint8_t)(max - g_brcmf.TxSeq) > 0x40) max = (uint8_t)(g_brcmf.TxSeq + 2);
|
||||
g_brcmf.TxMax = max;
|
||||
|
||||
if ((sw[1] & GLOM_DESC) || channel == CHAN_GLOM) {
|
||||
if (!g_glomWarned) {
|
||||
g_glomWarned = true;
|
||||
KernelLogStream(WARNING, "WiFi") << "Dropping a glommed superframe (rx glom is off)";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (offset < SDPCM_HDRLEN || offset > len) return;
|
||||
const uint8_t* p = f + offset;
|
||||
uint32_t plen = len - offset;
|
||||
if (plen == 0) return; // credit update only
|
||||
|
||||
if (channel == CHAN_CONTROL) {
|
||||
HandleControl(p, plen);
|
||||
return;
|
||||
}
|
||||
if (channel != CHAN_EVENT && channel != CHAN_DATA) return;
|
||||
|
||||
if (plen < BDC_HDRLEN || (p[0] >> 4) != BDC_VERSION) return;
|
||||
if ((p[2] & 0x0F) != 0) return; // another interface
|
||||
uint32_t skip = BDC_HDRLEN + (uint32_t)p[3] * 4;
|
||||
if (skip + 14 > plen) return;
|
||||
const uint8_t* eth = p + skip;
|
||||
uint32_t elen = plen - skip;
|
||||
|
||||
if (Be16(eth + 12) == ETHERTYPE_BRCM) {
|
||||
HandleEvent(eth, elen);
|
||||
return;
|
||||
}
|
||||
if (channel == CHAN_DATA) {
|
||||
g_brcmf.RxPackets++;
|
||||
BrcmfOnRxEthernet(eth, elen);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// The pump
|
||||
// =========================================================================
|
||||
|
||||
bool BrcmfTxPending() {
|
||||
g_txqLock.Acquire();
|
||||
bool pending = g_txHead != g_txTail;
|
||||
g_txqLock.Release();
|
||||
return pending;
|
||||
}
|
||||
|
||||
bool BrcmfPollDue(uint64_t now, uint64_t intervalMs) {
|
||||
if (g_brcmf.State != BrcmfState::Running) return false;
|
||||
if (g_brcmf.RxPending || BrcmfTxPending()) return true;
|
||||
if (Sdio::CardInterruptPending()) return true;
|
||||
uint64_t last = g_brcmf.LastPollMs;
|
||||
return now < last || now - last >= intervalMs;
|
||||
}
|
||||
|
||||
void BrcmfPoll() {
|
||||
if (g_brcmf.State != BrcmfState::Running) return;
|
||||
if (__atomic_test_and_set(&g_pumping, __ATOMIC_ACQUIRE)) return;
|
||||
struct Guard { ~Guard() { __atomic_clear(&g_pumping, __ATOMIC_RELEASE); } } guard;
|
||||
|
||||
const uint32_t sdio = g_brcmf.SdioCoreBase;
|
||||
for (int round = 0; round < 8 && g_brcmf.State == BrcmfState::Running; round++) {
|
||||
bool frames = g_brcmf.RxPending;
|
||||
uint32_t intStatus = 0;
|
||||
bool halted = false;
|
||||
|
||||
g_brcmf.BusLock.Acquire();
|
||||
if (BrcmfBpRead32(sdio + SD_INTSTATUS, intStatus)) {
|
||||
intStatus &= g_brcmf.HostIntMask;
|
||||
if (intStatus) BrcmfBpWrite32(sdio + SD_INTSTATUS, intStatus);
|
||||
if (intStatus & I_HMB_HOST_INT) {
|
||||
uint32_t mbox = 0;
|
||||
BrcmfBpRead32(sdio + SD_TOHOSTMAILBOXDATA, mbox);
|
||||
BrcmfBpWrite32(sdio + SD_TOSBMAILBOX, SMB_INT_ACK);
|
||||
halted = (mbox & HMB_DATA_FWHALT) != 0;
|
||||
}
|
||||
if (intStatus & I_HMB_FRAME_IND) frames = true;
|
||||
} else {
|
||||
NoteBusError();
|
||||
}
|
||||
g_brcmf.BusLock.Release();
|
||||
g_brcmf.LastPollMs = Timekeeping::GetMilliseconds();
|
||||
|
||||
if (halted) {
|
||||
BrcmfOnFirmwareFailure("firmware halted");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int n = 0; frames && n < 32; n++) {
|
||||
uint32_t len = 0;
|
||||
g_brcmf.BusLock.Acquire();
|
||||
int r = ReadFrameLocked(len);
|
||||
g_brcmf.BusLock.Release();
|
||||
if (r <= 0) {
|
||||
frames = false;
|
||||
break;
|
||||
}
|
||||
Dispatch((const uint8_t*)g_rxBuf, len);
|
||||
}
|
||||
g_brcmf.RxPending = frames;
|
||||
|
||||
if (BrcmfTxPending()) {
|
||||
g_brcmf.BusLock.Acquire();
|
||||
FlushTxLocked();
|
||||
g_brcmf.BusLock.Release();
|
||||
}
|
||||
|
||||
if (!frames && intStatus == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Transmit
|
||||
// =========================================================================
|
||||
|
||||
bool BrcmfTxEthernet(const uint8_t* frame, uint32_t len) {
|
||||
if (g_brcmf.State != BrcmfState::Running || len < 14 || len > 1518) return false;
|
||||
uint32_t total = SDPCM_HDRLEN + BDC_HDRLEN + len;
|
||||
uint32_t busLen = BusLength(total);
|
||||
|
||||
g_txqLock.Acquire();
|
||||
if (g_txHead - g_txTail >= TXQ_DEPTH) {
|
||||
g_txqLock.Release();
|
||||
return false;
|
||||
}
|
||||
TxSlot& slot = g_txq[g_txHead % TXQ_DEPTH];
|
||||
uint8_t* f = (uint8_t*)slot.Data;
|
||||
PutSdpcm(f, total, CHAN_DATA);
|
||||
f[12] = BDC_VERSION << 4;
|
||||
f[13] = 0; // priority: best effort
|
||||
f[14] = 0; // interface 0
|
||||
f[15] = 0; // no signalling TLVs
|
||||
memcpy(f + 16, frame, len);
|
||||
memset(f + total, 0, busLen - total);
|
||||
slot.Len = busLen;
|
||||
g_txHead++;
|
||||
g_txqLock.Release();
|
||||
|
||||
// Never wait for the bus here: the caller may be the network stack
|
||||
// inside a frame the pump is delivering, or a process that just
|
||||
// preempted the bus lock's holder. Whoever holds it, or the next
|
||||
// pump, sends what is queued.
|
||||
if (g_brcmf.BusLock.TryAcquire()) {
|
||||
FlushTxLocked();
|
||||
g_brcmf.BusLock.Release();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Control
|
||||
// =========================================================================
|
||||
|
||||
bool BrcmfIoctl(uint32_t cmd, bool set, void* buf, uint32_t len, int32_t* fwStatus) {
|
||||
if (fwStatus) *fwStatus = 0;
|
||||
if (g_brcmf.State != BrcmfState::Running || len > CTRL_MAX) return false;
|
||||
|
||||
g_ctrlLock.Acquire();
|
||||
|
||||
uint16_t id = ++g_reqId;
|
||||
if (id == 0) id = ++g_reqId;
|
||||
uint8_t* f = (uint8_t*)g_ctrlTx;
|
||||
uint32_t total = SDPCM_HDRLEN + CDC_HDRLEN + len;
|
||||
PutSdpcm(f, total, CHAN_CONTROL);
|
||||
uint8_t* cdc = f + SDPCM_HDRLEN;
|
||||
Le32(cdc, cmd);
|
||||
Le32(cdc + 4, len);
|
||||
Le32(cdc + 8, (uint32_t)id << 16 | (set ? CDCF_SET : 0));
|
||||
Le32(cdc + 12, 0);
|
||||
if (len) memcpy(cdc + CDC_HDRLEN, buf, len);
|
||||
uint32_t busLen = BusLength(total);
|
||||
memset(f + total, 0, busLen - total);
|
||||
|
||||
g_respLock.Acquire();
|
||||
g_waitId = id;
|
||||
g_ctrlDone.store(false, std::memory_order_relaxed);
|
||||
g_respLock.Release();
|
||||
|
||||
// Wait for transmit credit, pumping meanwhile: credit only ever
|
||||
// arrives in the header of a received frame.
|
||||
bool sent = false;
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (!sent && g_brcmf.State == BrcmfState::Running) {
|
||||
g_brcmf.BusLock.Acquire();
|
||||
if (CanSend()) {
|
||||
sent = SendFrameLocked(f, busLen);
|
||||
if (!sent) {
|
||||
g_brcmf.BusLock.Release();
|
||||
break;
|
||||
}
|
||||
}
|
||||
g_brcmf.BusLock.Release();
|
||||
if (sent) break;
|
||||
if (Timekeeping::GetMilliseconds() - start > CREDIT_TIMEOUT_MS) break;
|
||||
BrcmfPoll();
|
||||
Timekeeping::BusyWaitUs(100);
|
||||
}
|
||||
|
||||
bool done = false;
|
||||
if (sent) {
|
||||
start = Timekeeping::GetMilliseconds();
|
||||
while (g_brcmf.State == BrcmfState::Running) {
|
||||
if (g_ctrlDone.load(std::memory_order_acquire)) { done = true; break; }
|
||||
if (Timekeeping::GetMilliseconds() - start > IOCTL_TIMEOUT_MS) break;
|
||||
BrcmfPoll();
|
||||
if (g_ctrlDone.load(std::memory_order_acquire)) { done = true; break; }
|
||||
Timekeeping::BusyWaitUs(50);
|
||||
}
|
||||
}
|
||||
|
||||
g_respLock.Acquire();
|
||||
g_waitId = 0;
|
||||
bool ok = false;
|
||||
if (done) {
|
||||
int32_t status = g_ctrlStatus;
|
||||
if (fwStatus) *fwStatus = status;
|
||||
ok = !(g_ctrlFlags & CDCF_ERROR) && status >= 0;
|
||||
if (!set && len) {
|
||||
uint32_t n = g_ctrlRespLen < len ? g_ctrlRespLen : len;
|
||||
memcpy(buf, g_ctrlResp, n);
|
||||
}
|
||||
}
|
||||
g_respLock.Release();
|
||||
|
||||
if (done) {
|
||||
if (g_brcmf.CmdFailBucket) g_brcmf.CmdFailBucket--;
|
||||
} else if (g_brcmf.State == BrcmfState::Running) {
|
||||
KernelLogStream(WARNING, "WiFi") << "Firmware command " << (uint64_t)cmd
|
||||
<< (sent ? " went unanswered" : " could not be sent (no transmit credit)");
|
||||
if (++g_brcmf.CmdFailBucket >= FAIL_BUCKET_LIMIT)
|
||||
BrcmfOnFirmwareFailure("firmware stopped answering commands");
|
||||
}
|
||||
|
||||
g_ctrlLock.Release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
static constexpr uint32_t WLC_GET_VAR = 262;
|
||||
static constexpr uint32_t WLC_SET_VAR = 263;
|
||||
|
||||
static kcp::Mutex g_iovarLock;
|
||||
static uint8_t g_iovarBuf[CTRL_MAX];
|
||||
|
||||
static uint32_t NameLen(const char* name) {
|
||||
uint32_t n = 0;
|
||||
while (name[n]) n++;
|
||||
return n + 1;
|
||||
}
|
||||
|
||||
bool BrcmfIovarSet(const char* name, const void* data, uint32_t len, bool quiet) {
|
||||
uint32_t nameLen = NameLen(name);
|
||||
if (nameLen + len > CTRL_MAX) return false;
|
||||
g_iovarLock.Acquire();
|
||||
memcpy(g_iovarBuf, name, nameLen);
|
||||
if (len) memcpy(g_iovarBuf + nameLen, data, len);
|
||||
int32_t status = 0;
|
||||
bool ok = BrcmfIoctl(WLC_SET_VAR, true, g_iovarBuf, nameLen + len, &status);
|
||||
g_iovarLock.Release();
|
||||
if (!ok && !quiet)
|
||||
KernelLogStream(WARNING, "WiFi") << "Setting \"" << name << "\" failed (status "
|
||||
<< (int)status << ")";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool BrcmfIovarGet(const char* name, void* out, uint32_t len, bool quiet) {
|
||||
uint32_t nameLen = NameLen(name);
|
||||
uint32_t bufLen = nameLen > len ? nameLen : len;
|
||||
if (bufLen > CTRL_MAX) return false;
|
||||
g_iovarLock.Acquire();
|
||||
memset(g_iovarBuf, 0, bufLen);
|
||||
memcpy(g_iovarBuf, name, nameLen);
|
||||
int32_t status = 0;
|
||||
bool ok = BrcmfIoctl(WLC_GET_VAR, false, g_iovarBuf, bufLen, &status);
|
||||
if (ok) memcpy(out, g_iovarBuf, len);
|
||||
g_iovarLock.Release();
|
||||
if (!ok && !quiet)
|
||||
KernelLogStream(WARNING, "WiFi") << "Reading \"" << name << "\" failed (status "
|
||||
<< (int)status << ")";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool BrcmfIovarSetInt(const char* name, uint32_t value, bool quiet) {
|
||||
return BrcmfIovarSet(name, &value, 4, quiet);
|
||||
}
|
||||
|
||||
bool BrcmfIovarGetInt(const char* name, uint32_t& value, bool quiet) {
|
||||
return BrcmfIovarGet(name, &value, 4, quiet);
|
||||
}
|
||||
|
||||
bool BrcmfSetInt(uint32_t cmd, uint32_t value) {
|
||||
return BrcmfIoctl(cmd, true, &value, 4);
|
||||
}
|
||||
|
||||
bool BrcmfGetInt(uint32_t cmd, uint32_t& value) {
|
||||
uint32_t v = 0;
|
||||
if (!BrcmfIoctl(cmd, false, &v, 4)) return false;
|
||||
value = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
/*
|
||||
* BrcmfSdio.cpp
|
||||
* SDIO host on the Raspberry Pi 4/400's Arasan SD controller
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "BrcmfSdio.hpp"
|
||||
#include <Arch/Cpu.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>
|
||||
|
||||
// Register access follows Drivers/Storage/Sdhci: the Broadcom/Arasan
|
||||
// controller only takes 32-bit accesses, so 8/16-bit fields are written by
|
||||
// read-modify-write of their dword and the transfer-mode/command pair in
|
||||
// the one write that issues the command. Linux (sdhci-iproc, bcm2835 data)
|
||||
// also waits four SD clocks after every write while the bus runs at 400 kHz
|
||||
// or below; so does this.
|
||||
//
|
||||
// SDIO commands (SDIO Simplified Specification 3.00):
|
||||
// CMD5 IO_SEND_OP_COND, R4: OCR [23:0], ready [31]
|
||||
// CMD52 IO_RW_DIRECT, R5: flags [15:8], data [7:0]
|
||||
// arg: R/W [31], function [30:28], RAW [27], address [25:9], data [7:0]
|
||||
// CMD53 IO_RW_EXTENDED, R5
|
||||
// arg: R/W [31], function [30:28], block mode [27], incrementing
|
||||
// address [26], address [25:9], count [8:0] (0 = 512)
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Net::Wifi::Sdio {
|
||||
|
||||
static constexpr uint32_t REG_BLOCK_SIZE_COUNT = 0x04;
|
||||
static constexpr uint32_t REG_ARGUMENT = 0x08;
|
||||
static constexpr uint32_t REG_XFER_MODE_CMD = 0x0C;
|
||||
static constexpr uint32_t REG_RESPONSE = 0x10;
|
||||
static constexpr uint32_t REG_BUFFER_DATA = 0x20;
|
||||
static constexpr uint32_t REG_PRESENT_STATE = 0x24;
|
||||
static constexpr uint32_t REG_HOST_CONTROL = 0x28;
|
||||
static constexpr uint32_t REG_CLOCK_CONTROL = 0x2C;
|
||||
static constexpr uint32_t REG_INT_STATUS = 0x30;
|
||||
static constexpr uint32_t REG_INT_ENABLE = 0x34;
|
||||
static constexpr uint32_t REG_INT_SIGNAL = 0x38;
|
||||
static constexpr uint32_t REG_CAPABILITIES = 0x40;
|
||||
static constexpr uint32_t REG_VERSION = 0xFC;
|
||||
|
||||
static constexpr uint32_t PS_CMD_INHIBIT = 1u << 0;
|
||||
static constexpr uint32_t PS_DAT_INHIBIT = 1u << 1;
|
||||
|
||||
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_CARD = 1u << 8;
|
||||
static constexpr uint32_t INT_ERROR = 1u << 15;
|
||||
static constexpr uint32_t INT_ERROR_MASK = 0xFFFF0000u;
|
||||
// Status enables: the completions, both buffer-ready bits, the card
|
||||
// interrupt and every error. Card insert/remove stay off: nothing is
|
||||
// ever inserted, and the detect line is not wired to the radio.
|
||||
static constexpr uint32_t INT_ENABLES = 0x03FF0000u | INT_CMD_COMPLETE | INT_XFER_COMPLETE
|
||||
| INT_WRITE_READY | INT_READ_READY | INT_CARD;
|
||||
|
||||
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 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;
|
||||
|
||||
static constexpr uint32_t RESET_ALL = 1u << 24;
|
||||
static constexpr uint32_t RESET_CMD = 1u << 25;
|
||||
static constexpr uint32_t RESET_DAT = 1u << 26;
|
||||
|
||||
static constexpr uint32_t XM_BLOCK_COUNT_EN = 1u << 1;
|
||||
static constexpr uint32_t XM_READ = 1u << 4;
|
||||
static constexpr uint32_t XM_MULTI_BLOCK = 1u << 5;
|
||||
|
||||
enum Response : uint32_t {
|
||||
RespNone = 0,
|
||||
Resp48 = 2 | 3u << 3, // R1, R5, R6: CRC and index checked
|
||||
Resp48NoCrc = 2, // R4
|
||||
};
|
||||
static constexpr uint32_t CMD_DATA = 1u << 5;
|
||||
|
||||
// R5 flags: COM_CRC_ERROR, ILLEGAL_COMMAND, ERROR, FUNCTION_NUMBER,
|
||||
// OUT_OF_RANGE.
|
||||
static constexpr uint32_t R5_ERRORS = 0xCB00;
|
||||
|
||||
// CCCR
|
||||
static constexpr uint32_t CCCR_IOEN = 0x02;
|
||||
static constexpr uint32_t CCCR_IORDY = 0x03;
|
||||
static constexpr uint32_t CCCR_INTEN = 0x04;
|
||||
static constexpr uint32_t CCCR_ABORT = 0x06;
|
||||
static constexpr uint32_t CCCR_BUSIF = 0x07;
|
||||
static constexpr uint32_t BUSIF_4BIT = 0x02;
|
||||
static constexpr uint32_t BUSIF_CD_DIS = 0x80;
|
||||
static constexpr uint32_t FBR_BLKSIZE = 0x10; // in function n's FBR at n * 0x100
|
||||
|
||||
static constexpr uint64_t CommandTimeoutMs = 100;
|
||||
static constexpr uint64_t DataTimeoutMs = 500;
|
||||
// CMD53's count field is nine bits.
|
||||
static constexpr uint32_t MaxBlocks = 511;
|
||||
|
||||
// The legacy EMMC clock the firmware sets up; only used when neither the
|
||||
// mailbox nor the capabilities name one. Assuming it high errs towards a
|
||||
// slower bus, never a faster one.
|
||||
static constexpr uint32_t FallbackBaseClockHz = 250000000;
|
||||
|
||||
static volatile uint8_t* g_regs = nullptr;
|
||||
static uint32_t g_version = 0;
|
||||
static uint32_t g_baseClockHz = 0;
|
||||
static uint32_t g_clockHz = 0;
|
||||
static uint32_t g_rca = 0;
|
||||
static bool g_initialized = false;
|
||||
|
||||
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;
|
||||
if (g_clockHz <= 400000) Timekeeping::BusyWaitUs(10);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 & ~INT_CARD);
|
||||
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 | (data ? PS_DAT_INHIBIT : 0);
|
||||
if (!WaitClear(REG_PRESENT_STATE, inhibit, CommandTimeoutMs)) {
|
||||
Reset(RESET_CMD | RESET_DAT);
|
||||
return false;
|
||||
}
|
||||
|
||||
Write(REG_INT_STATUS, ~INT_CARD);
|
||||
Write(REG_ARGUMENT, argument);
|
||||
uint32_t command = index << 8 | response | (data ? CMD_DATA : 0);
|
||||
Write(REG_XFER_MODE_CMD, command << 16 | transferMode);
|
||||
return WaitStatus(INT_CMD_COMPLETE, CommandTimeoutMs);
|
||||
}
|
||||
|
||||
static bool SetClock(uint32_t hz) {
|
||||
Write(REG_CLOCK_CONTROL, Read(REG_CLOCK_CONTROL) & ~0xFFFFu);
|
||||
|
||||
uint32_t divider = 0; // base / (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) {
|
||||
uint32_t p = 1;
|
||||
while (p < divider) p <<= 1;
|
||||
divider = p;
|
||||
}
|
||||
}
|
||||
uint32_t actual = divider ? g_baseClockHz / (2 * divider) : g_baseClockHz;
|
||||
|
||||
uint32_t field = (divider & 0xFF) << 8 | ((divider >> 8) & 0x3) << 6;
|
||||
Write(REG_CLOCK_CONTROL, (Read(REG_CLOCK_CONTROL) & ~0xFFFFu) | field | CLK_INT_EN);
|
||||
|
||||
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);
|
||||
g_clockHz = actual;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool Cmd52(bool write, uint32_t fn, uint32_t addr, uint8_t in, uint8_t* out) {
|
||||
uint32_t arg = (write ? 1u << 31 : 0) | (fn & 7) << 28 | (addr & 0x1FFFF) << 9 | in;
|
||||
if (!SendCommand(52, arg, Resp48)) return false;
|
||||
uint32_t r = Read(REG_RESPONSE);
|
||||
if (r & R5_ERRORS) return false;
|
||||
if (out) *out = (uint8_t)r;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Read8(uint32_t fn, uint32_t addr, uint8_t& value) {
|
||||
return Cmd52(false, fn, addr, 0, &value);
|
||||
}
|
||||
|
||||
bool Write8(uint32_t fn, uint32_t addr, uint8_t value) {
|
||||
return Cmd52(true, fn, addr, value, nullptr);
|
||||
}
|
||||
|
||||
// One CMD53: `count` blocks of `blockSize` bytes, or (byte mode) one run
|
||||
// of `blockSize` bytes. `words` is 4-byte aligned.
|
||||
static bool Cmd53(bool write, uint32_t fn, uint32_t addr, bool incr, uint32_t* words,
|
||||
bool blockMode, uint32_t count, uint32_t blockSize) {
|
||||
uint32_t arg = (write ? 1u << 31 : 0) | (fn & 7) << 28 | (blockMode ? 1u << 27 : 0)
|
||||
| (incr ? 1u << 26 : 0) | (addr & 0x1FFFF) << 9
|
||||
| ((blockMode ? count : blockSize) & 0x1FF);
|
||||
uint32_t blocks = blockMode ? count : 1;
|
||||
Write(REG_BLOCK_SIZE_COUNT, blocks << 16 | blockSize);
|
||||
|
||||
uint32_t mode = write ? 0 : XM_READ;
|
||||
if (blockMode) mode |= XM_BLOCK_COUNT_EN | (blocks > 1 ? XM_MULTI_BLOCK : 0);
|
||||
if (!SendCommand(53, arg, Resp48, mode, true)) return false;
|
||||
if (Read(REG_RESPONSE) & R5_ERRORS) {
|
||||
Abort(fn);
|
||||
Reset(RESET_CMD | RESET_DAT);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t perBlock = (blockSize + 3) / 4;
|
||||
for (uint32_t b = 0; b < blocks; b++) {
|
||||
if (!WaitStatus(write ? INT_WRITE_READY : INT_READ_READY, DataTimeoutMs)) {
|
||||
Abort(fn);
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < perBlock; i++) {
|
||||
if (write) *(volatile uint32_t*)(g_regs + REG_BUFFER_DATA) = *words++;
|
||||
else *words++ = Read(REG_BUFFER_DATA);
|
||||
}
|
||||
}
|
||||
if (!WaitStatus(INT_XFER_COMPLETE, DataTimeoutMs)) {
|
||||
Abort(fn);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool Transfer(bool write, uint32_t fn, uint32_t addr, bool incr,
|
||||
uint8_t* buffer, uint32_t len) {
|
||||
if (!g_initialized || (len & 3) != 0) return false;
|
||||
uint32_t blockSize = fn == FN2 ? FN2_BLOCK : FN1_BLOCK;
|
||||
|
||||
// The data port moves dwords; callers' buffers are only byte aligned
|
||||
// in general.
|
||||
uint32_t bounce[FN2_BLOCK / 4];
|
||||
while (len > 0) {
|
||||
bool blockMode = len >= blockSize;
|
||||
uint32_t count = blockMode ? len / blockSize : 0;
|
||||
if (count > MaxBlocks) count = MaxBlocks;
|
||||
uint32_t run = blockMode ? count * blockSize : len;
|
||||
if (!blockMode && run > FN2_BLOCK) run = FN2_BLOCK;
|
||||
|
||||
bool aligned = ((uintptr_t)buffer & 3) == 0;
|
||||
if (aligned) {
|
||||
if (!Cmd53(write, fn, addr, incr, (uint32_t*)buffer, blockMode, count,
|
||||
blockMode ? blockSize : run)) return false;
|
||||
} else {
|
||||
// Unaligned: one block (or the byte-mode run) at a time
|
||||
// through the bounce buffer.
|
||||
if (blockMode) { count = 1; run = blockSize; }
|
||||
if (write) memcpy(bounce, buffer, run);
|
||||
if (!Cmd53(write, fn, addr, incr, bounce, blockMode, count,
|
||||
blockMode ? blockSize : run)) return false;
|
||||
if (!write) memcpy(buffer, bounce, run);
|
||||
}
|
||||
buffer += run;
|
||||
if (incr) addr += run;
|
||||
len -= run;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadBlock(uint32_t fn, uint32_t addr, bool incr, uint8_t* buffer, uint32_t len) {
|
||||
return Transfer(false, fn, addr, incr, buffer, len);
|
||||
}
|
||||
|
||||
bool WriteBlock(uint32_t fn, uint32_t addr, bool incr, const uint8_t* buffer, uint32_t len) {
|
||||
return Transfer(true, fn, addr, incr, (uint8_t*)buffer, len);
|
||||
}
|
||||
|
||||
void Abort(uint32_t fn) {
|
||||
Cmd52(true, FN0, CCCR_ABORT, (uint8_t)fn, nullptr);
|
||||
}
|
||||
|
||||
bool EnableFunction(uint32_t fn, bool enable, uint64_t timeoutMs) {
|
||||
uint8_t ioen = 0;
|
||||
if (!Read8(FN0, CCCR_IOEN, ioen)) return false;
|
||||
uint8_t bit = (uint8_t)(1u << fn);
|
||||
ioen = enable ? (ioen | bit) : (ioen & ~bit);
|
||||
if (!Write8(FN0, CCCR_IOEN, ioen)) return false;
|
||||
if (!enable) return true;
|
||||
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
for (;;) {
|
||||
uint8_t ready = 0;
|
||||
if (Read8(FN0, CCCR_IORDY, ready) && (ready & bit)) return true;
|
||||
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||
Timekeeping::BusyWaitUs(500);
|
||||
}
|
||||
}
|
||||
|
||||
bool EnableCardInterrupts(uint8_t mask) {
|
||||
return Write8(FN0, CCCR_INTEN, mask);
|
||||
}
|
||||
|
||||
bool CardInterruptPending() {
|
||||
return g_initialized && (Read(REG_INT_STATUS) & INT_CARD) != 0;
|
||||
}
|
||||
|
||||
bool IsInitialized() { return g_initialized; }
|
||||
|
||||
bool HostInit(uint64_t mmioPhys, uint32_t baseClockHz) {
|
||||
g_initialized = false;
|
||||
if (!g_regs) {
|
||||
Memory::VMM::g_paging->MapMMIO(mmioPhys & ~0xFFFULL, Memory::HHDM(mmioPhys & ~0xFFFULL));
|
||||
g_regs = (volatile uint8_t*)Memory::HHDM(mmioPhys);
|
||||
}
|
||||
g_clockHz = 0;
|
||||
|
||||
if (!Reset(RESET_ALL)) {
|
||||
KernelLogStream(ERROR, "WiFi-SDIO") << "Controller reset timed out";
|
||||
return false;
|
||||
}
|
||||
|
||||
g_version = (Read(REG_VERSION) >> 16) & 0xFF;
|
||||
uint32_t caps = Read(REG_CAPABILITIES);
|
||||
uint32_t capsMhz = g_version >= 2 ? (caps >> 8) & 0xFF : (caps >> 8) & 0x3F;
|
||||
g_baseClockHz = baseClockHz ? baseClockHz
|
||||
: capsMhz ? capsMhz * 1000000 : FallbackBaseClockHz;
|
||||
|
||||
// The radio's 3.3 V supply is not switched by the controller, but the
|
||||
// bus power bit gates the controller's own drivers.
|
||||
Write(REG_HOST_CONTROL, (Read(REG_HOST_CONTROL) & ~0xFF00u) | PWR_330 | PWR_ON);
|
||||
Write(REG_INT_ENABLE, INT_ENABLES);
|
||||
Write(REG_INT_SIGNAL, 0);
|
||||
Write(REG_CLOCK_CONTROL, (Read(REG_CLOCK_CONTROL) & ~0xFF0000u) | TIMEOUT_MAX);
|
||||
|
||||
if (!SetClock(400000)) {
|
||||
KernelLogStream(ERROR, "WiFi-SDIO") << "Clock did not stabilize";
|
||||
return false;
|
||||
}
|
||||
// 74 clocks before the first command.
|
||||
Timekeeping::BusyWaitMs(2);
|
||||
|
||||
KernelLogStream(INFO, "WiFi-SDIO") << "Arasan controller at 0x" << base::hex << mmioPhys
|
||||
<< base::dec << ", version " << (uint64_t)(g_version + 1) << ".00, base clock "
|
||||
<< (uint64_t)(g_baseClockHz / 1000000) << " MHz";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CardInit() {
|
||||
// Allow Transfer()/Cmd52 during bring-up; cleared again on failure.
|
||||
g_initialized = true;
|
||||
auto fail = [](const char* what) {
|
||||
KernelLogStream(WARNING, "WiFi-SDIO") << what;
|
||||
g_initialized = false;
|
||||
return false;
|
||||
};
|
||||
|
||||
SendCommand(0, 0, RespNone);
|
||||
Timekeeping::BusyWaitMs(2);
|
||||
|
||||
// CMD5 with no voltage first to read the OCR; the radio may still be
|
||||
// coming out of reset, so give it a few tries.
|
||||
uint32_t ocr = 0;
|
||||
bool answered = false;
|
||||
for (int i = 0; i < 10 && !answered; i++) {
|
||||
answered = SendCommand(5, 0, Resp48NoCrc);
|
||||
if (!answered) Timekeeping::BusyWaitMs(10);
|
||||
}
|
||||
if (!answered) return fail("No SDIO card answers CMD5");
|
||||
ocr = Read(REG_RESPONSE) & 0x00FFFFFF;
|
||||
|
||||
uint32_t want = ocr & 0x00300000; // 3.2-3.4 V
|
||||
if (!want) want = ocr & 0x00FF8000;
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
for (;;) {
|
||||
if (!SendCommand(5, want, Resp48NoCrc)) return fail("CMD5 failed");
|
||||
if (Read(REG_RESPONSE) & (1u << 31)) break;
|
||||
if (Timekeeping::GetMilliseconds() - start > 1000) return fail("Card stayed busy (CMD5)");
|
||||
Timekeeping::BusyWaitMs(1);
|
||||
}
|
||||
|
||||
if (!SendCommand(3, 0, Resp48)) return fail("CMD3 failed");
|
||||
g_rca = Read(REG_RESPONSE) >> 16;
|
||||
if (!SendCommand(7, g_rca << 16, Resp48)) return fail("CMD7 failed");
|
||||
|
||||
// 4-bit bus; disconnect the card-detect pull-up on DAT3.
|
||||
uint8_t busif = 0;
|
||||
if (!Read8(FN0, CCCR_BUSIF, busif)) return fail("CCCR read failed");
|
||||
busif = (uint8_t)((busif & ~0x03) | BUSIF_4BIT | BUSIF_CD_DIS);
|
||||
if (!Write8(FN0, CCCR_BUSIF, busif)) return fail("Could not select the 4-bit bus");
|
||||
Write(REG_HOST_CONTROL, Read(REG_HOST_CONTROL) | HC_4BIT);
|
||||
|
||||
if (!SetClock(25000000)) return fail("Clock did not stabilize at 25 MHz");
|
||||
|
||||
// Function block sizes (little endian in the FBR).
|
||||
bool ok = Write8(FN0, 1 * 0x100 + FBR_BLKSIZE, (uint8_t)(FN1_BLOCK & 0xFF))
|
||||
&& Write8(FN0, 1 * 0x100 + FBR_BLKSIZE + 1, (uint8_t)(FN1_BLOCK >> 8))
|
||||
&& Write8(FN0, 2 * 0x100 + FBR_BLKSIZE, (uint8_t)(FN2_BLOCK & 0xFF))
|
||||
&& Write8(FN0, 2 * 0x100 + FBR_BLKSIZE + 1, (uint8_t)(FN2_BLOCK >> 8));
|
||||
if (!ok) return fail("Could not set the function block sizes");
|
||||
|
||||
if (!EnableFunction(FN1, true, 1000)) return fail("Function 1 did not become ready");
|
||||
|
||||
KernelLogStream(OK, "WiFi-SDIO") << "SDIO card ready (RCA 0x" << base::hex << (uint64_t)g_rca
|
||||
<< base::dec << ", OCR 0x" << base::hex << (uint64_t)ocr << base::dec << ", "
|
||||
<< (uint64_t)(g_clockHz / 1000) << " kHz, 4-bit)";
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* BrcmfSdio.hpp
|
||||
* SDIO host on the Raspberry Pi 4/400's Arasan SD controller, for the
|
||||
* CYW4345x Wi-Fi radio wired to it
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// The BCM2711 has two SD controllers: EMMC2 runs the card slot
|
||||
// (Drivers/Storage/Sdhci) and the older Arasan-derived EMMC, routed to GPIO
|
||||
// 34-39, runs the radio's SDIO interface. This is a minimal SDIO host for
|
||||
// the latter: one card, polled, programmed I/O, CMD52 and CMD53 only.
|
||||
//
|
||||
// None of these calls lock; the Broadcom driver above serializes them.
|
||||
|
||||
namespace Drivers::Net::Wifi::Sdio {
|
||||
|
||||
// SDIO functions on the radio.
|
||||
constexpr uint32_t FN0 = 0; // CCCR / FBR
|
||||
constexpr uint32_t FN1 = 1; // backplane
|
||||
constexpr uint32_t FN2 = 2; // WLAN frames
|
||||
|
||||
// Block sizes the card is configured with.
|
||||
constexpr uint32_t FN1_BLOCK = 64;
|
||||
constexpr uint32_t FN2_BLOCK = 512;
|
||||
|
||||
// Map the controller's registers and reset it. `baseClockHz` is the
|
||||
// controller's input clock (0 = take it from the capabilities).
|
||||
bool HostInit(uint64_t mmioPhys, uint32_t baseClockHz);
|
||||
|
||||
// Enumerate the card: CMD0/5/3/7, 4-bit bus at 25 MHz, function 1/2
|
||||
// block sizes, function 1 enabled. Returns false when nothing answers.
|
||||
bool CardInit();
|
||||
|
||||
// CMD52: one byte.
|
||||
bool Read8(uint32_t fn, uint32_t addr, uint8_t& value);
|
||||
bool Write8(uint32_t fn, uint32_t addr, uint8_t value);
|
||||
|
||||
// CMD53: `len` bytes, a multiple of 4. `incr` selects an incrementing
|
||||
// address (backplane memory) over a fixed one (the function 2 FIFO).
|
||||
// Longer transfers are split into block-mode runs plus a byte-mode tail.
|
||||
bool ReadBlock(uint32_t fn, uint32_t addr, bool incr, uint8_t* buffer, uint32_t len);
|
||||
bool WriteBlock(uint32_t fn, uint32_t addr, bool incr, const uint8_t* buffer, uint32_t len);
|
||||
|
||||
// Enable or disable function `fn`'s I/O; waits up to `timeoutMs` for the
|
||||
// card to report it ready.
|
||||
bool EnableFunction(uint32_t fn, bool enable, uint64_t timeoutMs);
|
||||
|
||||
// CCCR interrupt enables (master | functions).
|
||||
bool EnableCardInterrupts(uint8_t mask);
|
||||
|
||||
// Abort a function's transfer in progress (CCCR I/O Abort).
|
||||
void Abort(uint32_t fn);
|
||||
|
||||
// The card is signalling an interrupt on DAT1. One register read, safe
|
||||
// from any context.
|
||||
bool CardInterruptPending();
|
||||
|
||||
bool IsInitialized();
|
||||
|
||||
}
|
||||
@@ -443,6 +443,12 @@ namespace Drivers::Net::Wifi {
|
||||
// Send an Ethernet frame over the air (called by the netif).
|
||||
bool IwxConnectSendEthernet(const uint8_t* frame, uint32_t len);
|
||||
|
||||
// The supplicant's hooks (Wpa.hpp) for this MLME; Wifi.cpp routes them.
|
||||
bool IwxWpaTxEapol(const uint8_t* body, uint32_t len);
|
||||
bool IwxWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher);
|
||||
bool IwxWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc);
|
||||
|
||||
// Sink for decapsulated Ethernet frames, implemented by Wifi.cpp.
|
||||
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
|
||||
}
|
||||
@@ -535,7 +535,7 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
// The supplicant transmits EAPOL frames unencrypted: they are what
|
||||
// establishes the key in the first place.
|
||||
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||
bool IwxWpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||
return SendDataFrame(g_bssid, ETHERTYPE_EAPOL, body, len, false);
|
||||
}
|
||||
|
||||
@@ -545,14 +545,14 @@ namespace Drivers::Net::Wifi {
|
||||
static InstalledKey g_ptkKey = {};
|
||||
static InstalledKey g_gtkKey = {};
|
||||
|
||||
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||
bool IwxWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||
if (!IwxSetKey(tk, tkLen, 0, true, cipher, nullptr)) return false;
|
||||
g_ptkKey = { true, 0, cipher, (uint8_t)tkLen };
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc) {
|
||||
bool IwxWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc) {
|
||||
if (gtkLen != 16 && gtkLen != 32) return false;
|
||||
if (!IwxSetKey(gtk, gtkLen, keyIdx, false, cipher, rsc)) return false;
|
||||
g_gtkKey = { true, keyIdx, cipher, (uint8_t)gtkLen };
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "Wifi.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include "Iwx.hpp"
|
||||
#include "Brcmf.hpp"
|
||||
#include "Wpa.hpp"
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
@@ -34,6 +35,72 @@ namespace Drivers::Net::Wifi {
|
||||
static std::atomic<bool> g_initPending{false};
|
||||
static bool g_initialized = false;
|
||||
|
||||
// =========================================================================
|
||||
// Backend
|
||||
//
|
||||
// Two kinds of adapter sit behind this facade: the Intel AX210 family on
|
||||
// PCIe (Iwx*: the firmware is a SoftMAC, the MLME is ours) and the
|
||||
// Raspberry Pi 4/400's Broadcom/Cypress radio on SDIO (Brcmf*: FullMAC,
|
||||
// the firmware joins by itself). A machine has at most one; whichever
|
||||
// probes first owns the facade. Both use the same scan table, the same
|
||||
// supplicant and the same connection-state numbering (WIFI_CONN_*, which
|
||||
// IwxConnStateId mirrors).
|
||||
// =========================================================================
|
||||
|
||||
enum class Backend : uint8_t { None, Iwx, Brcmf };
|
||||
static Backend g_backend = Backend::None;
|
||||
|
||||
static bool IsBrcmf() { return g_backend == Backend::Brcmf; }
|
||||
|
||||
static bool FwRunning() {
|
||||
return IsBrcmf() ? BrcmfGetState() == BrcmfState::Running
|
||||
: g_iwx.State == IwxFwState::Running;
|
||||
}
|
||||
|
||||
static bool FwFailed() {
|
||||
return IsBrcmf() ? BrcmfGetState() == BrcmfState::Error
|
||||
: g_iwx.State == IwxFwState::Error;
|
||||
}
|
||||
|
||||
static bool ScanActive() { return IsBrcmf() ? BrcmfScanActive() : g_iwx.ScanActive; }
|
||||
static bool BeginScan() { return IsBrcmf() ? BrcmfStartScan() : IwxStartScan(nullptr); }
|
||||
|
||||
static void AbortScan() {
|
||||
if (IsBrcmf()) BrcmfAbortScan();
|
||||
else IwxAbortScan();
|
||||
}
|
||||
|
||||
// What a blocking wait pumps: the receive path, and for a join the
|
||||
// state machine behind it.
|
||||
static void PumpEvents() {
|
||||
if (IsBrcmf()) BrcmfService();
|
||||
else IwxProcessEvents();
|
||||
}
|
||||
|
||||
static void PumpConnection() {
|
||||
if (IsBrcmf()) {
|
||||
BrcmfService();
|
||||
} else {
|
||||
IwxProcessEvents();
|
||||
IwxConnectService();
|
||||
}
|
||||
}
|
||||
|
||||
static int ConnState() { return IsBrcmf() ? BrcmfConnectState() : IwxConnectState(); }
|
||||
|
||||
static void ConnAbort() {
|
||||
if (IsBrcmf()) BrcmfConnectAbort();
|
||||
else IwxConnectAbort();
|
||||
}
|
||||
|
||||
// A handshake that got as far as exchanging EAPOL frames and then failed
|
||||
// is almost always a wrong passphrase.
|
||||
static bool JoinFailedOnAuth() {
|
||||
return IsBrcmf() ? BrcmfJoinFailedOnAuth() : WpaGetState() == WpaState::Failed;
|
||||
}
|
||||
|
||||
static bool LinkUp() { return IsBrcmf() ? BrcmfLinkUp() : IwxLinkUp(); }
|
||||
|
||||
// =========================================================================
|
||||
// Scan result table
|
||||
// =========================================================================
|
||||
@@ -141,22 +208,12 @@ namespace Drivers::Net::Wifi {
|
||||
uint8_t RsnLen = 0;
|
||||
};
|
||||
|
||||
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
|
||||
// 24-byte MAC header, then timestamp(8) + beacon interval(2) + caps(2).
|
||||
constexpr uint32_t HDR = 24;
|
||||
constexpr uint32_t FIXED = 12;
|
||||
if (len < HDR + FIXED) return false;
|
||||
|
||||
const uint8_t* fixed = frame + HDR;
|
||||
out->BeaconInterval = (uint16_t)(fixed[8] | (fixed[9] << 8));
|
||||
uint16_t caps = (uint16_t)(fixed[10] | (fixed[11] << 8));
|
||||
bool privacy = (caps & 0x0010) != 0;
|
||||
|
||||
// The information elements of a beacon or probe response; `privacy` is
|
||||
// the capability field's Privacy bit.
|
||||
static void ParseIes(const uint8_t* ie, const uint8_t* end, bool privacy, ParsedBeacon* out) {
|
||||
bool haveRsn = false, haveWpa = false;
|
||||
|
||||
const uint8_t* ie = frame + HDR + FIXED;
|
||||
const uint8_t* end = frame + len;
|
||||
while (ie + 2 <= end) {
|
||||
while (ie && ie + 2 <= end) {
|
||||
uint8_t id = ie[0];
|
||||
uint8_t ielen = ie[1];
|
||||
if (ie + 2 + ielen > end) break;
|
||||
@@ -200,9 +257,24 @@ namespace Drivers::Net::Wifi {
|
||||
else if (privacy) out->Security = WIFI_SEC_WEP;
|
||||
else out->Security = WIFI_SEC_OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
|
||||
// 24-byte MAC header, then timestamp(8) + beacon interval(2) + caps(2).
|
||||
constexpr uint32_t HDR = 24;
|
||||
constexpr uint32_t FIXED = 12;
|
||||
if (len < HDR + FIXED) return false;
|
||||
|
||||
const uint8_t* fixed = frame + HDR;
|
||||
out->BeaconInterval = (uint16_t)(fixed[8] | (fixed[9] << 8));
|
||||
uint16_t caps = (uint16_t)(fixed[10] | (fixed[11] << 8));
|
||||
ParseIes(frame + HDR + FIXED, frame + len, (caps & 0x0010) != 0, out);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void StoreScanResult(const uint8_t* bssid, const ParsedBeacon& pb, uint8_t ch,
|
||||
int8_t rssiDbm);
|
||||
|
||||
// Called by the MVM RX path for every received management frame.
|
||||
void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
|
||||
int8_t rssiDbm) {
|
||||
@@ -224,9 +296,21 @@ namespace Drivers::Net::Wifi {
|
||||
ParsedBeacon pb;
|
||||
if (!ParseBeacon(frame, len, &pb)) return;
|
||||
|
||||
const uint8_t* bssid = frame + 16; // addr3 of a beacon
|
||||
uint8_t ch = pb.Channel ? pb.Channel : channel;
|
||||
// addr3 of a beacon
|
||||
StoreScanResult(frame + 16, pb, pb.Channel ? pb.Channel : channel, rssiDbm);
|
||||
}
|
||||
|
||||
// One BSS a FullMAC firmware reported from its own scan.
|
||||
void WifiRxBssInfo(const uint8_t* bssid, uint16_t beaconInterval, uint16_t capability,
|
||||
const uint8_t* ies, uint32_t iesLen, uint8_t channel, int8_t rssiDbm) {
|
||||
ParsedBeacon pb;
|
||||
pb.BeaconInterval = beaconInterval;
|
||||
ParseIes(ies, ies ? ies + iesLen : nullptr, (capability & 0x0010) != 0, &pb);
|
||||
StoreScanResult(bssid, pb, pb.Channel ? pb.Channel : channel, rssiDbm);
|
||||
}
|
||||
|
||||
static void StoreScanResult(const uint8_t* bssid, const ParsedBeacon& pb, uint8_t ch,
|
||||
int8_t rssiDbm) {
|
||||
g_resultLock.Acquire();
|
||||
|
||||
ScanEntry* slot = nullptr;
|
||||
@@ -275,7 +359,9 @@ namespace Drivers::Net::Wifi {
|
||||
// =========================================================================
|
||||
|
||||
bool Probe(const Pci::PciDevice& dev) {
|
||||
if (g_backend != Backend::None) return false;
|
||||
if (!IwxProbe(dev)) return false;
|
||||
g_backend = Backend::Iwx;
|
||||
|
||||
// Firmware comes off the ramdisk, which is not mounted during the PCI
|
||||
// scan; hand the rest of the bring-up to the idle loop.
|
||||
@@ -283,7 +369,23 @@ namespace Drivers::Net::Wifi {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProbeBroadcomSdio(uint64_t mmioPhys, const char* name) {
|
||||
if (g_backend != Backend::None) return false;
|
||||
if (!BrcmfProbe(mmioPhys)) return false;
|
||||
g_backend = Backend::Brcmf;
|
||||
KernelLogStream(INFO, "WiFi") << "Broadcom SDIO radio on " << name
|
||||
<< "; firmware loads once the root filesystem is up";
|
||||
// Same reason as the PCI path: the firmware is on the root volume.
|
||||
g_initPending.store(true, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void CompleteInit() {
|
||||
if (IsBrcmf()) {
|
||||
g_initialized = BrcmfCompleteInit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IwxReadFirmware()) {
|
||||
g_iwx.State = IwxFwState::Error;
|
||||
return;
|
||||
@@ -348,7 +450,7 @@ namespace Drivers::Net::Wifi {
|
||||
static constexpr uint64_t RECOVERY_BACKOFF_MS = 5000;
|
||||
|
||||
static void ServiceRecovery() {
|
||||
if (g_iwx.State != IwxFwState::Error || g_recoveryGaveUp) return;
|
||||
if (!FwFailed() || g_recoveryGaveUp) return;
|
||||
// An adapter that never finished its first bring-up is ServiceDeferredInit's
|
||||
// problem, not this one.
|
||||
if (!g_initialized) return;
|
||||
@@ -366,7 +468,7 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
// Re-check under the gate: the winner of the race may have just
|
||||
// finished a reset that fixed things.
|
||||
if (g_iwx.State != IwxFwState::Error) return;
|
||||
if (!FwFailed()) return;
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_lastRecoveryMs && now - g_lastRecoveryMs < RECOVERY_BACKOFF_MS) return;
|
||||
@@ -384,6 +486,24 @@ namespace Drivers::Net::Wifi {
|
||||
<< " (attempt " << (uint64_t)g_recoveryAttempts << " of "
|
||||
<< (uint64_t)MAX_RECOVERY_ATTEMPTS << ")";
|
||||
|
||||
if (IsBrcmf()) {
|
||||
// A FullMAC radio is reset by power: WL_ON low, then the whole
|
||||
// bring-up (firmware download included) again.
|
||||
g_initialized = false;
|
||||
g_asyncConnect = false;
|
||||
g_scanDeadline = 0;
|
||||
g_lastError = WIFI_ERR_NO_ADAPTER;
|
||||
ClearResults();
|
||||
BrcmfShutdown();
|
||||
CompleteInit();
|
||||
if (g_initialized) {
|
||||
KernelLogStream(OK, "WiFi") << "Adapter recovered";
|
||||
g_recoveryAttempts = 0;
|
||||
g_lastError = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Nothing may be mid-command while the device is torn down. Taking
|
||||
// CmdLock is enough: it is what serializes every sender, and the RX
|
||||
// pump has its own reentrancy guard.
|
||||
@@ -517,8 +637,24 @@ namespace Drivers::Net::Wifi {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The facade's own deadlines (async scan and join) and a due reset.
|
||||
static bool FacadeWorkDue(uint64_t now) {
|
||||
if (g_scanDeadline != 0 && (now >= g_scanDeadline || !ScanActive())) return true;
|
||||
if (g_asyncConnect) return true;
|
||||
return FwFailed() && g_initialized && !g_recoveryGaveUp &&
|
||||
(g_lastRecoveryMs == 0 || now - g_lastRecoveryMs >= RECOVERY_BACKOFF_MS);
|
||||
}
|
||||
|
||||
bool HasPolledWork() {
|
||||
return IsBrcmf() && BrcmfHasWork(Timekeeping::GetMilliseconds());
|
||||
}
|
||||
|
||||
bool HasDeferredWork() {
|
||||
if (InitDue()) return true;
|
||||
if (IsBrcmf()) {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
return BrcmfHasWork(now) || FacadeWorkDue(now);
|
||||
}
|
||||
if (!g_iwx.Mmio) return false;
|
||||
if (g_iwx.WorkPending) return true;
|
||||
|
||||
@@ -527,7 +663,29 @@ namespace Drivers::Net::Wifi {
|
||||
return HasBlockingWork(now);
|
||||
}
|
||||
|
||||
// The Broadcom radio has no interrupt in use: everything on its bus is
|
||||
// polled, and everything on its bus waits (SDIO transfers, ioctls), so
|
||||
// all of it goes through the blocking gate.
|
||||
static void ServiceBrcmf() {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
bool radioWork = BrcmfHasWork(now);
|
||||
if (!radioWork && !FacadeWorkDue(now)) return;
|
||||
if (LeaveToAp()) return;
|
||||
if (__atomic_test_and_set(&g_blockingBusy, __ATOMIC_ACQUIRE)) return;
|
||||
struct Guard {
|
||||
~Guard() { __atomic_clear(&g_blockingBusy, __ATOMIC_RELEASE); }
|
||||
} guard;
|
||||
|
||||
if (radioWork) BrcmfService();
|
||||
ServiceAsync();
|
||||
ServiceRecovery();
|
||||
}
|
||||
|
||||
void ServiceEvents() {
|
||||
if (IsBrcmf()) {
|
||||
ServiceBrcmf();
|
||||
return;
|
||||
}
|
||||
if (!g_iwx.Mmio) return;
|
||||
|
||||
// The non-blocking half: any core, the BSP included.
|
||||
@@ -573,7 +731,11 @@ namespace Drivers::Net::Wifi {
|
||||
}
|
||||
|
||||
bool IsInitialized() { return g_initialized; }
|
||||
bool IsPresent() { return g_iwx.State != IwxFwState::Absent; }
|
||||
|
||||
bool IsPresent() {
|
||||
return IsBrcmf() ? BrcmfGetState() != BrcmfState::Absent
|
||||
: g_iwx.State != IwxFwState::Absent;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Public operations
|
||||
@@ -611,26 +773,26 @@ namespace Drivers::Net::Wifi {
|
||||
int Scan(WifiNetwork* out, int maxCount, uint32_t timeoutMs) {
|
||||
if (!out || maxCount <= 0) return -1;
|
||||
if (!g_initialized) return -1;
|
||||
if (g_iwx.State != IwxFwState::Running) return -1;
|
||||
if (!FwRunning()) return -1;
|
||||
|
||||
if (timeoutMs < 1000) timeoutMs = 1000;
|
||||
if (timeoutMs > 20000) timeoutMs = 20000;
|
||||
|
||||
ClearResults();
|
||||
|
||||
if (!IwxStartScan(nullptr)) return -1;
|
||||
if (!BeginScan()) return -1;
|
||||
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (g_iwx.ScanActive && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
IwxProcessEvents();
|
||||
while (ScanActive() && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
PumpEvents();
|
||||
for (int i = 0; i < 200; i++) Arch::Pause();
|
||||
}
|
||||
|
||||
if (g_iwx.ScanActive) {
|
||||
IwxAbortScan();
|
||||
if (ScanActive()) {
|
||||
AbortScan();
|
||||
// Drain whatever the firmware queued before the abort landed.
|
||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - t0 < 200) IwxProcessEvents();
|
||||
while (Timekeeping::GetMilliseconds() - t0 < 200) PumpEvents();
|
||||
}
|
||||
|
||||
g_scanGeneration++;
|
||||
@@ -639,14 +801,14 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
int StartScan(uint32_t timeoutMs) {
|
||||
if (!g_initialized) return -1;
|
||||
if (g_iwx.State != IwxFwState::Running) return -1;
|
||||
if (g_iwx.ScanActive) return 1; // already sweeping
|
||||
if (!FwRunning()) return -1;
|
||||
if (ScanActive()) return 1; // already sweeping
|
||||
|
||||
if (timeoutMs < 1000) timeoutMs = 1000;
|
||||
if (timeoutMs > 20000) timeoutMs = 20000;
|
||||
|
||||
ClearResults();
|
||||
if (!IwxStartScan(nullptr)) return -1;
|
||||
if (!BeginScan()) return -1;
|
||||
|
||||
g_scanDeadline = Timekeeping::GetMilliseconds() + timeoutMs;
|
||||
return 0;
|
||||
@@ -657,33 +819,47 @@ namespace Drivers::Net::Wifi {
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
out->present = IsPresent() ? 1 : 0;
|
||||
out->state = (uint8_t)g_iwx.State;
|
||||
if (!IsPresent()) return -1;
|
||||
|
||||
memcpy(out->mac, g_iwx.Nvm.HwAddr, 6);
|
||||
out->scanning = g_iwx.ScanActive ? 1 : 0;
|
||||
out->bands = (uint8_t)((g_iwx.Nvm.Sku24GHz ? 1 : 0)
|
||||
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
|
||||
out->channels = (uint16_t)g_iwx.ChannelCount;
|
||||
out->rxPackets = g_iwx.RxPackets;
|
||||
out->txPackets = g_iwx.TxPackets;
|
||||
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
||||
out->connState = (uint32_t)IwxConnectState();
|
||||
out->connected = IwxLinkUp() ? 1 : 0;
|
||||
const char* fwVersion;
|
||||
if (IsBrcmf()) {
|
||||
out->state = (uint8_t)BrcmfGetState();
|
||||
memcpy(out->mac, g_brcmf.Mac, 6);
|
||||
out->bands = (uint8_t)((g_brcmf.Band24 ? 1 : 0) | (g_brcmf.Band5 ? 2 : 0));
|
||||
out->channels = g_brcmf.ChannelCount;
|
||||
out->rxPackets = g_brcmf.RxPackets;
|
||||
out->txPackets = g_brcmf.TxPackets;
|
||||
out->fwErrors = g_brcmf.FwErrors;
|
||||
fwVersion = g_brcmf.FwVersion;
|
||||
} else {
|
||||
out->state = (uint8_t)g_iwx.State;
|
||||
memcpy(out->mac, g_iwx.Nvm.HwAddr, 6);
|
||||
out->bands = (uint8_t)((g_iwx.Nvm.Sku24GHz ? 1 : 0)
|
||||
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
|
||||
out->channels = (uint16_t)g_iwx.ChannelCount;
|
||||
out->rxPackets = g_iwx.RxPackets;
|
||||
out->txPackets = g_iwx.TxPackets;
|
||||
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
||||
fwVersion = g_iwx.Fw.Version;
|
||||
}
|
||||
out->scanning = ScanActive() ? 1 : 0;
|
||||
out->connState = (uint32_t)ConnState();
|
||||
out->connected = LinkUp() ? 1 : 0;
|
||||
out->lastError = (int32_t)g_lastError;
|
||||
out->scanGeneration = g_scanGeneration;
|
||||
out->joining = g_asyncConnect ? 1 : 0;
|
||||
|
||||
if (IwxConnectState() != (int)IwxConnStateId::Idle) {
|
||||
const char* ssid = IwxConnectSsid();
|
||||
if (ConnState() != (int)IwxConnStateId::Idle) {
|
||||
const char* ssid = IsBrcmf() ? BrcmfConnectSsid() : IwxConnectSsid();
|
||||
int k = 0;
|
||||
for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k];
|
||||
out->ssid[k] = '\0';
|
||||
memcpy(out->bssid, IwxConnectBssid(), 6);
|
||||
memcpy(out->bssid, IsBrcmf() ? BrcmfConnectBssid() : IwxConnectBssid(), 6);
|
||||
if (IsBrcmf()) out->channel = BrcmfConnectChannel();
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i];
|
||||
for (; i < 31 && fwVersion[i]; i++) out->fwVersion[i] = fwVersion[i];
|
||||
out->fwVersion[i] = '\0';
|
||||
return 0;
|
||||
}
|
||||
@@ -695,24 +871,20 @@ namespace Drivers::Net::Wifi {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
IwxProcessEvents();
|
||||
IwxConnectService();
|
||||
PumpConnection();
|
||||
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
auto state = (IwxConnStateId)ConnState();
|
||||
if (state == IwxConnStateId::Connected) return 0;
|
||||
if (state == IwxConnStateId::Failed) {
|
||||
// A handshake that got as far as exchanging EAPOL frames and
|
||||
// then failed is almost always a wrong passphrase.
|
||||
int rc = WpaGetState() == WpaState::Failed
|
||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
int rc = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
// A rejection seen on the RX path only marks the state; the
|
||||
// firmware contexts are still up and have to come back down.
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
return rc;
|
||||
}
|
||||
if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED;
|
||||
if (g_iwx.State == IwxFwState::Error) {
|
||||
IwxConnectAbort();
|
||||
if (FwFailed()) {
|
||||
ConnAbort();
|
||||
return WIFI_ERR_FAILED;
|
||||
}
|
||||
|
||||
@@ -721,7 +893,7 @@ namespace Drivers::Net::Wifi {
|
||||
Sched::Schedule();
|
||||
}
|
||||
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
return WIFI_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
@@ -732,7 +904,7 @@ namespace Drivers::Net::Wifi {
|
||||
// scan table is worth a (blocking) sweep to look for it.
|
||||
static int StartJoin(const char* ssid, const char* password, bool rescan) {
|
||||
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
|
||||
if (g_iwx.State != IwxFwState::Running) return WIFI_ERR_NO_ADAPTER;
|
||||
if (!FwRunning()) return WIFI_ERR_NO_ADAPTER;
|
||||
|
||||
// Locate the network in the most recent scan results: the firmware
|
||||
// contexts need its BSSID and channel, and the connect path needs its
|
||||
@@ -809,17 +981,24 @@ namespace Drivers::Net::Wifi {
|
||||
if (!havePassword) return WIFI_ERR_NEED_KEY;
|
||||
}
|
||||
|
||||
if (!IwxConnectStart(bssid, channel, is5, ssid,
|
||||
havePassword ? password : nullptr,
|
||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
|
||||
beaconInterval, dtimPeriod)) {
|
||||
// IwxConnectStart already logged the specific reason. Only report
|
||||
bool started = IsBrcmf()
|
||||
? BrcmfConnectStart(bssid, channel, ssid,
|
||||
havePassword ? password : nullptr,
|
||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen)
|
||||
: IwxConnectStart(bssid, channel, is5, ssid,
|
||||
havePassword ? password : nullptr,
|
||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
|
||||
beaconInterval, dtimPeriod);
|
||||
if (!started) {
|
||||
// The backend already logged the specific reason. Only report
|
||||
// a security problem when that is genuinely what happened; a radio
|
||||
// or firmware failure needs completely different advice.
|
||||
bool security_ = IwxConnectRefusedForSecurity();
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
||||
bool security_ = IsBrcmf() ? BrcmfConnectRefusedForSecurity()
|
||||
: IwxConnectRefusedForSecurity();
|
||||
auto state = (IwxConnStateId)ConnState();
|
||||
if (state != IwxConnStateId::Idle) ConnAbort();
|
||||
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
|
||||
}
|
||||
|
||||
@@ -835,8 +1014,8 @@ namespace Drivers::Net::Wifi {
|
||||
int ConnectAsync(const char* ssid, const char* password) {
|
||||
// A join already in flight owns the firmware contexts; tear it down
|
||||
// rather than stacking a second one on top.
|
||||
if (g_asyncConnect || (IwxConnectState() != (int)IwxConnStateId::Idle))
|
||||
IwxConnectAbort();
|
||||
if (g_asyncConnect || (ConnState() != (int)IwxConnStateId::Idle))
|
||||
ConnAbort();
|
||||
|
||||
g_asyncConnect = false;
|
||||
g_lastError = 0;
|
||||
@@ -858,7 +1037,7 @@ namespace Drivers::Net::Wifi {
|
||||
if (!g_initialized) return -1;
|
||||
g_asyncConnect = false;
|
||||
g_lastError = 0;
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -881,11 +1060,11 @@ namespace Drivers::Net::Wifi {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
|
||||
if (g_scanDeadline) {
|
||||
if (!g_iwx.ScanActive) {
|
||||
if (!ScanActive()) {
|
||||
g_scanDeadline = 0;
|
||||
g_scanGeneration++;
|
||||
} else if (now >= g_scanDeadline) {
|
||||
IwxAbortScan();
|
||||
AbortScan();
|
||||
g_scanDeadline = 0;
|
||||
g_scanGeneration++;
|
||||
}
|
||||
@@ -893,31 +1072,29 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
if (!g_asyncConnect) return;
|
||||
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
auto state = (IwxConnStateId)ConnState();
|
||||
if (state == IwxConnStateId::Connected) {
|
||||
g_asyncConnect = false;
|
||||
g_lastError = 0;
|
||||
return;
|
||||
}
|
||||
if (state == IwxConnStateId::Failed) {
|
||||
// Same reading as the blocking path: a handshake that exchanged
|
||||
// EAPOL frames and then failed is almost always a wrong passphrase.
|
||||
g_lastError = WpaGetState() == WpaState::Failed
|
||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
// Same reading as the blocking path.
|
||||
g_lastError = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
g_asyncConnect = false;
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
return;
|
||||
}
|
||||
if (state == IwxConnStateId::Idle || g_iwx.State == IwxFwState::Error) {
|
||||
if (state == IwxConnStateId::Idle || FwFailed()) {
|
||||
g_lastError = WIFI_ERR_FAILED;
|
||||
g_asyncConnect = false;
|
||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
||||
if (state != IwxConnStateId::Idle) ConnAbort();
|
||||
return;
|
||||
}
|
||||
if (now >= g_asyncConnectDeadline) {
|
||||
g_lastError = WIFI_ERR_TIMEOUT;
|
||||
g_asyncConnect = false;
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,13 +1104,33 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
static RxCallback g_rxCallback = nullptr;
|
||||
|
||||
const uint8_t* GetMacAddress() { return g_iwx.Nvm.HwAddr; }
|
||||
const uint8_t* GetMacAddress() { return IsBrcmf() ? g_brcmf.Mac : g_iwx.Nvm.HwAddr; }
|
||||
|
||||
bool IsLinkUp() { return g_initialized && IwxLinkUp(); }
|
||||
bool IsLinkUp() { return g_initialized && LinkUp(); }
|
||||
|
||||
bool SendPacket(const uint8_t* data, uint16_t length) {
|
||||
if (!IsLinkUp()) return false;
|
||||
return IwxConnectSendEthernet(data, length);
|
||||
return IsBrcmf() ? BrcmfSendEthernet(data, length)
|
||||
: IwxConnectSendEthernet(data, length);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Supplicant hooks (Wpa.hpp): to whichever backend is running the join
|
||||
// =========================================================================
|
||||
|
||||
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||
return IsBrcmf() ? BrcmfWpaTxEapol(body, len) : IwxWpaTxEapol(body, len);
|
||||
}
|
||||
|
||||
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||
return IsBrcmf() ? BrcmfWpaInstallPtk(tk, tkLen, cipher)
|
||||
: IwxWpaInstallPtk(tk, tkLen, cipher);
|
||||
}
|
||||
|
||||
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc) {
|
||||
return IsBrcmf() ? BrcmfWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc)
|
||||
: IwxWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc);
|
||||
}
|
||||
|
||||
void SetRxCallback(RxCallback callback) { g_rxCallback = callback; }
|
||||
|
||||
@@ -14,6 +14,11 @@ namespace Drivers::Net::Wifi {
|
||||
// PCI driver-table probe entry point.
|
||||
bool Probe(const Pci::PciDevice& dev);
|
||||
|
||||
// The Raspberry Pi 4/400's Broadcom/Cypress radio, on the SDIO
|
||||
// controller (Arasan) at `mmioPhys`. Claims it; the bring-up waits for
|
||||
// the root filesystem like the PCI path.
|
||||
bool ProbeBroadcomSdio(uint64_t mmioPhys, const char* name);
|
||||
|
||||
// Complete the firmware-dependent bring-up once the ramdisk is mounted.
|
||||
// Cheap no-op unless a device is waiting. Called from the idle loop.
|
||||
void ServiceDeferredInit();
|
||||
@@ -25,6 +30,11 @@ namespace Drivers::Net::Wifi {
|
||||
// deadline, or a due recovery attempt needs idle-context servicing.
|
||||
bool HasDeferredWork();
|
||||
|
||||
// A polled adapter (the Broadcom radio has no interrupt in use) is due a
|
||||
// sweep. Cheap and safe from the timer interrupt: the tick uses it to
|
||||
// force a bottom-half pass when the cores never go idle.
|
||||
bool HasPolledWork();
|
||||
|
||||
bool IsInitialized();
|
||||
bool IsPresent();
|
||||
|
||||
|
||||
@@ -66,7 +66,8 @@ namespace Drivers::Net::Wifi {
|
||||
bool WpaParseApRsn(const uint8_t* ie, uint32_t len, WpaConfig& cfg);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hooks implemented by the MLME (IwxConnect.cpp)
|
||||
// Hooks into the driver running the join (Wifi.cpp routes them to
|
||||
// IwxConnect.cpp or BrcmfConnect.cpp)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Transmit an EAPOL frame body (802.1X header included) to the AP.
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* RpiFirmware.cpp
|
||||
* Raspberry Pi VideoCore firmware mailbox (property interface)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "RpiFirmware.hpp"
|
||||
#include <ACPI/AcpiDevices.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Boot/Boot.hpp>
|
||||
#include <Memory/Memmap.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
// Property interface (raspberrypi/firmware wiki, "Mailbox property
|
||||
// interface"): the ARM writes a message buffer's bus address, tagged with
|
||||
// channel 8, to mailbox 1 and reads the same address back from mailbox 0
|
||||
// once the firmware has written its responses into the buffer.
|
||||
//
|
||||
// u32 size, u32 code (0 = request; 0x80000000 = success),
|
||||
// tags: { u32 id, u32 value buffer size, u32 req/resp (bit 31 + length),
|
||||
// value buffer ... }, u32 0 (end tag)
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Platform::RpiFirmware {
|
||||
|
||||
// Mailbox registers (offsets from the block ACPI describes, 0x7E00B880)
|
||||
static constexpr uint32_t MBOX_READ = 0x00; // mailbox 0: VC -> ARM
|
||||
static constexpr uint32_t MBOX0_STATUS = 0x18;
|
||||
static constexpr uint32_t MBOX_WRITE = 0x20; // mailbox 1: ARM -> VC
|
||||
static constexpr uint32_t MBOX1_STATUS = 0x38;
|
||||
static constexpr uint32_t STATUS_FULL = 1u << 31;
|
||||
static constexpr uint32_t STATUS_EMPTY = 1u << 30;
|
||||
|
||||
static constexpr uint32_t CHANNEL_PROPERTY = 8;
|
||||
|
||||
static constexpr uint32_t CODE_REQUEST = 0x00000000;
|
||||
static constexpr uint32_t CODE_SUCCESS = 0x80000000;
|
||||
static constexpr uint32_t TAG_RESPONSE = 0x80000000;
|
||||
|
||||
static constexpr uint32_t TAG_GET_BOARD_MAC = 0x00010003;
|
||||
static constexpr uint32_t TAG_SET_CLOCK_STATE = 0x00038001;
|
||||
static constexpr uint32_t TAG_GET_CLOCK_RATE = 0x00030002;
|
||||
static constexpr uint32_t TAG_GET_GPIO_STATE = 0x00030041;
|
||||
static constexpr uint32_t TAG_SET_GPIO_STATE = 0x00038041;
|
||||
static constexpr uint32_t TAG_GET_GPIO_CONFIG = 0x00030043;
|
||||
static constexpr uint32_t TAG_SET_GPIO_CONFIG = 0x00038043;
|
||||
|
||||
// The VideoCore sees the first GiB of SDRAM at bus address 0xC0000000
|
||||
// (the uncached alias), and nothing above it, so the message buffer
|
||||
// must sit below 1 GiB.
|
||||
static constexpr uint64_t VC_BUS_ALIAS = 0xC0000000;
|
||||
static constexpr uint64_t VC_DMA_LIMIT = 0x40000000;
|
||||
|
||||
static constexpr uint64_t TimeoutMs = 200;
|
||||
|
||||
static volatile uint8_t* g_mmio = nullptr;
|
||||
static volatile uint32_t* g_buffer = nullptr; // uncached, below 1 GiB
|
||||
static uint64_t g_bufferPhys = 0;
|
||||
static bool g_probed = false;
|
||||
static kcp::Mutex g_lock;
|
||||
|
||||
static inline uint32_t Read32(uint32_t offset) {
|
||||
return *(volatile uint32_t*)(g_mmio + offset);
|
||||
}
|
||||
|
||||
static inline void Write32(uint32_t offset, uint32_t value) {
|
||||
*(volatile uint32_t*)(g_mmio + offset) = value;
|
||||
}
|
||||
|
||||
// A page the VideoCore can reach. The page-frame allocator only manages
|
||||
// the largest run of usable RAM, which on a Pi 4 with the 3 GB limit is
|
||||
// 0x40000000 and up, so it never has one. The usable regions below 1 GiB
|
||||
// are otherwise untouched: take the last page of the highest one.
|
||||
static void* AllocateLowPage(uint64_t& phys) {
|
||||
const auto& map = montauk::boot::Info().memoryMap;
|
||||
Memory::LargestSection pool = Memory::Scan(map);
|
||||
|
||||
uint64_t best = 0;
|
||||
for (uint64_t i = 0; i < map.count; i++) {
|
||||
const auto& r = map.regions[i];
|
||||
if (r.kind != montauk::boot::MemoryKind::Usable || r.base == 0) continue;
|
||||
if (r.base == pool.address) continue; // the allocator's
|
||||
uint64_t end = r.base + r.length;
|
||||
if (end > VC_DMA_LIMIT) end = VC_DMA_LIMIT;
|
||||
if (end < r.base + 0x1000) continue;
|
||||
uint64_t page = (end - 0x1000) & ~0xFFFULL;
|
||||
if (page >= r.base && page > best) best = page;
|
||||
}
|
||||
if (best == 0) return nullptr;
|
||||
|
||||
// Zero it through its cacheable HHDM alias, push the lines out, then
|
||||
// make the alias uncached so neither side needs cache maintenance.
|
||||
void* virt = (void*)Memory::HHDM(best);
|
||||
memset(virt, 0, 0x1000);
|
||||
Arch::InvalidateDataCache(virt, 0x1000);
|
||||
Memory::VMM::g_paging->MapWC(best, (uint64_t)virt);
|
||||
phys = best;
|
||||
return virt;
|
||||
}
|
||||
|
||||
bool Initialize() {
|
||||
if (g_probed) return g_buffer != nullptr;
|
||||
g_probed = true;
|
||||
|
||||
Hal::AcpiDevices::DeviceInfo info{};
|
||||
if (!Hal::AcpiDevices::FindDeviceByHid("BCM2849", info)) return false;
|
||||
|
||||
Hal::AML::ResourceList resources;
|
||||
uint64_t base = 0, length = 0;
|
||||
if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) ||
|
||||
!resources.GetMemory(base, length) || base == 0) {
|
||||
KernelLogStream(WARNING, "RpiFw") << "Mailbox " << info.Path << ": no register block in _CRS";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t page = base & ~0xFFFULL;
|
||||
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
|
||||
if (((base + 0x40) & ~0xFFFULL) != page) {
|
||||
Memory::VMM::g_paging->MapMMIO(page + 0x1000, Memory::HHDM(page + 0x1000));
|
||||
}
|
||||
g_mmio = (volatile uint8_t*)Memory::HHDM(base);
|
||||
|
||||
uint64_t phys = 0;
|
||||
void* buffer = AllocateLowPage(phys);
|
||||
if (buffer == nullptr) {
|
||||
KernelLogStream(ERROR, "RpiFw") << "No message buffer below 1 GiB";
|
||||
g_mmio = nullptr;
|
||||
return false;
|
||||
}
|
||||
g_buffer = (volatile uint32_t*)buffer;
|
||||
g_bufferPhys = phys;
|
||||
|
||||
KernelLogStream(OK, "RpiFw") << "VideoCore mailbox at 0x" << base::hex << base
|
||||
<< ", buffer 0x" << phys << base::dec;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsPresent() {
|
||||
return Initialize();
|
||||
}
|
||||
|
||||
// Send the message in g_buffer and wait for the firmware's reply.
|
||||
// Caller holds g_lock.
|
||||
static bool Call() {
|
||||
uint32_t message = (uint32_t)((g_bufferPhys | VC_BUS_ALIAS) & ~0xFULL) | CHANNEL_PROPERTY;
|
||||
|
||||
// Drop any stale replies (a previous call that timed out).
|
||||
for (int i = 0; i < 16 && !(Read32(MBOX0_STATUS) & STATUS_EMPTY); i++) {
|
||||
(void)Read32(MBOX_READ);
|
||||
}
|
||||
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Read32(MBOX1_STATUS) & STATUS_FULL) {
|
||||
if (Timekeeping::GetMilliseconds() - start > TimeoutMs) return false;
|
||||
Arch::Pause();
|
||||
}
|
||||
|
||||
// The buffer is uncached; the fence orders its stores before the
|
||||
// doorbell write the firmware reacts to.
|
||||
Arch::MemoryFence();
|
||||
Write32(MBOX_WRITE, message);
|
||||
|
||||
start = Timekeeping::GetMilliseconds();
|
||||
for (;;) {
|
||||
while (Read32(MBOX0_STATUS) & STATUS_EMPTY) {
|
||||
if (Timekeeping::GetMilliseconds() - start > TimeoutMs) {
|
||||
KernelLogStream(WARNING, "RpiFw") << "Mailbox call timed out";
|
||||
return false;
|
||||
}
|
||||
Arch::Pause();
|
||||
}
|
||||
uint32_t reply = Read32(MBOX_READ);
|
||||
if (reply == message) break;
|
||||
}
|
||||
Arch::MemoryFence();
|
||||
return g_buffer[1] == CODE_SUCCESS;
|
||||
}
|
||||
|
||||
// One-tag property call. `values` holds valueWords request words and
|
||||
// receives up to valueWords response words. Returns the response length
|
||||
// in bytes, or -1 on failure.
|
||||
static int PropertyCall(uint32_t tag, uint32_t* values, uint32_t valueWords) {
|
||||
if (!Initialize()) return -1;
|
||||
if (valueWords > 16) return -1;
|
||||
|
||||
g_lock.Acquire();
|
||||
uint32_t words = 0;
|
||||
g_buffer[words++] = 0; // size, filled in below
|
||||
g_buffer[words++] = CODE_REQUEST;
|
||||
g_buffer[words++] = tag;
|
||||
g_buffer[words++] = valueWords * 4; // value buffer size
|
||||
g_buffer[words++] = 0; // request
|
||||
uint32_t valueStart = words;
|
||||
for (uint32_t i = 0; i < valueWords; i++) g_buffer[words++] = values[i];
|
||||
g_buffer[words++] = 0; // end tag
|
||||
g_buffer[0] = words * 4;
|
||||
|
||||
// The firmware does not set the response bit for every tag: the
|
||||
// expander GPIO tags answer through value[0] alone and leave the
|
||||
// status word 0. Linux checks only the message code, so do the same
|
||||
// and take the whole value buffer back when the bit is missing; each
|
||||
// caller validates the values it gets.
|
||||
int result = -1;
|
||||
bool answered = Call();
|
||||
uint32_t status = g_buffer[valueStart - 1];
|
||||
if (answered) {
|
||||
uint32_t length = (status & TAG_RESPONSE) ? status & ~TAG_RESPONSE : valueWords * 4;
|
||||
uint32_t copy = (length + 3) / 4;
|
||||
if (copy > valueWords) copy = valueWords;
|
||||
for (uint32_t i = 0; i < copy; i++) values[i] = g_buffer[valueStart + i];
|
||||
result = (int)length;
|
||||
}
|
||||
uint32_t code = g_buffer[1];
|
||||
g_lock.Release();
|
||||
|
||||
if (result < 0) {
|
||||
KernelLogStream(WARNING, "RpiFw") << "Property tag 0x" << base::hex << (uint64_t)tag
|
||||
<< " failed: message code 0x" << (uint64_t)code << base::dec;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool GetClockRate(uint32_t clockId, uint32_t& hz) {
|
||||
uint32_t v[2] = { clockId, 0 };
|
||||
if (PropertyCall(TAG_GET_CLOCK_RATE, v, 2) < 8 || v[0] != clockId) return false;
|
||||
hz = v[1];
|
||||
return hz != 0;
|
||||
}
|
||||
|
||||
// State bit 0 = on; the answer echoes the clock ID and its new state
|
||||
// (bit 1 set: no such clock).
|
||||
bool SetClockState(uint32_t clockId, bool on) {
|
||||
uint32_t v[2] = { clockId, on ? 1u : 0u };
|
||||
if (PropertyCall(TAG_SET_CLOCK_STATE, v, 2) < 8 || v[0] != clockId) return false;
|
||||
return (v[1] & 2) == 0 && ((v[1] & 1) != 0) == on;
|
||||
}
|
||||
|
||||
// The GPIO tags answer in place of the GPIO number with a status word,
|
||||
// 0 for success (as Linux's raspberrypi-exp-gpio driver checks).
|
||||
bool SetGpioState(uint32_t gpio, bool high) {
|
||||
uint32_t v[2] = { gpio, high ? 1u : 0u };
|
||||
return PropertyCall(TAG_SET_GPIO_STATE, v, 2) >= 4 && v[0] == 0;
|
||||
}
|
||||
|
||||
// As Linux's rpi_exp_gpio_dir_out: keep the line's polarity, make it an
|
||||
// output driving `high`. A line the firmware still has as an input
|
||||
// refuses SET_GPIO_STATE.
|
||||
bool SetGpioOutput(uint32_t gpio, bool high) {
|
||||
uint32_t polarity = 0;
|
||||
uint32_t cfg[5] = { gpio, 0, 0, 0, 0 }; // gpio, direction, polarity, term_en, term_pull_up
|
||||
if (PropertyCall(TAG_GET_GPIO_CONFIG, cfg, 5) >= 12 && cfg[0] == 0) polarity = cfg[2];
|
||||
|
||||
uint32_t v[6] = { gpio, 1 /* output */, polarity, 0, 0, high ? 1u : 0u };
|
||||
if (PropertyCall(TAG_SET_GPIO_CONFIG, v, 6) < 4 || v[0] != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetGpioState(uint32_t gpio, bool& high) {
|
||||
uint32_t v[2] = { gpio, 0 };
|
||||
if (PropertyCall(TAG_GET_GPIO_STATE, v, 2) < 8 || v[0] != 0) return false;
|
||||
high = v[1] != 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetBoardMacAddress(uint8_t mac[6]) {
|
||||
uint32_t v[2] = {};
|
||||
if (PropertyCall(TAG_GET_BOARD_MAC, v, 2) < 6) return false;
|
||||
const uint8_t* bytes = (const uint8_t*)v;
|
||||
bool allZero = true;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
mac[i] = bytes[i];
|
||||
if (bytes[i] != 0) allZero = false;
|
||||
}
|
||||
return !allZero;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* RpiFirmware.hpp
|
||||
* Raspberry Pi VideoCore firmware mailbox (property interface)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// The VideoCore firmware owns parts of the board the ARM cores cannot reach
|
||||
// directly: the GPIO expander that powers the Wi-Fi/Bluetooth radio, the
|
||||
// clock tree, the board's OTP (serial number, MAC address). The ARM side asks
|
||||
// for them through the property channel of the VideoCore mailbox, which
|
||||
// ACPI describes as BCM2849 (\_SB.GDV0.RPIQ) on the Pi Firmware Task Force
|
||||
// UEFI firmware.
|
||||
//
|
||||
// All calls are synchronous, take a process-context lock, and must not be
|
||||
// made from interrupt context. They return false when there is no mailbox
|
||||
// (any machine but a Pi) or the firmware rejects the request.
|
||||
|
||||
namespace Drivers::Platform::RpiFirmware {
|
||||
|
||||
// Find the mailbox through ACPI and allocate its message buffer.
|
||||
// Idempotent; the other calls initialize on first use.
|
||||
bool Initialize();
|
||||
|
||||
bool IsPresent();
|
||||
|
||||
// Clock identifiers (the firmware's numbering).
|
||||
constexpr uint32_t CLOCK_EMMC = 1; // legacy EMMC (Arasan), runs the Wi-Fi SDIO bus
|
||||
constexpr uint32_t CLOCK_CORE = 4; // VPU core clock, feeds the mini UART
|
||||
|
||||
// Current rate of a firmware clock, in Hz.
|
||||
bool GetClockRate(uint32_t clockId, uint32_t& hz);
|
||||
|
||||
// Gate a firmware clock on or off.
|
||||
bool SetClockState(uint32_t clockId, bool on);
|
||||
|
||||
// The firmware's GPIO expander lines are numbered from 128.
|
||||
constexpr uint32_t EXPGPIO_BASE = 128;
|
||||
constexpr uint32_t EXPGPIO_BT_ON = EXPGPIO_BASE + 0; // Bluetooth power (high = on)
|
||||
constexpr uint32_t EXPGPIO_WL_ON = EXPGPIO_BASE + 1; // Wi-Fi power (high = on)
|
||||
|
||||
bool SetGpioState(uint32_t gpio, bool high);
|
||||
// Configure the line as an output, then drive it (SET_GPIO_CONFIG).
|
||||
bool SetGpioOutput(uint32_t gpio, bool high);
|
||||
bool GetGpioState(uint32_t gpio, bool& high);
|
||||
|
||||
// The board's Ethernet MAC address from OTP, in transmission order
|
||||
// (mac[0] is the first octet of the OUI).
|
||||
bool GetBoardMacAddress(uint8_t mac[6]);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* RpiGpio.cpp
|
||||
* BCM2711 GPIO pin function and pull control
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "RpiGpio.hpp"
|
||||
#include <ACPI/AcpiDevices.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Platform::RpiGpio {
|
||||
|
||||
// BCM2711 ARM Peripherals, chapter 5
|
||||
static constexpr uint32_t REG_GPFSEL0 = 0x00; // 10 pins per register, 3 bits each
|
||||
static constexpr uint32_t REG_PUP_PDN_CNTRL0 = 0xE4; // 16 pins per register, 2 bits each
|
||||
static constexpr uint32_t PinCount = 58;
|
||||
|
||||
static volatile uint8_t* g_mmio = nullptr;
|
||||
static bool g_probed = false;
|
||||
static kcp::Spinlock g_lock;
|
||||
|
||||
static bool Initialize() {
|
||||
if (g_probed) return g_mmio != nullptr;
|
||||
g_probed = true;
|
||||
|
||||
Hal::AcpiDevices::DeviceInfo info{};
|
||||
if (!Hal::AcpiDevices::FindDeviceByHid("BCM2845", info)) return false;
|
||||
|
||||
Hal::AML::ResourceList resources;
|
||||
uint64_t base = 0, length = 0;
|
||||
if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) ||
|
||||
!resources.GetMemory(base, length) || base == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t page = base & ~0xFFFULL;
|
||||
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
|
||||
g_mmio = (volatile uint8_t*)Memory::HHDM(base);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void Modify(uint32_t offset, uint32_t mask, uint32_t value) {
|
||||
g_lock.Acquire();
|
||||
volatile uint32_t* reg = (volatile uint32_t*)(g_mmio + offset);
|
||||
*reg = (*reg & ~mask) | (value & mask);
|
||||
g_lock.Release();
|
||||
}
|
||||
|
||||
bool SetFunction(uint32_t pin, Function function) {
|
||||
if (pin >= PinCount || !Initialize()) return false;
|
||||
uint32_t shift = (pin % 10) * 3;
|
||||
Modify(REG_GPFSEL0 + (pin / 10) * 4, 7u << shift, (uint32_t)function << shift);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SetPull(uint32_t pin, Pull pull) {
|
||||
if (pin >= PinCount || !Initialize()) return false;
|
||||
uint32_t shift = (pin % 16) * 2;
|
||||
Modify(REG_PUP_PDN_CNTRL0 + (pin / 16) * 4, 3u << shift, (uint32_t)pull << shift);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* RpiGpio.hpp
|
||||
* BCM2711 GPIO pin function and pull control
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// The SoC's own GPIO block (ACPI BCM2845, \_SB.GDV0.GPI0), used to route
|
||||
// pins to the peripheral that drives them. On Linux the device tree's
|
||||
// pinctrl does this; booted through ACPI, nothing does it for us. Only the
|
||||
// BCM2711 (Raspberry Pi 4/400) pull-control layout is implemented.
|
||||
|
||||
namespace Drivers::Platform::RpiGpio {
|
||||
|
||||
enum class Function : uint8_t {
|
||||
Input = 0b000,
|
||||
Output = 0b001,
|
||||
Alt0 = 0b100,
|
||||
Alt1 = 0b101,
|
||||
Alt2 = 0b110,
|
||||
Alt3 = 0b111,
|
||||
Alt4 = 0b011,
|
||||
Alt5 = 0b010,
|
||||
};
|
||||
|
||||
enum class Pull : uint8_t {
|
||||
None = 0b00,
|
||||
Up = 0b01,
|
||||
Down = 0b10,
|
||||
};
|
||||
|
||||
bool SetFunction(uint32_t pin, Function function);
|
||||
bool SetPull(uint32_t pin, Pull pull);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* BtUart.cpp
|
||||
* Interrupt-driven UART to an on-board Bluetooth controller
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "BtUart.hpp"
|
||||
#include <ACPI/GsiRouting.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <atomic>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Serial::BtUart {
|
||||
|
||||
// =========================================================================
|
||||
// Registers
|
||||
// =========================================================================
|
||||
|
||||
// Mini UART, offsets from the AUX block (BCM2711 ARM Peripherals 2.2,
|
||||
// with the elinux.org BCM2835 datasheet errata: IER bit 0 enables the
|
||||
// receive interrupt and bit 1 the transmit one, IER bits 3:2 must be set
|
||||
// for interrupts to be raised, LCR needs 0b11 for 8 data bits).
|
||||
static constexpr uint32_t AUX_ENABLES = 0x04;
|
||||
static constexpr uint32_t MU_IO = 0x40;
|
||||
static constexpr uint32_t MU_IER = 0x44;
|
||||
static constexpr uint32_t MU_IIR = 0x48;
|
||||
static constexpr uint32_t MU_LCR = 0x4C;
|
||||
static constexpr uint32_t MU_MCR = 0x50;
|
||||
static constexpr uint32_t MU_LSR = 0x54;
|
||||
static constexpr uint32_t MU_CNTL = 0x60;
|
||||
static constexpr uint32_t MU_BAUD = 0x68;
|
||||
|
||||
static constexpr uint32_t AUX_ENABLE_MU = 1u << 0;
|
||||
static constexpr uint32_t MU_IER_RX = 1u << 0;
|
||||
static constexpr uint32_t MU_IER_TX = 1u << 1;
|
||||
static constexpr uint32_t MU_IER_REQUIRED = 3u << 2;
|
||||
static constexpr uint32_t MU_IIR_FLUSH = 0xC6; // clear both FIFOs, FIFOs on
|
||||
static constexpr uint32_t MU_LCR_8BIT = 0x03;
|
||||
static constexpr uint32_t MU_MCR_RTS = 1u << 1; // RTS pin low (asserted)
|
||||
static constexpr uint32_t MU_LSR_DATA_READY = 1u << 0;
|
||||
static constexpr uint32_t MU_LSR_OVERRUN = 1u << 1;
|
||||
static constexpr uint32_t MU_LSR_TX_SPACE = 1u << 5; // FIFO accepts a byte
|
||||
static constexpr uint32_t MU_LSR_TX_IDLE = 1u << 6; // FIFO empty, shifter idle
|
||||
static constexpr uint32_t MU_CNTL_RXEN = 1u << 0;
|
||||
static constexpr uint32_t MU_CNTL_TXEN = 1u << 1;
|
||||
// Auto RTS at the default level (deassert with 3 FIFO spaces left).
|
||||
// Bit 6 picks the level auto RTS asserts: set = low, clear (the reset
|
||||
// value) = high. A UART's RTS is active low, so it must be set: left
|
||||
// clear, RTS reads "stop" to the controller the moment auto RTS is on.
|
||||
// Bits 3 and 7 are the same for CTS: on a Pi 400, with bit 7 clear the
|
||||
// transmitter held its bytes while the controller drove CTS low (ready).
|
||||
static constexpr uint32_t MU_CNTL_AUTORTS = 1u << 2;
|
||||
static constexpr uint32_t MU_CNTL_AUTOCTS = 1u << 3;
|
||||
static constexpr uint32_t MU_CNTL_RTS_LOW = 1u << 6;
|
||||
static constexpr uint32_t MU_CNTL_CTS_LOW = 1u << 7;
|
||||
|
||||
// PL011 (ARM PrimeCell UART TRM)
|
||||
static constexpr uint32_t PL_DR = 0x00;
|
||||
static constexpr uint32_t PL_FR = 0x18;
|
||||
static constexpr uint32_t PL_IBRD = 0x24;
|
||||
static constexpr uint32_t PL_FBRD = 0x28;
|
||||
static constexpr uint32_t PL_LCRH = 0x2C;
|
||||
static constexpr uint32_t PL_CR = 0x30;
|
||||
static constexpr uint32_t PL_IFLS = 0x34;
|
||||
static constexpr uint32_t PL_IMSC = 0x38;
|
||||
static constexpr uint32_t PL_ICR = 0x44;
|
||||
|
||||
static constexpr uint32_t PL_DR_OE = 1u << 11;
|
||||
static constexpr uint32_t PL_FR_BUSY = 1u << 3;
|
||||
static constexpr uint32_t PL_FR_RXFE = 1u << 4;
|
||||
static constexpr uint32_t PL_FR_TXFF = 1u << 5;
|
||||
static constexpr uint32_t PL_FR_TXFE = 1u << 7;
|
||||
static constexpr uint32_t PL_LCRH_FEN = 1u << 4;
|
||||
static constexpr uint32_t PL_LCRH_8BIT = 3u << 5;
|
||||
static constexpr uint32_t PL_CR_UARTEN = 1u << 0;
|
||||
static constexpr uint32_t PL_CR_TXE = 1u << 8;
|
||||
static constexpr uint32_t PL_CR_RXE = 1u << 9;
|
||||
static constexpr uint32_t PL_CR_RTS = 1u << 11;
|
||||
static constexpr uint32_t PL_CR_RTSEN = 1u << 14;
|
||||
static constexpr uint32_t PL_CR_CTSEN = 1u << 15;
|
||||
static constexpr uint32_t PL_INT_RX = 1u << 4;
|
||||
static constexpr uint32_t PL_INT_TX = 1u << 5;
|
||||
static constexpr uint32_t PL_INT_RT = 1u << 6; // receive timeout
|
||||
static constexpr uint32_t PL_INT_OE = 1u << 10;
|
||||
static constexpr uint32_t PL_IFLS_RX_HALF = 2u << 3;
|
||||
static constexpr uint32_t PL_IFLS_TX_EIGHTH = 0;
|
||||
|
||||
// =========================================================================
|
||||
// State
|
||||
// =========================================================================
|
||||
|
||||
static constexpr uint32_t RX_RING_SIZE = 16384; // powers of two
|
||||
static constexpr uint32_t TX_RING_SIZE = 16384;
|
||||
|
||||
static Kind g_kind = Kind::MiniUart;
|
||||
static volatile uint8_t* g_mmio = nullptr;
|
||||
static uint32_t g_clockHz = 0;
|
||||
static uint32_t g_baud = 0;
|
||||
static bool g_flowControl = false;
|
||||
static bool g_interrupt = false;
|
||||
static bool g_initialized = false;
|
||||
|
||||
// Guards the hardware and the transmit ring: taken by the IRQ handler,
|
||||
// Service() and every producer, with interrupts off.
|
||||
static kcp::Spinlock g_hwLock;
|
||||
|
||||
// Receive ring: produced under g_hwLock, consumed by Read() (one reader).
|
||||
static uint8_t g_rxRing[RX_RING_SIZE];
|
||||
static std::atomic<uint32_t> g_rxHead{0};
|
||||
static std::atomic<uint32_t> g_rxTail{0};
|
||||
|
||||
// Transmit ring: entirely under g_hwLock. g_txCount mirrors the fill for
|
||||
// lock-free readers.
|
||||
static uint8_t g_txRing[TX_RING_SIZE];
|
||||
static uint32_t g_txHead = 0;
|
||||
static uint32_t g_txTail = 0;
|
||||
static std::atomic<uint32_t> g_txCount{0};
|
||||
static bool g_txIrqEnabled = false;
|
||||
|
||||
static Stats g_stats = {};
|
||||
|
||||
static inline uint32_t Read32(uint32_t offset) {
|
||||
return *(volatile uint32_t*)(g_mmio + offset);
|
||||
}
|
||||
|
||||
static inline void Write32(uint32_t offset, uint32_t value) {
|
||||
*(volatile uint32_t*)(g_mmio + offset) = value;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FIFO service (caller holds g_hwLock)
|
||||
// =========================================================================
|
||||
|
||||
static inline void PushRx(uint8_t byte) {
|
||||
uint32_t head = g_rxHead.load(std::memory_order_relaxed);
|
||||
if (head - g_rxTail.load(std::memory_order_acquire) >= RX_RING_SIZE) {
|
||||
g_stats.rxDropped++;
|
||||
return;
|
||||
}
|
||||
g_rxRing[head & (RX_RING_SIZE - 1)] = byte;
|
||||
g_rxHead.store(head + 1, std::memory_order_release);
|
||||
g_stats.rxBytes++;
|
||||
}
|
||||
|
||||
static bool DrainRxLocked() {
|
||||
bool any = false;
|
||||
if (g_kind == Kind::MiniUart) {
|
||||
for (;;) {
|
||||
uint32_t lsr = Read32(MU_LSR);
|
||||
if (lsr & MU_LSR_OVERRUN) g_stats.overruns++;
|
||||
if (!(lsr & MU_LSR_DATA_READY)) break;
|
||||
PushRx((uint8_t)Read32(MU_IO));
|
||||
any = true;
|
||||
}
|
||||
} else {
|
||||
while (!(Read32(PL_FR) & PL_FR_RXFE)) {
|
||||
uint32_t data = Read32(PL_DR);
|
||||
if (data & PL_DR_OE) g_stats.overruns++;
|
||||
PushRx((uint8_t)data);
|
||||
any = true;
|
||||
}
|
||||
Write32(PL_ICR, PL_INT_RX | PL_INT_RT | PL_INT_OE);
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
static void SetTxIrqLocked(bool on) {
|
||||
if (on == g_txIrqEnabled || !g_interrupt) return;
|
||||
g_txIrqEnabled = on;
|
||||
if (g_kind == Kind::MiniUart) {
|
||||
Write32(MU_IER, MU_IER_RX | MU_IER_REQUIRED | (on ? MU_IER_TX : 0));
|
||||
} else {
|
||||
Write32(PL_IMSC, PL_INT_RX | PL_INT_RT | PL_INT_OE | (on ? PL_INT_TX : 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Move queued bytes into the transmit FIFO. The transmit interrupt stays
|
||||
// enabled exactly while bytes remain queued: the PL011's only fires on
|
||||
// crossing its FIFO threshold, so the FIFO must be refilled here first.
|
||||
static void FillTxLocked() {
|
||||
while (g_txHead != g_txTail) {
|
||||
bool space = (g_kind == Kind::MiniUart)
|
||||
? (Read32(MU_LSR) & MU_LSR_TX_SPACE) != 0
|
||||
: (Read32(PL_FR) & PL_FR_TXFF) == 0;
|
||||
if (!space) break;
|
||||
uint8_t byte = g_txRing[g_txTail & (TX_RING_SIZE - 1)];
|
||||
g_txTail++;
|
||||
Write32(g_kind == Kind::MiniUart ? MU_IO : PL_DR, byte);
|
||||
g_stats.txBytes++;
|
||||
}
|
||||
g_txCount.store(g_txHead - g_txTail, std::memory_order_release);
|
||||
if (g_kind == Kind::Pl011) Write32(PL_ICR, PL_INT_TX);
|
||||
SetTxIrqLocked(g_txHead != g_txTail);
|
||||
}
|
||||
|
||||
static bool TransmitterIdleLocked() {
|
||||
if (g_txHead != g_txTail) return false;
|
||||
if (g_kind == Kind::MiniUart) return (Read32(MU_LSR) & MU_LSR_TX_IDLE) != 0;
|
||||
uint32_t fr = Read32(PL_FR);
|
||||
return (fr & PL_FR_TXFE) && !(fr & PL_FR_BUSY);
|
||||
}
|
||||
|
||||
static void HandleInterrupt(uint8_t, bool fromUser) {
|
||||
g_hwLock.Acquire();
|
||||
g_stats.interrupts++;
|
||||
bool received = DrainRxLocked();
|
||||
FillTxLocked();
|
||||
g_hwLock.Release();
|
||||
|
||||
// Received bytes are parsed in process context. When this interrupt
|
||||
// landed in user mode, ask for an idle-context pass at once (as the
|
||||
// timer does for USB) instead of waiting for the core to go idle.
|
||||
if (received && fromUser) Sched::RequestKernelWork();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Baud rate (caller holds g_hwLock, transmitter idle)
|
||||
// =========================================================================
|
||||
|
||||
static bool WithinTolerance(uint64_t actual, uint32_t wanted) {
|
||||
uint64_t diff = actual > wanted ? actual - wanted : wanted - actual;
|
||||
return diff * 40 <= wanted; // 2.5%
|
||||
}
|
||||
|
||||
// The divisor for `baud`: mini UART, baud = clock / (8 * divisor) (the
|
||||
// register holds divisor - 1); PL011, clock / (16 * baud) in 16.6 fixed
|
||||
// point (IBRD = divisor >> 6, FBRD = divisor & 63).
|
||||
static bool ComputeDivisor(uint32_t baud, uint64_t& divisor) {
|
||||
if (baud == 0 || g_clockHz == 0) return false;
|
||||
if (g_kind == Kind::MiniUart) {
|
||||
divisor = ((uint64_t)g_clockHz + 4ull * baud) / (8ull * baud);
|
||||
if (divisor < 1 || divisor > 0x10000) return false;
|
||||
return WithinTolerance((uint64_t)g_clockHz / (8 * divisor), baud);
|
||||
}
|
||||
divisor = ((uint64_t)g_clockHz * 8 / baud + 1) / 2;
|
||||
if ((divisor >> 6) < 1 || (divisor >> 6) > 0xFFFF) return false;
|
||||
return WithinTolerance((uint64_t)g_clockHz * 4 / divisor, baud);
|
||||
}
|
||||
|
||||
static bool ProgramBaudLocked(uint32_t baud) {
|
||||
uint64_t divisor = 0;
|
||||
if (!ComputeDivisor(baud, divisor)) return false;
|
||||
|
||||
if (g_kind == Kind::MiniUart) {
|
||||
uint32_t cntl = Read32(MU_CNTL);
|
||||
Write32(MU_CNTL, 0);
|
||||
Write32(MU_BAUD, (uint32_t)(divisor - 1));
|
||||
Write32(MU_CNTL, cntl);
|
||||
} else {
|
||||
uint32_t ibrd = (uint32_t)(divisor >> 6);
|
||||
uint32_t fbrd = (uint32_t)(divisor & 63);
|
||||
uint32_t cr = Read32(PL_CR);
|
||||
Write32(PL_CR, 0);
|
||||
for (int i = 0; i < 100000 && (Read32(PL_FR) & PL_FR_BUSY); i++) Arch::Pause();
|
||||
Write32(PL_LCRH, 0); // flush the FIFOs
|
||||
Write32(PL_IBRD, ibrd);
|
||||
Write32(PL_FBRD, fbrd);
|
||||
Write32(PL_LCRH, PL_LCRH_8BIT | PL_LCRH_FEN); // latches IBRD/FBRD
|
||||
Write32(PL_CR, cr);
|
||||
}
|
||||
g_baud = baud;
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Public API
|
||||
// =========================================================================
|
||||
|
||||
bool Initialize(const Config& config) {
|
||||
if (g_initialized) return true;
|
||||
if (config.mmioPhys == 0 || config.clockHz == 0) return false;
|
||||
|
||||
g_kind = config.kind;
|
||||
g_clockHz = config.clockHz;
|
||||
|
||||
// The mini UART lives 0x40 into the AUX block; accept either base.
|
||||
uint64_t base = config.mmioPhys;
|
||||
if (g_kind == Kind::MiniUart && (base & 0xFF) == MU_IO) base -= MU_IO;
|
||||
uint64_t page = base & ~0xFFFULL;
|
||||
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
|
||||
g_mmio = (volatile uint8_t*)Memory::HHDM(base);
|
||||
|
||||
g_hwLock.Acquire();
|
||||
if (g_kind == Kind::MiniUart) {
|
||||
// AUX_ENABLES also gates SPI1/SPI2; leave their bits alone.
|
||||
Write32(AUX_ENABLES, Read32(AUX_ENABLES) | AUX_ENABLE_MU);
|
||||
Write32(MU_CNTL, 0);
|
||||
Write32(MU_IER, 0);
|
||||
Write32(MU_LCR, MU_LCR_8BIT);
|
||||
Write32(MU_MCR, MU_MCR_RTS);
|
||||
Write32(MU_IIR, MU_IIR_FLUSH);
|
||||
Write32(MU_CNTL, MU_CNTL_RXEN | MU_CNTL_TXEN);
|
||||
} else {
|
||||
Write32(PL_CR, 0);
|
||||
Write32(PL_IMSC, 0);
|
||||
Write32(PL_ICR, 0x7FF);
|
||||
Write32(PL_IFLS, PL_IFLS_RX_HALF | PL_IFLS_TX_EIGHTH);
|
||||
Write32(PL_LCRH, PL_LCRH_8BIT | PL_LCRH_FEN);
|
||||
Write32(PL_CR, PL_CR_UARTEN | PL_CR_TXE | PL_CR_RXE | PL_CR_RTS);
|
||||
}
|
||||
bool baudOk = ProgramBaudLocked(115200);
|
||||
while (DrainRxLocked()) {}
|
||||
g_rxTail.store(g_rxHead.load(std::memory_order_relaxed), std::memory_order_release);
|
||||
g_hwLock.Release();
|
||||
|
||||
if (!baudOk) {
|
||||
KernelLogStream(ERROR, "BtUart") << "115200 baud unreachable from a "
|
||||
<< (uint64_t)g_clockHz << " Hz clock";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.gsi != 0) {
|
||||
uint8_t line = Hal::GsiRouting::LineForGsi(config.gsi, config.levelTriggered,
|
||||
config.activeLow);
|
||||
if (line != 0) {
|
||||
Arch::RegisterIrqHandler(line, HandleInterrupt);
|
||||
g_interrupt = true;
|
||||
g_hwLock.Acquire();
|
||||
if (g_kind == Kind::MiniUart) {
|
||||
Write32(MU_IER, MU_IER_RX | MU_IER_REQUIRED);
|
||||
} else {
|
||||
Write32(PL_IMSC, PL_INT_RX | PL_INT_RT | PL_INT_OE);
|
||||
}
|
||||
g_hwLock.Release();
|
||||
Arch::EnableLegacyIrq(line);
|
||||
}
|
||||
}
|
||||
|
||||
g_initialized = true;
|
||||
KernelLogStream(OK, "BtUart") << (g_kind == Kind::MiniUart ? "Mini UART" : "PL011")
|
||||
<< " at 0x" << base::hex << base << base::dec << ", " << (uint64_t)g_clockHz
|
||||
<< " Hz clock, " << (g_interrupt ? "interrupt driven" : "polled (no interrupt)");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsInitialized() { return g_initialized; }
|
||||
bool HasInterrupt() { return g_interrupt; }
|
||||
uint32_t GetBaud() { return g_baud; }
|
||||
Kind GetKind() { return g_kind; }
|
||||
bool FlowControl() { return g_flowControl; }
|
||||
|
||||
bool BaudReachable(uint32_t baud) {
|
||||
uint64_t divisor = 0;
|
||||
return g_initialized && ComputeDivisor(baud, divisor);
|
||||
}
|
||||
|
||||
bool SetBaud(uint32_t baud) {
|
||||
if (!g_initialized) return false;
|
||||
WaitTxIdle(1000);
|
||||
g_hwLock.Acquire();
|
||||
bool ok = ProgramBaudLocked(baud);
|
||||
g_hwLock.Release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
void SetFlowControl(bool on) {
|
||||
if (!g_initialized) return;
|
||||
g_hwLock.Acquire();
|
||||
if (g_kind == Kind::MiniUart) {
|
||||
uint32_t cntl = MU_CNTL_RXEN | MU_CNTL_TXEN
|
||||
| (on ? MU_CNTL_AUTORTS | MU_CNTL_RTS_LOW
|
||||
| MU_CNTL_AUTOCTS | MU_CNTL_CTS_LOW : 0);
|
||||
Write32(MU_CNTL, cntl);
|
||||
// With auto RTS off, hold RTS asserted by hand.
|
||||
Write32(MU_MCR, MU_MCR_RTS);
|
||||
} else {
|
||||
uint32_t cr = Read32(PL_CR) & ~(PL_CR_RTSEN | PL_CR_CTSEN);
|
||||
if (on) cr |= PL_CR_RTSEN | PL_CR_CTSEN;
|
||||
Write32(PL_CR, cr | PL_CR_RTS);
|
||||
}
|
||||
g_flowControl = on;
|
||||
g_hwLock.Release();
|
||||
}
|
||||
|
||||
void DeassertRts() {
|
||||
if (!g_initialized) return;
|
||||
g_hwLock.Acquire();
|
||||
if (g_kind == Kind::MiniUart) {
|
||||
Write32(MU_CNTL, MU_CNTL_RXEN | MU_CNTL_TXEN);
|
||||
Write32(MU_MCR, 0);
|
||||
} else {
|
||||
Write32(PL_CR, Read32(PL_CR) & ~(PL_CR_RTSEN | PL_CR_CTSEN | PL_CR_RTS));
|
||||
}
|
||||
g_flowControl = false;
|
||||
g_hwLock.Release();
|
||||
}
|
||||
|
||||
int Read(uint8_t* buffer, int maxLength) {
|
||||
if (buffer == nullptr || maxLength <= 0) return 0;
|
||||
uint32_t tail = g_rxTail.load(std::memory_order_relaxed);
|
||||
uint32_t head = g_rxHead.load(std::memory_order_acquire);
|
||||
uint32_t available = head - tail;
|
||||
uint32_t n = available < (uint32_t)maxLength ? available : (uint32_t)maxLength;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
buffer[i] = g_rxRing[(tail + i) & (RX_RING_SIZE - 1)];
|
||||
}
|
||||
g_rxTail.store(tail + n, std::memory_order_release);
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
bool HasRxData() {
|
||||
if (!g_initialized) return false;
|
||||
if (g_rxHead.load(std::memory_order_acquire) != g_rxTail.load(std::memory_order_relaxed))
|
||||
return true;
|
||||
// Polled: nothing fills the ring until Service() runs, so look at
|
||||
// the FIFO itself.
|
||||
if (g_interrupt) return false;
|
||||
return g_kind == Kind::MiniUart ? (Read32(MU_LSR) & MU_LSR_DATA_READY) != 0
|
||||
: (Read32(PL_FR) & PL_FR_RXFE) == 0;
|
||||
}
|
||||
|
||||
void FlushRx() {
|
||||
if (!g_initialized) return;
|
||||
g_hwLock.Acquire();
|
||||
while (DrainRxLocked()) {}
|
||||
g_rxTail.store(g_rxHead.load(std::memory_order_relaxed), std::memory_order_release);
|
||||
g_hwLock.Release();
|
||||
}
|
||||
|
||||
bool Write(const uint8_t* head, uint32_t headLength,
|
||||
const uint8_t* body, uint32_t bodyLength) {
|
||||
if (body == nullptr) bodyLength = 0;
|
||||
uint32_t length = headLength + bodyLength;
|
||||
if (!g_initialized || length == 0) return length == 0;
|
||||
if (length > TX_RING_SIZE) return false;
|
||||
g_hwLock.Acquire();
|
||||
if (TX_RING_SIZE - (g_txHead - g_txTail) < length) {
|
||||
g_hwLock.Release();
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < headLength; i++) {
|
||||
g_txRing[(g_txHead + i) & (TX_RING_SIZE - 1)] = head[i];
|
||||
}
|
||||
for (uint32_t i = 0; i < bodyLength; i++) {
|
||||
g_txRing[(g_txHead + headLength + i) & (TX_RING_SIZE - 1)] = body[i];
|
||||
}
|
||||
g_txHead += length;
|
||||
FillTxLocked();
|
||||
g_hwLock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t TxFree() {
|
||||
return TX_RING_SIZE - g_txCount.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
uint32_t TxPending() {
|
||||
return g_txCount.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool WaitTxIdle(uint32_t timeoutMs) {
|
||||
if (!g_initialized) return false;
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
for (;;) {
|
||||
g_hwLock.Acquire();
|
||||
FillTxLocked();
|
||||
bool idle = TransmitterIdleLocked();
|
||||
g_hwLock.Release();
|
||||
if (idle) return true;
|
||||
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||
Arch::Pause();
|
||||
}
|
||||
}
|
||||
|
||||
void Service() {
|
||||
if (!g_initialized) return;
|
||||
g_hwLock.Acquire();
|
||||
DrainRxLocked();
|
||||
FillTxLocked();
|
||||
g_hwLock.Release();
|
||||
}
|
||||
|
||||
void GetStats(Stats& out) {
|
||||
g_hwLock.Acquire();
|
||||
out = g_stats;
|
||||
g_hwLock.Release();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* BtUart.hpp
|
||||
* Interrupt-driven UART to an on-board Bluetooth controller
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// A byte pipe to a peripheral wired to one of the Broadcom SoC's UARTs --
|
||||
// on the Raspberry Pi 4/400 the Bluetooth half of the CYW4345x radio.
|
||||
// Unlike the console (Uart.cpp: polled, transmit only) this one receives,
|
||||
// so it is interrupt driven: the hard IRQ moves bytes between the FIFOs and
|
||||
// two software rings, and everything else runs in process context.
|
||||
//
|
||||
// Two UARTs can carry it:
|
||||
// - the mini UART (BCM2835 AUX, 8-byte FIFOs, baud from the VPU core
|
||||
// clock), which is where Bluetooth sits with the miniuart-bt overlay
|
||||
// the Pi firmware task force's UEFI ships;
|
||||
// - the PL011 (32-byte FIFOs, fixed 48 MHz reference clock).
|
||||
// Pin routing (GPIO 30-33) is the caller's job.
|
||||
|
||||
namespace Drivers::Serial::BtUart {
|
||||
|
||||
enum class Kind : uint8_t {
|
||||
MiniUart,
|
||||
Pl011,
|
||||
};
|
||||
|
||||
struct Config {
|
||||
Kind kind;
|
||||
uint64_t mmioPhys; // the block ACPI describes (mini UART: the AUX block)
|
||||
uint32_t clockHz; // UART reference clock
|
||||
uint32_t gsi; // 0 = no interrupt, poll from Service()
|
||||
bool levelTriggered;
|
||||
bool activeLow;
|
||||
};
|
||||
|
||||
// Program the UART to 115200 8N1 with RTS asserted and start receiving.
|
||||
bool Initialize(const Config& config);
|
||||
bool IsInitialized();
|
||||
|
||||
// Whether the receive path is interrupt driven. Without it, bytes only
|
||||
// move when Service() runs, so a caller should keep the baud rate low.
|
||||
bool HasInterrupt();
|
||||
|
||||
// Change the baud rate. Waits for queued output to drain first, since
|
||||
// the peer is about to switch too. Returns false if the rate is
|
||||
// unreachable from the reference clock (more than 2.5% off).
|
||||
bool SetBaud(uint32_t baud);
|
||||
bool BaudReachable(uint32_t baud);
|
||||
uint32_t GetBaud();
|
||||
Kind GetKind();
|
||||
|
||||
// Hardware flow control: deassert RTS while the receive FIFO is nearly
|
||||
// full so the peer pauses instead of overrunning it, and hold our
|
||||
// transmitter while the peer deasserts CTS.
|
||||
void SetFlowControl(bool on);
|
||||
bool FlowControl();
|
||||
|
||||
// Drive RTS deasserted (and stop auto RTS) until the next
|
||||
// SetFlowControl. The peer may sample it at power-on: a Broadcom
|
||||
// controller that sees its CTS asserted then boots into autobaud mode.
|
||||
void DeassertRts();
|
||||
|
||||
// Receive ring (single consumer). Read returns the number of bytes copied.
|
||||
int Read(uint8_t* buffer, int maxLength);
|
||||
bool HasRxData();
|
||||
void FlushRx();
|
||||
|
||||
// Queue bytes for transmission: all of them, or none (false) when the
|
||||
// transmit ring lacks room. Safe from several producers at once; each
|
||||
// call's bytes (head, then body) stay contiguous on the wire.
|
||||
bool Write(const uint8_t* head, uint32_t headLength,
|
||||
const uint8_t* body = nullptr, uint32_t bodyLength = 0);
|
||||
uint32_t TxFree();
|
||||
uint32_t TxPending();
|
||||
|
||||
// Wait until everything queued has left the transmitter.
|
||||
bool WaitTxIdle(uint32_t timeoutMs);
|
||||
|
||||
// Move bytes between the FIFOs and the rings from process context: the
|
||||
// whole receive path when there is no interrupt, and a safety net when
|
||||
// there is one.
|
||||
void Service();
|
||||
|
||||
struct Stats {
|
||||
uint64_t rxBytes;
|
||||
uint64_t txBytes;
|
||||
uint64_t rxDropped; // receive ring full
|
||||
uint64_t overruns; // receive FIFO overflowed in hardware
|
||||
uint64_t interrupts;
|
||||
};
|
||||
void GetStats(Stats& out);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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 uint64_t g_consoleAddress = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
g_consoleAddress = reg.Address;
|
||||
AttachSerialConsole(sink);
|
||||
KernelLogStream(OK, "Serial") << "Console on " << name << " at " << (isIo ? "port " : "")
|
||||
<< "0x" << base::hex << reg.Address;
|
||||
}
|
||||
|
||||
uint64_t ConsoleAddress() {
|
||||
return g_consoleAddress;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
// Physical address of the console UART's registers, or 0 when there is
|
||||
// none (so another driver does not take over the console's UART).
|
||||
uint64_t ConsoleAddress();
|
||||
|
||||
}
|
||||
@@ -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,376 @@
|
||||
/*
|
||||
* 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 ResetChildPort(uint8_t hubSlot, uint8_t port, uint32_t& speed) {
|
||||
if (hubSlot == 0 || hubSlot > Xhci::MAX_SLOTS || !g_hubs[hubSlot].Active) return false;
|
||||
return ResetPort(hubSlot, port, g_hubs[hubSlot].Info.SuperSpeed, speed);
|
||||
}
|
||||
|
||||
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,61 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
// Reset a hub port again (an enumeration retry) and report the speed the
|
||||
// device came up at. False if the hub is gone or the reset failed.
|
||||
bool ResetChildPort(uint8_t hubSlot, uint8_t port, uint32_t& speed);
|
||||
|
||||
// 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();
|
||||
|
||||
}
|
||||
@@ -51,6 +51,16 @@ namespace Drivers::USB::MassStorage {
|
||||
|
||||
constexpr uint8_t SERVICE_READ_CAPACITY16 = 0x10;
|
||||
|
||||
constexpr uint8_t SENSE_NOT_READY = 0x02;
|
||||
constexpr uint8_t SENSE_UNIT_ATTENTION = 0x06;
|
||||
constexpr uint8_t ASC_MEDIUM_NOT_PRESENT = 0x3A;
|
||||
|
||||
// TEST UNIT READY attempts, 100 ms apart: any failure gets a few, a unit
|
||||
// that says it is coming up (see TestUnitReady) gets about five seconds.
|
||||
constexpr int TUR_TRIES = 5;
|
||||
constexpr int TUR_SETTLE_TRIES = 50;
|
||||
constexpr int TUR_NO_MEDIUM_TRIES = 3;
|
||||
|
||||
constexpr uint32_t MAX_BULK_TRANSFER_BYTES = 0x10000;
|
||||
constexpr int MAX_MSC_DEVICES = 16;
|
||||
|
||||
@@ -275,6 +285,15 @@ namespace Drivers::USB::MassStorage {
|
||||
return maxLun;
|
||||
}
|
||||
|
||||
// A failed stage leaves its endpoint halted, or after a timeout its TRB
|
||||
// still queued; either way the next transfer would fail too.
|
||||
static uint32_t BulkStage(Device& dev, bool dirIn, void* data, uint64_t dataPhys,
|
||||
uint32_t length, uint32_t* actual) {
|
||||
uint32_t cc = Xhci::BulkTransfer(dev.SlotId, dirIn, data, dataPhys, length, actual);
|
||||
if (!IsGoodCompletion(cc)) Xhci::RecoverBulkEndpoint(dev.SlotId, dirIn);
|
||||
return cc;
|
||||
}
|
||||
|
||||
static bool ScsiCommand(Device& dev, const uint8_t* cdb, uint8_t cdbLen,
|
||||
void* data, uint64_t dataPhys, uint32_t dataLen,
|
||||
bool dataIn) {
|
||||
@@ -295,24 +314,34 @@ namespace Drivers::USB::MassStorage {
|
||||
memcpy(cbw->CommandBlock, cdb, cdbLen);
|
||||
|
||||
uint32_t actual = 0;
|
||||
uint32_t cc = Xhci::BulkTransfer(dev.SlotId, false, dev.CbwBuf, dev.CbwPhys,
|
||||
sizeof(CommandBlockWrapper), &actual);
|
||||
uint32_t cc = BulkStage(dev, false, dev.CbwBuf, dev.CbwPhys,
|
||||
sizeof(CommandBlockWrapper), &actual);
|
||||
if (!IsGoodCompletion(cc) || actual != sizeof(CommandBlockWrapper)) {
|
||||
ReleaseTransport();
|
||||
return false;
|
||||
}
|
||||
|
||||
// A device that fails the command, or has less data than asked for,
|
||||
// may STALL the data stage; the status still follows in the CSW once
|
||||
// the halt is cleared (BOT 6.7.2, 6.7.3). Card readers do this for
|
||||
// commands on a slot that is not ready.
|
||||
if (dataLen > 0) {
|
||||
cc = Xhci::BulkTransfer(dev.SlotId, dataIn, data, dataPhys, dataLen, &actual);
|
||||
if (!IsGoodCompletion(cc)) {
|
||||
cc = BulkStage(dev, dataIn, data, dataPhys, dataLen, &actual);
|
||||
if (!IsGoodCompletion(cc) && cc != Xhci::CC_STALL_ERROR) {
|
||||
ReleaseTransport();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// A STALL on the CSW itself is cleared and the CSW read again once
|
||||
// (BOT 5.3.3, figure 2).
|
||||
memset(dev.CswBuf, 0, 0x1000);
|
||||
cc = Xhci::BulkTransfer(dev.SlotId, true, dev.CswBuf, dev.CswPhys,
|
||||
sizeof(CommandStatusWrapper), &actual);
|
||||
cc = BulkStage(dev, true, dev.CswBuf, dev.CswPhys,
|
||||
sizeof(CommandStatusWrapper), &actual);
|
||||
if (cc == Xhci::CC_STALL_ERROR) {
|
||||
cc = BulkStage(dev, true, dev.CswBuf, dev.CswPhys,
|
||||
sizeof(CommandStatusWrapper), &actual);
|
||||
}
|
||||
if (!IsGoodCompletion(cc) || actual < sizeof(CommandStatusWrapper)) {
|
||||
ReleaseTransport();
|
||||
return false;
|
||||
@@ -335,35 +364,66 @@ namespace Drivers::USB::MassStorage {
|
||||
// SCSI commands
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static bool RequestSense(Device& dev) {
|
||||
struct Sense {
|
||||
uint8_t Key;
|
||||
uint8_t Asc;
|
||||
uint8_t Ascq;
|
||||
};
|
||||
|
||||
static bool RequestSense(Device& dev, Sense& out) {
|
||||
uint8_t cdb[6] = {};
|
||||
cdb[0] = SCSI_REQUEST_SENSE;
|
||||
cdb[4] = 18;
|
||||
|
||||
memset(dev.ScratchBuf, 0, 18);
|
||||
bool ok = ScsiCommand(dev, cdb, 6, dev.ScratchBuf, dev.ScratchPhys, 18, true);
|
||||
if (ok) {
|
||||
uint8_t senseKey = dev.ScratchBuf[2] & 0x0F;
|
||||
uint8_t asc = dev.ScratchBuf[12];
|
||||
uint8_t ascq = dev.ScratchBuf[13];
|
||||
KernelLogStream(INFO, "USB-MSC") << "REQUEST SENSE: key="
|
||||
<< (uint64_t)senseKey << " asc=" << base::hex << (uint64_t)asc
|
||||
<< " ascq=" << (uint64_t)ascq << base::dec;
|
||||
if (!ScsiCommand(dev, cdb, 6, dev.ScratchBuf, dev.ScratchPhys, 18, true)) {
|
||||
return false;
|
||||
}
|
||||
return ok;
|
||||
|
||||
out.Key = dev.ScratchBuf[2] & 0x0F;
|
||||
out.Asc = dev.ScratchBuf[12];
|
||||
out.Ascq = dev.ScratchBuf[13];
|
||||
return true;
|
||||
}
|
||||
|
||||
// Right after a reset a unit reports UNIT ATTENTION (power on, medium
|
||||
// changed), and a card reader then reports NOT READY while it brings the
|
||||
// card up, which can take well over a second. Keep asking while the sense
|
||||
// says the unit is on its way; an empty slot (MEDIUM NOT PRESENT) is final.
|
||||
static bool TestUnitReady(Device& dev) {
|
||||
uint8_t cdb[6] = {};
|
||||
cdb[0] = SCSI_TEST_UNIT_READY;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (ScsiCommand(dev, cdb, 6, nullptr, 0, 0, false)) return true;
|
||||
RequestSense(dev);
|
||||
PollWaitMs(50);
|
||||
}
|
||||
int limit = TUR_TRIES;
|
||||
int noMedium = 0;
|
||||
Sense last = {0xFF, 0xFF, 0xFF};
|
||||
|
||||
return false;
|
||||
for (int i = 0; ; i++) {
|
||||
if (ScsiCommand(dev, cdb, 6, nullptr, 0, 0, false)) return true;
|
||||
|
||||
Sense sense = {};
|
||||
if (RequestSense(dev, sense)) {
|
||||
if (sense.Key != last.Key || sense.Asc != last.Asc || sense.Ascq != last.Ascq) {
|
||||
KernelLogStream(INFO, "USB-MSC") << "LUN " << (uint64_t)dev.Lun
|
||||
<< " REQUEST SENSE: key=" << (uint64_t)sense.Key
|
||||
<< " asc=" << base::hex << (uint64_t)sense.Asc
|
||||
<< " ascq=" << (uint64_t)sense.Ascq << base::dec;
|
||||
last = sense;
|
||||
}
|
||||
|
||||
if (sense.Key == SENSE_NOT_READY && sense.Asc == ASC_MEDIUM_NOT_PRESENT) {
|
||||
if (++noMedium >= TUR_NO_MEDIUM_TRIES) return false;
|
||||
} else {
|
||||
noMedium = 0;
|
||||
if (sense.Key == SENSE_NOT_READY || sense.Key == SENSE_UNIT_ATTENTION) {
|
||||
limit = TUR_SETTLE_TRIES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (i + 1 >= limit) return false;
|
||||
PollWaitMs(100);
|
||||
}
|
||||
}
|
||||
|
||||
static bool Inquiry(Device& dev) {
|
||||
@@ -621,7 +681,14 @@ namespace Drivers::USB::MassStorage {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ReadCapacity(dev)) {
|
||||
// Some readers fail the first READ CAPACITY after the unit has
|
||||
// just become ready.
|
||||
bool haveCapacity = false;
|
||||
for (int attempt = 0; attempt < 3 && !haveCapacity; attempt++) {
|
||||
if (attempt > 0) PollWaitMs(100);
|
||||
haveCapacity = ReadCapacity(dev);
|
||||
}
|
||||
if (!haveCapacity) {
|
||||
KernelLogStream(WARNING, "USB-MSC") << "READ CAPACITY failed for LUN "
|
||||
<< (uint64_t)lun;
|
||||
dev.Active = false;
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
#include "HidKeyboard.hpp"
|
||||
#include "HidMouse.hpp"
|
||||
#include "MassStorage.hpp"
|
||||
#include "Bluetooth/Bluetooth.hpp"
|
||||
#include "Hub.hpp"
|
||||
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/HHDM.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,29 +106,100 @@ namespace Drivers::USB::UsbDevice {
|
||||
// EnumerateDevice
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static bool EnumerateInSlot(uint8_t slotId, const DeviceLocation& location, uint32_t speed,
|
||||
bool& isHub, Hub::HubDescriptorInfo& hubInfo, bool& retryable);
|
||||
|
||||
// Attempts at bringing a device up, each after a fresh port reset (as
|
||||
// Linux's hub_port_init): a device can fail its first requests with a
|
||||
// transaction error while it is still settling after the reset. Card
|
||||
// readers do, and USB devices the firmware used to boot from.
|
||||
constexpr int ENUM_TRIES = 3;
|
||||
|
||||
uint8_t EnumerateDevice(uint8_t portId, uint32_t speed) {
|
||||
KernelLogStream(INFO, "USB") << "Enumerating device on port "
|
||||
<< (uint64_t)portId << " speed=" << SpeedToString(speed);
|
||||
DeviceLocation location = {};
|
||||
location.RootPort = portId;
|
||||
return EnumerateDevice(location, speed);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Step 1: Enable Slot
|
||||
// -----------------------------------------------------------------
|
||||
Xhci::TRB enableSlotTrb = {};
|
||||
enableSlotTrb.Control = (Xhci::TRB_ENABLE_SLOT << Xhci::TRB_TYPE_SHIFT);
|
||||
|
||||
uint32_t cc = Xhci::SendCommand(enableSlotTrb);
|
||||
if (cc != Xhci::CC_SUCCESS) {
|
||||
KernelLogStream(ERROR, "USB") << "Enable Slot failed, cc=" << (uint64_t)cc;
|
||||
return 0;
|
||||
uint8_t EnumerateDevice(const DeviceLocation& location, uint32_t speed) {
|
||||
if (location.ParentSlot == 0) {
|
||||
KernelLogStream(INFO, "USB") << "Enumerating device on port "
|
||||
<< (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);
|
||||
}
|
||||
|
||||
uint8_t slotId = (uint8_t)Xhci::g_cmdCompletionSlotId;
|
||||
if (slotId == 0 || slotId > Xhci::MAX_SLOTS) {
|
||||
KernelLogStream(ERROR, "USB") << "Invalid slot ID: " << (uint64_t)slotId;
|
||||
return 0;
|
||||
}
|
||||
for (int attempt = 1; ; attempt++) {
|
||||
// -------------------------------------------------------------
|
||||
// Step 1: Enable Slot
|
||||
// -------------------------------------------------------------
|
||||
Xhci::TRB enableSlotTrb = {};
|
||||
enableSlotTrb.Control = (Xhci::TRB_ENABLE_SLOT << Xhci::TRB_TYPE_SHIFT);
|
||||
|
||||
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId << " enabled";
|
||||
uint32_t cc = Xhci::SendCommand(enableSlotTrb);
|
||||
if (cc != Xhci::CC_SUCCESS) {
|
||||
KernelLogStream(ERROR, "USB") << "Enable Slot failed, cc=" << (uint64_t)cc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t slotId = (uint8_t)Xhci::g_cmdCompletionSlotId;
|
||||
if (slotId == 0 || slotId > Xhci::MAX_SLOTS) {
|
||||
KernelLogStream(ERROR, "USB") << "Invalid slot ID: " << (uint64_t)slotId;
|
||||
return 0;
|
||||
}
|
||||
|
||||
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId << " enabled";
|
||||
|
||||
bool isHub = false;
|
||||
bool retryable = false;
|
||||
Hub::HubDescriptorInfo hubInfo = {};
|
||||
if (EnumerateInSlot(slotId, location, speed, isHub, hubInfo, retryable)) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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) = {};
|
||||
|
||||
if (!retryable || attempt >= ENUM_TRIES) return 0;
|
||||
|
||||
// Back the device off to its default state, giving it longer to
|
||||
// settle each time.
|
||||
BusyWaitMs(100 * (uint64_t)attempt);
|
||||
bool reset = location.ParentSlot == 0
|
||||
? Xhci::ResetRootPort(location.RootPort, speed)
|
||||
: Hub::ResetChildPort(location.ParentSlot, location.ParentPort, speed);
|
||||
if (!reset) {
|
||||
KernelLogStream(WARNING, "USB") << "Port reset for retry failed, giving up";
|
||||
return 0;
|
||||
}
|
||||
BusyWaitMs(10 * (uint64_t)(attempt + 1)); // reset recovery, and then some
|
||||
|
||||
KernelLogStream(INFO, "USB") << "Retrying enumeration (attempt "
|
||||
<< (uint64_t)(attempt + 1) << " of " << (uint64_t)ENUM_TRIES
|
||||
<< ") speed=" << SpeedToString(speed);
|
||||
}
|
||||
}
|
||||
|
||||
static bool EnumerateInSlot(uint8_t slotId, const DeviceLocation& location, uint32_t speed,
|
||||
bool& isHub, Hub::HubDescriptorInfo& hubInfo, bool& retryable) {
|
||||
uint8_t portId = location.RootPort;
|
||||
uint32_t cc = 0;
|
||||
|
||||
// Until the device is configured nothing has claimed it, so a failure
|
||||
// up to then can be retried from a port reset.
|
||||
retryable = true;
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Step 2: Allocate device output context and set DCBAA entry
|
||||
@@ -133,9 +209,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 +228,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 +236,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 +287,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,11 +301,15 @@ 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];
|
||||
// USB 3 devices give bMaxPacketSize0 as an exponent (9: 512 bytes).
|
||||
uint16_t bMaxPacketSize0 = partialDesc[7];
|
||||
if (speed >= Xhci::SPEED_SUPER) {
|
||||
bMaxPacketSize0 = (bMaxPacketSize0 >= 5 && bMaxPacketSize0 <= 15)
|
||||
? (uint16_t)(1u << bMaxPacketSize0) : maxPacket;
|
||||
}
|
||||
if (bMaxPacketSize0 == 0) bMaxPacketSize0 = maxPacket; // fallback
|
||||
|
||||
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId
|
||||
@@ -227,7 +319,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 +372,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 +389,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 +409,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 +421,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 +429,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 +447,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 +511,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 +558,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 +605,24 @@ 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;
|
||||
}
|
||||
retryable = 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 +640,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 +654,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 +672,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 +699,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 +724,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 +749,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 +841,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 +863,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);
|
||||
|
||||
};
|
||||
+515
-64
@@ -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,45 @@ namespace Drivers::USB::Xhci {
|
||||
// ControlTransfer - perform a control transfer on EP0
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// A STALL or a transaction error halts EP0 (xHCI 4.10.2.1), with the
|
||||
// rest of the TD still queued; until it is reset every later request on
|
||||
// the slot times out. Reset Endpoint, then Set TR Dequeue past the TD.
|
||||
static void RecoverEP0(uint8_t slotId) {
|
||||
UsbDeviceInfo& dev = g_devices[slotId];
|
||||
|
||||
TRB resetTrb = {};
|
||||
resetTrb.Control = (TRB_RESET_ENDPOINT << TRB_TYPE_SHIFT)
|
||||
| ((uint32_t)slotId << 24)
|
||||
| (1 << 16); // DCI=1 (EP0)
|
||||
SendCommand(resetTrb);
|
||||
|
||||
uint64_t newDeq = dev.EP0RingPhys + (uint64_t)dev.EP0RingEnqueue * sizeof(TRB);
|
||||
if (dev.EP0RingCCS) 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)
|
||||
| (1 << 16); // DCI=1 (EP0)
|
||||
SendCommand(deqTrb);
|
||||
}
|
||||
|
||||
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 +978,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 +1021,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 +1042,7 @@ namespace Drivers::USB::Xhci {
|
||||
if (dev.EP0RingCCS) {
|
||||
dataControl |= TRB_CYCLE_BIT;
|
||||
}
|
||||
dataTrb.Control = dataControl;
|
||||
PublishTrb(dataTrb, dataControl);
|
||||
|
||||
AdvanceEP0Ring(dev);
|
||||
}
|
||||
@@ -895,10 +1063,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,8 +1092,9 @@ namespace Drivers::USB::Xhci {
|
||||
while (Timekeeping::GetMilliseconds() - xferStart < 2000) {
|
||||
PollEvents();
|
||||
if (g_xferCompleted[slotId]) {
|
||||
g_controlXferLock.Release();
|
||||
return g_xferCompletionCode[slotId];
|
||||
uint32_t cc = g_xferCompletionCode[slotId];
|
||||
if (cc != CC_SUCCESS && cc != CC_SHORT_PACKET) RecoverEP0(slotId);
|
||||
return cc;
|
||||
}
|
||||
for (int j = 0; j < 100; j++) {
|
||||
Arch::CompilerBarrier();
|
||||
@@ -956,7 +1127,6 @@ namespace Drivers::USB::Xhci {
|
||||
| (1 << 16); // DCI=1 (EP0)
|
||||
SendCommand(deqTrb);
|
||||
|
||||
g_controlXferLock.Release();
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
@@ -986,7 +1156,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);
|
||||
@@ -1095,6 +1265,86 @@ namespace Drivers::USB::Xhci {
|
||||
SendCommand(deqTrb);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ResetRootPort - reset a connected root hub port again
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
bool ResetRootPort(uint8_t portId, uint32_t& speed) {
|
||||
if (!g_initialized || portId == 0 || portId > g_maxPorts) return false;
|
||||
uint32_t reg = OP_PORTSC_BASE + (portId - 1) * OP_PORTSC_STRIDE;
|
||||
|
||||
uint32_t portsc = ReadOp(reg);
|
||||
if (!(portsc & PORTSC_CCS)) return false;
|
||||
WriteOp(reg, (portsc & PORTSC_PRESERVE) | PORTSC_PR | PORTSC_CHANGE_BITS);
|
||||
|
||||
bool resetDone = false;
|
||||
for (uint32_t i = 0; i < 100000; i++) {
|
||||
PollEvents();
|
||||
if (PortResetComplete(ReadOp(reg))) { resetDone = true; break; }
|
||||
for (int j = 0; j < 100; j++) Arch::CompilerBarrier();
|
||||
}
|
||||
if (!resetDone) return false;
|
||||
|
||||
portsc = ReadOp(reg);
|
||||
WriteOp(reg, (portsc & PORTSC_PRESERVE) | PORTSC_CHANGE_BITS);
|
||||
if (!(portsc & PORTSC_PED)) return false;
|
||||
speed = (portsc >> 10) & 0xF;
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// RecoverBulkEndpoint - clear a halt or a timed-out TRB on a bulk endpoint
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
void RecoverBulkEndpoint(uint8_t slotId, bool dirIn) {
|
||||
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
|
||||
UsbDeviceInfo& dev = g_devices[slotId];
|
||||
uint8_t epNum = dirIn ? dev.BulkInEpNum : dev.BulkOutEpNum;
|
||||
TRB* ring = dirIn ? dev.BulkInRing : dev.BulkOutRing;
|
||||
if (epNum == 0 || ring == nullptr) return;
|
||||
|
||||
uint8_t dci = dirIn ? (uint8_t)(epNum * 2 + 1) : (uint8_t)(epNum * 2);
|
||||
|
||||
// Reset Endpoint succeeds only on a halted endpoint; one that is still
|
||||
// running (the transfer timed out) is stopped instead. Either way it
|
||||
// ends up Stopped, which Set TR Dequeue requires.
|
||||
TRB resetTrb = {};
|
||||
resetTrb.Control = (TRB_RESET_ENDPOINT << TRB_TYPE_SHIFT)
|
||||
| ((uint32_t)slotId << 24)
|
||||
| ((uint32_t)dci << 16);
|
||||
bool wasHalted = SendCommand(resetTrb) == CC_SUCCESS;
|
||||
|
||||
if (!wasHalted) {
|
||||
TRB stopTrb = {};
|
||||
stopTrb.Control = (TRB_STOP_ENDPOINT << TRB_TYPE_SHIFT)
|
||||
| ((uint32_t)slotId << 24)
|
||||
| ((uint32_t)dci << 16);
|
||||
SendCommand(stopTrb);
|
||||
}
|
||||
|
||||
uint64_t newDeq = dirIn
|
||||
? dev.BulkInRingPhys + (uint64_t)dev.BulkInRingEnqueue * sizeof(TRB)
|
||||
: dev.BulkOutRingPhys + (uint64_t)dev.BulkOutRingEnqueue * sizeof(TRB);
|
||||
if (dirIn ? dev.BulkInRingCCS : dev.BulkOutRingCCS) 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);
|
||||
|
||||
// Reset Endpoint put the host's data toggle back to DATA0; CLEAR_FEATURE
|
||||
// (ENDPOINT_HALT) clears the device's stall and does the same on its
|
||||
// side (USB 2.0 9.4.5). Without a reset the toggles still agree.
|
||||
if (wasHalted) {
|
||||
ControlTransfer(slotId, 0x02 /* host-to-dev, standard, endpoint */,
|
||||
0x01 /* CLEAR_FEATURE */, 0 /* ENDPOINT_HALT */,
|
||||
(uint16_t)((dirIn ? 0x80 : 0x00) | epNum), 0, nullptr, false);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// QueueBulkInTransfer
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -1123,7 +1373,7 @@ namespace Drivers::USB::Xhci {
|
||||
if (dev.BulkInRingCCS) {
|
||||
control |= TRB_CYCLE_BIT;
|
||||
}
|
||||
trb.Control = control;
|
||||
PublishTrb(trb, control);
|
||||
|
||||
AdvanceBulkInRing(dev);
|
||||
|
||||
@@ -1250,6 +1500,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 +1517,7 @@ namespace Drivers::USB::Xhci {
|
||||
if (dev.BulkOutRingCCS) {
|
||||
control |= TRB_CYCLE_BIT;
|
||||
}
|
||||
trb.Control = control;
|
||||
PublishTrb(trb, control);
|
||||
|
||||
AdvanceBulkOutRing(dev);
|
||||
|
||||
@@ -1277,8 +1530,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 +1569,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 +1583,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 +1663,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 +1731,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 +1756,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 +1872,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 +1890,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 +1925,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 +1953,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 +1977,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 +2012,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 +2067,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 +2116,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 +2158,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 +2187,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 +2493,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;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
@@ -161,6 +161,7 @@ namespace Drivers::USB::Xhci {
|
||||
|
||||
// Completion codes (from Status field bits 31:24)
|
||||
constexpr uint32_t CC_SUCCESS = 1;
|
||||
constexpr uint32_t CC_STALL_ERROR = 6;
|
||||
constexpr uint32_t CC_SHORT_PACKET = 13;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -177,6 +178,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 +237,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 +313,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();
|
||||
|
||||
@@ -337,6 +368,17 @@ namespace Drivers::USB::Xhci {
|
||||
// outstanding-transfer accounting before submitting fresh work.
|
||||
void ResetBulkOutEndpoint(uint8_t slotId);
|
||||
|
||||
// Reset a connected root port again (an enumeration retry) and report the
|
||||
// speed the device came up at. False if it is gone or did not enable.
|
||||
bool ResetRootPort(uint8_t portId, uint32_t& speed);
|
||||
|
||||
// Bring a bulk endpoint back after a failed BulkTransfer(): clear a halt
|
||||
// (a STALL or a transaction error) on both the host and the device, or,
|
||||
// after a timeout, stop the endpoint and drop its stale TRB, so the next
|
||||
// transfer neither fails nor completes against the old one. Process or
|
||||
// boot context only (it waits on commands and EP0).
|
||||
void RecoverBulkEndpoint(uint8_t slotId, bool dirIn);
|
||||
|
||||
// True while PollEvents() is draining the event ring. A command submitter
|
||||
// reached from inside an event callback must fire-and-forget (not wait),
|
||||
// since a nested PollEvents() is a no-op.
|
||||
@@ -374,6 +416,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;
|
||||
|
||||
|
||||
Loaded 100 of 240 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user