Compare commits

..
11 Commits
105 changed files with 10681 additions and 426 deletions

No files matched your search

+6
View File
@@ -429,6 +429,12 @@ ifneq ($(ARCH),aarch64)
endif endif
./scripts/mk-rpi4-sd.sh kernel/bin-$(ARCH)/kernel ramdisk.tar $@ $(ROOTFS) $(PROGRAMS_BIN)-rpi ./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
+214 -3
View File
@@ -12,6 +12,50 @@ make ARCH=aarch64 rpi4-sd # -> montauk-rpi4.img
sudo dd if=montauk-rpi4.img of=/dev/sdX bs=4M conv=fsync 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 `scripts/mk-rpi4-sd.sh` downloads the firmware release once into
`rpi4-firmware/` (`PFTF_VERSION=v1.53` by default) and builds an MBR image: `rpi4-firmware/` (`PFTF_VERSION=v1.53` by default) and builds an MBR image:
@@ -25,6 +69,11 @@ module. Finding no ramdisk, the kernel mounts the partition labelled
`MONTAUKROOT` as `0:/` (`Fs/Boot.cpp`), read through the SD driver `MONTAUKROOT` as `0:/` (`Fs/Boot.cpp`), read through the SD driver
(`Drivers/Storage/Sdhci`), and the ESP after it. Files load as they are (`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. 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 Unlike the ramdisk, the root is persistent: changes to `0:/` survive a
reboot, so shut down from the desktop rather than pulling power. 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 The image is sized to its contents (`ROOT_FREE_MIB`, default 64, sets the
@@ -69,6 +118,15 @@ the firmware to load Limine from. The card is padded to
`RPI4_QEMU_CARD_MIB` (default 4096), and every run starts from a fresh `RPI4_QEMU_CARD_MIB` (default 4096), and every run starts from a fresh
copy, so every run is a first boot. 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: The firmware defaults are what MontaukOS needs; leave them:
- **System Table Mode: ACPI.** The kernel reads ACPI only. - **System Table Mode: ACPI.** The kernel reads ACPI only.
@@ -95,6 +153,11 @@ PL011 on GPIO 14/15: header pins 8 (TX) and 10 (RX), ground on pin 6, 115200
| Keyboard (built in), USB ports | VL805 xHCI as ACPI `\_SB.SCB0.XHC0` (`PNP0D10`) | `Drivers/USB/Xhci.cpp` platform probe | | 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` | | 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` | | 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 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 (the root bridge's `_DSD` says `linux-ecam-quirk-id = "bcm2711"`), so the
@@ -114,18 +177,166 @@ 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, keyboard or mouse is always a low/full-speed device behind a high-speed hub,
reached through that hub's transaction translator. 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 ## Not supported yet
- Ethernet (BCM54213 via GENET v5, `BCM6E4E`).
- SD high-speed mode: the driver transfers by polled PIO at 25 MHz on a - 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; 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. switching the card to 50 MHz (CMD6) would double that.
- Wi-Fi and Bluetooth (CYW43455 over SDIO/UART). - 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). - Audio (HDMI via VideoCore, the 3.5 mm jack via PWM).
## Status ## Status
Everything above is verified on QEMU only, including the Pi-specific path: 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 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 `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 DMA. Real hardware has not been tried yet. The ext2 root on the SD card boots on QEMU (`run-rpi4-sd`); the
+6 -6
View File
@@ -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;
} }
+22 -22
View File
@@ -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";
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 73 #define MONTAUK_BUILD_NUMBER 123
+16 -2
View File
@@ -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)");
} }
+5
View File
@@ -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");
} }
} }
+3 -1
View File
@@ -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);
+3
View File
@@ -405,6 +405,9 @@ namespace montauk::abi {
and returns 1 while more remains, 0 once done. CAP_STORAGE_ADMIN. */ 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 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;
+5 -1
View File
@@ -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;
} }
+8
View File
@@ -330,6 +330,14 @@ namespace Memory::VMM {
} }
} }
// 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(); Arch::ActivateKernelTranslation();
Kt::KernelLogStream(Kt::OK, "VMM") << "Switched to kernel translation tables (HHDM limit 0x" Kt::KernelLogStream(Kt::OK, "VMM") << "Switched to kernel translation tables (HHDM limit 0x"
+8
View File
@@ -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);
}
} }
} }
+6
View File
@@ -32,6 +32,7 @@
#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>
@@ -178,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");
+3
View File
@@ -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);
} }
+20 -20
View File
@@ -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
@@ -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;
@@ -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
+555
View File
@@ -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";
}
}
}
+27
View File
@@ -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
@@ -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})
@@ -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
+135 -2
View File
@@ -10,6 +10,7 @@
#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>
@@ -17,6 +18,7 @@
#include <Drivers/Storage/Sdhci.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>
@@ -355,10 +357,136 @@ namespace Drivers {
Storage::Sdhci::ProbePlatform(base, info.Path); 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(); ProbePlatformXhci();
ProbePlatformSdhci(); ProbePlatformSdhci();
ProbePlatformGenet();
ProbePlatformBroadcomWifi();
Bluetooth::ProbePlatformAdapter();
} }
void InitializeNetwork() { void InitializeNetwork() {
@@ -371,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().
} }
} }
+5
View File
@@ -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();
+713
View File
@@ -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);
}
}
+197
View File
@@ -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();
}
+218
View File
@@ -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);
}
+849
View File
@@ -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);
}
}
+592
View File
@@ -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;
}
}
+436
View File
@@ -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;
}
}
+63
View File
@@ -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();
}
+6
View File
@@ -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);
} }
+4 -4
View File
@@ -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 };
+279 -82
View File
@@ -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; }
+10
View File
@@ -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();
+2 -1
View File
@@ -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.
+284
View File
@@ -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]);
}
+69
View File
@@ -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;
}
}
+37
View File
@@ -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);
}
+488
View File
@@ -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();
}
}
+96
View File
@@ -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);
}
+6
View File
@@ -64,6 +64,7 @@ namespace Drivers::Serial {
static volatile uint8_t* g_mmio = nullptr; static volatile uint8_t* g_mmio = nullptr;
static uint16_t g_port = 0; static uint16_t g_port = 0;
static uint32_t g_stride = 1; static uint32_t g_stride = 1;
static uint64_t g_consoleAddress = 0;
static inline uint32_t Read32(uint64_t offset) { static inline uint32_t Read32(uint64_t offset) {
return *(volatile uint32_t*)(g_mmio + offset); return *(volatile uint32_t*)(g_mmio + offset);
@@ -173,9 +174,14 @@ namespace Drivers::Serial {
return; return;
} }
g_consoleAddress = reg.Address;
AttachSerialConsole(sink); AttachSerialConsole(sink);
KernelLogStream(OK, "Serial") << "Console on " << name << " at " << (isIo ? "port " : "") KernelLogStream(OK, "Serial") << "Console on " << name << " at " << (isIo ? "port " : "")
<< "0x" << base::hex << reg.Address; << "0x" << base::hex << reg.Address;
} }
uint64_t ConsoleAddress() {
return g_consoleAddress;
}
} }
+4
View File
@@ -15,4 +15,8 @@ namespace Drivers::Serial {
// tables (the registers are mapped uncached through the HHDM). // tables (the registers are mapped uncached through the HHDM).
void InitializeConsole(Hal::ACPI::CommonSDTHeader* xsdt); 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();
} }
+5
View File
@@ -335,6 +335,11 @@ namespace Drivers::USB::Hub {
g_pending.store(true, std::memory_order_release); 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() { bool HasPendingWork() {
return g_pending.load(std::memory_order_acquire); return g_pending.load(std::memory_order_acquire);
} }
+4
View File
@@ -47,6 +47,10 @@ namespace Drivers::USB::Hub {
// afresh, as if replugged. // afresh, as if replugged.
void RequestPortCheck(uint8_t hubSlot, uint8_t port); 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. // Port changes are waiting for ProcessPendingWork.
bool HasPendingWork(); bool HasPendingWork();
+90 -23
View File
@@ -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;
+65 -29
View File
@@ -11,7 +11,7 @@
#include "HidMouse.hpp" #include "HidMouse.hpp"
#include "MassStorage.hpp" #include "MassStorage.hpp"
#include "Hub.hpp" #include "Hub.hpp"
#include "Bluetooth/Bluetooth.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>
@@ -107,7 +107,13 @@ namespace Drivers::USB::UsbDevice {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static bool EnumerateInSlot(uint8_t slotId, const DeviceLocation& location, uint32_t speed, static bool EnumerateInSlot(uint8_t slotId, const DeviceLocation& location, uint32_t speed,
bool& isHub, Hub::HubDescriptorInfo& hubInfo); 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) {
DeviceLocation location = {}; DeviceLocation location = {};
@@ -126,29 +132,37 @@ namespace Drivers::USB::UsbDevice {
<< ", root port " << (uint64_t)location.RootPort << ") speed=" << SpeedToString(speed); << ", root port " << (uint64_t)location.RootPort << ") speed=" << SpeedToString(speed);
} }
// ----------------------------------------------------------------- for (int attempt = 1; ; attempt++) {
// Step 1: Enable Slot // -------------------------------------------------------------
// ----------------------------------------------------------------- // Step 1: Enable Slot
Xhci::TRB enableSlotTrb = {}; // -------------------------------------------------------------
enableSlotTrb.Control = (Xhci::TRB_ENABLE_SLOT << Xhci::TRB_TYPE_SHIFT); Xhci::TRB enableSlotTrb = {};
enableSlotTrb.Control = (Xhci::TRB_ENABLE_SLOT << Xhci::TRB_TYPE_SHIFT);
uint32_t cc = Xhci::SendCommand(enableSlotTrb); uint32_t cc = Xhci::SendCommand(enableSlotTrb);
if (cc != Xhci::CC_SUCCESS) { if (cc != Xhci::CC_SUCCESS) {
KernelLogStream(ERROR, "USB") << "Enable Slot failed, cc=" << (uint64_t)cc; KernelLogStream(ERROR, "USB") << "Enable Slot failed, cc=" << (uint64_t)cc;
return 0; return 0;
} }
uint8_t slotId = (uint8_t)Xhci::g_cmdCompletionSlotId; uint8_t slotId = (uint8_t)Xhci::g_cmdCompletionSlotId;
if (slotId == 0 || slotId > Xhci::MAX_SLOTS) { if (slotId == 0 || slotId > Xhci::MAX_SLOTS) {
KernelLogStream(ERROR, "USB") << "Invalid slot ID: " << (uint64_t)slotId; KernelLogStream(ERROR, "USB") << "Invalid slot ID: " << (uint64_t)slotId;
return 0; return 0;
} }
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId << " enabled"; 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;
}
bool isHub = false;
Hub::HubDescriptorInfo hubInfo = {};
if (!EnumerateInSlot(slotId, location, speed, isHub, hubInfo)) {
// Give the slot back, or every failed attempt (a flaky device // Give the slot back, or every failed attempt (a flaky device
// re-reported by its hub) would leak one until none are left. // re-reported by its hub) would leak one until none are left.
Xhci::TRB disableTrb = {}; Xhci::TRB disableTrb = {};
@@ -157,20 +171,36 @@ namespace Drivers::USB::UsbDevice {
Xhci::SendCommand(disableTrb); Xhci::SendCommand(disableTrb);
Xhci::g_dcbaa[slotId] = 0; Xhci::g_dcbaa[slotId] = 0;
*Xhci::GetDevice(slotId) = {}; *Xhci::GetDevice(slotId) = {};
return 0;
}
// A hub enumerates what is behind it now that its own slot is if (!retryable || attempt >= ENUM_TRIES) return 0;
// published (its children name it as their parent).
if (isHub) Hub::RegisterHub(slotId, hubInfo); // Back the device off to its default state, giving it longer to
return slotId; // 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, static bool EnumerateInSlot(uint8_t slotId, const DeviceLocation& location, uint32_t speed,
bool& isHub, Hub::HubDescriptorInfo& hubInfo) { bool& isHub, Hub::HubDescriptorInfo& hubInfo, bool& retryable) {
uint8_t portId = location.RootPort; uint8_t portId = location.RootPort;
uint32_t cc = 0; 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
// ----------------------------------------------------------------- // -----------------------------------------------------------------
@@ -274,7 +304,12 @@ namespace Drivers::USB::UsbDevice {
return false; return false;
} }
uint8_t bMaxPacketSize0 = partialDesc[7]; // USB 3 devices give bMaxPacketSize0 as an exponent (9: 512 bytes).
uint16_t bMaxPacketSize0 = partialDesc[7];
if (speed >= Xhci::SPEED_SUPER) {
bMaxPacketSize0 = (bMaxPacketSize0 >= 5 && bMaxPacketSize0 <= 15)
? (uint16_t)(1u << bMaxPacketSize0) : maxPacket;
}
if (bMaxPacketSize0 == 0) bMaxPacketSize0 = maxPacket; // fallback if (bMaxPacketSize0 == 0) bMaxPacketSize0 = maxPacket; // fallback
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId
@@ -572,6 +607,7 @@ namespace Drivers::USB::UsbDevice {
KernelLogStream(ERROR, "USB") << "SET_CONFIGURATION failed, cc=" << (uint64_t)cc; KernelLogStream(ERROR, "USB") << "SET_CONFIGURATION failed, cc=" << (uint64_t)cc;
return false; return false;
} }
retryable = false;
// ----------------------------------------------------------------- // -----------------------------------------------------------------
// Step 8b: Hub descriptor (the slot context must describe the hub) // Step 8b: Hub descriptor (the slot context must describe the hub)
+107 -1
View File
@@ -926,6 +926,30 @@ 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, static uint32_t ControlTransferLocked(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, uint64_t dataPhys, bool dirIn, bool trueNested); void* data, uint64_t dataPhys, bool dirIn, bool trueNested);
@@ -1068,7 +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]) {
return g_xferCompletionCode[slotId]; uint32_t cc = g_xferCompletionCode[slotId];
if (cc != CC_SUCCESS && cc != CC_SHORT_PACKET) RecoverEP0(slotId);
return cc;
} }
for (int j = 0; j < 100; j++) { for (int j = 0; j < 100; j++) {
Arch::CompilerBarrier(); Arch::CompilerBarrier();
@@ -1239,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
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
+12
View File
@@ -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;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -367,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.
+23 -2
View File
@@ -11,6 +11,8 @@
#include <Fs/Fat32.hpp> #include <Fs/Fat32.hpp>
#include <Fs/Ext2.hpp> #include <Fs/Ext2.hpp>
#include <Fs/FsProbe.hpp> #include <Fs/FsProbe.hpp>
#include <Drivers/Init.hpp>
#include <Timekeeping/Timer.hpp>
namespace Fs { namespace Fs {
@@ -18,6 +20,12 @@ namespace Fs {
constexpr const char* RootLabel = "MONTAUKROOT"; constexpr const char* RootLabel = "MONTAUKROOT";
// How long a boot without a ramdisk waits for the root partition to
// appear. A USB root (a stick, or a card reader still bringing its
// card up) can register after the boot scan; until then nothing would
// provide 0:/os/init.elf.
constexpr uint64_t RootWaitMs = 10000;
bool StringsEqual(const char* lhs, const char* rhs) { bool StringsEqual(const char* lhs, const char* rhs) {
if (lhs == nullptr || rhs == nullptr) return false; if (lhs == nullptr || rhs == nullptr) return false;
@@ -126,8 +134,21 @@ namespace Fs {
Fat32::RegisterProbe(); Fat32::RegisterProbe();
Ext2::RegisterProbe(); Ext2::RegisterProbe();
if (!hasRamdisk && FsProbe::MountRootByLabel(RootLabel) < 0) { if (!hasRamdisk && FsProbe::MountRootByLabel(RootLabel) < 0) {
Kt::KernelLogStream(Kt::WARNING, "Fs") Kt::KernelLogStream(Kt::INFO, "Fs")
<< "No ramdisk and no " << RootLabel << " partition; using the first one found"; << "Waiting for the " << RootLabel << " partition";
// A device that registers now gets its root partition mounted by
// FsProbe::MountPartitionsForBlockDevice, which knows the label.
// Counted in BusyWaitMs steps, which are bounded even without a tick.
for (uint64_t waited = 0; !Vfs::IsDriveRegistered(0) && waited < RootWaitMs; waited += 10) {
Drivers::ServiceStorageHotplug();
Timekeeping::BusyWaitMs(10);
}
if (!Vfs::IsDriveRegistered(0)) {
Kt::KernelLogStream(Kt::WARNING, "Fs")
<< "No ramdisk and no " << RootLabel << " partition; using the first one found";
}
} }
FsProbe::MountPartitions(hasRamdisk ? 1 : 0); FsProbe::MountPartitions(hasRamdisk ? 1 : 0);
} }
+10 -1
View File
@@ -16,6 +16,7 @@ namespace Fs::FsProbe {
static int g_probeCount = 0; static int g_probeCount = 0;
static bool g_mounted[Drivers::Storage::Gpt::MaxPartitions] = {}; static bool g_mounted[Drivers::Storage::Gpt::MaxPartitions] = {};
static int g_driveForPart[Drivers::Storage::Gpt::MaxPartitions] = {}; static int g_driveForPart[Drivers::Storage::Gpt::MaxPartitions] = {};
static const char* g_rootLabel = nullptr;
// A successful probe returns an owned mount. Registration transfers that // A successful probe returns an owned mount. Registration transfers that
// ownership to the VFS; if registration loses a drive-slot race, release // ownership to the VFS; if registration loses a drive-slot race, release
@@ -86,6 +87,7 @@ namespace Fs::FsProbe {
int MountRootByLabel(const char* label) { int MountRootByLabel(const char* label) {
if (!Vfs::IsInitialized() || g_probeCount == 0 || label == nullptr) return -1; if (!Vfs::IsInitialized() || g_probeCount == 0 || label == nullptr) return -1;
g_rootLabel = label;
if (Vfs::IsDriveRegistered(0)) return -1; if (Vfs::IsDriveRegistered(0)) return -1;
int partCount = Drivers::Storage::Gpt::GetPartitionCount(); int partCount = Drivers::Storage::Gpt::GetPartitionCount();
@@ -136,9 +138,16 @@ namespace Fs::FsProbe {
int MountPartitionsForBlockDevice(int blockDevIndex, int firstDrive) { int MountPartitionsForBlockDevice(int blockDevIndex, int firstDrive) {
if (!Vfs::IsInitialized() || g_probeCount == 0) return 0; if (!Vfs::IsInitialized() || g_probeCount == 0) return 0;
int partCount = Drivers::Storage::Gpt::GetPartitionCount();
int mounted = 0; int mounted = 0;
// Drive 0 free means the boot is still waiting for its root (Fs/Boot.cpp):
// this device may be the one carrying it, and nothing else may take 0.
if (firstDrive == 0 && !Vfs::IsDriveRegistered(0)) {
if (g_rootLabel != nullptr && MountRootByLabel(g_rootLabel) >= 0) mounted++;
firstDrive = 1;
}
int partCount = Drivers::Storage::Gpt::GetPartitionCount();
for (int i = 0; i < partCount; i++) { for (int i = 0; i < partCount; i++) {
auto* part = Drivers::Storage::Gpt::GetPartition(i); auto* part = Drivers::Storage::Gpt::GetPartition(i);
if (!part || part->BlockDevIndex != blockDevIndex || IsEfiPartition(part)) continue; if (!part || part->BlockDevIndex != blockDevIndex || IsEfiPartition(part)) continue;
+4 -1
View File
@@ -21,6 +21,8 @@ namespace Fs::FsProbe {
// Mount the non-EFI partition whose filesystem label is `label` as drive 0 // Mount the non-EFI partition whose filesystem label is `label` as drive 0
// (the system root when booting without a ramdisk). Returns the global // (the system root when booting without a ramdisk). Returns the global
// partition index, or -1 if none matched or drive 0 is already taken. // partition index, or -1 if none matched or drive 0 is already taken.
// The label is remembered: while drive 0 stays free, a block device that
// registers later (USB) is checked for it before anything else is mounted.
int MountRootByLabel(const char* label); int MountRootByLabel(const char* label);
// Probe all discovered storage partitions and auto-mount recognized filesystems. // Probe all discovered storage partitions and auto-mount recognized filesystems.
@@ -28,7 +30,8 @@ namespace Fs::FsProbe {
void MountPartitions(int firstDrive = 1); void MountPartitions(int firstDrive = 1);
// Probe discovered partitions for a single block device and mount recognized // Probe discovered partitions for a single block device and mount recognized
// filesystems at the lowest currently available VFS drive numbers. // filesystems at the lowest currently available VFS drive numbers. A free
// drive 0 is only ever given to the root partition MountRootByLabel names.
int MountPartitionsForBlockDevice(int blockDevIndex, int firstDrive = 0); int MountPartitionsForBlockDevice(int blockDevIndex, int firstDrive = 0);
// Try to mount a single partition (by global partition index) as the given VFS drive. // Try to mount a single partition (by global partition index) as the given VFS drive.
+13
View File
@@ -16,6 +16,7 @@
#include <Net/NetConfig.hpp> #include <Net/NetConfig.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 <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
@@ -56,6 +57,18 @@ namespace Net {
Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived); Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived);
} }
// Raspberry Pi 4/400 on-board Ethernet. Genet::IsLinkUp already
// reports no link for a controller that never initialized.
if (Drivers::Net::Genet::IsInitialized()) {
NetIf::Register({
"eth0", NetIf::Kind::Ethernet,
Drivers::Net::Genet::GetMacAddress,
Drivers::Net::Genet::SendPacket,
Drivers::Net::Genet::IsLinkUp,
});
Drivers::Net::Genet::SetRxCallback(Ethernet::OnFrameReceived);
}
// Wi-Fi registers whenever the adapter exists; its link only comes up // Wi-Fi registers whenever the adapter exists; its link only comes up
// once a network has been joined and keyed. // once a network has been joined and keyed.
if (Drivers::Net::Wifi::IsPresent()) { if (Drivers::Net::Wifi::IsPresent()) {
+3 -1
View File
@@ -37,7 +37,9 @@ static int64_t DateToEpoch(int year, int month, int day, int hour, int minute, i
static Timekeeping::DateTime EpochToDate(int64_t epoch) { static Timekeeping::DateTime EpochToDate(int64_t epoch) {
Timekeeping::DateTime dt = {}; Timekeeping::DateTime dt = {};
int64_t rem = epoch; /* Before the first NTP sync on a board with no RTC the clock sits near
the epoch, and a negative time zone pushes local time before it. */
int64_t rem = epoch < 0 ? 0 : epoch;
int year = 1970; int year = 1970;
while (true) { while (true) {
int64_t daysInYear = IsLeapYear(year) ? 366 : 365; int64_t daysInYear = IsLeapYear(year) ? 366 : 365;
+15 -6
View File
@@ -16,10 +16,12 @@
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.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/Audio/IntelHda.hpp> #include <Drivers/Audio/IntelHda.hpp>
#include <Drivers/USB/Xhci.hpp> #include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp> #include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Serial/BtUart.hpp>
#include <Drivers/USB/HidKeyboard.hpp> #include <Drivers/USB/HidKeyboard.hpp>
using namespace Kt; using namespace Kt;
@@ -58,10 +60,15 @@ namespace Timekeeping {
// desktop may never enter idle. Force one bounded idle-context // desktop may never enter idle. Force one bounded idle-context
// pass for USB immediately, and for network RX at most every // pass for USB immediately, and for network RX at most every
// 10 ms (important for E1000E's polling fallback). // 10 ms (important for E1000E's polling fallback).
bool usbWork = Drivers::USB::Xhci::HasDeferredWork(); // UART Bluetooth (Raspberry Pi) has no xHCI behind it: its
// received bytes wait in the UART ring for the same pass.
bool usbWork = Drivers::USB::Xhci::HasDeferredWork() ||
Drivers::Serial::BtUart::HasRxData();
bool audioWork = Drivers::Audio::IntelHda::HasDeferredWork(); bool audioWork = Drivers::Audio::IntelHda::HasDeferredWork();
bool netWork = Drivers::Net::E1000::HasDeferredWork() || bool netWork = Drivers::Net::E1000::HasDeferredWork() ||
Drivers::Net::E1000E::HasDeferredWork(); Drivers::Net::E1000E::HasDeferredWork() ||
Drivers::Net::Genet::HasDeferredWork() ||
Drivers::Net::Wifi::HasPolledWork();
// A forced trip into the idle bottom-half context is safe only when // A forced trip into the idle bottom-half context is safe only when
// this timer interrupted ring 3. If it interrupted a syscall, that // this timer interrupted ring 3. If it interrupted a syscall, that
// suspended kernel stack may own one of the process-context mutexes // suspended kernel stack may own one of the process-context mutexes
@@ -172,6 +179,8 @@ namespace Timekeeping {
Drivers::Net::E1000::ProcessDeferredWork(); Drivers::Net::E1000::ProcessDeferredWork();
if (Drivers::Net::E1000E::HasDeferredWork()) if (Drivers::Net::E1000E::HasDeferredWork())
Drivers::Net::E1000E::ProcessDeferredWork(); Drivers::Net::E1000E::ProcessDeferredWork();
if (Drivers::Net::Genet::HasDeferredWork())
Drivers::Net::Genet::ProcessDeferredWork();
// HDA completion IRQs only acknowledge/mask. Resampling and DMA-ring // HDA completion IRQs only acknowledge/mask. Resampling and DMA-ring
// refill are far too expensive for hard interrupt context. // refill are far too expensive for hard interrupt context.
@@ -185,9 +194,9 @@ namespace Timekeeping {
// Cheap no-op unless an adapter is waiting; self-claiming, and safe // Cheap no-op unless an adapter is waiting; self-claiming, and safe
// to preempt (the scheduler saves/resumes the idle context). // to preempt (the scheduler saves/resumes the idle context).
bool bluetoothWork = usbWork || bool bluetoothWork = usbWork ||
Drivers::USB::Bluetooth::HasDeferredWork(); Drivers::Bluetooth::HasDeferredWork();
if (bluetoothWork) if (bluetoothWork)
Drivers::USB::Bluetooth::ServiceDeferredInit(); Drivers::Bluetooth::ServiceDeferredInit();
// Service Bluetooth inbound traffic (the headset's SDP/AVRCP queries, // Service Bluetooth inbound traffic (the headset's SDP/AVRCP queries,
// AVDTP commands, ACL flow-control credits) whenever a core idles. // AVDTP commands, ACL flow-control credits) whenever a core idles.
@@ -197,7 +206,7 @@ namespace Timekeeping {
// queries never answered). Self-serializing and a cheap no-op when // queries never answered). Self-serializing and a cheap no-op when
// the adapter is down. // the adapter is down.
if (bluetoothWork) if (bluetoothWork)
Drivers::USB::Bluetooth::ServiceEvents(); Drivers::Bluetooth::ServiceEvents();
// Wi-Fi mirrors the Bluetooth split: the firmware load needs the // Wi-Fi mirrors the Bluetooth split: the firmware load needs the
// ramdisk, and the RX/notification ring must be drained outside hard // ramdisk, and the RX/notification ring must be drained outside hard
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,98 @@
# Cloned from bcm94345wlpagb_p2xx.txt
NVRAMRev=$Rev: 498373 $
sromrev=11
vendid=0x14e4
devid=0x43ab
manfid=0x2d0
prodid=0x06e4
#macaddr=00:90:4c:c5:12:38
macaddr=b8:27:eb:74:f2:6c
nocrc=1
boardtype=0x6e4
boardrev=0x1304
#XTAL 37.4MHz
xtalfreq=37400
btc_mode=1
#------------------------------------------------------
#boardflags: 5GHz eTR switch by default
# 2.4GHz eTR switch by default
# bit1 for btcoex
boardflags=0x00480201
boardflags2=0x40800000
boardflags3=0x44200100
phycal_tempdelta=15
rxchain=1
txchain=1
aa2g=1
aa5g=1
tssipos5g=1
tssipos2g=1
femctrl=0
AvVmid_c0=1,165,2,100,2,100,2,100,2,100
pa2ga0=-129,6525,-718
pa2ga1=-149,4408,-601
pa5ga0=-185,6836,-815,-186,6838,-815,-184,6859,-815,-184,6882,-818
pa5ga1=-202,4285,-574,-201,4312,-578,-196,4391,-586,-201,4294,-575
itrsw=1
pdoffsetcckma0=2
pdoffset2gperchan=0,-2,1,0,1,0,1,1,1,0,0,-1,-1,0
pdoffset2g40ma0=16
pdoffset40ma0=0x8888
pdoffset80ma0=0x8888
extpagain5g=2
extpagain2g=2
tworangetssi2g=1
tworangetssi5g=1
# LTECX flags
# WCI2
ltecxmux=0
ltecxpadnum=0x0504
ltecxfnsel=0x22
ltecxgcigpio=0x32
maxp2ga0=80
ofdmlrbw202gpo=0x0022
dot11agofdmhrbw202gpo=0x4442
mcsbw202gpo=0x98444422
mcsbw402gpo=0x98444422
maxp5ga0=82,82,82,82
mcsbw205glpo=0xb9555000
mcsbw205gmpo=0xb9555000
mcsbw205ghpo=0xb9555000
mcsbw405glpo=0xb9555000
mcsbw405gmpo=0xb9555000
mcsbw405ghpo=0xb9555000
mcsbw805glpo=0xb9555000
mcsbw805gmpo=0xb9555000
mcsbw805ghpo=0xb9555000
swctrlmap_2g=0x00000000,0x00000000,0x00000000,0x010000,0x3ff
swctrlmap_5g=0x00100010,0x00200020,0x00200020,0x010000,0x3fe
swctrlmapext_5g=0x00000000,0x00000000,0x00000000,0x000000,0x3
swctrlmapext_2g=0x00000000,0x00000000,0x00000000,0x000000,0x3
vcodivmode=1
deadman_to=481500000
ed_thresh2g=-54
ed_thresh5g=-54
eu_edthresh2g=-54
eu_edthresh5g=-54
ldo1=4
rawtempsense=0x1ff
cckPwrIdxCorr=3
cckTssiDelay=150
ofdmTssiDelay=150
txpwr2gAdcScale=1
txpwr5gAdcScale=1
dot11b_opts=0x3aa85
cbfilttype=1
fdsslevel_ch11=6
# Improved Bluetooth coexistence parameters from Cypress
btc_mode=1
btc_params8=0x4e20
btc_params1=0x7530
btc_params50=0x972c
Binary file not shown.
@@ -0,0 +1,97 @@
# Cloned from bcm94345wlpagb_p2xx.txt
NVRAMRev=$Rev: 498373 $
sromrev=11
vendid=0x14e4
devid=0x43ab
manfid=0x2d0
prodid=0x06e4
#macaddr=00:90:4c:c5:12:38
macaddr=b8:27:eb:74:f2:6c
nocrc=1
boardtype=0x6e4
boardrev=0x1304
#XTAL 37.4MHz
xtalfreq=37400
btc_mode=1
#------------------------------------------------------
#boardflags: 5GHz eTR switch by default
# 2.4GHz eTR switch by default
# bit1 for btcoex
boardflags=0x00480201
boardflags2=0x40800000
boardflags3=0x44200100
phycal_tempdelta=15
rxchain=1
txchain=1
aa2g=1
aa5g=1
tssipos5g=1
tssipos2g=1
femctrl=0
AvVmid_c0=0,157,1,126,1,126,1,126,1,126
pa2ga0=-170,5896,-680
pa2ga1=-156,4032,-547
pa5ga0=-150,5547,-628,-150,5583,-640,-158,5608,-649,-160,5757,-673
pa5ga1=-109,3831,-519,-88,3821,-512,-125,3731,-528,-103,3829,-516
itrsw=1
pdoffsetcckma0=2
pdoffset2gperchan=0,-2,1,0,1,0,1,1,1,0,0,-1,-1,0
pdoffset2g40ma0=16
pdoffset40ma0=0x8888
pdoffset80ma0=0x8888
extpagain5g=2
extpagain2g=2
tworangetssi2g=1
tworangetssi5g=1
# LTECX flags
# WCI2
ltecxmux=0
ltecxpadnum=0x0504
ltecxfnsel=0x22
ltecxgcigpio=0x32
maxp2ga0=80
ofdmlrbw202gpo=0x0022
dot11agofdmhrbw202gpo=0x4442
mcsbw202gpo=0x98444422
mcsbw402gpo=0x98444422
maxp5ga0=82,82,82,82
mcsbw205glpo=0xb9555000
mcsbw205gmpo=0xb9555000
mcsbw205ghpo=0xb9555000
mcsbw405glpo=0xb9555000
mcsbw405gmpo=0xb9555000
mcsbw405ghpo=0xb9555000
mcsbw805glpo=0xb9555000
mcsbw805gmpo=0xb9555000
mcsbw805ghpo=0xb9555000
swctrlmap_2g=0x00000000,0x00000000,0x00000000,0x010000,0x3ff
swctrlmap_5g=0x00100010,0x00200020,0x00200020,0x010000,0x3fe
swctrlmapext_5g=0x00000000,0x00000000,0x00000000,0x000000,0x3
swctrlmapext_2g=0x00000000,0x00000000,0x00000000,0x000000,0x3
vcodivmode=1
deadman_to=481500000
ed_thresh2g=-54
ed_thresh5g=-54
eu_edthresh2g=-54
eu_edthresh5g=-54
ldo1=4
rawtempsense=0x1ff
cckPwrIdxCorr=3
cckTssiDelay=150
ofdmTssiDelay=150
txpwr2gAdcScale=1
txpwr5gAdcScale=1
dot11b_opts=0x3aa85
cbfilttype=1
fdsslevel_ch11=6
# Improved Bluetooth coexistence parameters from Cypress
btc_mode=1
btc_params8=0x4e20
btc_params1=0x7530
@@ -0,0 +1,139 @@
### CYPRESS WIRELESS CONNECTIVITY DEVICES
### DRIVER END USER LICENSE AGREEMENT (SOURCE AND BINARY DISTRIBUTION)
PLEASE READ THIS END USER LICENSE AGREEMENT ("Agreement") CAREFULLY BEFORE
DOWNLOADING, INSTALLING, OR USING THIS SOFTWARE, ANY ACCOMPANYING
DOCUMENTATION, OR ANY UPDATES PROVIDED BY CYPRESS ("Software"). BY
DOWNLOADING, INSTALLING, OR USING THE SOFTWARE, YOU ARE AGREEING TO BE BOUND
BY THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL OF THE TERMS OF THIS
AGREEMENT, PROMPTLY RETURN AND DO NOT USE THE SOFTWARE. IF YOU HAVE
PURCHASED THE SOFTWARE, YOUR RIGHT TO RETURN THE SOFTWARE EXPIRES 30 DAYS
AFTER YOUR PURCHASE AND APPLIES ONLY TO THE ORIGINAL PURCHASER.
Software Provided in Binary Code Form. This paragraph applies to any Software
provided in binary code form. Subject to the terms and conditions of this
Agreement, Cypress Semiconductor Corporation ("Cypress") grants you a
non-exclusive, non-transferable license under its copyright rights in the
Software to reproduce and distribute the Software in object code form only,
solely for use in connection with Cypress integrated circuit products
("Purpose").
Software Provided in Source Code Form. This paragraph applies to any Software
provided in source code form ("Cypress Source Code"). Subject to the terms and
conditions of this Agreement, Cypress grants you a non-exclusive,
non-transferable license under its copyright rights in the Cypress Source Code
to reproduce, modify, compile, and distribute the Cypress Source Code (whether
in source code form or as compiled into binary code form) solely for the
Purpose. Cypress retains ownership of the Cypress Source Code and any compiled
version thereof. Subject to Cypress' ownership of the underlying Cypress
Source Code, you retain ownership of any modifications you make to the
Cypress Source Code. You agree not to remove any Cypress copyright or other
notices from the Cypress Source Code and any modifications thereof. Any
reproduction, modification, translation, compilation, or representation of
the Cypress Source Code except as permitted in this paragraph is prohibited
without the express written permission of Cypress.
Free and Open Source Software. Portions of the Software may be licensed under
free and/or open source licenses such as the GNU General Public License
("FOSS"). FOSS is subject to the applicable license agreement and not this
Agreement. If you are entitled to receive the source code from Cypress for any
FOSS included with the Software, either the source code will be included with
the Software or you may obtain the source code at no charge from
<http://www.cypress.com/go/opensource>. The applicable license terms will
accompany each source code package. To review the license terms applicable to
any FOSS for which Cypress is not required to provide you with source code,
please see the Software's installation directory on your computer.
Proprietary Rights. The Software, including all intellectual property rights
therein, is and will remain the sole and exclusive property of Cypress or its
suppliers. Except as otherwise expressly provided in this Agreement, you may
not: (i) modify, adapt, or create derivative works based upon the Software;
(ii) copy the Software; (iii) except and only to the extent explicitly
permitted by applicable law despite this limitation, decompile, translate,
reverse engineer, disassemble or otherwise reduce the Software to
human-readable form; or (iv) use the Software other than for the Purpose.
No Support. Cypress may, but is not required to, provide technical support for
the Software.
Term and Termination. This Agreement is effective until terminated. This
Agreement and Your license rights will terminate immediately without notice
from Cypress if you fail to comply with any provision of this Agreement. Upon
termination, you must destroy all copies of Software in your possession or
control. Termination of this Agreement will not affect any licenses validly
granted as of the termination date to any end users of the Software. The
following paragraphs shall survive any termination of this Agreement: "Free and
Open Source Software," "Proprietary Rights," "Compliance With Law,"
"Disclaimer," "Limitation of Liability," and "General."
Compliance With Law. Each party agrees to comply with all applicable laws,
rules and regulations in connection with its activities under this Agreement.
Without limiting the foregoing, the Software may be subject to export control
laws and regulations of the United States and other countries. You agree to
comply strictly with all such laws and regulations and acknowledge that you
have the responsibility to obtain licenses to export, re-export, or import
the Software.
Disclaimer. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, CYPRESS MAKES
NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THE SOFTWARE,
INCLUDING, BUT NOT LIMITED TO, INFRINGEMENT AND THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. Cypress reserves the
right to make changes to the Software without notice. Cypress does not assume
any liability arising out of the application or use of Software or any
product or circuit described in the Software. Cypress does not authorize its
products for use as critical components in life-support systems where a
malfunction or failure may reasonably be expected to result in significant
injury to the user. The inclusion of Cypress' product in a life-support
system or application implies that the manufacturer of such system or
application assumes all risk of such use and in doing so indemnifies Cypress
against all charges.
Limitation of Liability. IN NO EVENT WILL CYPRESS OR ITS SUPPLIERS,
RESELLERS, OR DISTRIBUTORS BE LIABLE FOR ANY LOST REVENUE, PROFIT, OR DATA,
OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES
HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE
USE OF OR INABILITY TO USE THE SOFTWARE EVEN IF CYPRESS OR ITS SUPPLIERS,
RESELLERS, OR DISTRIBUTORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES. IN NO EVENT SHALL CYPRESS' OR ITS SUPPLIERS' RESELLERS', OR
DISTRIBUTORS' TOTAL LIABILITY TO YOU, WHETHER IN CONTRACT, TORT (INCLUDING
NEGLIGENCE), OR OTHERWISE, EXCEED THE PRICE PAID BY YOU FOR THE SOFTWARE.
THE FOREGOING LIMITATIONS SHALL APPLY EVEN IF THE ABOVE-STATED WARRANTY FAILS
OF ITS ESSENTIAL PURPOSE. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW
LIMITATION OR EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE
LIMITATION MAY NOT APPLY TO YOU.
Restricted Rights. The Software under this Agreement is commercial computer
software as that term is described in 48 C.F.R. 252.227-7014(a)(1). If
acquired by or on behalf of a civilian agency, the U.S. Government acquires
this commercial computer software and/or commercial computer software
documentation subject to the terms of this Agreement as specified in 48
C.F.R. 12.212 (Computer Software) and 12.211 (Technical Data) of the Federal
Acquisition Regulations ("FAR") and its successors. If acquired by or on
behalf of any agency within the Department of Defense ("DOD"), the U.S.
Government acquires this commercial computer software and/or commercial
computer software documentation subject to the terms of this Agreement as
specified in 48 C.F.R. 227.7202-3 of the DOD FAR Supplement ("DFAR") and its
successors.
General. This Agreement will bind and inure to the benefit of each party's
successors and assigns, provided that you may not assign or transfer this
Agreement, in whole or in part, without Cypress' written consent. This
Agreement shall be governed by and construed in accordance with the laws of
the State of California, United States of America, as if performed wholly
within the state and without giving effect to the principles of conflict of
law. The parties consent to personal and exclusive jurisdiction of and venue
in, the state and federal courts within Santa Clara County, California;
provided however, that nothing in this Agreement will limit Cypress' right to
bring legal action in any venue in order to protect or enforce its
intellectual property rights. No failure of either party to exercise or
enforce any of its rights under this Agreement will act as a waiver of such
rights. If any portion hereof is found to be void or unenforceable, the
remaining provisions of this Agreement shall remain in full force and
effect. This Agreement is the complete and exclusive agreement between the
parties with respect to the subject matter hereof, superseding and replacing
any and all prior agreements, communications, and understandings (both
written and oral) regarding such subject matter. Any notice to Cypress will
be deemed effective when actually received and must be sent to Cypress
Semiconductor Corporation, ATTN: Chief Legal Officer, 198 Champion Court, San
Jose, CA 95134 USA.
+75
View File
@@ -655,6 +655,81 @@ License: zlib License
Jean-loup Gailly Mark Adler Jean-loup Gailly Mark Adler
jloup@gzip.org madler@alumni.caltech.edu jloup@gzip.org madler@alumni.caltech.edu
================================================================================
23. Cypress (Broadcom) Bluetooth firmware
================================================================================
Shipped as binary firmware patch files (0:/os/firmware/brcm/BCM4345C0.hcd and
BCM4345C5.hcd), loaded at runtime into the CYW43455 / CYW43456 Bluetooth
controller of the Raspberry Pi 4 and 400 by the MontaukOS Bluetooth driver.
The files are taken unmodified from Raspberry Pi OS's bluez-firmware package.
Copyright (c) Cypress Semiconductor Corporation (an Infineon company)
Upstream: https://github.com/RPi-Distro/bluez-firmware
(debian/firmware/broadcom/)
License: Cypress Wireless Connectivity Devices Driver End User License
Agreement (source and binary distribution)
Reproduction and distribution in object code form are permitted solely for
use with Cypress integrated circuit products. The full license text is
included at 0:/os/licenses/Cypress-bt-firmware.txt.
================================================================================
24. Cypress / Synaptics (Broadcom) Wi-Fi firmware
================================================================================
Shipped as binary firmware, CLM regulatory blobs and NVRAM files
(0:/os/firmware/brcm/brcmfmac43455-sdio.{bin,clm_blob,txt} and
brcmfmac43456-sdio.{bin,clm_blob,txt}), loaded at runtime into the CYW43455 /
CYW43456 Wi-Fi radio of the Raspberry Pi 4 and 400 by the MontaukOS Wi-Fi
driver. The files are taken unmodified from Raspberry Pi OS's firmware-nonfree
package (brcmfmac43455-sdio.bin and .clm_blob are its
cypress/cyfmac43455-sdio-standard.bin and cyfmac43455-sdio.clm_blob).
Upstream: https://github.com/RPi-Distro/firmware-nonfree
(debian/config/brcm80211/)
CYW43455 firmware, CLM blob and NVRAM:
Copyright (c) Cypress Semiconductor Corporation (an Infineon company)
License: Cypress Wireless Connectivity Devices Driver End User License
Agreement (the same text as for the Bluetooth firmware above); full
text at 0:/os/licenses/Cypress-bt-firmware.txt. Reproduction and
distribution in object code form are permitted solely for use with
Cypress integrated circuit products.
CYW43456 firmware, CLM blob and NVRAM:
Copyright (c) Synaptics Incorporated
License: Synaptics Wireless Connectivity Devices Driver End User License
Agreement (binary distribution); full text at
0:/os/licenses/Synaptics-wifi-firmware.txt. Distribution in object code
form is permitted solely for use with Synaptics integrated circuit
products.
================================================================================
25. Linux brcmfmac (Broadcom FullMAC Wi-Fi driver)
================================================================================
The MontaukOS Broadcom Wi-Fi driver (kernel/src/Drivers/Net/Wifi/Brcmf.hpp,
BrcmfChip.cpp, BrcmfProto.cpp, BrcmfConnect.cpp) is derived in part from the
brcmfmac driver in the Linux kernel (drivers/net/wireless/broadcom/brcm80211/):
its chip enumeration, core reset, clock and firmware start-up sequences, NVRAM
format, and the firmware interface's constants and structure layouts.
License: ISC License
Copyright (c) 2010-2014 Broadcom Corporation
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
================================================================================ ================================================================================
For licensing inquiries regarding MontaukOS itself, contact: licensing@montaukos.org For licensing inquiries regarding MontaukOS itself, contact: licensing@montaukos.org
================================================================================ ================================================================================
@@ -0,0 +1,161 @@
SYNAPTICS WIRELESS CONNECTIVITY DEVICES
DRIVER END USER LICENSE AGREEMENT (BINARY DISTRIBUTION)
PLEASE READ THIS END USER LICENSE AGREEMENT ("Agreement") CAREFULLY BEFORE
DOWNLOADING, INSTALLING, OR USING THIS SOFTWARE, ANY ACCOMPANYING
DOCUMENTATION, OR ANY UPDATES PROVIDED BY SYNAPTICS ("Software"). BY
DOWNLOADING, INSTALLING, OR USING THE SOFTWARE, YOU ARE AGREEING TO BE BOUND
BY THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL OF THE TERMS OF THIS
AGREEMENT, PROMPTLY RETURN AND DO NOT USE THE SOFTWARE. IF YOU HAVE
PURCHASED THE SOFTWARE, YOUR RIGHT TO RETURN THE SOFTWARE EXPIRES 30 DAYS
AFTER YOUR PURCHASE AND APPLIES ONLY TO THE ORIGINAL PURCHASER.
Software Provided in Binary Code Form. This paragraph applies to any Software
provided in binary code form. Subject to the terms and conditions of this
Agreement, Synaptics Incorporated ("Synaptics") grants you a
non-exclusive, non-transferable license under its copyright rights in the
Software to reproduce and distribute the Software in object code form only,
solely for use in connection with Synaptics integrated circuit products
("Purpose").
Proprietary Rights. The Software, including all intellectual property rights
therein, is and will remain the sole and exclusive property of Synaptics or its
suppliers. Except as otherwise expressly provided in this Agreement, you may
not: (i) modify, adapt, or create derivative works based upon the Software;
(ii) copy the Software; (iii) except and only to the extent explicitly
permitted by applicable law despite this limitation, decompile, translate,
reverse engineer, disassemble or otherwise reduce the Software to
human-readable form; or (iv) use the Software other than for the Purpose.
No Support. Synaptics may, but is not required to, provide technical support for
the Software.
Term and Termination. This Agreement is effective until terminated. This
Agreement and Your license rights will terminate immediately without notice
from Synaptics if you fail to comply with any provision of this Agreement. Upon
termination, you must destroy all copies of Software in your possession or
control. Termination of this Agreement will not affect any licenses validly
granted as of the termination date to any end users of the Software. The
following paragraphs shall survive any termination of this Agreement:
"Proprietary Rights," "Compliance With Law,"
"Disclaimer," "Limitation of Liability," and "General."
Compliance With Law. Each party agrees to comply with all applicable laws,
rules and regulations in connection with its activities under this Agreement.
Without limiting the foregoing, the Software may be subject to export control
laws and regulations of the United States and other countries. You agree to
comply strictly with all such laws and regulations and acknowledge that you
have the responsibility to obtain licenses to export, re-export, or import
the Software.
Disclaimer. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, SYNAPTICS MAKES
NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THE SOFTWARE,
INCLUDING, BUT NOT LIMITED TO, INFRINGEMENT AND THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. Synaptics reserves the
right to make changes to the Software without notice. Synaptics does not assume
any liability arising out of the application or use of Software or any
product or circuit described in the Software. Synaptics does not authorize its
products for use as critical components in life-support systems where a
malfunction or failure may reasonably be expected to result in significant
injury to the user. The inclusion of Synaptics product in a life-support
system or application implies that the manufacturer of such system or
application assumes all risk of such use and in doing so indemnifies Synaptics
against all charges.
Limitation of Liability. IN NO EVENT WILL SYNAPTICS OR ITS SUPPLIERS,
RESELLERS, OR DISTRIBUTORS BE LIABLE FOR ANY LOST REVENUE, PROFIT, OR DATA,
OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES
HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE
USE OF OR INABILITY TO USE THE SOFTWARE EVEN IF SYNAPTICS OR ITS SUPPLIERS,
RESELLERS, OR DISTRIBUTORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES. IN NO EVENT SHALL SYNAPTICS' OR ITS SUPPLIERS' RESELLERS', OR
DISTRIBUTORS' TOTAL LIABILITY TO YOU, WHETHER IN CONTRACT, TORT (INCLUDING
NEGLIGENCE), OR OTHERWISE, EXCEED THE PRICE PAID BY YOU FOR THE SOFTWARE.
THE FOREGOING LIMITATIONS SHALL APPLY EVEN IF THE ABOVE-STATED WARRANTY FAILS
OF ITS ESSENTIAL PURPOSE. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW
LIMITATION OR EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE
LIMITATION MAY NOT APPLY TO YOU.
Restricted Rights. The Software under this Agreement is commercial computer
software as that term is described in 48 C.F.R. 252.227-7014(a)(1). If
acquired by or on behalf of a civilian agency, the U.S. Government acquires
this commercial computer software and/or commercial computer software
documentation subject to the terms of this Agreement as specified in 48
C.F.R. 12.212 (Computer Software) and 12.211 (Technical Data) of the Federal
Acquisition Regulations ("FAR") and its successors. If acquired by or on
behalf of any agency within the Department of Defense ("DOD"), the U.S.
Government acquires this commercial computer software and/or commercial
computer software documentation subject to the terms of this Agreement as
specified in 48 C.F.R. 227.7202-3 of the DOD FAR Supplement ("DFAR") and its
successors.
General. This Agreement will bind and inure to the benefit of each party's
successors and assigns, provided that you may not assign or transfer this
Agreement, in whole or in part, without Synaptics' written consent. This
Agreement shall be governed by and construed in accordance with the laws of
the State of California, United States of America, as if performed wholly
within the state and without giving effect to the principles of conflict of
law. The parties consent to personal and exclusive jurisdiction of and venue
in, the state and federal courts within Santa Clara County, California;
provided however, that nothing in this Agreement will limit Synaptics' right to
bring legal action in any venue in order to protect or enforce its
intellectual property rights. No failure of either party to exercise or
enforce any of its rights under this Agreement will act as a waiver of such
rights. If any portion hereof is found to be void or unenforceable, the
remaining provisions of this Agreement shall remain in full force and
effect. This Agreement is the complete and exclusive agreement between the
parties with respect to the subject matter hereof, superseding and replacing
any and all prior agreements, communications, and understandings (both
written and oral) regarding such subject matter. Any notice to Synaptics will
application assumes all risk of such use and in doing so indemnifies Synaptics
against all charges.
Limitation of Liability. IN NO EVENT WILL SYNAPTICS OR ITS SUPPLIERS,
RESELLERS, OR DISTRIBUTORS BE LIABLE FOR ANY LOST REVENUE, PROFIT, OR DATA,
OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES
HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE
USE OF OR INABILITY TO USE THE SOFTWARE EVEN IF SYNAPTICS OR ITS SUPPLIERS,
RESELLERS, OR DISTRIBUTORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES. IN NO EVENT SHALL SYNAPTICS' OR ITS SUPPLIERS' RESELLERS', OR
DISTRIBUTORS' TOTAL LIABILITY TO YOU, WHETHER IN CONTRACT, TORT (INCLUDING
NEGLIGENCE), OR OTHERWISE, EXCEED THE PRICE PAID BY YOU FOR THE SOFTWARE.
THE FOREGOING LIMITATIONS SHALL APPLY EVEN IF THE ABOVE-STATED WARRANTY FAILS
OF ITS ESSENTIAL PURPOSE. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW
LIMITATION OR EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE
LIMITATION MAY NOT APPLY TO YOU.
Restricted Rights. The Software under this Agreement is commercial computer
software as that term is described in 48 C.F.R. 252.227-7014(a)(1). If
acquired by or on behalf of a civilian agency, the U.S. Government acquires
this commercial computer software and/or commercial computer software
documentation subject to the terms of this Agreement as specified in 48
C.F.R. 12.212 (Computer Software) and 12.211 (Technical Data) of the Federal
Acquisition Regulations ("FAR") and its successors. If acquired by or on
behalf of any agency within the Department of Defense ("DOD"), the U.S.
Government acquires this commercial computer software and/or commercial
computer software documentation subject to the terms of this Agreement as
specified in 48 C.F.R. 227.7202-3 of the DOD FAR Supplement ("DFAR") and its
successors.
General. This Agreement will bind and inure to the benefit of each party's
successors and assigns, provided that you may not assign or transfer this
Agreement, in whole or in part, without Synaptics' written consent. This
Agreement shall be governed by and construed in accordance with the laws of
the State of California, United States of America, as if performed wholly
within the state and without giving effect to the principles of conflict of
law. The parties consent to personal and exclusive jurisdiction of and venue
in, the state and federal courts within Santa Clara County, California;
provided however, that nothing in this Agreement will limit Synaptics' right to
bring legal action in any venue in order to protect or enforce its
intellectual property rights. No failure of either party to exercise or
enforce any of its rights under this Agreement will act as a waiver of such
rights. If any portion hereof is found to be void or unenforceable, the
remaining provisions of this Agreement shall remain in full force and
effect. This Agreement is the complete and exclusive agreement between the
parties with respect to the subject matter hereof, superseding and replacing
any and all prior agreements, communications, and understandings (both
written and oral) regarding such subject matter. Any notice to Synaptics will
be deemed effective when actually received and must be sent to Synaptics
Incorporated, ATTN: General Counsel, 1109 McKay Dr.
San Jose, CA 95131, USA
+3
View File
@@ -317,6 +317,9 @@ namespace montauk::abi {
and returns 1 while more remains, 0 once done. CAP_STORAGE_ADMIN. */ 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 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;
+5
View File
@@ -232,6 +232,7 @@ extern "C" {
#define MTK_SYS_WIN_DRAG_FORMATS 206 #define MTK_SYS_WIN_DRAG_FORMATS 206
#define MTK_SYS_WIN_DRAG_GET_DATA 207 #define MTK_SYS_WIN_DRAG_GET_DATA 207
#define MTK_SYS_FS_GROW 208 #define MTK_SYS_FS_GROW 208
#define MTK_SYS_GETUNIXTIME 209
/* @SYSCALLS-END */ /* @SYSCALLS-END */
/* Syscall status codes (montauk/statuscodes.h for C++). */ /* Syscall status codes (montauk/statuscodes.h for C++). */
@@ -805,6 +806,10 @@ static inline void mtk_gettime(mtk_datetime *out) {
_mtk_syscall1(MTK_SYS_GETTIME, (long)out); _mtk_syscall1(MTK_SYS_GETTIME, (long)out);
} }
static inline int64_t mtk_get_unix_time(void) {
return (int64_t)_mtk_syscall0(MTK_SYS_GETUNIXTIME);
}
static inline int mtk_set_unix_time(int64_t unix_seconds) { static inline int mtk_set_unix_time(int64_t unix_seconds) {
return (int)_mtk_syscall1(MTK_SYS_SETUNIXTIME, (long)unix_seconds); return (int)_mtk_syscall1(MTK_SYS_SETUNIXTIME, (long)unix_seconds);
} }
+6 -41
View File
@@ -51,42 +51,6 @@ inline uint32_t read_be32(const uint8_t* p) {
((uint32_t)p[2] << 8) | (uint32_t)p[3]; ((uint32_t)p[2] << 8) | (uint32_t)p[3];
} }
inline void write_be64(uint8_t* p, uint64_t value) {
for (int i = 7; i >= 0; i--) {
p[i] = (uint8_t)value;
value >>= 8;
}
}
inline bool is_leap_year(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
inline int days_in_month(int month, int year) {
static const int days[] = {0, 31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31};
return month == 2 && is_leap_year(year) ? 29 : days[month];
}
inline int64_t current_unix_seconds() {
montauk::abi::DateTime now = {};
montauk::gettime(&now);
if (now.Year < 1970 || now.Month < 1 || now.Month > 12 ||
now.Day < 1 || now.Day > days_in_month(now.Month, now.Year))
return 0;
int64_t days = 0;
for (int year = 1970; year < (int)now.Year; year++)
days += is_leap_year(year) ? 366 : 365;
for (int month = 1; month < (int)now.Month; month++)
days += days_in_month(month, now.Year);
days += now.Day - 1;
int64_t local = days * 86400 + (int64_t)now.Hour * 3600 +
(int64_t)now.Minute * 60 + now.Second;
return local - (int64_t)montauk::gettz() * 60;
}
inline bool same_bytes(const uint8_t* a, const uint8_t* b, int count) { inline bool same_bytes(const uint8_t* a, const uint8_t* b, int count) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
if (a[i] != b[i]) return false; if (a[i] != b[i]) return false;
@@ -115,14 +79,15 @@ inline int synchronize(const char* server, uint32_t timeout_ms = 5000,
request[2] = 6; // poll exponent: 2^6 = 64 seconds request[2] = 6; // poll exponent: 2^6 = 64 seconds
request[3] = 0xEC; // precision: approximately 2^-20 seconds request[3] = 0xEC; // precision: approximately 2^-20 seconds
int64_t unix_now = current_unix_seconds(); // The transmit timestamp only has to come back in the reply's origin
if (unix_now <= 0) { // field, so send a random cookie rather
// than the local clock, which may still be invalid before the first sync.
uint8_t cookie[8];
if (montauk::getrandom(cookie, sizeof(cookie)) != (int64_t)sizeof(cookie)) {
montauk::closesocket(fd); montauk::closesocket(fd);
return CLOCK_ERROR; return CLOCK_ERROR;
} }
uint64_t transmit_timestamp = for (int i = 0; i < 8; i++) request[40 + i] = cookie[i];
((uint64_t)unix_now + UNIX_EPOCH_DELTA) << 32;
write_be64(request + 40, transmit_timestamp);
progress(progress_callback, "Sending NTP request..."); progress(progress_callback, "Sending NTP request...");
if (montauk::sendto(fd, request, sizeof(request), server_ip, SERVER_PORT) < 0) { if (montauk::sendto(fd, request, sizeof(request), server_ip, SERVER_PORT) < 0) {
+2
View File
@@ -505,6 +505,8 @@ namespace montauk {
// Timekeeping (wall-clock) // Timekeeping (wall-clock)
inline void gettime(montauk::abi::DateTime* out) { syscall1(montauk::abi::SYS_GETTIME, (uint64_t)out); } inline void gettime(montauk::abi::DateTime* out) { syscall1(montauk::abi::SYS_GETTIME, (uint64_t)out); }
// UTC seconds since 1970 (gettime() is local time).
inline int64_t get_unix_time() { return syscall0(montauk::abi::SYS_GETUNIXTIME); }
inline int set_unix_time(int64_t unix_seconds) { inline int set_unix_time(int64_t unix_seconds) {
return (int)syscall1(montauk::abi::SYS_SETUNIXTIME, (uint64_t)unix_seconds); return (int)syscall1(montauk::abi::SYS_SETUNIXTIME, (uint64_t)unix_seconds);
} }
Binary file not shown.
Binary file not shown.
+6 -11
View File
@@ -196,17 +196,12 @@ void free_trust_anchors(TrustAnchors* tas) {
} }
void get_bearssl_time(uint32_t* days, uint32_t* seconds) { void get_bearssl_time(uint32_t* days, uint32_t* seconds) {
montauk::abi::DateTime dt; // Certificate validity is UTC; BearSSL counts days from 0000-01-01,
montauk::gettime(&dt); // and 1970-01-01 is day 719528.
int y = dt.Year, m = dt.Month, d = dt.Day; int64_t t = montauk::get_unix_time();
uint32_t total = 365u * (uint32_t)y if (t < 0) t = 0;
+ (uint32_t)((y+3)/4) - (uint32_t)((y+99)/100) + (uint32_t)((y+399)/400); *days = 719528u + (uint32_t)(t / 86400);
const int md[] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; *seconds = (uint32_t)(t % 86400);
for (int mo = 1; mo < m && mo <= 12; mo++) total += md[mo];
if (y%4==0 && (y%100!=0 || y%400==0) && m > 2) total++;
total += d - 1;
*days = total;
*seconds = (uint32_t)(dt.Hour*3600 + dt.Minute*60 + dt.Second);
} }
int tls_send_all(int fd, const unsigned char* data, size_t len) { int tls_send_all(int fd, const unsigned char* data, size_t len) {
+12 -6
View File
@@ -250,16 +250,22 @@ extern "C" void _start() {
montauk::service_log("MontaukOS DHCP Client"); montauk::service_log("MontaukOS DHCP Client");
// 0. Check if any device with an up link even exists. // 0. Check if any device with an up link even exists. A wired PHY
// still autonegotiating at boot (a few seconds after its reset) gets
// a grace period; with no wired interface at all there is nothing to
// wait for.
montauk::abi::NetIfInfo ifs[4]; montauk::abi::NetIfInfo ifs[4];
int n = montauk::net_interfaces(ifs, 4);
bool link = false; bool link = false;
for (int i = 0; i < n; i++) { for (int attempt = 0; attempt < 40 && !link; attempt++) {
if (ifs[i].linkUp == true) { int n = montauk::net_interfaces(ifs, 4);
link = true; bool wired = false;
for (int i = 0; i < n; i++) {
if (ifs[i].linkUp) link = true;
if (ifs[i].kind == montauk::abi::NETIF_KIND_ETHERNET) wired = true;
} }
if (link || !wired) break;
montauk::sleep_ms(250);
} }
if (link == false) { if (link == false) {
+5
View File
@@ -43,6 +43,11 @@ static void open_filemanager_internal(DesktopState* ds, const char* initial_path
prefs_load(ds); prefs_load(ds);
fm->show_hidden = prefs_show_hidden(); fm->show_hidden = prefs_show_hidden();
fm->sidebar_width = prefs_sidebar_width(); fm->sidebar_width = prefs_sidebar_width();
fm->col_resizing = -1;
for (int col = 0; col < FM_COL_COUNT; col++) {
int w = prefs_column_width(col);
fm->list_col_w[col] = w > 0 ? w : FM_COL_DEFAULT_W[col];
}
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100); fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
+56 -5
View File
@@ -55,6 +55,53 @@ static bool filemanager_click_entry(FileManagerState* fm, int idx) {
return false; return false;
} }
// List view column divider under (x, y) in the header row, -1 if none.
static int filemanager_column_divider_at(FileManagerState* fm, const FileManagerLayout& l,
int x, int y) {
if (fm->grid_view || y < l.body_y || y >= l.list_y) return -1;
FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w);
for (int col = 0; col < FM_COL_COUNT; col++) {
int cx = cols.x(col);
if (cx >= 0 && x >= cx - 4 - FM_COL_GRIP && x <= cx - 4 + FM_COL_GRIP) return col;
}
return -1;
}
// Dragging a column divider in the list view header. The divider is the
// column's left edge: the column grows leftwards and Name gives up the room.
// Returns true if the event was consumed; sets *grip over a divider.
static bool filemanager_column_resize_mouse(FileManagerState* fm, const FileManagerLayout& l,
int x, int y, MouseEvent& ev, bool* grip) {
if (fm->col_resizing >= 0) {
int col = fm->col_resizing;
if (ev.left_held()) {
int w = fm->col_drag_right - (x - fm->col_drag_dx);
fm->list_col_w[col] = gui_clamp(w, FM_COL_MIN_W, fm->col_drag_max);
} else {
fm->col_resizing = -1;
prefs_set_column_widths(fm->desktop, fm->list_col_w);
}
return true;
}
int col = filemanager_column_divider_at(fm, l, x, y);
if (col < 0) return false;
*grip = true;
if (!ev.left_pressed()) return false;
FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w);
int divider = cols.x(col) - 4;
fm->col_resizing = col;
fm->col_drag_dx = x - divider;
fm->col_drag_right = divider + cols.w[col];
// Name may shrink to its minimum, never further: past that a column
// would be dropped mid-drag.
int slack = cols.name_right + 8 - (l.view_x + FM_LIST_MIN_NAME_W);
fm->col_drag_max = gui_max(gui_min(cols.w[col] + slack, FM_COL_MAX_W), cols.w[col]);
if (fm->pathbar_editing) filemanager_cancel_pathbar(fm);
return true;
}
void filemanager_on_mouse(Window* win, MouseEvent& ev) { void filemanager_on_mouse(Window* win, MouseEvent& ev) {
FileManagerState* fm = (FileManagerState*)win->app_data; FileManagerState* fm = (FileManagerState*)win->app_data;
if (!fm) return; if (!fm) return;
@@ -203,10 +250,14 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
FileManagerLayout l = filemanager_layout(fm, fm->grid_view, cw); FileManagerLayout l = filemanager_layout(fm, fm->grid_view, cw);
// ---- Sidebar edge: drag to resize ---- // ---- Sidebar edge and list column dividers: drag to resize ----
if (!fm->marquee_active && fm->press_idx < 0 && !fm->scrollbar.dragging && if (!fm->marquee_active && fm->press_idx < 0 && !fm->scrollbar.dragging) {
filemanager_sidebar_resize_mouse(win, fm, l, local_x, local_y, ev)) bool grip = false;
return; if (filemanager_sidebar_resize_mouse(win, fm, l, local_x, local_y, ev, &grip) ||
filemanager_column_resize_mouse(fm, l, local_x, local_y, ev, &grip))
return;
filemanager_set_cursor(win, fm, grip ? CURSOR_RESIZE_H : CURSOR_ARROW);
}
// ---- Cancel rename on click outside rename area ---- // ---- Cancel rename on click outside rename area ----
if (fm->rename_active && ev.left_pressed()) { if (fm->rename_active && ev.left_pressed()) {
@@ -277,7 +328,7 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
// Column header click (list view): sort by that column, again = reverse. // Column header click (list view): sort by that column, again = reverse.
if (!fm->grid_view && local_y >= l.body_y && local_y < l.list_y && if (!fm->grid_view && local_y >= l.body_y && local_y < l.list_y &&
local_x >= l.view_x && filemanager_is_listing(fm)) { local_x >= l.view_x && filemanager_is_listing(fm)) {
FileManagerListColumns cols = filemanager_list_columns(l.view_x, l.view_w); FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w);
if (cols.type_x >= 0 && local_x >= cols.type_x) if (cols.type_x >= 0 && local_x >= cols.type_x)
filemanager_set_sort(fm, FM_SORT_TYPE); filemanager_set_sort(fm, FM_SORT_TYPE);
else if (cols.size_x >= 0 && local_x >= cols.size_x) else if (cols.size_x >= 0 && local_x >= cols.size_x)
+51 -16
View File
@@ -205,6 +205,14 @@ struct FileManagerState {
int sidebar_width; // wanted width (prefs); the layout clamps it to the window int sidebar_width; // wanted width (prefs); the layout clamps it to the window
bool sidebar_resizing; // dragging the sidebar's right edge bool sidebar_resizing; // dragging the sidebar's right edge
int sidebar_drag_dx; // pointer offset from the edge when the drag began int sidebar_drag_dx; // pointer offset from the edge when the drag began
// List view column widths (FileManagerListColumn order; prefs) and the
// divider being dragged, -1 = none. A drag keeps the column's right edge
// fixed, so the name column absorbs the change.
int list_col_w[3];
int col_resizing;
int col_drag_dx; // pointer offset from the divider when the drag began
int col_drag_right; // the column's right edge during the drag
int col_drag_max; // widest it may get without dropping a column
int cursor_style; // CursorStyle last sent to the Window Server int cursor_style; // CursorStyle last sent to the Window Server
// Undo / redo of the last file operations (undo.cpp). // Undo / redo of the last file operations (undo.cpp).
@@ -284,34 +292,56 @@ inline FileManagerLayout filemanager_layout(const FileManagerState* fm, bool gri
return l; return l;
} }
// List view columns right of Name, in screen order. Name takes the rest.
enum FileManagerListColumn : int {
FM_COL_MODIFIED = 0,
FM_COL_SIZE,
FM_COL_TYPE,
FM_COL_COUNT,
};
inline constexpr int FM_COL_DEFAULT_W[FM_COL_COUNT] = {132, 76, 96};
inline constexpr int FM_COL_MIN_W = 48;
inline constexpr int FM_COL_MAX_W = 400;
inline constexpr int FM_LIST_MIN_NAME_W = 160;
inline constexpr int FM_COL_GRIP = 3; // drag zone either side of a divider
// List view column layout. Columns are dropped right to left as the window // List view column layout. Columns are dropped right to left as the window
// narrows (Modified first, then Type, then Size) so the name keeps room. // narrows (Modified first, then Type, then Size) so the name keeps room.
// A column's text starts at its x; its divider line sits at x - 4.
struct FileManagerListColumns { struct FileManagerListColumns {
int name_x; int name_x;
int name_right; // right edge available to the name text int name_right; // right edge available to the name text
int modified_x; // -1 = hidden int modified_x; // -1 = hidden
int size_x; // -1 = hidden int size_x; // -1 = hidden
int type_x; // -1 = hidden int type_x; // -1 = hidden
int w[FM_COL_COUNT];
int x(int col) const {
return col == FM_COL_MODIFIED ? modified_x : col == FM_COL_SIZE ? size_x : type_x;
}
}; };
// Columns of a list view occupying [x0, x0 + view_w). // Columns of a list view occupying [x0, x0 + view_w).
inline FileManagerListColumns filemanager_list_columns(int x0, int view_w) { inline FileManagerListColumns filemanager_list_columns(const FileManagerState* fm,
static constexpr int MIN_NAME_W = 160; int x0, int view_w) {
static constexpr int TYPE_W = 96; FileManagerListColumns cols = {x0 + 8, 0, -1, -1, -1, {}};
static constexpr int SIZE_W = 76; for (int i = 0; i < FM_COL_COUNT; i++) cols.w[i] = fm->list_col_w[i];
static constexpr int MODIFIED_W = 132; const int* w = cols.w;
FileManagerListColumns cols = {x0 + 8, 0, -1, -1, -1}; int min_right = x0 + FM_LIST_MIN_NAME_W;
int min_right = x0 + MIN_NAME_W;
int right = x0 + view_w; int right = x0 + view_w;
if (right - SIZE_W >= min_right) { right -= SIZE_W; cols.size_x = right; } if (right - w[FM_COL_SIZE] >= min_right) { right -= w[FM_COL_SIZE]; cols.size_x = right; }
if (right - TYPE_W >= min_right) { if (right - w[FM_COL_TYPE] >= min_right) {
// Type sits right of Size; shift Size left to make room. // Type sits right of Size; shift Size left to make room.
cols.type_x = x0 + view_w - TYPE_W; cols.type_x = x0 + view_w - w[FM_COL_TYPE];
if (cols.size_x >= 0) cols.size_x -= TYPE_W; if (cols.size_x >= 0) cols.size_x -= w[FM_COL_TYPE];
right -= TYPE_W; right -= w[FM_COL_TYPE];
}
if (right - w[FM_COL_MODIFIED] >= min_right) {
right -= w[FM_COL_MODIFIED];
cols.modified_x = right;
} }
if (right - MODIFIED_W >= min_right) { right -= MODIFIED_W; cols.modified_x = right; }
cols.name_right = right - 8; cols.name_right = right - 8;
return cols; return cols;
} }
@@ -547,6 +577,8 @@ bool prefs_show_hidden();
void prefs_set_show_hidden(DesktopState* ds, bool show); void prefs_set_show_hidden(DesktopState* ds, bool show);
int prefs_sidebar_width(); // 0 = default int prefs_sidebar_width(); // 0 = default
void prefs_set_sidebar_width(DesktopState* ds, int width); void prefs_set_sidebar_width(DesktopState* ds, int width);
int prefs_column_width(int col); // FileManagerListColumn, 0 = default
void prefs_set_column_widths(DesktopState* ds, const int* widths);
int prefs_bookmark_count(); int prefs_bookmark_count();
const char* prefs_bookmark(int i); const char* prefs_bookmark(int i);
bool prefs_has_bookmark(const char* path); bool prefs_has_bookmark(const char* path);
@@ -564,9 +596,12 @@ void filemanager_sidebar_draw(Canvas& c, FileManagerState* fm, const FileManager
bool filemanager_sidebar_mouse(FileManagerState* fm, const FileManagerLayout& l, bool filemanager_sidebar_mouse(FileManagerState* fm, const FileManagerLayout& l,
int x, int y, MouseEvent& ev, int ch); int x, int y, MouseEvent& ev, int ch);
// Dragging the sidebar's right edge to resize it. Returns true if the event // Dragging the sidebar's right edge to resize it. Returns true if the event
// was consumed (a drag is in progress or just began). // was consumed (a drag is in progress or just began); sets *grip when the
bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm, // pointer is over the edge.
const FileManagerLayout& l, int x, int y, MouseEvent& ev); bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm, const FileManagerLayout& l,
int x, int y, MouseEvent& ev, bool* grip);
// The Window Server cursor for this window (CursorStyle), sent on change only.
void filemanager_set_cursor(Window* win, FileManagerState* fm, int style);
int filemanager_sidebar_item_at(FileManagerState* fm, const FileManagerLayout& l, int x, int y); int filemanager_sidebar_item_at(FileManagerState* fm, const FileManagerLayout& l, int x, int y);
void filemanager_sidebar_open(FileManagerState* fm, int idx); void filemanager_sidebar_open(FileManagerState* fm, int idx);
// Folder a sidebar row stands for, for drops ("" for Computer/headers). // Folder a sidebar row stands for, for drops ("" for Computer/headers).
+30
View File
@@ -13,6 +13,8 @@ namespace filemanager {
// Layout of files.toml: // Layout of files.toml:
// [view] // [view]
// show_hidden = false // show_hidden = false
// [list]
// modified_width = 132 (also size_width, type_width; absent = default)
// [sidebar] // [sidebar]
// width = 160 // width = 160
// bookmarks = ["0:/users/me/Projects", ...] // bookmarks = ["0:/users/me/Projects", ...]
@@ -24,6 +26,10 @@ namespace filemanager {
static constexpr const char* PREFS_NAME = "files"; static constexpr const char* PREFS_NAME = "files";
static constexpr int MAX_BOOKMARKS = 32; static constexpr int MAX_BOOKMARKS = 32;
static constexpr int MAX_FOLDER_VIEWS = 128; static constexpr int MAX_FOLDER_VIEWS = 128;
// Keys of the list view column widths, FileManagerListColumn order.
static constexpr const char* COLUMN_WIDTH_KEYS[FM_COL_COUNT] = {
"list.modified_width", "list.size_width", "list.type_width",
};
struct FolderView { struct FolderView {
char path[256]; char path[256];
@@ -35,6 +41,7 @@ struct FolderView {
struct Prefs { struct Prefs {
bool show_hidden; bool show_hidden;
int sidebar_width; // 0 = default int sidebar_width; // 0 = default
int column_widths[FM_COL_COUNT]; // 0 = default
char bookmarks[MAX_BOOKMARKS][256]; char bookmarks[MAX_BOOKMARKS][256];
int bookmark_count; int bookmark_count;
FolderView views[MAX_FOLDER_VIEWS]; FolderView views[MAX_FOLDER_VIEWS];
@@ -99,6 +106,10 @@ static void read_from_disk(DesktopState* ds) {
g_prefs.show_hidden = doc.get_bool("view.show_hidden", false); g_prefs.show_hidden = doc.get_bool("view.show_hidden", false);
int64_t width = doc.get_int("sidebar.width", 0); int64_t width = doc.get_int("sidebar.width", 0);
g_prefs.sidebar_width = width > 0 && width <= FM_SIDEBAR_MAX_W ? (int)width : 0; g_prefs.sidebar_width = width > 0 && width <= FM_SIDEBAR_MAX_W ? (int)width : 0;
for (int col = 0; col < FM_COL_COUNT; col++) {
int64_t cw = doc.get_int(COLUMN_WIDTH_KEYS[col], 0);
g_prefs.column_widths[col] = cw >= FM_COL_MIN_W && cw <= FM_COL_MAX_W ? (int)cw : 0;
}
if (auto* arr = doc.get_array("sidebar.bookmarks")) { if (auto* arr = doc.get_array("sidebar.bookmarks")) {
for (int i = 0; i < arr->array.count && g_prefs.bookmark_count < MAX_BOOKMARKS; i++) { for (int i = 0; i < arr->array.count && g_prefs.bookmark_count < MAX_BOOKMARKS; i++) {
@@ -133,6 +144,10 @@ static void write_to_disk(DesktopState* ds) {
montauk::config::set_bool(&doc, "view.show_hidden", g_prefs.show_hidden); montauk::config::set_bool(&doc, "view.show_hidden", g_prefs.show_hidden);
if (g_prefs.sidebar_width > 0) if (g_prefs.sidebar_width > 0)
montauk::config::set_int(&doc, "sidebar.width", g_prefs.sidebar_width); montauk::config::set_int(&doc, "sidebar.width", g_prefs.sidebar_width);
for (int col = 0; col < FM_COL_COUNT; col++) {
if (g_prefs.column_widths[col] > 0)
montauk::config::set_int(&doc, COLUMN_WIDTH_KEYS[col], g_prefs.column_widths[col]);
}
auto* bookmarks = string_array("sidebar.bookmarks"); auto* bookmarks = string_array("sidebar.bookmarks");
for (int i = 0; i < g_prefs.bookmark_count; i++) push_string(bookmarks, g_prefs.bookmarks[i]); for (int i = 0; i < g_prefs.bookmark_count; i++) push_string(bookmarks, g_prefs.bookmarks[i]);
@@ -177,6 +192,21 @@ void prefs_set_sidebar_width(DesktopState* ds, int width) {
write_to_disk(ds); write_to_disk(ds);
} }
int prefs_column_width(int col) {
return col >= 0 && col < FM_COL_COUNT ? g_prefs.column_widths[col] : 0;
}
void prefs_set_column_widths(DesktopState* ds, const int* widths) {
read_from_disk(ds);
bool changed = false;
for (int col = 0; col < FM_COL_COUNT; col++) {
int w = widths[col] == FM_COL_DEFAULT_W[col] ? 0 : widths[col];
if (g_prefs.column_widths[col] != w) changed = true;
g_prefs.column_widths[col] = w;
}
if (changed) write_to_disk(ds);
}
int prefs_bookmark_count() { int prefs_bookmark_count() {
return g_prefs.bookmark_count; return g_prefs.bookmark_count;
} }
+41 -37
View File
@@ -135,13 +135,16 @@ static void draw_marquee(Canvas& c, FileManagerState* fm, int list_y) {
} }
} }
// Column title, plus a small arrow when it is the active sort column. // Column title, plus a small arrow when it is the active sort column and
static void draw_column_header(Canvas& c, int x, int header_y, const char* label, // there is room for it. max_w is the width the title may take.
static void draw_column_header(Canvas& c, int x, int header_y, const char* label, int max_w,
bool active, bool desc, Color color) { bool active, bool desc, Color color) {
c.text(x, header_y + 2, label, active ? colors::TEXT_COLOR : color); char fit[32];
if (!active) return; fit_label_with_ellipsis(fit, sizeof(fit), label, max_w);
c.text(x, header_y + 2, fit, active ? colors::TEXT_COLOR : color);
int ax = x + text_width(fit) + 6;
if (!active || ax + 7 > x + max_w) return;
int ax = x + text_width(label) + 6;
int ay = header_y + FM_HEADER_H / 2 - 2; int ay = header_y + FM_HEADER_H / 2 - 2;
for (int r = 0; r < 4; r++) { for (int r = 0; r < 4; r++) {
// Ascending points up, descending points down. // Ascending points up, descending points down.
@@ -337,7 +340,7 @@ bool filemanager_rename_box(FileManagerState* fm, const FileManagerLayout& l, in
r = {x, cell_y + FM_GRID_PAD + FM_GRID_ICON, w, fh + 6}; r = {x, cell_y + FM_GRID_PAD + FM_GRID_ICON, w, fh + 6};
} else { } else {
// Over the name column; text lines up with the names (view_x + 30). // Over the name column; text lines up with the names (view_x + 30).
FileManagerListColumns cols = filemanager_list_columns(l.view_x, l.view_w); FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w);
int iy = l.list_y + i * FM_ITEM_H - fm->scrollbar.scroll_offset; int iy = l.list_y + i * FM_ITEM_H - fm->scrollbar.scroll_offset;
int x = l.view_x + 30 - fm->rename.pad; int x = l.view_x + 30 - fm->rename.pad;
r = {x, iy + 2, gui_max(cols.name_right - x, 60), FM_ITEM_H - 4}; r = {x, iy + 2, gui_max(cols.name_right - x, 60), FM_ITEM_H - 4};
@@ -480,31 +483,42 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
int header_y = l.body_y; int header_y = l.body_y;
c.fill_rect(l.view_x, header_y, c.w - l.view_x, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8)); c.fill_rect(l.view_x, header_y, c.w - l.view_x, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
FileManagerListColumns cols = filemanager_list_columns(l.view_x, l.view_w); FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w);
// Text room of each column: up to the next divider, less a gap.
// Type runs to the view's right edge.
int col_text_w[FM_COL_COUNT];
for (int col = 0; col < FM_COL_COUNT; col++) col_text_w[col] = cols.w[col] - 8;
col_text_w[FM_COL_TYPE] = l.view_x + l.view_w - cols.type_x - 4;
// Sorting only applies to real directories; Computer and the virtual // Sorting only applies to real directories; Computer and the virtual
// views keep their curated order and show no sort arrow. // views keep their curated order and show no sort arrow.
bool sortable = !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm); bool sortable = !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
draw_column_header(c, cols.name_x, header_y, "Name", draw_column_header(c, cols.name_x, header_y, "Name", cols.name_right - cols.name_x,
sortable && fm->sort_key == FM_SORT_NAME, fm->sort_desc, dim); sortable && fm->sort_key == FM_SORT_NAME, fm->sort_desc, dim);
if (cols.modified_x >= 0) if (cols.modified_x >= 0)
draw_column_header(c, cols.modified_x, header_y, "Modified", draw_column_header(c, cols.modified_x, header_y, "Modified", col_text_w[FM_COL_MODIFIED],
sortable && fm->sort_key == FM_SORT_MODIFIED, fm->sort_desc, dim); sortable && fm->sort_key == FM_SORT_MODIFIED, fm->sort_desc, dim);
if (cols.size_x >= 0) if (cols.size_x >= 0)
draw_column_header(c, cols.size_x, header_y, "Size", draw_column_header(c, cols.size_x, header_y, "Size", col_text_w[FM_COL_SIZE],
sortable && fm->sort_key == FM_SORT_SIZE, fm->sort_desc, dim); sortable && fm->sort_key == FM_SORT_SIZE, fm->sort_desc, dim);
if (cols.type_x >= 0) if (cols.type_x >= 0)
draw_column_header(c, cols.type_x, header_y, "Type", draw_column_header(c, cols.type_x, header_y, "Type", col_text_w[FM_COL_TYPE],
sortable && fm->sort_key == FM_SORT_TYPE, fm->sort_desc, dim); sortable && fm->sort_key == FM_SORT_TYPE, fm->sort_desc, dim);
// Header separator // Header separator
c.hline(l.view_x, header_y + FM_HEADER_H - 1, c.w - l.view_x, colors::BORDER); c.hline(l.view_x, header_y + FM_HEADER_H - 1, c.w - l.view_x, colors::BORDER);
// Column separator lines // Column separator lines: the one after Name runs down the list,
// the rest mark the draggable dividers in the header.
int first_col_x = cols.modified_x >= 0 ? cols.modified_x int first_col_x = cols.modified_x >= 0 ? cols.modified_x
: cols.size_x >= 0 ? cols.size_x : cols.type_x; : cols.size_x >= 0 ? cols.size_x : cols.type_x;
if (first_col_x >= 0) if (first_col_x >= 0)
c.vline(first_col_x - 4, header_y, c.h - header_y, colors::BORDER); c.vline(first_col_x - 4, header_y, c.h - header_y, colors::BORDER);
for (int col = 0; col < FM_COL_COUNT; col++) {
int cx = cols.x(col);
if (cx >= 0 && cx != first_col_x)
c.vline(cx - 4, header_y, FM_HEADER_H, colors::BORDER);
}
int tz = montauk::gettz(); int tz = montauk::gettz();
// ---- File entries ---- // ---- File entries ----
@@ -589,28 +603,12 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
// The entry being renamed shows its rename box (drawn later). // The entry being renamed shows its rename box (drawn later).
if (!(fm->rename_active && fm->rename_idx == i)) { if (!(fm->rename_active && fm->rename_idx == i)) {
if (ty >= list_y && ty + fm_sfh <= c.h) { if (ty >= list_y && ty + fm_sfh <= c.h) {
// Truncate name to fit the name column width // Truncate name to fit the name column width. The buffer
const char* name = fm->entry_names[i]; // matches entry_names so long names are only ever cut by
int name_max_w = cols.name_right - tx; // pixel width, never silently by byte count.
char label[64]; char label[sizeof(fm->entry_names[0])];
montauk::utf8::copy(label, 64, name); fit_label_with_ellipsis(label, sizeof(label), fm->entry_names[i],
cols.name_right - tx);
if (text_width(label) > name_max_w && montauk::slen(name) > 3) {
int ellipsis_w = text_width("...");
int final_len = montauk::slen(label);
while (final_len > 1) {
label[final_len] = '\0';
int w = text_width(label);
if (w + ellipsis_w <= name_max_w) break;
final_len = montauk::utf8::prev_boundary(label, final_len);
}
if (final_len > 60)
final_len = montauk::utf8::floor_boundary(label, final_len, 60);
label[final_len] = '.';
label[final_len + 1] = '.';
label[final_len + 2] = '.';
label[final_len + 3] = '\0';
}
c.text(tx, ty, label, colors::TEXT_COLOR); c.text(tx, ty, label, colors::TEXT_COLOR);
} }
} }
@@ -621,14 +619,20 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
if (cols.modified_x >= 0 && row_text_visible && fm->entry_mtimes[i] > 0) { if (cols.modified_x >= 0 && row_text_visible && fm->entry_mtimes[i] > 0) {
char date_str[24]; char date_str[24];
format_datetime(date_str, sizeof(date_str), fm->entry_mtimes[i], tz); format_datetime(date_str, sizeof(date_str), fm->entry_mtimes[i], tz);
c.text(cols.modified_x, ty, date_str, dim); char date_fit[24];
fit_label_with_ellipsis(date_fit, sizeof(date_fit), date_str,
col_text_w[FM_COL_MODIFIED]);
c.text(cols.modified_x, ty, date_fit, dim);
} }
// Size // Size
if (cols.size_x >= 0 && !fm->is_dir[i] && row_text_visible) { if (cols.size_x >= 0 && !fm->is_dir[i] && row_text_visible) {
char size_str[16]; char size_str[16];
format_size(size_str, fm->entry_sizes[i]); format_size(size_str, fm->entry_sizes[i]);
c.text(cols.size_x, ty, size_str, dim); char size_fit[16];
fit_label_with_ellipsis(size_fit, sizeof(size_fit), size_str,
col_text_w[FM_COL_SIZE]);
c.text(cols.size_x, ty, size_fit, dim);
} }
// Type // Type
@@ -637,7 +641,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
filemanager_type_label(fm, i, type_str, sizeof(type_str)); filemanager_type_label(fm, i, type_str, sizeof(type_str));
char type_fit[24]; char type_fit[24];
fit_label_with_ellipsis(type_fit, sizeof(type_fit), type_str, fit_label_with_ellipsis(type_fit, sizeof(type_fit), type_str,
l.view_x + l.view_w - cols.type_x - 4); col_text_w[FM_COL_TYPE]);
c.text(cols.type_x, ty, type_fit, dim); c.text(cols.type_x, ty, type_fit, dim);
} }
} }
+4 -4
View File
@@ -226,14 +226,14 @@ bool filemanager_sidebar_target_path(FileManagerState* fm, int idx, char* out, i
return true; return true;
} }
static void set_window_cursor(Window* win, FileManagerState* fm, int style) { void filemanager_set_cursor(Window* win, FileManagerState* fm, int style) {
if (fm->cursor_style == style) return; if (fm->cursor_style == style) return;
fm->cursor_style = style; fm->cursor_style = style;
montauk::win_setcursor(win->ext_win_id, style); montauk::win_setcursor(win->ext_win_id, style);
} }
bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm, bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm, const FileManagerLayout& l,
const FileManagerLayout& l, int x, int y, MouseEvent& ev) { int x, int y, MouseEvent& ev, bool* grip) {
if (fm->sidebar_resizing) { if (fm->sidebar_resizing) {
if (ev.left_held()) { if (ev.left_held()) {
fm->sidebar_width = filemanager_sidebar_width(x - fm->sidebar_drag_dx, fm->sidebar_width = filemanager_sidebar_width(x - fm->sidebar_drag_dx,
@@ -248,7 +248,7 @@ bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm,
// The grip straddles the border line at sidebar_w - 1. // The grip straddles the border line at sidebar_w - 1.
bool on_edge = l.sidebar_w > 0 && y >= l.body_y && bool on_edge = l.sidebar_w > 0 && y >= l.body_y &&
x >= l.sidebar_w - FM_SIDEBAR_GRIP && x < l.sidebar_w + FM_SIDEBAR_GRIP; x >= l.sidebar_w - FM_SIDEBAR_GRIP && x < l.sidebar_w + FM_SIDEBAR_GRIP;
set_window_cursor(win, fm, on_edge ? CURSOR_RESIZE_H : CURSOR_ARROW); if (on_edge) *grip = true;
if (!on_edge || !ev.left_pressed()) return false; if (!on_edge || !ev.left_pressed()) return false;
fm->sidebar_resizing = true; fm->sidebar_resizing = true;
+14 -3
View File
@@ -33,6 +33,12 @@ static bool load_settings(char* server, int server_cap) {
return enabled; return enabled;
} }
static bool reached_server(int result) {
return result == montauk::ntp::TIMEOUT ||
result == montauk::ntp::INVALID_REPLY ||
result == montauk::ntp::SERVER_REFUSED;
}
extern "C" void _start() { extern "C" void _start() {
log("service started"); log("service started");
@@ -68,16 +74,21 @@ extern "C" void _start() {
montauk::abi::NetCfg net = {}; montauk::abi::NetCfg net = {};
montauk::get_netcfg(&net); montauk::get_netcfg(&net);
// No lease yet (DHCP may still be waiting for link): keep polling
// instead of backing off, so the first sync lands right after DHCP.
if (net.ipAddress == 0 || net.dnsServer == 0) { if (net.ipAddress == 0 || net.dnsServer == 0) {
next_attempt = now + RETRY_INTERVAL_MS;
montauk::sleep_ms(CONFIG_POLL_MS); montauk::sleep_ms(CONFIG_POLL_MS);
continue; continue;
} }
int result = montauk::ntp::synchronize(server); int result = montauk::ntp::synchronize(server);
log(montauk::ntp::result_string(result)); log(montauk::ntp::result_string(result));
next_attempt = montauk::get_milliseconds() + // The server rate limit only applies once a request reached it;
(result == montauk::ntp::OK ? SYNC_INTERVAL_MS : RETRY_INTERVAL_MS); // failures before sending (DNS, socket) retry at the poll interval.
uint64_t delay = SYNC_INTERVAL_MS;
if (result != montauk::ntp::OK)
delay = reached_server(result) ? RETRY_INTERVAL_MS : CONFIG_POLL_MS;
next_attempt = montauk::get_milliseconds() + delay;
montauk::sleep_ms(CONFIG_POLL_MS); montauk::sleep_ms(CONFIG_POLL_MS);
} }
} }
+48 -7
View File
@@ -103,6 +103,9 @@ static mtk::TextInputState g_search = {};
static bool g_search_focus = false; static bool g_search_focus = false;
static bool g_grid_focus = false; static bool g_grid_focus = false;
static int g_scroll = 0; static int g_scroll = 0;
static bool g_sb_hover = false; // pointer over the scrollbar track
static bool g_sb_dragging = false; // dragging the scrollbar thumb
static int g_sb_drag_offset = 0; // grab point within the thumb
static int g_selected = -1; // index into g_placed (a tile), keyboard focus static int g_selected = -1; // index into g_placed (a tile), keyboard focus
static int g_hover_tile = -1; // index into g_placed static int g_hover_tile = -1; // index into g_placed
static int g_hover_cat = CAT_NONE; static int g_hover_cat = CAT_NONE;
@@ -458,6 +461,18 @@ static void relayout() {
if (g_hover_tile >= g_placed_count) g_hover_tile = -1; if (g_hover_tile >= g_placed_count) g_hover_tile = -1;
} }
// The scrollbar spans the whole pane right of the sidebar. Both rects are
// empty when the content fits.
static void scrollbar_rects(Rect& track, Rect& thumb) {
Rect body = body_rect();
track = {0, 0, 0, 0};
thumb = {0, 0, 0, 0};
if (!mtk::scrollbar_needed(g_content_h, body.h)) return;
Rect view = {SIDEBAR_W, 0, g_win.width - SIDEBAR_W, body.h};
track = mtk::scrollbar_track_rect(view);
thumb = mtk::scrollbar_thumb_rect(track, g_content_h, body.h, g_scroll);
}
static Rect placed_on_screen(int i) { static Rect placed_on_screen(int i) {
Rect r = g_placed[i].r; Rect r = g_placed[i].r;
r.y -= g_scroll; r.y -= g_scroll;
@@ -681,13 +696,9 @@ static void draw_body(Canvas& c, const mtk::Theme& theme) {
theme.text_muted); theme.text_muted);
} }
Rect body = body_rect(); Rect track, thumb;
if (mtk::scrollbar_needed(g_content_h, body.h)) { scrollbar_rects(track, thumb);
Rect view = {SIDEBAR_W, 0, g_win.width - SIDEBAR_W, body.h}; mtk::draw_scrollbar(c, track, thumb, g_sb_hover, g_sb_dragging, theme);
Rect track = mtk::scrollbar_track_rect(view);
Rect thumb = mtk::scrollbar_thumb_rect(track, g_content_h, body.h, g_scroll);
mtk::draw_scrollbar(c, track, thumb, false, false, theme);
}
} }
static void draw_header(Canvas& c, const mtk::Theme& theme) { static void draw_header(Canvas& c, const mtk::Theme& theme) {
@@ -883,6 +894,36 @@ static void handle_mouse(const montauk::abi::WinEvent& ev) {
relayout(); relayout();
} }
// Scrollbar: drag the thumb, or click the track to jump there and drag.
Rect track, thumb;
scrollbar_rects(track, thumb);
bool scrollable = !track.empty() && !thumb.empty();
if (g_sb_dragging && (!(buttons & 1) || !scrollable)) g_sb_dragging = false;
if (g_sb_dragging) {
g_scroll = mtk::scrollbar_offset_from_thumb_top(track, g_content_h, body_rect().h,
thumb.h, g_mouse_y - g_sb_drag_offset);
relayout();
g_hover_tile = -1;
return;
}
g_sb_hover = scrollable && track.contains(g_mouse_x, g_mouse_y);
if (g_sb_hover) {
g_hover_tile = -1;
g_hover_cat = CAT_NONE;
if (!pressed) return;
g_search_focus = false;
g_sb_dragging = true;
if (thumb.contains(g_mouse_x, g_mouse_y)) {
g_sb_drag_offset = g_mouse_y - thumb.y;
} else {
g_sb_drag_offset = thumb.h / 2;
g_scroll = mtk::scrollbar_offset_from_thumb_top(track, g_content_h, body_rect().h,
thumb.h, g_mouse_y - g_sb_drag_offset);
relayout();
}
return;
}
g_hover_tile = tile_at(g_mouse_x, g_mouse_y); g_hover_tile = tile_at(g_mouse_x, g_mouse_y);
g_hover_cat = g_mouse_x < SIDEBAR_W ? sidebar_category_at(g_mouse_x, g_mouse_y) : CAT_NONE; g_hover_cat = g_mouse_x < SIDEBAR_W ? sidebar_category_at(g_mouse_x, g_mouse_y) : CAT_NONE;
+2 -2
View File
@@ -146,8 +146,8 @@ static abi::WifiNetwork g_nets[64];
static bool require_adapter(abi::WifiInfo& info) { static bool require_adapter(abi::WifiInfo& info) {
if (wifi_info(&info) != 0 || !info.present) { if (wifi_info(&info) != 0 || !info.present) {
print("wifi: no supported Wi-Fi adapter found.\n"); print("wifi: no supported Wi-Fi adapter found.\n");
print(" Only Intel AX210-family devices with an RF type of GF " print(" Supported: Intel AX210-family devices with an RF type of GF "
"(AX211) are supported.\n"); "(AX211), and the Raspberry Pi 4/400's on-board radio.\n");
return false; return false;
} }
if (info.state != abi::WIFI_STATE_RUNNING) { if (info.state != abi::WIFI_STATE_RUNNING) {
Binary file not shown.
+29 -4
View File
@@ -33,6 +33,13 @@
# the image with e.g. # the image with e.g.
# sudo dd if=montauk-rpi4.img of=/dev/sdX bs=4M conv=fsync # sudo dd if=montauk-rpi4.img of=/dev/sdX bs=4M conv=fsync
# #
# Boot order: the firmware's RPI_EFI.fd ships with an empty variable store,
# which scripts/rpi4-efi-bootorder.py seeds with a "MontaukOS" Boot0000 first
# in BootOrder, naming the ESP by this image's (random) MBR disk ID and
# partition number. The card then boots without a trip to the boot menu,
# ahead of the PXE/HTTP entries the firmware adds on its own, from the SD
# slot or a USB drive alike.
#
# Firmware settings (UEFI setup, Esc at boot) MontaukOS relies on are the # Firmware settings (UEFI setup, Esc at boot) MontaukOS relies on are the
# defaults: System Table Mode "ACPI" and "Limit RAM to 3 GB" enabled (the # defaults: System Table Mode "ACPI" and "Limit RAM to 3 GB" enabled (the
# BCM2711's PCIe can only DMA below 3 GB, see the _DMA objects in its ACPI # BCM2711's PCIe can only DMA below 3 GB, see the _DMA objects in its ACPI
@@ -150,13 +157,19 @@ if [ "$ROOTFS" = ext2 ]; then
-d "$WORK/root" "$WORK/root.ext2" "${root_mib}M" -d "$WORK/root" "$WORK/root.ext2" "${root_mib}M"
fi fi
# The MBR disk ID the firmware's boot entry names the ESP by.
DISK_ID=0
while [ $((DISK_ID)) -eq 0 ]; do
DISK_ID="0x$(od -An -N4 -tx4 /dev/urandom | tr -d ' ')"
done
rm -f "$OUTPUT" rm -f "$OUTPUT"
dd if=/dev/zero of="$OUTPUT" bs=1M count=0 seek=$((1 + esp_mib + root_mib)) status=none dd if=/dev/zero of="$OUTPUT" bs=1M count=0 seek=$((1 + esp_mib + root_mib)) status=none
if [ "$ROOTFS" = ext2 ]; then if [ "$ROOTFS" = ext2 ]; then
printf 'label: dos\nstart=2048, size=%dMiB, type=ef\nstart=%dMiB, type=83\n' \ printf 'label: dos\nlabel-id: %s\nstart=2048, size=%dMiB, type=ef\nstart=%dMiB, type=83\n' \
"$esp_mib" "$((1 + esp_mib))" "$DISK_ID" "$esp_mib" "$((1 + esp_mib))"
else else
printf 'label: dos\nstart=2048, type=ef\n' printf 'label: dos\nlabel-id: %s\nstart=2048, type=ef\n' "$DISK_ID"
fi | PATH=$PATH:/usr/sbin:/sbin sfdisk --quiet "$OUTPUT" fi | PATH=$PATH:/usr/sbin:/sbin sfdisk --quiet "$OUTPUT"
# The ESP is built as its own file: mtools sizes a filesystem at an @@offset # The ESP is built as its own file: mtools sizes a filesystem at an @@offset
@@ -165,10 +178,22 @@ IMG="$WORK/esp.fat"
dd if=/dev/zero of="$IMG" bs=1M count=0 seek=$esp_mib status=none dd if=/dev/zero of="$IMG" bs=1M count=0 seek=$esp_mib status=none
mformat -i "$IMG" -F -v MONTAUK :: mformat -i "$IMG" -F -v MONTAUK ::
# Firmware: everything in the release except its readme. # Firmware: everything in the release except its readme. Its config.txt
# gets the VPU core clock pinned: the miniuart-bt overlay leaves Bluetooth on
# the mini UART, whose baud rate is derived from that clock, and the
# overlay's documentation requires core_freq and core_freq_min to match or
# the mini UART breaks when the firmware scales the clock (500 MHz is the
# Pi 4/400 default).
for f in "$FW_DIR"/*; do for f in "$FW_DIR"/*; do
case "$(basename "$f")" in case "$(basename "$f")" in
Readme.md) ;; Readme.md) ;;
RPI_EFI.fd)
python3 -I "$ROOT/scripts/rpi4-efi-bootorder.py" "$f" "$WORK/RPI_EFI.fd" \
"$DISK_ID" 1 2048 $((esp_mib * 2048))
mcopy -i "$IMG" "$WORK/RPI_EFI.fd" :: ;;
config.txt)
{ cat "$f"; printf '\ncore_freq=500\ncore_freq_min=500\n'; } > "$WORK/config.txt"
mcopy -i "$IMG" "$WORK/config.txt" :: ;;
*) mcopy -s -i "$IMG" "$f" :: ;; *) mcopy -s -i "$IMG" "$f" :: ;;
esac esac
done done
+1739
View File
File diff suppressed because it is too large. Load diff
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
# rpi4-efi-bootorder.py - make MontaukOS the Pi UEFI firmware's first boot option
#
# Usage: rpi4-efi-bootorder.py <RPI_EFI.fd> <output.fd> <disk-id> <partition> <start-lba> <sectors>
# (run by mk-rpi4-sd.sh)
#
# The firmware (github.com/pftf/RPi4, EDK2) keeps its UEFI variables in an
# EDK2 variable store inside RPI_EFI.fd, empty in a release. Left to itself it
# builds the boot order from whatever it enumerates (SD, USB, PXE and HTTP over
# IPv4 and IPv6, ...), appending entries it finds later, so the card can end up
# behind the network entries and booting it means picking it from the boot
# menu. This writes two variables into the empty store:
#
# Boot0000 "MontaukOS": HD(<partition>, MBR, <disk-id>)/\EFI\BOOT\BOOTAA64.EFI
# BootOrder { 0000 }
#
# The device path is the short form that starts at the partition: EDK2 finds
# it by the MBR disk ID and partition number on any block device (connecting
# everything the first time it looks, then caching the full path), so it
# boots the card whether it is in the SD slot or written to a USB drive. The
# firmware appends the entries it creates after it, and never deletes it (it
# only prunes the entries it created itself). The image's MBR must carry the
# same disk ID; montauk-imager.py's `update --firmware` copies it to the card.
import struct
import sys
import uuid
NV_FV_GUID = uuid.UUID("fff12b8d-7696-4c8b-a985-2747075b4f50").bytes_le # gEfiSystemNvDataFvGuid
AUTH_STORE_GUID = uuid.UUID("aaf32c78-947b-439a-a180-2e144ec37792").bytes_le # gEfiAuthenticatedVariableGuid
GLOBAL_GUID = uuid.UUID("8be4df61-93ca-11d2-aa0d-00e098032b8c").bytes_le # gEfiGlobalVariableGuid
STORE_FORMATTED = 0x5A
STORE_HEALTHY = 0xFE
VARIABLE_DATA = 0x55AA
VAR_ADDED = 0x3F
NV_BS_RT = 0x7 # NON_VOLATILE | BOOTSERVICE_ACCESS | RUNTIME_ACCESS
LOAD_OPTION_ACTIVE = 0x1
LOADER = "\\EFI\\BOOT\\BOOTAA64.EFI"
def fail(msg):
print(f"rpi4-efi-bootorder: {msg}", file=sys.stderr)
sys.exit(1)
def find_store(fd):
"""Offsets of the variable store header and of its end."""
for fv in range(0, len(fd) - 0x48, 0x1000):
if fd[fv + 0x10:fv + 0x20] == NV_FV_GUID and fd[fv + 0x28:fv + 0x2C] == b"_FVH":
store = fv + struct.unpack_from("<H", fd, fv + 0x30)[0]
guid, size, fmt, state = struct.unpack_from("<16sIBB", fd, store)
if guid != AUTH_STORE_GUID or fmt != STORE_FORMATTED or state != STORE_HEALTHY:
fail("the NV variable store is not an authenticated EDK2 store")
return store, store + size
fail("no NV variable store in the firmware image")
def variable(name, data):
# AUTHENTICATED_VARIABLE_HEADER (packed, 60 bytes), name, data; the next
# header starts 4-byte aligned, the gap left erased.
name = (name + "\0").encode("utf-16-le")
v = struct.pack("<HBBIQ16sIII16s", VARIABLE_DATA, VAR_ADDED, 0, NV_BS_RT, 0, bytes(16), 0,
len(name), len(data), GLOBAL_GUID) + name + data
return v + b"\xff" * (-len(v) % 4)
def load_option(desc, disk_id, part, start, sectors):
hd = struct.pack("<BBHIQQ16sBB", 4, 1, 42, part, start, sectors,
struct.pack("<I", disk_id).ljust(16, b"\0"), 1, 1) # MBR, MBR signature
path = (LOADER + "\0").encode("utf-16-le")
dp = hd + struct.pack("<BBH", 4, 4, 4 + len(path)) + path + struct.pack("<BBH", 0x7F, 0xFF, 4)
return struct.pack("<IH", LOAD_OPTION_ACTIVE, len(dp)) + (desc + "\0").encode("utf-16-le") + dp
def main():
if len(sys.argv) != 7:
fail("usage: rpi4-efi-bootorder.py <RPI_EFI.fd> <output.fd> <disk-id> <partition> <start-lba> <sectors>")
src, out = sys.argv[1], sys.argv[2]
disk_id, part, start, sectors = (int(a, 0) for a in sys.argv[3:])
if not 0 < disk_id <= 0xFFFFFFFF:
fail(f"bad MBR disk ID {sys.argv[3]}")
fd = bytearray(open(src, "rb").read())
store, end = find_store(fd)
first = (store + 28 + 3) & ~3
if struct.unpack_from("<H", fd, first)[0] != 0xFFFF:
fail("the firmware's variable store is not empty; seed only a release RPI_EFI.fd")
blob = (variable("Boot0000", load_option("MontaukOS", disk_id, part, start, sectors))
+ variable("BootOrder", struct.pack("<H", 0)))
if first + len(blob) > end:
fail("the variable store is too small")
fd[first:first + len(blob)] = blob
open(out, "wb").write(fd)
if __name__ == "__main__":
main()
+769
View File
@@ -0,0 +1,769 @@
/*
* brcmf_harness.cpp
* Host harness for the Broadcom FullMAC driver's protocol and connection
* layers (BrcmfProto.cpp, BrcmfConnect.cpp, with the real supplicant).
*
* The SDIO bus and the backplane are replaced by a simulated CYW4345x
* firmware: it decodes every SDPCM frame the driver writes to function 2
* (CDC ioctls, BDC data), answers ioctls the way the firmware does, and
* feeds back events and data frames through the same framing. The access
* point side of the WPA2 handshake is written here independently of
* Wpa.cpp, on the crypto primitives crypto_vectors checks against the
* published vectors.
*
* What stays untested is everything that needs the chip: the SDIO host,
* the backplane, firmware download (BrcmfSdio.cpp, BrcmfChip.cpp), and
* the firmware's own opinion of the structures it is sent.
*
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <cstdlib>
#include <deque>
#include <string>
#include <vector>
#include "Drivers/Net/Wifi/Brcmf.hpp"
#include "Drivers/Net/Wifi/BrcmfSdio.hpp"
#include "Drivers/Net/Wifi/Ieee80211.hpp"
#include "Drivers/Net/Wifi/Wpa.hpp"
#include "Drivers/Platform/RpiFirmware.hpp"
#include "Libraries/Crypto.hpp"
using namespace Drivers::Net::Wifi;
using Bytes = std::vector<uint8_t>;
namespace Timekeeping {
uint64_t g_ms = 1000;
uint64_t GetMilliseconds() { return g_ms; }
void BusyWaitMs(uint64_t ms) { g_ms += ms; }
void BusyWaitUs(uint64_t us) { g_ms += (us + 999) / 1000; }
}
static int g_fails = 0;
static void check(bool cond, const char* what) {
printf(" %s %s\n", cond ? "PASS" : "FAIL", what);
if (!cond) g_fails++;
}
static uint16_t be16(const uint8_t* p) { return (uint16_t)(p[0] << 8 | p[1]); }
static uint16_t le16(const uint8_t* p) { return (uint16_t)(p[0] | p[1] << 8); }
static uint32_t le32(const uint8_t* p) { uint32_t v; memcpy(&v, p, 4); return v; }
static void putle16(Bytes& b, uint16_t v) { b.push_back((uint8_t)v); b.push_back((uint8_t)(v >> 8)); }
static void putle32(Bytes& b, uint32_t v) { for (int i = 0; i < 4; i++) b.push_back((uint8_t)(v >> (8 * i))); }
static void putbe16(Bytes& b, uint16_t v) { b.push_back((uint8_t)(v >> 8)); b.push_back((uint8_t)v); }
static void putbe32(Bytes& b, uint32_t v) { for (int i = 3; i >= 0; i--) b.push_back((uint8_t)(v >> (8 * i))); }
static void append(Bytes& b, const uint8_t* p, size_t n) { b.insert(b.end(), p, p + n); }
static const uint8_t PLACEHOLDER_MAC[6] = { 0xb8, 0x27, 0xeb, 0x74, 0xf2, 0x6c };
static const uint8_t BOARD_MAC[6] = { 0xdc, 0xa6, 0x32, 0x01, 0x02, 0xff };
static const uint8_t WLAN_MAC[6] = { 0xdc, 0xa6, 0x32, 0x01, 0x03, 0x00 }; // board + 1, with carry
static const uint8_t AP_OPEN[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x01 };
static const uint8_t AP_WPA[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x02 };
// =============================================================================
// Simulated firmware
// =============================================================================
struct Ioctl {
uint32_t Cmd;
bool Set;
std::string Name; // iovar name, for GET_VAR/SET_VAR
Bytes Data; // what follows the name (or the whole payload)
uint64_t Order;
};
struct Fw {
std::deque<Bytes> ToHost; // complete SDPCM frames
Bytes Cur;
size_t CurOff = 0;
uint8_t HostSeq = 0; // next sequence number expected
uint8_t Window = 8;
bool SeqError = false;
bool Mute = false; // stop answering ioctls
std::vector<Ioctl> Ioctls;
std::vector<std::pair<Bytes, uint64_t>> DataTx; // Ethernet frames, order
uint64_t Order = 0;
bool AutoJoin = true;
void (*OnEapol)(const Bytes& body) = nullptr;
};
static Fw fw;
static void QueueFrame(uint8_t channel, const Bytes& payload) {
Bytes f(12, 0);
uint16_t len = (uint16_t)(12 + payload.size());
f[0] = (uint8_t)len; f[1] = (uint8_t)(len >> 8);
f[2] = (uint8_t)~len; f[3] = (uint8_t)(~len >> 8);
f[4] = 0;
f[5] = channel;
f[7] = 12;
f[9] = (uint8_t)(fw.HostSeq + fw.Window); // credit
append(f, payload.data(), payload.size());
fw.ToHost.push_back(f);
}
static Bytes Bdc(const Bytes& eth) {
Bytes b = { 0x20, 0, 0, 0 };
append(b, eth.data(), eth.size());
return b;
}
static void QueueEvent(uint32_t type, uint32_t status, uint32_t reason, uint16_t flags,
const uint8_t* addr, const Bytes& data = {}) {
Bytes e;
append(e, WLAN_MAC, 6);
append(e, AP_WPA, 6);
putbe16(e, 0x886C);
putbe16(e, 0x8001);
putbe16(e, (uint16_t)(48 + data.size()));
e.push_back(1);
e.push_back(0x00); e.push_back(0x10); e.push_back(0x18);
putbe16(e, 1);
putbe16(e, 2); // version
putbe16(e, flags);
putbe32(e, type);
putbe32(e, status);
putbe32(e, reason);
putbe32(e, 0); // auth type
putbe32(e, (uint32_t)data.size());
Bytes a(6, 0);
if (addr) memcpy(a.data(), addr, 6);
append(e, a.data(), 6);
Bytes ifname(16, 0);
memcpy(ifname.data(), "wlan0", 5);
append(e, ifname.data(), 16);
e.push_back(0); // ifidx
e.push_back(0); // bsscfgidx
append(e, data.data(), data.size());
QueueFrame(1, Bdc(e));
}
static void QueueEthernet(const uint8_t* src, uint16_t type, const Bytes& body) {
Bytes e;
append(e, WLAN_MAC, 6);
append(e, src, 6);
putbe16(e, type);
append(e, body.data(), body.size());
QueueFrame(2, Bdc(e));
}
// One brcmf_bss_info_le, laid out as Linux's struct.
static Bytes BssInfo(const uint8_t* bssid, const char* ssid, uint16_t chanspec, uint8_t ctl,
int16_t rssi, uint16_t caps, const Bytes& ies) {
Bytes b(128, 0);
memcpy(&b[0], "\x6d\x00\x00\x00", 4); // version 109
uint32_t len = (uint32_t)(128 + ies.size());
memcpy(&b[4], &len, 4);
memcpy(&b[8], bssid, 6);
uint16_t bcn = 100;
memcpy(&b[14], &bcn, 2);
memcpy(&b[16], &caps, 2);
b[18] = (uint8_t)strlen(ssid);
memcpy(&b[19], ssid, strlen(ssid));
memcpy(&b[72], &chanspec, 2);
b[76] = 3;
memcpy(&b[78], &rssi, 2);
b[88] = ctl;
uint16_t ieOff = 128;
memcpy(&b[116], &ieOff, 2);
uint32_t ieLen = (uint32_t)ies.size();
memcpy(&b[120], &ieLen, 4);
append(b, ies.data(), ies.size());
return b;
}
static const Bytes RSN_CCMP = {
0x01, 0x00, 0x00, 0x0f, 0xac, 0x04, 0x01, 0x00, 0x00, 0x0f, 0xac, 0x04,
0x01, 0x00, 0x00, 0x0f, 0xac, 0x02, 0x00, 0x00,
};
static const Bytes RSN_GCMP = {
0x01, 0x00, 0x00, 0x0f, 0xac, 0x08, 0x01, 0x00, 0x00, 0x0f, 0xac, 0x08,
0x01, 0x00, 0x00, 0x0f, 0xac, 0x02, 0x00, 0x00,
};
static Bytes Ies(const char* ssid, uint8_t channel, const Bytes* rsn) {
Bytes ie;
ie.push_back(0); ie.push_back((uint8_t)strlen(ssid));
append(ie, (const uint8_t*)ssid, strlen(ssid));
ie.push_back(1); ie.push_back(4); ie.push_back(0x82); ie.push_back(0x84); ie.push_back(0x0b); ie.push_back(0x16);
ie.push_back(3); ie.push_back(1); ie.push_back(channel);
if (rsn) {
ie.push_back(48); ie.push_back((uint8_t)rsn->size());
append(ie, rsn->data(), rsn->size());
}
return ie;
}
static void QueueEscan(uint16_t syncId) {
Bytes bss1 = BssInfo(AP_OPEN, "OpenNet", 0x1006, 6, -48, 0x0401, Ies("OpenNet", 6, nullptr));
Bytes bss2 = BssInfo(AP_WPA, "SecureNet", 0xd024, 36, -61, 0x0411, Ies("SecureNet", 36, &RSN_CCMP));
for (const Bytes* bss : { &bss1, &bss2 }) {
Bytes r;
putle32(r, (uint32_t)(12 + bss->size()));
putle32(r, 1);
putle16(r, syncId);
putle16(r, 1);
append(r, bss->data(), bss->size());
QueueEvent(69, 8, 0, 0, nullptr, r); // PARTIAL
}
QueueEvent(69, 0, 0, 0, nullptr); // done
}
static void FwControl(const uint8_t* p, size_t len) {
uint32_t cmd = le32(p), dlen = le32(p + 4), flags = le32(p + 8);
bool set = flags & 2;
const uint8_t* data = p + 16;
size_t avail = len - 16;
if (dlen > avail) dlen = (uint32_t)avail;
Ioctl io{cmd, set, "", {}, fw.Order++};
if (cmd == 262 || cmd == 263) {
size_t n = strnlen((const char*)data, dlen);
io.Name.assign((const char*)data, n);
if (n + 1 < dlen) io.Data.assign(data + n + 1, data + dlen);
} else {
io.Data.assign(data, data + dlen);
}
fw.Ioctls.push_back(io);
if (fw.Mute) return;
int32_t status = 0;
Bytes resp(dlen, 0);
if (cmd == 262) {
if (io.Name == "cur_etheraddr") memcpy(resp.data(), PLACEHOLDER_MAC, 6);
else if (io.Name == "ver") {
const char* v = "wl0: Aug 29 2023 01:51:02 version 7.45.265 (28bca26 CY) FWID 01-b677b91b\n";
memcpy(resp.data(), v, strlen(v) < dlen ? strlen(v) : dlen);
} else if (io.Name == "clmload_status") {
} else status = -23; // BCME_UNSUPPORTED
} else if (cmd == 263) {
if (io.Name == "escan") {
QueueEscan(le16(io.Data.data() + 6));
} else if (io.Name == "join" && fw.AutoJoin) {
QueueEvent(0, 0, 0, 0, io.Data.data() + 56);
QueueEvent(16, 0, 0, 1, io.Data.data() + 56);
} else if (io.Name == "bus:rxglom" || io.Name == "mfp" || io.Name == "sup_wpa") {
status = -23;
}
} else if (cmd == 217) { // valid channels
uint32_t list[3] = { 2, 1, 36 };
memcpy(resp.data(), list, sizeof(list) < dlen ? sizeof(list) : dlen);
}
Bytes r;
putle32(r, cmd);
putle32(r, dlen);
putle32(r, (flags & 0xFFFF0002u) | (status < 0 ? 1 : 0));
putle32(r, (uint32_t)status);
append(r, resp.data(), resp.size());
QueueFrame(0, r);
}
static void FwReceive(const uint8_t* f, size_t busLen) {
uint16_t len = le16(f), check = le16(f + 2);
if ((uint16_t)(len ^ check) != 0xFFFF || len > busLen) { fw.SeqError = true; return; }
if (f[4] != fw.HostSeq) fw.SeqError = true;
fw.HostSeq = (uint8_t)(f[4] + 1);
uint8_t ch = f[5] & 0x0F, off = f[7];
const uint8_t* p = f + off;
size_t plen = len - off;
if (ch == 0) {
FwControl(p, plen);
} else if (ch == 2) {
if (plen < 4 || (p[0] >> 4) != 2 || p[3] != 0) { fw.SeqError = true; return; }
Bytes eth(p + 4, p + plen);
fw.DataTx.push_back({eth, fw.Order++});
if (eth.size() >= 14 && be16(&eth[12]) == 0x888E && fw.OnEapol)
fw.OnEapol(Bytes(eth.begin() + 14, eth.end()));
}
}
// =============================================================================
// What the driver links against instead of the hardware
// =============================================================================
namespace Drivers::Net::Wifi {
BrcmfDevice g_brcmf;
bool BrcmfBpRead32(uint32_t addr, uint32_t& value) {
value = 0;
if (addr == g_brcmf.SdioCoreBase + 0x20 && (!fw.ToHost.empty() || !fw.Cur.empty()))
value = 0x40; // I_HMB_FRAME_IND
return true;
}
bool BrcmfBpWrite32(uint32_t, uint32_t) { return true; }
bool BrcmfBpReadMem(uint32_t, uint8_t*, uint32_t) { return true; }
bool BrcmfBpWriteMem(uint32_t, const uint8_t*, uint32_t) { return true; }
bool BrcmfBpSelectWindow(uint32_t) { return true; }
bool BrcmfF1Read8(uint32_t, uint8_t& v) { v = 0; return true; }
bool BrcmfF1Write8(uint32_t, uint8_t) { return true; }
bool BrcmfChipBringUp() {
g_brcmf.FwName = "43456";
g_brcmf.SdioCoreBase = 0x18002000;
g_brcmf.HostIntMask = 0x200000F0;
return true;
}
void BrcmfChipPowerOff() {}
void BrcmfDumpConsole() {}
uint8_t* BrcmfLoadFile(const char* ext, uint32_t& size, bool) {
if (strcmp(ext, "clm_blob") != 0) return nullptr;
size = 3000;
uint8_t* p = (uint8_t*)malloc(size);
for (uint32_t i = 0; i < size; i++) p[i] = (uint8_t)i;
return p;
}
struct Seen { Bytes Bssid; Bytes Ies; uint16_t Beacon, Caps; uint8_t Channel; int8_t Rssi; };
static std::vector<Seen> g_seen;
static std::vector<Bytes> g_delivered;
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) {
g_seen.push_back({Bytes(bssid, bssid + 6), Bytes(ies, ies + iesLen), beaconInterval,
capability, channel, rssiDbm});
}
void WifiRxEthernet(const uint8_t* frame, uint32_t len) {
g_delivered.push_back(Bytes(frame, frame + len));
}
// Routed by Wifi.cpp in the kernel.
bool WpaTxEapol(const uint8_t* body, uint32_t len) { return BrcmfWpaTxEapol(body, len); }
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
return BrcmfWpaInstallPtk(tk, tkLen, cipher);
}
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
uint8_t cipher, const uint8_t* rsc) {
return BrcmfWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc);
}
}
namespace Drivers::Net::Wifi::Sdio {
bool HostInit(uint64_t, uint32_t) { return true; }
bool CardInit() { return true; }
bool Read8(uint32_t, uint32_t, uint8_t& v) { v = 0; return true; }
bool Write8(uint32_t, uint32_t, uint8_t) { return true; }
bool EnableFunction(uint32_t, bool, uint64_t) { return true; }
bool EnableCardInterrupts(uint8_t) { return true; }
void Abort(uint32_t) {}
bool CardInterruptPending() { return !fw.ToHost.empty(); }
bool IsInitialized() { return true; }
bool ReadBlock(uint32_t fn, uint32_t, bool, uint8_t* buffer, uint32_t len) {
if (fn != FN2 || (len & 3)) return false;
Timekeeping::g_ms++;
if (fw.Cur.empty()) {
if (fw.ToHost.empty()) { memset(buffer, 0, len); return true; }
fw.Cur = fw.ToHost.front();
fw.ToHost.pop_front();
fw.CurOff = 0;
}
for (uint32_t i = 0; i < len; i++, fw.CurOff++)
buffer[i] = fw.CurOff < fw.Cur.size() ? fw.Cur[fw.CurOff] : 0;
if (fw.CurOff >= ((fw.Cur.size() + 3) & ~(size_t)3)) fw.Cur.clear();
return true;
}
bool WriteBlock(uint32_t fn, uint32_t, bool, const uint8_t* buffer, uint32_t len) {
if (fn != FN2 || (len & 3)) return false;
if (len > FN2_BLOCK && len % FN2_BLOCK) fw.SeqError = true; // padded to whole blocks
Timekeeping::g_ms++;
FwReceive(buffer, len);
return true;
}
}
namespace Drivers::Platform::RpiFirmware {
bool GetBoardMacAddress(uint8_t mac[6]) { memcpy(mac, BOARD_MAC, 6); return true; }
bool SetGpioOutput(uint32_t, bool) { return true; }
}
// =============================================================================
// An independent WPA2-PSK authenticator
// =============================================================================
namespace Ap {
using namespace Kt::Crypto;
static const char* SSID = "SecureNet";
static const char* PASS = "supersecret123";
static uint8_t pmk[32], ptk[48], anonce[32], snonce[32], gtk[16];
static uint64_t replay = 0;
static Bytes lastMsg2, lastMsg4, staRsn;
static bool msg2MicOk = false, msg4MicOk = false;
static void Prf(const uint8_t* key, const char* label, const Bytes& data, uint8_t* out, size_t n) {
uint8_t counter = 0;
size_t done = 0;
while (done < n) {
Bytes m((const uint8_t*)label, (const uint8_t*)label + strlen(label) + 1);
append(m, data.data(), data.size());
m.push_back(counter++);
uint8_t h[20];
HmacSha1(key, 32, m.data(), m.size(), h);
size_t take = n - done < 20 ? n - done : 20;
memcpy(out + done, h, take);
done += take;
}
}
static Bytes KeyFrame(uint16_t info, const Bytes& keyData, bool mic) {
Bytes k(99, 0);
k[0] = 2; k[1] = 3;
uint16_t bodyLen = (uint16_t)(95 + keyData.size());
k[2] = (uint8_t)(bodyLen >> 8); k[3] = (uint8_t)bodyLen;
k[4] = 2;
k[5] = (uint8_t)(info >> 8); k[6] = (uint8_t)info;
k[8] = 16;
for (int i = 0; i < 8; i++) k[9 + i] = (uint8_t)(replay >> (56 - 8 * i));
memcpy(&k[17], anonce, 32);
k[97] = (uint8_t)(keyData.size() >> 8); k[98] = (uint8_t)keyData.size();
append(k, keyData.data(), keyData.size());
if (mic) {
uint8_t h[20];
HmacSha1(ptk, 16, k.data(), k.size(), h);
memcpy(&k[81], h, 16);
}
return k;
}
static bool MicOk(Bytes frame) {
if (frame.size() < 99) return false;
uint8_t mic[16];
memcpy(mic, &frame[81], 16);
memset(&frame[81], 0, 16);
uint8_t h[20];
HmacSha1(ptk, 16, frame.data(), frame.size(), h);
return memcmp(h, mic, 16) == 0;
}
static void Start(const char* pass) {
Pbkdf2Sha1(pass, strlen(pass), (const uint8_t*)SSID, strlen(SSID), 4096, pmk, 32);
for (int i = 0; i < 32; i++) anonce[i] = (uint8_t)(0xA0 + i);
for (int i = 0; i < 16; i++) gtk[i] = (uint8_t)(0x30 + i);
replay = 1;
lastMsg2.clear(); lastMsg4.clear(); staRsn.clear();
msg2MicOk = msg4MicOk = false;
QueueEthernet(AP_WPA, 0x888E, KeyFrame(0x008A, {}, false)); // msg 1
}
static void OnEapol(const Bytes& f) {
if (f.size() < 99 || f[1] != 3) return;
uint16_t info = be16(&f[5]);
if (!(info & 0x0100)) return;
if (lastMsg2.empty()) {
lastMsg2 = f;
memcpy(snonce, &f[17], 32);
Bytes data;
const uint8_t* a = memcmp(AP_WPA, WLAN_MAC, 6) < 0 ? AP_WPA : WLAN_MAC;
const uint8_t* b = a == AP_WPA ? WLAN_MAC : AP_WPA;
append(data, a, 6); append(data, b, 6);
const uint8_t* n1 = memcmp(anonce, snonce, 32) < 0 ? anonce : snonce;
const uint8_t* n2 = n1 == anonce ? snonce : anonce;
append(data, n1, 32); append(data, n2, 32);
Prf(pmk, "Pairwise key expansion", data, ptk, 48);
msg2MicOk = MicOk(f);
uint16_t kdl = be16(&f[97]);
staRsn.assign(f.begin() + 99, f.begin() + 99 + kdl);
if (!msg2MicOk) {
// Wrong passphrase: the AP gives up and deauthenticates.
QueueEvent(6, 0, 15, 0, AP_WPA);
return;
}
// Message 3: our RSN IE and the GTK KDE, key-wrapped under the KEK.
Bytes plain = { 0x30, (uint8_t)RSN_CCMP.size() };
append(plain, RSN_CCMP.data(), RSN_CCMP.size());
Bytes kde = { 0xdd, 22, 0x00, 0x0f, 0xac, 0x01, 0x01, 0x00 };
append(kde, gtk, 16);
append(plain, kde.data(), kde.size());
plain.push_back(0xdd);
while (plain.size() % 8) plain.push_back(0);
Bytes wrapped(plain.size() + 8);
AesKeyWrap(ptk + 16, 16, plain.data(), plain.size(), wrapped.data());
replay = 2;
QueueEthernet(AP_WPA, 0x888E, KeyFrame(0x13CA, wrapped, true));
} else if (lastMsg4.empty()) {
lastMsg4 = f;
msg4MicOk = MicOk(f);
}
}
}
// =============================================================================
// Cases
// =============================================================================
static void Pump(int rounds = 20) {
for (int i = 0; i < rounds; i++) {
BrcmfService();
Timekeeping::g_ms += 5;
}
}
static const Ioctl* LastSet(const char* name) {
for (auto it = fw.Ioctls.rbegin(); it != fw.Ioctls.rend(); ++it)
if (it->Set && it->Name == name) return &*it;
return nullptr;
}
static uint32_t SetInt(const char* name) {
const Ioctl* io = LastSet(name);
return io && io->Data.size() >= 4 ? le32(io->Data.data()) : 0xDEADBEEF;
}
static const Ioctl* LastCmd(uint32_t cmd) {
for (auto it = fw.Ioctls.rbegin(); it != fw.Ioctls.rend(); ++it)
if (it->Cmd == cmd) return &*it;
return nullptr;
}
static constexpr int CONN_IDLE = 0, CONN_ASSOCIATING = 4, CONN_HANDSHAKING = 6,
CONN_CONNECTED = 7, CONN_FAILED = 8;
static void CaseInit() {
printf("== bring-up and preinit ==\n");
check(BrcmfProbe(0xFE300000), "the Arasan controller is claimed");
check(BrcmfCompleteInit(), "preinit completes against the firmware");
check(!fw.SeqError, "every frame carries the next sequence number and a valid header");
check(BrcmfGetState() == BrcmfState::Running, "the radio is running");
check(memcmp(g_brcmf.Mac, WLAN_MAC, 6) == 0, "the NVRAM placeholder MAC becomes the board address + 1");
const Ioctl* mac = LastSet("cur_etheraddr");
check(mac && mac->Data.size() == 6 && memcmp(mac->Data.data(), WLAN_MAC, 6) == 0,
"and is programmed into the firmware");
check(strcmp(g_brcmf.FwVersion, "7.45.265") == 0, "the firmware version is read from \"ver\"");
std::vector<const Ioctl*> clm;
for (auto& io : fw.Ioctls) if (io.Set && io.Name == "clmload") clm.push_back(&io);
check(clm.size() == 3, "a 3000-byte CLM blob goes down in three chunks");
if (clm.size() == 3) {
check(le16(clm[0]->Data.data()) == 0x1002 && le16(clm[1]->Data.data()) == 0x1000 &&
le16(clm[2]->Data.data()) == 0x1004, "flagged begin / middle / end, handler version 1");
check(le16(clm[0]->Data.data() + 2) == 2, "of type CLM");
check(le32(clm[0]->Data.data() + 4) == 1400 && le32(clm[2]->Data.data() + 4) == 200,
"with 1400-byte chunks and the remainder last");
check(clm[1]->Data[12] == (uint8_t)1400 && clm[2]->Data[12 + 199] == (uint8_t)2999,
"carrying the blob's bytes in order");
}
const Ioctl* ev = LastSet("event_msgs");
bool evOk = ev && ev->Data.size() == 16;
if (evOk) {
for (uint32_t e : { 0u, 5u, 6u, 12u, 16u, 69u })
evOk = evOk && (ev->Data[e / 8] & (1u << (e % 8)));
}
check(evOk, "events for join, link, deauth/disassoc and escan are enabled");
check(SetInt("roam_off") == 1, "firmware roaming is off");
const Ioctl* up = LastCmd(2);
check(up && up->Set, "WLC_UP is sent");
const Ioctl* pm = LastCmd(86);
check(pm && le32(pm->Data.data()) == 0, "power save is off");
check(g_brcmf.ChannelCount == 2 && g_brcmf.Band24 && g_brcmf.Band5,
"the valid channel list gives the channel count and bands");
uint32_t v = 0;
check(!BrcmfIovarGetInt("no_such_iovar", v, true), "an iovar the firmware rejects reports failure");
check(BrcmfGetState() == BrcmfState::Running, "without counting against the firmware's health");
}
static void CaseScan() {
printf("== escan ==\n");
check(BrcmfStartScan(), "a scan starts");
const Ioctl* es = LastSet("escan");
check(es && es->Data.size() == 72, "escan carries the 72-byte v1 parameter block");
if (es && es->Data.size() == 72) {
const uint8_t* p = es->Data.data();
check(le32(p) == 1 && le16(p + 4) == 1, "version 1, action start");
bool bcast = true;
for (int i = 0; i < 6; i++) bcast = bcast && p[44 + i] == 0xFF;
check(bcast && p[50] == 2 && p[51] == 0, "broadcast BSSID, any BSS type, active");
check(le32(p + 52) == 0xFFFFFFFF && le32(p + 68) == 0, "default dwells, every channel");
}
Pump();
check(!BrcmfScanActive(), "the completion event ends the scan");
check(g_seen.size() == 2, "both BSSs reach the scan table");
if (g_seen.size() == 2) {
check(memcmp(g_seen[0].Bssid.data(), AP_OPEN, 6) == 0 && g_seen[0].Channel == 6 &&
g_seen[0].Rssi == -48 && g_seen[0].Beacon == 100, "BSSID, channel, RSSI, beacon interval");
check(g_seen[1].Channel == 36 && (g_seen[1].Caps & 0x10), "the 5 GHz network, privacy set");
check(g_seen[1].Ies.size() == Ies("SecureNet", 36, &RSN_CCMP).size() && g_seen[1].Ies[0] == 0,
"with its information elements intact");
}
}
static void CaseOpenJoin() {
printf("== open network ==\n");
check(BrcmfConnectStart(AP_OPEN, 6, "OpenNet", nullptr, nullptr, 0), "the join is accepted");
check(SetInt("wpa_auth") == 0 && SetInt("wsec") == 0 && SetInt("auth") == 0,
"no WPA, no encryption, open system authentication");
const Ioctl* j = LastSet("join");
check(j && j->Data.size() == 72, "join carries brcmf_ext_join_params_le");
if (j && j->Data.size() == 72) {
const uint8_t* p = j->Data.data();
check(le32(p) == 7 && memcmp(p + 4, "OpenNet", 7) == 0, "the SSID");
check(memcmp(p + 56, AP_OPEN, 6) == 0 && le32(p + 64) == 1 && le16(p + 68) == 0x1006,
"the BSSID and the chanspec escan reported");
}
check(BrcmfConnectState() == CONN_ASSOCIATING, "associating until the firmware says otherwise");
Pump();
check(BrcmfConnectState() == CONN_CONNECTED && BrcmfLinkUp(), "E_SET_SSID + E_LINK bring the link up");
Bytes eth;
append(eth, AP_OPEN, 6); append(eth, WLAN_MAC, 6); putbe16(eth, 0x0800);
for (int i = 0; i < 100; i++) eth.push_back((uint8_t)i);
size_t before = fw.DataTx.size();
check(BrcmfSendEthernet(eth.data(), (uint32_t)eth.size()), "an Ethernet frame is accepted");
Pump(2);
check(fw.DataTx.size() == before + 1 && fw.DataTx.back().first == eth,
"and reaches the firmware unchanged behind a BDC header");
Bytes big = eth;
big.resize(1514, 0x5a);
check(BrcmfSendEthernet(big.data(), (uint32_t)big.size()), "a full-size frame is accepted");
Pump(2);
check(fw.DataTx.back().first == big && !fw.SeqError, "and goes out padded to whole blocks");
g_delivered.clear();
QueueEthernet(AP_OPEN, 0x0800, Bytes(60, 0x11));
Pump(2);
check(g_delivered.size() == 1 && g_delivered[0].size() == 74 && be16(&g_delivered[0][12]) == 0x0800,
"a received data frame is delivered to the stack");
QueueEvent(6, 0, 3, 0, AP_OPEN); // DEAUTH_IND
Pump();
check(BrcmfConnectState() == CONN_IDLE && !BrcmfLinkUp(), "a deauthentication takes the link down");
check(!BrcmfSendEthernet(eth.data(), (uint32_t)eth.size()), "and nothing is sent while it is down");
}
static const Ioctl* FindKey(uint32_t index) {
for (auto it = fw.Ioctls.rbegin(); it != fw.Ioctls.rend(); ++it)
if (it->Set && it->Name == "wsec_key" && it->Data.size() == 164 && le32(it->Data.data()) == index)
return &*it;
return nullptr;
}
static void CaseWpaJoin() {
printf("== WPA2-PSK ==\n");
fw.OnEapol = Ap::OnEapol;
size_t ioctlsBefore = fw.Ioctls.size();
check(BrcmfConnectStart(AP_WPA, 36, "SecureNet", Ap::PASS, RSN_CCMP.data(), (uint8_t)RSN_CCMP.size()),
"the join is accepted");
check(SetInt("wpa_auth") == 0x80 && SetInt("wsec") == 4 && SetInt("auth") == 0,
"WPA2-PSK, AES, open system authentication");
const Ioctl* ie = LastSet("wpaie");
Bytes want = { 0x30, (uint8_t)RSN_CCMP.size() };
append(want, RSN_CCMP.data(), RSN_CCMP.size());
check(ie && ie->Data == want, "the supplicant's RSN element goes into the association request");
const Ioctl* j = LastSet("join");
check(j && le16(j->Data.data() + 68) == 0xd024, "the 5 GHz chanspec from the scan");
Pump();
check(BrcmfConnectState() == CONN_HANDSHAKING && !BrcmfLinkUp(),
"association waits for the handshake before the link is usable");
Ap::Start(Ap::PASS);
Pump(40);
check(!Ap::lastMsg2.empty() && Ap::msg2MicOk, "message 2 carries a MIC the AP verifies");
check(Ap::staRsn == want, "and the same RSN element");
check(!Ap::lastMsg4.empty() && Ap::msg4MicOk, "message 4 follows message 3, MIC verified");
check(BrcmfConnectState() == CONN_CONNECTED && BrcmfLinkUp(), "the link is up once keyed");
const Ioctl* ptk = FindKey(0);
const Ioctl* gtk = FindKey(1);
check(ptk && memcmp(ptk->Data.data() + 8, Ap::ptk + 32, 16) == 0 && le32(ptk->Data.data() + 4) == 16,
"the pairwise key is the TK the AP derived");
if (ptk) {
const uint8_t* k = ptk->Data.data();
check(le32(k + 112) == 4 && le32(k + 116) == 0 && memcmp(k + 156, AP_WPA, 6) == 0,
"AES-CCM, bound to the AP's address");
}
check(gtk && memcmp(gtk->Data.data() + 8, Ap::gtk, 16) == 0, "the group key is the AP's GTK");
if (gtk) {
const uint8_t* k = gtk->Data.data();
bool noEa = true;
for (int i = 0; i < 6; i++) noEa = noEa && k[156 + i] == 0;
check(le32(k + 116) == 2 && noEa && le32(k + 132) == 1, "primary, broadcast, receive IV set");
}
uint64_t msg4Order = 0;
for (auto& d : fw.DataTx)
if (d.first.size() > 14 && be16(&d.first[12]) == 0x888E) msg4Order = d.second;
check(ptk && msg4Order != 0 && msg4Order < ptk->Order,
"message 4 reaches the radio before the pairwise key is installed");
(void)ioctlsBefore;
QueueEvent(16, 0, 0, 0, AP_WPA); // link down
Pump();
check(BrcmfConnectState() == CONN_IDLE, "losing the link returns to idle");
}
static void CaseWrongPassphrase() {
printf("== wrong passphrase ==\n");
check(BrcmfConnectStart(AP_WPA, 36, "SecureNet", "not-the-password", RSN_CCMP.data(),
(uint8_t)RSN_CCMP.size()), "the join is accepted");
Pump();
Ap::Start(Ap::PASS);
Pump(40);
check(!Ap::lastMsg2.empty() && !Ap::msg2MicOk, "the AP rejects message 2's MIC");
check(BrcmfConnectState() == CONN_FAILED, "the deauthentication fails the join");
check(BrcmfJoinFailedOnAuth(), "and it is reported as an authentication failure");
BrcmfConnectAbort();
const Ioctl* d = LastCmd(52);
check(d && d->Set && memcmp(d->Data.data() + 4, AP_WPA, 6) == 0, "abort disassociates from the AP");
check(BrcmfConnectState() == CONN_IDLE, "and leaves the connection idle");
}
static void CaseRefusals() {
printf("== refusals ==\n");
check(!BrcmfConnectStart(AP_WPA, 36, "SecureNet", Ap::PASS, RSN_GCMP.data(), (uint8_t)RSN_GCMP.size()),
"a GCMP network is refused before anything is sent");
check(BrcmfConnectRefusedForSecurity(), "as a security refusal");
check(!BrcmfConnectStart(AP_WPA, 36, "SecureNet", nullptr, RSN_CCMP.data(), (uint8_t)RSN_CCMP.size()),
"an encrypted network without a passphrase is refused");
}
static void CaseJoinTimeout() {
printf("== join timeout ==\n");
fw.AutoJoin = false;
check(BrcmfConnectStart(AP_OPEN, 6, "OpenNet", nullptr, nullptr, 0), "the join is accepted");
Timekeeping::g_ms += 13000;
Pump(2);
check(BrcmfConnectState() == CONN_FAILED, "no answer from the firmware fails the join in bounded time");
BrcmfConnectAbort();
check(BrcmfConnectStart(AP_OPEN, 6, "OpenNet", nullptr, nullptr, 0), "another join is accepted");
QueueEvent(0, 3, 0, 0, AP_OPEN); // SET_SSID: no networks
Pump();
check(BrcmfConnectState() == CONN_FAILED, "an E_SET_SSID failure fails it at once");
BrcmfConnectAbort();
fw.AutoJoin = true;
}
static void CaseDeadFirmware() {
printf("== firmware stops answering ==\n");
fw.Mute = true;
uint32_t v = 0;
for (int i = 0; i < 3; i++) BrcmfIovarGetInt("ver", v, true);
check(BrcmfGetState() == BrcmfState::Error, "three unanswered commands mark the radio failed");
check(!BrcmfStartScan(), "and nothing more is attempted");
fw.Mute = false;
}
int main() {
CaseInit();
CaseScan();
CaseOpenJoin();
CaseWpaJoin();
CaseWrongPassphrase();
CaseRefusals();
CaseJoinTimeout();
CaseDeadFirmware();
if (g_fails) {
printf("\n%d Broadcom case(s) FAILED\n", g_fails);
return 1;
}
printf("\nAll Broadcom protocol and connection cases passed.\n");
return 0;
}
+10
View File
@@ -135,6 +135,16 @@ namespace Drivers::Net::Wifi {
void WifiRxEthernet(const uint8_t* frame, uint32_t len) { void WifiRxEthernet(const uint8_t* frame, uint32_t len) {
puthex("ETH", frame, len); puthex("ETH", frame, len);
} }
// Wifi.cpp routes the supplicant's hooks to the running backend.
bool WpaTxEapol(const uint8_t* body, uint32_t len) { return IwxWpaTxEapol(body, len); }
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
return IwxWpaInstallPtk(tk, tkLen, cipher);
}
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
uint8_t cipher, const uint8_t* rsc) {
return IwxWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc);
}
} }
// ============================================================================= // =============================================================================
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more