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/
|
archives/isos/
|
||||||
montaukos.org/downloads/*.iso
|
montaukos.org/downloads/*.iso
|
||||||
*.hdd
|
*.hdd
|
||||||
|
*.img
|
||||||
|
rpi4-firmware/
|
||||||
qemu.log
|
qemu.log
|
||||||
toolchain/local/
|
toolchain/local/
|
||||||
toolchain/build/
|
toolchain/build/
|
||||||
toolchain/src/
|
toolchain/src/
|
||||||
toolchain/sysroot/
|
toolchain/sysroot/
|
||||||
|
toolchain/sysroot-*/
|
||||||
programs/bin/
|
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/obj/
|
||||||
programs/src/*/obj/
|
programs/src/*/obj/
|
||||||
programs/libs/*/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)
|
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.
|
# Toolchain for building the 'limine' executable for the host.
|
||||||
HOST_CC := cc
|
HOST_CC := cc
|
||||||
HOST_CFLAGS := -g -O2 -pipe
|
HOST_CFLAGS := -g -O2 -pipe
|
||||||
@@ -51,29 +77,72 @@ run-hdd-x86_64: $(IMAGE_NAME).hdd
|
|||||||
$(QEMUFLAGS)
|
$(QEMUFLAGS)
|
||||||
|
|
||||||
.PHONY: run-aarch64
|
.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
|
run-aarch64: $(IMAGE_NAME).iso
|
||||||
qemu-system-$(ARCH) \
|
qemu-system-$(ARCH) \
|
||||||
-M virt \
|
-M virt,gic-version=2 \
|
||||||
-cpu cortex-a72 \
|
-cpu cortex-a72 \
|
||||||
-device ramfb \
|
-device ramfb \
|
||||||
-device qemu-xhci,id=xhci \
|
-device qemu-xhci,id=xhci \
|
||||||
-device usb-kbd,bus=xhci.0 \
|
-device usb-hub,bus=xhci.0,port=1 \
|
||||||
-device usb-mouse,bus=xhci.0 \
|
-device usb-kbd,bus=xhci.0,port=1.1 \
|
||||||
-bios /usr/share/ovmf/OVMF.fd \
|
-device usb-mouse,bus=xhci.0,port=1.2 \
|
||||||
|
-bios $(AARCH64_EFI_FIRMWARE) \
|
||||||
-cdrom $(IMAGE_NAME).iso \
|
-cdrom $(IMAGE_NAME).iso \
|
||||||
|
-serial stdio \
|
||||||
$(QEMUFLAGS)
|
$(QEMUFLAGS)
|
||||||
|
|
||||||
.PHONY: run-hdd-aarch64
|
.PHONY: run-hdd-aarch64
|
||||||
run-hdd-aarch64: $(IMAGE_NAME).hdd
|
run-hdd-aarch64: $(IMAGE_NAME).hdd
|
||||||
qemu-system-$(ARCH) \
|
qemu-system-$(ARCH) \
|
||||||
-M virt \
|
-M virt,gic-version=2 \
|
||||||
-cpu cortex-a72 \
|
-cpu cortex-a72 \
|
||||||
-device ramfb \
|
-device ramfb \
|
||||||
-device qemu-xhci,id=xhci \
|
-device qemu-xhci,id=xhci \
|
||||||
-device usb-kbd,bus=xhci.0 \
|
-device usb-hub,bus=xhci.0,port=1 \
|
||||||
-device usb-mouse,bus=xhci.0 \
|
-device usb-kbd,bus=xhci.0,port=1.1 \
|
||||||
-bios /usr/share/ovmf/OVMF.fd \
|
-device usb-mouse,bus=xhci.0,port=1.2 \
|
||||||
-hda $(IMAGE_NAME).hdd \
|
-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)
|
$(QEMUFLAGS)
|
||||||
|
|
||||||
.PHONY: run-riscv64
|
.PHONY: run-riscv64
|
||||||
@@ -196,13 +265,23 @@ ports: programs
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
.PHONY: ramdisk
|
.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
|
ramdisk: limine/limine kernel programs
|
||||||
mkdir -p programs/bin/boot/limine
|
mkdir -p $(PROGRAMS_BIN)/boot/limine
|
||||||
cp -v limine/BOOTX64.EFI programs/bin/boot/limine/
|
cp -v limine/$(EFI_LOADER_$(ARCH)) $(PROGRAMS_BIN)/boot/limine/
|
||||||
cp -v limine.conf programs/bin/boot/limine/
|
cp -v limine.conf $(PROGRAMS_BIN)/boot/limine/
|
||||||
cp -v kernel/bin-$(ARCH)/kernel programs/bin/boot/
|
cp -v kernel/bin-$(ARCH)/kernel $(PROGRAMS_BIN)/boot/
|
||||||
rm -f programs/bin/boot/ramdisk.tar
|
rm -f $(PROGRAMS_BIN)/boot/ramdisk.tar
|
||||||
./scripts/mkramdisk.sh programs/bin ramdisk.tar
|
./scripts/mkramdisk.sh $(PROGRAMS_BIN) ramdisk.tar
|
||||||
|
endif
|
||||||
|
|
||||||
$(IMAGE_NAME).iso: limine/limine kernel ramdisk
|
$(IMAGE_NAME).iso: limine/limine kernel ramdisk
|
||||||
rm -rf iso_root
|
rm -rf iso_root
|
||||||
@@ -261,8 +340,8 @@ release: limine/limine kernel ramdisk
|
|||||||
rm -rf iso_root .release-tmp
|
rm -rf iso_root .release-tmp
|
||||||
mkdir -p iso_root/boot
|
mkdir -p iso_root/boot
|
||||||
cp kernel/bin-$(ARCH)/kernel iso_root/boot/
|
cp kernel/bin-$(ARCH)/kernel iso_root/boot/
|
||||||
strip iso_root/boot/kernel
|
$(STRIP) iso_root/boot/kernel
|
||||||
cp -r programs/bin .release-tmp
|
cp -r $(PROGRAMS_BIN) .release-tmp
|
||||||
# A release ISO ships only in-tree apps, so drop any out-of-tree port bundles
|
# 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).
|
# 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.
|
# 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
|
.release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk
|
||||||
endif
|
endif
|
||||||
rm -f $(addprefix .release-tmp/os/,$(RELEASE_EXCLUDE_PROGRAMS))
|
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 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
|
# 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.
|
# *.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
|
./scripts/mkramdisk.sh .release-tmp iso_root/boot/ramdisk.tar
|
||||||
rm -rf .release-tmp
|
rm -rf .release-tmp
|
||||||
mkdir -p iso_root/boot/limine
|
mkdir -p iso_root/boot/limine
|
||||||
@@ -336,6 +415,26 @@ ifeq ($(ARCH),loongarch64)
|
|||||||
endif
|
endif
|
||||||
rm -rf iso_root
|
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
|
$(IMAGE_NAME).hdd: limine/limine kernel ramdisk
|
||||||
rm -f $(IMAGE_NAME).hdd
|
rm -f $(IMAGE_NAME).hdd
|
||||||
dd if=/dev/zero bs=1M count=0 seek=64 of=$(IMAGE_NAME).hdd
|
dd if=/dev/zero bs=1M count=0 seek=64 of=$(IMAGE_NAME).hdd
|
||||||
@@ -367,7 +466,7 @@ endif
|
|||||||
clean:
|
clean:
|
||||||
$(MAKE) -C kernel clean
|
$(MAKE) -C kernel clean
|
||||||
# $(MAKE) -C programs 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
|
.PHONY: distclean
|
||||||
distclean:
|
distclean:
|
||||||
|
|||||||
+103
-13
@@ -36,7 +36,7 @@ kernel/src/Arch/
|
|||||||
Platform.hpp boot-sequence hooks, platform device list, reboot
|
Platform.hpp boot-sequence hooks, platform device list, reboot
|
||||||
CpuPower.hpp frequency scaling / thermal governor (may be no-ops)
|
CpuPower.hpp frequency scaling / thermal governor (may be no-ops)
|
||||||
x86_64/ the complete port
|
x86_64/ the complete port
|
||||||
aarch64/ skeleton port
|
aarch64/ the port in progress (see below)
|
||||||
```
|
```
|
||||||
|
|
||||||
Other interfaces a port implements:
|
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
|
- Interrupts are numbered IRQ slots (`Arch::IRQ_COUNT`); slot numbers for
|
||||||
MSI-capable drivers are fixed in the drivers.
|
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
|
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_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
|
`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).
|
- SMP (`Smp.cpp`): Limine wakes each secondary CPU (PSCI `CPU_ON`
|
||||||
- Exception vectors (`VBAR_EL1`): synchronous faults, `svc`, IRQs.
|
underneath) on the bootloader's tables; it programs its own system
|
||||||
- GIC driver: SPI routing, SGIs for IPIs, MSIs via ITS/GICv2m
|
registers and the kernel translation regime (`Bringup.hpp`), its banked
|
||||||
(`ComposeMsi` returns false, so drivers fall back to INTx/polling).
|
GIC CPU interface and timer PPI, then idles until a reschedule SGI.
|
||||||
- Generic timer tick (`Arch::Timer::InitializeBsp` returns false).
|
TLB maintenance is broadcast (`tlbi ...is`).
|
||||||
- Secondary CPU start-up, PSCI reset and suspend.
|
|
||||||
|
|
||||||
Building it needs `clang` and `ld.lld` (`make kernel ARCH=aarch64`;
|
The generic kernel is compiled `-fsigned-char` for AArch64: it was written
|
||||||
`KERNEL_LD=` points at a different `ld.lld`, e.g. rustup's
|
for x86's signed plain `char`.
|
||||||
`lib/rustlib/x86_64-unknown-linux-gnu/bin/gcc-ld/ld.lld`).
|
|
||||||
|
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
|
## 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 += \
|
override CXX += \
|
||||||
-target aarch64-unknown-none
|
-target aarch64-unknown-none
|
||||||
endif
|
endif
|
||||||
|
# Plain char is signed on x86; the generic kernel is written for that.
|
||||||
override SHARED_FLAGS += \
|
override SHARED_FLAGS += \
|
||||||
-mgeneral-regs-only
|
-mgeneral-regs-only \
|
||||||
|
-fsigned-char
|
||||||
override LDFLAGS += \
|
override LDFLAGS += \
|
||||||
-Wl,-m,aarch64elf
|
-Wl,-m,aarch64elf
|
||||||
endif
|
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.
|
# so a clang-built non-x86 kernel is linked by ld.lld directly.
|
||||||
ifneq ($(ARCH),x86_64)
|
ifneq ($(ARCH),x86_64)
|
||||||
ifeq ($(CXX_IS_CLANG),1)
|
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_aarch64 := aarch64elf
|
||||||
override LD_EMULATION_riscv64 := elf64lriscv
|
override LD_EMULATION_riscv64 := elf64lriscv
|
||||||
override LD_EMULATION_loongarch64 := elf64loongarch
|
override LD_EMULATION_loongarch64 := elf64loongarch
|
||||||
|
|||||||
@@ -38,8 +38,24 @@ namespace Hal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map and parse the DSDT to find \_S5_
|
|
||||||
auto* dsdt = (void*)Memory::HHDM(fadt.DsdtAddress);
|
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);
|
AML::S5Object s5 = AML::FindS5(dsdt);
|
||||||
|
|
||||||
if (!s5.Valid) {
|
if (!s5.Valid) {
|
||||||
@@ -55,23 +71,6 @@ namespace Hal {
|
|||||||
|
|
||||||
KernelLogStream(OK, "ACPI") << "ACPI shutdown initialized (PM1a=" << base::hex
|
KernelLogStream(OK, "ACPI") << "ACPI shutdown initialized (PM1a=" << base::hex
|
||||||
<< (uint64_t)g_pm1aControlBlock << " SLP_TYPa=" << (uint64_t)g_slpTypA << ")";
|
<< (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() {
|
bool IsAvailable() {
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ namespace Hal {
|
|||||||
result.AcpiEnable = fadt->AcpiEnable;
|
result.AcpiEnable = fadt->AcpiEnable;
|
||||||
result.AcpiDisable = fadt->AcpiDisable;
|
result.AcpiDisable = fadt->AcpiDisable;
|
||||||
result.Flags = fadt->Flags;
|
result.Flags = fadt->Flags;
|
||||||
|
result.ArmBootArchFlags = fadt->Header.Length >= offsetof(Table, ArmBootArchFlags) + 2
|
||||||
|
? fadt->ArmBootArchFlags : 0;
|
||||||
result.Valid = true;
|
result.Valid = true;
|
||||||
|
|
||||||
KernelLogStream(INFO, "FADT") << "DSDT at " << base::hex << result.DsdtAddress;
|
KernelLogStream(INFO, "FADT") << "DSDT at " << base::hex << result.DsdtAddress;
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ namespace Hal {
|
|||||||
uint8_t X_GPE1Block[12];
|
uint8_t X_GPE1Block[12];
|
||||||
} __attribute__((packed));
|
} __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 {
|
struct ParsedFADT {
|
||||||
uint64_t DsdtAddress;
|
uint64_t DsdtAddress;
|
||||||
uint64_t FacsAddress;
|
uint64_t FacsAddress;
|
||||||
@@ -91,6 +98,7 @@ namespace Hal {
|
|||||||
uint8_t AcpiEnable;
|
uint8_t AcpiEnable;
|
||||||
uint8_t AcpiDisable;
|
uint8_t AcpiDisable;
|
||||||
uint32_t Flags;
|
uint32_t Flags;
|
||||||
|
uint16_t ArmBootArchFlags; // 0 when the table predates the field
|
||||||
bool Valid;
|
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/IntelHda.hpp>
|
||||||
#include <Drivers/Audio/Mixer.hpp>
|
#include <Drivers/Audio/Mixer.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
#include <Drivers/Bluetooth/A2dp.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
|
|
||||||
#include "Syscall.hpp"
|
#include "Syscall.hpp"
|
||||||
@@ -55,10 +55,10 @@ namespace montauk::abi {
|
|||||||
return Drivers::Audio::Mixer::SetOutput(
|
return Drivers::Audio::Mixer::SetOutput(
|
||||||
(Drivers::Audio::Mixer::Output)value);
|
(Drivers::Audio::Mixer::Output)value);
|
||||||
if (cmd == AUDIO_CTL_BT_STATUS) {
|
if (cmd == AUDIO_CTL_BT_STATUS) {
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||||
if (Drivers::USB::Bluetooth::A2dp::IsReady()) return 2;
|
if (Drivers::Bluetooth::A2dp::IsReady()) return 2;
|
||||||
return Drivers::USB::Bluetooth::A2dp::GetState()
|
return Drivers::Bluetooth::A2dp::GetState()
|
||||||
== Drivers::USB::Bluetooth::A2dp::State::Idle
|
== Drivers::Bluetooth::A2dp::State::Idle
|
||||||
? 0 : 1;
|
? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/Hci.hpp>
|
#include <Drivers/Bluetooth/Hci.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
@@ -19,14 +19,14 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
|
static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
|
||||||
if (!buf || maxCount <= 0) return InvalidArgument;
|
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)
|
// 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;
|
int kMax = maxCount;
|
||||||
if (kMax > 16) kMax = 16;
|
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;
|
if (found < 0) return found;
|
||||||
|
|
||||||
// Convert kernel InquiryDevice to userspace BtScanResult
|
// Convert kernel InquiryDevice to userspace BtScanResult
|
||||||
@@ -47,38 +47,38 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
static int64_t Sys_BtConnect(const uint8_t* bdAddr) {
|
static int64_t Sys_BtConnect(const uint8_t* bdAddr) {
|
||||||
if (!bdAddr) return InvalidArgument;
|
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) {
|
static int64_t Sys_BtDisconnect(const uint8_t* bdAddr) {
|
||||||
if (!bdAddr) return InvalidArgument;
|
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
|
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
|
||||||
<< (uint64_t)Sched::GetCurrentPid()
|
<< (uint64_t)Sched::GetCurrentPid()
|
||||||
<< " entered Bluetooth disconnect stage";
|
<< " 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) {
|
static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) {
|
||||||
if (!bdAddr) return InvalidArgument;
|
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.
|
// 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) {
|
static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) {
|
||||||
if (!buf || maxCount <= 0) return 0;
|
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;
|
int kMax = maxCount;
|
||||||
if (kMax > 8) kMax = 8;
|
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++) {
|
for (int i = 0; i < count; i++) {
|
||||||
memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6);
|
memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6);
|
||||||
@@ -91,20 +91,20 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
static int64_t Sys_BtForget(const uint8_t* bdAddr) {
|
static int64_t Sys_BtForget(const uint8_t* bdAddr) {
|
||||||
if (!bdAddr) return InvalidArgument;
|
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) {
|
static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) {
|
||||||
if (!buf || maxCount <= 0) return 0;
|
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;
|
int kMax = maxCount;
|
||||||
if (kMax > 4) kMax = 4;
|
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++) {
|
for (int i = 0; i < count; i++) {
|
||||||
memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6);
|
memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6);
|
||||||
@@ -123,14 +123,14 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
memset(buf, 0, sizeof(BtAdapterInfo));
|
memset(buf, 0, sizeof(BtAdapterInfo));
|
||||||
|
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) {
|
if (!Drivers::Bluetooth::IsInitialized()) {
|
||||||
buf->initialized = 0;
|
buf->initialized = 0;
|
||||||
return NoDevice;
|
return NoDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf->initialized = 1;
|
buf->initialized = 1;
|
||||||
memcpy(buf->bdAddr, Drivers::USB::Bluetooth::GetBdAddr(), 6);
|
memcpy(buf->bdAddr, Drivers::Bluetooth::GetBdAddr(), 6);
|
||||||
buf->scanning = Drivers::USB::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
|
buf->scanning = Drivers::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
|
||||||
|
|
||||||
// Copy adapter name
|
// Copy adapter name
|
||||||
const char* name = "MontaukOS";
|
const char* name = "MontaukOS";
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 26
|
#define MONTAUK_BUILD_NUMBER 123
|
||||||
@@ -8,9 +8,10 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
#include <Drivers/USB/Xhci.hpp>
|
||||||
#include <Drivers/USB/UsbDevice.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/E1000.hpp>
|
||||||
#include <Drivers/Net/E1000E.hpp>
|
#include <Drivers/Net/E1000E.hpp>
|
||||||
|
#include <Drivers/Net/Genet.hpp>
|
||||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||||
#include <Drivers/Storage/Ahci.hpp>
|
#include <Drivers/Storage/Ahci.hpp>
|
||||||
@@ -122,11 +123,24 @@ namespace montauk::abi {
|
|||||||
if (Drivers::Net::E1000E::IsInitialized()) {
|
if (Drivers::Net::E1000E::IsInitialized()) {
|
||||||
add(5, "Intel E1000E", "Gigabit Ethernet (82574L)");
|
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()) {
|
if (Drivers::Net::Wifi::IsPresent()) {
|
||||||
add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp()
|
add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp()
|
||||||
? "802.11 wireless (connected)"
|
? "802.11 wireless (connected)"
|
||||||
: "802.11 wireless");
|
: "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)
|
// Display (category 6)
|
||||||
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
|
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
|
||||||
@@ -148,7 +162,7 @@ namespace montauk::abi {
|
|||||||
add(9, "Intel HDA", detail);
|
add(9, "Intel HDA", detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Drivers::USB::Bluetooth::IsInitialized()) {
|
if (Drivers::Bluetooth::IsInitialized()) {
|
||||||
add(9, "Bluetooth Audio", "A2DP (SBC)");
|
add(9, "Bluetooth Audio", "A2DP (SBC)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +262,10 @@ namespace montauk::abi {
|
|||||||
case Drivers::Storage::BLOCK_KIND_USB_MSC:
|
case Drivers::Storage::BLOCK_KIND_USB_MSC:
|
||||||
buf->type = 4;
|
buf->type = 4;
|
||||||
break;
|
break;
|
||||||
|
case Drivers::Storage::BLOCK_KIND_SD:
|
||||||
|
buf->type = 5;
|
||||||
|
buf->rpm = 1;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <Net/NetIf.hpp>
|
#include <Net/NetIf.hpp>
|
||||||
#include <Drivers/Net/E1000.hpp>
|
#include <Drivers/Net/E1000.hpp>
|
||||||
#include <Drivers/Net/E1000E.hpp>
|
#include <Drivers/Net/E1000E.hpp>
|
||||||
|
#include <Drivers/Net/Genet.hpp>
|
||||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||||
|
|
||||||
#include "Syscall.hpp"
|
#include "Syscall.hpp"
|
||||||
@@ -157,6 +158,10 @@ namespace montauk::abi {
|
|||||||
out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount();
|
out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount();
|
||||||
out->txPackets = Drivers::Net::E1000E::GetTxPacketCount();
|
out->txPackets = Drivers::Net::E1000E::GetTxPacketCount();
|
||||||
CopyNetStatusDriver(out->driver, "Intel e1000e");
|
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();
|
Hal::AcpiShutdown::Shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hardware-reduced platforms (PSCI on ARM) */
|
||||||
|
Arch::PowerOff();
|
||||||
|
|
||||||
/* Fallback: EFI ResetSystem */
|
/* Fallback: EFI ResetSystem */
|
||||||
if (Efi::g_ResetSystem) {
|
if (Efi::g_ResetSystem) {
|
||||||
Memory::VMM::ActivateKernelAddressSpace();
|
Memory::VMM::ActivateKernelAddressSpace();
|
||||||
|
|||||||
@@ -163,6 +163,45 @@ namespace montauk::abi {
|
|||||||
return (int64_t)Fs::FsProbe::SyncAndUnmountAll();
|
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.
|
// Format a partition with a filesystem. Returns 0 on success, GenericFailure on error.
|
||||||
static int64_t Sys_FsFormat(const FsFormatParams* params) {
|
static int64_t Sys_FsFormat(const FsFormatParams* params) {
|
||||||
if (params == nullptr) return InvalidArgument;
|
if (params == nullptr) return InvalidArgument;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
#include "Terminal.hpp" // SYS_PRINT, SYS_PUTCHAR
|
#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 "Filesystem.hpp" // SYS_OPEN, SYS_READ, SYS_GETSIZE, SYS_CLOSE, SYS_READDIR, SYS_FWRITE, SYS_FCREATE
|
||||||
#include "Heap.hpp" // SYS_ALLOC, SYS_FREE
|
#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 "Keyboard.hpp" // SYS_ISKEYAVAILABLE, SYS_GETKEY, SYS_GETCHAR
|
||||||
#include "Info.hpp" // SYS_GETINFO
|
#include "Info.hpp" // SYS_GETINFO
|
||||||
#include "Graphics.hpp" // SYS_FBINFO, SYS_FBMAP, SYS_TERMSIZE, SYS_TERMSCALE
|
#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;
|
if (!UserMemory::Writable<DateTime>(frame->arg1)) return BadPointer;
|
||||||
Sys_GetTime((DateTime*)frame->arg1);
|
Sys_GetTime((DateTime*)frame->arg1);
|
||||||
return Success;
|
return Success;
|
||||||
|
case SYS_GETUNIXTIME:
|
||||||
|
return Sys_GetUnixTime();
|
||||||
case SYS_SETUNIXTIME:
|
case SYS_SETUNIXTIME:
|
||||||
if (!Sched::Consent::Require(CAP_SET_TIME)) return CapabilityDenied;
|
if (!Sched::Consent::Require(CAP_SET_TIME)) return CapabilityDenied;
|
||||||
return Sys_SetUnixTime((int64_t)frame->arg1);
|
return Sys_SetUnixTime((int64_t)frame->arg1);
|
||||||
@@ -454,6 +456,10 @@ namespace montauk::abi {
|
|||||||
case SYS_FS_SYNC:
|
case SYS_FS_SYNC:
|
||||||
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
||||||
return Sys_FsSync();
|
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:
|
case SYS_FSFORMAT:
|
||||||
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
||||||
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return BadPointer;
|
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_FORMATS = 206; // (serial, TransferFormatInfo*, max) -> count
|
||||||
static constexpr uint64_t SYS_WIN_DRAG_GET_DATA = 207; // (serial, mime, buf, max, uint32_t* len) -> status
|
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
|
/* Kernel-owned process capabilities. User identities may namespace
|
||||||
per-user resources, but never participate in authorization decisions. */
|
per-user resources, but never participate in authorization decisions. */
|
||||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||||
@@ -915,7 +923,7 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
struct DiskInfo {
|
struct DiskInfo {
|
||||||
uint8_t port; // block device index
|
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 sataGen; // SATA gen (1/2/3)
|
||||||
uint8_t _pad0;
|
uint8_t _pad0;
|
||||||
uint64_t sectorCount; // Total user-addressable sectors
|
uint64_t sectorCount; // Total user-addressable sectors
|
||||||
@@ -997,6 +1005,14 @@ namespace montauk::abi {
|
|||||||
char label[32]; // volume label
|
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 {
|
struct ProcInfo {
|
||||||
int32_t pid;
|
int32_t pid;
|
||||||
int32_t parentPid;
|
int32_t parentPid;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Time.hpp
|
* Time.hpp
|
||||||
* SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME syscalls
|
* SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME, SYS_GETUNIXTIME syscalls
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -39,6 +39,10 @@ namespace montauk::abi {
|
|||||||
return (int64_t)Timekeeping::GetTZOffset();
|
return (int64_t)Timekeeping::GetTZOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int64_t Sys_GetUnixTime() {
|
||||||
|
return Timekeeping::GetUnixTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
static int64_t Sys_SetUnixTime(int64_t unixSeconds) {
|
static int64_t Sys_SetUnixTime(int64_t unixSeconds) {
|
||||||
return Timekeeping::SetUnixTimestamp(unixSeconds) ? Success : InvalidArgument;
|
return Timekeeping::SetUnixTimestamp(unixSeconds) ? Success : InvalidArgument;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ namespace Arch {
|
|||||||
// Full read/write memory barrier.
|
// Full read/write memory barrier.
|
||||||
inline void MemoryFence();
|
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 ----
|
// ---- Interrupt state ----
|
||||||
|
|
||||||
inline void EnableInterrupts();
|
inline void EnableInterrupts();
|
||||||
@@ -79,6 +88,18 @@ namespace Arch {
|
|||||||
// Write back and invalidate all CPU caches (before power state changes).
|
// Write back and invalidate all CPU caches (before power state changes).
|
||||||
inline void WritebackInvalidateCaches();
|
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.
|
// Invalidate this CPU's translation for one virtual page.
|
||||||
inline void FlushTlbPage(uint64_t virtualAddress);
|
inline void FlushTlbPage(uint64_t virtualAddress);
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ namespace Arch {
|
|||||||
// Unmask platform interrupt line `irq` (already routed at boot).
|
// Unmask platform interrupt line `irq` (already routed at boot).
|
||||||
void EnableLegacyIrq(uint8_t irq);
|
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
|
// A message-signalled interrupt targeting IRQ slot `irq` on the boot
|
||||||
// CPU: the device writes `data` to `address` to raise it. Program these
|
// 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
|
// into a PCI MSI capability or MSI-X table entry. Returns false when the
|
||||||
|
|||||||
@@ -51,4 +51,8 @@ namespace Arch {
|
|||||||
// ResetSystem). Never returns.
|
// ResetSystem). Never returns.
|
||||||
[[noreturn]] void Reboot();
|
[[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");
|
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() {
|
inline void EnableInterrupts() {
|
||||||
asm volatile("msr daifclr, #2" ::: "memory");
|
asm volatile("msr daifclr, #2" ::: "memory");
|
||||||
}
|
}
|
||||||
@@ -92,9 +101,50 @@ namespace Arch {
|
|||||||
asm volatile("dsb sy\n\tisb" ::: "memory");
|
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) {
|
inline void FlushTlbPage(uint64_t virtualAddress) {
|
||||||
asm volatile("dsb ishst\n\ttlbi vaae1is, %0\n\tdsb ish\n\tisb"
|
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() {
|
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 {
|
namespace Arch {
|
||||||
|
|
||||||
// What the exception vectors save before calling into C++.
|
// What the exception vectors (Vectors.S) save before calling into C++.
|
||||||
struct PanicFrame {
|
struct PanicFrame {
|
||||||
uint64_t x[31]; // x0-x30
|
uint64_t x[31]; // x0-x30
|
||||||
uint64_t sp; // interrupted SP (SP_EL0 for EL0, else SP_EL1)
|
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
|
uint64_t far; // FAR_EL1: faulting address
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(PanicFrame) == 288, "must match Vectors.S");
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,52 +1,420 @@
|
|||||||
/*
|
/*
|
||||||
* Interrupts.cpp
|
* 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
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arch/Interrupts.hpp>
|
#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 <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
|
|
||||||
// IRQ slots are delivered by the interrupt controller driver (GICv2/v3,
|
// IRQ slots map onto GIC interrupt IDs (INTIDs):
|
||||||
// not written yet), which maps INTIDs onto slots and calls ArchIrqDispatch
|
// IRQ_TIMER the generic timer's PPI (Timer.cpp routes it)
|
||||||
// from the IRQ exception vector after acknowledging the interrupt.
|
// 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 {
|
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 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) {
|
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
|
||||||
if (irq >= IRQ_COUNT) return;
|
if (irq >= IRQ_COUNT) return;
|
||||||
g_irqHandlers[irq] = handler;
|
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;
|
<< 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) {
|
void RouteLegacyIrq(uint8_t irq) {
|
||||||
Kt::KernelLogStream(Kt::WARNING, "IRQ") << "aarch64: cannot route IRQ "
|
if (irq >= 24 || g_platformLines[irq].gsi == 0) {
|
||||||
<< base::dec << (uint64_t)irq << " (no GIC driver)";
|
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) {
|
void EnableLegacyIrq(uint8_t irq) {
|
||||||
RouteLegacyIrq(irq);
|
RouteLegacyIrq(irq);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(aarch64): GICv3 ITS / GICv2m doorbell.
|
bool ComposeMsi(uint8_t irq, MsiMessage& out) {
|
||||||
bool ComposeMsi(uint8_t, MsiMessage&) {
|
if (g_gicd == nullptr || irq < FirstMsiSlot || irq > LastMsiSlot) return false;
|
||||||
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 cpuIndex, uint8_t irq) {
|
||||||
bool SendIpi(int, uint8_t) {
|
if (g_gicd == nullptr) return false;
|
||||||
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) {
|
// IRQ exception vector (Vectors.S). As on x86, the interrupt is completed
|
||||||
if (irq < Arch::IRQ_COUNT && Arch::g_irqHandlers[irq] != nullptr) {
|
// BEFORE the handler runs: the handler may context-switch away and not
|
||||||
Arch::g_irqHandlers[irq]((uint8_t)irq, fromUser);
|
// 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/Paging.hpp>
|
||||||
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
|
#include <Memory/HHDM.hpp>
|
||||||
#include <Arch/Cpu.hpp>
|
#include <Arch/Cpu.hpp>
|
||||||
#include <Common/Panic.hpp>
|
#include <Common/Panic.hpp>
|
||||||
#include <CppLib/Spinlock.hpp>
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
#include <Arch/aarch64/Bringup.hpp>
|
||||||
|
|
||||||
// TODO(aarch64): 4 KiB-granule, 48-bit VA translation tables. The kernel
|
// 4 KiB granule, 48-bit virtual addresses, four levels (0-3). The kernel
|
||||||
// half lives in TTBR1_EL1 and is never switched; "address space roots"
|
// half is translated through TTBR1_EL1 (g_kernelHalfRoot), which never
|
||||||
// handed to the generic kernel are TTBR0_EL1 tables, so
|
// changes after Init. The "address space roots" the generic kernel switches
|
||||||
// CreateUserAddressSpace has no kernel half to copy. MAIR_EL1 needs
|
// between are TTBR0_EL1 tables: a process's root holds only its own lower
|
||||||
// Normal-WB, Normal-NC (MapWC) and Device-nGnRE (MapMMIO) indices.
|
// 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 {
|
namespace Memory::VMM {
|
||||||
|
|
||||||
Paging* g_paging = nullptr;
|
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;
|
static kcp::Mutex pagingLock;
|
||||||
|
|
||||||
void LockUserPaging() { pagingLock.Acquire(); }
|
void LockUserPaging() { pagingLock.Acquire(); }
|
||||||
void UnlockUserPaging() { pagingLock.Release(); }
|
void UnlockUserPaging() { pagingLock.Release(); }
|
||||||
|
|
||||||
[[noreturn]] static void NotImplemented(const char* what) {
|
|
||||||
Panic(what, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ActivateKernelAddressSpace() {
|
void ActivateKernelAddressSpace() {
|
||||||
Arch::WritePageTableRoot(g_paging->KernelRoot());
|
Arch::WritePageTableRoot(g_paging->KernelRoot());
|
||||||
}
|
}
|
||||||
@@ -36,75 +75,454 @@ namespace Memory::VMM {
|
|||||||
Arch::FlushTlbAll();
|
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() {
|
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&,
|
// Kernel mappings of lower-half addresses (EFI identity mappings) go to
|
||||||
const montauk::boot::Framebuffer&) {
|
// the kernel contexts' TTBR0 table.
|
||||||
NotImplemented("aarch64: Paging::Init not implemented");
|
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) {
|
uint64_t bootTtbr1 = ReadTtbr1();
|
||||||
NotImplemented("aarch64: Paging::Map not implemented");
|
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) {
|
static void MapKernelPage(uint64_t root, uint64_t phys, uint64_t va, uint64_t attr, const char* what) {
|
||||||
NotImplemented("aarch64: Paging::MapMMIO not implemented");
|
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) {
|
void Paging::Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||||
NotImplemented("aarch64: Paging::MapWC not implemented");
|
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) {
|
void Paging::MapMMIO(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||||
NotImplemented("aarch64: Paging::MapUser not implemented");
|
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) {
|
void Paging::MapWC(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||||
NotImplemented("aarch64: Paging::GetPhysAddr not implemented");
|
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) {
|
std::uint64_t Paging::GetPhysAddr(std::uint64_t virtualAddress) {
|
||||||
return GetPhysAddr(rootPhys, virtualAddress);
|
return GetPhysAddr(rootPhys, virtualAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The kernel half lives in TTBR1, so a new address space starts empty.
|
||||||
std::uint64_t Paging::CreateUserAddressSpace() {
|
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) {
|
bool Paging::MapUserIn(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||||
NotImplemented("aarch64: Paging::MapUserIn not implemented");
|
return MapUserInPermissions(root, physicalAddress, virtualAddress, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Paging::MapUserInPermissions(std::uint64_t, std::uint64_t, std::uint64_t, bool, bool) {
|
static bool MapUserLeaf(uint64_t root, uint64_t phys, uint64_t va, Descriptor desc, const char* what) {
|
||||||
NotImplemented("aarch64: Paging::MapUserInPermissions not implemented");
|
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) {
|
bool Paging::MapUserInPermissions(std::uint64_t root, std::uint64_t physicalAddress,
|
||||||
NotImplemented("aarch64: Paging::ProtectUserIn not implemented");
|
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) {
|
bool Paging::MapUserInWC(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||||
NotImplemented("aarch64: Paging::MapUserInWC not implemented");
|
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) {
|
// Permission changes need no break-before-make, only the TLB flush.
|
||||||
NotImplemented("aarch64: Paging::UnmapUserIn not implemented");
|
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) {
|
void Paging::UnmapUserIn(std::uint64_t root, std::uint64_t virtualAddress) {
|
||||||
NotImplemented("aarch64: Paging::FreeUserHalf not implemented");
|
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) {
|
void Paging::FreeUserHalf(std::uint64_t root) {
|
||||||
NotImplemented("aarch64: Paging::IsUserRangeAccessible not implemented");
|
// 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&) {
|
bool Paging::IsUserRangeAccessible(std::uint64_t root, std::uint64_t virtualAddress,
|
||||||
NotImplemented("aarch64: Paging::MapEfiRuntime not implemented");
|
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, "PSTATE (SPSR)", frame->spsr, BoxWidth);
|
||||||
PrintBoxedHex(kerr, "Stack Pointer", frame->sp, BoxWidth);
|
PrintBoxedHex(kerr, "Stack Pointer", frame->sp, BoxWidth);
|
||||||
PrintBoxedHex(kerr, "Link Register (x30)", frame->x[30], 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 {
|
struct CpuState {
|
||||||
uint64_t mpidr; // MPIDR_EL1 affinity of this CPU
|
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
|
// TPIDR_EL1 holds the CpuData pointer (zeroed in EarlyInitialize until
|
||||||
|
|||||||
@@ -7,16 +7,73 @@
|
|||||||
#include <Arch/Platform.hpp>
|
#include <Arch/Platform.hpp>
|
||||||
#include <Arch/Cpu.hpp>
|
#include <Arch/Cpu.hpp>
|
||||||
#include <Arch/Smp.hpp>
|
#include <Arch/Smp.hpp>
|
||||||
|
#include <Arch/aarch64/Gic.hpp>
|
||||||
|
#include <Arch/aarch64/Bringup.hpp>
|
||||||
|
#include <ACPI/FADT.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
|
extern "C" char ArchExceptionVectors[]; // Vectors.S
|
||||||
|
|
||||||
namespace Arch {
|
namespace Arch {
|
||||||
|
|
||||||
void EarlyInitialize() {
|
// SCTLR_EL1.WXN: writable memory is execute-never
|
||||||
// No per-CPU data yet: Smp::TryGetCurrentCpuData must see null.
|
static constexpr uint64_t SctlrWxn = 1ULL << 19;
|
||||||
asm volatile("msr tpidr_el1, xzr");
|
// 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
|
// CPACR_EL1.FPEN = 0b11: FP/SIMD usable at EL0 and EL1 (the kernel
|
||||||
// itself is built -mgeneral-regs-only; Context.S saves user state).
|
// 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));
|
asm volatile("msr cpacr_el1, %0\n\tisb" :: "r"(cpacr));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(aarch64): install the VBAR_EL1 vector table (EL1/EL0 synchronous
|
void EarlyInitialize() {
|
||||||
// exceptions -> panic / SyscallDispatch, IRQ -> GIC -> ArchIrqDispatch).
|
// No per-CPU data yet: Smp::TryGetCurrentCpuData must see null.
|
||||||
|
asm volatile("msr tpidr_el1, xzr");
|
||||||
|
|
||||||
|
InitializeCpuRegisters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installed by EarlyInitialize.
|
||||||
void InitializeTraps() {
|
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
|
// MAIR_EL1 attribute indices are chosen together with the page-table
|
||||||
@@ -37,10 +101,12 @@ namespace Arch {
|
|||||||
void InitializeMemoryAttributes() {
|
void InitializeMemoryAttributes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(aarch64): GIC distributor/redistributor from the ACPI MADT.
|
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||||
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader*) {
|
|
||||||
Smp::InitBsp();
|
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() {
|
void InitializePlatformDevices() {
|
||||||
@@ -53,12 +119,22 @@ namespace Arch {
|
|||||||
void LateInitialize() {
|
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() {
|
void Reboot() {
|
||||||
|
PsciCall(PsciSystemReset);
|
||||||
HaltForever();
|
HaltForever();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PowerOff() {
|
||||||
|
if (g_psci != PsciConduit::None) {
|
||||||
|
PsciCall(PsciSystemOff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
/*
|
/*
|
||||||
* Smp.cpp
|
* 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
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arch/Smp.hpp>
|
#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 <Libraries/Memory.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Timekeeping/Timer.hpp>
|
||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
@@ -47,14 +53,83 @@ namespace Smp {
|
|||||||
<< bsp.arch.mpidr << ")";
|
<< 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) {
|
void BootAPs(const montauk::boot::SmpInfo& smp) {
|
||||||
if (smp.cpuCount > 1) {
|
if (smp.cpus == nullptr || smp.startCpu == nullptr || smp.cpuCount <= 1) {
|
||||||
g_detectedCpuCount = (int)smp.cpuCount;
|
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
|
int apIndex = 1;
|
||||||
// (reschedule/TLB-shootdown SGIs) and the per-CPU generic timer work.
|
for (uint64_t i = 0; i < smp.cpuCount && apIndex < MaxCPUs; i++) {
|
||||||
KernelLogStream(WARNING, "SMP") << "aarch64: secondary CPUs not started ("
|
montauk::boot::BootCpu& info = smp.cpus[i];
|
||||||
<< base::dec << smp.cpuCount << " reported)";
|
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 {
|
namespace Arch {
|
||||||
|
|
||||||
// Saved by the EL0 synchronous-exception vector for `svc #0`:
|
// The exception frame Vectors.S saves for `svc #0` (the same layout as
|
||||||
// x0-x5 carry the arguments and x8 the system-call number.
|
// Arch::PanicFrame): x0-x5 carry the arguments and x8 the system-call
|
||||||
|
// number; the result is written back to x0.
|
||||||
struct SyscallFrame {
|
struct SyscallFrame {
|
||||||
uint64_t arg1, arg2, arg3, arg4, arg5, arg6; // x0-x5
|
uint64_t arg1, arg2, arg3, arg4, arg5, arg6; // x0-x5
|
||||||
|
uint64_t x6, x7;
|
||||||
uint64_t syscall_nr; // x8
|
uint64_t syscall_nr; // x8
|
||||||
uint64_t elr, spsr, sp_el0; // user PC, PSTATE, SP
|
uint64_t x9_x30[22];
|
||||||
uint64_t x19_x30[12]; // callee-saved + LR
|
uint64_t sp_el0, elr, spsr, esr, far;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(SyscallFrame) == 288, "must match Vectors.S");
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,116 @@
|
|||||||
/*
|
/*
|
||||||
* Timer.cpp
|
* 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
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arch/Timer.hpp>
|
#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
|
// The generic timer counts at CNTFRQ_EL0 on every CPU, so it needs no
|
||||||
// CNTFRQ_EL0, re-arming on each tick, once the GIC delivers its PPI.
|
// 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 {
|
namespace Arch::Timer {
|
||||||
|
|
||||||
bool InitializeBsp(uint32_t) {
|
// CNTV_CTL_EL0
|
||||||
return false;
|
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() {
|
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));
|
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) {
|
bool ComposeMsi(uint8_t irq, MsiMessage& out) {
|
||||||
// Physical destination mode, fixed delivery, edge triggered, to the
|
// Physical destination mode, fixed delivery, edge triggered, to the
|
||||||
// BSP (APIC ID 0 -- the destination field is left zero, as every
|
// BSP (APIC ID 0 -- the destination field is left zero, as every
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ namespace Arch {
|
|||||||
asm volatile("mfence" ::: "memory");
|
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() {
|
inline void EnableInterrupts() {
|
||||||
asm volatile("sti" ::: "memory");
|
asm volatile("sti" ::: "memory");
|
||||||
}
|
}
|
||||||
@@ -79,6 +88,15 @@ namespace Arch {
|
|||||||
asm volatile("wbinvd" ::: "memory");
|
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) {
|
inline void FlushTlbPage(uint64_t virtualAddress) {
|
||||||
asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory");
|
asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,4 +77,8 @@ namespace Arch {
|
|||||||
__builtin_unreachable();
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// x86 powers off through ACPI S5 (AcpiShutdown).
|
||||||
|
void PowerOff() {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -27,10 +27,12 @@
|
|||||||
#include <Pci/Pci.hpp>
|
#include <Pci/Pci.hpp>
|
||||||
#include <Timekeeping/Timer.hpp>
|
#include <Timekeeping/Timer.hpp>
|
||||||
#include <Drivers/Init.hpp>
|
#include <Drivers/Init.hpp>
|
||||||
|
#include <Drivers/Serial/Uart.hpp>
|
||||||
#include <Graphics/Framebuffer.hpp>
|
#include <Graphics/Framebuffer.hpp>
|
||||||
#include <Arch/Cpu.hpp>
|
#include <Arch/Cpu.hpp>
|
||||||
#include <Arch/Platform.hpp>
|
#include <Arch/Platform.hpp>
|
||||||
#include <Fs/Boot.hpp>
|
#include <Fs/Boot.hpp>
|
||||||
|
#include <Fs/Vfs.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
#include <Ipc/Ipc.hpp>
|
#include <Ipc/Ipc.hpp>
|
||||||
#include <Api/Syscall.hpp>
|
#include <Api/Syscall.hpp>
|
||||||
@@ -111,6 +113,8 @@ extern "C" void kmain() {
|
|||||||
|
|
||||||
Hal::ACPI g_acpi((Hal::ACPI::XSDP*)Memory::HHDM(boot.rsdpPhysical));
|
Hal::ACPI g_acpi((Hal::ACPI::XSDP*)Memory::HHDM(boot.rsdpPhysical));
|
||||||
|
|
||||||
|
Drivers::Serial::InitializeConsole(g_acpi.GetXSDT());
|
||||||
|
|
||||||
if (g_acpi.GetXSDT() != nullptr) {
|
if (g_acpi.GetXSDT() != nullptr) {
|
||||||
Hal::AcpiShutdown::Initialize(g_acpi.GetXSDT());
|
Hal::AcpiShutdown::Initialize(g_acpi.GetXSDT());
|
||||||
|
|
||||||
@@ -135,6 +139,8 @@ extern "C" void kmain() {
|
|||||||
// power control on the answer.
|
// power control on the answer.
|
||||||
Hal::AML::InitializeNamespaceObjects();
|
Hal::AML::InitializeNamespaceObjects();
|
||||||
|
|
||||||
|
Pci::AssignInterruptLines();
|
||||||
|
|
||||||
Drivers::ProbeEarly();
|
Drivers::ProbeEarly();
|
||||||
Drivers::InitializeGraphics();
|
Drivers::InitializeGraphics();
|
||||||
|
|
||||||
@@ -173,6 +179,11 @@ extern "C" void kmain() {
|
|||||||
|
|
||||||
Arch::LateInitialize();
|
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();
|
Kt::SuppressKernelLog();
|
||||||
Sched::Spawn("0:/os/init.elf");
|
Sched::Spawn("0:/os/init.elf");
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ namespace kcp {
|
|||||||
Arch::Pause();
|
Arch::Pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bool TryAcquire() {
|
||||||
|
return !flag.test_and_set(std::memory_order_acquire);
|
||||||
|
}
|
||||||
void Release() {
|
void Release() {
|
||||||
flag.clear(std::memory_order_release);
|
flag.clear(std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ namespace Drivers::Audio::IntelHda {
|
|||||||
// Write verb to CORB
|
// Write verb to CORB
|
||||||
g_corb[wp] = verb;
|
g_corb[wp] = verb;
|
||||||
|
|
||||||
// Update write pointer
|
// Update write pointer once the verb is visible
|
||||||
|
Arch::DmaWriteFence();
|
||||||
Write16(REG_CORBWP, wp);
|
Write16(REG_CORBWP, wp);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -202,6 +203,7 @@ namespace Drivers::Audio::IntelHda {
|
|||||||
uint16_t wp = Read16(REG_RIRBWP) & 0xFF;
|
uint16_t wp = Read16(REG_RIRBWP) & 0xFF;
|
||||||
|
|
||||||
if (g_rirbReadPtr != wp) {
|
if (g_rirbReadPtr != wp) {
|
||||||
|
Arch::DmaReadFence();
|
||||||
g_rirbReadPtr = (g_rirbReadPtr + 1) % g_rirbSize;
|
g_rirbReadPtr = (g_rirbReadPtr + 1) % g_rirbSize;
|
||||||
|
|
||||||
uint32_t ex = g_rirb[g_rirbReadPtr].ResponseEx;
|
uint32_t ex = g_rirb[g_rirbReadPtr].ResponseEx;
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
#include "Mixer.hpp"
|
#include "Mixer.hpp"
|
||||||
#include "IntelHda.hpp"
|
#include "IntelHda.hpp"
|
||||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
#include <Drivers/Bluetooth/A2dp.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/Avrcp.hpp>
|
#include <Drivers/Bluetooth/Avrcp.hpp>
|
||||||
#include <Memory/PageFrameAllocator.hpp>
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
#include <Memory/HHDM.hpp>
|
#include <Memory/HHDM.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
@@ -146,8 +146,8 @@ namespace Drivers::Audio::Mixer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool BluetoothReady() {
|
static bool BluetoothReady() {
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return false;
|
if (!Drivers::Bluetooth::IsInitialized()) return false;
|
||||||
return Drivers::USB::Bluetooth::A2dp::IsReady();
|
return Drivers::Bluetooth::A2dp::IsReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SyncHdaMasterMute() {
|
static void SyncHdaMasterMute() {
|
||||||
@@ -236,7 +236,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
static uint32_t BackendWriteSpace() {
|
static uint32_t BackendWriteSpace() {
|
||||||
if (g_output == Output::Bluetooth) {
|
if (g_output == Output::Bluetooth) {
|
||||||
return g_btOpened
|
return g_btOpened
|
||||||
? Drivers::USB::Bluetooth::A2dp::GetWriteSpace() : 0;
|
? Drivers::Bluetooth::A2dp::GetWriteSpace() : 0;
|
||||||
}
|
}
|
||||||
return g_hdaOpened ? IntelHda::GetWriteSpace(g_hdaHandle) : 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) {
|
static int BackendWrite(const uint8_t* data, uint32_t size) {
|
||||||
if (g_output == Output::Bluetooth) {
|
if (g_output == Output::Bluetooth) {
|
||||||
return g_btOpened
|
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;
|
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()) {
|
if (g_output == Output::Bluetooth && !g_btOpened && BluetoothReady()) {
|
||||||
g_switchingOutput = true;
|
g_switchingOutput = true;
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
bool btOk = Drivers::USB::Bluetooth::A2dp::ConfigureStream(
|
bool btOk = Drivers::Bluetooth::A2dp::ConfigureStream(
|
||||||
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
||||||
Drivers::USB::Bluetooth::A2dp::StartStream();
|
Drivers::Bluetooth::A2dp::StartStream();
|
||||||
g_lock.Acquire();
|
g_lock.Acquire();
|
||||||
g_btOpened = btOk;
|
g_btOpened = btOk;
|
||||||
if (!btOk) g_output = Output::Hda;
|
if (!btOk) g_output = Output::Hda;
|
||||||
if (btOk)
|
if (btOk)
|
||||||
Drivers::USB::Bluetooth::A2dp::SetMuted(
|
Drivers::Bluetooth::A2dp::SetMuted(
|
||||||
g_masterMute.load(std::memory_order_acquire));
|
g_masterMute.load(std::memory_order_acquire));
|
||||||
g_switchingOutput = false;
|
g_switchingOutput = false;
|
||||||
wakeSwitchWaiters = true;
|
wakeSwitchWaiters = true;
|
||||||
@@ -515,7 +515,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
if (closeBt) g_btOpened = false;
|
if (closeBt) g_btOpened = false;
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
if (closeHda) IntelHda::Close(hdaHandle);
|
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 (ringToFree) FreeRing(ringToFree);
|
||||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
@@ -535,7 +535,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
bool serviceBluetooth = g_output == Output::Bluetooth;
|
bool serviceBluetooth = g_output == Output::Bluetooth;
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
if (serviceBluetooth)
|
if (serviceBluetooth)
|
||||||
Drivers::USB::Bluetooth::A2dp::ServiceMedia();
|
Drivers::Bluetooth::A2dp::ServiceMedia();
|
||||||
return (int)written;
|
return (int)written;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,7 +651,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
if (changed) BumpSerialLocked();
|
if (changed) BumpSerialLocked();
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
if (closeHda) IntelHda::Close(hdaHandle);
|
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]);
|
for (int i = 0; i < ringCount; i++) FreeRing(ringsToFree[i]);
|
||||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
@@ -668,7 +668,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
Drivers::USB::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
|
Drivers::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
|
||||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,7 +688,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
|
|
||||||
if (changed) SyncHdaMasterMute();
|
if (changed) SyncHdaMasterMute();
|
||||||
if (changed)
|
if (changed)
|
||||||
Drivers::USB::Bluetooth::A2dp::SetMuted(muted);
|
Drivers::Bluetooth::A2dp::SetMuted(muted);
|
||||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,11 +733,11 @@ namespace Drivers::Audio::Mixer {
|
|||||||
}
|
}
|
||||||
} else if (needBackend && output == Output::Bluetooth) {
|
} else if (needBackend && output == Output::Bluetooth) {
|
||||||
prepared = BluetoothReady() &&
|
prepared = BluetoothReady() &&
|
||||||
Drivers::USB::Bluetooth::A2dp::ConfigureStream(
|
Drivers::Bluetooth::A2dp::ConfigureStream(
|
||||||
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
||||||
Drivers::USB::Bluetooth::A2dp::StartStream();
|
Drivers::Bluetooth::A2dp::StartStream();
|
||||||
if (prepared)
|
if (prepared)
|
||||||
Drivers::USB::Bluetooth::A2dp::SetMuted(
|
Drivers::Bluetooth::A2dp::SetMuted(
|
||||||
g_masterMute.load(std::memory_order_acquire));
|
g_masterMute.load(std::memory_order_acquire));
|
||||||
} else if (output == Output::Hda) {
|
} else if (output == Output::Hda) {
|
||||||
prepared = IntelHda::IsInitialized();
|
prepared = IntelHda::IsInitialized();
|
||||||
@@ -779,7 +779,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
|
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
|
||||||
|
|
||||||
if (closeHda) IntelHda::Close(oldHdaHandle);
|
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);
|
Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -815,7 +815,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
wake = true;
|
wake = true;
|
||||||
}
|
}
|
||||||
g_lock.Release();
|
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);
|
if (wake) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include "Sbc.hpp"
|
#include "Sbc.hpp"
|
||||||
#include "L2cap.hpp"
|
#include "L2cap.hpp"
|
||||||
#include "Hci.hpp"
|
#include "Hci.hpp"
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
@@ -18,7 +17,7 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::A2dp {
|
namespace Drivers::Bluetooth::A2dp {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// AVDTP constants
|
// AVDTP constants
|
||||||
@@ -222,7 +221,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents(); // AVDTP responses arrive as ACL data
|
Hci::DrainEvents(); // AVDTP responses arrive as ACL data
|
||||||
if (g_avdtpResponseReady.load(std::memory_order_acquire)) {
|
if (g_avdtpResponseReady.load(std::memory_order_acquire)) {
|
||||||
// Freeze this mailbox before returning to the caller. A late
|
// Freeze this mailbox before returning to the caller. A late
|
||||||
@@ -557,7 +556,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
|
|
||||||
void OnChannelReady(uint16_t l2capCid) {
|
void OnChannelReady(uint16_t l2capCid) {
|
||||||
// Invoked from L2CAP inside the ACL receive path -- i.e. NESTED under
|
// 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
|
// must NOT run the blocking AVDTP signaling chain here; a nested poll
|
||||||
// would stall and every WaitAvdtpResponse() would time out. Just record
|
// would stall and every WaitAvdtpResponse() would time out. Just record
|
||||||
// the channel; StartSource() drives the handshakes from top-level
|
// the channel; StartSource() drives the handshakes from top-level
|
||||||
@@ -1161,7 +1160,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
bool answered = false;
|
bool answered = false;
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
if (g_sdpRspReady.load(std::memory_order_acquire)) {
|
if (g_sdpRspReady.load(std::memory_order_acquire)) {
|
||||||
answered = true;
|
answered = true;
|
||||||
@@ -1196,7 +1195,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
static bool WaitEncrypted(uint32_t timeoutMs) {
|
static bool WaitEncrypted(uint32_t timeoutMs) {
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
Hci::ProcessPendingCommands();
|
Hci::ProcessPendingCommands();
|
||||||
auto* c = Hci::GetConnection(g_aclHandle.load(std::memory_order_acquire));
|
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();
|
uint64_t waitStart = Timekeeping::GetMilliseconds();
|
||||||
while (g_sourceSetupActive.load(std::memory_order_acquire)
|
while (g_sourceSetupActive.load(std::memory_order_acquire)
|
||||||
&& Timekeeping::GetMilliseconds() - waitStart < timeoutMs) {
|
&& Timekeeping::GetMilliseconds() - waitStart < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
for (int j = 0; j < 100; j++) Arch::Pause();
|
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) {
|
if (g_sigCid.load(std::memory_order_acquire) == 0) {
|
||||||
uint64_t lStart = Timekeeping::GetMilliseconds();
|
uint64_t lStart = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - lStart < 1000) {
|
while (Timekeeping::GetMilliseconds() - lStart < 1000) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
Hci::ProcessPendingCommands();
|
Hci::ProcessPendingCommands();
|
||||||
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
|
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 attemptWaitMs = timeoutMs < 1500 ? timeoutMs : 1500;
|
||||||
uint32_t waitBudgetMs = attemptWaitMs;
|
uint32_t waitBudgetMs = attemptWaitMs;
|
||||||
while (Timekeeping::GetMilliseconds() - sigStart < waitBudgetMs) {
|
while (Timekeeping::GetMilliseconds() - sigStart < waitBudgetMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
|
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
|
||||||
// PENDING is explicitly non-final. A peer performing service
|
// PENDING is explicitly non-final. A peer performing service
|
||||||
@@ -1363,7 +1362,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
if (sig) L2cap::FreeChannel(sig);
|
if (sig) L2cap::FreeChannel(sig);
|
||||||
uint64_t st = Timekeeping::GetMilliseconds();
|
uint64_t st = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - st < 400) {
|
while (Timekeeping::GetMilliseconds() - st < 400) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
for (int j = 0; j < 200; j++) Arch::Pause();
|
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)) {
|
if (g_peerConfigured.load(std::memory_order_acquire)) {
|
||||||
uint64_t peerStart = Timekeeping::GetMilliseconds();
|
uint64_t peerStart = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - peerStart < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - peerStart < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
State s = g_state.load(std::memory_order_acquire);
|
State s = g_state.load(std::memory_order_acquire);
|
||||||
if (s == State::Open || s == State::Streaming) break;
|
if (s == State::Open || s == State::Streaming) break;
|
||||||
@@ -1483,7 +1482,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
uint64_t mStart = Timekeeping::GetMilliseconds();
|
uint64_t mStart = Timekeeping::GetMilliseconds();
|
||||||
while (g_mediaCid.load(std::memory_order_acquire) == 0
|
while (g_mediaCid.load(std::memory_order_acquire) == 0
|
||||||
&& Timekeeping::GetMilliseconds() - mStart < kMediaWaitMs) {
|
&& Timekeeping::GetMilliseconds() - mStart < kMediaWaitMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
auto* ch = media ? L2cap::GetChannel(media) : nullptr;
|
auto* ch = media ? L2cap::GetChannel(media) : nullptr;
|
||||||
if (ch && ch->Configured) {
|
if (ch && ch->Configured) {
|
||||||
@@ -1838,6 +1837,36 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
// ConfigureStream
|
// 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) {
|
bool ConfigureStream(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
|
||||||
// Encoder configuration and PumpMedia both mutate the SBC encoder.
|
// Encoder configuration and PumpMedia both mutate the SBC encoder.
|
||||||
// Device switching normally configures an Open stream, but explicitly
|
// 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).
|
// agreed to (Init's defaults may differ from the negotiated subset).
|
||||||
if (g_cfgSbc[0] || g_cfgSbc[1]) {
|
if (g_cfgSbc[0] || g_cfgSbc[1]) {
|
||||||
Sbc::Configure(&g_sbcEncoder, g_cfgSbc[0], g_cfgSbc[1], g_cfgSbc[3]);
|
Sbc::Configure(&g_sbcEncoder, g_cfgSbc[0], g_cfgSbc[1], g_cfgSbc[3]);
|
||||||
|
FitBitpoolToTransport(sampleRate);
|
||||||
}
|
}
|
||||||
g_sbcInitialized = true;
|
g_sbcInitialized = true;
|
||||||
g_seqNum = 0;
|
g_seqNum = 0;
|
||||||
@@ -2136,7 +2166,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
|
|
||||||
void ServiceMedia() {
|
void ServiceMedia() {
|
||||||
if (!AcquireMediaService()) return;
|
if (!AcquireMediaService()) return;
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
PumpMedia();
|
PumpMedia();
|
||||||
g_serviceActive.store(false, std::memory_order_release);
|
g_serviceActive.store(false, std::memory_order_release);
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::A2dp {
|
namespace Drivers::Bluetooth::A2dp {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// A2DP stream states
|
// A2DP stream states
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Avrcp {
|
namespace Drivers::Bluetooth::Avrcp {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Constants
|
// Constants
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Avrcp {
|
namespace Drivers::Bluetooth::Avrcp {
|
||||||
|
|
||||||
// Process an AVCTP packet arriving on an AVRCP control channel (PSM 0x0017).
|
// Process an AVCTP packet arriving on an AVRCP control channel (PSM 0x0017).
|
||||||
// Implements a minimal AVRCP Target: unit/subunit info, honest rejection of
|
// Implements a minimal AVRCP Target: unit/subunit info, honest rejection of
|
||||||
+55
-18
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Bluetooth.cpp
|
* 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
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "Hci.hpp"
|
#include "Hci.hpp"
|
||||||
#include "A2dp.hpp"
|
#include "A2dp.hpp"
|
||||||
#include "IntelFirmware.hpp"
|
#include "IntelFirmware.hpp"
|
||||||
|
#include "Broadcom.hpp"
|
||||||
#include <Drivers/Audio/Mixer.hpp>
|
#include <Drivers/Audio/Mixer.hpp>
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
#include <Drivers/USB/Xhci.hpp>
|
||||||
#include <Drivers/USB/UsbDevice.hpp>
|
#include <Drivers/USB/UsbDevice.hpp>
|
||||||
@@ -22,7 +23,9 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth {
|
namespace Drivers::Bluetooth {
|
||||||
|
|
||||||
|
namespace Xhci = Drivers::USB::Xhci;
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// State
|
// State
|
||||||
@@ -205,7 +208,9 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
void RegisterAdapter(uint8_t slotId) {
|
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";
|
KernelLogStream(WARNING, "BT") << "Bluetooth adapter already registered";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -249,17 +254,47 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
CompleteInit();
|
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)
|
// CompleteInit — firmware-dependent HCI bring-up (needs VFS/ramdisk)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
static void CompleteInit() {
|
static void CompleteInit() {
|
||||||
auto* dev = Xhci::GetDevice(g_slotId);
|
bool didReset = false;
|
||||||
if (!dev) return;
|
|
||||||
|
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)
|
// Intel-specific initialization (firmware download + HCI Reset)
|
||||||
bool didReset = false;
|
if (dev && IsIntelBt(dev->VendorId, dev->ProductId)) {
|
||||||
if (IsIntelBt(dev->VendorId, dev->ProductId)) {
|
Hci::SetVendor(Hci::Vendor::Intel);
|
||||||
if (InitIntelBluetooth()) {
|
if (InitIntelBluetooth()) {
|
||||||
didReset = true; // InitIntelBluetooth already sent HCI Reset
|
didReset = true; // InitIntelBluetooth already sent HCI Reset
|
||||||
} else {
|
} else {
|
||||||
@@ -280,9 +315,11 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply a persisted BD_ADDR override (0:/config/bluetooth.toml) now,
|
// A Broadcom part still on its chip-wide default address gets the
|
||||||
// after the last reset and before the address is read back, so the
|
// board's, then a persisted BD_ADDR override (0:/config/bluetooth.toml)
|
||||||
// whole bring-up below uses the overridden address.
|
// 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();
|
ApplyConfiguredAddress();
|
||||||
|
|
||||||
// Read BD_ADDR
|
// Read BD_ADDR
|
||||||
@@ -387,7 +424,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
void ServiceDeferredInit() {
|
void ServiceDeferredInit() {
|
||||||
if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return;
|
if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return;
|
||||||
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk still not mounted
|
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
|
// Claim the pending init (this runs from the idle loop; make sure only
|
||||||
// one pass performs the bring-up). The firmware download inside takes
|
// 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)) {
|
!g_initialized && Fs::Vfs::IsDriveRegistered(0)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return g_initialized && Hci::HasPendingCommands();
|
return g_initialized && (Hci::HasPendingCommands() || Hci::TransportHasRxData());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServiceEvents() {
|
void ServiceEvents() {
|
||||||
if (!g_initialized) return;
|
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
|
A2dp::ServiceMedia(); // reap events and feed queued media
|
||||||
// DrainEvents may just have queued an accept/auth/encryption reply.
|
// DrainEvents may just have queued an accept/auth/encryption reply.
|
||||||
// Send it in this same service pass instead of adding a scheduler-turn
|
// 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();
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||||
bool anyActive = true;
|
bool anyActive = true;
|
||||||
while (anyActive && Timekeeping::GetMilliseconds() - t0 < 2000) {
|
while (anyActive && Timekeeping::GetMilliseconds() - t0 < 2000) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
anyActive = false;
|
anyActive = false;
|
||||||
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||||
@@ -548,7 +585,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Poll until inquiry completes or timeout
|
// Poll until inquiry completes or timeout
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Hci::IsInquiryActive() && (Timekeeping::GetMilliseconds() - start < timeoutMs)) {
|
while (Hci::IsInquiryActive() && (Timekeeping::GetMilliseconds() - start < timeoutMs)) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
|
|
||||||
for (int j = 0; j < 200; j++) {
|
for (int j = 0; j < 200; j++) {
|
||||||
@@ -566,7 +603,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
uint64_t cancelStart = Timekeeping::GetMilliseconds();
|
uint64_t cancelStart = Timekeeping::GetMilliseconds();
|
||||||
while (Hci::IsInquiryActive()
|
while (Hci::IsInquiryActive()
|
||||||
&& Timekeeping::GetMilliseconds() - cancelStart < 250) {
|
&& Timekeeping::GetMilliseconds() - cancelStart < 250) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
for (int j = 0; j < 100; j++)
|
for (int j = 0; j < 100; j++)
|
||||||
Arch::Pause();
|
Arch::Pause();
|
||||||
@@ -612,7 +649,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Wait for Connection Complete event
|
// Wait for Connection Complete event
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (!target && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (!target && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
target = FindAclConnection(bdAddr);
|
target = FindAclConnection(bdAddr);
|
||||||
for (int j = 0; j < 200; j++) {
|
for (int j = 0; j < 200; j++) {
|
||||||
@@ -631,7 +668,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
if (reusedAcl) Hci::AuthenticateLink(target->Handle);
|
if (reusedAcl) Hci::AuthenticateLink(target->Handle);
|
||||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
Hci::ProcessPendingCommands();
|
Hci::ProcessPendingCommands();
|
||||||
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) break;
|
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) break;
|
||||||
+16
-9
@@ -8,28 +8,35 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include "Hci.hpp"
|
#include "Hci.hpp"
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth {
|
namespace Drivers::Bluetooth {
|
||||||
|
|
||||||
// Called by USB enumeration when a Bluetooth adapter is detected
|
// Called by USB enumeration when a Bluetooth adapter is detected
|
||||||
void RegisterAdapter(uint8_t slotId);
|
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
|
// Complete any firmware-dependent bring-up that was deferred because the
|
||||||
// adapter enumerated during the boot port scan, before the ramdisk (which
|
// adapter enumerated during the boot port scan, before the ramdisk (which
|
||||||
// holds the firmware images) was mounted. Safe to call unconditionally
|
// holds the firmware images) was mounted. Safe to call unconditionally
|
||||||
// once the boot filesystems are up; a no-op if nothing is pending.
|
// once the boot filesystems are up; a no-op if nothing is pending.
|
||||||
void ServiceDeferredInit();
|
void ServiceDeferredInit();
|
||||||
|
|
||||||
// Steady-state event service: drains the xHCI event ring + Bluetooth ACL
|
// Steady-state event service: drains the transport (xHCI event ring or
|
||||||
// RX ring and flushes queued HCI replies. Called from the idle loop so
|
// 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
|
// inbound traffic (the headset's SDP/AVRCP queries, AVDTP commands, ACL
|
||||||
// credits) is processed even when no syscall happens to be pumping a BT
|
// 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
|
// 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();
|
void ServiceEvents();
|
||||||
|
|
||||||
// True when boot-deferred initialization or queued HCI control work needs
|
// True when boot-deferred initialization, queued HCI control work or
|
||||||
// an idle-context service pass. USB receive events are signaled separately
|
// received UART bytes need an idle-context service pass. USB receive
|
||||||
// by xHCI and cause the dispatcher to service Bluetooth in the same pass.
|
// events are signaled separately by xHCI and cause the dispatcher to
|
||||||
|
// service Bluetooth in the same pass.
|
||||||
bool HasDeferredWork();
|
bool HasDeferredWork();
|
||||||
|
|
||||||
// Query adapter state
|
// Query adapter state
|
||||||
@@ -40,8 +47,8 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
const uint8_t* GetBdAddr();
|
const uint8_t* GetBdAddr();
|
||||||
|
|
||||||
// Change the adapter's BD_ADDR at runtime. Drops any active links (their
|
// 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
|
// pairing/crypto was bound to the old address), issues the vendor write
|
||||||
// write (0xFC31), re-reads the controller's address into the cache and
|
// (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
|
// re-asserts scan. Returns true only if the controller reports the new
|
||||||
// address. Persisting the choice to 0:/config/bluetooth.toml is the
|
// address. Persisting the choice to 0:/config/bluetooth.toml is the
|
||||||
// caller's job (userspace); the override is re-applied on boot by
|
// 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
|
* Hci.cpp
|
||||||
* Bluetooth HCI transport over USB
|
* Bluetooth HCI transport over USB or a UART (H4)
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "A2dp.hpp"
|
#include "A2dp.hpp"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <Fs/Vfs.hpp>
|
#include <Fs/Vfs.hpp>
|
||||||
|
#include <Drivers/Serial/BtUart.hpp>
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
#include <Drivers/USB/Xhci.hpp>
|
||||||
#include <Drivers/USB/UsbDevice.hpp>
|
#include <Drivers/USB/UsbDevice.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
@@ -22,12 +23,17 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Hci {
|
namespace Drivers::Bluetooth::Hci {
|
||||||
|
|
||||||
|
namespace Xhci = Drivers::USB::Xhci;
|
||||||
|
namespace BtUart = Drivers::Serial::BtUart;
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// State
|
// State
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
static Transport g_transport = Transport::None;
|
||||||
|
static Vendor g_vendor = Vendor::Unknown;
|
||||||
static uint8_t g_slotId = 0;
|
static uint8_t g_slotId = 0;
|
||||||
static bool g_initialized = false;
|
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
|
// USB transfer callback
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -641,7 +802,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
static void PollWait(uint32_t ms) {
|
static void PollWait(uint32_t ms) {
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < ms) {
|
while (Timekeeping::GetMilliseconds() - start < ms) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
for (int j = 0; j < 100; j++) {
|
for (int j = 0; j < 100; j++) {
|
||||||
Arch::CompilerBarrier();
|
Arch::CompilerBarrier();
|
||||||
@@ -672,10 +833,20 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// the configured USB function its settling delay. StartEventPipe then
|
// the configured USB function its settling delay. StartEventPipe then
|
||||||
// arms both receive endpoints before the first bootloader command.
|
// arms both receive endpoints before the first bootloader command.
|
||||||
|
|
||||||
|
g_transport = Transport::Usb;
|
||||||
g_initialized = true;
|
g_initialized = true;
|
||||||
KernelLogStream(OK, "BT-HCI") << "HCI transport initialized on slot " << (uint64_t)slotId;
|
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.
|
// 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
|
// 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
|
// 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) {
|
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;
|
uint16_t expected = 0;
|
||||||
if (!g_commandOpcode.compare_exchange_strong(expected, opcode,
|
if (!g_commandOpcode.compare_exchange_strong(expected, opcode,
|
||||||
@@ -745,6 +917,27 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
<< base::hex << (uint64_t)opcode << base::dec;
|
<< 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.
|
// Use DMA-allocated buffer (not stack) for the command data.
|
||||||
// xHCI reads from this buffer via DMA for OUT transfers.
|
// xHCI reads from this buffer via DMA for OUT transfers.
|
||||||
memset(g_cmdDmaBuf, 0, 512);
|
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
|
// 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
|
// is reaped by the active PollEvents after we return. The command was
|
||||||
// already submitted (fire-and-forget); report success.
|
// already submitted (fire-and-forget); report success.
|
||||||
if (Xhci::InPollContext()) {
|
if (InPumpContext()) {
|
||||||
FinishCommand(opcode);
|
FinishCommand(opcode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -799,7 +992,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
|
|
||||||
uint8_t event[sizeof(g_eventBuf)];
|
uint8_t event[sizeof(g_eventBuf)];
|
||||||
@@ -852,7 +1045,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
|
|
||||||
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs) {
|
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs) {
|
||||||
// See WaitCommandComplete: cannot wait when nested under PollEvents.
|
// See WaitCommandComplete: cannot wait when nested under PollEvents.
|
||||||
if (Xhci::InPollContext()) {
|
if (InPumpContext()) {
|
||||||
FinishCommand(opcode);
|
FinishCommand(opcode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -860,7 +1053,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
|
|
||||||
uint8_t event[sizeof(g_eventBuf)];
|
uint8_t event[sizeof(g_eventBuf)];
|
||||||
@@ -897,10 +1090,40 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// SendAcl — via USB bulk OUT
|
// 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) {
|
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 (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();
|
g_aclTxLock.Acquire();
|
||||||
|
|
||||||
// HCI host flow control is a hard limit, not merely a media-pacing
|
// 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; }
|
uint16_t AclMaxPackets() { return g_aclMaxNum; }
|
||||||
|
|
||||||
uint32_t AclTxInFlight() {
|
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 tx = g_aclTxCount.load(std::memory_order_relaxed);
|
||||||
uint32_t done = g_aclTxDoneCount.load(std::memory_order_relaxed);
|
uint32_t done = g_aclTxDoneCount.load(std::memory_order_relaxed);
|
||||||
return (tx >= done) ? (tx - done) : 0;
|
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 maxOut = g_aclMaxNum ? g_aclMaxNum : 4;
|
||||||
uint16_t mediaLimit = maxOut > 1 ? (uint16_t)(maxOut - 1) : maxOut;
|
uint16_t mediaLimit = maxOut > 1 ? (uint16_t)(maxOut - 1) : maxOut;
|
||||||
if (AclPendingCount() >= mediaLimit) return false;
|
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;
|
if (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1420,6 +1652,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case EVT_VENDOR_SPECIFIC: {
|
case EVT_VENDOR_SPECIFIC: {
|
||||||
|
if (g_vendor != Vendor::Intel) break;
|
||||||
// Intel vendor events carry a sub-opcode in the first byte.
|
// Intel vendor events carry a sub-opcode in the first byte.
|
||||||
// 0x02 = "bootup" (operational firmware booted after 0xFC01)
|
// 0x02 = "bootup" (operational firmware booted after 0xFC01)
|
||||||
// 0x06 = "secure send result": result(1) opcode(2) status(1)
|
// 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.
|
// copying past what the controller delivered.
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < 2000) {
|
while (Timekeeping::GetMilliseconds() - start < 2000) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
|
|
||||||
uint8_t event[sizeof(g_eventBuf)];
|
uint8_t event[sizeof(g_eventBuf)];
|
||||||
@@ -1676,7 +1909,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
void ProcessPendingCommands() {
|
void ProcessPendingCommands() {
|
||||||
// Top-level only: drains queued pairing replies with real, confirmed
|
// Top-level only: drains queued pairing replies with real, confirmed
|
||||||
// control transfers. Must NOT be called from inside PollEvents.
|
// 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
|
// One consumer at a time: this runs from the Connect wait loop AND the
|
||||||
// idle-core service pump, potentially on different cores; two consumers
|
// idle-core service pump, potentially on different cores; two consumers
|
||||||
// on the lockless queue would race the tail and interleave commands.
|
// 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) {
|
bool WaitSecureSendResult(uint32_t timeoutMs, uint8_t* outResult, uint8_t* outStatus) {
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
if (g_secureResultValid) {
|
if (g_secureResultValid) {
|
||||||
if (outResult) *outResult = g_secureResult;
|
if (outResult) *outResult = g_secureResult;
|
||||||
@@ -1763,7 +1996,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
<< " byte " << g_ssBytesSent;
|
<< " byte " << g_ssBytesSent;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
|
|
||||||
g_ssBytesSent += frag;
|
g_ssBytesSent += frag;
|
||||||
@@ -1784,7 +2017,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// header is deliberately absent, and the NOCP credit counter is NOT
|
// header is deliberately absent, and the NOCP credit counter is NOT
|
||||||
// touched: these are commands, not ACL data).
|
// touched: these are commands, not ACL data).
|
||||||
bool IntelSecureSendBulk(uint8_t fragmentType, const uint8_t* data, uint32_t len) {
|
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;
|
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
|
||||||
|
|
||||||
uint32_t off = 0;
|
uint32_t off = 0;
|
||||||
@@ -1796,7 +2029,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// controller from freezing the boot path.
|
// controller from freezing the boot path.
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) {
|
while (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
if (g_aclTxTransportError.load(std::memory_order_acquire)) {
|
if (g_aclTxTransportError.load(std::memory_order_acquire)) {
|
||||||
KernelLogStream(ERROR, "BT-HCI")
|
KernelLogStream(ERROR, "BT-HCI")
|
||||||
@@ -1843,7 +2076,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
bool DrainBulkTx(uint32_t timeoutMs) {
|
bool DrainBulkTx(uint32_t timeoutMs) {
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (AclTxInFlight() > 0) {
|
while (AclTxInFlight() > 0) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
|
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
|
||||||
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||||
@@ -1872,7 +2105,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
|
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
if (g_intelBootup) return true;
|
if (g_intelBootup) return true;
|
||||||
for (int j = 0; j < 100; j++) Arch::CompilerBarrier();
|
for (int j = 0; j < 100; j++) Arch::CompilerBarrier();
|
||||||
@@ -1953,9 +2186,14 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool SetBdAddr(const uint8_t* addr) {
|
bool SetBdAddr(const uint8_t* addr) {
|
||||||
// Intel 0xFC31: 6-byte BD_ADDR, little-endian (same order as ReadBdAddr).
|
// Intel 0xFC31 / Broadcom 0xFC01: 6-byte BD_ADDR, little-endian (same
|
||||||
if (!SendCommand(OP_INTEL_WRITE_BD_ADDR, addr, 6)) return false;
|
// order as ReadBdAddr).
|
||||||
return WaitCommandComplete(OP_INTEL_WRITE_BD_ADDR, nullptr, 0, 2000);
|
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) {
|
bool Disconnect(uint16_t handle, uint8_t reason) {
|
||||||
@@ -1,23 +1,35 @@
|
|||||||
/*
|
/*
|
||||||
* Hci.hpp
|
* Hci.hpp
|
||||||
* Bluetooth HCI (Host Controller Interface) layer
|
* 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
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#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:
|
// USB transport uses different endpoints for each packet type:
|
||||||
// Commands -> Control EP0 (class request)
|
// Commands -> Control EP0 (class request)
|
||||||
// ACL data -> Bulk OUT / Bulk IN
|
// ACL data -> Bulk OUT / Bulk IN
|
||||||
// Events -> Interrupt 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)
|
// 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_LOCAL_FEATURES = 0x1003;
|
||||||
constexpr uint16_t OP_READ_BUFFER_SIZE = 0x1005;
|
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)
|
// Intel vendor commands (OGF 0x3F)
|
||||||
constexpr uint16_t OP_INTEL_READ_VERSION = 0xFC05;
|
constexpr uint16_t OP_INTEL_READ_VERSION = 0xFC05;
|
||||||
constexpr uint16_t OP_INTEL_RESET = 0xFC01;
|
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
|
// Initialize HCI transport over USB for the given slot
|
||||||
void Initialize(uint8_t slotId);
|
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).
|
// 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
|
// 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
|
// 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.
|
// are intentionally ignored and any endpoint error is recovered here.
|
||||||
void EnableAclDataReception();
|
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);
|
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen);
|
||||||
|
|
||||||
// Wait for a Command Complete event matching the given opcode
|
// 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
|
// Wait for a Command Status event matching the given opcode
|
||||||
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs = 2000);
|
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);
|
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
|
// 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.
|
// Drives Link Key Request -> pairing; needed for bonded-device reconnects.
|
||||||
bool AuthenticateLink(uint16_t handle);
|
bool AuthenticateLink(uint16_t handle);
|
||||||
|
|
||||||
// Set the adapter's BD_ADDR (Intel vendor command 0xFC31). Used to dodge a
|
// Set the adapter's BD_ADDR (vendor command: Intel 0xFC31, Broadcom
|
||||||
// remote that holds a stale, un-clearable bond to our real address.
|
// 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);
|
bool SetBdAddr(const uint8_t* addr);
|
||||||
|
|
||||||
// Send any queued pairing replies (IO-cap / user-confirm / link-key) with
|
// Send any queued pairing replies (IO-cap / user-confirm / link-key) with
|
||||||
@@ -340,7 +392,8 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
uint16_t AclMaxPackets();
|
uint16_t AclMaxPackets();
|
||||||
|
|
||||||
// ACL packets handed to the xHCI whose bulk OUT completion has not been
|
// 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();
|
uint32_t AclTxInFlight();
|
||||||
|
|
||||||
// True when SendAcl can be called without overrunning either the
|
// True when SendAcl can be called without overrunning either the
|
||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth {
|
namespace Drivers::Bluetooth {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// .sfi / TLV format constants (from drivers/bluetooth/btintel.{c,h})
|
// .sfi / TLV format constants (from drivers/bluetooth/btintel.{c,h})
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth {
|
namespace Drivers::Bluetooth {
|
||||||
|
|
||||||
// Drive the Intel bootloader firmware download sequence for the currently
|
// Drive the Intel bootloader firmware download sequence for the currently
|
||||||
// registered adapter:
|
// registered adapter:
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
#include "Hci.hpp"
|
#include "Hci.hpp"
|
||||||
#include "A2dp.hpp"
|
#include "A2dp.hpp"
|
||||||
#include "Avrcp.hpp"
|
#include "Avrcp.hpp"
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::L2cap {
|
namespace Drivers::Bluetooth::L2cap {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// State
|
// State
|
||||||
@@ -548,7 +547,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents(); // process incoming ACL (the Connection Response!)
|
Hci::DrainEvents(); // process incoming ACL (the Connection Response!)
|
||||||
|
|
||||||
auto* ch = GetChannel(localCid);
|
auto* ch = GetChannel(localCid);
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::L2cap {
|
namespace Drivers::Bluetooth::L2cap {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// L2CAP CIDs (Channel Identifiers)
|
// L2CAP CIDs (Channel Identifiers)
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "Sbc.hpp"
|
#include "Sbc.hpp"
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Sbc {
|
namespace Drivers::Bluetooth::Sbc {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Analysis filterbank tables
|
// Analysis filterbank tables
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Sbc {
|
namespace Drivers::Bluetooth::Sbc {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// SBC configuration
|
// SBC configuration
|
||||||
+265
-2
@@ -10,15 +10,22 @@
|
|||||||
#include <Drivers/Graphics/DgpuPower.hpp>
|
#include <Drivers/Graphics/DgpuPower.hpp>
|
||||||
#include <Drivers/Net/E1000.hpp>
|
#include <Drivers/Net/E1000.hpp>
|
||||||
#include <Drivers/Net/E1000E.hpp>
|
#include <Drivers/Net/E1000E.hpp>
|
||||||
|
#include <Drivers/Net/Genet.hpp>
|
||||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
#include <Drivers/USB/Xhci.hpp>
|
||||||
#include <Drivers/Storage/Ahci.hpp>
|
#include <Drivers/Storage/Ahci.hpp>
|
||||||
#include <Drivers/Storage/Nvme.hpp>
|
#include <Drivers/Storage/Nvme.hpp>
|
||||||
|
#include <Drivers/Storage/Sdhci.hpp>
|
||||||
#include <Drivers/Storage/Gpt.hpp>
|
#include <Drivers/Storage/Gpt.hpp>
|
||||||
#include <Drivers/Audio/IntelHda.hpp>
|
#include <Drivers/Audio/IntelHda.hpp>
|
||||||
|
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Graphics/Framebuffer.hpp>
|
#include <Graphics/Framebuffer.hpp>
|
||||||
#include <Net/Net.hpp>
|
#include <Net/Net.hpp>
|
||||||
#include <Terminal/Terminal.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 {
|
namespace Drivers {
|
||||||
|
|
||||||
@@ -89,6 +96,10 @@ namespace Drivers {
|
|||||||
return Storage::Nvme::Probe(dev);
|
return Storage::Nvme::Probe(dev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool ProbeSdhci(const Pci::PciDevice& dev) {
|
||||||
|
return Storage::Sdhci::Probe(dev);
|
||||||
|
}
|
||||||
|
|
||||||
static bool ProbeIntelHda(const Pci::PciDevice& dev) {
|
static bool ProbeIntelHda(const Pci::PciDevice& dev) {
|
||||||
return Audio::IntelHda::Probe(dev);
|
return Audio::IntelHda::Probe(dev);
|
||||||
}
|
}
|
||||||
@@ -189,6 +200,18 @@ namespace Drivers {
|
|||||||
// also reports 0x01, but it exposes an I/O-space BAR0:
|
// also reports 0x01, but it exposes an I/O-space BAR0:
|
||||||
// ReadBar0 returns 0 for I/O BARs and Probe rejects it
|
// ReadBar0 returns 0 for I/O BARs and Probe rejects it
|
||||||
// cleanly, so matching it here is harmless.
|
// 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",
|
"IntelHDA",
|
||||||
0x8086, // VendorId (Intel)
|
0x8086, // VendorId (Intel)
|
||||||
@@ -227,8 +250,243 @@ namespace Drivers {
|
|||||||
Graphics::DgpuPower::PowerDownUnused();
|
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() {
|
void ProbeNormal() {
|
||||||
Pci::ProbeAll(g_driverTable, g_driverTableCount, Pci::ProbePhase::Normal);
|
Pci::ProbeAll(g_driverTable, g_driverTableCount, Pci::ProbePhase::Normal);
|
||||||
|
ProbePlatformXhci();
|
||||||
|
ProbePlatformSdhci();
|
||||||
|
ProbePlatformGenet();
|
||||||
|
ProbePlatformBroadcomWifi();
|
||||||
|
Bluetooth::ProbePlatformAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InitializeNetwork() {
|
void InitializeNetwork() {
|
||||||
@@ -241,10 +499,15 @@ namespace Drivers {
|
|||||||
Storage::Gpt::ProbeAll();
|
Storage::Gpt::ProbeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ServiceStorageHotplug() {
|
||||||
|
if (USB::Xhci::HasDeferredWork()) USB::Xhci::ProcessDeferredWork();
|
||||||
|
}
|
||||||
|
|
||||||
void InitializeAudio() {
|
void InitializeAudio() {
|
||||||
// HDA driver initializes during ProbeNormal().
|
// HDA driver initializes during ProbeNormal().
|
||||||
// Bluetooth audio (A2DP) initializes automatically during USB enumeration
|
// Bluetooth audio (A2DP) initializes with its adapter: during USB
|
||||||
// when a Bluetooth adapter is detected by the xHCI driver.
|
// 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.
|
// Post-probe: register SATA drives with VFS.
|
||||||
void InitializeStorage();
|
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.
|
// Post-probe: initialize audio subsystem.
|
||||||
void InitializeAudio();
|
void InitializeAudio();
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
#include <Arch/Interrupts.hpp>
|
#include <Arch/Interrupts.hpp>
|
||||||
#include <CppLib/Spinlock.hpp>
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
#include <Arch/Cpu.hpp>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
@@ -491,6 +492,7 @@ namespace Drivers::Net::E1000 {
|
|||||||
|
|
||||||
// Advance the tail pointer (tells the NIC there's a new packet)
|
// Advance the tail pointer (tells the NIC there's a new packet)
|
||||||
g_txTail = (g_txTail + 1) % TX_DESC_COUNT;
|
g_txTail = (g_txTail + 1) % TX_DESC_COUNT;
|
||||||
|
Arch::DmaWriteFence();
|
||||||
WriteReg(REG_TDT, g_txTail);
|
WriteReg(REG_TDT, g_txTail);
|
||||||
|
|
||||||
g_txPacketCount++;
|
g_txPacketCount++;
|
||||||
@@ -545,6 +547,7 @@ namespace Drivers::Net::E1000 {
|
|||||||
uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT;
|
uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT;
|
||||||
RxDescriptor& desc = g_rxDescs[nextIdx];
|
RxDescriptor& desc = g_rxDescs[nextIdx];
|
||||||
if (!(desc.Status & RXSTA_DD)) break;
|
if (!(desc.Status & RXSTA_DD)) break;
|
||||||
|
Arch::DmaReadFence();
|
||||||
|
|
||||||
uint16_t length = desc.Length;
|
uint16_t length = desc.Length;
|
||||||
g_rxPacketCount++;
|
g_rxPacketCount++;
|
||||||
@@ -554,6 +557,7 @@ namespace Drivers::Net::E1000 {
|
|||||||
desc.Length = 0;
|
desc.Length = 0;
|
||||||
desc.Errors = 0;
|
desc.Errors = 0;
|
||||||
g_rxTail = nextIdx;
|
g_rxTail = nextIdx;
|
||||||
|
Arch::DmaWriteFence();
|
||||||
WriteReg(REG_RDT, g_rxTail);
|
WriteReg(REG_RDT, g_rxTail);
|
||||||
processed++;
|
processed++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -667,6 +667,7 @@ namespace Drivers::Net::E1000E {
|
|||||||
if (!(desc.Status & RXSTA_DD)) {
|
if (!(desc.Status & RXSTA_DD)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Arch::DmaReadFence();
|
||||||
|
|
||||||
uint16_t length = desc.Length;
|
uint16_t length = desc.Length;
|
||||||
g_rxPacketCount++;
|
g_rxPacketCount++;
|
||||||
@@ -680,6 +681,7 @@ namespace Drivers::Net::E1000E {
|
|||||||
desc.Errors = 0;
|
desc.Errors = 0;
|
||||||
|
|
||||||
g_rxTail = nextIdx;
|
g_rxTail = nextIdx;
|
||||||
|
Arch::DmaWriteFence();
|
||||||
WriteReg(REG_RDT, g_rxTail);
|
WriteReg(REG_RDT, g_rxTail);
|
||||||
processed++;
|
processed++;
|
||||||
}
|
}
|
||||||
@@ -709,6 +711,7 @@ namespace Drivers::Net::E1000E {
|
|||||||
desc.Status = 0;
|
desc.Status = 0;
|
||||||
|
|
||||||
g_txTail = (g_txTail + 1) % TX_DESC_COUNT;
|
g_txTail = (g_txTail + 1) % TX_DESC_COUNT;
|
||||||
|
Arch::DmaWriteFence();
|
||||||
WriteReg(REG_TDT, g_txTail);
|
WriteReg(REG_TDT, g_txTail);
|
||||||
|
|
||||||
g_txPacketCount++;
|
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).
|
// Send an Ethernet frame over the air (called by the netif).
|
||||||
bool IwxConnectSendEthernet(const uint8_t* frame, uint32_t len);
|
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.
|
// Sink for decapsulated Ethernet frames, implemented by Wifi.cpp.
|
||||||
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
|
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
|
// The supplicant transmits EAPOL frames unencrypted: they are what
|
||||||
// establishes the key in the first place.
|
// 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);
|
return SendDataFrame(g_bssid, ETHERTYPE_EAPOL, body, len, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,14 +545,14 @@ namespace Drivers::Net::Wifi {
|
|||||||
static InstalledKey g_ptkKey = {};
|
static InstalledKey g_ptkKey = {};
|
||||||
static InstalledKey g_gtkKey = {};
|
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;
|
if (!IwxSetKey(tk, tkLen, 0, true, cipher, nullptr)) return false;
|
||||||
g_ptkKey = { true, 0, cipher, (uint8_t)tkLen };
|
g_ptkKey = { true, 0, cipher, (uint8_t)tkLen };
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
bool IwxWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||||
uint8_t cipher, const uint8_t* rsc) {
|
uint8_t cipher, const uint8_t* rsc) {
|
||||||
if (gtkLen != 16 && gtkLen != 32) return false;
|
if (gtkLen != 16 && gtkLen != 32) return false;
|
||||||
if (!IwxSetKey(gtk, gtkLen, keyIdx, false, cipher, rsc)) return false;
|
if (!IwxSetKey(gtk, gtkLen, keyIdx, false, cipher, rsc)) return false;
|
||||||
g_gtkKey = { true, keyIdx, cipher, (uint8_t)gtkLen };
|
g_gtkKey = { true, keyIdx, cipher, (uint8_t)gtkLen };
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "Wifi.hpp"
|
#include "Wifi.hpp"
|
||||||
#include <Arch/Cpu.hpp>
|
#include <Arch/Cpu.hpp>
|
||||||
#include "Iwx.hpp"
|
#include "Iwx.hpp"
|
||||||
|
#include "Brcmf.hpp"
|
||||||
#include "Wpa.hpp"
|
#include "Wpa.hpp"
|
||||||
#include <Fs/Vfs.hpp>
|
#include <Fs/Vfs.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
@@ -34,6 +35,72 @@ namespace Drivers::Net::Wifi {
|
|||||||
static std::atomic<bool> g_initPending{false};
|
static std::atomic<bool> g_initPending{false};
|
||||||
static bool g_initialized = 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
|
// Scan result table
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -141,22 +208,12 @@ namespace Drivers::Net::Wifi {
|
|||||||
uint8_t RsnLen = 0;
|
uint8_t RsnLen = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
|
// The information elements of a beacon or probe response; `privacy` is
|
||||||
// 24-byte MAC header, then timestamp(8) + beacon interval(2) + caps(2).
|
// the capability field's Privacy bit.
|
||||||
constexpr uint32_t HDR = 24;
|
static void ParseIes(const uint8_t* ie, const uint8_t* end, bool privacy, ParsedBeacon* out) {
|
||||||
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;
|
|
||||||
|
|
||||||
bool haveRsn = false, haveWpa = false;
|
bool haveRsn = false, haveWpa = false;
|
||||||
|
|
||||||
const uint8_t* ie = frame + HDR + FIXED;
|
while (ie && ie + 2 <= end) {
|
||||||
const uint8_t* end = frame + len;
|
|
||||||
while (ie + 2 <= end) {
|
|
||||||
uint8_t id = ie[0];
|
uint8_t id = ie[0];
|
||||||
uint8_t ielen = ie[1];
|
uint8_t ielen = ie[1];
|
||||||
if (ie + 2 + ielen > end) break;
|
if (ie + 2 + ielen > end) break;
|
||||||
@@ -200,9 +257,24 @@ namespace Drivers::Net::Wifi {
|
|||||||
else if (privacy) out->Security = WIFI_SEC_WEP;
|
else if (privacy) out->Security = WIFI_SEC_WEP;
|
||||||
else out->Security = WIFI_SEC_OPEN;
|
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;
|
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.
|
// Called by the MVM RX path for every received management frame.
|
||||||
void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
|
void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
|
||||||
int8_t rssiDbm) {
|
int8_t rssiDbm) {
|
||||||
@@ -224,9 +296,21 @@ namespace Drivers::Net::Wifi {
|
|||||||
ParsedBeacon pb;
|
ParsedBeacon pb;
|
||||||
if (!ParseBeacon(frame, len, &pb)) return;
|
if (!ParseBeacon(frame, len, &pb)) return;
|
||||||
|
|
||||||
const uint8_t* bssid = frame + 16; // addr3 of a beacon
|
// addr3 of a beacon
|
||||||
uint8_t ch = pb.Channel ? pb.Channel : channel;
|
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();
|
g_resultLock.Acquire();
|
||||||
|
|
||||||
ScanEntry* slot = nullptr;
|
ScanEntry* slot = nullptr;
|
||||||
@@ -275,7 +359,9 @@ namespace Drivers::Net::Wifi {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
bool Probe(const Pci::PciDevice& dev) {
|
bool Probe(const Pci::PciDevice& dev) {
|
||||||
|
if (g_backend != Backend::None) return false;
|
||||||
if (!IwxProbe(dev)) return false;
|
if (!IwxProbe(dev)) return false;
|
||||||
|
g_backend = Backend::Iwx;
|
||||||
|
|
||||||
// Firmware comes off the ramdisk, which is not mounted during the PCI
|
// Firmware comes off the ramdisk, which is not mounted during the PCI
|
||||||
// scan; hand the rest of the bring-up to the idle loop.
|
// scan; hand the rest of the bring-up to the idle loop.
|
||||||
@@ -283,7 +369,23 @@ namespace Drivers::Net::Wifi {
|
|||||||
return true;
|
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() {
|
static void CompleteInit() {
|
||||||
|
if (IsBrcmf()) {
|
||||||
|
g_initialized = BrcmfCompleteInit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!IwxReadFirmware()) {
|
if (!IwxReadFirmware()) {
|
||||||
g_iwx.State = IwxFwState::Error;
|
g_iwx.State = IwxFwState::Error;
|
||||||
return;
|
return;
|
||||||
@@ -348,7 +450,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
static constexpr uint64_t RECOVERY_BACKOFF_MS = 5000;
|
static constexpr uint64_t RECOVERY_BACKOFF_MS = 5000;
|
||||||
|
|
||||||
static void ServiceRecovery() {
|
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
|
// An adapter that never finished its first bring-up is ServiceDeferredInit's
|
||||||
// problem, not this one.
|
// problem, not this one.
|
||||||
if (!g_initialized) return;
|
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
|
// Re-check under the gate: the winner of the race may have just
|
||||||
// finished a reset that fixed things.
|
// finished a reset that fixed things.
|
||||||
if (g_iwx.State != IwxFwState::Error) return;
|
if (!FwFailed()) return;
|
||||||
|
|
||||||
uint64_t now = Timekeeping::GetMilliseconds();
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
if (g_lastRecoveryMs && now - g_lastRecoveryMs < RECOVERY_BACKOFF_MS) return;
|
if (g_lastRecoveryMs && now - g_lastRecoveryMs < RECOVERY_BACKOFF_MS) return;
|
||||||
@@ -384,6 +486,24 @@ namespace Drivers::Net::Wifi {
|
|||||||
<< " (attempt " << (uint64_t)g_recoveryAttempts << " of "
|
<< " (attempt " << (uint64_t)g_recoveryAttempts << " of "
|
||||||
<< (uint64_t)MAX_RECOVERY_ATTEMPTS << ")";
|
<< (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
|
// Nothing may be mid-command while the device is torn down. Taking
|
||||||
// CmdLock is enough: it is what serializes every sender, and the RX
|
// CmdLock is enough: it is what serializes every sender, and the RX
|
||||||
// pump has its own reentrancy guard.
|
// pump has its own reentrancy guard.
|
||||||
@@ -517,8 +637,24 @@ namespace Drivers::Net::Wifi {
|
|||||||
return false;
|
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() {
|
bool HasDeferredWork() {
|
||||||
if (InitDue()) return true;
|
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.Mmio) return false;
|
||||||
if (g_iwx.WorkPending) return true;
|
if (g_iwx.WorkPending) return true;
|
||||||
|
|
||||||
@@ -527,7 +663,29 @@ namespace Drivers::Net::Wifi {
|
|||||||
return HasBlockingWork(now);
|
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() {
|
void ServiceEvents() {
|
||||||
|
if (IsBrcmf()) {
|
||||||
|
ServiceBrcmf();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!g_iwx.Mmio) return;
|
if (!g_iwx.Mmio) return;
|
||||||
|
|
||||||
// The non-blocking half: any core, the BSP included.
|
// The non-blocking half: any core, the BSP included.
|
||||||
@@ -573,7 +731,11 @@ namespace Drivers::Net::Wifi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool IsInitialized() { return g_initialized; }
|
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
|
// Public operations
|
||||||
@@ -611,26 +773,26 @@ namespace Drivers::Net::Wifi {
|
|||||||
int Scan(WifiNetwork* out, int maxCount, uint32_t timeoutMs) {
|
int Scan(WifiNetwork* out, int maxCount, uint32_t timeoutMs) {
|
||||||
if (!out || maxCount <= 0) return -1;
|
if (!out || maxCount <= 0) return -1;
|
||||||
if (!g_initialized) 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 < 1000) timeoutMs = 1000;
|
||||||
if (timeoutMs > 20000) timeoutMs = 20000;
|
if (timeoutMs > 20000) timeoutMs = 20000;
|
||||||
|
|
||||||
ClearResults();
|
ClearResults();
|
||||||
|
|
||||||
if (!IwxStartScan(nullptr)) return -1;
|
if (!BeginScan()) return -1;
|
||||||
|
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (g_iwx.ScanActive && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (ScanActive() && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
IwxProcessEvents();
|
PumpEvents();
|
||||||
for (int i = 0; i < 200; i++) Arch::Pause();
|
for (int i = 0; i < 200; i++) Arch::Pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_iwx.ScanActive) {
|
if (ScanActive()) {
|
||||||
IwxAbortScan();
|
AbortScan();
|
||||||
// Drain whatever the firmware queued before the abort landed.
|
// Drain whatever the firmware queued before the abort landed.
|
||||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - t0 < 200) IwxProcessEvents();
|
while (Timekeeping::GetMilliseconds() - t0 < 200) PumpEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
g_scanGeneration++;
|
g_scanGeneration++;
|
||||||
@@ -639,14 +801,14 @@ namespace Drivers::Net::Wifi {
|
|||||||
|
|
||||||
int StartScan(uint32_t timeoutMs) {
|
int StartScan(uint32_t timeoutMs) {
|
||||||
if (!g_initialized) return -1;
|
if (!g_initialized) return -1;
|
||||||
if (g_iwx.State != IwxFwState::Running) return -1;
|
if (!FwRunning()) return -1;
|
||||||
if (g_iwx.ScanActive) return 1; // already sweeping
|
if (ScanActive()) return 1; // already sweeping
|
||||||
|
|
||||||
if (timeoutMs < 1000) timeoutMs = 1000;
|
if (timeoutMs < 1000) timeoutMs = 1000;
|
||||||
if (timeoutMs > 20000) timeoutMs = 20000;
|
if (timeoutMs > 20000) timeoutMs = 20000;
|
||||||
|
|
||||||
ClearResults();
|
ClearResults();
|
||||||
if (!IwxStartScan(nullptr)) return -1;
|
if (!BeginScan()) return -1;
|
||||||
|
|
||||||
g_scanDeadline = Timekeeping::GetMilliseconds() + timeoutMs;
|
g_scanDeadline = Timekeeping::GetMilliseconds() + timeoutMs;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -657,33 +819,47 @@ namespace Drivers::Net::Wifi {
|
|||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
|
|
||||||
out->present = IsPresent() ? 1 : 0;
|
out->present = IsPresent() ? 1 : 0;
|
||||||
out->state = (uint8_t)g_iwx.State;
|
|
||||||
if (!IsPresent()) return -1;
|
if (!IsPresent()) return -1;
|
||||||
|
|
||||||
memcpy(out->mac, g_iwx.Nvm.HwAddr, 6);
|
const char* fwVersion;
|
||||||
out->scanning = g_iwx.ScanActive ? 1 : 0;
|
if (IsBrcmf()) {
|
||||||
out->bands = (uint8_t)((g_iwx.Nvm.Sku24GHz ? 1 : 0)
|
out->state = (uint8_t)BrcmfGetState();
|
||||||
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
|
memcpy(out->mac, g_brcmf.Mac, 6);
|
||||||
out->channels = (uint16_t)g_iwx.ChannelCount;
|
out->bands = (uint8_t)((g_brcmf.Band24 ? 1 : 0) | (g_brcmf.Band5 ? 2 : 0));
|
||||||
out->rxPackets = g_iwx.RxPackets;
|
out->channels = g_brcmf.ChannelCount;
|
||||||
out->txPackets = g_iwx.TxPackets;
|
out->rxPackets = g_brcmf.RxPackets;
|
||||||
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
out->txPackets = g_brcmf.TxPackets;
|
||||||
out->connState = (uint32_t)IwxConnectState();
|
out->fwErrors = g_brcmf.FwErrors;
|
||||||
out->connected = IwxLinkUp() ? 1 : 0;
|
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->lastError = (int32_t)g_lastError;
|
||||||
out->scanGeneration = g_scanGeneration;
|
out->scanGeneration = g_scanGeneration;
|
||||||
out->joining = g_asyncConnect ? 1 : 0;
|
out->joining = g_asyncConnect ? 1 : 0;
|
||||||
|
|
||||||
if (IwxConnectState() != (int)IwxConnStateId::Idle) {
|
if (ConnState() != (int)IwxConnStateId::Idle) {
|
||||||
const char* ssid = IwxConnectSsid();
|
const char* ssid = IsBrcmf() ? BrcmfConnectSsid() : IwxConnectSsid();
|
||||||
int k = 0;
|
int k = 0;
|
||||||
for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k];
|
for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k];
|
||||||
out->ssid[k] = '\0';
|
out->ssid[k] = '\0';
|
||||||
memcpy(out->bssid, IwxConnectBssid(), 6);
|
memcpy(out->bssid, IsBrcmf() ? BrcmfConnectBssid() : IwxConnectBssid(), 6);
|
||||||
|
if (IsBrcmf()) out->channel = BrcmfConnectChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
int i = 0;
|
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';
|
out->fwVersion[i] = '\0';
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -695,24 +871,20 @@ namespace Drivers::Net::Wifi {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
IwxProcessEvents();
|
PumpConnection();
|
||||||
IwxConnectService();
|
|
||||||
|
|
||||||
auto state = (IwxConnStateId)IwxConnectState();
|
auto state = (IwxConnStateId)ConnState();
|
||||||
if (state == IwxConnStateId::Connected) return 0;
|
if (state == IwxConnStateId::Connected) return 0;
|
||||||
if (state == IwxConnStateId::Failed) {
|
if (state == IwxConnStateId::Failed) {
|
||||||
// A handshake that got as far as exchanging EAPOL frames and
|
int rc = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||||
// then failed is almost always a wrong passphrase.
|
|
||||||
int rc = WpaGetState() == WpaState::Failed
|
|
||||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
|
||||||
// A rejection seen on the RX path only marks the state; the
|
// A rejection seen on the RX path only marks the state; the
|
||||||
// firmware contexts are still up and have to come back down.
|
// firmware contexts are still up and have to come back down.
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED;
|
if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED;
|
||||||
if (g_iwx.State == IwxFwState::Error) {
|
if (FwFailed()) {
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return WIFI_ERR_FAILED;
|
return WIFI_ERR_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +893,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
Sched::Schedule();
|
Sched::Schedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return WIFI_ERR_TIMEOUT;
|
return WIFI_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,7 +904,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
// scan table is worth a (blocking) sweep to look for it.
|
// scan table is worth a (blocking) sweep to look for it.
|
||||||
static int StartJoin(const char* ssid, const char* password, bool rescan) {
|
static int StartJoin(const char* ssid, const char* password, bool rescan) {
|
||||||
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
|
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
|
// Locate the network in the most recent scan results: the firmware
|
||||||
// contexts need its BSSID and channel, and the connect path needs its
|
// 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 (!havePassword) return WIFI_ERR_NEED_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IwxConnectStart(bssid, channel, is5, ssid,
|
bool started = IsBrcmf()
|
||||||
havePassword ? password : nullptr,
|
? BrcmfConnectStart(bssid, channel, ssid,
|
||||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
havePassword ? password : nullptr,
|
||||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
|
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||||
beaconInterval, dtimPeriod)) {
|
security == WIFI_SEC_OPEN ? 0 : rsnIeLen)
|
||||||
// IwxConnectStart already logged the specific reason. Only report
|
: 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
|
// a security problem when that is genuinely what happened; a radio
|
||||||
// or firmware failure needs completely different advice.
|
// or firmware failure needs completely different advice.
|
||||||
bool security_ = IwxConnectRefusedForSecurity();
|
bool security_ = IsBrcmf() ? BrcmfConnectRefusedForSecurity()
|
||||||
auto state = (IwxConnStateId)IwxConnectState();
|
: IwxConnectRefusedForSecurity();
|
||||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
auto state = (IwxConnStateId)ConnState();
|
||||||
|
if (state != IwxConnStateId::Idle) ConnAbort();
|
||||||
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
|
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,8 +1014,8 @@ namespace Drivers::Net::Wifi {
|
|||||||
int ConnectAsync(const char* ssid, const char* password) {
|
int ConnectAsync(const char* ssid, const char* password) {
|
||||||
// A join already in flight owns the firmware contexts; tear it down
|
// A join already in flight owns the firmware contexts; tear it down
|
||||||
// rather than stacking a second one on top.
|
// rather than stacking a second one on top.
|
||||||
if (g_asyncConnect || (IwxConnectState() != (int)IwxConnStateId::Idle))
|
if (g_asyncConnect || (ConnState() != (int)IwxConnStateId::Idle))
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
|
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
g_lastError = 0;
|
g_lastError = 0;
|
||||||
@@ -858,7 +1037,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
if (!g_initialized) return -1;
|
if (!g_initialized) return -1;
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
g_lastError = 0;
|
g_lastError = 0;
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,11 +1060,11 @@ namespace Drivers::Net::Wifi {
|
|||||||
uint64_t now = Timekeeping::GetMilliseconds();
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
if (g_scanDeadline) {
|
if (g_scanDeadline) {
|
||||||
if (!g_iwx.ScanActive) {
|
if (!ScanActive()) {
|
||||||
g_scanDeadline = 0;
|
g_scanDeadline = 0;
|
||||||
g_scanGeneration++;
|
g_scanGeneration++;
|
||||||
} else if (now >= g_scanDeadline) {
|
} else if (now >= g_scanDeadline) {
|
||||||
IwxAbortScan();
|
AbortScan();
|
||||||
g_scanDeadline = 0;
|
g_scanDeadline = 0;
|
||||||
g_scanGeneration++;
|
g_scanGeneration++;
|
||||||
}
|
}
|
||||||
@@ -893,31 +1072,29 @@ namespace Drivers::Net::Wifi {
|
|||||||
|
|
||||||
if (!g_asyncConnect) return;
|
if (!g_asyncConnect) return;
|
||||||
|
|
||||||
auto state = (IwxConnStateId)IwxConnectState();
|
auto state = (IwxConnStateId)ConnState();
|
||||||
if (state == IwxConnStateId::Connected) {
|
if (state == IwxConnStateId::Connected) {
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
g_lastError = 0;
|
g_lastError = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state == IwxConnStateId::Failed) {
|
if (state == IwxConnStateId::Failed) {
|
||||||
// Same reading as the blocking path: a handshake that exchanged
|
// Same reading as the blocking path.
|
||||||
// EAPOL frames and then failed is almost always a wrong passphrase.
|
g_lastError = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||||
g_lastError = WpaGetState() == WpaState::Failed
|
|
||||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state == IwxConnStateId::Idle || g_iwx.State == IwxFwState::Error) {
|
if (state == IwxConnStateId::Idle || FwFailed()) {
|
||||||
g_lastError = WIFI_ERR_FAILED;
|
g_lastError = WIFI_ERR_FAILED;
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
if (state != IwxConnStateId::Idle) ConnAbort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (now >= g_asyncConnectDeadline) {
|
if (now >= g_asyncConnectDeadline) {
|
||||||
g_lastError = WIFI_ERR_TIMEOUT;
|
g_lastError = WIFI_ERR_TIMEOUT;
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,13 +1104,33 @@ namespace Drivers::Net::Wifi {
|
|||||||
|
|
||||||
static RxCallback g_rxCallback = nullptr;
|
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) {
|
bool SendPacket(const uint8_t* data, uint16_t length) {
|
||||||
if (!IsLinkUp()) return false;
|
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; }
|
void SetRxCallback(RxCallback callback) { g_rxCallback = callback; }
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ namespace Drivers::Net::Wifi {
|
|||||||
// PCI driver-table probe entry point.
|
// PCI driver-table probe entry point.
|
||||||
bool Probe(const Pci::PciDevice& dev);
|
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.
|
// Complete the firmware-dependent bring-up once the ramdisk is mounted.
|
||||||
// Cheap no-op unless a device is waiting. Called from the idle loop.
|
// Cheap no-op unless a device is waiting. Called from the idle loop.
|
||||||
void ServiceDeferredInit();
|
void ServiceDeferredInit();
|
||||||
@@ -25,6 +30,11 @@ namespace Drivers::Net::Wifi {
|
|||||||
// deadline, or a due recovery attempt needs idle-context servicing.
|
// deadline, or a due recovery attempt needs idle-context servicing.
|
||||||
bool HasDeferredWork();
|
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 IsInitialized();
|
||||||
bool IsPresent();
|
bool IsPresent();
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ namespace Drivers::Net::Wifi {
|
|||||||
bool WpaParseApRsn(const uint8_t* ie, uint32_t len, WpaConfig& cfg);
|
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.
|
// 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);
|
WritePortReg(port, PORT_CI, 1u << slot);
|
||||||
|
|
||||||
// Wait for completion
|
// Wait for completion
|
||||||
for (int i = 0; i < 5000000; i++) {
|
for (int i = 0; i < 5000000; i++) {
|
||||||
uint32_t ci = ReadPortReg(port, PORT_CI);
|
uint32_t ci = ReadPortReg(port, PORT_CI);
|
||||||
if (!(ci & (1u << slot))) {
|
if (!(ci & (1u << slot))) {
|
||||||
|
Arch::DmaReadFence();
|
||||||
// Check for errors
|
// Check for errors
|
||||||
uint32_t is = ReadPortReg(port, PORT_IS);
|
uint32_t is = ReadPortReg(port, PORT_IS);
|
||||||
if (is & PORT_IS_TFES) {
|
if (is & PORT_IS_TFES) {
|
||||||
@@ -641,10 +643,19 @@ namespace Drivers::Storage::Ahci {
|
|||||||
for (int i = 0; i < MAX_PORTS; i++) {
|
for (int i = 0; i < MAX_PORTS; i++) {
|
||||||
if (!(g_portsImplemented & (1u << i))) continue;
|
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);
|
PortType type = ClassifyPort(i);
|
||||||
g_ports[i].Type = type;
|
g_ports[i].Type = type;
|
||||||
|
|
||||||
if (type == PortType::None) continue;
|
if (type == PortType::None) {
|
||||||
|
StopPort(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const char* typeStr = "Unknown";
|
const char* typeStr = "Unknown";
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -658,8 +669,6 @@ namespace Drivers::Storage::Ahci {
|
|||||||
KernelLogStream(INFO, "AHCI") << "Port " << base::dec << (uint64_t)i
|
KernelLogStream(INFO, "AHCI") << "Port " << base::dec << (uint64_t)i
|
||||||
<< ": " << typeStr << " device detected";
|
<< ": " << typeStr << " device detected";
|
||||||
|
|
||||||
InitPort(i);
|
|
||||||
|
|
||||||
if (type == PortType::Sata) {
|
if (type == PortType::Sata) {
|
||||||
if (IdentifyDevice(i)) {
|
if (IdentifyDevice(i)) {
|
||||||
g_ports[i].Active = true;
|
g_ports[i].Active = true;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace Drivers::Storage {
|
|||||||
BLOCK_KIND_SATAPI = 2,
|
BLOCK_KIND_SATAPI = 2,
|
||||||
BLOCK_KIND_NVME = 3,
|
BLOCK_KIND_NVME = 3,
|
||||||
BLOCK_KIND_USB_MSC = 4,
|
BLOCK_KIND_USB_MSC = 4,
|
||||||
|
BLOCK_KIND_SD = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BlockDevice {
|
struct BlockDevice {
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ namespace Drivers::Storage::Gpt {
|
|||||||
return GUID_LINUX_SWAP;
|
return GUID_LINUX_SWAP;
|
||||||
case 0x83:
|
case 0x83:
|
||||||
return GUID_LINUX_FS;
|
return GUID_LINUX_FS;
|
||||||
|
case 0xEF:
|
||||||
|
return GUID_EFI_SYSTEM;
|
||||||
default:
|
default:
|
||||||
return GUID_BASIC_DATA;
|
return GUID_BASIC_DATA;
|
||||||
}
|
}
|
||||||
@@ -551,6 +553,78 @@ namespace Drivers::Storage::Gpt {
|
|||||||
return removed;
|
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
|
// ASCII to UTF-16LE for partition names
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -127,6 +127,18 @@ namespace Drivers::Storage::Gpt {
|
|||||||
// hot-pluggable block device disappears.
|
// hot-pluggable block device disappears.
|
||||||
int RemovePartitionsForBlockDevice(int blockDevIndex);
|
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.
|
// Look up the human-readable name for a partition type GUID.
|
||||||
const char* GetTypeName(const Guid& typeGuid);
|
const char* GetTypeName(const Guid& typeGuid);
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,9 @@ namespace Drivers::Storage::Nvme {
|
|||||||
// CQ y Head Doorbell offset = 0x1000 + ((2y+1) * doorbellStride)
|
// 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) {
|
static void WriteSqTailDoorbell(uint16_t queueId, uint16_t value) {
|
||||||
|
Arch::DmaWriteFence();
|
||||||
uint32_t offset = 0x1000 + (2 * queueId) * g_doorbellStride;
|
uint32_t offset = 0x1000 + (2 * queueId) * g_doorbellStride;
|
||||||
*(volatile uint32_t*)(g_mmioBase + offset) = value;
|
*(volatile uint32_t*)(g_mmioBase + offset) = value;
|
||||||
}
|
}
|
||||||
@@ -152,6 +154,8 @@ namespace Drivers::Storage::Nvme {
|
|||||||
uint16_t status = cqe->Status;
|
uint16_t status = cqe->Status;
|
||||||
|
|
||||||
if ((status & CQE_PHASE_BIT) == phase) {
|
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;
|
uint16_t consumedId = cqe->CommandId;
|
||||||
uint32_t result = cqe->Result;
|
uint32_t result = cqe->Result;
|
||||||
uint16_t sqHead = cqe->SqHead;
|
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 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 uint32_t MAX_BULK_TRANSFER_BYTES = 0x10000;
|
||||||
constexpr int MAX_MSC_DEVICES = 16;
|
constexpr int MAX_MSC_DEVICES = 16;
|
||||||
|
|
||||||
@@ -275,6 +285,15 @@ namespace Drivers::USB::MassStorage {
|
|||||||
return maxLun;
|
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,
|
static bool ScsiCommand(Device& dev, const uint8_t* cdb, uint8_t cdbLen,
|
||||||
void* data, uint64_t dataPhys, uint32_t dataLen,
|
void* data, uint64_t dataPhys, uint32_t dataLen,
|
||||||
bool dataIn) {
|
bool dataIn) {
|
||||||
@@ -295,24 +314,34 @@ namespace Drivers::USB::MassStorage {
|
|||||||
memcpy(cbw->CommandBlock, cdb, cdbLen);
|
memcpy(cbw->CommandBlock, cdb, cdbLen);
|
||||||
|
|
||||||
uint32_t actual = 0;
|
uint32_t actual = 0;
|
||||||
uint32_t cc = Xhci::BulkTransfer(dev.SlotId, false, dev.CbwBuf, dev.CbwPhys,
|
uint32_t cc = BulkStage(dev, false, dev.CbwBuf, dev.CbwPhys,
|
||||||
sizeof(CommandBlockWrapper), &actual);
|
sizeof(CommandBlockWrapper), &actual);
|
||||||
if (!IsGoodCompletion(cc) || actual != sizeof(CommandBlockWrapper)) {
|
if (!IsGoodCompletion(cc) || actual != sizeof(CommandBlockWrapper)) {
|
||||||
ReleaseTransport();
|
ReleaseTransport();
|
||||||
return false;
|
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) {
|
if (dataLen > 0) {
|
||||||
cc = Xhci::BulkTransfer(dev.SlotId, dataIn, data, dataPhys, dataLen, &actual);
|
cc = BulkStage(dev, dataIn, data, dataPhys, dataLen, &actual);
|
||||||
if (!IsGoodCompletion(cc)) {
|
if (!IsGoodCompletion(cc) && cc != Xhci::CC_STALL_ERROR) {
|
||||||
ReleaseTransport();
|
ReleaseTransport();
|
||||||
return false;
|
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);
|
memset(dev.CswBuf, 0, 0x1000);
|
||||||
cc = Xhci::BulkTransfer(dev.SlotId, true, dev.CswBuf, dev.CswPhys,
|
cc = BulkStage(dev, true, dev.CswBuf, dev.CswPhys,
|
||||||
sizeof(CommandStatusWrapper), &actual);
|
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)) {
|
if (!IsGoodCompletion(cc) || actual < sizeof(CommandStatusWrapper)) {
|
||||||
ReleaseTransport();
|
ReleaseTransport();
|
||||||
return false;
|
return false;
|
||||||
@@ -335,35 +364,66 @@ namespace Drivers::USB::MassStorage {
|
|||||||
// SCSI commands
|
// 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] = {};
|
uint8_t cdb[6] = {};
|
||||||
cdb[0] = SCSI_REQUEST_SENSE;
|
cdb[0] = SCSI_REQUEST_SENSE;
|
||||||
cdb[4] = 18;
|
cdb[4] = 18;
|
||||||
|
|
||||||
memset(dev.ScratchBuf, 0, 18);
|
memset(dev.ScratchBuf, 0, 18);
|
||||||
bool ok = ScsiCommand(dev, cdb, 6, dev.ScratchBuf, dev.ScratchPhys, 18, true);
|
if (!ScsiCommand(dev, cdb, 6, dev.ScratchBuf, dev.ScratchPhys, 18, true)) {
|
||||||
if (ok) {
|
return false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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) {
|
static bool TestUnitReady(Device& dev) {
|
||||||
uint8_t cdb[6] = {};
|
uint8_t cdb[6] = {};
|
||||||
cdb[0] = SCSI_TEST_UNIT_READY;
|
cdb[0] = SCSI_TEST_UNIT_READY;
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
int limit = TUR_TRIES;
|
||||||
if (ScsiCommand(dev, cdb, 6, nullptr, 0, 0, false)) return true;
|
int noMedium = 0;
|
||||||
RequestSense(dev);
|
Sense last = {0xFF, 0xFF, 0xFF};
|
||||||
PollWaitMs(50);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
static bool Inquiry(Device& dev) {
|
||||||
@@ -621,7 +681,14 @@ namespace Drivers::USB::MassStorage {
|
|||||||
continue;
|
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 "
|
KernelLogStream(WARNING, "USB-MSC") << "READ CAPACITY failed for LUN "
|
||||||
<< (uint64_t)lun;
|
<< (uint64_t)lun;
|
||||||
dev.Active = false;
|
dev.Active = false;
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
#include "HidKeyboard.hpp"
|
#include "HidKeyboard.hpp"
|
||||||
#include "HidMouse.hpp"
|
#include "HidMouse.hpp"
|
||||||
#include "MassStorage.hpp"
|
#include "MassStorage.hpp"
|
||||||
#include "Bluetooth/Bluetooth.hpp"
|
#include "Hub.hpp"
|
||||||
|
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
#include <Memory/HHDM.hpp>
|
#include <Memory/HHDM.hpp>
|
||||||
@@ -65,24 +66,28 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
return speed; // Same encoding
|
return speed; // Same encoding
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert USB endpoint bInterval to xHCI Endpoint Context Interval value.
|
// Convert USB endpoint bInterval to xHCI Endpoint Context Interval value
|
||||||
// HS/SS: bInterval is already in 2^(n-1) * 125µs encoding — use directly.
|
// (a period of 2^Interval * 125 us), as Linux's xhci_get_endpoint_interval.
|
||||||
// FS/LS: bInterval is in milliseconds (frames) — convert via fls(bInterval * 8).
|
// 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) {
|
static uint32_t ConvertInterval(uint32_t speed, uint8_t bInterval) {
|
||||||
if (bInterval == 0) return 0;
|
|
||||||
|
|
||||||
if (speed == Xhci::SPEED_HIGH || speed == Xhci::SPEED_SUPER) {
|
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 microframes = (uint32_t)bInterval * 8;
|
||||||
uint32_t interval = 0;
|
uint32_t interval = 0;
|
||||||
while (microframes > 0) {
|
while (microframes > 1) {
|
||||||
interval++;
|
interval++;
|
||||||
microframes >>= 1;
|
microframes >>= 1;
|
||||||
}
|
}
|
||||||
if (interval > 15) interval = 15;
|
if (interval < 3) interval = 3;
|
||||||
|
if (interval > 10) interval = 10;
|
||||||
return interval;
|
return interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,29 +106,100 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
// EnumerateDevice
|
// 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) {
|
uint8_t EnumerateDevice(uint8_t portId, uint32_t speed) {
|
||||||
KernelLogStream(INFO, "USB") << "Enumerating device on port "
|
DeviceLocation location = {};
|
||||||
<< (uint64_t)portId << " speed=" << SpeedToString(speed);
|
location.RootPort = portId;
|
||||||
|
return EnumerateDevice(location, speed);
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
uint8_t EnumerateDevice(const DeviceLocation& location, uint32_t speed) {
|
||||||
// Step 1: Enable Slot
|
if (location.ParentSlot == 0) {
|
||||||
// -----------------------------------------------------------------
|
KernelLogStream(INFO, "USB") << "Enumerating device on port "
|
||||||
Xhci::TRB enableSlotTrb = {};
|
<< (uint64_t)location.RootPort << " speed=" << SpeedToString(speed);
|
||||||
enableSlotTrb.Control = (Xhci::TRB_ENABLE_SLOT << Xhci::TRB_TYPE_SHIFT);
|
} else {
|
||||||
|
KernelLogStream(INFO, "USB") << "Enumerating device on hub slot "
|
||||||
uint32_t cc = Xhci::SendCommand(enableSlotTrb);
|
<< (uint64_t)location.ParentSlot << " port " << (uint64_t)location.ParentPort
|
||||||
if (cc != Xhci::CC_SUCCESS) {
|
<< " (route " << base::hex << (uint64_t)location.RouteString << base::dec
|
||||||
KernelLogStream(ERROR, "USB") << "Enable Slot failed, cc=" << (uint64_t)cc;
|
<< ", root port " << (uint64_t)location.RootPort << ") speed=" << SpeedToString(speed);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t slotId = (uint8_t)Xhci::g_cmdCompletionSlotId;
|
for (int attempt = 1; ; attempt++) {
|
||||||
if (slotId == 0 || slotId > Xhci::MAX_SLOTS) {
|
// -------------------------------------------------------------
|
||||||
KernelLogStream(ERROR, "USB") << "Invalid slot ID: " << (uint64_t)slotId;
|
// Step 1: Enable Slot
|
||||||
return 0;
|
// -------------------------------------------------------------
|
||||||
}
|
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
|
// Step 2: Allocate device output context and set DCBAA entry
|
||||||
@@ -133,9 +209,16 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
dev->Active = true;
|
dev->Active = true;
|
||||||
dev->PortId = portId;
|
dev->PortId = portId;
|
||||||
dev->Speed = speed;
|
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
|
// 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->OutputContext = outputCtx;
|
||||||
dev->OutputContextPhys = Memory::SubHHDM(outputCtx);
|
dev->OutputContextPhys = Memory::SubHHDM(outputCtx);
|
||||||
|
|
||||||
@@ -145,7 +228,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Step 3: Build Input Context for Address Device command
|
// 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)
|
// Input Control Context: add Slot Context (bit 0) and EP0 (bit 1)
|
||||||
inputCtx->ICC.AddFlags = 0x3;
|
inputCtx->ICC.AddFlags = 0x3;
|
||||||
@@ -153,12 +236,18 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
// Slot Context
|
// Slot Context
|
||||||
uint32_t speedVal = SpeedToSlotContextValue(speed);
|
uint32_t speedVal = SpeedToSlotContextValue(speed);
|
||||||
uint32_t ctxEntries = 1; // Context Entries = 1 (Slot + EP0 only)
|
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
|
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
|
// EP0 Context
|
||||||
// Allocate EP0 transfer ring
|
// Allocate EP0 transfer ring
|
||||||
auto* ep0Ring = (Xhci::TRB*)Memory::g_pfa->AllocateZeroed();
|
auto* ep0Ring = (Xhci::TRB*)Xhci::AllocateDmaPage();
|
||||||
dev->EP0Ring = ep0Ring;
|
dev->EP0Ring = ep0Ring;
|
||||||
dev->EP0RingPhys = Memory::SubHHDM(ep0Ring);
|
dev->EP0RingPhys = Memory::SubHHDM(ep0Ring);
|
||||||
dev->EP0RingEnqueue = 0;
|
dev->EP0RingEnqueue = 0;
|
||||||
@@ -198,8 +287,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
if (cc != Xhci::CC_SUCCESS) {
|
if (cc != Xhci::CC_SUCCESS) {
|
||||||
KernelLogStream(ERROR, "USB") << "Address Device (BSR=1) failed, slot="
|
KernelLogStream(ERROR, "USB") << "Address Device (BSR=1) failed, slot="
|
||||||
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
|
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
|
||||||
dev->Active = false;
|
return false;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId << " initialized (BSR=1)";
|
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId << " initialized (BSR=1)";
|
||||||
@@ -213,11 +301,15 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
partialDesc, true);
|
partialDesc, true);
|
||||||
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
||||||
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(8-byte) failed, cc=" << (uint64_t)cc;
|
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(8-byte) failed, cc=" << (uint64_t)cc;
|
||||||
dev->Active = false;
|
return false;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
if (bMaxPacketSize0 == 0) bMaxPacketSize0 = maxPacket; // fallback
|
||||||
|
|
||||||
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId
|
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
|
// Step 4c: Evaluate Context — update EP0 max packet size if needed
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
if (bMaxPacketSize0 != maxPacket) {
|
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
|
// Only updating EP0 — set AddFlags bit 1 (EP0), no slot context needed
|
||||||
evalCtx->ICC.AddFlags = (1 << 1);
|
evalCtx->ICC.AddFlags = (1 << 1);
|
||||||
@@ -280,8 +372,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
if (cc != Xhci::CC_SUCCESS) {
|
if (cc != Xhci::CC_SUCCESS) {
|
||||||
KernelLogStream(ERROR, "USB") << "Address Device failed, slot="
|
KernelLogStream(ERROR, "USB") << "Address Device failed, slot="
|
||||||
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
|
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
|
||||||
dev->Active = false;
|
return false;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set-address recovery time (USB spec requires >= 2ms, use 10ms for safety)
|
// Set-address recovery time (USB spec requires >= 2ms, use 10ms for safety)
|
||||||
@@ -298,8 +389,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
&devDesc, true);
|
&devDesc, true);
|
||||||
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
||||||
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(Device) failed, cc=" << (uint64_t)cc;
|
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(Device) failed, cc=" << (uint64_t)cc;
|
||||||
dev->Active = false;
|
return false;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dev->VendorId = devDesc.idVendor;
|
dev->VendorId = devDesc.idVendor;
|
||||||
@@ -319,8 +409,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
&cfgHdr, true);
|
&cfgHdr, true);
|
||||||
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
||||||
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(Config header) failed, cc=" << (uint64_t)cc;
|
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(Config header) failed, cc=" << (uint64_t)cc;
|
||||||
dev->Active = false;
|
return false;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t cfgBuf[256] = {};
|
uint8_t cfgBuf[256] = {};
|
||||||
@@ -332,8 +421,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
cfgBuf, true);
|
cfgBuf, true);
|
||||||
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
|
||||||
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(Config full) failed, cc=" << (uint64_t)cc;
|
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(Config full) failed, cc=" << (uint64_t)cc;
|
||||||
dev->Active = false;
|
return false;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
@@ -341,6 +429,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
uint16_t offset = 0;
|
uint16_t offset = 0;
|
||||||
bool foundHid = false;
|
bool foundHid = false;
|
||||||
|
bool currentHub = false;
|
||||||
bool foundBt = false;
|
bool foundBt = false;
|
||||||
bool foundMsc = false;
|
bool foundMsc = false;
|
||||||
bool currentMsc = false;
|
bool currentMsc = false;
|
||||||
@@ -358,9 +447,23 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
auto* iface = (InterfaceDescriptor*)&cfgBuf[offset];
|
auto* iface = (InterfaceDescriptor*)&cfgBuf[offset];
|
||||||
// Reset at each new interface boundary
|
// Reset at each new interface boundary
|
||||||
foundHid = false;
|
foundHid = false;
|
||||||
|
currentHub = false;
|
||||||
foundBt = false;
|
foundBt = false;
|
||||||
currentMsc = 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 &&
|
if (!foundEp &&
|
||||||
iface->bInterfaceClass == CLASS_HID &&
|
iface->bInterfaceClass == CLASS_HID &&
|
||||||
iface->bInterfaceSubClass == SUBCLASS_BOOT) {
|
iface->bInterfaceSubClass == SUBCLASS_BOOT) {
|
||||||
@@ -408,8 +511,8 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
uint8_t xferType = ep->bmAttributes & EP_XFER_TYPE_MASK;
|
uint8_t xferType = ep->bmAttributes & EP_XFER_TYPE_MASK;
|
||||||
bool isIn = (ep->bEndpointAddress & EP_DIR_IN) != 0;
|
bool isIn = (ep->bEndpointAddress & EP_DIR_IN) != 0;
|
||||||
|
|
||||||
// HID interrupt IN endpoint
|
// HID or hub interrupt IN endpoint
|
||||||
if (foundHid && !foundEp && isIn && xferType == EP_XFER_INTERRUPT) {
|
if ((foundHid || currentHub) && !foundEp && isIn && xferType == EP_XFER_INTERRUPT) {
|
||||||
dev->InterruptEpNum = ep->bEndpointAddress & 0x0F;
|
dev->InterruptEpNum = ep->bEndpointAddress & 0x0F;
|
||||||
dev->InterruptMaxPacket = ep->wMaxPacketSize & 0x7FF;
|
dev->InterruptMaxPacket = ep->wMaxPacketSize & 0x7FF;
|
||||||
dev->InterruptInterval = ep->bInterval;
|
dev->InterruptInterval = ep->bInterval;
|
||||||
@@ -455,7 +558,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
// The xHCI slot model currently stores one interface; a future model
|
// The xHCI slot model currently stores one interface; a future model
|
||||||
// can retain every alternate/interface without changing the userspace
|
// can retain every alternate/interface without changing the userspace
|
||||||
// claim ABI, which already names the interface number explicitly.
|
// claim ABI, which already names the interface number explicitly.
|
||||||
bool knownInterface = foundBt || foundMsc ||
|
bool knownInterface = foundBt || foundMsc || isHub ||
|
||||||
dev->InterfaceClass == CLASS_HID;
|
dev->InterfaceClass == CLASS_HID;
|
||||||
if (!knownInterface) {
|
if (!knownInterface) {
|
||||||
bool inFirstInterface = false;
|
bool inFirstInterface = false;
|
||||||
@@ -502,15 +605,24 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
cfgHdr.bConfigurationValue, 0, 0, nullptr, false);
|
cfgHdr.bConfigurationValue, 0, 0, nullptr, false);
|
||||||
if (cc != Xhci::CC_SUCCESS) {
|
if (cc != Xhci::CC_SUCCESS) {
|
||||||
KernelLogStream(ERROR, "USB") << "SET_CONFIGURATION failed, cc=" << (uint64_t)cc;
|
KernelLogStream(ERROR, "USB") << "SET_CONFIGURATION failed, cc=" << (uint64_t)cc;
|
||||||
dev->Active = false;
|
return false;
|
||||||
return 0;
|
}
|
||||||
|
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
|
// Step 9: Configure Endpoints
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
if (foundEp || foundBulkIn || foundBulkOut) {
|
if (foundEp || foundBulkIn || foundBulkOut) {
|
||||||
auto* inputCtx2 = (Xhci::InputContext*)Memory::g_pfa->AllocateZeroed();
|
auto* inputCtx2 = (Xhci::InputContext*)Xhci::AllocateDmaPage();
|
||||||
|
|
||||||
// Start with slot context
|
// Start with slot context
|
||||||
inputCtx2->ICC.AddFlags = (1 << 0);
|
inputCtx2->ICC.AddFlags = (1 << 0);
|
||||||
@@ -528,7 +640,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
if (dci > maxDci) maxDci = dci;
|
if (dci > maxDci) maxDci = dci;
|
||||||
|
|
||||||
// Allocate interrupt transfer ring
|
// Allocate interrupt transfer ring
|
||||||
auto* intRing = (Xhci::TRB*)Memory::g_pfa->AllocateZeroed();
|
auto* intRing = (Xhci::TRB*)Xhci::AllocateDmaPage();
|
||||||
dev->InterruptRing = intRing;
|
dev->InterruptRing = intRing;
|
||||||
dev->InterruptRingPhys = Memory::SubHHDM(intRing);
|
dev->InterruptRingPhys = Memory::SubHHDM(intRing);
|
||||||
dev->InterruptRingEnqueue = 0;
|
dev->InterruptRingEnqueue = 0;
|
||||||
@@ -542,6 +654,10 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
|
|
||||||
auto& epCtx = inputCtx2->EP[dci - 1];
|
auto& epCtx = inputCtx2->EP[dci - 1];
|
||||||
uint32_t xhciInterval = ConvertInterval(speed, dev->InterruptInterval);
|
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.Field0 = (xhciInterval << 16);
|
||||||
epCtx.Field1 = (3 << 1)
|
epCtx.Field1 = (3 << 1)
|
||||||
| (Xhci::EP_TYPE_INTERRUPT_IN << 3)
|
| (Xhci::EP_TYPE_INTERRUPT_IN << 3)
|
||||||
@@ -556,7 +672,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
inputCtx2->ICC.AddFlags |= (1 << dci);
|
inputCtx2->ICC.AddFlags |= (1 << dci);
|
||||||
if (dci > maxDci) maxDci = dci;
|
if (dci > maxDci) maxDci = dci;
|
||||||
|
|
||||||
auto* bulkInRing = (Xhci::TRB*)Memory::g_pfa->AllocateZeroed();
|
auto* bulkInRing = (Xhci::TRB*)Xhci::AllocateDmaPage();
|
||||||
dev->BulkInRing = bulkInRing;
|
dev->BulkInRing = bulkInRing;
|
||||||
dev->BulkInRingPhys = Memory::SubHHDM(bulkInRing);
|
dev->BulkInRingPhys = Memory::SubHHDM(bulkInRing);
|
||||||
dev->BulkInRingEnqueue = 0;
|
dev->BulkInRingEnqueue = 0;
|
||||||
@@ -583,7 +699,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
inputCtx2->ICC.AddFlags |= (1 << dci);
|
inputCtx2->ICC.AddFlags |= (1 << dci);
|
||||||
if (dci > maxDci) maxDci = dci;
|
if (dci > maxDci) maxDci = dci;
|
||||||
|
|
||||||
auto* bulkOutRing = (Xhci::TRB*)Memory::g_pfa->AllocateZeroed();
|
auto* bulkOutRing = (Xhci::TRB*)Xhci::AllocateDmaPage();
|
||||||
dev->BulkOutRing = bulkOutRing;
|
dev->BulkOutRing = bulkOutRing;
|
||||||
dev->BulkOutRingPhys = Memory::SubHHDM(bulkOutRing);
|
dev->BulkOutRingPhys = Memory::SubHHDM(bulkOutRing);
|
||||||
dev->BulkOutRingEnqueue = 0;
|
dev->BulkOutRingEnqueue = 0;
|
||||||
@@ -608,6 +724,19 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
inputCtx2->Slot.Field0 = (inputCtx2->Slot.Field0 & ~(0x1Fu << 27))
|
inputCtx2->Slot.Field0 = (inputCtx2->Slot.Field0 & ~(0x1Fu << 27))
|
||||||
| (maxDci << 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
|
// Send Configure Endpoint command
|
||||||
Xhci::TRB cfgTrb = {};
|
Xhci::TRB cfgTrb = {};
|
||||||
uint64_t inputCtx2Phys = Memory::SubHHDM(inputCtx2);
|
uint64_t inputCtx2Phys = Memory::SubHHDM(inputCtx2);
|
||||||
@@ -620,8 +749,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
if (cc != Xhci::CC_SUCCESS) {
|
if (cc != Xhci::CC_SUCCESS) {
|
||||||
KernelLogStream(ERROR, "USB") << "Configure Endpoint failed, slot="
|
KernelLogStream(ERROR, "USB") << "Configure Endpoint failed, slot="
|
||||||
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
|
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
|
||||||
dev->Active = false;
|
return false;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundEp) {
|
if (foundEp) {
|
||||||
@@ -713,6 +841,8 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
KernelLogStream(OK, "USB") << "Slot " << (uint64_t)slotId << ": Bluetooth Adapter"
|
KernelLogStream(OK, "USB") << "Slot " << (uint64_t)slotId << ": Bluetooth Adapter"
|
||||||
<< " VID:" << base::hex << (uint64_t)dev->VendorId
|
<< " VID:" << base::hex << (uint64_t)dev->VendorId
|
||||||
<< " PID:" << (uint64_t)dev->ProductId << base::dec;
|
<< " PID:" << (uint64_t)dev->ProductId << base::dec;
|
||||||
|
} else if (isHub) {
|
||||||
|
dev->KernelDriverBound = true;
|
||||||
} else if (dev->InterfaceClass == CLASS_MASS_STORAGE &&
|
} else if (dev->InterfaceClass == CLASS_MASS_STORAGE &&
|
||||||
dev->InterfaceSubClass == SUBCLASS_SCSI &&
|
dev->InterfaceSubClass == SUBCLASS_SCSI &&
|
||||||
dev->InterfaceProtocol == PROTOCOL_BULK_ONLY &&
|
dev->InterfaceProtocol == PROTOCOL_BULK_ONLY &&
|
||||||
@@ -733,7 +863,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
// Publish to userspace only after endpoint configuration and kernel
|
// Publish to userspace only after endpoint configuration and kernel
|
||||||
// class-driver binding decisions are complete.
|
// class-driver binding decisions are complete.
|
||||||
dev->Ready = true;
|
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_KEYBOARD = 0x01;
|
||||||
constexpr uint8_t PROTOCOL_MOUSE = 0x02;
|
constexpr uint8_t PROTOCOL_MOUSE = 0x02;
|
||||||
|
|
||||||
|
// Hub class
|
||||||
|
constexpr uint8_t CLASS_HUB = 0x09;
|
||||||
|
|
||||||
// Wireless Controller class (Bluetooth)
|
// Wireless Controller class (Bluetooth)
|
||||||
constexpr uint8_t CLASS_WIRELESS = 0xE0;
|
constexpr uint8_t CLASS_WIRELESS = 0xE0;
|
||||||
constexpr uint8_t SUBCLASS_RF = 0x01;
|
constexpr uint8_t SUBCLASS_RF = 0x01;
|
||||||
@@ -119,8 +122,24 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
// Public API
|
// 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.
|
// Returns the assigned slot ID, or 0 on failure.
|
||||||
uint8_t EnumerateDevice(uint8_t portId, uint32_t speed);
|
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 "HidKeyboard.hpp"
|
||||||
#include "HidMouse.hpp"
|
#include "HidMouse.hpp"
|
||||||
#include "MassStorage.hpp"
|
#include "MassStorage.hpp"
|
||||||
|
#include "Hub.hpp"
|
||||||
#include "UserUsb.hpp"
|
#include "UserUsb.hpp"
|
||||||
#include <Pci/Pci.hpp>
|
#include <Pci/Pci.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
#include <Memory/HHDM.hpp>
|
#include <Memory/HHDM.hpp>
|
||||||
|
#include <Memory/Dma.hpp>
|
||||||
|
#include <ACPI/GsiRouting.hpp>
|
||||||
#include <Memory/Paging.hpp>
|
#include <Memory/Paging.hpp>
|
||||||
#include <Memory/PageFrameAllocator.hpp>
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
#include <Libraries/Memory.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_hotplugProcessing{false};
|
||||||
static std::atomic<bool> g_irqDrainPending{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
|
// PollEvents also runs directly from the MSI handler. KernelLogStream uses
|
||||||
// a process-context mutex, so printing there can deadlock if the interrupt
|
// a process-context mutex, so printing there can deadlock if the interrupt
|
||||||
// preempts a CPU that already owns the terminal lock. Capture the small
|
// 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;
|
*(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) {
|
static uint32_t ReadOp(uint32_t reg) {
|
||||||
return *(volatile uint32_t*)(g_opBase + reg);
|
return *(volatile uint32_t*)(g_opBase + reg);
|
||||||
}
|
}
|
||||||
@@ -236,18 +261,44 @@ namespace Drivers::USB::Xhci {
|
|||||||
return *(volatile uint32_t*)(g_mmioBase + reg);
|
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) {
|
static void WriteDoorbell(uint32_t index, uint32_t value) {
|
||||||
|
Arch::DmaWriteFence();
|
||||||
*(volatile uint32_t*)(g_dbBase + index * 4) = value;
|
*(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
|
// 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) {
|
static uint8_t* AllocateDmaBuffer(uint64_t& outPhysAddr) {
|
||||||
void* virt = Memory::g_pfa->AllocateZeroed();
|
return AllocateDmaPage(outPhysAddr);
|
||||||
outPhysAddr = Memory::SubHHDM(virt);
|
|
||||||
return (uint8_t*)virt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -412,6 +463,24 @@ namespace Drivers::USB::Xhci {
|
|||||||
// MSI setup (same pattern as E1000E)
|
// 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) {
|
static bool SetupMsi(uint8_t bus, uint8_t dev, uint8_t func) {
|
||||||
uint8_t cap = Pci::FindCapability(bus, dev, func, Pci::PCI_CAP_MSI);
|
uint8_t cap = Pci::FindCapability(bus, dev, func, Pci::PCI_CAP_MSI);
|
||||||
if (cap == 0) {
|
if (cap == 0) {
|
||||||
@@ -474,6 +543,16 @@ namespace Drivers::USB::Xhci {
|
|||||||
BudgetExhausted,
|
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) {
|
static PollResult PollEventsBounded(uint32_t maxEvents) {
|
||||||
// PollEvents runs from synchronous poll loops (ControlTransfer,
|
// PollEvents runs from synchronous poll loops (ControlTransfer,
|
||||||
// SendCommand, firmware download) and idle bottom halves on different
|
// SendCommand, firmware download) and idle bottom halves on different
|
||||||
@@ -506,6 +585,8 @@ namespace Drivers::USB::Xhci {
|
|||||||
if (evtCycle != g_evtRingCCS) {
|
if (evtCycle != g_evtRingCCS) {
|
||||||
break; // No more events
|
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;
|
uint32_t trbType = (evt.Control & TRB_TYPE_MASK) >> TRB_TYPE_SHIFT;
|
||||||
lastType = trbType;
|
lastType = trbType;
|
||||||
@@ -528,8 +609,12 @@ namespace Drivers::USB::Xhci {
|
|||||||
WriteOp(OP_PORTSC_BASE + (portId - 1) * OP_PORTSC_STRIDE,
|
WriteOp(OP_PORTSC_BASE + (portId - 1) * OP_PORTSC_STRIDE,
|
||||||
(portsc & PORTSC_PRESERVE) | PORTSC_CHANGE_BITS);
|
(portsc & PORTSC_PRESERVE) | PORTSC_CHANGE_BITS);
|
||||||
|
|
||||||
// Defer enumeration to ProcessDeferredWork outside interrupt context.
|
// Defer enumeration to ProcessDeferredWork outside interrupt
|
||||||
if (g_bootScanComplete && portId >= 1 && portId <= g_maxPorts) {
|
// 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_hotplugPending[portId - 1].store(true, std::memory_order_release);
|
||||||
g_deferredWorkPending.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 len = (residual < reqLen)
|
||||||
? (uint16_t)(reqLen - residual) : 0;
|
? (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) {
|
if (dev.InterfaceProtocol == UsbDevice::PROTOCOL_KEYBOARD) {
|
||||||
HidKeyboard::ProcessReport(g_interruptDataBuf[slotId], len);
|
HidKeyboard::ProcessReport(g_interruptDataBuf[slotId], len);
|
||||||
} else if (dev.InterfaceProtocol == UsbDevice::PROTOCOL_MOUSE) {
|
} else if (dev.InterfaceProtocol == UsbDevice::PROTOCOL_MOUSE) {
|
||||||
HidMouse::ProcessReport(g_interruptDataBuf[slotId], len);
|
HidMouse::ProcessReport(g_interruptDataBuf[slotId], len);
|
||||||
}
|
}
|
||||||
|
g_hidErrorStreak[slotId].store(0, std::memory_order_relaxed);
|
||||||
// Re-queue for next HID report
|
// Re-queue for next HID report
|
||||||
QueueInterruptTransfer(slotId);
|
QueueInterruptTransfer(slotId);
|
||||||
} else if (g_transferCallbacks[slotId]) {
|
} else if (g_transferCallbacks[slotId]) {
|
||||||
@@ -655,6 +745,23 @@ namespace Drivers::USB::Xhci {
|
|||||||
g_diagnosticWorkPending.store(true, std::memory_order_release);
|
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
|
// Notify callback of errors too
|
||||||
if (g_transferCallbacks[slotId]) {
|
if (g_transferCallbacks[slotId]) {
|
||||||
g_transferCallbacks[slotId](slotId, epDci,
|
g_transferCallbacks[slotId](slotId, epDci,
|
||||||
@@ -770,7 +877,7 @@ namespace Drivers::USB::Xhci {
|
|||||||
if (g_cmdRingCCS) {
|
if (g_cmdRingCCS) {
|
||||||
control |= TRB_CYCLE_BIT;
|
control |= TRB_CYCLE_BIT;
|
||||||
}
|
}
|
||||||
slot.Control = control;
|
PublishTrb(slot, control);
|
||||||
|
|
||||||
// Advance enqueue pointer
|
// Advance enqueue pointer
|
||||||
g_cmdRingEnqueue++;
|
g_cmdRingEnqueue++;
|
||||||
@@ -819,10 +926,45 @@ namespace Drivers::USB::Xhci {
|
|||||||
// ControlTransfer - perform a control transfer on EP0
|
// 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,
|
uint32_t ControlTransfer(uint8_t slotId, uint8_t bmRequestType, uint8_t bRequest,
|
||||||
uint16_t wValue, uint16_t wIndex, uint16_t wLength,
|
uint16_t wValue, uint16_t wIndex, uint16_t wLength,
|
||||||
void* data, bool dirIn) {
|
void* data, bool dirIn) {
|
||||||
|
|
||||||
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) {
|
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) {
|
||||||
return 0xFF;
|
return 0xFF;
|
||||||
}
|
}
|
||||||
@@ -836,6 +978,32 @@ namespace Drivers::USB::Xhci {
|
|||||||
g_pollOwnerCpu.load(std::memory_order_relaxed) == CurrentCpuIndex();
|
g_pollOwnerCpu.load(std::memory_order_relaxed) == CurrentCpuIndex();
|
||||||
if (!trueNested) g_controlXferLock.Acquire();
|
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];
|
UsbDeviceInfo& dev = g_devices[slotId];
|
||||||
|
|
||||||
// --- Setup Stage TRB ---
|
// --- Setup Stage TRB ---
|
||||||
@@ -853,15 +1021,15 @@ namespace Drivers::USB::Xhci {
|
|||||||
if (dev.EP0RingCCS) {
|
if (dev.EP0RingCCS) {
|
||||||
setupControl |= TRB_CYCLE_BIT;
|
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)
|
// Advance EP0 enqueue (handles Link TRB wrap)
|
||||||
AdvanceEP0Ring(dev);
|
AdvanceEP0Ring(dev);
|
||||||
|
|
||||||
// --- Data Stage TRB (if wLength > 0) ---
|
// --- Data Stage TRB (if wLength > 0) ---
|
||||||
if (wLength > 0 && data != nullptr) {
|
if (wLength > 0 && data != nullptr) {
|
||||||
uint64_t dataPhys = Memory::SubHHDM(data);
|
|
||||||
|
|
||||||
TRB& dataTrb = dev.EP0Ring[dev.EP0RingEnqueue];
|
TRB& dataTrb = dev.EP0Ring[dev.EP0RingEnqueue];
|
||||||
dataTrb.Parameter0 = (uint32_t)(dataPhys & 0xFFFFFFFF);
|
dataTrb.Parameter0 = (uint32_t)(dataPhys & 0xFFFFFFFF);
|
||||||
dataTrb.Parameter1 = (uint32_t)(dataPhys >> 32);
|
dataTrb.Parameter1 = (uint32_t)(dataPhys >> 32);
|
||||||
@@ -874,7 +1042,7 @@ namespace Drivers::USB::Xhci {
|
|||||||
if (dev.EP0RingCCS) {
|
if (dev.EP0RingCCS) {
|
||||||
dataControl |= TRB_CYCLE_BIT;
|
dataControl |= TRB_CYCLE_BIT;
|
||||||
}
|
}
|
||||||
dataTrb.Control = dataControl;
|
PublishTrb(dataTrb, dataControl);
|
||||||
|
|
||||||
AdvanceEP0Ring(dev);
|
AdvanceEP0Ring(dev);
|
||||||
}
|
}
|
||||||
@@ -895,10 +1063,12 @@ namespace Drivers::USB::Xhci {
|
|||||||
if (dev.EP0RingCCS) {
|
if (dev.EP0RingCCS) {
|
||||||
statusControl |= TRB_CYCLE_BIT;
|
statusControl |= TRB_CYCLE_BIT;
|
||||||
}
|
}
|
||||||
status.Control = statusControl;
|
PublishTrb(status, statusControl);
|
||||||
|
|
||||||
AdvanceEP0Ring(dev);
|
AdvanceEP0Ring(dev);
|
||||||
|
|
||||||
|
PublishTrb(setup, setupControl);
|
||||||
|
|
||||||
// Ring doorbell for this slot, target EP0 (DCI 1)
|
// Ring doorbell for this slot, target EP0 (DCI 1)
|
||||||
g_xferCompleted[slotId] = false;
|
g_xferCompleted[slotId] = false;
|
||||||
WriteDoorbell(slotId, 1);
|
WriteDoorbell(slotId, 1);
|
||||||
@@ -922,8 +1092,9 @@ namespace Drivers::USB::Xhci {
|
|||||||
while (Timekeeping::GetMilliseconds() - xferStart < 2000) {
|
while (Timekeeping::GetMilliseconds() - xferStart < 2000) {
|
||||||
PollEvents();
|
PollEvents();
|
||||||
if (g_xferCompleted[slotId]) {
|
if (g_xferCompleted[slotId]) {
|
||||||
g_controlXferLock.Release();
|
uint32_t cc = g_xferCompletionCode[slotId];
|
||||||
return g_xferCompletionCode[slotId];
|
if (cc != CC_SUCCESS && cc != CC_SHORT_PACKET) RecoverEP0(slotId);
|
||||||
|
return cc;
|
||||||
}
|
}
|
||||||
for (int j = 0; j < 100; j++) {
|
for (int j = 0; j < 100; j++) {
|
||||||
Arch::CompilerBarrier();
|
Arch::CompilerBarrier();
|
||||||
@@ -956,7 +1127,6 @@ namespace Drivers::USB::Xhci {
|
|||||||
| (1 << 16); // DCI=1 (EP0)
|
| (1 << 16); // DCI=1 (EP0)
|
||||||
SendCommand(deqTrb);
|
SendCommand(deqTrb);
|
||||||
|
|
||||||
g_controlXferLock.Release();
|
|
||||||
return 0xFF;
|
return 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,7 +1156,7 @@ namespace Drivers::USB::Xhci {
|
|||||||
if (dev.InterruptRingCCS) {
|
if (dev.InterruptRingCCS) {
|
||||||
control |= TRB_CYCLE_BIT;
|
control |= TRB_CYCLE_BIT;
|
||||||
}
|
}
|
||||||
trb.Control = control;
|
PublishTrb(trb, control);
|
||||||
|
|
||||||
// Advance enqueue (handles Link TRB wrap)
|
// Advance enqueue (handles Link TRB wrap)
|
||||||
AdvanceInterruptRing(dev);
|
AdvanceInterruptRing(dev);
|
||||||
@@ -1095,6 +1265,86 @@ namespace Drivers::USB::Xhci {
|
|||||||
SendCommand(deqTrb);
|
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
|
// QueueBulkInTransfer
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -1123,7 +1373,7 @@ namespace Drivers::USB::Xhci {
|
|||||||
if (dev.BulkInRingCCS) {
|
if (dev.BulkInRingCCS) {
|
||||||
control |= TRB_CYCLE_BIT;
|
control |= TRB_CYCLE_BIT;
|
||||||
}
|
}
|
||||||
trb.Control = control;
|
PublishTrb(trb, control);
|
||||||
|
|
||||||
AdvanceBulkInRing(dev);
|
AdvanceBulkInRing(dev);
|
||||||
|
|
||||||
@@ -1250,6 +1500,9 @@ namespace Drivers::USB::Xhci {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
void QueueBulkOutTransfer(uint8_t slotId, uint8_t* data, uint64_t dataPhys, uint32_t length) {
|
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;
|
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
|
||||||
|
|
||||||
UsbDeviceInfo& dev = g_devices[slotId];
|
UsbDeviceInfo& dev = g_devices[slotId];
|
||||||
@@ -1264,7 +1517,7 @@ namespace Drivers::USB::Xhci {
|
|||||||
if (dev.BulkOutRingCCS) {
|
if (dev.BulkOutRingCCS) {
|
||||||
control |= TRB_CYCLE_BIT;
|
control |= TRB_CYCLE_BIT;
|
||||||
}
|
}
|
||||||
trb.Control = control;
|
PublishTrb(trb, control);
|
||||||
|
|
||||||
AdvanceBulkOutRing(dev);
|
AdvanceBulkOutRing(dev);
|
||||||
|
|
||||||
@@ -1277,8 +1530,23 @@ namespace Drivers::USB::Xhci {
|
|||||||
// BulkTransfer - blocking bulk IN/OUT transfer
|
// 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 BulkTransfer(uint8_t slotId, bool dirIn, void* data, uint64_t dataPhys,
|
||||||
uint32_t length, uint32_t* actualLength) {
|
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 (actualLength) *actualLength = 0;
|
||||||
if (length == 0) return CC_SUCCESS;
|
if (length == 0) return CC_SUCCESS;
|
||||||
if (length > 0x10000) return 0xFF;
|
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;
|
uint32_t control = (TRB_NORMAL << TRB_TYPE_SHIFT) | TRB_IOC | TRB_ISP;
|
||||||
if (dev.BulkInRingCCS) control |= TRB_CYCLE_BIT;
|
if (dev.BulkInRingCCS) control |= TRB_CYCLE_BIT;
|
||||||
trb.Control = control;
|
PublishTrb(trb, control);
|
||||||
|
|
||||||
AdvanceBulkInRing(dev);
|
AdvanceBulkInRing(dev);
|
||||||
target = dev.BulkInEpNum * 2 + 1;
|
target = dev.BulkInEpNum * 2 + 1;
|
||||||
@@ -1315,7 +1583,7 @@ namespace Drivers::USB::Xhci {
|
|||||||
|
|
||||||
uint32_t control = (TRB_NORMAL << TRB_TYPE_SHIFT) | TRB_IOC;
|
uint32_t control = (TRB_NORMAL << TRB_TYPE_SHIFT) | TRB_IOC;
|
||||||
if (dev.BulkOutRingCCS) control |= TRB_CYCLE_BIT;
|
if (dev.BulkOutRingCCS) control |= TRB_CYCLE_BIT;
|
||||||
trb.Control = control;
|
PublishTrb(trb, control);
|
||||||
|
|
||||||
AdvanceBulkOutRing(dev);
|
AdvanceBulkOutRing(dev);
|
||||||
target = dev.BulkOutEpNum * 2;
|
target = dev.BulkOutEpNum * 2;
|
||||||
@@ -1395,10 +1663,25 @@ namespace Drivers::USB::Xhci {
|
|||||||
return g_initialized;
|
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() {
|
bool HasDeferredWork() {
|
||||||
|
if (g_initialized && g_pollMode && EventRingHasWork()) {
|
||||||
|
g_irqDrainPending.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
return g_initialized &&
|
return g_initialized &&
|
||||||
(g_irqDrainPending.load(std::memory_order_acquire) ||
|
(g_irqDrainPending.load(std::memory_order_acquire) ||
|
||||||
g_deferredWorkPending.load(std::memory_order_acquire) ||
|
g_deferredWorkPending.load(std::memory_order_acquire) ||
|
||||||
|
(g_bootScanComplete && (Hub::HasPendingWork() || HidRecoveryDue())) ||
|
||||||
g_diagnosticWorkPending.load(std::memory_order_acquire));
|
g_diagnosticWorkPending.load(std::memory_order_acquire));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1448,7 +1731,9 @@ namespace Drivers::USB::Xhci {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void UnregisterClassDriver(uint8_t slotId, const UsbDeviceInfo& dev) {
|
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);
|
MassStorage::UnregisterDevice(slotId);
|
||||||
} else if (dev.InterfaceClass == UsbDevice::CLASS_HID &&
|
} else if (dev.InterfaceClass == UsbDevice::CLASS_HID &&
|
||||||
dev.InterfaceProtocol == UsbDevice::PROTOCOL_KEYBOARD) {
|
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.
|
// ProcessDeferredWork - handle hot-plug outside interrupt context.
|
||||||
// Called from the BSP idle path so class-driver probing can wait on timers.
|
// 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 haveDiagnostics = g_diagnosticWorkPending.load(std::memory_order_acquire);
|
||||||
bool haveHotplug = g_bootScanComplete &&
|
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;
|
if (!haveDiagnostics && !haveHotplug) return;
|
||||||
|
|
||||||
// CAS claim: any idle core may call this, but only one runs at a time.
|
// CAS claim: any idle core may call this, but only one runs at a time.
|
||||||
@@ -1505,6 +1890,9 @@ namespace Drivers::USB::Xhci {
|
|||||||
return;
|
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);
|
g_deferredWorkPending.store(false, std::memory_order_release);
|
||||||
|
|
||||||
for (uint32_t port = 0; port < g_maxPorts; port++) {
|
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++) {
|
for (uint32_t i = 0; i < 100000; i++) {
|
||||||
PollEvents();
|
PollEvents();
|
||||||
uint32_t ps = ReadOp(OP_PORTSC_BASE + port * OP_PORTSC_STRIDE);
|
uint32_t ps = ReadOp(OP_PORTSC_BASE + port * OP_PORTSC_STRIDE);
|
||||||
if (ps & PORTSC_PRC) {
|
if (PortResetComplete(ps)) {
|
||||||
resetDone = true;
|
resetDone = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1565,25 +1953,23 @@ namespace Drivers::USB::Xhci {
|
|||||||
UsbDevice::EnumerateDevice(port + 1, speed);
|
UsbDevice::EnumerateDevice(port + 1, speed);
|
||||||
}
|
}
|
||||||
} else {
|
} 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++) {
|
for (uint8_t s = 1; s <= MAX_SLOTS; s++) {
|
||||||
if (g_devices[s].Active && g_devices[s].PortId == port + 1) {
|
if (g_devices[s].Active && g_devices[s].PortId == port + 1 &&
|
||||||
UserUsb::DeviceDisconnected(s);
|
g_devices[s].ParentSlot == 0) {
|
||||||
UnregisterClassDriver(s, g_devices[s]);
|
KernelLogStream(INFO, "xHCI") << "Hot-unplug: port "
|
||||||
g_devices[s].Active = false;
|
<< base::dec << (uint64_t)(port + 1);
|
||||||
g_transferCallbacks[s] = nullptr;
|
RemoveDevice(s);
|
||||||
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";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Port changes behind hubs.
|
||||||
|
Hub::ProcessPendingWork();
|
||||||
|
|
||||||
g_hotplugProcessing.store(false, std::memory_order_release);
|
g_hotplugProcessing.store(false, std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1591,32 +1977,22 @@ namespace Drivers::USB::Xhci {
|
|||||||
// Probe (called by PCI driver matching framework)
|
// Probe (called by PCI driver matching framework)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
bool Probe(const Pci::PciDevice& dev) {
|
// Map the register block, reset the controller and set up its
|
||||||
if (g_initialized) return false;
|
// context and command/event rings. Interrupt delivery is the caller's.
|
||||||
|
static void InitializeController(uint64_t mmioPhys, uint64_t mmioSize) {
|
||||||
KernelLogStream(OK, "xHCI") << "Found controller at PCI "
|
for (uint64_t offset = 0; offset < mmioSize; offset += 0x1000) {
|
||||||
<< 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) {
|
|
||||||
Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset));
|
Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset));
|
||||||
}
|
}
|
||||||
|
|
||||||
g_mmioBase = (volatile uint8_t*)Memory::HHDM(mmioPhys);
|
g_mmioBase = (volatile uint8_t*)Memory::HHDM(mmioPhys);
|
||||||
|
|
||||||
Pci::EnableBusMaster(dev.Bus, dev.Device, dev.Function);
|
// Parse capability registers. CAPLENGTH and HCIVERSION share the
|
||||||
KernelLogStream(OK, "xHCI") << "Bus mastering enabled";
|
// 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
|
uint32_t hciVersion = capBase >> 16;
|
||||||
g_capLength = *(volatile uint8_t*)(g_mmioBase + CAP_CAPLENGTH);
|
|
||||||
|
|
||||||
uint32_t hciVersion = *(volatile uint16_t*)(g_mmioBase + CAP_HCIVERSION);
|
|
||||||
KernelLogStream(INFO, "xHCI") << "Version: " << base::hex << (uint64_t)hciVersion
|
KernelLogStream(INFO, "xHCI") << "Version: " << base::hex << (uint64_t)hciVersion
|
||||||
<< ", CapLength: " << (uint64_t)g_capLength;
|
<< ", CapLength: " << (uint64_t)g_capLength;
|
||||||
|
|
||||||
@@ -1636,6 +2012,16 @@ namespace Drivers::USB::Xhci {
|
|||||||
g_rtBase = g_mmioBase + rtsOff;
|
g_rtBase = g_mmioBase + rtsOff;
|
||||||
g_dbBase = g_mmioBase + dbOff;
|
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
|
KernelLogStream(INFO, "xHCI") << "MaxSlots: " << base::dec << (uint64_t)g_maxSlots
|
||||||
<< ", MaxPorts: " << (uint64_t)g_maxPorts
|
<< ", MaxPorts: " << (uint64_t)g_maxPorts
|
||||||
<< ", ScratchpadBufs: " << (uint64_t)maxScratchpadBufs;
|
<< ", ScratchpadBufs: " << (uint64_t)maxScratchpadBufs;
|
||||||
@@ -1681,6 +2067,8 @@ namespace Drivers::USB::Xhci {
|
|||||||
// Program CONFIG
|
// Program CONFIG
|
||||||
WriteOp(OP_CONFIG, g_maxSlots);
|
WriteOp(OP_CONFIG, g_maxSlots);
|
||||||
|
|
||||||
|
if (!g_dmaCoherent) g_ctrlBounce = AllocateDmaBuffer(g_ctrlBouncePhys);
|
||||||
|
|
||||||
// Allocate DCBAA
|
// Allocate DCBAA
|
||||||
g_dcbaa = (uint64_t*)AllocateDmaBuffer(g_dcbaaPhys);
|
g_dcbaa = (uint64_t*)AllocateDmaBuffer(g_dcbaaPhys);
|
||||||
WriteOp(OP_DCBAAP, (uint32_t)(g_dcbaaPhys & 0xFFFFFFFF));
|
WriteOp(OP_DCBAAP, (uint32_t)(g_dcbaaPhys & 0xFFFFFFFF));
|
||||||
@@ -1728,12 +2116,10 @@ namespace Drivers::USB::Xhci {
|
|||||||
g_evtRingCCS = true;
|
g_evtRingCCS = true;
|
||||||
g_evtRingDequeue = 0;
|
g_evtRingDequeue = 0;
|
||||||
KernelLogStream(OK, "xHCI") << "Event ring at phys " << base::hex << g_evtRingPhys;
|
KernelLogStream(OK, "xHCI") << "Event ring at phys " << base::hex << g_evtRingPhys;
|
||||||
|
}
|
||||||
|
|
||||||
// MSI setup
|
// Enable the interrupter, run the controller, power and enumerate ports.
|
||||||
if (!SetupMsi(dev.Bus, dev.Device, dev.Function)) {
|
static bool StartController() {
|
||||||
KernelLogStream(WARNING, "xHCI") << "MSI not available, using poll mode";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable interrupter 0
|
// Enable interrupter 0
|
||||||
WriteRt(IR0_IMAN, IMAN_IE);
|
WriteRt(IR0_IMAN, IMAN_IE);
|
||||||
WriteRt(IR0_IMOD, IMOD_INTERVAL_100US);
|
WriteRt(IR0_IMOD, IMOD_INTERVAL_100US);
|
||||||
@@ -1772,7 +2158,7 @@ namespace Drivers::USB::Xhci {
|
|||||||
for (uint32_t i = 0; i < 100000; i++) {
|
for (uint32_t i = 0; i < 100000; i++) {
|
||||||
PollEvents();
|
PollEvents();
|
||||||
uint32_t ps = ReadOp(OP_PORTSC_BASE + port * OP_PORTSC_STRIDE);
|
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();
|
for (int j = 0; j < 100; j++) Arch::CompilerBarrier();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1801,11 +2187,75 @@ namespace Drivers::USB::Xhci {
|
|||||||
UsbDevice::EnumerateDevice(port + 1, speed);
|
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;
|
g_bootScanComplete = true;
|
||||||
KernelLogStream(OK, "xHCI") << "Initialization complete";
|
KernelLogStream(OK, "xHCI") << "Initialization complete";
|
||||||
return true;
|
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)
|
// Initialize (standalone fallback path)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -2043,8 +2493,9 @@ namespace Drivers::USB::Xhci {
|
|||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Step 12: MSI setup
|
// Step 12: MSI setup
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
if (!SetupMsi(foundDev->Bus, foundDev->Device, foundDev->Function)) {
|
if (!SetupMsi(foundDev->Bus, foundDev->Device, foundDev->Function) && !SetupIntx(foundDev->Bus, foundDev->Device, foundDev->Function)) {
|
||||||
KernelLogStream(WARNING, "xHCI") << "MSI not available, using poll mode";
|
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)
|
// Completion codes (from Status field bits 31:24)
|
||||||
constexpr uint32_t CC_SUCCESS = 1;
|
constexpr uint32_t CC_SUCCESS = 1;
|
||||||
|
constexpr uint32_t CC_STALL_ERROR = 6;
|
||||||
constexpr uint32_t CC_SHORT_PACKET = 13;
|
constexpr uint32_t CC_SHORT_PACKET = 13;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -177,6 +178,10 @@ namespace Drivers::USB::Xhci {
|
|||||||
// Device Context structures (xHCI spec section 6.2)
|
// 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 {
|
struct SlotContext {
|
||||||
uint32_t Field0; // Route String, Speed, MTT, Hub, Context Entries
|
uint32_t Field0; // Route String, Speed, MTT, Hub, Context Entries
|
||||||
uint32_t Field1; // Max Exit Latency, Root Hub Port Number, Num Ports
|
uint32_t Field1; // Max Exit Latency, Root Hub Port Number, Num Ports
|
||||||
@@ -232,8 +237,21 @@ namespace Drivers::USB::Xhci {
|
|||||||
struct UsbDeviceInfo {
|
struct UsbDeviceInfo {
|
||||||
bool Active;
|
bool Active;
|
||||||
bool Ready; // descriptors/endpoints and binding are complete
|
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;
|
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 VendorId;
|
||||||
uint16_t ProductId;
|
uint16_t ProductId;
|
||||||
uint8_t InterfaceClass;
|
uint8_t InterfaceClass;
|
||||||
@@ -295,6 +313,19 @@ namespace Drivers::USB::Xhci {
|
|||||||
|
|
||||||
void Initialize();
|
void Initialize();
|
||||||
bool Probe(const Pci::PciDevice& dev);
|
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 IsInitialized();
|
||||||
bool HasDeferredWork();
|
bool HasDeferredWork();
|
||||||
|
|
||||||
@@ -337,6 +368,17 @@ namespace Drivers::USB::Xhci {
|
|||||||
// outstanding-transfer accounting before submitting fresh work.
|
// outstanding-transfer accounting before submitting fresh work.
|
||||||
void ResetBulkOutEndpoint(uint8_t slotId);
|
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
|
// True while PollEvents() is draining the event ring. A command submitter
|
||||||
// reached from inside an event callback must fire-and-forget (not wait),
|
// reached from inside an event callback must fire-and-forget (not wait),
|
||||||
// since a nested PollEvents() is a no-op.
|
// since a nested PollEvents() is a no-op.
|
||||||
@@ -374,6 +416,10 @@ namespace Drivers::USB::Xhci {
|
|||||||
// Access device info
|
// Access device info
|
||||||
UsbDeviceInfo* GetDevice(uint8_t slotId);
|
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
|
// Poll the event ring from process/idle context. The hard interrupt only
|
||||||
// acknowledges and masks the source, then queues deferred draining.
|
// acknowledges and masks the source, then queues deferred draining.
|
||||||
void PollEvents();
|
void PollEvents();
|
||||||
|
|||||||
@@ -276,6 +276,13 @@ namespace Efi {
|
|||||||
inline EFI_RESET_SYSTEM g_ResetSystem = nullptr;
|
inline EFI_RESET_SYSTEM g_ResetSystem = nullptr;
|
||||||
|
|
||||||
inline void Init(SystemTable* ST, const montauk::boot::EfiInfo& efi) {
|
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 Minor Revision: " << ST->Header.Revision.MinorRevision;
|
||||||
Kt::KernelLogStream(Kt::OK, "UEFI") << "ST Major Revision: " << ST->Header.Revision.MajorRevision;
|
Kt::KernelLogStream(Kt::OK, "UEFI") << "ST Major Revision: " << ST->Header.Revision.MajorRevision;
|
||||||
|
|
||||||
@@ -284,12 +291,6 @@ namespace Efi {
|
|||||||
if (ST->RuntimeServices != nullptr) {
|
if (ST->RuntimeServices != nullptr) {
|
||||||
Kt::KernelLogStream(Kt::OK, "UEFI") << "EFI Runtime Service API is available.";
|
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 Time;
|
||||||
EFI_TIME_CAPABILITIES TimeCapabilities;
|
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