Compare commits
11
Commits
808e1d66fd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51a2b3a932 | ||
|
|
8b4a938164 | ||
|
|
be84506fdf | ||
|
|
559c3e2875 | ||
|
|
36291cd1cc | ||
|
|
b781cf8c97 | ||
|
|
7e036c94e7 | ||
|
|
1d00c1c016 | ||
|
|
3310b3d6de | ||
|
|
93d8becce3 | ||
|
|
ffee9002a6 |
No files matched your search
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
#include <Drivers/Audio/IntelHda.hpp>
|
#include <Drivers/Audio/IntelHda.hpp>
|
||||||
#include <Drivers/Audio/Mixer.hpp>
|
#include <Drivers/Audio/Mixer.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
#include <Drivers/Bluetooth/A2dp.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
|
|
||||||
#include "Syscall.hpp"
|
#include "Syscall.hpp"
|
||||||
@@ -55,10 +55,10 @@ namespace montauk::abi {
|
|||||||
return Drivers::Audio::Mixer::SetOutput(
|
return Drivers::Audio::Mixer::SetOutput(
|
||||||
(Drivers::Audio::Mixer::Output)value);
|
(Drivers::Audio::Mixer::Output)value);
|
||||||
if (cmd == AUDIO_CTL_BT_STATUS) {
|
if (cmd == AUDIO_CTL_BT_STATUS) {
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||||
if (Drivers::USB::Bluetooth::A2dp::IsReady()) return 2;
|
if (Drivers::Bluetooth::A2dp::IsReady()) return 2;
|
||||||
return Drivers::USB::Bluetooth::A2dp::GetState()
|
return Drivers::Bluetooth::A2dp::GetState()
|
||||||
== Drivers::USB::Bluetooth::A2dp::State::Idle
|
== Drivers::Bluetooth::A2dp::State::Idle
|
||||||
? 0 : 1;
|
? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/Hci.hpp>
|
#include <Drivers/Bluetooth/Hci.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
@@ -19,14 +19,14 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
|
static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
|
||||||
if (!buf || maxCount <= 0) return InvalidArgument;
|
if (!buf || maxCount <= 0) return InvalidArgument;
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||||
|
|
||||||
// Use a stack buffer for kernel-side results (max 16)
|
// Use a stack buffer for kernel-side results (max 16)
|
||||||
Drivers::USB::Bluetooth::Hci::InquiryDevice tmpBuf[16];
|
Drivers::Bluetooth::Hci::InquiryDevice tmpBuf[16];
|
||||||
int kMax = maxCount;
|
int kMax = maxCount;
|
||||||
if (kMax > 16) kMax = 16;
|
if (kMax > 16) kMax = 16;
|
||||||
|
|
||||||
int found = Drivers::USB::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
|
int found = Drivers::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
|
||||||
if (found < 0) return found;
|
if (found < 0) return found;
|
||||||
|
|
||||||
// Convert kernel InquiryDevice to userspace BtScanResult
|
// Convert kernel InquiryDevice to userspace BtScanResult
|
||||||
@@ -47,38 +47,38 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
static int64_t Sys_BtConnect(const uint8_t* bdAddr) {
|
static int64_t Sys_BtConnect(const uint8_t* bdAddr) {
|
||||||
if (!bdAddr) return InvalidArgument;
|
if (!bdAddr) return InvalidArgument;
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||||
|
|
||||||
return (int64_t)Drivers::USB::Bluetooth::Connect(bdAddr);
|
return (int64_t)Drivers::Bluetooth::Connect(bdAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int64_t Sys_BtDisconnect(const uint8_t* bdAddr) {
|
static int64_t Sys_BtDisconnect(const uint8_t* bdAddr) {
|
||||||
if (!bdAddr) return InvalidArgument;
|
if (!bdAddr) return InvalidArgument;
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||||
|
|
||||||
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
|
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
|
||||||
<< (uint64_t)Sched::GetCurrentPid()
|
<< (uint64_t)Sched::GetCurrentPid()
|
||||||
<< " entered Bluetooth disconnect stage";
|
<< " entered Bluetooth disconnect stage";
|
||||||
return (int64_t)Drivers::USB::Bluetooth::Disconnect(bdAddr);
|
return (int64_t)Drivers::Bluetooth::Disconnect(bdAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) {
|
static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) {
|
||||||
if (!bdAddr) return InvalidArgument;
|
if (!bdAddr) return InvalidArgument;
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||||
|
|
||||||
// bdAddr is a 6-byte buffer, addr[0] = least-significant octet.
|
// bdAddr is a 6-byte buffer, addr[0] = least-significant octet.
|
||||||
return Drivers::USB::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
|
return Drivers::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) {
|
static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) {
|
||||||
if (!buf || maxCount <= 0) return 0;
|
if (!buf || maxCount <= 0) return 0;
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||||
|
|
||||||
Drivers::USB::Bluetooth::Hci::BondInfo tmpBuf[8];
|
Drivers::Bluetooth::Hci::BondInfo tmpBuf[8];
|
||||||
int kMax = maxCount;
|
int kMax = maxCount;
|
||||||
if (kMax > 8) kMax = 8;
|
if (kMax > 8) kMax = 8;
|
||||||
|
|
||||||
int count = Drivers::USB::Bluetooth::ListBonded(tmpBuf, kMax);
|
int count = Drivers::Bluetooth::ListBonded(tmpBuf, kMax);
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6);
|
memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6);
|
||||||
@@ -91,20 +91,20 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
static int64_t Sys_BtForget(const uint8_t* bdAddr) {
|
static int64_t Sys_BtForget(const uint8_t* bdAddr) {
|
||||||
if (!bdAddr) return InvalidArgument;
|
if (!bdAddr) return InvalidArgument;
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||||
|
|
||||||
return (int64_t)Drivers::USB::Bluetooth::ForgetDevice(bdAddr);
|
return (int64_t)Drivers::Bluetooth::ForgetDevice(bdAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) {
|
static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) {
|
||||||
if (!buf || maxCount <= 0) return 0;
|
if (!buf || maxCount <= 0) return 0;
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||||
|
|
||||||
Drivers::USB::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
|
Drivers::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
|
||||||
int kMax = maxCount;
|
int kMax = maxCount;
|
||||||
if (kMax > 4) kMax = 4;
|
if (kMax > 4) kMax = 4;
|
||||||
|
|
||||||
int count = Drivers::USB::Bluetooth::ListConnected(tmpBuf, kMax);
|
int count = Drivers::Bluetooth::ListConnected(tmpBuf, kMax);
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6);
|
memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6);
|
||||||
@@ -123,14 +123,14 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
memset(buf, 0, sizeof(BtAdapterInfo));
|
memset(buf, 0, sizeof(BtAdapterInfo));
|
||||||
|
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) {
|
if (!Drivers::Bluetooth::IsInitialized()) {
|
||||||
buf->initialized = 0;
|
buf->initialized = 0;
|
||||||
return NoDevice;
|
return NoDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf->initialized = 1;
|
buf->initialized = 1;
|
||||||
memcpy(buf->bdAddr, Drivers::USB::Bluetooth::GetBdAddr(), 6);
|
memcpy(buf->bdAddr, Drivers::Bluetooth::GetBdAddr(), 6);
|
||||||
buf->scanning = Drivers::USB::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
|
buf->scanning = Drivers::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
|
||||||
|
|
||||||
// Copy adapter name
|
// Copy adapter name
|
||||||
const char* name = "MontaukOS";
|
const char* name = "MontaukOS";
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 73
|
#define MONTAUK_BUILD_NUMBER 123
|
||||||
@@ -8,9 +8,10 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
#include <Drivers/USB/Xhci.hpp>
|
||||||
#include <Drivers/USB/UsbDevice.hpp>
|
#include <Drivers/USB/UsbDevice.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Drivers/Net/E1000.hpp>
|
#include <Drivers/Net/E1000.hpp>
|
||||||
#include <Drivers/Net/E1000E.hpp>
|
#include <Drivers/Net/E1000E.hpp>
|
||||||
|
#include <Drivers/Net/Genet.hpp>
|
||||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||||
#include <Drivers/Storage/Ahci.hpp>
|
#include <Drivers/Storage/Ahci.hpp>
|
||||||
@@ -122,11 +123,24 @@ namespace montauk::abi {
|
|||||||
if (Drivers::Net::E1000E::IsInitialized()) {
|
if (Drivers::Net::E1000E::IsInitialized()) {
|
||||||
add(5, "Intel E1000E", "Gigabit Ethernet (82574L)");
|
add(5, "Intel E1000E", "Gigabit Ethernet (82574L)");
|
||||||
}
|
}
|
||||||
|
if (Drivers::Net::Genet::IsInitialized()) {
|
||||||
|
uint32_t speed = Drivers::Net::Genet::GetLinkSpeed();
|
||||||
|
add(5, "Broadcom GENET", speed == 1000 ? "Gigabit Ethernet (1000 Mbit/s)"
|
||||||
|
: speed == 100 ? "Gigabit Ethernet (100 Mbit/s)"
|
||||||
|
: speed == 10 ? "Gigabit Ethernet (10 Mbit/s)"
|
||||||
|
: "Gigabit Ethernet (no link)");
|
||||||
|
}
|
||||||
if (Drivers::Net::Wifi::IsPresent()) {
|
if (Drivers::Net::Wifi::IsPresent()) {
|
||||||
add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp()
|
add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp()
|
||||||
? "802.11 wireless (connected)"
|
? "802.11 wireless (connected)"
|
||||||
: "802.11 wireless");
|
: "802.11 wireless");
|
||||||
}
|
}
|
||||||
|
// An on-board UART Bluetooth controller (Raspberry Pi) is no USB
|
||||||
|
// device, so it is listed here; USB adapters appear above.
|
||||||
|
if (Drivers::Bluetooth::IsInitialized() &&
|
||||||
|
Drivers::Bluetooth::Hci::GetTransport() == Drivers::Bluetooth::Hci::Transport::Uart) {
|
||||||
|
add(5, "Bluetooth Adapter", "Broadcom, HCI over UART");
|
||||||
|
}
|
||||||
|
|
||||||
// Display (category 6)
|
// Display (category 6)
|
||||||
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
|
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
|
||||||
@@ -148,7 +162,7 @@ namespace montauk::abi {
|
|||||||
add(9, "Intel HDA", detail);
|
add(9, "Intel HDA", detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Drivers::USB::Bluetooth::IsInitialized()) {
|
if (Drivers::Bluetooth::IsInitialized()) {
|
||||||
add(9, "Bluetooth Audio", "A2DP (SBC)");
|
add(9, "Bluetooth Audio", "A2DP (SBC)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
#include "Mixer.hpp"
|
#include "Mixer.hpp"
|
||||||
#include "IntelHda.hpp"
|
#include "IntelHda.hpp"
|
||||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
#include <Drivers/Bluetooth/A2dp.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/Avrcp.hpp>
|
#include <Drivers/Bluetooth/Avrcp.hpp>
|
||||||
#include <Memory/PageFrameAllocator.hpp>
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
#include <Memory/HHDM.hpp>
|
#include <Memory/HHDM.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
@@ -146,8 +146,8 @@ namespace Drivers::Audio::Mixer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool BluetoothReady() {
|
static bool BluetoothReady() {
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return false;
|
if (!Drivers::Bluetooth::IsInitialized()) return false;
|
||||||
return Drivers::USB::Bluetooth::A2dp::IsReady();
|
return Drivers::Bluetooth::A2dp::IsReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SyncHdaMasterMute() {
|
static void SyncHdaMasterMute() {
|
||||||
@@ -236,7 +236,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
static uint32_t BackendWriteSpace() {
|
static uint32_t BackendWriteSpace() {
|
||||||
if (g_output == Output::Bluetooth) {
|
if (g_output == Output::Bluetooth) {
|
||||||
return g_btOpened
|
return g_btOpened
|
||||||
? Drivers::USB::Bluetooth::A2dp::GetWriteSpace() : 0;
|
? Drivers::Bluetooth::A2dp::GetWriteSpace() : 0;
|
||||||
}
|
}
|
||||||
return g_hdaOpened ? IntelHda::GetWriteSpace(g_hdaHandle) : 0;
|
return g_hdaOpened ? IntelHda::GetWriteSpace(g_hdaHandle) : 0;
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
static int BackendWrite(const uint8_t* data, uint32_t size) {
|
static int BackendWrite(const uint8_t* data, uint32_t size) {
|
||||||
if (g_output == Output::Bluetooth) {
|
if (g_output == Output::Bluetooth) {
|
||||||
return g_btOpened
|
return g_btOpened
|
||||||
? Drivers::USB::Bluetooth::A2dp::WriteAudio(data, size) : -1;
|
? Drivers::Bluetooth::A2dp::WriteAudio(data, size) : -1;
|
||||||
}
|
}
|
||||||
return g_hdaOpened ? IntelHda::Write(g_hdaHandle, data, size) : -1;
|
return g_hdaOpened ? IntelHda::Write(g_hdaHandle, data, size) : -1;
|
||||||
}
|
}
|
||||||
@@ -403,14 +403,14 @@ namespace Drivers::Audio::Mixer {
|
|||||||
if (g_output == Output::Bluetooth && !g_btOpened && BluetoothReady()) {
|
if (g_output == Output::Bluetooth && !g_btOpened && BluetoothReady()) {
|
||||||
g_switchingOutput = true;
|
g_switchingOutput = true;
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
bool btOk = Drivers::USB::Bluetooth::A2dp::ConfigureStream(
|
bool btOk = Drivers::Bluetooth::A2dp::ConfigureStream(
|
||||||
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
||||||
Drivers::USB::Bluetooth::A2dp::StartStream();
|
Drivers::Bluetooth::A2dp::StartStream();
|
||||||
g_lock.Acquire();
|
g_lock.Acquire();
|
||||||
g_btOpened = btOk;
|
g_btOpened = btOk;
|
||||||
if (!btOk) g_output = Output::Hda;
|
if (!btOk) g_output = Output::Hda;
|
||||||
if (btOk)
|
if (btOk)
|
||||||
Drivers::USB::Bluetooth::A2dp::SetMuted(
|
Drivers::Bluetooth::A2dp::SetMuted(
|
||||||
g_masterMute.load(std::memory_order_acquire));
|
g_masterMute.load(std::memory_order_acquire));
|
||||||
g_switchingOutput = false;
|
g_switchingOutput = false;
|
||||||
wakeSwitchWaiters = true;
|
wakeSwitchWaiters = true;
|
||||||
@@ -515,7 +515,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
if (closeBt) g_btOpened = false;
|
if (closeBt) g_btOpened = false;
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
if (closeHda) IntelHda::Close(hdaHandle);
|
if (closeHda) IntelHda::Close(hdaHandle);
|
||||||
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
if (closeBt) Drivers::Bluetooth::A2dp::StopStream(true);
|
||||||
if (ringToFree) FreeRing(ringToFree);
|
if (ringToFree) FreeRing(ringToFree);
|
||||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
@@ -535,7 +535,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
bool serviceBluetooth = g_output == Output::Bluetooth;
|
bool serviceBluetooth = g_output == Output::Bluetooth;
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
if (serviceBluetooth)
|
if (serviceBluetooth)
|
||||||
Drivers::USB::Bluetooth::A2dp::ServiceMedia();
|
Drivers::Bluetooth::A2dp::ServiceMedia();
|
||||||
return (int)written;
|
return (int)written;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,7 +651,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
if (changed) BumpSerialLocked();
|
if (changed) BumpSerialLocked();
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
if (closeHda) IntelHda::Close(hdaHandle);
|
if (closeHda) IntelHda::Close(hdaHandle);
|
||||||
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
if (closeBt) Drivers::Bluetooth::A2dp::StopStream(true);
|
||||||
for (int i = 0; i < ringCount; i++) FreeRing(ringsToFree[i]);
|
for (int i = 0; i < ringCount; i++) FreeRing(ringsToFree[i]);
|
||||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
@@ -668,7 +668,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
Drivers::USB::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
|
Drivers::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
|
||||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,7 +688,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
|
|
||||||
if (changed) SyncHdaMasterMute();
|
if (changed) SyncHdaMasterMute();
|
||||||
if (changed)
|
if (changed)
|
||||||
Drivers::USB::Bluetooth::A2dp::SetMuted(muted);
|
Drivers::Bluetooth::A2dp::SetMuted(muted);
|
||||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,11 +733,11 @@ namespace Drivers::Audio::Mixer {
|
|||||||
}
|
}
|
||||||
} else if (needBackend && output == Output::Bluetooth) {
|
} else if (needBackend && output == Output::Bluetooth) {
|
||||||
prepared = BluetoothReady() &&
|
prepared = BluetoothReady() &&
|
||||||
Drivers::USB::Bluetooth::A2dp::ConfigureStream(
|
Drivers::Bluetooth::A2dp::ConfigureStream(
|
||||||
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
|
||||||
Drivers::USB::Bluetooth::A2dp::StartStream();
|
Drivers::Bluetooth::A2dp::StartStream();
|
||||||
if (prepared)
|
if (prepared)
|
||||||
Drivers::USB::Bluetooth::A2dp::SetMuted(
|
Drivers::Bluetooth::A2dp::SetMuted(
|
||||||
g_masterMute.load(std::memory_order_acquire));
|
g_masterMute.load(std::memory_order_acquire));
|
||||||
} else if (output == Output::Hda) {
|
} else if (output == Output::Hda) {
|
||||||
prepared = IntelHda::IsInitialized();
|
prepared = IntelHda::IsInitialized();
|
||||||
@@ -779,7 +779,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
|
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
|
||||||
|
|
||||||
if (closeHda) IntelHda::Close(oldHdaHandle);
|
if (closeHda) IntelHda::Close(oldHdaHandle);
|
||||||
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
if (closeBt) Drivers::Bluetooth::A2dp::StopStream(true);
|
||||||
Sched::WakeObjectWaiters((void*)&g_serial);
|
Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -815,7 +815,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
wake = true;
|
wake = true;
|
||||||
}
|
}
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
if (stopBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
|
if (stopBt) Drivers::Bluetooth::A2dp::StopStream(true);
|
||||||
if (wake) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (wake) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include "Sbc.hpp"
|
#include "Sbc.hpp"
|
||||||
#include "L2cap.hpp"
|
#include "L2cap.hpp"
|
||||||
#include "Hci.hpp"
|
#include "Hci.hpp"
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
@@ -18,7 +17,7 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::A2dp {
|
namespace Drivers::Bluetooth::A2dp {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// AVDTP constants
|
// AVDTP constants
|
||||||
@@ -222,7 +221,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents(); // AVDTP responses arrive as ACL data
|
Hci::DrainEvents(); // AVDTP responses arrive as ACL data
|
||||||
if (g_avdtpResponseReady.load(std::memory_order_acquire)) {
|
if (g_avdtpResponseReady.load(std::memory_order_acquire)) {
|
||||||
// Freeze this mailbox before returning to the caller. A late
|
// Freeze this mailbox before returning to the caller. A late
|
||||||
@@ -557,7 +556,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
|
|
||||||
void OnChannelReady(uint16_t l2capCid) {
|
void OnChannelReady(uint16_t l2capCid) {
|
||||||
// Invoked from L2CAP inside the ACL receive path -- i.e. NESTED under
|
// Invoked from L2CAP inside the ACL receive path -- i.e. NESTED under
|
||||||
// Xhci::PollEvents. PollEvents is non-reentrant (re-entry guard), so we
|
// Hci::DrainEvents. That is non-reentrant (re-entry guard), so we
|
||||||
// must NOT run the blocking AVDTP signaling chain here; a nested poll
|
// must NOT run the blocking AVDTP signaling chain here; a nested poll
|
||||||
// would stall and every WaitAvdtpResponse() would time out. Just record
|
// would stall and every WaitAvdtpResponse() would time out. Just record
|
||||||
// the channel; StartSource() drives the handshakes from top-level
|
// the channel; StartSource() drives the handshakes from top-level
|
||||||
@@ -1161,7 +1160,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
bool answered = false;
|
bool answered = false;
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
if (g_sdpRspReady.load(std::memory_order_acquire)) {
|
if (g_sdpRspReady.load(std::memory_order_acquire)) {
|
||||||
answered = true;
|
answered = true;
|
||||||
@@ -1196,7 +1195,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
static bool WaitEncrypted(uint32_t timeoutMs) {
|
static bool WaitEncrypted(uint32_t timeoutMs) {
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
Hci::ProcessPendingCommands();
|
Hci::ProcessPendingCommands();
|
||||||
auto* c = Hci::GetConnection(g_aclHandle.load(std::memory_order_acquire));
|
auto* c = Hci::GetConnection(g_aclHandle.load(std::memory_order_acquire));
|
||||||
@@ -1235,7 +1234,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
uint64_t waitStart = Timekeeping::GetMilliseconds();
|
uint64_t waitStart = Timekeeping::GetMilliseconds();
|
||||||
while (g_sourceSetupActive.load(std::memory_order_acquire)
|
while (g_sourceSetupActive.load(std::memory_order_acquire)
|
||||||
&& Timekeeping::GetMilliseconds() - waitStart < timeoutMs) {
|
&& Timekeeping::GetMilliseconds() - waitStart < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
for (int j = 0; j < 100; j++) Arch::Pause();
|
for (int j = 0; j < 100; j++) Arch::Pause();
|
||||||
}
|
}
|
||||||
@@ -1265,7 +1264,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
if (g_sigCid.load(std::memory_order_acquire) == 0) {
|
if (g_sigCid.load(std::memory_order_acquire) == 0) {
|
||||||
uint64_t lStart = Timekeeping::GetMilliseconds();
|
uint64_t lStart = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - lStart < 1000) {
|
while (Timekeeping::GetMilliseconds() - lStart < 1000) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
Hci::ProcessPendingCommands();
|
Hci::ProcessPendingCommands();
|
||||||
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
|
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
|
||||||
@@ -1319,7 +1318,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
uint32_t attemptWaitMs = timeoutMs < 1500 ? timeoutMs : 1500;
|
uint32_t attemptWaitMs = timeoutMs < 1500 ? timeoutMs : 1500;
|
||||||
uint32_t waitBudgetMs = attemptWaitMs;
|
uint32_t waitBudgetMs = attemptWaitMs;
|
||||||
while (Timekeeping::GetMilliseconds() - sigStart < waitBudgetMs) {
|
while (Timekeeping::GetMilliseconds() - sigStart < waitBudgetMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
|
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
|
||||||
// PENDING is explicitly non-final. A peer performing service
|
// PENDING is explicitly non-final. A peer performing service
|
||||||
@@ -1363,7 +1362,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
if (sig) L2cap::FreeChannel(sig);
|
if (sig) L2cap::FreeChannel(sig);
|
||||||
uint64_t st = Timekeeping::GetMilliseconds();
|
uint64_t st = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - st < 400) {
|
while (Timekeeping::GetMilliseconds() - st < 400) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
for (int j = 0; j < 200; j++) Arch::Pause();
|
for (int j = 0; j < 200; j++) Arch::Pause();
|
||||||
}
|
}
|
||||||
@@ -1384,7 +1383,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
if (g_peerConfigured.load(std::memory_order_acquire)) {
|
if (g_peerConfigured.load(std::memory_order_acquire)) {
|
||||||
uint64_t peerStart = Timekeeping::GetMilliseconds();
|
uint64_t peerStart = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - peerStart < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - peerStart < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
State s = g_state.load(std::memory_order_acquire);
|
State s = g_state.load(std::memory_order_acquire);
|
||||||
if (s == State::Open || s == State::Streaming) break;
|
if (s == State::Open || s == State::Streaming) break;
|
||||||
@@ -1483,7 +1482,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
uint64_t mStart = Timekeeping::GetMilliseconds();
|
uint64_t mStart = Timekeeping::GetMilliseconds();
|
||||||
while (g_mediaCid.load(std::memory_order_acquire) == 0
|
while (g_mediaCid.load(std::memory_order_acquire) == 0
|
||||||
&& Timekeeping::GetMilliseconds() - mStart < kMediaWaitMs) {
|
&& Timekeeping::GetMilliseconds() - mStart < kMediaWaitMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
auto* ch = media ? L2cap::GetChannel(media) : nullptr;
|
auto* ch = media ? L2cap::GetChannel(media) : nullptr;
|
||||||
if (ch && ch->Configured) {
|
if (ch && ch->Configured) {
|
||||||
@@ -1838,6 +1837,36 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
// ConfigureStream
|
// ConfigureStream
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
// A UART transport has a hard byte budget: at 460800 baud it carries
|
||||||
|
// 46 KB/s, and SBC at bitpool 53 / 48 kHz needs ~46 KB/s with its
|
||||||
|
// headers, so the stream fell behind whenever anything else crossed the
|
||||||
|
// link and the sink played concealment. Keep the stream within three
|
||||||
|
// quarters of the line rate by lowering the bitpool (any value in the
|
||||||
|
// negotiated range is valid per frame). Over USB nothing changes.
|
||||||
|
static void FitBitpoolToTransport(uint32_t sampleRate) {
|
||||||
|
uint32_t budget = Hci::TransportBytesPerSecond() / 4 * 3;
|
||||||
|
if (budget == 0) return;
|
||||||
|
uint32_t rate = sampleRate ? sampleRate : 48000;
|
||||||
|
uint8_t minBitpool = g_cfgSbc[2] < 2 ? 2 : g_cfgSbc[2];
|
||||||
|
uint8_t original = g_sbcEncoder.Bitpool;
|
||||||
|
auto bytesPerSecond = [&]() {
|
||||||
|
// H4 + ACL + L2CAP + RTP + SCMS-T + SBC headers (23 bytes) per
|
||||||
|
// packet, spread over the ~4-5 frames bundled into each.
|
||||||
|
uint32_t frameCost = Sbc::GetFrameSize(&g_sbcEncoder) + 6;
|
||||||
|
return (uint64_t)frameCost * rate / Sbc::GetSamplesPerFrame(&g_sbcEncoder);
|
||||||
|
};
|
||||||
|
while (g_sbcEncoder.Bitpool > minBitpool && bytesPerSecond() > budget) {
|
||||||
|
Sbc::Configure(&g_sbcEncoder, g_cfgSbc[0], g_cfgSbc[1],
|
||||||
|
(uint8_t)(g_sbcEncoder.Bitpool - 1));
|
||||||
|
}
|
||||||
|
if (g_sbcEncoder.Bitpool != original) {
|
||||||
|
KernelLogStream(INFO, "BT-A2DP") << "Bitpool " << (uint64_t)original << " -> "
|
||||||
|
<< (uint64_t)g_sbcEncoder.Bitpool << " to fit the transport ("
|
||||||
|
<< bytesPerSecond() << " of " << (uint64_t)Hci::TransportBytesPerSecond()
|
||||||
|
<< " bytes/s)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool ConfigureStream(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
|
bool ConfigureStream(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
|
||||||
// Encoder configuration and PumpMedia both mutate the SBC encoder.
|
// Encoder configuration and PumpMedia both mutate the SBC encoder.
|
||||||
// Device switching normally configures an Open stream, but explicitly
|
// Device switching normally configures an Open stream, but explicitly
|
||||||
@@ -1852,6 +1881,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
// agreed to (Init's defaults may differ from the negotiated subset).
|
// agreed to (Init's defaults may differ from the negotiated subset).
|
||||||
if (g_cfgSbc[0] || g_cfgSbc[1]) {
|
if (g_cfgSbc[0] || g_cfgSbc[1]) {
|
||||||
Sbc::Configure(&g_sbcEncoder, g_cfgSbc[0], g_cfgSbc[1], g_cfgSbc[3]);
|
Sbc::Configure(&g_sbcEncoder, g_cfgSbc[0], g_cfgSbc[1], g_cfgSbc[3]);
|
||||||
|
FitBitpoolToTransport(sampleRate);
|
||||||
}
|
}
|
||||||
g_sbcInitialized = true;
|
g_sbcInitialized = true;
|
||||||
g_seqNum = 0;
|
g_seqNum = 0;
|
||||||
@@ -2136,7 +2166,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
|
|
||||||
void ServiceMedia() {
|
void ServiceMedia() {
|
||||||
if (!AcquireMediaService()) return;
|
if (!AcquireMediaService()) return;
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
PumpMedia();
|
PumpMedia();
|
||||||
g_serviceActive.store(false, std::memory_order_release);
|
g_serviceActive.store(false, std::memory_order_release);
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::A2dp {
|
namespace Drivers::Bluetooth::A2dp {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// A2DP stream states
|
// A2DP stream states
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Avrcp {
|
namespace Drivers::Bluetooth::Avrcp {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Constants
|
// Constants
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Avrcp {
|
namespace Drivers::Bluetooth::Avrcp {
|
||||||
|
|
||||||
// Process an AVCTP packet arriving on an AVRCP control channel (PSM 0x0017).
|
// Process an AVCTP packet arriving on an AVRCP control channel (PSM 0x0017).
|
||||||
// Implements a minimal AVRCP Target: unit/subunit info, honest rejection of
|
// Implements a minimal AVRCP Target: unit/subunit info, honest rejection of
|
||||||
+55
-18
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Bluetooth.cpp
|
* Bluetooth.cpp
|
||||||
* Top-level Bluetooth subsystem — adapter registration and Intel BT initialization
|
* Top-level Bluetooth subsystem — adapter registration and controller bring-up
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "Hci.hpp"
|
#include "Hci.hpp"
|
||||||
#include "A2dp.hpp"
|
#include "A2dp.hpp"
|
||||||
#include "IntelFirmware.hpp"
|
#include "IntelFirmware.hpp"
|
||||||
|
#include "Broadcom.hpp"
|
||||||
#include <Drivers/Audio/Mixer.hpp>
|
#include <Drivers/Audio/Mixer.hpp>
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
#include <Drivers/USB/Xhci.hpp>
|
||||||
#include <Drivers/USB/UsbDevice.hpp>
|
#include <Drivers/USB/UsbDevice.hpp>
|
||||||
@@ -22,7 +23,9 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth {
|
namespace Drivers::Bluetooth {
|
||||||
|
|
||||||
|
namespace Xhci = Drivers::USB::Xhci;
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// State
|
// State
|
||||||
@@ -205,7 +208,9 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
void RegisterAdapter(uint8_t slotId) {
|
void RegisterAdapter(uint8_t slotId) {
|
||||||
if (g_initialized) {
|
// One adapter at a time, whichever came first (an on-board UART
|
||||||
|
// controller may still be finishing its deferred init).
|
||||||
|
if (g_initialized || Hci::GetTransport() != Hci::Transport::None) {
|
||||||
KernelLogStream(WARNING, "BT") << "Bluetooth adapter already registered";
|
KernelLogStream(WARNING, "BT") << "Bluetooth adapter already registered";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -249,17 +254,47 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
CompleteInit();
|
CompleteInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// ProbePlatformAdapter — an on-board UART controller ACPI describes
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool ProbePlatformAdapter() {
|
||||||
|
if (g_initialized || Hci::GetTransport() != Hci::Transport::None) return false;
|
||||||
|
if (!Broadcom::ProbePlatform()) return false;
|
||||||
|
|
||||||
|
// The firmware patch lives on the root filesystem, which the boot
|
||||||
|
// driver probe precedes; finish from the idle loop like USB adapters.
|
||||||
|
if (!Fs::Vfs::IsDriveRegistered(0)) {
|
||||||
|
g_initPending.store(true, std::memory_order_release);
|
||||||
|
KernelLogStream(INFO, "BT") << "Transport up; deferring init until the root is mounted";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
CompleteInit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// CompleteInit — firmware-dependent HCI bring-up (needs VFS/ramdisk)
|
// CompleteInit — firmware-dependent HCI bring-up (needs VFS/ramdisk)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
static void CompleteInit() {
|
static void CompleteInit() {
|
||||||
auto* dev = Xhci::GetDevice(g_slotId);
|
bool didReset = false;
|
||||||
if (!dev) return;
|
|
||||||
|
if (Hci::GetTransport() == Hci::Transport::Uart) {
|
||||||
|
// Power cycle, firmware patch and operating baud; ends with a reset.
|
||||||
|
if (!Broadcom::Initialize()) {
|
||||||
|
KernelLogStream(ERROR, "BT") << "Broadcom BT initialization failed";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
didReset = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* dev = Hci::GetTransport() == Hci::Transport::Usb ? Xhci::GetDevice(g_slotId) : nullptr;
|
||||||
|
if (Hci::GetTransport() == Hci::Transport::Usb && !dev) return;
|
||||||
|
|
||||||
// Intel-specific initialization (firmware download + HCI Reset)
|
// Intel-specific initialization (firmware download + HCI Reset)
|
||||||
bool didReset = false;
|
if (dev && IsIntelBt(dev->VendorId, dev->ProductId)) {
|
||||||
if (IsIntelBt(dev->VendorId, dev->ProductId)) {
|
Hci::SetVendor(Hci::Vendor::Intel);
|
||||||
if (InitIntelBluetooth()) {
|
if (InitIntelBluetooth()) {
|
||||||
didReset = true; // InitIntelBluetooth already sent HCI Reset
|
didReset = true; // InitIntelBluetooth already sent HCI Reset
|
||||||
} else {
|
} else {
|
||||||
@@ -280,9 +315,11 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply a persisted BD_ADDR override (0:/config/bluetooth.toml) now,
|
// A Broadcom part still on its chip-wide default address gets the
|
||||||
// after the last reset and before the address is read back, so the
|
// board's, then a persisted BD_ADDR override (0:/config/bluetooth.toml)
|
||||||
// whole bring-up below uses the overridden address.
|
// wins over both. Both after the last reset and before the address
|
||||||
|
// is read back, so the whole bring-up below uses the final address.
|
||||||
|
if (Hci::GetVendor() == Hci::Vendor::Broadcom) Broadcom::FixDefaultAddress();
|
||||||
ApplyConfiguredAddress();
|
ApplyConfiguredAddress();
|
||||||
|
|
||||||
// Read BD_ADDR
|
// Read BD_ADDR
|
||||||
@@ -387,7 +424,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
void ServiceDeferredInit() {
|
void ServiceDeferredInit() {
|
||||||
if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return;
|
if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return;
|
||||||
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk still not mounted
|
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk still not mounted
|
||||||
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
if (Hci::InPumpContext()) return; // never nest under the transport pump
|
||||||
|
|
||||||
// Claim the pending init (this runs from the idle loop; make sure only
|
// Claim the pending init (this runs from the idle loop; make sure only
|
||||||
// one pass performs the bring-up). The firmware download inside takes
|
// one pass performs the bring-up). The firmware download inside takes
|
||||||
@@ -425,12 +462,12 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
!g_initialized && Fs::Vfs::IsDriveRegistered(0)) {
|
!g_initialized && Fs::Vfs::IsDriveRegistered(0)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return g_initialized && Hci::HasPendingCommands();
|
return g_initialized && (Hci::HasPendingCommands() || Hci::TransportHasRxData());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServiceEvents() {
|
void ServiceEvents() {
|
||||||
if (!g_initialized) return;
|
if (!g_initialized) return;
|
||||||
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
if (Hci::InPumpContext()) return; // never nest under the transport pump
|
||||||
A2dp::ServiceMedia(); // reap events and feed queued media
|
A2dp::ServiceMedia(); // reap events and feed queued media
|
||||||
// DrainEvents may just have queued an accept/auth/encryption reply.
|
// DrainEvents may just have queued an accept/auth/encryption reply.
|
||||||
// Send it in this same service pass instead of adding a scheduler-turn
|
// Send it in this same service pass instead of adding a scheduler-turn
|
||||||
@@ -492,7 +529,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||||
bool anyActive = true;
|
bool anyActive = true;
|
||||||
while (anyActive && Timekeeping::GetMilliseconds() - t0 < 2000) {
|
while (anyActive && Timekeeping::GetMilliseconds() - t0 < 2000) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
anyActive = false;
|
anyActive = false;
|
||||||
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||||
@@ -548,7 +585,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Poll until inquiry completes or timeout
|
// Poll until inquiry completes or timeout
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Hci::IsInquiryActive() && (Timekeeping::GetMilliseconds() - start < timeoutMs)) {
|
while (Hci::IsInquiryActive() && (Timekeeping::GetMilliseconds() - start < timeoutMs)) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
|
|
||||||
for (int j = 0; j < 200; j++) {
|
for (int j = 0; j < 200; j++) {
|
||||||
@@ -566,7 +603,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
uint64_t cancelStart = Timekeeping::GetMilliseconds();
|
uint64_t cancelStart = Timekeeping::GetMilliseconds();
|
||||||
while (Hci::IsInquiryActive()
|
while (Hci::IsInquiryActive()
|
||||||
&& Timekeeping::GetMilliseconds() - cancelStart < 250) {
|
&& Timekeeping::GetMilliseconds() - cancelStart < 250) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
for (int j = 0; j < 100; j++)
|
for (int j = 0; j < 100; j++)
|
||||||
Arch::Pause();
|
Arch::Pause();
|
||||||
@@ -612,7 +649,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Wait for Connection Complete event
|
// Wait for Connection Complete event
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (!target && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (!target && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
target = FindAclConnection(bdAddr);
|
target = FindAclConnection(bdAddr);
|
||||||
for (int j = 0; j < 200; j++) {
|
for (int j = 0; j < 200; j++) {
|
||||||
@@ -631,7 +668,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
if (reusedAcl) Hci::AuthenticateLink(target->Handle);
|
if (reusedAcl) Hci::AuthenticateLink(target->Handle);
|
||||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
Hci::ProcessPendingCommands();
|
Hci::ProcessPendingCommands();
|
||||||
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) break;
|
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) break;
|
||||||
+16
-9
@@ -8,28 +8,35 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include "Hci.hpp"
|
#include "Hci.hpp"
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth {
|
namespace Drivers::Bluetooth {
|
||||||
|
|
||||||
// Called by USB enumeration when a Bluetooth adapter is detected
|
// Called by USB enumeration when a Bluetooth adapter is detected
|
||||||
void RegisterAdapter(uint8_t slotId);
|
void RegisterAdapter(uint8_t slotId);
|
||||||
|
|
||||||
|
// Called by the boot driver probe: bring up an on-board UART controller
|
||||||
|
// ACPI describes (the Raspberry Pi 4/400's Broadcom radio). Returns true
|
||||||
|
// if one was found; its firmware-dependent init is deferred like USB's.
|
||||||
|
bool ProbePlatformAdapter();
|
||||||
|
|
||||||
// Complete any firmware-dependent bring-up that was deferred because the
|
// Complete any firmware-dependent bring-up that was deferred because the
|
||||||
// adapter enumerated during the boot port scan, before the ramdisk (which
|
// adapter enumerated during the boot port scan, before the ramdisk (which
|
||||||
// holds the firmware images) was mounted. Safe to call unconditionally
|
// holds the firmware images) was mounted. Safe to call unconditionally
|
||||||
// once the boot filesystems are up; a no-op if nothing is pending.
|
// once the boot filesystems are up; a no-op if nothing is pending.
|
||||||
void ServiceDeferredInit();
|
void ServiceDeferredInit();
|
||||||
|
|
||||||
// Steady-state event service: drains the xHCI event ring + Bluetooth ACL
|
// Steady-state event service: drains the transport (xHCI event ring or
|
||||||
// RX ring and flushes queued HCI replies. Called from the idle loop so
|
// UART receive ring) + Bluetooth ACL RX ring and flushes queued HCI
|
||||||
|
// replies. Called from the idle loop so
|
||||||
// inbound traffic (the headset's SDP/AVRCP queries, AVDTP commands, ACL
|
// inbound traffic (the headset's SDP/AVRCP queries, AVDTP commands, ACL
|
||||||
// credits) is processed even when no syscall happens to be pumping a BT
|
// credits) is processed even when no syscall happens to be pumping a BT
|
||||||
// wait loop. Cheap no-op when the adapter is down; safe from any core
|
// wait loop. Cheap no-op when the adapter is down; safe from any core
|
||||||
// (PollEvents/DrainEvents/ProcessPendingCommands all self-serialize).
|
// (Pump/DrainEvents/ProcessPendingCommands all self-serialize).
|
||||||
void ServiceEvents();
|
void ServiceEvents();
|
||||||
|
|
||||||
// True when boot-deferred initialization or queued HCI control work needs
|
// True when boot-deferred initialization, queued HCI control work or
|
||||||
// an idle-context service pass. USB receive events are signaled separately
|
// received UART bytes need an idle-context service pass. USB receive
|
||||||
// by xHCI and cause the dispatcher to service Bluetooth in the same pass.
|
// events are signaled separately by xHCI and cause the dispatcher to
|
||||||
|
// service Bluetooth in the same pass.
|
||||||
bool HasDeferredWork();
|
bool HasDeferredWork();
|
||||||
|
|
||||||
// Query adapter state
|
// Query adapter state
|
||||||
@@ -40,8 +47,8 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
const uint8_t* GetBdAddr();
|
const uint8_t* GetBdAddr();
|
||||||
|
|
||||||
// Change the adapter's BD_ADDR at runtime. Drops any active links (their
|
// Change the adapter's BD_ADDR at runtime. Drops any active links (their
|
||||||
// pairing/crypto was bound to the old address), issues the Intel vendor
|
// pairing/crypto was bound to the old address), issues the vendor write
|
||||||
// write (0xFC31), re-reads the controller's address into the cache and
|
// (Intel 0xFC31, Broadcom 0xFC01), re-reads the controller's address into the cache and
|
||||||
// re-asserts scan. Returns true only if the controller reports the new
|
// re-asserts scan. Returns true only if the controller reports the new
|
||||||
// address. Persisting the choice to 0:/config/bluetooth.toml is the
|
// address. Persisting the choice to 0:/config/bluetooth.toml is the
|
||||||
// caller's job (userspace); the override is re-applied on boot by
|
// caller's job (userspace); the override is re-applied on boot by
|
||||||
@@ -0,0 +1,555 @@
|
|||||||
|
/*
|
||||||
|
* Broadcom.cpp
|
||||||
|
* Broadcom/Cypress UART Bluetooth controller bring-up (Raspberry Pi 4/400)
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Broadcom.hpp"
|
||||||
|
#include "Hci.hpp"
|
||||||
|
#include <ACPI/AcpiDevices.hpp>
|
||||||
|
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||||
|
#include <Arch/Cpu.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Drivers/Platform/RpiFirmware.hpp>
|
||||||
|
#include <Drivers/Platform/RpiGpio.hpp>
|
||||||
|
#include <Drivers/Serial/BtUart.hpp>
|
||||||
|
#include <Drivers/Serial/Uart.hpp>
|
||||||
|
#include <Fs/Vfs.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
#include <Memory/Heap.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <Timekeeping/Timer.hpp>
|
||||||
|
|
||||||
|
// The Raspberry Pi 4B (CYW43455) and 400 (CYW43456) carry a Cypress radio
|
||||||
|
// whose Bluetooth half speaks HCI over a UART (H4) on GPIO 30-33: CTS, RTS,
|
||||||
|
// TXD, RXD. Its power switch, BT_ON, is line 0 of the VideoCore firmware's
|
||||||
|
// GPIO expander.
|
||||||
|
//
|
||||||
|
// Bring-up follows Linux's hci_bcm + btbcm:
|
||||||
|
// 1. power on, wait ~100 ms, HCI Reset at the controller's 115200 baud;
|
||||||
|
// 2. Read Local Version: the LMP subversion names the chip and so the
|
||||||
|
// patch file (BCM4345C0.hcd, BCM4345C5.hcd);
|
||||||
|
// 3. raise both ends to the operating speed (vendor 0xFC18, answered at
|
||||||
|
// the old speed);
|
||||||
|
// 4. Download Minidriver (0xFC2E), wait 50 ms, then send every record of
|
||||||
|
// the .hcd file -- each one is a complete HCI command, Write RAM
|
||||||
|
// (0xFC4C) and finally Launch RAM (0xFC4E) -- and wait 250 ms;
|
||||||
|
// 5. the patched firmware restarts at 115200: HCI Reset there, and raise
|
||||||
|
// the speed again.
|
||||||
|
//
|
||||||
|
// Raspberry Pi OS's btuart runs the controller at 460800 without flow
|
||||||
|
// control on the mini UART and at 3 Mbaud with it on the PL011. 460800 is
|
||||||
|
// too slow for A2DP here: 8N1 carries 46 KB/s and SBC at bitpool 53 needs
|
||||||
|
// ~46 KB/s with its headers, so music broke up whenever anything else used
|
||||||
|
// the link. The mini UART's auto RTS/CTS allows 921600, so it is tried
|
||||||
|
// first; without flow control (460800) is the fallback, and A2DP lowers its
|
||||||
|
// bitpool to fit (Hci::TransportBytesPerSecond).
|
||||||
|
//
|
||||||
|
// Our RTS must be deasserted while the controller powers on: with it
|
||||||
|
// asserted the controller boots into autobaud mode (see PowerCycle).
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Drivers::Bluetooth::Broadcom {
|
||||||
|
|
||||||
|
namespace BtUart = Drivers::Serial::BtUart;
|
||||||
|
namespace RpiFirmware = Drivers::Platform::RpiFirmware;
|
||||||
|
namespace RpiGpio = Drivers::Platform::RpiGpio;
|
||||||
|
|
||||||
|
// GPIO 30-33 carry CTS, RTS, TXD, RXD: alternate function 3 routes them
|
||||||
|
// to the PL011, 5 to the mini UART. Pulls as Linux's bcm2711 pinctrl
|
||||||
|
// sets them (uart0_ctsrts_gpio30, uart0_gpio32).
|
||||||
|
static constexpr uint32_t PIN_CTS = 30;
|
||||||
|
static constexpr uint32_t PIN_RTS = 31;
|
||||||
|
static constexpr uint32_t PIN_TXD = 32;
|
||||||
|
static constexpr uint32_t PIN_RXD = 33;
|
||||||
|
|
||||||
|
static constexpr uint32_t InitialBaud = 115200;
|
||||||
|
|
||||||
|
// LMP subversions of the ROM firmware -> patch file (Linux btbcm's
|
||||||
|
// bcm_uart_subver_table).
|
||||||
|
struct ChipName {
|
||||||
|
uint16_t subversion;
|
||||||
|
const char* name;
|
||||||
|
};
|
||||||
|
static constexpr ChipName g_chips[] = {
|
||||||
|
{ 0x6119, "BCM4345C0" }, // CYW43455: Raspberry Pi 3B+, 4B
|
||||||
|
{ 0x6606, "BCM4345C5" }, // CYW43456: Raspberry Pi 400, CM4
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool g_flowControl = false;
|
||||||
|
static bool g_powerSwitch = true;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Platform probe
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static bool EndsWith(const char* s, const char* suffix) {
|
||||||
|
int sl = 0, xl = 0;
|
||||||
|
while (s[sl]) sl++;
|
||||||
|
while (suffix[xl]) xl++;
|
||||||
|
if (xl > sl) return false;
|
||||||
|
for (int i = 0; i < xl; i++) {
|
||||||
|
if (s[sl - xl + i] != suffix[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The controller's UartSerialBusV2 descriptor (ACPI 6.4, 6.4.3.8.2.3)
|
||||||
|
// names the UART it hangs off in its ResourceSource string.
|
||||||
|
static bool ReadUartSource(int32_t node, char* source, int maxLength) {
|
||||||
|
Hal::AML::Object crs;
|
||||||
|
if (!Hal::AML::GetInterpreter().EvaluateChild(node, "_CRS", nullptr, 0, crs) ||
|
||||||
|
crs.Type != Hal::AML::ObjectType::Buffer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const uint8_t* data = crs.Buffer.Data;
|
||||||
|
uint32_t length = crs.Buffer.Length;
|
||||||
|
uint32_t pos = 0;
|
||||||
|
while (pos < length) {
|
||||||
|
uint8_t tag = data[pos];
|
||||||
|
if (!(tag & 0x80)) {
|
||||||
|
if ((tag & 0x78) == 0x78) break; // end tag
|
||||||
|
pos += 1 + (tag & 0x07);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pos + 3 > length) break;
|
||||||
|
uint32_t descLength = (uint32_t)data[pos + 1] | ((uint32_t)data[pos + 2] << 8);
|
||||||
|
uint32_t end = pos + 3 + descLength;
|
||||||
|
if (end > length) break;
|
||||||
|
static constexpr uint8_t SerialBusTag = 0x8E, SerialBusUart = 3;
|
||||||
|
if (tag == SerialBusTag && descLength >= 9 && data[pos + 5] == SerialBusUart) {
|
||||||
|
uint32_t typeDataLength = (uint32_t)data[pos + 10] | ((uint32_t)data[pos + 11] << 8);
|
||||||
|
uint32_t s = pos + 12 + typeDataLength;
|
||||||
|
int n = 0;
|
||||||
|
while (s < end && data[s] != 0 && n < maxLength - 1) source[n++] = (char)data[s++];
|
||||||
|
source[n] = '\0';
|
||||||
|
return n > 0;
|
||||||
|
}
|
||||||
|
pos = end;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ReadInteger(int32_t node, const char* name, uint64_t& value) {
|
||||||
|
Hal::AML::Object obj;
|
||||||
|
if (!Hal::AML::GetInterpreter().EvaluateChild(node, name, nullptr, 0, obj) ||
|
||||||
|
obj.Type != Hal::AML::ObjectType::Integer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = obj.Integer;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProbePlatform() {
|
||||||
|
Hal::AcpiDevices::DeviceInfo bt{};
|
||||||
|
if (!Hal::AcpiDevices::FindDeviceByHid("BCM2EA6", bt)) return false;
|
||||||
|
|
||||||
|
// Which UART: the descriptor's source, else the firmware's "UART in
|
||||||
|
// use" flag (0: the PL011 is the console, Bluetooth has the mini UART).
|
||||||
|
BtUart::Kind kind = BtUart::Kind::MiniUart;
|
||||||
|
char source[64] = {};
|
||||||
|
uint64_t uartInUse = 0;
|
||||||
|
if (ReadUartSource(bt.NodeIndex, source, sizeof(source))) {
|
||||||
|
kind = EndsWith(source, "URT0") ? BtUart::Kind::Pl011 : BtUart::Kind::MiniUart;
|
||||||
|
} else if (ReadInteger(bt.NodeIndex, "URIU", uartInUse)) {
|
||||||
|
kind = uartInUse == 0 ? BtUart::Kind::MiniUart : BtUart::Kind::Pl011;
|
||||||
|
}
|
||||||
|
bool mini = kind == BtUart::Kind::MiniUart;
|
||||||
|
|
||||||
|
Hal::AcpiDevices::DeviceInfo uart{};
|
||||||
|
if (!Hal::AcpiDevices::FindDeviceByHid(mini ? "BCM2836" : "BCM2837", uart)) {
|
||||||
|
KernelLogStream(WARNING, "BT-BCM") << bt.Path << ": its UART "
|
||||||
|
<< (source[0] ? source : (mini ? "(mini UART)" : "(PL011)")) << " is not in ACPI";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Hal::AML::ResourceList resources;
|
||||||
|
uint64_t base = 0, length = 0;
|
||||||
|
if (!Hal::AcpiDevices::EvaluateCrs(uart.NodeIndex, resources) ||
|
||||||
|
!resources.GetMemory(base, length) || base == 0) {
|
||||||
|
KernelLogStream(WARNING, "BT-BCM") << uart.Path << ": no register block in _CRS";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t console = Drivers::Serial::ConsoleAddress();
|
||||||
|
if (console != 0 && (console & ~0xFFFULL) == (base & ~0xFFFULL)) {
|
||||||
|
KernelLogStream(WARNING, "BT-BCM") << "Bluetooth's UART " << uart.Path
|
||||||
|
<< " is the serial console; leaving Bluetooth off";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BtUart::Config config{};
|
||||||
|
config.kind = kind;
|
||||||
|
config.mmioPhys = base;
|
||||||
|
config.levelTriggered = true;
|
||||||
|
if (auto* irq = resources.FindFirst(Hal::AML::ResourceType::ExtendedIrq)) {
|
||||||
|
config.gsi = irq->ExtendedIrq.Interrupt;
|
||||||
|
config.levelTriggered = (irq->ExtendedIrq.Flags & 1) == 0;
|
||||||
|
config.activeLow = (irq->ExtendedIrq.Flags & 2) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mini UART's baud generator runs off the VPU core clock: ask the
|
||||||
|
// firmware for its current rate, else take the rate the firmware
|
||||||
|
// patched into the table (MUCR; the placeholder 0x1000000 means it
|
||||||
|
// did not). The PL011's reference clock is fixed (CLCK, 48 MHz).
|
||||||
|
uint64_t tableClock = 0;
|
||||||
|
uint32_t clockHz = 0;
|
||||||
|
const char* clockSource = "firmware";
|
||||||
|
if (mini) {
|
||||||
|
if (!RpiFirmware::GetClockRate(RpiFirmware::CLOCK_CORE, clockHz)) {
|
||||||
|
if (ReadInteger(uart.NodeIndex, "MUCR", tableClock) && tableClock > 0x1000000) {
|
||||||
|
clockHz = (uint32_t)tableClock;
|
||||||
|
clockSource = "ACPI MUCR";
|
||||||
|
} else {
|
||||||
|
clockHz = 500000000;
|
||||||
|
clockSource = "assumed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clockSource = "PL011 reference";
|
||||||
|
clockHz = ReadInteger(uart.NodeIndex, "CLCK", tableClock) && tableClock != 0
|
||||||
|
? (uint32_t)tableClock : 48000000;
|
||||||
|
}
|
||||||
|
config.clockHz = clockHz;
|
||||||
|
|
||||||
|
RpiGpio::Function function = mini ? RpiGpio::Function::Alt5 : RpiGpio::Function::Alt3;
|
||||||
|
bool pinsOk = RpiGpio::SetFunction(PIN_CTS, function)
|
||||||
|
&& RpiGpio::SetFunction(PIN_RTS, function)
|
||||||
|
&& RpiGpio::SetFunction(PIN_TXD, function)
|
||||||
|
&& RpiGpio::SetFunction(PIN_RXD, function)
|
||||||
|
&& RpiGpio::SetPull(PIN_CTS, RpiGpio::Pull::Up)
|
||||||
|
&& RpiGpio::SetPull(PIN_RTS, RpiGpio::Pull::None)
|
||||||
|
&& RpiGpio::SetPull(PIN_TXD, RpiGpio::Pull::None)
|
||||||
|
&& RpiGpio::SetPull(PIN_RXD, RpiGpio::Pull::Up);
|
||||||
|
if (!pinsOk) {
|
||||||
|
KernelLogStream(WARNING, "BT-BCM")
|
||||||
|
<< "No GPIO controller (BCM2845): UART pins left as the firmware set them";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BtUart::Initialize(config)) return false;
|
||||||
|
|
||||||
|
Hci::InitializeUart();
|
||||||
|
Hci::SetVendor(Hci::Vendor::Broadcom);
|
||||||
|
KernelLogStream(OK, "BT-BCM") << "Bluetooth controller " << bt.Path << " on "
|
||||||
|
<< uart.Path << " (GSI " << (uint64_t)config.gsi << ", clock " << clockSource << ")";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Controller bring-up
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// BT_ON through the firmware's expander. The line has to be an output
|
||||||
|
// first (hci_bcm's GPIOD_OUT_LOW request does that on Linux); plain
|
||||||
|
// SET_GPIO_STATE is the fallback for firmware without the config tag.
|
||||||
|
static bool SetBtOn(bool on) {
|
||||||
|
return RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_BT_ON, on)
|
||||||
|
|| RpiFirmware::SetGpioState(RpiFirmware::EXPGPIO_BT_ON, on);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The controller samples its CTS (our RTS) as it leaves reset. Asserted,
|
||||||
|
// it boots into autobaud mode, where it answers HCI Reset and silently
|
||||||
|
// drops nearly everything else (on a Pi 400: Read Local Version, Read
|
||||||
|
// PIN Type, Write PIN Type, at any baud rate). hci_bcm deasserts it
|
||||||
|
// around power-on for the same reason; SyncAtInitialBaud asserts it
|
||||||
|
// again once the controller is up.
|
||||||
|
static void PowerCycle() {
|
||||||
|
if (g_powerSwitch) {
|
||||||
|
BtUart::DeassertRts();
|
||||||
|
if (SetBtOn(false)) {
|
||||||
|
Timekeeping::BusyWaitMs(20);
|
||||||
|
bool on = SetBtOn(true);
|
||||||
|
bool state = false;
|
||||||
|
bool readBack = RpiFirmware::GetGpioState(RpiFirmware::EXPGPIO_BT_ON, state);
|
||||||
|
KernelLogStream(on ? INFO : WARNING, "BT-BCM") << "BT_ON "
|
||||||
|
<< (on ? "switched on" : "did not switch on") << " (reads back "
|
||||||
|
<< (readBack ? (state ? "high" : "low") : "unknown") << ")";
|
||||||
|
// hci_bcm allows 100-120 ms to power on and leave reset.
|
||||||
|
Timekeeping::BusyWaitMs(150);
|
||||||
|
} else {
|
||||||
|
g_powerSwitch = false;
|
||||||
|
KernelLogStream(WARNING, "BT-BCM")
|
||||||
|
<< "Cannot switch BT_ON through the firmware; assuming the controller is powered";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Hci::ResetUartReceiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether received bytes move by interrupt and whether any were lost:
|
||||||
|
// the first thing to know when the controller seems not to answer.
|
||||||
|
static void LogUartStats(const char* when) {
|
||||||
|
BtUart::Stats stats{};
|
||||||
|
BtUart::GetStats(stats);
|
||||||
|
KernelLogStream(INFO, "BT-BCM") << when << ": UART rx " << stats.rxBytes << " bytes, tx "
|
||||||
|
<< stats.txBytes << ", " << stats.interrupts << " interrupts, " << stats.overruns
|
||||||
|
<< " overruns, " << stats.rxDropped << " dropped, " << (uint64_t)BtUart::GetBaud()
|
||||||
|
<< " baud, " << (BtUart::FlowControl() ? "RTS flow control" : "no flow control");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ResetController(uint32_t timeoutMs) {
|
||||||
|
if (!Hci::SendCommand(Hci::OP_RESET, nullptr, 0)) return false;
|
||||||
|
if (!Hci::WaitCommandComplete(Hci::OP_RESET, nullptr, 0, timeoutMs)) return false;
|
||||||
|
// The controller acknowledges the reset before it has finished it
|
||||||
|
// and ignores commands until then; btbcm_reset waits 100 ms.
|
||||||
|
Timekeeping::BusyWaitMs(100);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reach the controller at its power-on 115200 baud and read its ROM
|
||||||
|
// version. A flow-control mode counts only once a second command has
|
||||||
|
// been answered too: a controller in autobaud mode answers HCI Reset
|
||||||
|
// alone, and a flow-control polarity that holds either side back fails
|
||||||
|
// here rather than later.
|
||||||
|
static bool SyncAtInitialBaud(Hci::LocalVersion& rom) {
|
||||||
|
BtUart::SetBaud(InitialBaud);
|
||||||
|
static constexpr bool attempts[] = { true, true, false, false };
|
||||||
|
for (bool flow : attempts) {
|
||||||
|
BtUart::SetFlowControl(flow);
|
||||||
|
// Events the controller held back while it saw RTS deasserted
|
||||||
|
// pour out once it is asserted: let them arrive and drop them,
|
||||||
|
// so they neither overrun the FIFO under the next reply nor
|
||||||
|
// leave the deframer mid-packet.
|
||||||
|
Timekeeping::BusyWaitMs(50);
|
||||||
|
Hci::ResetUartReceiver();
|
||||||
|
if (!ResetController(1000)) {
|
||||||
|
LogUartStats(flow ? "No reset reply with flow control" : "No reset reply");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A controller slow to finish its reset gets a second chance.
|
||||||
|
bool answered = Hci::ReadLocalVersion(&rom);
|
||||||
|
if (!answered) {
|
||||||
|
Timekeeping::BusyWaitMs(200);
|
||||||
|
answered = Hci::ReadLocalVersion(&rom);
|
||||||
|
}
|
||||||
|
if (answered) {
|
||||||
|
g_flowControl = flow;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
LogUartStats(flow ? "Reset answered but not Read Local Version, with flow control"
|
||||||
|
: "Reset answered but not Read Local Version");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t PickOperatingBaud() {
|
||||||
|
uint32_t wanted;
|
||||||
|
if (BtUart::GetKind() == BtUart::Kind::Pl011) {
|
||||||
|
wanted = g_flowControl ? 3000000 : 921600;
|
||||||
|
} else if (!BtUart::HasInterrupt()) {
|
||||||
|
// Polled 8-byte FIFO: stay where nothing overruns.
|
||||||
|
return InitialBaud;
|
||||||
|
} else {
|
||||||
|
wanted = g_flowControl ? 921600 : 460800;
|
||||||
|
}
|
||||||
|
static constexpr uint32_t rates[] = { 3000000, 2000000, 921600, 460800, 230400 };
|
||||||
|
for (uint32_t rate : rates) {
|
||||||
|
if (rate <= wanted && BtUart::BaudReachable(rate)) return rate;
|
||||||
|
}
|
||||||
|
return InitialBaud;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both ends to `baud`. The controller answers 0xFC18 at the old speed,
|
||||||
|
// then switches. False leaves the host at its old speed; the controller
|
||||||
|
// may or may not have switched (the caller power-cycles to be sure).
|
||||||
|
static bool SwitchBaud(uint32_t baud) {
|
||||||
|
uint32_t oldBaud = BtUart::GetBaud();
|
||||||
|
uint8_t params[6] = {
|
||||||
|
0x00, 0x00,
|
||||||
|
(uint8_t)baud, (uint8_t)(baud >> 8), (uint8_t)(baud >> 16), (uint8_t)(baud >> 24),
|
||||||
|
};
|
||||||
|
if (!Hci::SendCommand(Hci::OP_BCM_UPDATE_BAUD_RATE, params, sizeof(params)) ||
|
||||||
|
!Hci::WaitCommandComplete(Hci::OP_BCM_UPDATE_BAUD_RATE, nullptr, 0, 1000)) {
|
||||||
|
KernelLogStream(WARNING, "BT-BCM") << "Controller refused " << (uint64_t)baud << " baud";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!BtUart::SetBaud(baud)) return false;
|
||||||
|
Timekeeping::BusyWaitMs(10);
|
||||||
|
Hci::ResetUartReceiver();
|
||||||
|
|
||||||
|
Hci::LocalVersion ver{};
|
||||||
|
if (Hci::ReadLocalVersion(&ver)) return true;
|
||||||
|
|
||||||
|
KernelLogStream(WARNING, "BT-BCM") << "No answer at " << (uint64_t)baud << " baud";
|
||||||
|
BtUart::SetBaud(oldBaud);
|
||||||
|
Hci::ResetUartReceiver();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* ChipFor(uint16_t subversion) {
|
||||||
|
for (const auto& chip : g_chips) {
|
||||||
|
if (chip.subversion == subversion) return chip.name;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 0:/os/firmware/brcm/<chip>.hcd. Returns nullptr (and logs) when
|
||||||
|
// the file is missing; the caller frees the image.
|
||||||
|
static uint8_t* LoadPatch(const char* chip, uint64_t& size) {
|
||||||
|
char path[64];
|
||||||
|
const char* prefix = "0:/os/firmware/brcm/";
|
||||||
|
int n = 0;
|
||||||
|
for (const char* p = prefix; *p; p++) path[n++] = *p;
|
||||||
|
for (const char* p = chip; *p && n < 56; p++) path[n++] = *p;
|
||||||
|
for (const char* p = ".hcd"; *p; p++) path[n++] = *p;
|
||||||
|
path[n] = '\0';
|
||||||
|
|
||||||
|
Fs::Vfs::BackendFile file;
|
||||||
|
if (Fs::Vfs::OpenBackendFile(path, file) < 0) {
|
||||||
|
KernelLogStream(WARNING, "BT-BCM") << "Firmware patch not found: " << path;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
size = Fs::Vfs::GetBackendFileSize(file);
|
||||||
|
uint8_t* image = size ? (uint8_t*)Memory::g_heap->Request(size) : nullptr;
|
||||||
|
if (image == nullptr) {
|
||||||
|
Fs::Vfs::CloseBackendFile(file);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
int got = Fs::Vfs::ReadBackendFile(file, image, 0, size);
|
||||||
|
Fs::Vfs::CloseBackendFile(file);
|
||||||
|
if (got != (int)size) {
|
||||||
|
Memory::g_heap->Free(image);
|
||||||
|
KernelLogStream(ERROR, "BT-BCM") << "Short read of " << path;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
KernelLogStream(INFO, "BT-BCM") << "Loading " << path << " (" << size << " bytes)";
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool DownloadPatch(const uint8_t* image, uint64_t size) {
|
||||||
|
if (!Hci::SendCommand(Hci::OP_BCM_DOWNLOAD_MINIDRV, nullptr, 0) ||
|
||||||
|
!Hci::WaitCommandComplete(Hci::OP_BCM_DOWNLOAD_MINIDRV, nullptr, 0, 2000)) {
|
||||||
|
KernelLogStream(ERROR, "BT-BCM") << "Download Minidriver failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Timekeeping::BusyWaitMs(50);
|
||||||
|
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
uint64_t off = 0;
|
||||||
|
uint32_t records = 0;
|
||||||
|
while (off + 3 <= size) {
|
||||||
|
uint16_t opcode = (uint16_t)image[off] | ((uint16_t)image[off + 1] << 8);
|
||||||
|
uint8_t paramLength = image[off + 2];
|
||||||
|
off += 3;
|
||||||
|
if (off + paramLength > size) {
|
||||||
|
KernelLogStream(ERROR, "BT-BCM") << "Truncated patch record " << (uint64_t)records;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Hci::SendCommand(opcode, image + off, paramLength)) return false;
|
||||||
|
bool acked = Hci::WaitCommandComplete(opcode, nullptr, 0, 2000);
|
||||||
|
if (!acked && opcode != Hci::OP_BCM_LAUNCH_RAM) {
|
||||||
|
KernelLogStream(ERROR, "BT-BCM") << "Patch record " << (uint64_t)records
|
||||||
|
<< " (opcode " << base::hex << (uint64_t)opcode << base::dec << ") failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
off += paramLength;
|
||||||
|
records++;
|
||||||
|
}
|
||||||
|
// The patched firmware starts after Launch RAM; btbcm waits 250 ms.
|
||||||
|
Timekeeping::BusyWaitMs(250);
|
||||||
|
KernelLogStream(OK, "BT-BCM") << "Patch downloaded: " << (uint64_t)records << " records in "
|
||||||
|
<< (Timekeeping::GetMilliseconds() - start) << " ms at "
|
||||||
|
<< (uint64_t)BtUart::GetBaud() << " baud";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void LogVersion(const char* chip, const Hci::LocalVersion& ver) {
|
||||||
|
uint16_t sub = ver.LmpSubversion;
|
||||||
|
KernelLogStream(OK, "BT-BCM") << (chip ? chip : "Broadcom controller") << " ("
|
||||||
|
<< (uint64_t)((sub & 0xE000) >> 13) << "." << (uint64_t)((sub & 0x1F00) >> 8)
|
||||||
|
<< "." << (uint64_t)(sub & 0x00FF) << ") build " << (uint64_t)(ver.HciRevision & 0x0FFF)
|
||||||
|
<< ", " << (uint64_t)BtUart::GetBaud() << " baud, "
|
||||||
|
<< (g_flowControl ? "RTS flow control" : "no flow control");
|
||||||
|
}
|
||||||
|
|
||||||
|
// One complete bring-up. `fast` raises the speed for the download and
|
||||||
|
// afterwards; without it everything stays at 115200.
|
||||||
|
static bool Attempt(bool fast) {
|
||||||
|
PowerCycle();
|
||||||
|
Hci::LocalVersion rom{};
|
||||||
|
if (!SyncAtInitialBaud(rom)) {
|
||||||
|
KernelLogStream(ERROR, "BT-BCM") << "Controller does not answer HCI commands"
|
||||||
|
<< (g_powerSwitch ? "" : " (BT_ON was never switched on: it is likely unpowered)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const char* chip = ChipFor(rom.LmpSubversion);
|
||||||
|
KernelLogStream(INFO, "BT-BCM") << "ROM firmware: LMP subversion " << base::hex
|
||||||
|
<< (uint64_t)rom.LmpSubversion << ", HCI revision " << (uint64_t)rom.HciRevision
|
||||||
|
<< base::dec << (chip ? " -> " : " (unknown chip)") << (chip ? chip : "");
|
||||||
|
|
||||||
|
uint32_t operating = fast ? PickOperatingBaud() : InitialBaud;
|
||||||
|
if (operating != InitialBaud && !SwitchBaud(operating)) return false;
|
||||||
|
|
||||||
|
uint64_t size = 0;
|
||||||
|
uint8_t* image = chip ? LoadPatch(chip, size) : nullptr;
|
||||||
|
if (image != nullptr) {
|
||||||
|
bool ok = DownloadPatch(image, size);
|
||||||
|
Memory::g_heap->Free(image);
|
||||||
|
if (!ok) return false;
|
||||||
|
|
||||||
|
// The patched firmware comes up at 115200 again.
|
||||||
|
BtUart::SetBaud(InitialBaud);
|
||||||
|
Hci::ResetUartReceiver();
|
||||||
|
bool reset = false;
|
||||||
|
for (int i = 0; i < 3 && !reset; i++) reset = ResetController(1000);
|
||||||
|
if (!reset) {
|
||||||
|
KernelLogStream(ERROR, "BT-BCM") << "Patched firmware does not answer HCI Reset";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (operating != InitialBaud && !SwitchBaud(operating)) return false;
|
||||||
|
} else {
|
||||||
|
KernelLogStream(WARNING, "BT-BCM") << "Running the controller's ROM firmware";
|
||||||
|
}
|
||||||
|
|
||||||
|
Hci::LocalVersion ver{};
|
||||||
|
if (Hci::ReadLocalVersion(&ver)) LogVersion(chip, ver);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Initialize() {
|
||||||
|
if (Hci::GetTransport() != Hci::Transport::Uart) return false;
|
||||||
|
if (Attempt(true)) return true;
|
||||||
|
KernelLogStream(WARNING, "BT-BCM") << "Retrying bring-up at 115200 baud";
|
||||||
|
return Attempt(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// BD_ADDR
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Addresses are in HCI order (addr[0] = least significant octet).
|
||||||
|
static bool IsDefaultAddress(const uint8_t* addr) {
|
||||||
|
bool allZero = true, allOnes = true;
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
if (addr[i] != 0x00) allZero = false;
|
||||||
|
if (addr[i] != 0xFF) allOnes = false;
|
||||||
|
}
|
||||||
|
if (allZero || allOnes) return true;
|
||||||
|
// The chip-wide defaults Linux's btbcm lists for these parts (the
|
||||||
|
// BCM4345C5's is 43:45:C5:00:1F:AC) start with 0x43, which has the
|
||||||
|
// IEEE group bit set: no assigned device address does.
|
||||||
|
return (addr[5] & 0x01) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FixDefaultAddress() {
|
||||||
|
uint8_t addr[6];
|
||||||
|
if (!Hci::ReadBdAddr(addr) || !IsDefaultAddress(addr)) return;
|
||||||
|
|
||||||
|
uint8_t mac[6];
|
||||||
|
if (!RpiFirmware::GetBoardMacAddress(mac)) {
|
||||||
|
KernelLogStream(WARNING, "BT-BCM")
|
||||||
|
<< "Controller has its chip-wide default BD_ADDR and no board MAC is available";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The board's Ethernet MAC is unique to it and in the Raspberry Pi's
|
||||||
|
// own address block.
|
||||||
|
uint8_t board[6];
|
||||||
|
for (int i = 0; i < 6; i++) board[i] = mac[5 - i];
|
||||||
|
if (Hci::SetBdAddr(board)) {
|
||||||
|
KernelLogStream(OK, "BT-BCM") << "Replaced the chip-wide default BD_ADDR with the board's";
|
||||||
|
} else {
|
||||||
|
KernelLogStream(WARNING, "BT-BCM") << "Controller rejected the board BD_ADDR";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Broadcom.hpp
|
||||||
|
* Broadcom/Cypress UART Bluetooth controller bring-up (Raspberry Pi 4/400)
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Drivers::Bluetooth::Broadcom {
|
||||||
|
|
||||||
|
// Find the on-board controller ACPI describes (BCM2EA6, \_SB.GDV0.BTH0),
|
||||||
|
// route its UART to GPIO 30-33 and start the UART and the H4 HCI
|
||||||
|
// transport. Needs no firmware files, so it runs during the boot driver
|
||||||
|
// probe. Returns false when there is no such controller.
|
||||||
|
bool ProbePlatform();
|
||||||
|
|
||||||
|
// Power-cycle the controller, load its firmware patch from
|
||||||
|
// 0:/os/firmware/brcm/ and raise the UART to its operating speed.
|
||||||
|
// Leaves the controller freshly reset and ready for standard HCI.
|
||||||
|
bool Initialize();
|
||||||
|
|
||||||
|
// After the last HCI Reset: give a controller still on its chip-wide
|
||||||
|
// default BD_ADDR (the CYW43456 ships with one) a board-unique address
|
||||||
|
// from the Pi's Ethernet MAC.
|
||||||
|
void FixDefaultAddress();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Hci.cpp
|
* Hci.cpp
|
||||||
* Bluetooth HCI transport over USB
|
* Bluetooth HCI transport over USB or a UART (H4)
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "A2dp.hpp"
|
#include "A2dp.hpp"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <Fs/Vfs.hpp>
|
#include <Fs/Vfs.hpp>
|
||||||
|
#include <Drivers/Serial/BtUart.hpp>
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
#include <Drivers/USB/Xhci.hpp>
|
||||||
#include <Drivers/USB/UsbDevice.hpp>
|
#include <Drivers/USB/UsbDevice.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
@@ -22,12 +23,17 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Hci {
|
namespace Drivers::Bluetooth::Hci {
|
||||||
|
|
||||||
|
namespace Xhci = Drivers::USB::Xhci;
|
||||||
|
namespace BtUart = Drivers::Serial::BtUart;
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// State
|
// State
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
static Transport g_transport = Transport::None;
|
||||||
|
static Vendor g_vendor = Vendor::Unknown;
|
||||||
static uint8_t g_slotId = 0;
|
static uint8_t g_slotId = 0;
|
||||||
static bool g_initialized = false;
|
static bool g_initialized = false;
|
||||||
|
|
||||||
@@ -541,6 +547,161 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// UART (H4) receive deframing
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// The UART delivers one byte stream: [type][header][payload]... Pump()
|
||||||
|
// feeds it here in process context, so complete packets go straight to
|
||||||
|
// the same command mailbox / async event ring / ACL ring the USB
|
||||||
|
// callback fills. A byte that cannot start a packet is dropped, which
|
||||||
|
// resynchronizes after line noise (e.g. while the controller powers up).
|
||||||
|
enum class H4State : uint8_t { Type, Header, Payload, Discard };
|
||||||
|
|
||||||
|
static H4State g_h4State = H4State::Type;
|
||||||
|
static uint8_t g_h4Type = 0;
|
||||||
|
static uint8_t g_h4Packet[ACL_RX_SLOT_SIZE] = {};
|
||||||
|
static uint16_t g_h4Len = 0; // bytes in g_h4Packet
|
||||||
|
static uint16_t g_h4HeaderLen = 0;
|
||||||
|
static uint16_t g_h4Expected = 0; // header + payload
|
||||||
|
static uint32_t g_h4Remaining = 0; // Discard: bytes left to skip
|
||||||
|
static uint32_t g_h4Dropped = 0; // bytes that could not start a packet
|
||||||
|
|
||||||
|
// The UART receive ring and the deframer have one consumer at a time:
|
||||||
|
// whoever holds this (Pump, or a flush).
|
||||||
|
static std::atomic<bool> g_uartConsumer{false};
|
||||||
|
|
||||||
|
void ResetUartReceiver() {
|
||||||
|
bool expected = false;
|
||||||
|
while (!g_uartConsumer.compare_exchange_weak(expected, true,
|
||||||
|
std::memory_order_acquire)) {
|
||||||
|
expected = false;
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
BtUart::FlushRx();
|
||||||
|
g_h4State = H4State::Type;
|
||||||
|
g_h4Len = 0;
|
||||||
|
g_h4Remaining = 0;
|
||||||
|
g_uartConsumer.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CompleteH4Packet() {
|
||||||
|
if (g_h4Type == H4_EVENT) {
|
||||||
|
QueueCompleteHciEvent(g_h4Packet, g_h4Len);
|
||||||
|
} else if (g_h4Type == H4_ACL) {
|
||||||
|
QueueCompleteAclPacket(g_h4Packet, g_h4Len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FeedH4Bytes(const uint8_t* data, uint32_t length) {
|
||||||
|
uint32_t off = 0;
|
||||||
|
while (off < length) {
|
||||||
|
switch (g_h4State) {
|
||||||
|
case H4State::Type: {
|
||||||
|
uint8_t type = data[off++];
|
||||||
|
g_h4Type = type;
|
||||||
|
g_h4Len = 0;
|
||||||
|
if (type == H4_EVENT) g_h4HeaderLen = sizeof(EventHeader);
|
||||||
|
else if (type == H4_ACL) g_h4HeaderLen = sizeof(AclHeader);
|
||||||
|
else if (type == H4_SCO) g_h4HeaderLen = 3;
|
||||||
|
else { g_h4Dropped++; break; }
|
||||||
|
g_h4State = H4State::Header;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case H4State::Header: {
|
||||||
|
g_h4Packet[g_h4Len++] = data[off++];
|
||||||
|
if (g_h4Len < g_h4HeaderLen) break;
|
||||||
|
uint32_t payload;
|
||||||
|
if (g_h4Type == H4_EVENT) payload = g_h4Packet[1];
|
||||||
|
else if (g_h4Type == H4_ACL) payload = (uint32_t)g_h4Packet[2]
|
||||||
|
| ((uint32_t)g_h4Packet[3] << 8);
|
||||||
|
else payload = g_h4Packet[2];
|
||||||
|
// SCO is not used; an ACL packet beyond the ring slot
|
||||||
|
// cannot be a valid one from a controller that reported
|
||||||
|
// its buffer size. Skip either without losing framing.
|
||||||
|
if (g_h4Type == H4_SCO
|
||||||
|
|| g_h4HeaderLen + payload > sizeof(g_h4Packet)) {
|
||||||
|
g_h4Remaining = payload;
|
||||||
|
g_h4State = payload ? H4State::Discard : H4State::Type;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
g_h4Expected = (uint16_t)(g_h4HeaderLen + payload);
|
||||||
|
if (payload == 0) {
|
||||||
|
CompleteH4Packet();
|
||||||
|
g_h4State = H4State::Type;
|
||||||
|
} else {
|
||||||
|
g_h4State = H4State::Payload;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case H4State::Payload: {
|
||||||
|
uint32_t take = g_h4Expected - g_h4Len;
|
||||||
|
if (take > length - off) take = length - off;
|
||||||
|
memcpy(&g_h4Packet[g_h4Len], &data[off], take);
|
||||||
|
g_h4Len = (uint16_t)(g_h4Len + take);
|
||||||
|
off += take;
|
||||||
|
if (g_h4Len == g_h4Expected) {
|
||||||
|
CompleteH4Packet();
|
||||||
|
g_h4State = H4State::Type;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case H4State::Discard: {
|
||||||
|
uint32_t take = length - off;
|
||||||
|
if (take > g_h4Remaining) take = g_h4Remaining;
|
||||||
|
off += take;
|
||||||
|
g_h4Remaining -= take;
|
||||||
|
if (g_h4Remaining == 0) g_h4State = H4State::Type;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Transport pump
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
void Pump() {
|
||||||
|
if (g_transport == Transport::Usb) {
|
||||||
|
Xhci::PollEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_transport != Transport::Uart) return;
|
||||||
|
|
||||||
|
bool expected = false;
|
||||||
|
if (!g_uartConsumer.compare_exchange_strong(expected, true,
|
||||||
|
std::memory_order_acquire)) return;
|
||||||
|
// Also moves bytes by hand, in case the UART interrupt never fires.
|
||||||
|
BtUart::Service();
|
||||||
|
uint8_t chunk[256];
|
||||||
|
int n;
|
||||||
|
while ((n = BtUart::Read(chunk, sizeof(chunk))) > 0) {
|
||||||
|
FeedH4Bytes(chunk, (uint32_t)n);
|
||||||
|
}
|
||||||
|
g_uartConsumer.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InPumpContext() {
|
||||||
|
// UART packets are only queued by Pump, never handled inside it.
|
||||||
|
return g_transport == Transport::Usb && Xhci::InPollContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransportHasRxData() {
|
||||||
|
return g_transport == Transport::Uart && BtUart::HasRxData();
|
||||||
|
}
|
||||||
|
|
||||||
|
Transport GetTransport() { return g_transport; }
|
||||||
|
|
||||||
|
uint32_t TransportBytesPerSecond() {
|
||||||
|
return g_transport == Transport::Uart ? BtUart::GetBaud() / 10 : 0;
|
||||||
|
}
|
||||||
|
void SetVendor(Vendor vendor) { g_vendor = vendor; }
|
||||||
|
Vendor GetVendor() { return g_vendor; }
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// USB transfer callback
|
// USB transfer callback
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -641,7 +802,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
static void PollWait(uint32_t ms) {
|
static void PollWait(uint32_t ms) {
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < ms) {
|
while (Timekeeping::GetMilliseconds() - start < ms) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
for (int j = 0; j < 100; j++) {
|
for (int j = 0; j < 100; j++) {
|
||||||
Arch::CompilerBarrier();
|
Arch::CompilerBarrier();
|
||||||
@@ -672,10 +833,20 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// the configured USB function its settling delay. StartEventPipe then
|
// the configured USB function its settling delay. StartEventPipe then
|
||||||
// arms both receive endpoints before the first bootloader command.
|
// arms both receive endpoints before the first bootloader command.
|
||||||
|
|
||||||
|
g_transport = Transport::Usb;
|
||||||
g_initialized = true;
|
g_initialized = true;
|
||||||
KernelLogStream(OK, "BT-HCI") << "HCI transport initialized on slot " << (uint64_t)slotId;
|
KernelLogStream(OK, "BT-HCI") << "HCI transport initialized on slot " << (uint64_t)slotId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InitializeUart() {
|
||||||
|
// No DMA buffers: SendCommand/SendAcl copy each H4 packet straight
|
||||||
|
// into the UART's transmit ring.
|
||||||
|
ResetUartReceiver();
|
||||||
|
g_transport = Transport::Uart;
|
||||||
|
g_initialized = true;
|
||||||
|
KernelLogStream(OK, "BT-HCI") << "HCI transport initialized on UART";
|
||||||
|
}
|
||||||
|
|
||||||
// Start receiving HCI events and ACL data — call after HCI init sequence.
|
// Start receiving HCI events and ACL data — call after HCI init sequence.
|
||||||
// Arms BOTH the interrupt IN (events) and the bulk IN (ACL). The bulk IN
|
// Arms BOTH the interrupt IN (events) and the bulk IN (ACL). The bulk IN
|
||||||
// MUST stay armed across the firmware download: on this controller the device
|
// MUST stay armed across the firmware download: on this controller the device
|
||||||
@@ -712,7 +883,8 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen) {
|
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen) {
|
||||||
if (!g_initialized || !g_cmdDmaBuf) return false;
|
if (!g_initialized) return false;
|
||||||
|
if (g_transport == Transport::Usb && !g_cmdDmaBuf) return false;
|
||||||
|
|
||||||
uint16_t expected = 0;
|
uint16_t expected = 0;
|
||||||
if (!g_commandOpcode.compare_exchange_strong(expected, opcode,
|
if (!g_commandOpcode.compare_exchange_strong(expected, opcode,
|
||||||
@@ -745,6 +917,27 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
<< base::hex << (uint64_t)opcode << base::dec;
|
<< base::hex << (uint64_t)opcode << base::dec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (g_transport == Transport::Uart) {
|
||||||
|
// H4: [0x01][opcode][paramLen][params]. The transmit ring is
|
||||||
|
// shared with ACL traffic; wait briefly for room behind it.
|
||||||
|
uint8_t header[4] = {
|
||||||
|
H4_COMMAND, (uint8_t)(opcode & 0xFF), (uint8_t)(opcode >> 8), paramLen
|
||||||
|
};
|
||||||
|
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
while (!BtUart::Write(header, sizeof(header), params, paramLen)) {
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > 1000) {
|
||||||
|
FinishCommand(opcode);
|
||||||
|
KernelLogStream(WARNING, "BT-HCI") << "SendCommand: UART transmit stalled, opcode="
|
||||||
|
<< base::hex << (uint64_t)opcode << base::dec;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BtUart::Service();
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Use DMA-allocated buffer (not stack) for the command data.
|
// Use DMA-allocated buffer (not stack) for the command data.
|
||||||
// xHCI reads from this buffer via DMA for OUT transfers.
|
// xHCI reads from this buffer via DMA for OUT transfers.
|
||||||
memset(g_cmdDmaBuf, 0, 512);
|
memset(g_cmdDmaBuf, 0, 512);
|
||||||
@@ -791,7 +984,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// cannot wait -- a nested PollEvents is a no-op, so the Command Complete
|
// cannot wait -- a nested PollEvents is a no-op, so the Command Complete
|
||||||
// is reaped by the active PollEvents after we return. The command was
|
// is reaped by the active PollEvents after we return. The command was
|
||||||
// already submitted (fire-and-forget); report success.
|
// already submitted (fire-and-forget); report success.
|
||||||
if (Xhci::InPollContext()) {
|
if (InPumpContext()) {
|
||||||
FinishCommand(opcode);
|
FinishCommand(opcode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -799,7 +992,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
|
|
||||||
uint8_t event[sizeof(g_eventBuf)];
|
uint8_t event[sizeof(g_eventBuf)];
|
||||||
@@ -852,7 +1045,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
|
|
||||||
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs) {
|
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs) {
|
||||||
// See WaitCommandComplete: cannot wait when nested under PollEvents.
|
// See WaitCommandComplete: cannot wait when nested under PollEvents.
|
||||||
if (Xhci::InPollContext()) {
|
if (InPumpContext()) {
|
||||||
FinishCommand(opcode);
|
FinishCommand(opcode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -860,7 +1053,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
|
|
||||||
uint8_t event[sizeof(g_eventBuf)];
|
uint8_t event[sizeof(g_eventBuf)];
|
||||||
@@ -897,10 +1090,40 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// SendAcl — via USB bulk OUT
|
// SendAcl — via USB bulk OUT
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
// Room the UART transmit ring must keep for one maximum-size ACL packet.
|
||||||
|
static uint32_t UartAclSlotBytes() {
|
||||||
|
return 1u + sizeof(AclHeader) + (g_aclMaxLen ? g_aclMaxLen : 1021);
|
||||||
|
}
|
||||||
|
|
||||||
bool SendAcl(uint16_t handle, uint16_t pbFlag, const uint8_t* data, uint16_t len) {
|
bool SendAcl(uint16_t handle, uint16_t pbFlag, const uint8_t* data, uint16_t len) {
|
||||||
if (!g_initialized || !g_aclTxRing[0] || !GetConnection(handle)) return false;
|
if (!g_initialized || !GetConnection(handle)) return false;
|
||||||
if (len + sizeof(AclHeader) > 4096) return false; // Single page DMA buffer
|
if (len + sizeof(AclHeader) > 4096) return false; // Single page DMA buffer
|
||||||
|
|
||||||
|
if (g_transport == Transport::Uart) {
|
||||||
|
g_aclTxLock.Acquire();
|
||||||
|
if (g_aclMaxNum != 0 && AclPendingCount() >= g_aclMaxNum) {
|
||||||
|
g_aclTxLock.Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint16_t handleFlags = (uint16_t)((handle & 0x0FFF) | pbFlag);
|
||||||
|
uint8_t header[1 + sizeof(AclHeader)] = {
|
||||||
|
H4_ACL,
|
||||||
|
(uint8_t)handleFlags, (uint8_t)(handleFlags >> 8),
|
||||||
|
(uint8_t)len, (uint8_t)(len >> 8)
|
||||||
|
};
|
||||||
|
// All or nothing: a partial H4 packet would desynchronize the
|
||||||
|
// controller's deframer. A full ring drops the packet, as a
|
||||||
|
// full xHCI TX ring does.
|
||||||
|
bool queued = BtUart::Write(header, sizeof(header), data, len);
|
||||||
|
if (queued) {
|
||||||
|
g_aclPendingCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
g_aclTxCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
g_aclTxLock.Release();
|
||||||
|
return queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_aclTxRing[0]) return false;
|
||||||
g_aclTxLock.Acquire();
|
g_aclTxLock.Acquire();
|
||||||
|
|
||||||
// HCI host flow control is a hard limit, not merely a media-pacing
|
// HCI host flow control is a hard limit, not merely a media-pacing
|
||||||
@@ -951,6 +1174,11 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
uint16_t AclMaxPackets() { return g_aclMaxNum; }
|
uint16_t AclMaxPackets() { return g_aclMaxNum; }
|
||||||
|
|
||||||
uint32_t AclTxInFlight() {
|
uint32_t AclTxInFlight() {
|
||||||
|
if (g_transport == Transport::Uart) {
|
||||||
|
uint32_t pending = BtUart::TxPending();
|
||||||
|
uint32_t slot = UartAclSlotBytes();
|
||||||
|
return (pending + slot - 1) / slot;
|
||||||
|
}
|
||||||
uint32_t tx = g_aclTxCount.load(std::memory_order_relaxed);
|
uint32_t tx = g_aclTxCount.load(std::memory_order_relaxed);
|
||||||
uint32_t done = g_aclTxDoneCount.load(std::memory_order_relaxed);
|
uint32_t done = g_aclTxDoneCount.load(std::memory_order_relaxed);
|
||||||
return (tx >= done) ? (tx - done) : 0;
|
return (tx >= done) ? (tx - done) : 0;
|
||||||
@@ -966,6 +1194,10 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
uint16_t maxOut = g_aclMaxNum ? g_aclMaxNum : 4;
|
uint16_t maxOut = g_aclMaxNum ? g_aclMaxNum : 4;
|
||||||
uint16_t mediaLimit = maxOut > 1 ? (uint16_t)(maxOut - 1) : maxOut;
|
uint16_t mediaLimit = maxOut > 1 ? (uint16_t)(maxOut - 1) : maxOut;
|
||||||
if (AclPendingCount() >= mediaLimit) return false;
|
if (AclPendingCount() >= mediaLimit) return false;
|
||||||
|
if (g_transport == Transport::Uart) {
|
||||||
|
// Room for this packet plus a signaling reply behind it.
|
||||||
|
return BtUart::TxFree() >= 2 * UartAclSlotBytes();
|
||||||
|
}
|
||||||
if (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) return false;
|
if (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1420,6 +1652,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case EVT_VENDOR_SPECIFIC: {
|
case EVT_VENDOR_SPECIFIC: {
|
||||||
|
if (g_vendor != Vendor::Intel) break;
|
||||||
// Intel vendor events carry a sub-opcode in the first byte.
|
// Intel vendor events carry a sub-opcode in the first byte.
|
||||||
// 0x02 = "bootup" (operational firmware booted after 0xFC01)
|
// 0x02 = "bootup" (operational firmware booted after 0xFC01)
|
||||||
// 0x06 = "secure send result": result(1) opcode(2) status(1)
|
// 0x06 = "secure send result": result(1) opcode(2) status(1)
|
||||||
@@ -1625,7 +1858,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// copying past what the controller delivered.
|
// copying past what the controller delivered.
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < 2000) {
|
while (Timekeeping::GetMilliseconds() - start < 2000) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
|
|
||||||
uint8_t event[sizeof(g_eventBuf)];
|
uint8_t event[sizeof(g_eventBuf)];
|
||||||
@@ -1676,7 +1909,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
void ProcessPendingCommands() {
|
void ProcessPendingCommands() {
|
||||||
// Top-level only: drains queued pairing replies with real, confirmed
|
// Top-level only: drains queued pairing replies with real, confirmed
|
||||||
// control transfers. Must NOT be called from inside PollEvents.
|
// control transfers. Must NOT be called from inside PollEvents.
|
||||||
if (Xhci::InPollContext()) return;
|
if (InPumpContext()) return;
|
||||||
// One consumer at a time: this runs from the Connect wait loop AND the
|
// One consumer at a time: this runs from the Connect wait loop AND the
|
||||||
// idle-core service pump, potentially on different cores; two consumers
|
// idle-core service pump, potentially on different cores; two consumers
|
||||||
// on the lockless queue would race the tail and interleave commands.
|
// on the lockless queue would race the tail and interleave commands.
|
||||||
@@ -1728,7 +1961,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
bool WaitSecureSendResult(uint32_t timeoutMs, uint8_t* outResult, uint8_t* outStatus) {
|
bool WaitSecureSendResult(uint32_t timeoutMs, uint8_t* outResult, uint8_t* outStatus) {
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
if (g_secureResultValid) {
|
if (g_secureResultValid) {
|
||||||
if (outResult) *outResult = g_secureResult;
|
if (outResult) *outResult = g_secureResult;
|
||||||
@@ -1763,7 +1996,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
<< " byte " << g_ssBytesSent;
|
<< " byte " << g_ssBytesSent;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
|
|
||||||
g_ssBytesSent += frag;
|
g_ssBytesSent += frag;
|
||||||
@@ -1784,7 +2017,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// header is deliberately absent, and the NOCP credit counter is NOT
|
// header is deliberately absent, and the NOCP credit counter is NOT
|
||||||
// touched: these are commands, not ACL data).
|
// touched: these are commands, not ACL data).
|
||||||
bool IntelSecureSendBulk(uint8_t fragmentType, const uint8_t* data, uint32_t len) {
|
bool IntelSecureSendBulk(uint8_t fragmentType, const uint8_t* data, uint32_t len) {
|
||||||
if (!g_initialized || !g_aclTxRing[0]) return false;
|
if (!g_initialized || g_transport != Transport::Usb || !g_aclTxRing[0]) return false;
|
||||||
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
|
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
|
||||||
|
|
||||||
uint32_t off = 0;
|
uint32_t off = 0;
|
||||||
@@ -1796,7 +2029,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// controller from freezing the boot path.
|
// controller from freezing the boot path.
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) {
|
while (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
if (g_aclTxTransportError.load(std::memory_order_acquire)) {
|
if (g_aclTxTransportError.load(std::memory_order_acquire)) {
|
||||||
KernelLogStream(ERROR, "BT-HCI")
|
KernelLogStream(ERROR, "BT-HCI")
|
||||||
@@ -1843,7 +2076,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
bool DrainBulkTx(uint32_t timeoutMs) {
|
bool DrainBulkTx(uint32_t timeoutMs) {
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (AclTxInFlight() > 0) {
|
while (AclTxInFlight() > 0) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
|
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
|
||||||
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||||
@@ -1872,7 +2105,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
|
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Pump();
|
||||||
DrainEvents();
|
DrainEvents();
|
||||||
if (g_intelBootup) return true;
|
if (g_intelBootup) return true;
|
||||||
for (int j = 0; j < 100; j++) Arch::CompilerBarrier();
|
for (int j = 0; j < 100; j++) Arch::CompilerBarrier();
|
||||||
@@ -1953,9 +2186,14 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool SetBdAddr(const uint8_t* addr) {
|
bool SetBdAddr(const uint8_t* addr) {
|
||||||
// Intel 0xFC31: 6-byte BD_ADDR, little-endian (same order as ReadBdAddr).
|
// Intel 0xFC31 / Broadcom 0xFC01: 6-byte BD_ADDR, little-endian (same
|
||||||
if (!SendCommand(OP_INTEL_WRITE_BD_ADDR, addr, 6)) return false;
|
// order as ReadBdAddr).
|
||||||
return WaitCommandComplete(OP_INTEL_WRITE_BD_ADDR, nullptr, 0, 2000);
|
uint16_t opcode;
|
||||||
|
if (g_vendor == Vendor::Intel) opcode = OP_INTEL_WRITE_BD_ADDR;
|
||||||
|
else if (g_vendor == Vendor::Broadcom) opcode = OP_BCM_WRITE_BD_ADDR;
|
||||||
|
else return false;
|
||||||
|
if (!SendCommand(opcode, addr, 6)) return false;
|
||||||
|
return WaitCommandComplete(opcode, nullptr, 0, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Disconnect(uint16_t handle, uint8_t reason) {
|
bool Disconnect(uint16_t handle, uint8_t reason) {
|
||||||
@@ -1,23 +1,35 @@
|
|||||||
/*
|
/*
|
||||||
* Hci.hpp
|
* Hci.hpp
|
||||||
* Bluetooth HCI (Host Controller Interface) layer
|
* Bluetooth HCI (Host Controller Interface) layer
|
||||||
* HCI transport over USB bulk/interrupt/control endpoints
|
* HCI transport over USB (bulk/interrupt/control endpoints) or a UART (H4)
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Hci {
|
namespace Drivers::Bluetooth::Hci {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// HCI packet types (for USB transport)
|
// HCI transports
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
// USB transport uses different endpoints for each packet type:
|
// USB transport uses different endpoints for each packet type:
|
||||||
// Commands -> Control EP0 (class request)
|
// Commands -> Control EP0 (class request)
|
||||||
// ACL data -> Bulk OUT / Bulk IN
|
// ACL data -> Bulk OUT / Bulk IN
|
||||||
// Events -> Interrupt IN
|
// Events -> Interrupt IN
|
||||||
|
//
|
||||||
|
// The UART transport (H4, Bluetooth Core spec Vol 4 Part A) carries all
|
||||||
|
// of them on one byte stream, each packet preceded by its type.
|
||||||
|
constexpr uint8_t H4_COMMAND = 0x01;
|
||||||
|
constexpr uint8_t H4_ACL = 0x02;
|
||||||
|
constexpr uint8_t H4_SCO = 0x03;
|
||||||
|
constexpr uint8_t H4_EVENT = 0x04;
|
||||||
|
|
||||||
|
enum class Transport : uint8_t { None, Usb, Uart };
|
||||||
|
|
||||||
|
// Controller vendor, for the vendor-specific commands (BD_ADDR write).
|
||||||
|
enum class Vendor : uint8_t { Unknown, Intel, Broadcom };
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// HCI command opcodes (OGF << 10 | OCF)
|
// HCI command opcodes (OGF << 10 | OCF)
|
||||||
@@ -61,6 +73,13 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
constexpr uint16_t OP_READ_LOCAL_FEATURES = 0x1003;
|
constexpr uint16_t OP_READ_LOCAL_FEATURES = 0x1003;
|
||||||
constexpr uint16_t OP_READ_BUFFER_SIZE = 0x1005;
|
constexpr uint16_t OP_READ_BUFFER_SIZE = 0x1005;
|
||||||
|
|
||||||
|
// Broadcom vendor commands (OGF 0x3F), from Linux btbcm/hci_bcm
|
||||||
|
constexpr uint16_t OP_BCM_WRITE_BD_ADDR = 0xFC01;
|
||||||
|
constexpr uint16_t OP_BCM_UPDATE_BAUD_RATE = 0xFC18;
|
||||||
|
constexpr uint16_t OP_BCM_DOWNLOAD_MINIDRV = 0xFC2E;
|
||||||
|
constexpr uint16_t OP_BCM_WRITE_RAM = 0xFC4C;
|
||||||
|
constexpr uint16_t OP_BCM_LAUNCH_RAM = 0xFC4E;
|
||||||
|
|
||||||
// Intel vendor commands (OGF 0x3F)
|
// Intel vendor commands (OGF 0x3F)
|
||||||
constexpr uint16_t OP_INTEL_READ_VERSION = 0xFC05;
|
constexpr uint16_t OP_INTEL_READ_VERSION = 0xFC05;
|
||||||
constexpr uint16_t OP_INTEL_RESET = 0xFC01;
|
constexpr uint16_t OP_INTEL_RESET = 0xFC01;
|
||||||
@@ -175,6 +194,37 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// Initialize HCI transport over USB for the given slot
|
// Initialize HCI transport over USB for the given slot
|
||||||
void Initialize(uint8_t slotId);
|
void Initialize(uint8_t slotId);
|
||||||
|
|
||||||
|
// Initialize HCI transport over the Bluetooth UART (Serial::BtUart,
|
||||||
|
// already initialized by the caller).
|
||||||
|
void InitializeUart();
|
||||||
|
|
||||||
|
Transport GetTransport();
|
||||||
|
|
||||||
|
// Bytes per second the host-to-controller link can carry: the UART's
|
||||||
|
// line rate (8N1), or 0 when the transport is not the bottleneck (USB).
|
||||||
|
uint32_t TransportBytesPerSecond();
|
||||||
|
|
||||||
|
void SetVendor(Vendor vendor);
|
||||||
|
Vendor GetVendor();
|
||||||
|
|
||||||
|
// Move received bytes from the transport into the HCI queues: the xHCI
|
||||||
|
// event ring for USB, the UART receive ring (H4 deframing) for UART.
|
||||||
|
// Every wait loop calls this before DrainEvents. Non-reentrant; a
|
||||||
|
// nested or concurrent call returns at once.
|
||||||
|
void Pump();
|
||||||
|
|
||||||
|
// True while running nested under Pump (USB transfer callbacks), where
|
||||||
|
// a command cannot be waited for.
|
||||||
|
bool InPumpContext();
|
||||||
|
|
||||||
|
// Received transport bytes are waiting for Pump (UART only; USB signals
|
||||||
|
// through the xHCI deferred-work flag).
|
||||||
|
bool TransportHasRxData();
|
||||||
|
|
||||||
|
// Discard all received UART input, queued bytes and any partial packet
|
||||||
|
// (after a power cycle or a baud-rate change).
|
||||||
|
void ResetUartReceiver();
|
||||||
|
|
||||||
// Start receiving HCI events and ACL data (call after HCI init sequence).
|
// Start receiving HCI events and ACL data (call after HCI init sequence).
|
||||||
// Arms both the interrupt IN and the bulk IN; the bulk IN must stay armed
|
// Arms both the interrupt IN and the bulk IN; the bulk IN must stay armed
|
||||||
// through the firmware download (it absorbs the device's ~635 KB cc=4 glitch
|
// through the firmware download (it absorbs the device's ~635 KB cc=4 glitch
|
||||||
@@ -186,7 +236,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// are intentionally ignored and any endpoint error is recovered here.
|
// are intentionally ignored and any endpoint error is recovered here.
|
||||||
void EnableAclDataReception();
|
void EnableAclDataReception();
|
||||||
|
|
||||||
// Send an HCI command via USB control transfer (EP0)
|
// Send an HCI command (USB: control transfer on EP0; UART: H4 packet)
|
||||||
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen);
|
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen);
|
||||||
|
|
||||||
// Wait for a Command Complete event matching the given opcode
|
// Wait for a Command Complete event matching the given opcode
|
||||||
@@ -197,7 +247,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// Wait for a Command Status event matching the given opcode
|
// Wait for a Command Status event matching the given opcode
|
||||||
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs = 2000);
|
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs = 2000);
|
||||||
|
|
||||||
// Send ACL data via USB bulk OUT
|
// Send ACL data (USB: bulk OUT; UART: H4 packet)
|
||||||
bool SendAcl(uint16_t handle, uint16_t pbFlag, const uint8_t* data, uint16_t len);
|
bool SendAcl(uint16_t handle, uint16_t pbFlag, const uint8_t* data, uint16_t len);
|
||||||
|
|
||||||
// Process an HCI event received on the interrupt IN endpoint
|
// Process an HCI event received on the interrupt IN endpoint
|
||||||
@@ -323,8 +373,10 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// Drives Link Key Request -> pairing; needed for bonded-device reconnects.
|
// Drives Link Key Request -> pairing; needed for bonded-device reconnects.
|
||||||
bool AuthenticateLink(uint16_t handle);
|
bool AuthenticateLink(uint16_t handle);
|
||||||
|
|
||||||
// Set the adapter's BD_ADDR (Intel vendor command 0xFC31). Used to dodge a
|
// Set the adapter's BD_ADDR (vendor command: Intel 0xFC31, Broadcom
|
||||||
// remote that holds a stale, un-clearable bond to our real address.
|
// 0xFC01). Used to dodge a remote that holds a stale, un-clearable bond
|
||||||
|
// to our real address, and to give Broadcom parts without a programmed
|
||||||
|
// address a unique one.
|
||||||
bool SetBdAddr(const uint8_t* addr);
|
bool SetBdAddr(const uint8_t* addr);
|
||||||
|
|
||||||
// Send any queued pairing replies (IO-cap / user-confirm / link-key) with
|
// Send any queued pairing replies (IO-cap / user-confirm / link-key) with
|
||||||
@@ -340,7 +392,8 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
uint16_t AclMaxPackets();
|
uint16_t AclMaxPackets();
|
||||||
|
|
||||||
// ACL packets handed to the xHCI whose bulk OUT completion has not been
|
// ACL packets handed to the xHCI whose bulk OUT completion has not been
|
||||||
// reaped yet (each one still owns a TX DMA ring slot).
|
// reaped yet (each one still owns a TX DMA ring slot). On UART, the
|
||||||
|
// transmit ring's backlog in maximum-size packets (0 once drained).
|
||||||
uint32_t AclTxInFlight();
|
uint32_t AclTxInFlight();
|
||||||
|
|
||||||
// True when SendAcl can be called without overrunning either the
|
// True when SendAcl can be called without overrunning either the
|
||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth {
|
namespace Drivers::Bluetooth {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// .sfi / TLV format constants (from drivers/bluetooth/btintel.{c,h})
|
// .sfi / TLV format constants (from drivers/bluetooth/btintel.{c,h})
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth {
|
namespace Drivers::Bluetooth {
|
||||||
|
|
||||||
// Drive the Intel bootloader firmware download sequence for the currently
|
// Drive the Intel bootloader firmware download sequence for the currently
|
||||||
// registered adapter:
|
// registered adapter:
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
#include "Hci.hpp"
|
#include "Hci.hpp"
|
||||||
#include "A2dp.hpp"
|
#include "A2dp.hpp"
|
||||||
#include "Avrcp.hpp"
|
#include "Avrcp.hpp"
|
||||||
#include <Drivers/USB/Xhci.hpp>
|
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::L2cap {
|
namespace Drivers::Bluetooth::L2cap {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// State
|
// State
|
||||||
@@ -548,7 +547,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Hci::Pump();
|
||||||
Hci::DrainEvents(); // process incoming ACL (the Connection Response!)
|
Hci::DrainEvents(); // process incoming ACL (the Connection Response!)
|
||||||
|
|
||||||
auto* ch = GetChannel(localCid);
|
auto* ch = GetChannel(localCid);
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::L2cap {
|
namespace Drivers::Bluetooth::L2cap {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// L2CAP CIDs (Channel Identifiers)
|
// L2CAP CIDs (Channel Identifiers)
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "Sbc.hpp"
|
#include "Sbc.hpp"
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Sbc {
|
namespace Drivers::Bluetooth::Sbc {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Analysis filterbank tables
|
// Analysis filterbank tables
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Drivers::USB::Bluetooth::Sbc {
|
namespace Drivers::Bluetooth::Sbc {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// SBC configuration
|
// SBC configuration
|
||||||
+135
-2
@@ -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().
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,713 @@
|
|||||||
|
/*
|
||||||
|
* Genet.cpp
|
||||||
|
* Broadcom GENET v5 Ethernet driver (Raspberry Pi 4 / 400)
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Genet.hpp"
|
||||||
|
#include <Arch/Cpu.hpp>
|
||||||
|
#include <Arch/Interrupts.hpp>
|
||||||
|
#include <ACPI/GsiRouting.hpp>
|
||||||
|
#include <Drivers/Platform/RpiFirmware.hpp>
|
||||||
|
#include <Memory/Dma.hpp>
|
||||||
|
#include <Memory/HHDM.hpp>
|
||||||
|
#include <Memory/Paging.hpp>
|
||||||
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
#include <Timekeeping/Timer.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Drivers::Net::Genet {
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// State
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
static bool g_initialized = false;
|
||||||
|
static bool g_coherent = false;
|
||||||
|
static PhyMode g_phyMode = PhyMode::RgmiiRxid;
|
||||||
|
static volatile uint8_t* g_regs = nullptr;
|
||||||
|
static uint8_t g_mac[6] = {};
|
||||||
|
static uint32_t g_phyAddr = 1;
|
||||||
|
|
||||||
|
// Packet buffers, BUF_LENGTH each, two to a page. Slot n of a ring
|
||||||
|
// always uses buffer n.
|
||||||
|
static uint8_t* g_rxBufs[RX_DESCS] = {};
|
||||||
|
static uint8_t* g_txBufs[TX_DESCS] = {};
|
||||||
|
|
||||||
|
// Software copies of the 16-bit ring counters: the RX consumer index we
|
||||||
|
// last wrote and the TX producer index. Slot = counter % ring size.
|
||||||
|
static std::atomic<uint32_t> g_rxCons{0};
|
||||||
|
static uint32_t g_txProd = 0;
|
||||||
|
static kcp::Spinlock g_txLock;
|
||||||
|
|
||||||
|
static std::atomic<bool> g_rxWorkPending{false};
|
||||||
|
static std::atomic<bool> g_processing{false};
|
||||||
|
|
||||||
|
// The PHY is polled: an external RGMII PHY raises no MAC interrupt on
|
||||||
|
// link changes, and its own interrupt pin is not wired to anything
|
||||||
|
// ACPI describes.
|
||||||
|
static constexpr uint64_t LINK_POLL_MS = 1000;
|
||||||
|
static std::atomic<uint64_t> g_nextLinkPollMs{0};
|
||||||
|
static std::atomic<bool> g_linkUp{false};
|
||||||
|
static uint32_t g_linkSpeed = 0;
|
||||||
|
static bool g_linkFullDuplex = false;
|
||||||
|
|
||||||
|
static uint64_t g_rxPacketCount = 0;
|
||||||
|
static uint64_t g_txPacketCount = 0;
|
||||||
|
static uint64_t g_rxErrorCount = 0;
|
||||||
|
|
||||||
|
static RxCallback g_rxCallback = nullptr;
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Register access
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
static uint32_t Read(uint32_t reg) {
|
||||||
|
return *(volatile uint32_t*)(g_regs + reg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Write(uint32_t reg, uint32_t value) {
|
||||||
|
*(volatile uint32_t*)(g_regs + reg) = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetBits(uint32_t reg, uint32_t bits) { Write(reg, Read(reg) | bits); }
|
||||||
|
static void ClearBits(uint32_t reg, uint32_t bits) { Write(reg, Read(reg) & ~bits); }
|
||||||
|
|
||||||
|
static uint32_t DescReg(uint32_t base, uint32_t slot, uint32_t word) {
|
||||||
|
return base + slot * DESC_SIZE + word * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// MDIO (the UniMAC's clause 22 master)
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
// Standard PHY registers and bits.
|
||||||
|
static constexpr uint32_t MII_BMCR = 0x00;
|
||||||
|
static constexpr uint32_t MII_BMSR = 0x01;
|
||||||
|
static constexpr uint32_t MII_PHYSID1 = 0x02;
|
||||||
|
static constexpr uint32_t MII_PHYSID2 = 0x03;
|
||||||
|
static constexpr uint32_t MII_ADVERTISE = 0x04;
|
||||||
|
static constexpr uint32_t MII_LPA = 0x05;
|
||||||
|
static constexpr uint32_t MII_CTRL1000 = 0x09;
|
||||||
|
static constexpr uint32_t MII_STAT1000 = 0x0A;
|
||||||
|
|
||||||
|
static constexpr uint16_t BMCR_SPEED1000 = 1u << 6;
|
||||||
|
static constexpr uint16_t BMCR_FULLDPLX = 1u << 8;
|
||||||
|
static constexpr uint16_t BMCR_ANRESTART = 1u << 9;
|
||||||
|
static constexpr uint16_t BMCR_ANENABLE = 1u << 12;
|
||||||
|
static constexpr uint16_t BMCR_SPEED100 = 1u << 13;
|
||||||
|
static constexpr uint16_t BMCR_RESET = 1u << 15;
|
||||||
|
static constexpr uint16_t BMSR_LSTATUS = 1u << 2;
|
||||||
|
static constexpr uint16_t BMSR_ANEGCOMPLETE = 1u << 5;
|
||||||
|
static constexpr uint16_t ADVERTISE_CSMA = 0x0001;
|
||||||
|
static constexpr uint16_t ADVERTISE_10HALF = 1u << 5;
|
||||||
|
static constexpr uint16_t ADVERTISE_10FULL = 1u << 6;
|
||||||
|
static constexpr uint16_t ADVERTISE_100HALF = 1u << 7;
|
||||||
|
static constexpr uint16_t ADVERTISE_100FULL = 1u << 8;
|
||||||
|
static constexpr uint16_t ADVERTISE_1000HALF = 1u << 8; // MII_CTRL1000
|
||||||
|
static constexpr uint16_t ADVERTISE_1000FULL = 1u << 9; // MII_CTRL1000
|
||||||
|
static constexpr uint16_t LPA_1000HALF = 1u << 10; // MII_STAT1000
|
||||||
|
static constexpr uint16_t LPA_1000FULL = 1u << 11; // MII_STAT1000
|
||||||
|
|
||||||
|
static bool MdioRun(uint32_t command) {
|
||||||
|
Write(MDIO_CMD, command);
|
||||||
|
Write(MDIO_CMD, command | MDIO_START_BUSY);
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
while (Read(MDIO_CMD) & MDIO_START_BUSY) {
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > 20) return false;
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool MdioRead(uint32_t phy, uint32_t reg, uint16_t& value) {
|
||||||
|
if (!MdioRun(MDIO_RD | (phy << MDIO_PMD_SHIFT) | (reg << MDIO_REG_SHIFT))) return false;
|
||||||
|
uint32_t result = Read(MDIO_CMD);
|
||||||
|
if (result & MDIO_READ_FAIL) return false;
|
||||||
|
value = (uint16_t)result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool MdioWrite(uint32_t phy, uint32_t reg, uint16_t value) {
|
||||||
|
return MdioRun(MDIO_WR | (phy << MDIO_PMD_SHIFT) | (reg << MDIO_REG_SHIFT) | value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t PhyRead(uint32_t reg) {
|
||||||
|
uint16_t value = 0;
|
||||||
|
if (!MdioRead(g_phyAddr, reg, value)) return 0;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool PhyPresent(uint32_t addr, uint32_t& id) {
|
||||||
|
uint16_t id1 = 0, id2 = 0;
|
||||||
|
if (!MdioRead(addr, MII_PHYSID1, id1) || !MdioRead(addr, MII_PHYSID2, id2)) return false;
|
||||||
|
if ((id1 == 0xFFFF && id2 == 0xFFFF) || (id1 == 0 && id2 == 0)) return false;
|
||||||
|
id = ((uint32_t)id1 << 16) | id2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The board puts the PHY at address 1; scan the bus if it is not there.
|
||||||
|
static bool FindPhy(uint32_t& id) {
|
||||||
|
if (PhyPresent(1, id)) { g_phyAddr = 1; return true; }
|
||||||
|
for (uint32_t addr = 0; addr < 32; addr++) {
|
||||||
|
if (addr != 1 && PhyPresent(addr, id)) { g_phyAddr = addr; return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Link
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
// BMSR latches link failures: the first read reports any loss since the
|
||||||
|
// last one, the second the current state.
|
||||||
|
static bool PhyLinkUp() {
|
||||||
|
(void)PhyRead(MII_BMSR);
|
||||||
|
uint16_t bmsr = PhyRead(MII_BMSR);
|
||||||
|
if (!(bmsr & BMSR_LSTATUS)) return false;
|
||||||
|
uint16_t bmcr = PhyRead(MII_BMCR);
|
||||||
|
return !(bmcr & BMCR_ANENABLE) || (bmsr & BMSR_ANEGCOMPLETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The highest mode both ends advertised (or the forced mode).
|
||||||
|
static void ResolveLink(uint32_t& speed, bool& fullDuplex) {
|
||||||
|
uint16_t bmcr = PhyRead(MII_BMCR);
|
||||||
|
if (!(bmcr & BMCR_ANENABLE)) {
|
||||||
|
speed = (bmcr & BMCR_SPEED1000) ? 1000 : (bmcr & BMCR_SPEED100) ? 100 : 10;
|
||||||
|
fullDuplex = (bmcr & BMCR_FULLDPLX) != 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t ctrl1000 = PhyRead(MII_CTRL1000);
|
||||||
|
uint16_t stat1000 = PhyRead(MII_STAT1000);
|
||||||
|
if ((ctrl1000 & ADVERTISE_1000FULL) && (stat1000 & LPA_1000FULL)) {
|
||||||
|
speed = 1000; fullDuplex = true; return;
|
||||||
|
}
|
||||||
|
if ((ctrl1000 & ADVERTISE_1000HALF) && (stat1000 & LPA_1000HALF)) {
|
||||||
|
speed = 1000; fullDuplex = false; return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t common = PhyRead(MII_ADVERTISE) & PhyRead(MII_LPA);
|
||||||
|
if (common & ADVERTISE_100FULL) { speed = 100; fullDuplex = true; }
|
||||||
|
else if (common & ADVERTISE_100HALF) { speed = 100; fullDuplex = false; }
|
||||||
|
else if (common & ADVERTISE_10FULL) { speed = 10; fullDuplex = true; }
|
||||||
|
else { speed = 10; fullDuplex = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point the RGMII block and the UniMAC at the negotiated mode and start
|
||||||
|
// both directions.
|
||||||
|
static void ConfigureMac(uint32_t speed, bool fullDuplex) {
|
||||||
|
uint32_t oob = Read(EXT_RGMII_OOB_CTRL);
|
||||||
|
oob &= ~OOB_DISABLE;
|
||||||
|
oob |= RGMII_LINK | RGMII_MODE_EN;
|
||||||
|
if (g_phyMode == PhyMode::Rgmii || g_phyMode == PhyMode::RgmiiId) oob |= ID_MODE_DIS;
|
||||||
|
else oob &= ~ID_MODE_DIS;
|
||||||
|
Write(EXT_RGMII_OOB_CTRL, oob);
|
||||||
|
|
||||||
|
uint32_t cmd = (speed == 1000 ? CMD_SPEED_1000 : speed == 100 ? CMD_SPEED_100 : CMD_SPEED_10)
|
||||||
|
<< CMD_SPEED_SHIFT;
|
||||||
|
if (!fullDuplex) cmd |= CMD_HD_EN;
|
||||||
|
Write(UMAC_CMD, cmd);
|
||||||
|
Write(UMAC_CMD, cmd | CMD_TX_EN | CMD_RX_EN);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PollLink() {
|
||||||
|
bool up = PhyLinkUp();
|
||||||
|
bool wasUp = g_linkUp.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (up) {
|
||||||
|
uint32_t speed = 0;
|
||||||
|
bool fullDuplex = false;
|
||||||
|
ResolveLink(speed, fullDuplex);
|
||||||
|
if (wasUp && speed == g_linkSpeed && fullDuplex == g_linkFullDuplex) return;
|
||||||
|
|
||||||
|
ConfigureMac(speed, fullDuplex);
|
||||||
|
g_linkSpeed = speed;
|
||||||
|
g_linkFullDuplex = fullDuplex;
|
||||||
|
g_linkUp.store(true, std::memory_order_release);
|
||||||
|
KernelLogStream(OK, "GENET") << "Link up: " << base::dec << (uint64_t)speed
|
||||||
|
<< " Mbit/s " << (fullDuplex ? "full" : "half") << " duplex";
|
||||||
|
} else if (wasUp) {
|
||||||
|
g_linkUp.store(false, std::memory_order_release);
|
||||||
|
g_linkSpeed = 0;
|
||||||
|
ClearBits(EXT_RGMII_OOB_CTRL, RGMII_LINK);
|
||||||
|
KernelLogStream(INFO, "GENET") << "Link down";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave a link the firmware already negotiated alone (renegotiating
|
||||||
|
// costs seconds); otherwise reset the PHY and advertise 10/100 and
|
||||||
|
// 1000 full duplex (the MAC has no 1000 half duplex).
|
||||||
|
static void StartPhy() {
|
||||||
|
if (PhyLinkUp()) {
|
||||||
|
KernelLogStream(INFO, "GENET") << "PHY already has a link, keeping it";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MdioWrite(g_phyAddr, MII_BMCR, BMCR_RESET);
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
while (PhyRead(MII_BMCR) & BMCR_RESET) {
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > 500) {
|
||||||
|
KernelLogStream(WARNING, "GENET") << "PHY reset timed out";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Timekeeping::BusyWaitUs(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
MdioWrite(g_phyAddr, MII_ADVERTISE, ADVERTISE_CSMA | ADVERTISE_10HALF | ADVERTISE_10FULL |
|
||||||
|
ADVERTISE_100HALF | ADVERTISE_100FULL);
|
||||||
|
uint16_t ctrl1000 = PhyRead(MII_CTRL1000);
|
||||||
|
ctrl1000 &= ~(ADVERTISE_1000HALF | ADVERTISE_1000FULL);
|
||||||
|
MdioWrite(g_phyAddr, MII_CTRL1000, ctrl1000 | ADVERTISE_1000FULL);
|
||||||
|
MdioWrite(g_phyAddr, MII_BMCR, BMCR_ANENABLE | BMCR_ANRESTART);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// MAC bring-up
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
static bool WaitDmaDisabled(uint32_t engineRegs) {
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
while (!(Read(engineRegs + DMA_STATUS) & DMA_DISABLED)) {
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > 5) return false;
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop both DMA engines (the firmware's network stack may have left
|
||||||
|
// them running) and drop whatever the buffers still hold.
|
||||||
|
static void StopDma() {
|
||||||
|
Write(TDMA_REGS + DMA_CTRL, 0);
|
||||||
|
Write(RDMA_REGS + DMA_CTRL, 0);
|
||||||
|
if (!WaitDmaDisabled(TDMA_REGS)) KernelLogStream(WARNING, "GENET") << "TX DMA did not stop";
|
||||||
|
if (!WaitDmaDisabled(RDMA_REGS)) KernelLogStream(WARNING, "GENET") << "RX DMA did not stop";
|
||||||
|
|
||||||
|
Write(UMAC_TX_FLUSH, 1);
|
||||||
|
Timekeeping::BusyWaitUs(10);
|
||||||
|
Write(UMAC_TX_FLUSH, 0);
|
||||||
|
|
||||||
|
SetBits(SYS_RBUF_FLUSH_CTRL, 1u << 0);
|
||||||
|
Timekeeping::BusyWaitUs(10);
|
||||||
|
ClearBits(SYS_RBUF_FLUSH_CTRL, 1u << 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the UniMAC with RGMII loopback on, so its receive clock stays
|
||||||
|
// stable through the reset, and leave it disabled until the link is up.
|
||||||
|
static void ResetUmac() {
|
||||||
|
SetBits(SYS_RBUF_FLUSH_CTRL, 1u << 1);
|
||||||
|
Timekeeping::BusyWaitUs(10);
|
||||||
|
ClearBits(SYS_RBUF_FLUSH_CTRL, 1u << 1);
|
||||||
|
Timekeeping::BusyWaitUs(10);
|
||||||
|
Write(SYS_RBUF_FLUSH_CTRL, 0);
|
||||||
|
Timekeeping::BusyWaitUs(10);
|
||||||
|
|
||||||
|
Write(UMAC_CMD, 0);
|
||||||
|
Write(UMAC_CMD, CMD_SW_RESET | CMD_LCL_LOOP_EN);
|
||||||
|
Timekeeping::BusyWaitUs(2);
|
||||||
|
Write(UMAC_CMD, 0);
|
||||||
|
|
||||||
|
Write(UMAC_MIB_CTRL, MIB_RESET_RX | MIB_RESET_TX | MIB_RESET_RUNT);
|
||||||
|
Write(UMAC_MIB_CTRL, 0);
|
||||||
|
|
||||||
|
Write(UMAC_MAX_FRAME_LEN, MAX_FRAME_LEN);
|
||||||
|
|
||||||
|
// No status blocks in front of frames; received frames start two
|
||||||
|
// bytes into their buffer, so the IP header lands 4-byte aligned.
|
||||||
|
uint32_t rbuf = Read(RBUF_CTRL);
|
||||||
|
rbuf &= ~RBUF_64B_EN;
|
||||||
|
rbuf |= RBUF_ALIGN_2B;
|
||||||
|
Write(RBUF_CTRL, rbuf);
|
||||||
|
ClearBits(TBUF_CTRL, TBUF_64B_EN);
|
||||||
|
Write(RBUF_TBUF_SIZE_CTRL, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ReadFirmwareMac(uint8_t mac[6]) {
|
||||||
|
uint32_t mac0 = Read(UMAC_MAC0);
|
||||||
|
uint32_t mac1 = Read(UMAC_MAC1);
|
||||||
|
mac[0] = (uint8_t)(mac0 >> 24);
|
||||||
|
mac[1] = (uint8_t)(mac0 >> 16);
|
||||||
|
mac[2] = (uint8_t)(mac0 >> 8);
|
||||||
|
mac[3] = (uint8_t)mac0;
|
||||||
|
mac[4] = (uint8_t)(mac1 >> 8);
|
||||||
|
mac[5] = (uint8_t)mac1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool MacUsable(const uint8_t mac[6]) {
|
||||||
|
bool allZero = true, allOnes = true;
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
if (mac[i] != 0x00) allZero = false;
|
||||||
|
if (mac[i] != 0xFF) allOnes = false;
|
||||||
|
}
|
||||||
|
return !allZero && !allOnes && !(mac[0] & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetMdfEntry(uint32_t& index, const uint8_t mac[6]) {
|
||||||
|
Write(UMAC_MDF_ADDR + index * 4, ((uint32_t)mac[0] << 8) | mac[1]);
|
||||||
|
Write(UMAC_MDF_ADDR + (index + 1) * 4, ((uint32_t)mac[2] << 24) | ((uint32_t)mac[3] << 16) |
|
||||||
|
((uint32_t)mac[4] << 8) | mac[5]);
|
||||||
|
index += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Station address, and the destination filter: broadcast and our own
|
||||||
|
// address. Filter n is enabled by bit (16 - n).
|
||||||
|
static void ProgramAddress() {
|
||||||
|
Write(UMAC_MAC0, ((uint32_t)g_mac[0] << 24) | ((uint32_t)g_mac[1] << 16) |
|
||||||
|
((uint32_t)g_mac[2] << 8) | g_mac[3]);
|
||||||
|
Write(UMAC_MAC1, ((uint32_t)g_mac[4] << 8) | g_mac[5]);
|
||||||
|
|
||||||
|
static constexpr uint8_t Broadcast[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||||
|
uint32_t index = 0;
|
||||||
|
SetMdfEntry(index, Broadcast);
|
||||||
|
SetMdfEntry(index, g_mac);
|
||||||
|
uint32_t filters = index / 2;
|
||||||
|
uint32_t enable = 0;
|
||||||
|
for (uint32_t i = 0; i < filters; i++) enable |= 1u << (MAX_MDF_FILTER - 1 - i);
|
||||||
|
Write(UMAC_MDF_CTRL, enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool AllocateBuffers(uint8_t** bufs, uint32_t count) {
|
||||||
|
constexpr uint32_t PerPage = 0x1000 / BUF_LENGTH;
|
||||||
|
for (uint32_t i = 0; i < count; i += PerPage) {
|
||||||
|
uint8_t* page = (uint8_t*)Memory::g_pfa->AllocateZeroed();
|
||||||
|
if (page == nullptr) return false;
|
||||||
|
// The zeroes sit dirty in the cache; write them back now so no
|
||||||
|
// eviction lands on top of a frame the MAC wrote later.
|
||||||
|
Memory::Dma::SyncForDevice(page, 0x1000, g_coherent);
|
||||||
|
for (uint32_t j = 0; j < PerPage && i + j < count; j++) bufs[i + j] = page + j * BUF_LENGTH;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ring 16 spans all 256 descriptors (START_ADDR/END_ADDR count 32-bit
|
||||||
|
// words). The producer/consumer counters are zeroed, but if the MAC
|
||||||
|
// keeps an old value the software side follows it instead: the slot is
|
||||||
|
// always counter % 256.
|
||||||
|
static void InitRxRing(uint32_t burst) {
|
||||||
|
Write(RDMA_REGS + DMA_SCB_BURST_SIZE, burst);
|
||||||
|
|
||||||
|
Write(RDMA_RING + RDMA_PROD_INDEX, 0);
|
||||||
|
Write(RDMA_RING + RDMA_CONS_INDEX, 0);
|
||||||
|
Write(RDMA_RING + DMA_START_ADDR, 0);
|
||||||
|
Write(RDMA_RING + RDMA_READ_PTR, 0);
|
||||||
|
Write(RDMA_RING + RDMA_WRITE_PTR, 0);
|
||||||
|
Write(RDMA_RING + DMA_END_ADDR, RX_DESCS * DESC_SIZE / 4 - 1);
|
||||||
|
Write(RDMA_RING + DMA_RING_BUF_SIZE, (RX_DESCS << 16) | BUF_LENGTH);
|
||||||
|
Write(RDMA_RING + RDMA_XON_XOFF_THRESH, (5u << 16) | (RX_DESCS >> 4));
|
||||||
|
Write(RDMA_RING + DMA_MBUF_DONE_THRESH, 1);
|
||||||
|
|
||||||
|
uint32_t cons = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF;
|
||||||
|
Write(RDMA_RING + RDMA_CONS_INDEX, cons);
|
||||||
|
g_rxCons.store(cons, std::memory_order_relaxed);
|
||||||
|
if (cons != 0) {
|
||||||
|
KernelLogStream(INFO, "GENET") << "RX ring resumes at index " << base::dec << (uint64_t)cons;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < RX_DESCS; i++) {
|
||||||
|
uint64_t phys = Memory::SubHHDM(g_rxBufs[i]);
|
||||||
|
Write(DescReg(RX_DESC_BASE, i, 1), (uint32_t)phys);
|
||||||
|
Write(DescReg(RX_DESC_BASE, i, 2), (uint32_t)(phys >> 32));
|
||||||
|
Write(DescReg(RX_DESC_BASE, i, 0), (BUF_LENGTH << DMA_BUFLENGTH_SHIFT) | DMA_OWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
Write(RDMA_REGS + DMA_RING_CFG, 1u << DEFAULT_Q);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InitTxRing(uint32_t burst) {
|
||||||
|
Write(TDMA_REGS + DMA_SCB_BURST_SIZE, burst);
|
||||||
|
|
||||||
|
Write(TDMA_RING + TDMA_PROD_INDEX, 0);
|
||||||
|
Write(TDMA_RING + TDMA_CONS_INDEX, 0);
|
||||||
|
Write(TDMA_RING + DMA_START_ADDR, 0);
|
||||||
|
Write(TDMA_RING + TDMA_READ_PTR, 0);
|
||||||
|
Write(TDMA_RING + TDMA_WRITE_PTR, 0);
|
||||||
|
Write(TDMA_RING + DMA_END_ADDR, TX_DESCS * DESC_SIZE / 4 - 1);
|
||||||
|
Write(TDMA_RING + DMA_RING_BUF_SIZE, (TX_DESCS << 16) | BUF_LENGTH);
|
||||||
|
Write(TDMA_RING + DMA_MBUF_DONE_THRESH, 1);
|
||||||
|
Write(TDMA_RING + TDMA_FLOW_PERIOD, 0);
|
||||||
|
|
||||||
|
g_txProd = Read(TDMA_RING + TDMA_CONS_INDEX) & 0xFFFF;
|
||||||
|
Write(TDMA_RING + TDMA_PROD_INDEX, g_txProd);
|
||||||
|
if (g_txProd != 0) {
|
||||||
|
KernelLogStream(INFO, "GENET") << "TX ring resumes at index " << base::dec << (uint64_t)g_txProd;
|
||||||
|
}
|
||||||
|
|
||||||
|
Write(TDMA_REGS + DMA_RING_CFG, 1u << DEFAULT_Q);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void StartDma() {
|
||||||
|
uint32_t ctrl = (1u << (DEFAULT_Q + DMA_RING_BUF_EN_SHIFT)) | DMA_EN;
|
||||||
|
Write(TDMA_REGS + DMA_CTRL, ctrl);
|
||||||
|
Write(RDMA_REGS + DMA_CTRL, ctrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Interrupt
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
// Acknowledge and mask; the frames are dispatched from the bottom half,
|
||||||
|
// which takes process-context locks.
|
||||||
|
static void HandleInterrupt(uint8_t, bool) {
|
||||||
|
if (!g_initialized) return;
|
||||||
|
uint32_t status = Read(INTRL2_0 + INTRL2_CPU_STAT) & ~Read(INTRL2_0 + INTRL2_CPU_MASK_STATUS);
|
||||||
|
if (status == 0) return;
|
||||||
|
Write(INTRL2_0 + INTRL2_CPU_CLEAR, status);
|
||||||
|
|
||||||
|
if (status & UMAC_IRQ_RXDMA_DONE) {
|
||||||
|
Write(INTRL2_0 + INTRL2_CPU_MASK_SET, UMAC_IRQ_RXDMA_DONE);
|
||||||
|
g_rxWorkPending.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Receive
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
static void PollRx() {
|
||||||
|
uint32_t cons = g_rxCons.load(std::memory_order_relaxed);
|
||||||
|
uint32_t prod = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF;
|
||||||
|
|
||||||
|
// At most one revolution per pass; the bottom half comes back for
|
||||||
|
// the rest.
|
||||||
|
uint32_t processed = 0;
|
||||||
|
while (cons != prod && processed < RX_DESCS) {
|
||||||
|
Arch::DmaReadFence();
|
||||||
|
uint32_t slot = cons % RX_DESCS;
|
||||||
|
uint32_t lenStat = Read(DescReg(RX_DESC_BASE, slot, 0));
|
||||||
|
uint32_t length = (lenStat >> DMA_BUFLENGTH_SHIFT) & DMA_BUFLENGTH_MASK;
|
||||||
|
uint8_t* buffer = g_rxBufs[slot];
|
||||||
|
|
||||||
|
bool whole = (lenStat & (DMA_SOP | DMA_EOP)) == (DMA_SOP | DMA_EOP);
|
||||||
|
if (!whole || (lenStat & DMA_RX_ERRORS) || length <= RX_BUF_OFFSET || length > BUF_LENGTH) {
|
||||||
|
g_rxErrorCount++;
|
||||||
|
} else {
|
||||||
|
Memory::Dma::SyncForCpu(buffer, length, g_coherent);
|
||||||
|
g_rxPacketCount++;
|
||||||
|
if (g_rxCallback != nullptr) {
|
||||||
|
g_rxCallback(buffer + RX_BUF_OFFSET, (uint16_t)(length - RX_BUF_OFFSET));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cons = (cons + 1) & 0xFFFF;
|
||||||
|
g_rxCons.store(cons, std::memory_order_relaxed);
|
||||||
|
Write(RDMA_RING + RDMA_CONS_INDEX, cons);
|
||||||
|
processed++;
|
||||||
|
if (cons == prod) prod = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool RxPending() {
|
||||||
|
uint32_t prod = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF;
|
||||||
|
return prod != g_rxCons.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Public API
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
bool ProbePlatform(uint64_t mmioPhys, uint64_t mmioSize, uint32_t gsi, bool levelTriggered,
|
||||||
|
bool activeLow, bool dmaCoherent, PhyMode phyMode, uint32_t burst) {
|
||||||
|
if (g_initialized) return false;
|
||||||
|
|
||||||
|
if (mmioSize < 0x10000) mmioSize = 0x10000;
|
||||||
|
for (uint64_t offset = 0; offset < mmioSize; offset += 0x1000) {
|
||||||
|
Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset));
|
||||||
|
}
|
||||||
|
g_regs = (volatile uint8_t*)Memory::HHDM(mmioPhys);
|
||||||
|
g_coherent = dmaCoherent;
|
||||||
|
g_phyMode = phyMode;
|
||||||
|
if (burst == 0) burst = 8;
|
||||||
|
|
||||||
|
// GENET v5 reports major revision 6 (v4 reports 5).
|
||||||
|
uint32_t rev = Read(SYS_REV_CTRL);
|
||||||
|
uint32_t major = (rev >> 24) & 0x0F;
|
||||||
|
if (major != 6) {
|
||||||
|
KernelLogStream(WARNING, "GENET") << "Unsupported revision 0x" << base::hex << (uint64_t)rev;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mask everything while the controller is reconfigured.
|
||||||
|
Write(INTRL2_0 + INTRL2_CPU_MASK_SET, 0xFFFFFFFF);
|
||||||
|
Write(INTRL2_0 + INTRL2_CPU_CLEAR, 0xFFFFFFFF);
|
||||||
|
Write(INTRL2_1 + INTRL2_CPU_MASK_SET, 0xFFFFFFFF);
|
||||||
|
Write(INTRL2_1 + INTRL2_CPU_CLEAR, 0xFFFFFFFF);
|
||||||
|
|
||||||
|
// The board's address comes from OTP through the firmware; else
|
||||||
|
// whatever the firmware's own driver programmed.
|
||||||
|
uint8_t firmwareMac[6];
|
||||||
|
ReadFirmwareMac(firmwareMac);
|
||||||
|
const char* macSource = "OTP";
|
||||||
|
if (!Platform::RpiFirmware::GetBoardMacAddress(g_mac) || !MacUsable(g_mac)) {
|
||||||
|
if (MacUsable(firmwareMac)) {
|
||||||
|
for (int i = 0; i < 6; i++) g_mac[i] = firmwareMac[i];
|
||||||
|
macSource = "firmware";
|
||||||
|
} else {
|
||||||
|
// Locally administered, fixed so DHCP leases survive reboots.
|
||||||
|
static constexpr uint8_t Fallback[6] = { 0x02, 0x4D, 0x54, 0x4B, 0x00, 0x01 };
|
||||||
|
for (int i = 0; i < 6; i++) g_mac[i] = Fallback[i];
|
||||||
|
macSource = "fallback";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write(SYS_PORT_CTRL, PORT_MODE_EXT_GPHY);
|
||||||
|
StopDma();
|
||||||
|
ResetUmac();
|
||||||
|
ProgramAddress();
|
||||||
|
|
||||||
|
if (!AllocateBuffers(g_rxBufs, RX_DESCS) || !AllocateBuffers(g_txBufs, TX_DESCS)) {
|
||||||
|
KernelLogStream(ERROR, "GENET") << "Out of memory for packet buffers";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
InitRxRing(burst);
|
||||||
|
InitTxRing(burst);
|
||||||
|
StartDma();
|
||||||
|
|
||||||
|
uint32_t phyId = 0;
|
||||||
|
if (!FindPhy(phyId)) {
|
||||||
|
KernelLogStream(ERROR, "GENET") << "No PHY answers on MDIO";
|
||||||
|
StopDma();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
KernelLogStream(OK, "GENET") << "GENET v5." << base::dec << (uint64_t)((rev >> 16) & 0x0F)
|
||||||
|
<< " at 0x" << base::hex << mmioPhys << ", PHY 0x" << (uint64_t)phyId
|
||||||
|
<< " at MDIO " << base::dec << (uint64_t)g_phyAddr
|
||||||
|
<< (dmaCoherent ? ", coherent DMA" : ", non-coherent DMA");
|
||||||
|
KernelLogStream(OK, "GENET") << "MAC " << base::hex
|
||||||
|
<< (uint64_t)g_mac[0] << ":" << (uint64_t)g_mac[1] << ":" << (uint64_t)g_mac[2] << ":"
|
||||||
|
<< (uint64_t)g_mac[3] << ":" << (uint64_t)g_mac[4] << ":" << (uint64_t)g_mac[5]
|
||||||
|
<< " (" << macSource << ")";
|
||||||
|
|
||||||
|
// RGMII on with the board's delays; the link (RGMII_LINK, speed,
|
||||||
|
// TX/RX enable) follows from the PHY poll.
|
||||||
|
uint32_t oob = Read(EXT_RGMII_OOB_CTRL);
|
||||||
|
oob &= ~(OOB_DISABLE | RGMII_LINK);
|
||||||
|
oob |= RGMII_MODE_EN;
|
||||||
|
if (phyMode == PhyMode::Rgmii || phyMode == PhyMode::RgmiiId) oob |= ID_MODE_DIS;
|
||||||
|
else oob &= ~ID_MODE_DIS;
|
||||||
|
Write(EXT_RGMII_OOB_CTRL, oob);
|
||||||
|
|
||||||
|
StartPhy();
|
||||||
|
|
||||||
|
uint8_t line = gsi != 0 ? Hal::GsiRouting::LineForGsi(gsi, levelTriggered, activeLow) : 0;
|
||||||
|
if (line != 0) {
|
||||||
|
Arch::RegisterIrqHandler(line, HandleInterrupt);
|
||||||
|
Arch::EnableLegacyIrq(line);
|
||||||
|
KernelLogStream(OK, "GENET") << "Interrupt: GSI " << base::dec << (uint64_t)gsi
|
||||||
|
<< " on platform line " << (uint64_t)line;
|
||||||
|
} else {
|
||||||
|
KernelLogStream(WARNING, "GENET") << "No interrupt, receive is polled";
|
||||||
|
}
|
||||||
|
|
||||||
|
g_initialized = true;
|
||||||
|
Write(INTRL2_0 + INTRL2_CPU_MASK_CLEAR, UMAC_IRQ_RXDMA_DONE);
|
||||||
|
|
||||||
|
PollLink();
|
||||||
|
g_nextLinkPollMs.store(Timekeeping::GetMilliseconds() + LINK_POLL_MS, std::memory_order_relaxed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frames are copied into the slot's buffer, so the caller's buffer is
|
||||||
|
// free again on return. Fails while the ring is full or the link down.
|
||||||
|
bool SendPacket(const uint8_t* data, uint16_t length) {
|
||||||
|
if (!g_initialized || data == nullptr || length == 0 || length > MAX_FRAME_LEN) return false;
|
||||||
|
if (!g_linkUp.load(std::memory_order_acquire)) return false;
|
||||||
|
|
||||||
|
g_txLock.Acquire();
|
||||||
|
|
||||||
|
uint32_t cons = Read(TDMA_RING + TDMA_CONS_INDEX) & 0xFFFF;
|
||||||
|
if (((g_txProd - cons) & 0xFFFF) >= TX_DESCS - 1) {
|
||||||
|
g_txLock.Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t slot = g_txProd % TX_DESCS;
|
||||||
|
uint8_t* buffer = g_txBufs[slot];
|
||||||
|
memcpy(buffer, data, length);
|
||||||
|
// The MAC sends short frames as given; pad to the 60-byte minimum.
|
||||||
|
uint32_t frameLength = length;
|
||||||
|
if (frameLength < 60) {
|
||||||
|
memset(buffer + frameLength, 0, 60 - frameLength);
|
||||||
|
frameLength = 60;
|
||||||
|
}
|
||||||
|
Memory::Dma::SyncForDevice(buffer, frameLength, g_coherent);
|
||||||
|
|
||||||
|
uint64_t phys = Memory::SubHHDM(buffer);
|
||||||
|
Write(DescReg(TX_DESC_BASE, slot, 1), (uint32_t)phys);
|
||||||
|
Write(DescReg(TX_DESC_BASE, slot, 2), (uint32_t)(phys >> 32));
|
||||||
|
Write(DescReg(TX_DESC_BASE, slot, 0), (frameLength << DMA_BUFLENGTH_SHIFT) |
|
||||||
|
(DMA_TX_QTAG_MASK << DMA_TX_QTAG_SHIFT) | DMA_TX_APPEND_CRC | DMA_SOP | DMA_EOP);
|
||||||
|
|
||||||
|
g_txProd = (g_txProd + 1) & 0xFFFF;
|
||||||
|
Arch::DmaWriteFence();
|
||||||
|
Write(TDMA_RING + TDMA_PROD_INDEX, g_txProd);
|
||||||
|
g_txPacketCount++;
|
||||||
|
|
||||||
|
g_txLock.Release();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* GetMacAddress() {
|
||||||
|
return g_mac;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsInitialized() {
|
||||||
|
return g_initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsLinkUp() {
|
||||||
|
return g_initialized && g_linkUp.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t GetLinkSpeed() {
|
||||||
|
return IsLinkUp() ? g_linkSpeed : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t GetRxPacketCount() {
|
||||||
|
return g_rxPacketCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t GetTxPacketCount() {
|
||||||
|
return g_txPacketCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetRxCallback(RxCallback callback) {
|
||||||
|
g_rxCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasDeferredWork() {
|
||||||
|
if (!g_initialized) return false;
|
||||||
|
return g_rxWorkPending.load(std::memory_order_acquire) || RxPending() ||
|
||||||
|
Timekeeping::GetMilliseconds() >= g_nextLinkPollMs.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessDeferredWork() {
|
||||||
|
if (!g_initialized) return;
|
||||||
|
|
||||||
|
bool expected = false;
|
||||||
|
if (!g_processing.compare_exchange_strong(expected, true,
|
||||||
|
std::memory_order_acquire, std::memory_order_relaxed)) return;
|
||||||
|
|
||||||
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
|
if (now >= g_nextLinkPollMs.load(std::memory_order_relaxed)) {
|
||||||
|
g_nextLinkPollMs.store(now + LINK_POLL_MS, std::memory_order_relaxed);
|
||||||
|
PollLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
g_rxWorkPending.store(false, std::memory_order_release);
|
||||||
|
PollRx();
|
||||||
|
|
||||||
|
// A frame that arrived while masked has latched the status bit, so
|
||||||
|
// unmasking raises the interrupt again rather than losing it.
|
||||||
|
Write(INTRL2_0 + INTRL2_CPU_MASK_CLEAR, UMAC_IRQ_RXDMA_DONE);
|
||||||
|
|
||||||
|
g_processing.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
/*
|
||||||
|
* Genet.hpp
|
||||||
|
* Broadcom GENET v5 Ethernet driver (Raspberry Pi 4 / 400)
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// The BCM2711's on-chip Gigabit Ethernet MAC (GENET v5, a UniMAC behind a
|
||||||
|
// DMA engine), wired over RGMII to a BCM54213PE PHY at MDIO address 1. The
|
||||||
|
// Pi Firmware Task Force UEFI firmware describes it as BCM6E4E
|
||||||
|
// (\_SB.ETH0): one register block, two interrupts (the first carries the
|
||||||
|
// default queue), _CCA = 0 (buffers do not snoop the CPU caches) and a
|
||||||
|
// _DSD with "phy-mode" and "brcm,max-dma-burst-size".
|
||||||
|
//
|
||||||
|
// Only the default queue (ring 16) is used, with all 256 descriptors of
|
||||||
|
// each direction. The descriptors live in the MAC's own SRAM, so only the
|
||||||
|
// packet buffers need cache maintenance.
|
||||||
|
|
||||||
|
namespace Drivers::Net::Genet {
|
||||||
|
|
||||||
|
// ---- Register map (offsets from the block base) ----
|
||||||
|
|
||||||
|
constexpr uint32_t SYS_REV_CTRL = 0x0000;
|
||||||
|
constexpr uint32_t SYS_PORT_CTRL = 0x0004;
|
||||||
|
constexpr uint32_t SYS_RBUF_FLUSH_CTRL = 0x0008;
|
||||||
|
constexpr uint32_t SYS_TBUF_FLUSH_CTRL = 0x000C;
|
||||||
|
|
||||||
|
constexpr uint32_t EXT_RGMII_OOB_CTRL = 0x008C;
|
||||||
|
|
||||||
|
constexpr uint32_t INTRL2_0 = 0x0200; // default queue, MAC events
|
||||||
|
constexpr uint32_t INTRL2_1 = 0x0240; // priority queues (unused)
|
||||||
|
constexpr uint32_t INTRL2_CPU_STAT = 0x00;
|
||||||
|
constexpr uint32_t INTRL2_CPU_CLEAR = 0x08;
|
||||||
|
constexpr uint32_t INTRL2_CPU_MASK_STATUS = 0x0C;
|
||||||
|
constexpr uint32_t INTRL2_CPU_MASK_SET = 0x10;
|
||||||
|
constexpr uint32_t INTRL2_CPU_MASK_CLEAR = 0x14;
|
||||||
|
|
||||||
|
constexpr uint32_t RBUF_CTRL = 0x0300;
|
||||||
|
constexpr uint32_t RBUF_TBUF_SIZE_CTRL = 0x03B4;
|
||||||
|
constexpr uint32_t TBUF_CTRL = 0x0600;
|
||||||
|
|
||||||
|
constexpr uint32_t UMAC_CMD = 0x0808;
|
||||||
|
constexpr uint32_t UMAC_MAC0 = 0x080C;
|
||||||
|
constexpr uint32_t UMAC_MAC1 = 0x0810;
|
||||||
|
constexpr uint32_t UMAC_MAX_FRAME_LEN = 0x0814;
|
||||||
|
constexpr uint32_t UMAC_TX_FLUSH = 0x0B34;
|
||||||
|
constexpr uint32_t UMAC_MIB_CTRL = 0x0D80;
|
||||||
|
constexpr uint32_t MDIO_CMD = 0x0E14;
|
||||||
|
constexpr uint32_t UMAC_MDF_CTRL = 0x0E50;
|
||||||
|
constexpr uint32_t UMAC_MDF_ADDR = 0x0E54;
|
||||||
|
|
||||||
|
// Descriptor SRAM, then the DMA registers: 17 rings of 0x40, then the
|
||||||
|
// engine's own registers.
|
||||||
|
constexpr uint32_t TOTAL_DESCS = 256;
|
||||||
|
constexpr uint32_t DESC_SIZE = 12; // length/status, address lo, address hi
|
||||||
|
constexpr uint32_t RX_DESC_BASE = 0x2000;
|
||||||
|
constexpr uint32_t TX_DESC_BASE = 0x4000;
|
||||||
|
constexpr uint32_t RDMA_BASE = RX_DESC_BASE + TOTAL_DESCS * DESC_SIZE;
|
||||||
|
constexpr uint32_t TDMA_BASE = TX_DESC_BASE + TOTAL_DESCS * DESC_SIZE;
|
||||||
|
constexpr uint32_t DEFAULT_Q = 16;
|
||||||
|
constexpr uint32_t DMA_RING_SIZE = 0x40;
|
||||||
|
constexpr uint32_t RDMA_RING = RDMA_BASE + DEFAULT_Q * DMA_RING_SIZE;
|
||||||
|
constexpr uint32_t TDMA_RING = TDMA_BASE + DEFAULT_Q * DMA_RING_SIZE;
|
||||||
|
constexpr uint32_t RDMA_REGS = RDMA_BASE + (DEFAULT_Q + 1) * DMA_RING_SIZE;
|
||||||
|
constexpr uint32_t TDMA_REGS = TDMA_BASE + (DEFAULT_Q + 1) * DMA_RING_SIZE;
|
||||||
|
|
||||||
|
// Per-ring registers. RX: the MAC produces, software consumes; TX the
|
||||||
|
// other way round. Both indices are 16-bit counters.
|
||||||
|
constexpr uint32_t RDMA_WRITE_PTR = 0x00;
|
||||||
|
constexpr uint32_t RDMA_PROD_INDEX = 0x08;
|
||||||
|
constexpr uint32_t RDMA_CONS_INDEX = 0x0C;
|
||||||
|
constexpr uint32_t RDMA_XON_XOFF_THRESH = 0x28;
|
||||||
|
constexpr uint32_t RDMA_READ_PTR = 0x2C;
|
||||||
|
constexpr uint32_t TDMA_READ_PTR = 0x00;
|
||||||
|
constexpr uint32_t TDMA_CONS_INDEX = 0x08;
|
||||||
|
constexpr uint32_t TDMA_PROD_INDEX = 0x0C;
|
||||||
|
constexpr uint32_t TDMA_FLOW_PERIOD = 0x28;
|
||||||
|
constexpr uint32_t TDMA_WRITE_PTR = 0x2C;
|
||||||
|
constexpr uint32_t DMA_RING_BUF_SIZE = 0x10;
|
||||||
|
constexpr uint32_t DMA_START_ADDR = 0x14;
|
||||||
|
constexpr uint32_t DMA_END_ADDR = 0x1C;
|
||||||
|
constexpr uint32_t DMA_MBUF_DONE_THRESH = 0x24;
|
||||||
|
|
||||||
|
// Engine registers.
|
||||||
|
constexpr uint32_t DMA_RING_CFG = 0x00;
|
||||||
|
constexpr uint32_t DMA_CTRL = 0x04;
|
||||||
|
constexpr uint32_t DMA_STATUS = 0x08;
|
||||||
|
constexpr uint32_t DMA_SCB_BURST_SIZE = 0x0C;
|
||||||
|
|
||||||
|
// ---- Register bits ----
|
||||||
|
|
||||||
|
constexpr uint32_t PORT_MODE_EXT_GPHY = 3;
|
||||||
|
|
||||||
|
constexpr uint32_t RGMII_LINK = 1u << 4;
|
||||||
|
constexpr uint32_t OOB_DISABLE = 1u << 5;
|
||||||
|
constexpr uint32_t RGMII_MODE_EN = 1u << 6;
|
||||||
|
constexpr uint32_t ID_MODE_DIS = 1u << 16; // no MAC-side TX clock delay
|
||||||
|
|
||||||
|
constexpr uint32_t RBUF_64B_EN = 1u << 0; // receive status block
|
||||||
|
constexpr uint32_t RBUF_ALIGN_2B = 1u << 1; // 2 pad bytes before each frame
|
||||||
|
constexpr uint32_t TBUF_64B_EN = 1u << 0; // transmit status block
|
||||||
|
|
||||||
|
constexpr uint32_t CMD_TX_EN = 1u << 0;
|
||||||
|
constexpr uint32_t CMD_RX_EN = 1u << 1;
|
||||||
|
constexpr uint32_t CMD_SPEED_SHIFT = 2;
|
||||||
|
constexpr uint32_t CMD_SPEED_10 = 0;
|
||||||
|
constexpr uint32_t CMD_SPEED_100 = 1;
|
||||||
|
constexpr uint32_t CMD_SPEED_1000 = 2;
|
||||||
|
constexpr uint32_t CMD_PROMISC = 1u << 4;
|
||||||
|
constexpr uint32_t CMD_HD_EN = 1u << 10;
|
||||||
|
constexpr uint32_t CMD_SW_RESET = 1u << 13;
|
||||||
|
constexpr uint32_t CMD_LCL_LOOP_EN = 1u << 15;
|
||||||
|
|
||||||
|
constexpr uint32_t MIB_RESET_RX = 1u << 0;
|
||||||
|
constexpr uint32_t MIB_RESET_RUNT = 1u << 1;
|
||||||
|
constexpr uint32_t MIB_RESET_TX = 1u << 2;
|
||||||
|
|
||||||
|
constexpr uint32_t MDIO_START_BUSY = 1u << 29;
|
||||||
|
constexpr uint32_t MDIO_READ_FAIL = 1u << 28;
|
||||||
|
constexpr uint32_t MDIO_RD = 2u << 26;
|
||||||
|
constexpr uint32_t MDIO_WR = 1u << 26;
|
||||||
|
constexpr uint32_t MDIO_PMD_SHIFT = 21;
|
||||||
|
constexpr uint32_t MDIO_REG_SHIFT = 16;
|
||||||
|
|
||||||
|
constexpr uint32_t MAX_MDF_FILTER = 17;
|
||||||
|
|
||||||
|
constexpr uint32_t DMA_EN = 1u << 0;
|
||||||
|
constexpr uint32_t DMA_RING_BUF_EN_SHIFT = 1;
|
||||||
|
constexpr uint32_t DMA_DISABLED = 1u << 0; // DMA_STATUS
|
||||||
|
|
||||||
|
constexpr uint32_t UMAC_IRQ_RXDMA_DONE = 1u << 13; // ring 16 reached its done threshold
|
||||||
|
|
||||||
|
// Descriptor length/status word: length in 27:16, flags in 15:0.
|
||||||
|
constexpr uint32_t DMA_BUFLENGTH_SHIFT = 16;
|
||||||
|
constexpr uint32_t DMA_BUFLENGTH_MASK = 0x0FFF;
|
||||||
|
constexpr uint32_t DMA_OWN = 0x8000;
|
||||||
|
constexpr uint32_t DMA_EOP = 0x4000;
|
||||||
|
constexpr uint32_t DMA_SOP = 0x2000;
|
||||||
|
constexpr uint32_t DMA_TX_APPEND_CRC = 0x0040;
|
||||||
|
constexpr uint32_t DMA_TX_QTAG_SHIFT = 7;
|
||||||
|
constexpr uint32_t DMA_TX_QTAG_MASK = 0x3F;
|
||||||
|
constexpr uint32_t DMA_RX_LG = 0x0010;
|
||||||
|
constexpr uint32_t DMA_RX_NO = 0x0008;
|
||||||
|
constexpr uint32_t DMA_RX_RXER = 0x0004;
|
||||||
|
constexpr uint32_t DMA_RX_CRC_ERROR = 0x0002;
|
||||||
|
constexpr uint32_t DMA_RX_OV = 0x0001;
|
||||||
|
constexpr uint32_t DMA_RX_ERRORS = DMA_RX_LG | DMA_RX_NO | DMA_RX_RXER |
|
||||||
|
DMA_RX_CRC_ERROR | DMA_RX_OV;
|
||||||
|
|
||||||
|
// ---- Driver parameters ----
|
||||||
|
|
||||||
|
constexpr uint32_t RX_DESCS = TOTAL_DESCS;
|
||||||
|
constexpr uint32_t TX_DESCS = TOTAL_DESCS;
|
||||||
|
constexpr uint32_t BUF_LENGTH = 2048;
|
||||||
|
constexpr uint32_t RX_BUF_OFFSET = 2; // RBUF_ALIGN_2B padding
|
||||||
|
// Payload 1500 + header 14 + VLAN 4 + Broadcom tag 6 + FCS 4 + pad 8.
|
||||||
|
constexpr uint32_t MAX_FRAME_LEN = 1536;
|
||||||
|
|
||||||
|
// How the board wires RGMII clock delays (ACPI _DSD "phy-mode").
|
||||||
|
enum class PhyMode : uint8_t {
|
||||||
|
Rgmii, // no delays from the PHY or the MAC
|
||||||
|
RgmiiId, // the PHY delays both clocks
|
||||||
|
RgmiiRxid, // the PHY delays RX, the MAC delays TX (Raspberry Pi 4)
|
||||||
|
RgmiiTxid,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Take over the controller at `mmioPhys`. `gsi` is the default-queue
|
||||||
|
// interrupt (0: poll only). `burst` is the DMA burst size in 32-bit
|
||||||
|
// words from "brcm,max-dma-burst-size" (0: 8).
|
||||||
|
bool ProbePlatform(uint64_t mmioPhys, uint64_t mmioSize, uint32_t gsi, bool levelTriggered,
|
||||||
|
bool activeLow, bool dmaCoherent, PhyMode phyMode, uint32_t burst);
|
||||||
|
|
||||||
|
bool SendPacket(const uint8_t* data, uint16_t length);
|
||||||
|
const uint8_t* GetMacAddress();
|
||||||
|
bool IsInitialized();
|
||||||
|
|
||||||
|
// Carrier as of the last PHY poll (once a second from deferred work).
|
||||||
|
bool IsLinkUp();
|
||||||
|
|
||||||
|
// Negotiated speed in Mbit/s, 0 while the link is down.
|
||||||
|
uint32_t GetLinkSpeed();
|
||||||
|
|
||||||
|
uint64_t GetRxPacketCount();
|
||||||
|
uint64_t GetTxPacketCount();
|
||||||
|
|
||||||
|
using RxCallback = void(*)(const uint8_t* data, uint16_t length);
|
||||||
|
void SetRxCallback(RxCallback callback);
|
||||||
|
|
||||||
|
// The interrupt only masks and flags; frames are dispatched and the PHY
|
||||||
|
// polled from the idle bottom half. HasDeferredWork also looks at the
|
||||||
|
// ring itself, so receive keeps working if the interrupt never fires.
|
||||||
|
bool HasDeferredWork();
|
||||||
|
void ProcessDeferredWork();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
* Brcmf.hpp
|
||||||
|
* Broadcom/Cypress FullMAC Wi-Fi over SDIO (CYW43455 / CYW43456 on the
|
||||||
|
* Raspberry Pi 4 / 400): driver-internal interface
|
||||||
|
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
|
||||||
|
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
|
||||||
|
// A FullMAC radio runs the 802.11 MLME itself: the host asks for scans and
|
||||||
|
// joins through ioctls and sees Ethernet frames. Layout, after Linux's
|
||||||
|
// brcmfmac:
|
||||||
|
//
|
||||||
|
// BrcmfSdio.cpp the SD controller as an SDIO host (CMD52/CMD53)
|
||||||
|
// BrcmfChip.cpp backplane access, core enumeration (EROM), clocks,
|
||||||
|
// firmware + NVRAM download, F2 bring-up, firmware console
|
||||||
|
// BrcmfProto.cpp SDPCM framing, CDC ioctls, BDC data, the RX pump,
|
||||||
|
// firmware events
|
||||||
|
// BrcmfConnect.cpp preinit, CLM, escan, join, keys, the facade entry points
|
||||||
|
//
|
||||||
|
// The handshake runs on the host (Wpa.cpp), exactly as for the Intel
|
||||||
|
// adapter: EAPOL frames arrive as Ethernet frames and the keys go back to the
|
||||||
|
// firmware with "wsec_key".
|
||||||
|
//
|
||||||
|
// Threading: everything on the bus holds g_brcmf.BusLock for the length of one
|
||||||
|
// SDIO exchange. The RX pump (BrcmfPoll) has a reentrancy guard and releases
|
||||||
|
// the bus lock before dispatching, so the network stack may transmit from
|
||||||
|
// inside a delivered frame. Dispatch must never issue an ioctl -- its answer
|
||||||
|
// is read by the very pump it would be waiting inside. Work needing one is
|
||||||
|
// queued for BrcmfConnectService().
|
||||||
|
|
||||||
|
namespace Drivers::Net::Wifi {
|
||||||
|
|
||||||
|
// Facade-visible adapter state; values are the WIFI_STATE_* ABI ones.
|
||||||
|
enum class BrcmfState : uint8_t {
|
||||||
|
Absent = 0,
|
||||||
|
Detected,
|
||||||
|
Booting,
|
||||||
|
Running,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BrcmfCore {
|
||||||
|
uint16_t Id = 0;
|
||||||
|
uint8_t Rev = 0;
|
||||||
|
uint32_t Base = 0;
|
||||||
|
uint32_t Wrap = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BrcmfDevice {
|
||||||
|
BrcmfState State = BrcmfState::Absent;
|
||||||
|
uint64_t ArasanMmio = 0;
|
||||||
|
|
||||||
|
// Chip
|
||||||
|
uint16_t ChipId = 0;
|
||||||
|
uint8_t ChipRev = 0;
|
||||||
|
const char* FwName = nullptr; // "43455" / "43456"
|
||||||
|
BrcmfCore Cores[16];
|
||||||
|
uint32_t CoreCount = 0;
|
||||||
|
uint32_t RamBase = 0;
|
||||||
|
uint32_t RamSize = 0;
|
||||||
|
uint32_t SdioCoreBase = 0;
|
||||||
|
uint32_t PmuBase = 0;
|
||||||
|
bool SrEnabled = false;
|
||||||
|
uint32_t SharedAddr = 0; // sdpcm_shared, 0 = not read yet
|
||||||
|
uint32_t ConsoleIdx = 0; // console bytes already printed
|
||||||
|
|
||||||
|
// Bus
|
||||||
|
kcp::Mutex BusLock;
|
||||||
|
uint32_t Window = 0xFFFFFFFF; // backplane window, 0xFFFFFFFF = unknown
|
||||||
|
uint32_t HostIntMask = 0;
|
||||||
|
|
||||||
|
// SDPCM
|
||||||
|
uint8_t TxSeq = 0;
|
||||||
|
uint8_t TxMax = 1;
|
||||||
|
bool RxPending = false;
|
||||||
|
|
||||||
|
// Firmware
|
||||||
|
uint8_t Mac[6] = {};
|
||||||
|
char FwVersion[32] = {};
|
||||||
|
uint16_t ChannelCount = 0;
|
||||||
|
bool Band24 = true;
|
||||||
|
bool Band5 = false;
|
||||||
|
|
||||||
|
// Health
|
||||||
|
uint32_t FwErrors = 0;
|
||||||
|
uint32_t CmdFailBucket = 0; // leaky bucket of unanswered ioctls
|
||||||
|
uint64_t RxPackets = 0;
|
||||||
|
uint64_t TxPackets = 0;
|
||||||
|
uint64_t LastPollMs = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern BrcmfDevice g_brcmf;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// BrcmfChip.cpp
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool BrcmfBpRead32(uint32_t addr, uint32_t& value);
|
||||||
|
bool BrcmfBpWrite32(uint32_t addr, uint32_t value);
|
||||||
|
bool BrcmfBpReadMem(uint32_t addr, uint8_t* buffer, uint32_t len);
|
||||||
|
bool BrcmfBpWriteMem(uint32_t addr, const uint8_t* buffer, uint32_t len);
|
||||||
|
// Point the backplane window at `addr` (function 2 transfers want it on
|
||||||
|
// ChipCommon, as Linux keeps it).
|
||||||
|
bool BrcmfBpSelectWindow(uint32_t addr);
|
||||||
|
|
||||||
|
// Function-1 SDIO-core registers in the 0x1000x space.
|
||||||
|
bool BrcmfF1Read8(uint32_t reg, uint8_t& value);
|
||||||
|
bool BrcmfF1Write8(uint32_t reg, uint8_t value);
|
||||||
|
|
||||||
|
// Power the radio, enumerate the SDIO card, identify the chip, download
|
||||||
|
// firmware + NVRAM, start the ARM and bring up function 2. Leaves
|
||||||
|
// g_brcmf ready for SDPCM traffic.
|
||||||
|
bool BrcmfChipBringUp();
|
||||||
|
|
||||||
|
// Hold the radio in reset (WL_ON low).
|
||||||
|
void BrcmfChipPowerOff();
|
||||||
|
|
||||||
|
// Print what the firmware wrote to its console since the last call, and
|
||||||
|
// a trap/assert summary if it has one. Takes the bus lock.
|
||||||
|
void BrcmfDumpConsole();
|
||||||
|
|
||||||
|
// Load 0:/os/firmware/brcm/brcmfmac<chip>-sdio.<ext>. The caller frees.
|
||||||
|
uint8_t* BrcmfLoadFile(const char* ext, uint32_t& size, bool required);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// BrcmfProto.cpp
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
void BrcmfProtoReset();
|
||||||
|
|
||||||
|
// Drain the device: interrupt status, mailbox, every pending frame, and
|
||||||
|
// queued transmits. Reentrancy guarded; returns at once if another core
|
||||||
|
// is already pumping.
|
||||||
|
void BrcmfPoll();
|
||||||
|
|
||||||
|
// Whether a poll is due (card interrupt, queued TX, the periodic sweep).
|
||||||
|
bool BrcmfPollDue(uint64_t now, uint64_t intervalMs);
|
||||||
|
|
||||||
|
// Synchronous firmware command. `buf` carries the request (for a query,
|
||||||
|
// its first bytes, e.g. an iovar name) and receives up to `len` bytes of
|
||||||
|
// answer. Returns false on a transport failure, a timeout, or a negative
|
||||||
|
// firmware status (*fwStatus, when given, receives it).
|
||||||
|
bool BrcmfIoctl(uint32_t cmd, bool set, void* buf, uint32_t len, int32_t* fwStatus = nullptr);
|
||||||
|
|
||||||
|
bool BrcmfSetInt(uint32_t cmd, uint32_t value);
|
||||||
|
bool BrcmfGetInt(uint32_t cmd, uint32_t& value);
|
||||||
|
bool BrcmfIovarSet(const char* name, const void* data, uint32_t len, bool quiet = false);
|
||||||
|
bool BrcmfIovarGet(const char* name, void* out, uint32_t len, bool quiet = false);
|
||||||
|
bool BrcmfIovarSetInt(const char* name, uint32_t value, bool quiet = false);
|
||||||
|
bool BrcmfIovarGetInt(const char* name, uint32_t& value, bool quiet = false);
|
||||||
|
|
||||||
|
// Queue an Ethernet frame for transmission (802.3, 14-byte header).
|
||||||
|
bool BrcmfTxEthernet(const uint8_t* frame, uint32_t len);
|
||||||
|
bool BrcmfTxPending();
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// BrcmfConnect.cpp -- called from the protocol layer
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// A firmware event. Runs inside the pump: no ioctls.
|
||||||
|
void BrcmfOnEvent(uint32_t type, uint32_t status, uint32_t reason, uint16_t flags,
|
||||||
|
const uint8_t* addr, const uint8_t* data, uint32_t len);
|
||||||
|
|
||||||
|
// A received Ethernet frame. Runs inside the pump.
|
||||||
|
void BrcmfOnRxEthernet(const uint8_t* frame, uint32_t len);
|
||||||
|
|
||||||
|
// The firmware halted or stopped answering.
|
||||||
|
void BrcmfOnFirmwareFailure(const char* why);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// BrcmfConnect.cpp -- the facade's backend (Wifi.cpp)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool BrcmfProbe(uint64_t arasanMmio);
|
||||||
|
bool BrcmfCompleteInit();
|
||||||
|
void BrcmfShutdown(); // for recovery: power the radio off
|
||||||
|
|
||||||
|
BrcmfState BrcmfGetState();
|
||||||
|
bool BrcmfHasWork(uint64_t now);
|
||||||
|
void BrcmfService(); // pump + connection service
|
||||||
|
|
||||||
|
bool BrcmfStartScan();
|
||||||
|
void BrcmfAbortScan();
|
||||||
|
bool BrcmfScanActive();
|
||||||
|
|
||||||
|
bool BrcmfConnectStart(const uint8_t* bssid, uint8_t channel, const char* ssid,
|
||||||
|
const char* password, const uint8_t* rsnIe, uint8_t rsnIeLen);
|
||||||
|
void BrcmfConnectAbort();
|
||||||
|
int BrcmfConnectState(); // IwxConnStateId values
|
||||||
|
bool BrcmfConnectRefusedForSecurity();
|
||||||
|
bool BrcmfJoinFailedOnAuth();
|
||||||
|
const char* BrcmfConnectSsid();
|
||||||
|
const uint8_t* BrcmfConnectBssid();
|
||||||
|
uint8_t BrcmfConnectChannel();
|
||||||
|
bool BrcmfLinkUp();
|
||||||
|
bool BrcmfSendEthernet(const uint8_t* frame, uint32_t len);
|
||||||
|
|
||||||
|
bool BrcmfWpaTxEapol(const uint8_t* body, uint32_t len);
|
||||||
|
bool BrcmfWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher);
|
||||||
|
bool BrcmfWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||||
|
uint8_t cipher, const uint8_t* rsc);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Wifi.cpp -- the facade, called by the backend
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// One BSS from a scan: its information elements, as in a beacon body
|
||||||
|
// after the fixed fields.
|
||||||
|
void WifiRxBssInfo(const uint8_t* bssid, uint16_t beaconInterval, uint16_t capability,
|
||||||
|
const uint8_t* ies, uint32_t iesLen, uint8_t channel, int8_t rssiDbm);
|
||||||
|
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,849 @@
|
|||||||
|
/*
|
||||||
|
* BrcmfChip.cpp
|
||||||
|
* Broadcom FullMAC over SDIO: backplane access, core enumeration, clocks,
|
||||||
|
* firmware and NVRAM download, function 2 bring-up, firmware console
|
||||||
|
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
|
||||||
|
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Brcmf.hpp"
|
||||||
|
#include "BrcmfSdio.hpp"
|
||||||
|
#include <Arch/Cpu.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Drivers/Platform/RpiFirmware.hpp>
|
||||||
|
#include <Drivers/Platform/RpiGpio.hpp>
|
||||||
|
#include <Fs/Vfs.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
#include <Memory/Heap.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <Timekeeping/Timer.hpp>
|
||||||
|
|
||||||
|
// The radio is a small SoC of its own (a Broadcom "AI" interconnect): an
|
||||||
|
// ARM Cortex-R4 with its tightly coupled RAM, the 802.11 MAC/PHY core (D11),
|
||||||
|
// ChipCommon (with the PMU) and an SDIO device core. Function 1 of the SDIO
|
||||||
|
// card is a window onto that backplane: three window registers select the
|
||||||
|
// 32 KiB page, and CMD53 at offset | 0x8000 reaches it.
|
||||||
|
//
|
||||||
|
// Bring-up follows Linux's brcmfmac (sdio.c, chip.c, firmware.c):
|
||||||
|
// 1. route GPIO 34-39 to the Arasan controller, pulse WL_ON (line 1 of the
|
||||||
|
// VideoCore's GPIO expander), enumerate the card;
|
||||||
|
// 2. request the ALP clock, read the chip ID, walk the enumeration ROM for
|
||||||
|
// the cores;
|
||||||
|
// 3. halt the ARM and hold the D11 core in reset, size the RAM from the
|
||||||
|
// CR4's bank registers;
|
||||||
|
// 4. write the firmware at the RAM base and the NVRAM at its top (with the
|
||||||
|
// length token in the last word), write the reset vector to address 0
|
||||||
|
// and release the ARM;
|
||||||
|
// 5. wait for the HT clock the firmware brings up, tell the SDIO core the
|
||||||
|
// protocol version, enable function 2 (frames) and its interrupts.
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Drivers::Net::Wifi {
|
||||||
|
|
||||||
|
namespace RpiFirmware = Drivers::Platform::RpiFirmware;
|
||||||
|
namespace RpiGpio = Drivers::Platform::RpiGpio;
|
||||||
|
|
||||||
|
BrcmfDevice g_brcmf;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Registers
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Function 1, SDIO core local space
|
||||||
|
static constexpr uint32_t F1_WATERMARK = 0x10008;
|
||||||
|
static constexpr uint32_t F1_DEVICE_CTL = 0x10009;
|
||||||
|
static constexpr uint32_t F1_SBADDRLOW = 0x1000A;
|
||||||
|
static constexpr uint32_t F1_SBADDRMID = 0x1000B;
|
||||||
|
static constexpr uint32_t F1_SBADDRHIGH = 0x1000C;
|
||||||
|
static constexpr uint32_t F1_CHIPCLKCSR = 0x1000E;
|
||||||
|
static constexpr uint32_t F1_SDIOPULLUP = 0x1000F;
|
||||||
|
static constexpr uint32_t F1_MESBUSYCTRL = 0x1001D;
|
||||||
|
static constexpr uint32_t F1_WAKEUPCTRL = 0x1001E;
|
||||||
|
|
||||||
|
static constexpr uint8_t CLK_FORCE_ALP = 0x01;
|
||||||
|
static constexpr uint8_t CLK_FORCE_HT = 0x02;
|
||||||
|
static constexpr uint8_t CLK_ALP_AVAIL_REQ = 0x08;
|
||||||
|
static constexpr uint8_t CLK_HT_AVAIL_REQ = 0x10;
|
||||||
|
static constexpr uint8_t CLK_FORCE_HW_REQ_OFF= 0x20;
|
||||||
|
static constexpr uint8_t CLK_ALP_AVAIL = 0x40;
|
||||||
|
static constexpr uint8_t CLK_HT_AVAIL = 0x80;
|
||||||
|
|
||||||
|
static constexpr uint8_t DEVCTL_F2WM_ENAB = 0x10;
|
||||||
|
static constexpr uint8_t MESBUSY_ENAB = 0x80;
|
||||||
|
static constexpr uint8_t WAKEUP_HTWAIT = 0x02;
|
||||||
|
|
||||||
|
// CYW43455 values (Linux CY_43455_*): a deeper F2 watermark and
|
||||||
|
// MES busy signalling keep the card's FIFO from overrunning.
|
||||||
|
static constexpr uint8_t F2_WATERMARK_43455 = 0x60;
|
||||||
|
static constexpr uint8_t MES_WATERMARK_43455 = 0x50;
|
||||||
|
|
||||||
|
// Broadcom vendor CCCR
|
||||||
|
static constexpr uint32_t CCCR_BRCM_CARDCAP = 0xF0;
|
||||||
|
static constexpr uint8_t CARDCAP_CMD14 = 0x02 | 0x04; // support | ext
|
||||||
|
static constexpr uint32_t CCCR_BRCM_CARDCTRL = 0xF1;
|
||||||
|
static constexpr uint8_t CARDCTRL_WLANRESET = 0x02;
|
||||||
|
|
||||||
|
// Backplane
|
||||||
|
static constexpr uint32_t SI_ENUM_BASE = 0x18000000; // ChipCommon
|
||||||
|
static constexpr uint32_t CC_CHIPID = 0x000;
|
||||||
|
static constexpr uint32_t CC_CAPS_EXT = 0x0AC;
|
||||||
|
static constexpr uint32_t CAPS_EXT_AOB = 0x40; // PMU in its own "always on" core
|
||||||
|
static constexpr uint32_t CC_EROMPTR = 0x0FC;
|
||||||
|
static constexpr uint32_t CC_PMUCONTROL = 0x600;
|
||||||
|
static constexpr uint32_t CC_CHIPCONTROL_ADDR = 0x650;
|
||||||
|
static constexpr uint32_t CC_CHIPCONTROL_DATA = 0x654;
|
||||||
|
static constexpr uint32_t PMU_CTL_RES_RELOAD = 2u << 13;
|
||||||
|
|
||||||
|
// SDIO device core
|
||||||
|
static constexpr uint32_t SD_INTSTATUS = 0x20;
|
||||||
|
static constexpr uint32_t SD_HOSTINTMASK = 0x24;
|
||||||
|
static constexpr uint32_t SD_TOSBMAILBOXDATA = 0x48;
|
||||||
|
static constexpr uint32_t SDPCM_PROT_VERSION = 4;
|
||||||
|
static constexpr uint32_t HOSTINTMASK = 0x000000F0 | (1u << 29); // HMB SW | CHIPACTIVE
|
||||||
|
|
||||||
|
// Core wrapper (AI)
|
||||||
|
static constexpr uint32_t WRAP_IOCTL = 0x408;
|
||||||
|
static constexpr uint32_t WRAP_RESET_CTL = 0x800;
|
||||||
|
static constexpr uint32_t IOCTL_CLK = 0x0001;
|
||||||
|
static constexpr uint32_t IOCTL_FGC = 0x0002;
|
||||||
|
static constexpr uint32_t RESET_CTL_RESET = 0x0001;
|
||||||
|
|
||||||
|
static constexpr uint32_t CR4_IOCTL_CPUHALT = 0x0020;
|
||||||
|
static constexpr uint32_t D11_IOCTL_PHYCLKEN = 0x0004;
|
||||||
|
static constexpr uint32_t D11_IOCTL_PHYRESET = 0x0008;
|
||||||
|
|
||||||
|
// ARM CR4 core
|
||||||
|
static constexpr uint32_t CR4_CAP = 0x04;
|
||||||
|
static constexpr uint32_t CR4_BANKIDX = 0x40;
|
||||||
|
static constexpr uint32_t CR4_BANKINFO = 0x44;
|
||||||
|
|
||||||
|
// Core IDs
|
||||||
|
static constexpr uint16_t CORE_CHIPCOMMON = 0x800;
|
||||||
|
static constexpr uint16_t CORE_80211 = 0x812;
|
||||||
|
static constexpr uint16_t CORE_PMU = 0x827;
|
||||||
|
static constexpr uint16_t CORE_SDIO_DEV = 0x829;
|
||||||
|
static constexpr uint16_t CORE_ARM_CR4 = 0x83E;
|
||||||
|
static constexpr uint16_t CORE_GCI = 0x840;
|
||||||
|
|
||||||
|
static constexpr uint16_t CHIP_4345 = 0x4345;
|
||||||
|
static constexpr uint32_t RAMBASE_4345 = 0x198000;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Function 1 and the backplane
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool BrcmfF1Read8(uint32_t reg, uint8_t& value) { return Sdio::Read8(Sdio::FN1, reg, value); }
|
||||||
|
bool BrcmfF1Write8(uint32_t reg, uint8_t value) { return Sdio::Write8(Sdio::FN1, reg, value); }
|
||||||
|
|
||||||
|
static bool SetWindow(uint32_t addr) {
|
||||||
|
uint32_t window = addr & ~0x7FFFu;
|
||||||
|
if (window == g_brcmf.Window) return true;
|
||||||
|
bool ok = BrcmfF1Write8(F1_SBADDRLOW, (uint8_t)((window >> 8) & 0x80))
|
||||||
|
&& BrcmfF1Write8(F1_SBADDRMID, (uint8_t)(window >> 16))
|
||||||
|
&& BrcmfF1Write8(F1_SBADDRHIGH, (uint8_t)(window >> 24));
|
||||||
|
g_brcmf.Window = ok ? window : 0xFFFFFFFF;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfBpSelectWindow(uint32_t addr) { return SetWindow(addr); }
|
||||||
|
|
||||||
|
// 0x8000 in the function 1 address selects a 4-byte backplane access.
|
||||||
|
static constexpr uint32_t SB_ACCESS_4B = 0x8000;
|
||||||
|
|
||||||
|
bool BrcmfBpRead32(uint32_t addr, uint32_t& value) {
|
||||||
|
uint32_t word = 0;
|
||||||
|
if (!SetWindow(addr)) return false;
|
||||||
|
if (!Sdio::ReadBlock(Sdio::FN1, (addr & 0x7FFF) | SB_ACCESS_4B, true, (uint8_t*)&word, 4))
|
||||||
|
return false;
|
||||||
|
value = word;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfBpWrite32(uint32_t addr, uint32_t value) {
|
||||||
|
if (!SetWindow(addr)) return false;
|
||||||
|
return Sdio::WriteBlock(Sdio::FN1, (addr & 0x7FFF) | SB_ACCESS_4B, true, (const uint8_t*)&value, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool BpMem(bool write, uint32_t addr, uint8_t* buffer, uint32_t len) {
|
||||||
|
while (len > 0) {
|
||||||
|
uint32_t offset = addr & 0x7FFF;
|
||||||
|
uint32_t run = 0x8000 - offset;
|
||||||
|
if (run > len) run = len;
|
||||||
|
if (!SetWindow(addr)) return false;
|
||||||
|
bool ok = write ? Sdio::WriteBlock(Sdio::FN1, offset, true, buffer, run)
|
||||||
|
: Sdio::ReadBlock(Sdio::FN1, offset, true, buffer, run);
|
||||||
|
if (!ok) return false;
|
||||||
|
addr += run;
|
||||||
|
buffer += run;
|
||||||
|
len -= run;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfBpReadMem(uint32_t addr, uint8_t* buffer, uint32_t len) {
|
||||||
|
return BpMem(false, addr, buffer, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfBpWriteMem(uint32_t addr, const uint8_t* buffer, uint32_t len) {
|
||||||
|
return BpMem(true, addr, (uint8_t*)buffer, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t Rd(uint32_t addr) {
|
||||||
|
uint32_t v = 0;
|
||||||
|
BrcmfBpRead32(addr, v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Cores
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static const BrcmfCore* FindCore(uint16_t id) {
|
||||||
|
for (uint32_t i = 0; i < g_brcmf.CoreCount; i++)
|
||||||
|
if (g_brcmf.Cores[i].Id == id) return &g_brcmf.Cores[i];
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enumeration ROM (Linux brcmf_chip_dmp_erom_scan).
|
||||||
|
static constexpr uint32_t DESC_TYPE_MSK = 0x0000000F;
|
||||||
|
static constexpr uint32_t DESC_VALID = 0x00000001;
|
||||||
|
static constexpr uint32_t DESC_COMPONENT = 0x00000001;
|
||||||
|
static constexpr uint32_t DESC_MASTER_PORT = 0x00000003;
|
||||||
|
static constexpr uint32_t DESC_ADDRESS = 0x00000005;
|
||||||
|
static constexpr uint32_t DESC_ADDRSIZE_GT32 = 0x00000008;
|
||||||
|
static constexpr uint32_t DESC_EOT = 0x0000000F;
|
||||||
|
static constexpr uint32_t COMP_PARTNUM_MASK = 0x000FFF00, COMP_PARTNUM_SHIFT = 8;
|
||||||
|
static constexpr uint32_t COMP_REV_MASK = 0xFF000000, COMP_REV_SHIFT = 24;
|
||||||
|
static constexpr uint32_t COMP_NSW_MASK = 0x00F80000, COMP_NSW_SHIFT = 19;
|
||||||
|
static constexpr uint32_t COMP_NMW_MASK = 0x0007C000, COMP_NMW_SHIFT = 14;
|
||||||
|
static constexpr uint32_t SLAVE_ADDR_BASE = 0xFFFFF000;
|
||||||
|
static constexpr uint32_t SLAVE_TYPE_MASK = 0x000000C0, SLAVE_TYPE_SHIFT = 6;
|
||||||
|
static constexpr uint32_t SLAVE_TYPE_SLAVE = 0;
|
||||||
|
static constexpr uint32_t SLAVE_TYPE_SWRAP = 2;
|
||||||
|
static constexpr uint32_t SLAVE_TYPE_MWRAP = 3;
|
||||||
|
static constexpr uint32_t SLAVE_SIZE_MASK = 0x00000030, SLAVE_SIZE_SHIFT = 4;
|
||||||
|
static constexpr uint32_t SLAVE_SIZE_4K = 0;
|
||||||
|
static constexpr uint32_t SLAVE_SIZE_8K = 1;
|
||||||
|
static constexpr uint32_t SLAVE_SIZE_DESC = 3;
|
||||||
|
|
||||||
|
static uint32_t EromNext(uint32_t& erom, uint32_t* type) {
|
||||||
|
uint32_t v = Rd(erom);
|
||||||
|
erom += 4;
|
||||||
|
uint32_t t = v & DESC_TYPE_MSK;
|
||||||
|
if ((t & ~DESC_ADDRSIZE_GT32) == DESC_ADDRESS) t = DESC_ADDRESS;
|
||||||
|
if (type) *type = t;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool EromRegAddr(uint32_t& erom, uint32_t& regBase, uint32_t& wrapBase) {
|
||||||
|
regBase = 0;
|
||||||
|
wrapBase = 0;
|
||||||
|
uint32_t desc = 0, val = 0;
|
||||||
|
uint32_t wrapType;
|
||||||
|
|
||||||
|
// One descriptor decides it, as in Linux: a master port means the
|
||||||
|
// core's wrapper is a master wrapper (any further master ports are
|
||||||
|
// skipped by the address search below); an address straight away
|
||||||
|
// means a slave wrapper. Cores with master ports -- the CR4, D11,
|
||||||
|
// the SDIO core, and ChipCommon on these chips -- must not be
|
||||||
|
// looked up as slave-wrapped: their wrapper is never found, and the
|
||||||
|
// halt/release writes go nowhere.
|
||||||
|
val = EromNext(erom, &desc);
|
||||||
|
if (desc == DESC_MASTER_PORT) {
|
||||||
|
wrapType = SLAVE_TYPE_MWRAP;
|
||||||
|
} else if (desc == DESC_ADDRESS) {
|
||||||
|
erom -= 4;
|
||||||
|
wrapType = SLAVE_TYPE_SWRAP;
|
||||||
|
} else {
|
||||||
|
erom -= 4;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int guard = 0; guard < 64; guard++) {
|
||||||
|
do {
|
||||||
|
val = EromNext(erom, &desc);
|
||||||
|
if (desc == DESC_EOT) { erom -= 4; return false; }
|
||||||
|
} while (desc != DESC_ADDRESS && desc != DESC_COMPONENT);
|
||||||
|
|
||||||
|
if (desc == DESC_COMPONENT) { erom -= 4; return true; }
|
||||||
|
|
||||||
|
if (val & DESC_ADDRSIZE_GT32) EromNext(erom, nullptr);
|
||||||
|
uint32_t sizeType = (val & SLAVE_SIZE_MASK) >> SLAVE_SIZE_SHIFT;
|
||||||
|
if (sizeType == SLAVE_SIZE_DESC) {
|
||||||
|
uint32_t size = EromNext(erom, nullptr);
|
||||||
|
if (size & DESC_ADDRSIZE_GT32) EromNext(erom, nullptr);
|
||||||
|
}
|
||||||
|
if (sizeType != SLAVE_SIZE_4K && sizeType != SLAVE_SIZE_8K) continue;
|
||||||
|
|
||||||
|
uint32_t slaveType = (val & SLAVE_TYPE_MASK) >> SLAVE_TYPE_SHIFT;
|
||||||
|
if (regBase == 0 && slaveType == SLAVE_TYPE_SLAVE) regBase = val & SLAVE_ADDR_BASE;
|
||||||
|
if (wrapBase == 0 && slaveType == wrapType) wrapBase = val & SLAVE_ADDR_BASE;
|
||||||
|
if (regBase && wrapBase) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ScanCores() {
|
||||||
|
g_brcmf.CoreCount = 0;
|
||||||
|
uint32_t erom = Rd(SI_ENUM_BASE + CC_EROMPTR);
|
||||||
|
if (erom == 0 || erom == 0xFFFFFFFF) return false;
|
||||||
|
|
||||||
|
uint32_t desc = 0;
|
||||||
|
for (int guard = 0; guard < 512 && desc != DESC_EOT; guard++) {
|
||||||
|
uint32_t val = EromNext(erom, &desc);
|
||||||
|
if (!(val & DESC_VALID) || desc != DESC_COMPONENT) continue;
|
||||||
|
uint16_t id = (uint16_t)((val & COMP_PARTNUM_MASK) >> COMP_PARTNUM_SHIFT);
|
||||||
|
|
||||||
|
val = EromNext(erom, &desc);
|
||||||
|
if (desc != DESC_COMPONENT) return false;
|
||||||
|
uint32_t nmw = (val & COMP_NMW_MASK) >> COMP_NMW_SHIFT;
|
||||||
|
uint32_t nsw = (val & COMP_NSW_MASK) >> COMP_NSW_SHIFT;
|
||||||
|
uint8_t rev = (uint8_t)((val & COMP_REV_MASK) >> COMP_REV_SHIFT);
|
||||||
|
if (nmw + nsw == 0 && id != CORE_PMU && id != CORE_GCI) continue;
|
||||||
|
|
||||||
|
uint32_t base = 0, wrap = 0;
|
||||||
|
if (!EromRegAddr(erom, base, wrap)) continue;
|
||||||
|
if (g_brcmf.CoreCount < sizeof(g_brcmf.Cores) / sizeof(g_brcmf.Cores[0])) {
|
||||||
|
g_brcmf.Cores[g_brcmf.CoreCount++] = { id, rev, base, wrap };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||||
|
return FindCore(CORE_CHIPCOMMON) && FindCore(CORE_SDIO_DEV) && cr4 && cr4->Wrap != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CoreDisable(const BrcmfCore* core, uint32_t prereset, uint32_t reset) {
|
||||||
|
if (!(Rd(core->Wrap + WRAP_RESET_CTL) & RESET_CTL_RESET)) {
|
||||||
|
BrcmfBpWrite32(core->Wrap + WRAP_IOCTL, prereset | IOCTL_FGC | IOCTL_CLK);
|
||||||
|
Rd(core->Wrap + WRAP_IOCTL);
|
||||||
|
BrcmfBpWrite32(core->Wrap + WRAP_RESET_CTL, RESET_CTL_RESET);
|
||||||
|
Timekeeping::BusyWaitUs(20);
|
||||||
|
for (int i = 0; i < 30 && Rd(core->Wrap + WRAP_RESET_CTL) != RESET_CTL_RESET; i++)
|
||||||
|
Timekeeping::BusyWaitUs(10);
|
||||||
|
}
|
||||||
|
BrcmfBpWrite32(core->Wrap + WRAP_IOCTL, reset | IOCTL_FGC | IOCTL_CLK);
|
||||||
|
Rd(core->Wrap + WRAP_IOCTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CoreReset(const BrcmfCore* core, uint32_t prereset, uint32_t reset, uint32_t postreset) {
|
||||||
|
CoreDisable(core, prereset, reset);
|
||||||
|
for (int i = 0; i < 50 && (Rd(core->Wrap + WRAP_RESET_CTL) & RESET_CTL_RESET); i++) {
|
||||||
|
BrcmfBpWrite32(core->Wrap + WRAP_RESET_CTL, 0);
|
||||||
|
Timekeeping::BusyWaitUs(50);
|
||||||
|
}
|
||||||
|
BrcmfBpWrite32(core->Wrap + WRAP_IOCTL, postreset | IOCTL_CLK);
|
||||||
|
Rd(core->Wrap + WRAP_IOCTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CR4's TCM: ATCM and BTCM bank counts in CAP, each bank's size in
|
||||||
|
// 8 KiB units in BANKINFO.
|
||||||
|
static uint32_t Cr4RamSize(const BrcmfCore* cr4) {
|
||||||
|
uint32_t cap = Rd(cr4->Base + CR4_CAP);
|
||||||
|
uint32_t banks = (cap & 0xF) + ((cap >> 4) & 0xF);
|
||||||
|
uint32_t size = 0;
|
||||||
|
for (uint32_t i = 0; i < banks; i++) {
|
||||||
|
BrcmfBpWrite32(cr4->Base + CR4_BANKIDX, i);
|
||||||
|
uint32_t info = Rd(cr4->Base + CR4_BANKINFO);
|
||||||
|
size += ((info & 0x3F) + 1) * 8192;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Firmware files
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
uint8_t* BrcmfLoadFile(const char* ext, uint32_t& size, bool required) {
|
||||||
|
char path[80];
|
||||||
|
int n = 0;
|
||||||
|
auto add = [&](const char* s) { while (*s && n < 78) path[n++] = *s++; };
|
||||||
|
add("0:/os/firmware/brcm/brcmfmac");
|
||||||
|
add(g_brcmf.FwName);
|
||||||
|
add("-sdio.");
|
||||||
|
add(ext);
|
||||||
|
path[n] = '\0';
|
||||||
|
|
||||||
|
size = 0;
|
||||||
|
Fs::Vfs::BackendFile file;
|
||||||
|
if (Fs::Vfs::OpenBackendFile(path, file) < 0) {
|
||||||
|
if (required) KernelLogStream(ERROR, "WiFi") << "Firmware file not found: " << path;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
uint64_t fileSize = Fs::Vfs::GetBackendFileSize(file);
|
||||||
|
if (fileSize == 0 || fileSize > 4 * 1024 * 1024) {
|
||||||
|
Fs::Vfs::CloseBackendFile(file);
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Unusable firmware file " << path;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// Rounded up and zero padded: everything goes over the bus in dwords.
|
||||||
|
uint32_t padded = ((uint32_t)fileSize + 3) & ~3u;
|
||||||
|
uint8_t* data = (uint8_t*)Memory::g_heap->Request(padded);
|
||||||
|
if (!data) {
|
||||||
|
Fs::Vfs::CloseBackendFile(file);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
memset(data, 0, padded);
|
||||||
|
int got = Fs::Vfs::ReadBackendFile(file, data, 0, fileSize);
|
||||||
|
Fs::Vfs::CloseBackendFile(file);
|
||||||
|
if (got != (int)fileSize) {
|
||||||
|
Memory::g_heap->Free(data);
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Short read of " << path;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
size = (uint32_t)fileSize;
|
||||||
|
KernelLogStream(INFO, "WiFi") << "Loaded " << path << " (" << (uint64_t)size << " bytes)";
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The NVRAM text ("key=value" lines, '#' comments) becomes the binary
|
||||||
|
// form the firmware reads: NUL-terminated pairs, zero padded to a dword
|
||||||
|
// (at least one extra NUL), then a token word holding the padded length
|
||||||
|
// in dwords and its complement (Linux brcmf_fw_nvram_strip).
|
||||||
|
static uint8_t* BuildNvram(const uint8_t* text, uint32_t len, uint32_t& outLen) {
|
||||||
|
uint8_t* out = (uint8_t*)Memory::g_heap->Request(len + 8);
|
||||||
|
if (!out) return nullptr;
|
||||||
|
uint32_t n = 0;
|
||||||
|
uint32_t i = 0;
|
||||||
|
auto printable = [](uint8_t c) { return c > 0x20 && c < 0x7F && c != '#'; };
|
||||||
|
while (i < len) {
|
||||||
|
while (i < len && (text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n')) i++;
|
||||||
|
if (i >= len) break;
|
||||||
|
if (text[i] == '#') {
|
||||||
|
while (i < len && text[i] != '\n') i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint32_t start = i;
|
||||||
|
bool sawEquals = false;
|
||||||
|
while (i < len && printable(text[i])) {
|
||||||
|
if (text[i] == '=') sawEquals = true;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (sawEquals && i > start) {
|
||||||
|
memcpy(out + n, text + start, i - start);
|
||||||
|
n += i - start;
|
||||||
|
out[n++] = '\0';
|
||||||
|
}
|
||||||
|
while (i < len && text[i] != '\n') i++;
|
||||||
|
}
|
||||||
|
uint32_t padded = (n + 1 + 3) & ~3u;
|
||||||
|
while (n < padded) out[n++] = 0;
|
||||||
|
uint32_t words = padded / 4;
|
||||||
|
uint32_t token = (~words << 16) | (words & 0xFFFF);
|
||||||
|
memcpy(out + n, &token, 4);
|
||||||
|
outLen = n + 4;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool DownloadAndVerify(uint32_t addr, const uint8_t* data, uint32_t len) {
|
||||||
|
if (!BrcmfBpWriteMem(addr, data, len)) return false;
|
||||||
|
|
||||||
|
// Read the image back: a marginal SDIO bus shows up here as a clean
|
||||||
|
// error instead of as a firmware that never starts.
|
||||||
|
uint8_t* check = (uint8_t*)Memory::g_heap->Request(4096);
|
||||||
|
if (!check) return true;
|
||||||
|
bool ok = true;
|
||||||
|
for (uint32_t off = 0; off < len && ok; off += 4096) {
|
||||||
|
uint32_t run = len - off < 4096 ? len - off : 4096;
|
||||||
|
if (!BrcmfBpReadMem(addr + off, check, run) || memcmp(check, data + off, run) != 0) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Firmware verify failed at offset 0x"
|
||||||
|
<< base::hex << (uint64_t)off << base::dec;
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Memory::g_heap->Free(check);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Bring-up
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static bool WaitClock(uint8_t bit, uint64_t timeoutMs) {
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
for (;;) {
|
||||||
|
uint8_t v = 0;
|
||||||
|
if (BrcmfF1Read8(F1_CHIPCLKCSR, v) && (v & bit)) return true;
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||||
|
Timekeeping::BusyWaitUs(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfChipPowerOff() {
|
||||||
|
RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_WL_ON, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool PowerOnAndEnumerate() {
|
||||||
|
// GPIO 34-39: CLK, CMD, DAT0-3 on ALT3 (the Arasan), pulled up but
|
||||||
|
// for the clock, as Linux's sdio_pins.
|
||||||
|
for (uint32_t pin = 34; pin <= 39; pin++) {
|
||||||
|
RpiGpio::SetFunction(pin, RpiGpio::Function::Alt3);
|
||||||
|
RpiGpio::SetPull(pin, pin == 34 ? RpiGpio::Pull::None : RpiGpio::Pull::Up);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_WL_ON, false)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Cannot switch the radio's power (WL_ON)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Timekeeping::BusyWaitMs(20);
|
||||||
|
RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_WL_ON, true);
|
||||||
|
Timekeeping::BusyWaitMs(250);
|
||||||
|
|
||||||
|
// The legacy EMMC clock feeds the Arasan; nothing on the boot path
|
||||||
|
// uses that controller, so make sure the clock is not gated.
|
||||||
|
if (!RpiFirmware::SetClockState(RpiFirmware::CLOCK_EMMC, true))
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Could not switch the EMMC clock on";
|
||||||
|
uint32_t clockHz = 0;
|
||||||
|
if (!RpiFirmware::GetClockRate(RpiFirmware::CLOCK_EMMC, clockHz)) clockHz = 0;
|
||||||
|
if (!Sdio::HostInit(g_brcmf.ArasanMmio, clockHz)) return false;
|
||||||
|
return Sdio::CardInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool AttachChip() {
|
||||||
|
g_brcmf.Window = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
// ALP first: the backplane runs from it until the firmware has set
|
||||||
|
// up the PLL.
|
||||||
|
uint8_t req = CLK_FORCE_HW_REQ_OFF | CLK_ALP_AVAIL_REQ;
|
||||||
|
uint8_t v = 0;
|
||||||
|
if (!BrcmfF1Write8(F1_CHIPCLKCSR, req) || !BrcmfF1Read8(F1_CHIPCLKCSR, v) ||
|
||||||
|
(v & ~(CLK_ALP_AVAIL | CLK_HT_AVAIL)) != req) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "ChipClkCSR: wrote 0x" << base::hex << (uint64_t)req
|
||||||
|
<< ", read 0x" << (uint64_t)v << base::dec;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!WaitClock(CLK_ALP_AVAIL, 100)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "ALP clock never became available";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_FORCE_HW_REQ_OFF | CLK_FORCE_ALP);
|
||||||
|
Timekeeping::BusyWaitUs(65);
|
||||||
|
BrcmfF1Write8(F1_SDIOPULLUP, 0);
|
||||||
|
|
||||||
|
uint32_t chipId = 0;
|
||||||
|
if (!BrcmfBpRead32(SI_ENUM_BASE + CC_CHIPID, chipId)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Backplane read failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_brcmf.ChipId = (uint16_t)(chipId & 0xFFFF);
|
||||||
|
g_brcmf.ChipRev = (uint8_t)((chipId >> 16) & 0xF);
|
||||||
|
uint32_t socType = chipId >> 28;
|
||||||
|
|
||||||
|
KernelLogStream(INFO, "WiFi") << "Broadcom chip " << base::hex << (uint64_t)g_brcmf.ChipId
|
||||||
|
<< base::dec << " rev " << (uint64_t)g_brcmf.ChipRev;
|
||||||
|
|
||||||
|
if (g_brcmf.ChipId != CHIP_4345 || socType != 1) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Unsupported chip (only the BCM4345 family: "
|
||||||
|
"CYW43455 / CYW43456)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Linux's table: revision 9 is the CYW43456 (Pi 400), the others
|
||||||
|
// the CYW43455 (Pi 4B).
|
||||||
|
g_brcmf.FwName = g_brcmf.ChipRev == 9 ? "43456" : "43455";
|
||||||
|
|
||||||
|
if (!ScanCores()) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Enumeration ROM scan found no usable cores";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Linux brcmf_chip_get_pmu: the PMU registers live in a core of their
|
||||||
|
// own only when ChipCommon (rev 35+) says the always-on block exists;
|
||||||
|
// otherwise they are ChipCommon's, at the same offsets.
|
||||||
|
const BrcmfCore* cc = FindCore(CORE_CHIPCOMMON);
|
||||||
|
const BrcmfCore* pmu = FindCore(CORE_PMU);
|
||||||
|
uint32_t capsExt = cc->Rev >= 35 ? Rd(cc->Base + CC_CAPS_EXT) : 0;
|
||||||
|
g_brcmf.PmuBase = (pmu && (capsExt & CAPS_EXT_AOB)) ? pmu->Base : cc->Base;
|
||||||
|
g_brcmf.SdioCoreBase = FindCore(CORE_SDIO_DEV)->Base;
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < g_brcmf.CoreCount; i++) {
|
||||||
|
const BrcmfCore& c = g_brcmf.Cores[i];
|
||||||
|
KernelLogStream(INFO, "WiFi") << " core 0x" << base::hex << (uint64_t)c.Id << " rev "
|
||||||
|
<< base::dec << (uint64_t)c.Rev << " at 0x" << base::hex << (uint64_t)c.Base
|
||||||
|
<< ", wrapper 0x" << (uint64_t)c.Wrap << base::dec;
|
||||||
|
}
|
||||||
|
KernelLogStream(INFO, "WiFi") << "ChipCommon rev " << (uint64_t)cc->Rev << ", caps_ext 0x"
|
||||||
|
<< base::hex << (uint64_t)capsExt << ", PMU registers at 0x" << (uint64_t)g_brcmf.PmuBase
|
||||||
|
<< base::dec;
|
||||||
|
|
||||||
|
// Passive: ARM halted, D11 held in reset with its PHY clock on.
|
||||||
|
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||||
|
CoreReset(cr4, CR4_IOCTL_CPUHALT, CR4_IOCTL_CPUHALT, CR4_IOCTL_CPUHALT);
|
||||||
|
if (const BrcmfCore* d11 = FindCore(CORE_80211)) {
|
||||||
|
CoreReset(d11, D11_IOCTL_PHYRESET | D11_IOCTL_PHYCLKEN, D11_IOCTL_PHYCLKEN, D11_IOCTL_PHYCLKEN);
|
||||||
|
}
|
||||||
|
uint32_t haltIoctl = Rd(cr4->Wrap + WRAP_IOCTL);
|
||||||
|
uint32_t haltReset = Rd(cr4->Wrap + WRAP_RESET_CTL);
|
||||||
|
if (haltIoctl != (CR4_IOCTL_CPUHALT | IOCTL_CLK) || (haltReset & RESET_CTL_RESET)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "The CR4 did not take the halt (wrapper 0x" << base::hex
|
||||||
|
<< (uint64_t)cr4->Wrap << ": ioctl 0x" << (uint64_t)haltIoctl << " reset 0x"
|
||||||
|
<< (uint64_t)haltReset << base::dec << ", expected 0x21 / 0x0)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_brcmf.RamBase = RAMBASE_4345;
|
||||||
|
g_brcmf.RamSize = Cr4RamSize(cr4);
|
||||||
|
if (g_brcmf.RamSize == 0 || g_brcmf.RamSize > 4 * 1024 * 1024) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Implausible RAM size " << (uint64_t)g_brcmf.RamSize;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An SDIO card reset also resets the WLAN backplane, and a backplane
|
||||||
|
// reset reloads the PMU state.
|
||||||
|
uint8_t cardCtrl = 0;
|
||||||
|
if (Sdio::Read8(Sdio::FN0, CCCR_BRCM_CARDCTRL, cardCtrl))
|
||||||
|
Sdio::Write8(Sdio::FN0, CCCR_BRCM_CARDCTRL, cardCtrl | CARDCTRL_WLANRESET);
|
||||||
|
BrcmfBpWrite32(g_brcmf.PmuBase + CC_PMUCONTROL,
|
||||||
|
Rd(g_brcmf.PmuBase + CC_PMUCONTROL) | PMU_CTL_RES_RELOAD);
|
||||||
|
|
||||||
|
KernelLogStream(INFO, "WiFi") << "CYW" << g_brcmf.FwName << ": " << (uint64_t)g_brcmf.CoreCount
|
||||||
|
<< " cores, " << (uint64_t)(g_brcmf.RamSize / 1024) << " KiB RAM at 0x" << base::hex
|
||||||
|
<< (uint64_t)g_brcmf.RamBase << base::dec;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool DownloadFirmware() {
|
||||||
|
uint32_t fwLen = 0, nvLen = 0;
|
||||||
|
uint8_t* fw = BrcmfLoadFile("bin", fwLen, true);
|
||||||
|
if (!fw) return false;
|
||||||
|
uint8_t* nvText = BrcmfLoadFile("txt", nvLen, true);
|
||||||
|
if (!nvText) {
|
||||||
|
Memory::g_heap->Free(fw);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t nvramLen = 0;
|
||||||
|
uint8_t* nvram = BuildNvram(nvText, nvLen, nvramLen);
|
||||||
|
Memory::g_heap->Free(nvText);
|
||||||
|
uint32_t fwPadded = (fwLen + 3) & ~3u;
|
||||||
|
|
||||||
|
bool ok = nvram && fwPadded + nvramLen < g_brcmf.RamSize;
|
||||||
|
if (!ok) KernelLogStream(ERROR, "WiFi") << "Firmware and NVRAM do not fit in the radio's RAM";
|
||||||
|
|
||||||
|
uint32_t rstvec = 0;
|
||||||
|
memcpy(&rstvec, fw, 4);
|
||||||
|
|
||||||
|
// Download under a plain ALP request, as Linux does (alp_only):
|
||||||
|
// the probe's FORCE_HW_REQ_OFF | FORCE_ALP must be gone before the
|
||||||
|
// ARM starts, or the firmware's own clock requests -- the HT clock
|
||||||
|
// its PLL setup asks for above all -- are blocked.
|
||||||
|
if (ok) {
|
||||||
|
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_ALP_AVAIL_REQ);
|
||||||
|
if (!WaitClock(CLK_ALP_AVAIL, 100))
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "ALP clock lost on its plain request";
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
if (ok && !DownloadAndVerify(g_brcmf.RamBase, fw, fwPadded)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Firmware download failed";
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
if (ok && !DownloadAndVerify(g_brcmf.RamBase + g_brcmf.RamSize - nvramLen, nvram, nvramLen)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "NVRAM download failed";
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
KernelLogStream(INFO, "WiFi") << "Firmware and NVRAM written in "
|
||||||
|
<< (Timekeeping::GetMilliseconds() - start) << " ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
Memory::g_heap->Free(fw);
|
||||||
|
if (nvram) Memory::g_heap->Free(nvram);
|
||||||
|
if (!ok) return false;
|
||||||
|
|
||||||
|
// Start the ARM: clear the SDIO core's interrupts, put the reset
|
||||||
|
// vector at address 0 and take the CPU out of halt.
|
||||||
|
BrcmfBpWrite32(g_brcmf.SdioCoreBase + SD_INTSTATUS, 0xFFFFFFFF);
|
||||||
|
uint32_t vecBack = 0;
|
||||||
|
bool vecOk = BrcmfBpWriteMem(0, (const uint8_t*)&rstvec, 4)
|
||||||
|
&& BrcmfBpReadMem(0, (uint8_t*)&vecBack, 4) && vecBack == rstvec;
|
||||||
|
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||||
|
CoreReset(cr4, CR4_IOCTL_CPUHALT, 0, 0);
|
||||||
|
if (!vecOk)
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Reset vector did not stick: address 0 reads 0x"
|
||||||
|
<< base::hex << (uint64_t)vecBack << base::dec;
|
||||||
|
KernelLogStream(INFO, "WiFi") << "Reset vector 0x" << base::hex << (uint64_t)rstvec
|
||||||
|
<< "; CR4 released: ioctl 0x" << (uint64_t)Rd(cr4->Wrap + WRAP_IOCTL) << " reset 0x"
|
||||||
|
<< (uint64_t)Rd(cr4->Wrap + WRAP_RESET_CTL) << base::dec << " (expect 0x1 / 0x0)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save/restore: the firmware can power its domains down between
|
||||||
|
// frames; the host then has to allow for the HT clock coming back.
|
||||||
|
static bool SrCapable() {
|
||||||
|
BrcmfBpWrite32(g_brcmf.PmuBase + CC_CHIPCONTROL_ADDR, 3);
|
||||||
|
return (Rd(g_brcmf.PmuBase + CC_CHIPCONTROL_DATA) & (1u << 2)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool StartFrames() {
|
||||||
|
// Clear the ALP force, then ask for HT: the running firmware brings
|
||||||
|
// up the PLL.
|
||||||
|
BrcmfF1Write8(F1_CHIPCLKCSR, 0);
|
||||||
|
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_HT_AVAIL_REQ);
|
||||||
|
bool ht = WaitClock(CLK_HT_AVAIL, 20);
|
||||||
|
if (!ht) {
|
||||||
|
// Not yet: a snapshot 20 ms in tells a CPU that never ran from
|
||||||
|
// one that ran and was reset.
|
||||||
|
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||||
|
uint8_t csr = 0;
|
||||||
|
BrcmfF1Read8(F1_CHIPCLKCSR, csr);
|
||||||
|
KernelLogStream(INFO, "WiFi") << "20 ms after release: ChipClkCSR 0x" << base::hex
|
||||||
|
<< (uint64_t)csr << ", CR4 ioctl 0x" << (uint64_t)Rd(cr4->Wrap + WRAP_IOCTL) << " reset 0x"
|
||||||
|
<< (uint64_t)Rd(cr4->Wrap + WRAP_RESET_CTL) << ", RAM top 0x"
|
||||||
|
<< (uint64_t)Rd(g_brcmf.RamBase + g_brcmf.RamSize - 4) << base::dec;
|
||||||
|
ht = WaitClock(CLK_HT_AVAIL, 2000);
|
||||||
|
}
|
||||||
|
if (!ht) {
|
||||||
|
// What the hardware was doing, for the next attempt: the clock
|
||||||
|
// register (or whether function 1 still answers at all), the
|
||||||
|
// CPU's wrapper state, and whether the firmware ever replaced
|
||||||
|
// the NVRAM token in the last word of RAM with its shared area.
|
||||||
|
uint8_t csr = 0;
|
||||||
|
bool csrOk = BrcmfF1Read8(F1_CHIPCLKCSR, csr);
|
||||||
|
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
|
||||||
|
uint32_t ioctl = Rd(cr4->Wrap + WRAP_IOCTL);
|
||||||
|
uint32_t reset = Rd(cr4->Wrap + WRAP_RESET_CTL);
|
||||||
|
uint32_t top = Rd(g_brcmf.RamBase + g_brcmf.RamSize - 4);
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "HT clock never became available (firmware did not start)";
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "ChipClkCSR " << (csrOk ? "0x" : "unreadable")
|
||||||
|
<< base::hex << (uint64_t)(csrOk ? csr : 0) << ", CR4 ioctl 0x" << (uint64_t)ioctl
|
||||||
|
<< " reset 0x" << (uint64_t)reset << ", RAM top word 0x" << (uint64_t)top
|
||||||
|
<< base::dec << (((~top >> 16) & 0xFFFF) == (top & 0xFFFF)
|
||||||
|
? " (still the NVRAM token)" : " (written by the firmware)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t saveClk = 0;
|
||||||
|
BrcmfF1Read8(F1_CHIPCLKCSR, saveClk);
|
||||||
|
BrcmfF1Write8(F1_CHIPCLKCSR, saveClk | CLK_FORCE_HT);
|
||||||
|
|
||||||
|
BrcmfBpWrite32(g_brcmf.SdioCoreBase + SD_TOSBMAILBOXDATA, SDPCM_PROT_VERSION << 16);
|
||||||
|
if (!Sdio::EnableFunction(Sdio::FN2, true, 3000)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Function 2 did not become ready";
|
||||||
|
Sdio::EnableFunction(Sdio::FN2, false, 0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_brcmf.HostIntMask = HOSTINTMASK;
|
||||||
|
BrcmfBpWrite32(g_brcmf.SdioCoreBase + SD_HOSTINTMASK, HOSTINTMASK);
|
||||||
|
|
||||||
|
BrcmfF1Write8(F1_WATERMARK, F2_WATERMARK_43455);
|
||||||
|
uint8_t devCtl = 0;
|
||||||
|
if (BrcmfF1Read8(F1_DEVICE_CTL, devCtl))
|
||||||
|
BrcmfF1Write8(F1_DEVICE_CTL, devCtl | DEVCTL_F2WM_ENAB);
|
||||||
|
BrcmfF1Write8(F1_MESBUSYCTRL, MES_WATERMARK_43455 | MESBUSY_ENAB);
|
||||||
|
|
||||||
|
g_brcmf.SrEnabled = SrCapable();
|
||||||
|
if (g_brcmf.SrEnabled) {
|
||||||
|
uint8_t wake = 0;
|
||||||
|
if (BrcmfF1Read8(F1_WAKEUPCTRL, wake)) BrcmfF1Write8(F1_WAKEUPCTRL, wake | WAKEUP_HTWAIT);
|
||||||
|
Sdio::Write8(Sdio::FN0, CCCR_BRCM_CARDCAP, CARDCAP_CMD14);
|
||||||
|
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_FORCE_HT);
|
||||||
|
} else {
|
||||||
|
BrcmfF1Write8(F1_CHIPCLKCSR, saveClk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master, function 1 and function 2.
|
||||||
|
Sdio::EnableCardInterrupts(0x07);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfChipBringUp() {
|
||||||
|
g_brcmf.BusLock.Acquire();
|
||||||
|
g_brcmf.SharedAddr = 0;
|
||||||
|
g_brcmf.ConsoleIdx = 0;
|
||||||
|
g_brcmf.RamSize = 0;
|
||||||
|
bool ok = PowerOnAndEnumerate() && AttachChip() && DownloadFirmware() && StartFrames();
|
||||||
|
g_brcmf.BusLock.Release();
|
||||||
|
if (!ok) {
|
||||||
|
// A firmware that started and then trapped has said why on its
|
||||||
|
// console (no-op when it never got that far).
|
||||||
|
BrcmfDumpConsole();
|
||||||
|
BrcmfChipPowerOff();
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Firmware console and trap report
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static bool InRam(uint32_t addr, uint32_t len) {
|
||||||
|
return addr >= g_brcmf.RamBase && addr + len <= g_brcmf.RamBase + g_brcmf.RamSize
|
||||||
|
&& addr + len >= addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ReadShared(uint32_t sh[16]) {
|
||||||
|
if (!g_brcmf.SharedAddr) {
|
||||||
|
uint32_t addr = Rd(g_brcmf.RamBase + g_brcmf.RamSize - 4);
|
||||||
|
// Still the NVRAM token: the firmware has not published it.
|
||||||
|
if (((~addr >> 16) & 0xFFFF) == (addr & 0xFFFF) || !InRam(addr, 64)) return false;
|
||||||
|
g_brcmf.SharedAddr = addr;
|
||||||
|
}
|
||||||
|
return BrcmfBpReadMem(g_brcmf.SharedAddr, (uint8_t*)sh, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void EmitConsole(const char* buf, uint32_t size, uint32_t from, uint32_t to) {
|
||||||
|
char line[161];
|
||||||
|
uint32_t n = 0;
|
||||||
|
for (uint32_t i = from; i != to; i = (i + 1) % size) {
|
||||||
|
char c = buf[i];
|
||||||
|
if (c == '\r') continue;
|
||||||
|
if (c == '\n' || n == sizeof(line) - 1) {
|
||||||
|
line[n] = '\0';
|
||||||
|
if (n) KernelLogStream(INFO, "WiFi-FW") << line;
|
||||||
|
n = 0;
|
||||||
|
if (c == '\n') continue;
|
||||||
|
}
|
||||||
|
line[n++] = (c >= 0x20 && c < 0x7F) ? c : '.';
|
||||||
|
}
|
||||||
|
line[n] = '\0';
|
||||||
|
if (n) KernelLogStream(INFO, "WiFi-FW") << line;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfDumpConsole() {
|
||||||
|
if (g_brcmf.RamSize == 0) return;
|
||||||
|
g_brcmf.BusLock.Acquire();
|
||||||
|
|
||||||
|
// sdpcm_shared: flags, trap_addr, assert_exp_addr, assert_file_addr,
|
||||||
|
// assert_line, console_addr, msgtrace_addr, tag[32], brpt_addr
|
||||||
|
uint32_t sh[16] = {};
|
||||||
|
if (!ReadShared(sh)) {
|
||||||
|
g_brcmf.BusLock.Release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t flags = sh[0];
|
||||||
|
|
||||||
|
// rte_console: vcons_in, vcons_out, then the log: buf, buf_size,
|
||||||
|
// idx, out_idx.
|
||||||
|
uint32_t log[4] = {};
|
||||||
|
if (InRam(sh[5], 24) && BrcmfBpReadMem(sh[5] + 8, (uint8_t*)log, 16)
|
||||||
|
&& log[1] > 0 && log[1] <= 16384 && InRam(log[0], log[1]) && log[2] < log[1]) {
|
||||||
|
uint32_t size = log[1];
|
||||||
|
char* buf = (char*)Memory::g_heap->Request((size + 3) & ~3u);
|
||||||
|
if (buf && BrcmfBpReadMem(log[0], (uint8_t*)buf, (size + 3) & ~3u)) {
|
||||||
|
if (g_brcmf.ConsoleIdx >= size) g_brcmf.ConsoleIdx = 0;
|
||||||
|
EmitConsole(buf, size, g_brcmf.ConsoleIdx, log[2]);
|
||||||
|
g_brcmf.ConsoleIdx = log[2];
|
||||||
|
}
|
||||||
|
if (buf) Memory::g_heap->Free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & 0x0400) && InRam(sh[1], 72)) {
|
||||||
|
// brcmf_trap_info: type, epc, cpsr, spsr, r0-r12, sp, lr, pc
|
||||||
|
uint32_t trap[20] = {};
|
||||||
|
if (BrcmfBpReadMem(sh[1], (uint8_t*)trap, sizeof(trap))) {
|
||||||
|
KernelLogStream(ERROR, "WiFi-FW") << "Firmware trap: type 0x" << base::hex
|
||||||
|
<< (uint64_t)trap[0] << ", epc 0x" << (uint64_t)trap[1] << ", lr 0x"
|
||||||
|
<< (uint64_t)trap[18] << ", pc 0x" << (uint64_t)trap[19] << base::dec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags & 0x0200) {
|
||||||
|
char file[64] = {};
|
||||||
|
if (InRam(sh[3], 60)) BrcmfBpReadMem(sh[3], (uint8_t*)file, 60);
|
||||||
|
file[63] = '\0';
|
||||||
|
KernelLogStream(ERROR, "WiFi-FW") << "Firmware assert at " << file << ":"
|
||||||
|
<< (uint64_t)sh[4];
|
||||||
|
}
|
||||||
|
g_brcmf.BusLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,902 @@
|
|||||||
|
/*
|
||||||
|
* BrcmfConnect.cpp
|
||||||
|
* Broadcom FullMAC over SDIO: firmware preinit (CLM, events), escan,
|
||||||
|
* join, key installation, link state, and the entry points the Wi-Fi
|
||||||
|
* facade calls
|
||||||
|
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
|
||||||
|
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Brcmf.hpp"
|
||||||
|
#include "BrcmfSdio.hpp"
|
||||||
|
#include "Ieee80211.hpp"
|
||||||
|
#include "Wpa.hpp"
|
||||||
|
#include <Api/Syscall.hpp>
|
||||||
|
#include <Arch/Cpu.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Drivers/Platform/RpiFirmware.hpp>
|
||||||
|
#include <Libraries/Crypto.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
#include <Memory/Heap.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <Timekeeping/Timer.hpp>
|
||||||
|
|
||||||
|
// Joining follows brcmf_cfg80211_connect with the supplicant on the host:
|
||||||
|
// "wpaie" the RSN element our supplicant built (the firmware puts it in
|
||||||
|
// the association request, and it must match message 2's copy)
|
||||||
|
// "wpa_auth" WPA2-PSK (0x80) or PSK-SHA256 (0x8000), 0 for open
|
||||||
|
// "auth" open system
|
||||||
|
// "wsec" AES (4) for CCMP, 0 for open
|
||||||
|
// "join" SSID, BSSID and the chanspec escan reported
|
||||||
|
// The firmware answers with E_SET_SSID (status 0 = associated) and E_LINK;
|
||||||
|
// the access point then starts the 4-way handshake, whose EAPOL frames come
|
||||||
|
// and go as ordinary Ethernet frames. The keys it produces go to the
|
||||||
|
// firmware with "wsec_key". The radio does AES-CCMP only: GCMP networks are
|
||||||
|
// refused.
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
using namespace montauk::abi;
|
||||||
|
|
||||||
|
namespace Drivers::Net::Wifi {
|
||||||
|
|
||||||
|
// WLC ioctls
|
||||||
|
static constexpr uint32_t WLC_UP = 2;
|
||||||
|
static constexpr uint32_t WLC_SET_INFRA = 20;
|
||||||
|
static constexpr uint32_t WLC_SET_SSID = 26;
|
||||||
|
static constexpr uint32_t WLC_SCAN = 50;
|
||||||
|
static constexpr uint32_t WLC_DISASSOC = 52;
|
||||||
|
static constexpr uint32_t WLC_SET_PM = 86;
|
||||||
|
static constexpr uint32_t WLC_GET_VALID_CHANNELS = 217;
|
||||||
|
|
||||||
|
// Events
|
||||||
|
static constexpr uint32_t E_SET_SSID = 0;
|
||||||
|
static constexpr uint32_t E_AUTH = 3;
|
||||||
|
static constexpr uint32_t E_DEAUTH = 5;
|
||||||
|
static constexpr uint32_t E_DEAUTH_IND = 6;
|
||||||
|
static constexpr uint32_t E_ASSOC = 7;
|
||||||
|
static constexpr uint32_t E_REASSOC = 9;
|
||||||
|
static constexpr uint32_t E_DISASSOC = 11;
|
||||||
|
static constexpr uint32_t E_DISASSOC_IND = 12;
|
||||||
|
static constexpr uint32_t E_LINK = 16;
|
||||||
|
static constexpr uint32_t E_ROAM = 19;
|
||||||
|
static constexpr uint32_t E_PSK_SUP = 46;
|
||||||
|
static constexpr uint32_t E_ESCAN_RESULT = 69;
|
||||||
|
|
||||||
|
static constexpr uint32_t STATUS_SUCCESS = 0;
|
||||||
|
static constexpr uint32_t STATUS_PARTIAL = 8;
|
||||||
|
static constexpr uint16_t EVENT_FLAG_LINK = 0x01;
|
||||||
|
|
||||||
|
static constexpr uint32_t WPA_AUTH_DISABLED = 0;
|
||||||
|
static constexpr uint32_t WPA2_AUTH_PSK = 0x80;
|
||||||
|
static constexpr uint32_t WPA2_AUTH_PSK_SHA256= 0x8000;
|
||||||
|
static constexpr uint32_t AES_ENABLED = 4;
|
||||||
|
static constexpr uint32_t CRYPTO_ALGO_AES_CCM = 4;
|
||||||
|
static constexpr uint32_t KEY_FLAG_PRIMARY = 1u << 1;
|
||||||
|
|
||||||
|
|
||||||
|
static constexpr uint64_t JOIN_TIMEOUT_MS = 12000;
|
||||||
|
static constexpr uint64_t SCAN_TIMEOUT_MS = 25000;
|
||||||
|
static constexpr uint64_t POLL_ACTIVE_MS = 10;
|
||||||
|
static constexpr uint64_t POLL_IDLE_MS = 250;
|
||||||
|
|
||||||
|
// The NVRAM files ship one address for every board; the radio's own
|
||||||
|
// OTP normally overrides it.
|
||||||
|
static constexpr uint8_t NVRAM_PLACEHOLDER_MAC[6] = { 0xb8, 0x27, 0xeb, 0x74, 0xf2, 0x6c };
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// State
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static volatile uint32_t g_conn = WIFI_CONN_IDLE;
|
||||||
|
static uint8_t g_bssid[6];
|
||||||
|
static char g_ssid[33];
|
||||||
|
static uint8_t g_ssidLen = 0;
|
||||||
|
static uint8_t g_channel = 0;
|
||||||
|
static bool g_secure = false;
|
||||||
|
static bool g_refusedSecurity = false;
|
||||||
|
static bool g_authFailed = false;
|
||||||
|
static bool g_sawEapol = false;
|
||||||
|
static uint64_t g_joinStartMs = 0;
|
||||||
|
static uint64_t g_lastServiceMs = 0;
|
||||||
|
static WpaConfig g_wpaCfg = {};
|
||||||
|
|
||||||
|
// Raised by events inside the pump, consumed by ConnectService().
|
||||||
|
static volatile bool g_evAssoc = false;
|
||||||
|
static volatile bool g_evJoinFailed = false;
|
||||||
|
static volatile bool g_evLinkDown = false;
|
||||||
|
static volatile uint32_t g_evStatus = 0;
|
||||||
|
static volatile uint32_t g_evReason = 0;
|
||||||
|
|
||||||
|
static volatile bool g_scanActive = false;
|
||||||
|
static uint64_t g_scanStartMs = 0;
|
||||||
|
static uint16_t g_syncId = 0;
|
||||||
|
static uint32_t g_scanSeen = 0;
|
||||||
|
|
||||||
|
static volatile bool g_dumpConsole = false;
|
||||||
|
static bool g_failureLogged = false;
|
||||||
|
|
||||||
|
// The chanspec escan reported for each BSS, in the firmware's own
|
||||||
|
// encoding, so the join names the channel exactly.
|
||||||
|
struct BssChan { uint8_t Bssid[6]; uint16_t Chanspec; bool Used; };
|
||||||
|
static BssChan g_chans[64];
|
||||||
|
static kcp::Spinlock g_chanLock;
|
||||||
|
|
||||||
|
struct PendingEapol { uint32_t Len; uint8_t Data[512]; };
|
||||||
|
static constexpr uint32_t EAPOL_DEPTH = 4;
|
||||||
|
static PendingEapol g_eapolQ[EAPOL_DEPTH];
|
||||||
|
static uint32_t g_eapolHead = 0, g_eapolTail = 0;
|
||||||
|
static kcp::Spinlock g_eapolLock;
|
||||||
|
|
||||||
|
static inline uint16_t Le16(const uint8_t* p) { return (uint16_t)(p[0] | p[1] << 8); }
|
||||||
|
static inline uint32_t Le32(const uint8_t* p) {
|
||||||
|
return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
|
||||||
|
}
|
||||||
|
static inline void Put32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); }
|
||||||
|
static inline void Put16(uint8_t* p, uint16_t v) { memcpy(p, &v, 2); }
|
||||||
|
|
||||||
|
static bool Elapsed(uint64_t now, uint64_t since, uint64_t ms) {
|
||||||
|
return now >= since && now - since >= ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PutMac(KernelLogStream& s, const uint8_t* m) {
|
||||||
|
static const char hex[] = "0123456789abcdef";
|
||||||
|
char buf[18];
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
buf[i * 3] = hex[m[i] >> 4];
|
||||||
|
buf[i * 3 + 1] = hex[m[i] & 0xF];
|
||||||
|
buf[i * 3 + 2] = i < 5 ? ':' : '\0';
|
||||||
|
}
|
||||||
|
s << buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Events (inside the pump: record, never send)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static void RememberChanspec(const uint8_t* bssid, uint16_t chanspec) {
|
||||||
|
g_chanLock.Acquire();
|
||||||
|
BssChan* slot = nullptr;
|
||||||
|
for (auto& c : g_chans) {
|
||||||
|
if (c.Used && memcmp(c.Bssid, bssid, 6) == 0) { slot = &c; break; }
|
||||||
|
if (!c.Used && !slot) slot = &c;
|
||||||
|
}
|
||||||
|
if (slot) {
|
||||||
|
memcpy(slot->Bssid, bssid, 6);
|
||||||
|
slot->Chanspec = chanspec;
|
||||||
|
slot->Used = true;
|
||||||
|
}
|
||||||
|
g_chanLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t LookupChanspec(const uint8_t* bssid, uint8_t channel) {
|
||||||
|
uint16_t found = 0;
|
||||||
|
g_chanLock.Acquire();
|
||||||
|
for (auto& c : g_chans) {
|
||||||
|
if (c.Used && memcmp(c.Bssid, bssid, 6) == 0) { found = c.Chanspec; break; }
|
||||||
|
}
|
||||||
|
g_chanLock.Release();
|
||||||
|
if (found) return found;
|
||||||
|
// 802.11ac encoding: channel, 20 MHz, band.
|
||||||
|
return (uint16_t)(channel | 0x1000 | (channel > 14 ? 0xC000 : 0x0000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// brcmf_escan_result_le: buflen, version, sync_id, bss_count, then
|
||||||
|
// brcmf_bss_info_le records (128 bytes fixed, IEs at ie_offset).
|
||||||
|
static void HandleEscanResult(const uint8_t* data, uint32_t len) {
|
||||||
|
if (len < 12) return;
|
||||||
|
uint16_t count = Le16(data + 10);
|
||||||
|
uint32_t off = 12;
|
||||||
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
|
if (off + 128 > len) break;
|
||||||
|
const uint8_t* bi = data + off;
|
||||||
|
uint32_t biLen = Le32(bi + 4);
|
||||||
|
if (biLen < 128 || biLen > len - off) break;
|
||||||
|
|
||||||
|
const uint8_t* bssid = bi + 8;
|
||||||
|
uint16_t beacon = Le16(bi + 14);
|
||||||
|
uint16_t caps = Le16(bi + 16);
|
||||||
|
uint16_t chanspec = Le16(bi + 72);
|
||||||
|
int16_t rssi = (int16_t)Le16(bi + 78);
|
||||||
|
uint8_t ctlCh = bi[88];
|
||||||
|
uint16_t ieOff = Le16(bi + 116);
|
||||||
|
uint32_t ieLen = Le32(bi + 120);
|
||||||
|
if (ieOff > biLen || ieLen > biLen - ieOff) ieLen = 0;
|
||||||
|
|
||||||
|
uint8_t channel = ctlCh ? ctlCh : (uint8_t)(chanspec & 0xFF);
|
||||||
|
if (rssi < -127) rssi = -127;
|
||||||
|
if (rssi > 0) rssi = 0;
|
||||||
|
RememberChanspec(bssid, chanspec);
|
||||||
|
WifiRxBssInfo(bssid, beacon, caps, ieLen ? bi + ieOff : nullptr, ieLen,
|
||||||
|
channel, (int8_t)rssi);
|
||||||
|
g_scanSeen++;
|
||||||
|
off += biLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfOnEvent(uint32_t type, uint32_t status, uint32_t reason, uint16_t flags,
|
||||||
|
const uint8_t* addr, const uint8_t* data, uint32_t len) {
|
||||||
|
(void)addr;
|
||||||
|
switch (type) {
|
||||||
|
case E_ESCAN_RESULT:
|
||||||
|
if (status == STATUS_PARTIAL) {
|
||||||
|
HandleEscanResult(data, len);
|
||||||
|
} else if (g_scanActive) {
|
||||||
|
g_scanActive = false;
|
||||||
|
KernelLogStream(INFO, "WiFi") << "Scan " << (status == STATUS_SUCCESS ? "complete" : "ended")
|
||||||
|
<< ": " << (uint64_t)g_scanSeen << " results";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case E_SET_SSID:
|
||||||
|
if (g_conn != WIFI_CONN_ASSOCIATING) break;
|
||||||
|
if (status == STATUS_SUCCESS) {
|
||||||
|
g_evAssoc = true;
|
||||||
|
} else {
|
||||||
|
g_evStatus = status;
|
||||||
|
g_evJoinFailed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case E_LINK:
|
||||||
|
if (flags & EVENT_FLAG_LINK) {
|
||||||
|
if (g_conn == WIFI_CONN_ASSOCIATING) g_evAssoc = true;
|
||||||
|
} else if (g_conn >= WIFI_CONN_ASSOCIATED && g_conn != WIFI_CONN_FAILED) {
|
||||||
|
g_evReason = reason;
|
||||||
|
g_evLinkDown = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case E_DEAUTH:
|
||||||
|
case E_DEAUTH_IND:
|
||||||
|
case E_DISASSOC_IND:
|
||||||
|
if (g_conn >= WIFI_CONN_ASSOCIATED && g_conn != WIFI_CONN_FAILED) {
|
||||||
|
g_evReason = reason;
|
||||||
|
g_evLinkDown = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case E_AUTH:
|
||||||
|
if (status != STATUS_SUCCESS && g_conn == WIFI_CONN_ASSOCIATING)
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Authentication rejected (status "
|
||||||
|
<< (uint64_t)status << ", reason " << (uint64_t)reason << ")";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case E_ASSOC:
|
||||||
|
case E_REASSOC:
|
||||||
|
if (status != STATUS_SUCCESS && g_conn == WIFI_CONN_ASSOCIATING)
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Association rejected (status "
|
||||||
|
<< (uint64_t)status << ", reason " << (uint64_t)reason << ")";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void QueueEapol(const uint8_t* body, uint32_t len) {
|
||||||
|
if (len > sizeof(g_eapolQ[0].Data)) return;
|
||||||
|
g_eapolLock.Acquire();
|
||||||
|
uint32_t next = (g_eapolHead + 1) % EAPOL_DEPTH;
|
||||||
|
if (next == g_eapolTail) g_eapolTail = (g_eapolTail + 1) % EAPOL_DEPTH;
|
||||||
|
memcpy(g_eapolQ[g_eapolHead].Data, body, len);
|
||||||
|
g_eapolQ[g_eapolHead].Len = len;
|
||||||
|
g_eapolHead = next;
|
||||||
|
g_eapolLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool EapolPending() {
|
||||||
|
g_eapolLock.Acquire();
|
||||||
|
bool pending = g_eapolHead != g_eapolTail;
|
||||||
|
g_eapolLock.Release();
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfOnRxEthernet(const uint8_t* frame, uint32_t len) {
|
||||||
|
uint16_t etherType = (uint16_t)(frame[12] << 8 | frame[13]);
|
||||||
|
if (etherType == ETHERTYPE_EAPOL) {
|
||||||
|
if (g_secure && g_conn >= WIFI_CONN_ASSOCIATING && g_conn != WIFI_CONN_FAILED) {
|
||||||
|
QueueEapol(frame + 14, len - 14);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (BrcmfLinkUp()) WifiRxEthernet(frame, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfOnFirmwareFailure(const char* why) {
|
||||||
|
if (g_brcmf.State != BrcmfState::Running) return;
|
||||||
|
g_brcmf.State = BrcmfState::Error;
|
||||||
|
g_brcmf.FwErrors++;
|
||||||
|
g_scanActive = false;
|
||||||
|
g_dumpConsole = true;
|
||||||
|
if (!g_failureLogged) {
|
||||||
|
g_failureLogged = true;
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Radio failed: " << why;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Bring-up
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// CLM blob: the regulatory database the firmware ships without
|
||||||
|
// ("clm_min"). Sent in 1400-byte chunks behind brcmf_dload_data_le:
|
||||||
|
// flag (handler version 1 << 12 | begin 2 | end 4), type (2 = CLM),
|
||||||
|
// length, CRC (unused), data.
|
||||||
|
static bool LoadClm() {
|
||||||
|
uint32_t size = 0;
|
||||||
|
uint8_t* blob = BrcmfLoadFile("clm_blob", size, false);
|
||||||
|
if (!blob) {
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "No CLM blob; the firmware's own regulatory data is used";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint32_t CHUNK = 1400;
|
||||||
|
constexpr uint32_t HDR = 12;
|
||||||
|
uint8_t* chunk = (uint8_t*)Memory::g_heap->Request(HDR + CHUNK + 4);
|
||||||
|
bool ok = chunk != nullptr;
|
||||||
|
uint32_t off = 0;
|
||||||
|
uint16_t flag = 1u << 12 | 0x0002;
|
||||||
|
while (ok && off < size) {
|
||||||
|
uint32_t n = size - off > CHUNK ? CHUNK : size - off;
|
||||||
|
if (off + n == size) flag |= 0x0004;
|
||||||
|
memset(chunk, 0, HDR + CHUNK + 4);
|
||||||
|
Put16(chunk, flag);
|
||||||
|
Put16(chunk + 2, 2);
|
||||||
|
Put32(chunk + 4, n);
|
||||||
|
Put32(chunk + 8, 0);
|
||||||
|
memcpy(chunk + HDR, blob + off, n);
|
||||||
|
// Linux sends sizeof(struct) + len - 1: three bytes of padding.
|
||||||
|
ok = BrcmfIovarSet("clmload", chunk, HDR + n + 3);
|
||||||
|
flag &= (uint16_t)~0x0002;
|
||||||
|
off += n;
|
||||||
|
}
|
||||||
|
if (chunk) Memory::g_heap->Free(chunk);
|
||||||
|
Memory::g_heap->Free(blob);
|
||||||
|
|
||||||
|
uint32_t status = 0;
|
||||||
|
if (ok && BrcmfIovarGetInt("clmload_status", status, true) && status != 0) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "The firmware rejected the CLM blob (status "
|
||||||
|
<< (uint64_t)status << ")";
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
if (ok) KernelLogStream(INFO, "WiFi") << "CLM blob loaded (" << (uint64_t)size << " bytes)";
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetEventMask() {
|
||||||
|
uint8_t mask[16] = {};
|
||||||
|
const uint32_t events[] = {
|
||||||
|
E_SET_SSID, E_AUTH, E_DEAUTH, E_DEAUTH_IND, E_ASSOC, E_REASSOC, E_DISASSOC,
|
||||||
|
E_DISASSOC_IND, E_LINK, E_ROAM, E_PSK_SUP, E_ESCAN_RESULT,
|
||||||
|
};
|
||||||
|
for (uint32_t e : events) mask[e / 8] |= (uint8_t)(1u << (e % 8));
|
||||||
|
BrcmfIovarSet("event_msgs", mask, sizeof(mask));
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Version: 7.45.265 (28bca26 CY) CRC: ..." -> "7.45.265"
|
||||||
|
static void ParseVersion(const char* ver) {
|
||||||
|
const char* p = ver;
|
||||||
|
const char* key = "version ";
|
||||||
|
while (*p) {
|
||||||
|
const char* a = p;
|
||||||
|
const char* b = key;
|
||||||
|
while (*a && *b && (*a | 0x20) == *b) { a++; b++; }
|
||||||
|
if (!*b) { p = a; break; }
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (!*p) p = ver;
|
||||||
|
int n = 0;
|
||||||
|
while (p[n] && p[n] != ' ' && p[n] != '\n' && n < 31) {
|
||||||
|
g_brcmf.FwVersion[n] = p[n];
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
g_brcmf.FwVersion[n] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ReadChannels() {
|
||||||
|
uint32_t list[65] = {};
|
||||||
|
list[0] = 64;
|
||||||
|
g_brcmf.ChannelCount = 0;
|
||||||
|
if (!BrcmfIoctl(WLC_GET_VALID_CHANNELS, false, list, sizeof(list))) return;
|
||||||
|
uint32_t count = list[0] > 64 ? 64 : list[0];
|
||||||
|
bool b24 = false, b5 = false;
|
||||||
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
|
if (list[1 + i] > 14) b5 = true;
|
||||||
|
else if (list[1 + i] > 0) b24 = true;
|
||||||
|
}
|
||||||
|
g_brcmf.ChannelCount = (uint16_t)count;
|
||||||
|
g_brcmf.Band24 = b24;
|
||||||
|
g_brcmf.Band5 = b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Preinit() {
|
||||||
|
uint8_t mac[6] = {};
|
||||||
|
if (!BrcmfIovarGet("cur_etheraddr", mac, 6)) return false;
|
||||||
|
if (memcmp(mac, NVRAM_PLACEHOLDER_MAC, 6) == 0) {
|
||||||
|
// Raspberry Pi convention: wlan0 is the board (Ethernet) address
|
||||||
|
// plus one.
|
||||||
|
uint8_t board[6];
|
||||||
|
if (Drivers::Platform::RpiFirmware::GetBoardMacAddress(board)) {
|
||||||
|
memcpy(mac, board, 6);
|
||||||
|
for (int i = 5; i >= 3; i--) if (++mac[i] != 0) break;
|
||||||
|
BrcmfIovarSet("cur_etheraddr", mac, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memcpy(g_brcmf.Mac, mac, 6);
|
||||||
|
|
||||||
|
char ver[256] = {};
|
||||||
|
if (BrcmfIovarGet("ver", ver, sizeof(ver) - 1)) {
|
||||||
|
ver[sizeof(ver) - 1] = '\0';
|
||||||
|
for (char& c : ver) if (c == '\n' || c == '\r') c = ' ';
|
||||||
|
KernelLogStream(INFO, "WiFi") << "Firmware: " << ver;
|
||||||
|
ParseVersion(ver);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LoadClm()) return false;
|
||||||
|
|
||||||
|
// No glomming in either direction: one SDPCM frame per transfer.
|
||||||
|
BrcmfIovarSetInt("bus:txglom", 0, true);
|
||||||
|
BrcmfIovarSetInt("bus:rxglom", 0, true);
|
||||||
|
|
||||||
|
BrcmfSetInt(WLC_SET_INFRA, 1);
|
||||||
|
// Roaming would re-key without the host's supplicant; ARP/ND offload
|
||||||
|
// would answer for an address the firmware was never told.
|
||||||
|
BrcmfIovarSetInt("roam_off", 1, true);
|
||||||
|
BrcmfIovarSetInt("arpoe", 0, true);
|
||||||
|
BrcmfIovarSetInt("ndoe", 0, true);
|
||||||
|
// Power save off: the radio misses broadcasts (ARP, DHCP offers)
|
||||||
|
// while dozing, and the Pi is not on a battery.
|
||||||
|
BrcmfSetInt(WLC_SET_PM, 0);
|
||||||
|
|
||||||
|
SetEventMask();
|
||||||
|
if (!BrcmfSetInt(WLC_UP, 1)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "The firmware would not bring the interface up";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ReadChannels();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ResetConnection() {
|
||||||
|
g_conn = WIFI_CONN_IDLE;
|
||||||
|
g_evAssoc = false;
|
||||||
|
g_evJoinFailed = false;
|
||||||
|
g_evLinkDown = false;
|
||||||
|
g_secure = false;
|
||||||
|
g_sawEapol = false;
|
||||||
|
g_eapolLock.Acquire();
|
||||||
|
g_eapolHead = g_eapolTail = 0;
|
||||||
|
g_eapolLock.Release();
|
||||||
|
WpaReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfProbe(uint64_t arasanMmio) {
|
||||||
|
if (g_brcmf.State != BrcmfState::Absent) return false;
|
||||||
|
g_brcmf.ArasanMmio = arasanMmio;
|
||||||
|
g_brcmf.State = BrcmfState::Detected;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfCompleteInit() {
|
||||||
|
g_brcmf.State = BrcmfState::Booting;
|
||||||
|
g_failureLogged = false;
|
||||||
|
g_dumpConsole = false;
|
||||||
|
g_scanActive = false;
|
||||||
|
g_brcmf.CmdFailBucket = 0;
|
||||||
|
ResetConnection();
|
||||||
|
BrcmfProtoReset();
|
||||||
|
|
||||||
|
// One retry from power-off: the Bluetooth half of the same chip may
|
||||||
|
// be powering up or taking its patch download at the same moment.
|
||||||
|
bool up = BrcmfChipBringUp();
|
||||||
|
if (!up) {
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Bring-up failed; power-cycling the radio and trying once more";
|
||||||
|
Timekeeping::BusyWaitMs(100);
|
||||||
|
up = BrcmfChipBringUp();
|
||||||
|
}
|
||||||
|
if (!up) {
|
||||||
|
g_brcmf.State = BrcmfState::Error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Running from here: the preinit below is the first SDPCM traffic.
|
||||||
|
g_brcmf.LastPollMs = Timekeeping::GetMilliseconds();
|
||||||
|
g_brcmf.State = BrcmfState::Running;
|
||||||
|
BrcmfDumpConsole();
|
||||||
|
|
||||||
|
if (!Preinit()) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "Firmware initialization failed";
|
||||||
|
BrcmfDumpConsole();
|
||||||
|
g_brcmf.State = BrcmfState::Error;
|
||||||
|
BrcmfChipPowerOff();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BrcmfDumpConsole();
|
||||||
|
|
||||||
|
auto log = KernelLogStream(OK, "WiFi");
|
||||||
|
log << "CYW" << g_brcmf.FwName << " ready: firmware " << g_brcmf.FwVersion << ", "
|
||||||
|
<< (uint64_t)g_brcmf.ChannelCount << " channels, MAC ";
|
||||||
|
PutMac(log, g_brcmf.Mac);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfShutdown() {
|
||||||
|
g_scanActive = false;
|
||||||
|
ResetConnection();
|
||||||
|
BrcmfChipPowerOff();
|
||||||
|
g_brcmf.State = BrcmfState::Detected;
|
||||||
|
}
|
||||||
|
|
||||||
|
BrcmfState BrcmfGetState() { return g_brcmf.State; }
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Scanning
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool BrcmfScanActive() { return g_scanActive; }
|
||||||
|
|
||||||
|
bool BrcmfStartScan() {
|
||||||
|
if (g_brcmf.State != BrcmfState::Running) return false;
|
||||||
|
if (g_scanActive) return true;
|
||||||
|
|
||||||
|
// brcmf_escan_params_le: version 1, action 1 (start), sync id, then
|
||||||
|
// brcmf_scan_params_le: SSID (any), BSSID (broadcast), BSS type any,
|
||||||
|
// active scan, firmware defaults for every dwell, all channels.
|
||||||
|
uint8_t p[72] = {};
|
||||||
|
Put32(p, 1);
|
||||||
|
Put16(p + 4, 1);
|
||||||
|
Put16(p + 6, ++g_syncId);
|
||||||
|
memset(p + 44, 0xFF, 6);
|
||||||
|
p[50] = 2;
|
||||||
|
p[51] = 0;
|
||||||
|
Put32(p + 52, 0xFFFFFFFF);
|
||||||
|
Put32(p + 56, 0xFFFFFFFF);
|
||||||
|
Put32(p + 60, 0xFFFFFFFF);
|
||||||
|
Put32(p + 64, 0xFFFFFFFF);
|
||||||
|
Put32(p + 68, 0);
|
||||||
|
|
||||||
|
g_scanSeen = 0;
|
||||||
|
g_scanStartMs = Timekeeping::GetMilliseconds();
|
||||||
|
g_scanActive = true; // results may arrive while the ioctl is pumping
|
||||||
|
if (!BrcmfIovarSet("escan", p, sizeof(p))) {
|
||||||
|
g_scanActive = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfAbortScan() {
|
||||||
|
if (!g_scanActive) return;
|
||||||
|
// A WLC_SCAN naming channel -1 cancels the scan in progress.
|
||||||
|
uint8_t p[68] = {};
|
||||||
|
memset(p + 36, 0xFF, 6);
|
||||||
|
p[42] = 2;
|
||||||
|
Put32(p + 44, 0xFFFFFFFF);
|
||||||
|
Put32(p + 48, 0xFFFFFFFF);
|
||||||
|
Put32(p + 52, 0xFFFFFFFF);
|
||||||
|
Put32(p + 56, 0xFFFFFFFF);
|
||||||
|
Put32(p + 60, 1);
|
||||||
|
Put16(p + 64, 0xFFFF);
|
||||||
|
BrcmfIoctl(WLC_SCAN, true, p, sizeof(p));
|
||||||
|
g_scanActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Joining
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
int BrcmfConnectState() { return (int)g_conn; }
|
||||||
|
bool BrcmfConnectRefusedForSecurity() { return g_refusedSecurity; }
|
||||||
|
bool BrcmfJoinFailedOnAuth() { return g_authFailed || WpaGetState() == WpaState::Failed; }
|
||||||
|
const char* BrcmfConnectSsid() { return g_ssid; }
|
||||||
|
const uint8_t* BrcmfConnectBssid() { return g_bssid; }
|
||||||
|
uint8_t BrcmfConnectChannel() { return g_channel; }
|
||||||
|
bool BrcmfLinkUp() { return g_brcmf.State == BrcmfState::Running && g_conn == WIFI_CONN_CONNECTED; }
|
||||||
|
|
||||||
|
static void Disassociate() {
|
||||||
|
uint8_t scb[12] = {};
|
||||||
|
Put32(scb, 3); // deauthenticated: leaving
|
||||||
|
memcpy(scb + 4, g_bssid, 6);
|
||||||
|
BrcmfIoctl(WLC_DISASSOC, true, scb, sizeof(scb));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfConnectAbort() {
|
||||||
|
if (g_conn != WIFI_CONN_IDLE && g_brcmf.State == BrcmfState::Running) Disassociate();
|
||||||
|
ResetConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfConnectStart(const uint8_t* bssid, uint8_t channel, const char* ssid,
|
||||||
|
const char* password, const uint8_t* rsnIe, uint8_t rsnIeLen) {
|
||||||
|
if (g_brcmf.State != BrcmfState::Running) return false;
|
||||||
|
if (g_conn != WIFI_CONN_IDLE) BrcmfConnectAbort();
|
||||||
|
g_refusedSecurity = false;
|
||||||
|
g_authFailed = false;
|
||||||
|
|
||||||
|
memcpy(g_bssid, bssid, 6);
|
||||||
|
g_channel = channel;
|
||||||
|
g_ssidLen = 0;
|
||||||
|
while (ssid && g_ssidLen < 32 && ssid[g_ssidLen]) {
|
||||||
|
g_ssid[g_ssidLen] = ssid[g_ssidLen];
|
||||||
|
g_ssidLen++;
|
||||||
|
}
|
||||||
|
g_ssid[g_ssidLen] = '\0';
|
||||||
|
|
||||||
|
bool secure = rsnIe && rsnIeLen > 0;
|
||||||
|
uint8_t ie[64];
|
||||||
|
uint32_t ieLen = 0;
|
||||||
|
if (secure) {
|
||||||
|
memset(&g_wpaCfg, 0, sizeof(g_wpaCfg));
|
||||||
|
if (!WpaParseApRsn(rsnIe, rsnIeLen, g_wpaCfg)) {
|
||||||
|
g_refusedSecurity = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (g_wpaCfg.PairwiseCipher != RSN_CIPHER_CCMP || g_wpaCfg.GroupCipher != RSN_CIPHER_CCMP) {
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "This radio does AES-CCMP only; the network uses GCMP";
|
||||||
|
g_refusedSecurity = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t pl = 0;
|
||||||
|
if (password) while (pl < sizeof(g_wpaCfg.Passphrase) - 1 && password[pl]) pl++;
|
||||||
|
if (pl == 0) {
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "This network is encrypted and needs a passphrase";
|
||||||
|
g_refusedSecurity = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(g_wpaCfg.Passphrase, password, pl);
|
||||||
|
g_wpaCfg.Passphrase[pl] = '\0';
|
||||||
|
g_wpaCfg.PassLen = (uint8_t)pl;
|
||||||
|
memcpy(g_wpaCfg.OwnMac, g_brcmf.Mac, 6);
|
||||||
|
memcpy(g_wpaCfg.Bssid, g_bssid, 6);
|
||||||
|
memcpy(g_wpaCfg.Ssid, g_ssid, g_ssidLen);
|
||||||
|
g_wpaCfg.SsidLen = g_ssidLen;
|
||||||
|
if (!WpaStart(g_wpaCfg)) {
|
||||||
|
g_refusedSecurity = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ieLen = WpaBuildRsnIe(ie, sizeof(ie));
|
||||||
|
if (ieLen == 0) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_scanActive) BrcmfAbortScan();
|
||||||
|
|
||||||
|
bool sha256 = secure && g_wpaCfg.Akm == RSN_AKM_PSK_SHA256;
|
||||||
|
if (secure) BrcmfIovarSet("wpaie", ie, ieLen);
|
||||||
|
else BrcmfIovarSet("wpaie", nullptr, 0, true);
|
||||||
|
bool ok = BrcmfIovarSetInt("wpa_auth", secure ? (sha256 ? WPA2_AUTH_PSK_SHA256 : WPA2_AUTH_PSK)
|
||||||
|
: WPA_AUTH_DISABLED)
|
||||||
|
&& BrcmfIovarSetInt("auth", 0)
|
||||||
|
&& BrcmfIovarSetInt("wsec", secure ? AES_ENABLED : 0);
|
||||||
|
BrcmfIovarSetInt("mfp", 0, true);
|
||||||
|
BrcmfIovarSetInt("sup_wpa", 0, true);
|
||||||
|
if (!ok) {
|
||||||
|
WpaReset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t chanspec = LookupChanspec(g_bssid, channel);
|
||||||
|
|
||||||
|
g_secure = secure;
|
||||||
|
g_evAssoc = false;
|
||||||
|
g_evJoinFailed = false;
|
||||||
|
g_evLinkDown = false;
|
||||||
|
g_joinStartMs = Timekeeping::GetMilliseconds();
|
||||||
|
g_conn = WIFI_CONN_ASSOCIATING; // before "join": its events may land while it pumps
|
||||||
|
|
||||||
|
// brcmf_ext_join_params_le: SSID; join scan (type default, probes
|
||||||
|
// and dwells as Linux uses for a known channel); BSSID, one chanspec.
|
||||||
|
uint8_t jp[72] = {};
|
||||||
|
Put32(jp, g_ssidLen);
|
||||||
|
memcpy(jp + 4, g_ssid, g_ssidLen);
|
||||||
|
jp[36] = 0xFF;
|
||||||
|
Put32(jp + 40, 16);
|
||||||
|
Put32(jp + 44, 320);
|
||||||
|
Put32(jp + 48, 400);
|
||||||
|
Put32(jp + 52, 0xFFFFFFFF);
|
||||||
|
memcpy(jp + 56, g_bssid, 6);
|
||||||
|
Put32(jp + 64, 1);
|
||||||
|
Put16(jp + 68, chanspec);
|
||||||
|
|
||||||
|
if (!BrcmfIovarSet("join", jp, sizeof(jp), true)) {
|
||||||
|
// Older firmware: WLC_SET_SSID with brcmf_join_params_le.
|
||||||
|
uint8_t sp[52] = {};
|
||||||
|
Put32(sp, g_ssidLen);
|
||||||
|
memcpy(sp + 4, g_ssid, g_ssidLen);
|
||||||
|
memcpy(sp + 36, g_bssid, 6);
|
||||||
|
Put32(sp + 44, 1);
|
||||||
|
Put16(sp + 48, chanspec);
|
||||||
|
if (!BrcmfIoctl(WLC_SET_SSID, true, sp, sizeof(sp))) {
|
||||||
|
KernelLogStream(ERROR, "WiFi") << "The firmware refused the join request";
|
||||||
|
ResetConnection();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto log = KernelLogStream(INFO, "WiFi");
|
||||||
|
log << "Joining \"" << g_ssid << "\" (";
|
||||||
|
PutMac(log, g_bssid);
|
||||||
|
log << ", channel " << (uint64_t)channel << ", chanspec 0x" << base::hex << (uint64_t)chanspec
|
||||||
|
<< base::dec << (secure ? ", WPA2-PSK)" : ", open)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Keys and EAPOL (the supplicant's hooks)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool BrcmfWpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||||
|
uint8_t frame[14 + 512];
|
||||||
|
if (len > 512) return false;
|
||||||
|
memcpy(frame, g_bssid, 6);
|
||||||
|
memcpy(frame + 6, g_brcmf.Mac, 6);
|
||||||
|
frame[12] = ETHERTYPE_EAPOL >> 8;
|
||||||
|
frame[13] = ETHERTYPE_EAPOL & 0xFF;
|
||||||
|
memcpy(frame + 14, body, len);
|
||||||
|
return BrcmfTxEthernet(frame, 14 + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// brcmf_wsec_key_le: index, len, data[32], pad[18], algo, flags, pad[3],
|
||||||
|
// iv_initialized, pad, rxiv {hi, lo}, pad[2], ea[6] -- 164 bytes.
|
||||||
|
static bool SendKey(uint32_t index, const uint8_t* key, uint32_t keyLen, uint32_t flags,
|
||||||
|
const uint8_t* ea, const uint8_t* rsc) {
|
||||||
|
if (keyLen > 32) return false;
|
||||||
|
uint8_t k[164] = {};
|
||||||
|
Put32(k, index);
|
||||||
|
Put32(k + 4, keyLen);
|
||||||
|
memcpy(k + 8, key, keyLen);
|
||||||
|
Put32(k + 112, CRYPTO_ALGO_AES_CCM);
|
||||||
|
Put32(k + 116, flags);
|
||||||
|
if (rsc) {
|
||||||
|
Put32(k + 132, 1);
|
||||||
|
Put32(k + 140, (uint32_t)rsc[2] | (uint32_t)rsc[3] << 8 | (uint32_t)rsc[4] << 16
|
||||||
|
| (uint32_t)rsc[5] << 24);
|
||||||
|
Put16(k + 144, (uint16_t)(rsc[0] | rsc[1] << 8));
|
||||||
|
}
|
||||||
|
if (ea) memcpy(k + 156, ea, 6);
|
||||||
|
bool ok = BrcmfIovarSet("wsec_key", k, sizeof(k));
|
||||||
|
Crypto::SecureZero(k, sizeof(k));
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message 4 is still in the transmit queue when the supplicant installs
|
||||||
|
// the pairwise key; it has to reach the radio first or it goes out
|
||||||
|
// encrypted and the access point never sees it (Linux waits for its
|
||||||
|
// pending 802.1X frames the same way).
|
||||||
|
static void DrainTx() {
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
while (BrcmfTxPending() && Timekeeping::GetMilliseconds() - start < 200) {
|
||||||
|
BrcmfPoll();
|
||||||
|
Timekeeping::BusyWaitUs(200);
|
||||||
|
}
|
||||||
|
Timekeeping::BusyWaitMs(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||||
|
if (cipher != RSN_CIPHER_CCMP) return false;
|
||||||
|
DrainTx();
|
||||||
|
return SendKey(0, tk, tkLen, 0, g_bssid, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||||
|
uint8_t cipher, const uint8_t* rsc) {
|
||||||
|
if (cipher != RSN_CIPHER_CCMP) return false;
|
||||||
|
DrainTx();
|
||||||
|
return SendKey(keyIdx, gtk, gtkLen, KEY_FLAG_PRIMARY, nullptr, rsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Service (idle loop / syscall waits: may send ioctls)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static void ConnectService() {
|
||||||
|
static volatile bool inService = false;
|
||||||
|
if (__atomic_test_and_set(&inService, __ATOMIC_ACQUIRE)) return;
|
||||||
|
struct Guard { ~Guard() { __atomic_clear(&inService, __ATOMIC_RELEASE); } } guard;
|
||||||
|
|
||||||
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
|
g_lastServiceMs = now;
|
||||||
|
|
||||||
|
if (g_scanActive && Elapsed(now, g_scanStartMs, SCAN_TIMEOUT_MS)) {
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Scan never finished; abandoning it";
|
||||||
|
BrcmfAbortScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_conn == WIFI_CONN_ASSOCIATING) {
|
||||||
|
if (g_evJoinFailed) {
|
||||||
|
g_evJoinFailed = false;
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Could not join \"" << g_ssid << "\" (status "
|
||||||
|
<< (uint64_t)g_evStatus << (g_evStatus == 3 ? ": network not found)" : ")");
|
||||||
|
g_conn = WIFI_CONN_FAILED;
|
||||||
|
} else if (g_evAssoc) {
|
||||||
|
g_evAssoc = false;
|
||||||
|
if (g_secure) {
|
||||||
|
KernelLogStream(OK, "WiFi") << "Associated with \"" << g_ssid << "\"; starting the key handshake";
|
||||||
|
g_conn = WIFI_CONN_HANDSHAKING;
|
||||||
|
} else {
|
||||||
|
KernelLogStream(OK, "WiFi") << "Connected to \"" << g_ssid << "\" (open network)";
|
||||||
|
g_conn = WIFI_CONN_CONNECTED;
|
||||||
|
}
|
||||||
|
} else if (Elapsed(Timekeeping::GetMilliseconds(), g_joinStartMs, JOIN_TIMEOUT_MS)) {
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Timed out joining \"" << g_ssid << "\"";
|
||||||
|
g_conn = WIFI_CONN_FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_evLinkDown && g_conn >= WIFI_CONN_ASSOCIATED && g_conn != WIFI_CONN_FAILED) {
|
||||||
|
g_evLinkDown = false;
|
||||||
|
if (g_conn == WIFI_CONN_HANDSHAKING) {
|
||||||
|
// The access point gives up on a handshake whose MICs do not
|
||||||
|
// verify: a wrong passphrase, nearly always.
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "The access point dropped the connection during the key "
|
||||||
|
"handshake (reason " << (uint64_t)g_evReason << "); check the passphrase";
|
||||||
|
g_authFailed = g_sawEapol;
|
||||||
|
g_conn = WIFI_CONN_FAILED;
|
||||||
|
} else {
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Disconnected from \"" << g_ssid << "\" (reason "
|
||||||
|
<< (uint64_t)g_evReason << ")";
|
||||||
|
ResetConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_conn == WIFI_CONN_HANDSHAKING || g_conn == WIFI_CONN_CONNECTED) {
|
||||||
|
uint8_t body[512];
|
||||||
|
for (;;) {
|
||||||
|
uint32_t len = 0;
|
||||||
|
g_eapolLock.Acquire();
|
||||||
|
if (g_eapolHead != g_eapolTail) {
|
||||||
|
len = g_eapolQ[g_eapolTail].Len;
|
||||||
|
memcpy(body, g_eapolQ[g_eapolTail].Data, len);
|
||||||
|
g_eapolTail = (g_eapolTail + 1) % EAPOL_DEPTH;
|
||||||
|
}
|
||||||
|
g_eapolLock.Release();
|
||||||
|
if (!len) break;
|
||||||
|
g_sawEapol = true;
|
||||||
|
WpaOnEapol(body, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_conn == WIFI_CONN_HANDSHAKING) {
|
||||||
|
WpaService(Timekeeping::GetMilliseconds());
|
||||||
|
if (WpaIsComplete()) {
|
||||||
|
KernelLogStream(OK, "WiFi") << "Connected to \"" << g_ssid << "\"";
|
||||||
|
g_conn = WIFI_CONN_CONNECTED;
|
||||||
|
} else if (WpaGetState() == WpaState::Failed) {
|
||||||
|
g_authFailed = true;
|
||||||
|
g_conn = WIFI_CONN_FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfHasWork(uint64_t now) {
|
||||||
|
if (g_brcmf.State == BrcmfState::Error) return g_dumpConsole;
|
||||||
|
if (g_brcmf.State != BrcmfState::Running) return false;
|
||||||
|
|
||||||
|
bool active = g_scanActive || (g_conn != WIFI_CONN_IDLE && g_conn != WIFI_CONN_FAILED);
|
||||||
|
if (BrcmfPollDue(now, active ? POLL_ACTIVE_MS : POLL_IDLE_MS)) return true;
|
||||||
|
if (g_evAssoc || g_evJoinFailed || g_evLinkDown || EapolPending()) return true;
|
||||||
|
// Deadlines and the supplicant's retransmissions, even with the air quiet.
|
||||||
|
if ((g_conn == WIFI_CONN_ASSOCIATING || g_conn == WIFI_CONN_HANDSHAKING || g_scanActive)
|
||||||
|
&& Elapsed(now, g_lastServiceMs, 50))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfService() {
|
||||||
|
if (g_brcmf.State == BrcmfState::Error) {
|
||||||
|
if (g_dumpConsole) {
|
||||||
|
g_dumpConsole = false;
|
||||||
|
BrcmfDumpConsole();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_brcmf.State != BrcmfState::Running) return;
|
||||||
|
BrcmfPoll();
|
||||||
|
ConnectService();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfSendEthernet(const uint8_t* frame, uint32_t len) {
|
||||||
|
if (!BrcmfLinkUp()) return false;
|
||||||
|
return BrcmfTxEthernet(frame, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,592 @@
|
|||||||
|
/*
|
||||||
|
* BrcmfProto.cpp
|
||||||
|
* Broadcom FullMAC over SDIO: SDPCM framing, CDC ioctls, BDC data frames,
|
||||||
|
* firmware events, the RX pump and the transmit queue
|
||||||
|
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
|
||||||
|
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Brcmf.hpp"
|
||||||
|
#include "BrcmfSdio.hpp"
|
||||||
|
#include <Arch/Cpu.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <Timekeeping/Timer.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
// Every frame on function 2 starts with the SDPCM header (Linux sdio.c):
|
||||||
|
//
|
||||||
|
// hardware u16 len (the whole frame), u16 ~len
|
||||||
|
// software u8 seq, u8 channel (low nibble; bit 7 = glom descriptor),
|
||||||
|
// u8 next length, u8 data offset (from the frame start),
|
||||||
|
// u8 flow control bitmap, u8 max seq (TX credit, inbound),
|
||||||
|
// u16 reserved
|
||||||
|
//
|
||||||
|
// Channel 0 carries ioctls and their answers behind a 16-byte CDC header
|
||||||
|
// (cmd, len, flags: id [31:16] | interface [15:12] | set [1] | error [0],
|
||||||
|
// status). Channels 1 (events) and 2 (data) carry Ethernet frames behind a
|
||||||
|
// 4-byte BDC header (flags: version 2 in [7:4]; priority; flags2: interface;
|
||||||
|
// data offset in dwords to skip). Events are Ethernet frames of type 0x886C
|
||||||
|
// with Broadcom's OUI.
|
||||||
|
//
|
||||||
|
// Transmit credit: the device's max seq bounds what may be sent, every frame
|
||||||
|
// (ioctls included) takes the next sequence number.
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Drivers::Net::Wifi {
|
||||||
|
|
||||||
|
static constexpr uint32_t SDPCM_HDRLEN = 12;
|
||||||
|
static constexpr uint8_t CHAN_CONTROL = 0;
|
||||||
|
static constexpr uint8_t CHAN_EVENT = 1;
|
||||||
|
static constexpr uint8_t CHAN_DATA = 2;
|
||||||
|
static constexpr uint8_t CHAN_GLOM = 3;
|
||||||
|
static constexpr uint8_t GLOM_DESC = 0x80;
|
||||||
|
|
||||||
|
static constexpr uint32_t CDC_HDRLEN = 16;
|
||||||
|
static constexpr uint32_t CDCF_ERROR = 0x01;
|
||||||
|
static constexpr uint32_t CDCF_SET = 0x02;
|
||||||
|
static constexpr uint32_t BDC_HDRLEN = 4;
|
||||||
|
static constexpr uint8_t BDC_VERSION = 2;
|
||||||
|
|
||||||
|
static constexpr uint16_t ETHERTYPE_BRCM = 0x886C;
|
||||||
|
|
||||||
|
// SDIO device core
|
||||||
|
static constexpr uint32_t SD_INTSTATUS = 0x20;
|
||||||
|
static constexpr uint32_t SD_TOSBMAILBOX = 0x40;
|
||||||
|
static constexpr uint32_t SD_TOHOSTMAILBOXDATA = 0x4C;
|
||||||
|
static constexpr uint32_t I_HMB_FRAME_IND = 1u << 6;
|
||||||
|
static constexpr uint32_t I_HMB_HOST_INT = 1u << 7;
|
||||||
|
static constexpr uint32_t SMB_INT_ACK = 1u << 1;
|
||||||
|
static constexpr uint32_t HMB_DATA_FWHALT = 0x10;
|
||||||
|
|
||||||
|
static constexpr uint32_t F1_FRAMECTRL = 0x1000D;
|
||||||
|
static constexpr uint32_t F1_RFRAMEBCLO = 0x1001B;
|
||||||
|
static constexpr uint32_t F1_RFRAMEBCHI = 0x1001C;
|
||||||
|
static constexpr uint8_t SFC_RF_TERM = 0x01;
|
||||||
|
|
||||||
|
// Function 2's FIFO; the address is ChipCommon's window | 4-byte access.
|
||||||
|
static constexpr uint32_t F2_ADDR = 0x8000;
|
||||||
|
static constexpr uint32_t SI_ENUM_BASE = 0x18000000;
|
||||||
|
|
||||||
|
static constexpr uint32_t RX_MAX = 8192;
|
||||||
|
static constexpr uint32_t CTRL_MAX = 2048; // ioctl payload
|
||||||
|
static constexpr uint32_t TX_SLOT = 2048; // padded data frame
|
||||||
|
static constexpr uint32_t TXQ_DEPTH = 32;
|
||||||
|
static constexpr uint64_t CREDIT_TIMEOUT_MS = 1000;
|
||||||
|
static constexpr uint64_t IOCTL_TIMEOUT_MS = 2000;
|
||||||
|
static constexpr uint32_t FAIL_BUCKET_LIMIT = 3;
|
||||||
|
static constexpr uint32_t BUS_ERROR_LIMIT = 16;
|
||||||
|
|
||||||
|
static uint32_t g_rxBuf[RX_MAX / 4 + 16];
|
||||||
|
static volatile bool g_pumping = false;
|
||||||
|
static uint32_t g_busErrors = 0;
|
||||||
|
static bool g_glomWarned = false;
|
||||||
|
|
||||||
|
struct TxSlot {
|
||||||
|
uint32_t Len; // bytes on the bus (padded)
|
||||||
|
uint32_t Data[TX_SLOT / 4];
|
||||||
|
};
|
||||||
|
static TxSlot g_txq[TXQ_DEPTH];
|
||||||
|
static uint32_t g_txHead = 0, g_txTail = 0;
|
||||||
|
static kcp::Spinlock g_txqLock;
|
||||||
|
|
||||||
|
// The outstanding ioctl, if any. The pump on whichever core finds the
|
||||||
|
// answer copies it here under g_respLock; the caller copies it out.
|
||||||
|
static kcp::Mutex g_ctrlLock;
|
||||||
|
static kcp::Spinlock g_respLock;
|
||||||
|
// Room for the largest ioctl padded to whole blocks.
|
||||||
|
static uint32_t g_ctrlTx[(SDPCM_HDRLEN + CDC_HDRLEN + CTRL_MAX + Sdio::FN2_BLOCK - 1)
|
||||||
|
/ Sdio::FN2_BLOCK * Sdio::FN2_BLOCK / 4];
|
||||||
|
static uint8_t g_ctrlResp[CTRL_MAX];
|
||||||
|
static uint32_t g_ctrlRespLen = 0;
|
||||||
|
static uint16_t g_reqId = 0;
|
||||||
|
static uint16_t g_waitId = 0; // 0 = nobody waiting
|
||||||
|
static int32_t g_ctrlStatus = 0;
|
||||||
|
static uint32_t g_ctrlFlags = 0;
|
||||||
|
static std::atomic<bool> g_ctrlDone{false};
|
||||||
|
|
||||||
|
static inline uint16_t Be16(const uint8_t* p) { return (uint16_t)(p[0] << 8 | p[1]); }
|
||||||
|
static inline uint32_t Be32(const uint8_t* p) {
|
||||||
|
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
|
||||||
|
}
|
||||||
|
static inline void Le32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); }
|
||||||
|
|
||||||
|
void BrcmfProtoReset() {
|
||||||
|
g_brcmf.TxSeq = 0;
|
||||||
|
g_brcmf.TxMax = 1;
|
||||||
|
g_brcmf.RxPending = false;
|
||||||
|
g_txqLock.Acquire();
|
||||||
|
g_txHead = g_txTail = 0;
|
||||||
|
g_txqLock.Release();
|
||||||
|
g_respLock.Acquire();
|
||||||
|
g_waitId = 0;
|
||||||
|
g_respLock.Release();
|
||||||
|
g_busErrors = 0;
|
||||||
|
g_glomWarned = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Bus transfers (bus lock held)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static bool CanSend() {
|
||||||
|
uint8_t room = (uint8_t)(g_brcmf.TxMax - g_brcmf.TxSeq);
|
||||||
|
return room != 0 && (room & 0x80) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void NoteBusError() {
|
||||||
|
if (++g_busErrors == BUS_ERROR_LIMIT) BrcmfOnFirmwareFailure("SDIO bus errors");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool SendFrameLocked(uint8_t* frame, uint32_t busLen) {
|
||||||
|
frame[4] = g_brcmf.TxSeq;
|
||||||
|
if (!BrcmfBpSelectWindow(SI_ENUM_BASE) ||
|
||||||
|
!Sdio::WriteBlock(Sdio::FN2, F2_ADDR, false, frame, busLen)) {
|
||||||
|
NoteBusError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_busErrors = 0;
|
||||||
|
g_brcmf.TxSeq++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes on the bus for a frame: dwords, and whole blocks once it is
|
||||||
|
// longer than one (the device takes the length from the header).
|
||||||
|
static uint32_t BusLength(uint32_t len) {
|
||||||
|
if (len > Sdio::FN2_BLOCK)
|
||||||
|
return (len + Sdio::FN2_BLOCK - 1) / Sdio::FN2_BLOCK * Sdio::FN2_BLOCK;
|
||||||
|
return (len + 3) & ~3u;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PutSdpcm(uint8_t* f, uint32_t len, uint8_t channel) {
|
||||||
|
uint16_t l = (uint16_t)len;
|
||||||
|
f[0] = (uint8_t)l; f[1] = (uint8_t)(l >> 8);
|
||||||
|
f[2] = (uint8_t)~l; f[3] = (uint8_t)(~l >> 8);
|
||||||
|
f[4] = 0; // seq, filled in at send time
|
||||||
|
f[5] = channel;
|
||||||
|
f[6] = 0;
|
||||||
|
f[7] = SDPCM_HDRLEN;
|
||||||
|
f[8] = f[9] = f[10] = f[11] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abandon a frame half read: abort function 2, have the device drop the
|
||||||
|
// rest of it, and wait for its read FIFO to empty (Linux rxfail).
|
||||||
|
static void RxFailLocked() {
|
||||||
|
Sdio::Abort(Sdio::FN2);
|
||||||
|
BrcmfF1Write8(F1_FRAMECTRL, SFC_RF_TERM);
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
uint8_t hi = 0, lo = 0;
|
||||||
|
if (!BrcmfF1Read8(F1_RFRAMEBCHI, hi) || !BrcmfF1Read8(F1_RFRAMEBCLO, lo)) break;
|
||||||
|
if (hi == 0 && lo == 0) break;
|
||||||
|
}
|
||||||
|
NoteBusError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read one frame into g_rxBuf. 1 = a frame, 0 = none waiting, -1 = error.
|
||||||
|
static int ReadFrameLocked(uint32_t& len) {
|
||||||
|
uint8_t* buf = (uint8_t*)g_rxBuf;
|
||||||
|
if (!BrcmfBpSelectWindow(SI_ENUM_BASE) ||
|
||||||
|
!Sdio::ReadBlock(Sdio::FN2, F2_ADDR, false, buf, 4)) {
|
||||||
|
RxFailLocked();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
uint16_t l = (uint16_t)(buf[0] | buf[1] << 8);
|
||||||
|
uint16_t check = (uint16_t)(buf[2] | buf[3] << 8);
|
||||||
|
if ((l | check) == 0) return 0;
|
||||||
|
if ((uint16_t)(l ^ check) != 0xFFFF || l < SDPCM_HDRLEN || l > RX_MAX) {
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Bad SDPCM header (len 0x" << base::hex
|
||||||
|
<< (uint64_t)l << ", check 0x" << (uint64_t)check << base::dec << ")";
|
||||||
|
RxFailLocked();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
uint32_t rest = ((l + 3u) & ~3u) - 4;
|
||||||
|
if (rest && !Sdio::ReadBlock(Sdio::FN2, F2_ADDR, false, buf + 4, rest)) {
|
||||||
|
RxFailLocked();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
g_busErrors = 0;
|
||||||
|
len = l;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FlushTxLocked() {
|
||||||
|
for (;;) {
|
||||||
|
g_txqLock.Acquire();
|
||||||
|
bool empty = g_txHead == g_txTail;
|
||||||
|
g_txqLock.Release();
|
||||||
|
if (empty || !CanSend()) return;
|
||||||
|
|
||||||
|
TxSlot& slot = g_txq[g_txTail % TXQ_DEPTH];
|
||||||
|
if (SendFrameLocked((uint8_t*)slot.Data, slot.Len)) g_brcmf.TxPackets++;
|
||||||
|
|
||||||
|
g_txqLock.Acquire();
|
||||||
|
g_txTail++;
|
||||||
|
g_txqLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Receive dispatch (bus lock released)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static void HandleControl(const uint8_t* p, uint32_t len) {
|
||||||
|
if (len < CDC_HDRLEN) return;
|
||||||
|
uint32_t cmd, dlen, flags, status;
|
||||||
|
memcpy(&cmd, p, 4);
|
||||||
|
memcpy(&dlen, p + 4, 4);
|
||||||
|
memcpy(&flags, p + 8, 4);
|
||||||
|
memcpy(&status, p + 12, 4);
|
||||||
|
(void)cmd;
|
||||||
|
(void)dlen;
|
||||||
|
uint16_t id = (uint16_t)(flags >> 16);
|
||||||
|
|
||||||
|
g_respLock.Acquire();
|
||||||
|
if (g_waitId != 0 && id == g_waitId) {
|
||||||
|
uint32_t n = len - CDC_HDRLEN;
|
||||||
|
if (n > CTRL_MAX) n = CTRL_MAX;
|
||||||
|
memcpy(g_ctrlResp, p + CDC_HDRLEN, n);
|
||||||
|
g_ctrlRespLen = n;
|
||||||
|
g_ctrlStatus = (int32_t)status;
|
||||||
|
g_ctrlFlags = flags;
|
||||||
|
g_waitId = 0;
|
||||||
|
g_ctrlDone.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
g_respLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void HandleEvent(const uint8_t* eth, uint32_t len) {
|
||||||
|
// Ethernet (14), bcm_ethhdr (10): subtype 0x8001, length, version 1,
|
||||||
|
// OUI 00:10:18, user subtype 1; then the 48-byte event message.
|
||||||
|
constexpr uint32_t MSG = 24;
|
||||||
|
constexpr uint32_t DATA = MSG + 48;
|
||||||
|
if (len < DATA) return;
|
||||||
|
const uint8_t* h = eth + 14;
|
||||||
|
if (Be16(h) != 0x8001 || h[5] != 0x00 || h[6] != 0x10 || h[7] != 0x18 || Be16(h + 8) != 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const uint8_t* m = eth + MSG;
|
||||||
|
uint16_t flags = Be16(m + 2);
|
||||||
|
uint32_t type = Be32(m + 4);
|
||||||
|
uint32_t status = Be32(m + 8);
|
||||||
|
uint32_t reason = Be32(m + 12);
|
||||||
|
uint32_t dataLen = Be32(m + 20);
|
||||||
|
const uint8_t* addr = m + 24;
|
||||||
|
uint8_t ifidx = m[46];
|
||||||
|
if (ifidx != 0) return;
|
||||||
|
if (dataLen > len - DATA) dataLen = len - DATA;
|
||||||
|
BrcmfOnEvent(type, status, reason, flags, addr, eth + DATA, dataLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Dispatch(const uint8_t* f, uint32_t len) {
|
||||||
|
const uint8_t* sw = f + 4;
|
||||||
|
uint8_t channel = sw[1] & 0x0F;
|
||||||
|
uint8_t offset = sw[3];
|
||||||
|
|
||||||
|
// Credit: the device's max seq, sanity checked as Linux does.
|
||||||
|
uint8_t max = sw[5];
|
||||||
|
if ((uint8_t)(max - g_brcmf.TxSeq) > 0x40) max = (uint8_t)(g_brcmf.TxSeq + 2);
|
||||||
|
g_brcmf.TxMax = max;
|
||||||
|
|
||||||
|
if ((sw[1] & GLOM_DESC) || channel == CHAN_GLOM) {
|
||||||
|
if (!g_glomWarned) {
|
||||||
|
g_glomWarned = true;
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Dropping a glommed superframe (rx glom is off)";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (offset < SDPCM_HDRLEN || offset > len) return;
|
||||||
|
const uint8_t* p = f + offset;
|
||||||
|
uint32_t plen = len - offset;
|
||||||
|
if (plen == 0) return; // credit update only
|
||||||
|
|
||||||
|
if (channel == CHAN_CONTROL) {
|
||||||
|
HandleControl(p, plen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (channel != CHAN_EVENT && channel != CHAN_DATA) return;
|
||||||
|
|
||||||
|
if (plen < BDC_HDRLEN || (p[0] >> 4) != BDC_VERSION) return;
|
||||||
|
if ((p[2] & 0x0F) != 0) return; // another interface
|
||||||
|
uint32_t skip = BDC_HDRLEN + (uint32_t)p[3] * 4;
|
||||||
|
if (skip + 14 > plen) return;
|
||||||
|
const uint8_t* eth = p + skip;
|
||||||
|
uint32_t elen = plen - skip;
|
||||||
|
|
||||||
|
if (Be16(eth + 12) == ETHERTYPE_BRCM) {
|
||||||
|
HandleEvent(eth, elen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (channel == CHAN_DATA) {
|
||||||
|
g_brcmf.RxPackets++;
|
||||||
|
BrcmfOnRxEthernet(eth, elen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// The pump
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool BrcmfTxPending() {
|
||||||
|
g_txqLock.Acquire();
|
||||||
|
bool pending = g_txHead != g_txTail;
|
||||||
|
g_txqLock.Release();
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfPollDue(uint64_t now, uint64_t intervalMs) {
|
||||||
|
if (g_brcmf.State != BrcmfState::Running) return false;
|
||||||
|
if (g_brcmf.RxPending || BrcmfTxPending()) return true;
|
||||||
|
if (Sdio::CardInterruptPending()) return true;
|
||||||
|
uint64_t last = g_brcmf.LastPollMs;
|
||||||
|
return now < last || now - last >= intervalMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrcmfPoll() {
|
||||||
|
if (g_brcmf.State != BrcmfState::Running) return;
|
||||||
|
if (__atomic_test_and_set(&g_pumping, __ATOMIC_ACQUIRE)) return;
|
||||||
|
struct Guard { ~Guard() { __atomic_clear(&g_pumping, __ATOMIC_RELEASE); } } guard;
|
||||||
|
|
||||||
|
const uint32_t sdio = g_brcmf.SdioCoreBase;
|
||||||
|
for (int round = 0; round < 8 && g_brcmf.State == BrcmfState::Running; round++) {
|
||||||
|
bool frames = g_brcmf.RxPending;
|
||||||
|
uint32_t intStatus = 0;
|
||||||
|
bool halted = false;
|
||||||
|
|
||||||
|
g_brcmf.BusLock.Acquire();
|
||||||
|
if (BrcmfBpRead32(sdio + SD_INTSTATUS, intStatus)) {
|
||||||
|
intStatus &= g_brcmf.HostIntMask;
|
||||||
|
if (intStatus) BrcmfBpWrite32(sdio + SD_INTSTATUS, intStatus);
|
||||||
|
if (intStatus & I_HMB_HOST_INT) {
|
||||||
|
uint32_t mbox = 0;
|
||||||
|
BrcmfBpRead32(sdio + SD_TOHOSTMAILBOXDATA, mbox);
|
||||||
|
BrcmfBpWrite32(sdio + SD_TOSBMAILBOX, SMB_INT_ACK);
|
||||||
|
halted = (mbox & HMB_DATA_FWHALT) != 0;
|
||||||
|
}
|
||||||
|
if (intStatus & I_HMB_FRAME_IND) frames = true;
|
||||||
|
} else {
|
||||||
|
NoteBusError();
|
||||||
|
}
|
||||||
|
g_brcmf.BusLock.Release();
|
||||||
|
g_brcmf.LastPollMs = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
|
if (halted) {
|
||||||
|
BrcmfOnFirmwareFailure("firmware halted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int n = 0; frames && n < 32; n++) {
|
||||||
|
uint32_t len = 0;
|
||||||
|
g_brcmf.BusLock.Acquire();
|
||||||
|
int r = ReadFrameLocked(len);
|
||||||
|
g_brcmf.BusLock.Release();
|
||||||
|
if (r <= 0) {
|
||||||
|
frames = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Dispatch((const uint8_t*)g_rxBuf, len);
|
||||||
|
}
|
||||||
|
g_brcmf.RxPending = frames;
|
||||||
|
|
||||||
|
if (BrcmfTxPending()) {
|
||||||
|
g_brcmf.BusLock.Acquire();
|
||||||
|
FlushTxLocked();
|
||||||
|
g_brcmf.BusLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frames && intStatus == 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Transmit
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool BrcmfTxEthernet(const uint8_t* frame, uint32_t len) {
|
||||||
|
if (g_brcmf.State != BrcmfState::Running || len < 14 || len > 1518) return false;
|
||||||
|
uint32_t total = SDPCM_HDRLEN + BDC_HDRLEN + len;
|
||||||
|
uint32_t busLen = BusLength(total);
|
||||||
|
|
||||||
|
g_txqLock.Acquire();
|
||||||
|
if (g_txHead - g_txTail >= TXQ_DEPTH) {
|
||||||
|
g_txqLock.Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TxSlot& slot = g_txq[g_txHead % TXQ_DEPTH];
|
||||||
|
uint8_t* f = (uint8_t*)slot.Data;
|
||||||
|
PutSdpcm(f, total, CHAN_DATA);
|
||||||
|
f[12] = BDC_VERSION << 4;
|
||||||
|
f[13] = 0; // priority: best effort
|
||||||
|
f[14] = 0; // interface 0
|
||||||
|
f[15] = 0; // no signalling TLVs
|
||||||
|
memcpy(f + 16, frame, len);
|
||||||
|
memset(f + total, 0, busLen - total);
|
||||||
|
slot.Len = busLen;
|
||||||
|
g_txHead++;
|
||||||
|
g_txqLock.Release();
|
||||||
|
|
||||||
|
// Never wait for the bus here: the caller may be the network stack
|
||||||
|
// inside a frame the pump is delivering, or a process that just
|
||||||
|
// preempted the bus lock's holder. Whoever holds it, or the next
|
||||||
|
// pump, sends what is queued.
|
||||||
|
if (g_brcmf.BusLock.TryAcquire()) {
|
||||||
|
FlushTxLocked();
|
||||||
|
g_brcmf.BusLock.Release();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Control
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool BrcmfIoctl(uint32_t cmd, bool set, void* buf, uint32_t len, int32_t* fwStatus) {
|
||||||
|
if (fwStatus) *fwStatus = 0;
|
||||||
|
if (g_brcmf.State != BrcmfState::Running || len > CTRL_MAX) return false;
|
||||||
|
|
||||||
|
g_ctrlLock.Acquire();
|
||||||
|
|
||||||
|
uint16_t id = ++g_reqId;
|
||||||
|
if (id == 0) id = ++g_reqId;
|
||||||
|
uint8_t* f = (uint8_t*)g_ctrlTx;
|
||||||
|
uint32_t total = SDPCM_HDRLEN + CDC_HDRLEN + len;
|
||||||
|
PutSdpcm(f, total, CHAN_CONTROL);
|
||||||
|
uint8_t* cdc = f + SDPCM_HDRLEN;
|
||||||
|
Le32(cdc, cmd);
|
||||||
|
Le32(cdc + 4, len);
|
||||||
|
Le32(cdc + 8, (uint32_t)id << 16 | (set ? CDCF_SET : 0));
|
||||||
|
Le32(cdc + 12, 0);
|
||||||
|
if (len) memcpy(cdc + CDC_HDRLEN, buf, len);
|
||||||
|
uint32_t busLen = BusLength(total);
|
||||||
|
memset(f + total, 0, busLen - total);
|
||||||
|
|
||||||
|
g_respLock.Acquire();
|
||||||
|
g_waitId = id;
|
||||||
|
g_ctrlDone.store(false, std::memory_order_relaxed);
|
||||||
|
g_respLock.Release();
|
||||||
|
|
||||||
|
// Wait for transmit credit, pumping meanwhile: credit only ever
|
||||||
|
// arrives in the header of a received frame.
|
||||||
|
bool sent = false;
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
while (!sent && g_brcmf.State == BrcmfState::Running) {
|
||||||
|
g_brcmf.BusLock.Acquire();
|
||||||
|
if (CanSend()) {
|
||||||
|
sent = SendFrameLocked(f, busLen);
|
||||||
|
if (!sent) {
|
||||||
|
g_brcmf.BusLock.Release();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_brcmf.BusLock.Release();
|
||||||
|
if (sent) break;
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > CREDIT_TIMEOUT_MS) break;
|
||||||
|
BrcmfPoll();
|
||||||
|
Timekeeping::BusyWaitUs(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool done = false;
|
||||||
|
if (sent) {
|
||||||
|
start = Timekeeping::GetMilliseconds();
|
||||||
|
while (g_brcmf.State == BrcmfState::Running) {
|
||||||
|
if (g_ctrlDone.load(std::memory_order_acquire)) { done = true; break; }
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > IOCTL_TIMEOUT_MS) break;
|
||||||
|
BrcmfPoll();
|
||||||
|
if (g_ctrlDone.load(std::memory_order_acquire)) { done = true; break; }
|
||||||
|
Timekeeping::BusyWaitUs(50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_respLock.Acquire();
|
||||||
|
g_waitId = 0;
|
||||||
|
bool ok = false;
|
||||||
|
if (done) {
|
||||||
|
int32_t status = g_ctrlStatus;
|
||||||
|
if (fwStatus) *fwStatus = status;
|
||||||
|
ok = !(g_ctrlFlags & CDCF_ERROR) && status >= 0;
|
||||||
|
if (!set && len) {
|
||||||
|
uint32_t n = g_ctrlRespLen < len ? g_ctrlRespLen : len;
|
||||||
|
memcpy(buf, g_ctrlResp, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_respLock.Release();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
if (g_brcmf.CmdFailBucket) g_brcmf.CmdFailBucket--;
|
||||||
|
} else if (g_brcmf.State == BrcmfState::Running) {
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Firmware command " << (uint64_t)cmd
|
||||||
|
<< (sent ? " went unanswered" : " could not be sent (no transmit credit)");
|
||||||
|
if (++g_brcmf.CmdFailBucket >= FAIL_BUCKET_LIMIT)
|
||||||
|
BrcmfOnFirmwareFailure("firmware stopped answering commands");
|
||||||
|
}
|
||||||
|
|
||||||
|
g_ctrlLock.Release();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr uint32_t WLC_GET_VAR = 262;
|
||||||
|
static constexpr uint32_t WLC_SET_VAR = 263;
|
||||||
|
|
||||||
|
static kcp::Mutex g_iovarLock;
|
||||||
|
static uint8_t g_iovarBuf[CTRL_MAX];
|
||||||
|
|
||||||
|
static uint32_t NameLen(const char* name) {
|
||||||
|
uint32_t n = 0;
|
||||||
|
while (name[n]) n++;
|
||||||
|
return n + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfIovarSet(const char* name, const void* data, uint32_t len, bool quiet) {
|
||||||
|
uint32_t nameLen = NameLen(name);
|
||||||
|
if (nameLen + len > CTRL_MAX) return false;
|
||||||
|
g_iovarLock.Acquire();
|
||||||
|
memcpy(g_iovarBuf, name, nameLen);
|
||||||
|
if (len) memcpy(g_iovarBuf + nameLen, data, len);
|
||||||
|
int32_t status = 0;
|
||||||
|
bool ok = BrcmfIoctl(WLC_SET_VAR, true, g_iovarBuf, nameLen + len, &status);
|
||||||
|
g_iovarLock.Release();
|
||||||
|
if (!ok && !quiet)
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Setting \"" << name << "\" failed (status "
|
||||||
|
<< (int)status << ")";
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfIovarGet(const char* name, void* out, uint32_t len, bool quiet) {
|
||||||
|
uint32_t nameLen = NameLen(name);
|
||||||
|
uint32_t bufLen = nameLen > len ? nameLen : len;
|
||||||
|
if (bufLen > CTRL_MAX) return false;
|
||||||
|
g_iovarLock.Acquire();
|
||||||
|
memset(g_iovarBuf, 0, bufLen);
|
||||||
|
memcpy(g_iovarBuf, name, nameLen);
|
||||||
|
int32_t status = 0;
|
||||||
|
bool ok = BrcmfIoctl(WLC_GET_VAR, false, g_iovarBuf, bufLen, &status);
|
||||||
|
if (ok) memcpy(out, g_iovarBuf, len);
|
||||||
|
g_iovarLock.Release();
|
||||||
|
if (!ok && !quiet)
|
||||||
|
KernelLogStream(WARNING, "WiFi") << "Reading \"" << name << "\" failed (status "
|
||||||
|
<< (int)status << ")";
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfIovarSetInt(const char* name, uint32_t value, bool quiet) {
|
||||||
|
return BrcmfIovarSet(name, &value, 4, quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfIovarGetInt(const char* name, uint32_t& value, bool quiet) {
|
||||||
|
return BrcmfIovarGet(name, &value, 4, quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfSetInt(uint32_t cmd, uint32_t value) {
|
||||||
|
return BrcmfIoctl(cmd, true, &value, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BrcmfGetInt(uint32_t cmd, uint32_t& value) {
|
||||||
|
uint32_t v = 0;
|
||||||
|
if (!BrcmfIoctl(cmd, false, &v, 4)) return false;
|
||||||
|
value = v;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
/*
|
||||||
|
* BrcmfSdio.cpp
|
||||||
|
* SDIO host on the Raspberry Pi 4/400's Arasan SD controller
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "BrcmfSdio.hpp"
|
||||||
|
#include <Arch/Cpu.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
#include <Memory/HHDM.hpp>
|
||||||
|
#include <Memory/Paging.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <Timekeeping/Timer.hpp>
|
||||||
|
|
||||||
|
// Register access follows Drivers/Storage/Sdhci: the Broadcom/Arasan
|
||||||
|
// controller only takes 32-bit accesses, so 8/16-bit fields are written by
|
||||||
|
// read-modify-write of their dword and the transfer-mode/command pair in
|
||||||
|
// the one write that issues the command. Linux (sdhci-iproc, bcm2835 data)
|
||||||
|
// also waits four SD clocks after every write while the bus runs at 400 kHz
|
||||||
|
// or below; so does this.
|
||||||
|
//
|
||||||
|
// SDIO commands (SDIO Simplified Specification 3.00):
|
||||||
|
// CMD5 IO_SEND_OP_COND, R4: OCR [23:0], ready [31]
|
||||||
|
// CMD52 IO_RW_DIRECT, R5: flags [15:8], data [7:0]
|
||||||
|
// arg: R/W [31], function [30:28], RAW [27], address [25:9], data [7:0]
|
||||||
|
// CMD53 IO_RW_EXTENDED, R5
|
||||||
|
// arg: R/W [31], function [30:28], block mode [27], incrementing
|
||||||
|
// address [26], address [25:9], count [8:0] (0 = 512)
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Drivers::Net::Wifi::Sdio {
|
||||||
|
|
||||||
|
static constexpr uint32_t REG_BLOCK_SIZE_COUNT = 0x04;
|
||||||
|
static constexpr uint32_t REG_ARGUMENT = 0x08;
|
||||||
|
static constexpr uint32_t REG_XFER_MODE_CMD = 0x0C;
|
||||||
|
static constexpr uint32_t REG_RESPONSE = 0x10;
|
||||||
|
static constexpr uint32_t REG_BUFFER_DATA = 0x20;
|
||||||
|
static constexpr uint32_t REG_PRESENT_STATE = 0x24;
|
||||||
|
static constexpr uint32_t REG_HOST_CONTROL = 0x28;
|
||||||
|
static constexpr uint32_t REG_CLOCK_CONTROL = 0x2C;
|
||||||
|
static constexpr uint32_t REG_INT_STATUS = 0x30;
|
||||||
|
static constexpr uint32_t REG_INT_ENABLE = 0x34;
|
||||||
|
static constexpr uint32_t REG_INT_SIGNAL = 0x38;
|
||||||
|
static constexpr uint32_t REG_CAPABILITIES = 0x40;
|
||||||
|
static constexpr uint32_t REG_VERSION = 0xFC;
|
||||||
|
|
||||||
|
static constexpr uint32_t PS_CMD_INHIBIT = 1u << 0;
|
||||||
|
static constexpr uint32_t PS_DAT_INHIBIT = 1u << 1;
|
||||||
|
|
||||||
|
static constexpr uint32_t INT_CMD_COMPLETE = 1u << 0;
|
||||||
|
static constexpr uint32_t INT_XFER_COMPLETE = 1u << 1;
|
||||||
|
static constexpr uint32_t INT_WRITE_READY = 1u << 4;
|
||||||
|
static constexpr uint32_t INT_READ_READY = 1u << 5;
|
||||||
|
static constexpr uint32_t INT_CARD = 1u << 8;
|
||||||
|
static constexpr uint32_t INT_ERROR = 1u << 15;
|
||||||
|
static constexpr uint32_t INT_ERROR_MASK = 0xFFFF0000u;
|
||||||
|
// Status enables: the completions, both buffer-ready bits, the card
|
||||||
|
// interrupt and every error. Card insert/remove stay off: nothing is
|
||||||
|
// ever inserted, and the detect line is not wired to the radio.
|
||||||
|
static constexpr uint32_t INT_ENABLES = 0x03FF0000u | INT_CMD_COMPLETE | INT_XFER_COMPLETE
|
||||||
|
| INT_WRITE_READY | INT_READ_READY | INT_CARD;
|
||||||
|
|
||||||
|
static constexpr uint32_t HC_4BIT = 1u << 1;
|
||||||
|
static constexpr uint32_t PWR_ON = 1u << 8;
|
||||||
|
static constexpr uint32_t PWR_330 = 7u << 9;
|
||||||
|
|
||||||
|
static constexpr uint32_t CLK_INT_EN = 1u << 0;
|
||||||
|
static constexpr uint32_t CLK_INT_STABLE = 1u << 1;
|
||||||
|
static constexpr uint32_t CLK_SD_EN = 1u << 2;
|
||||||
|
static constexpr uint32_t TIMEOUT_MAX = 0xEu << 16;
|
||||||
|
|
||||||
|
static constexpr uint32_t RESET_ALL = 1u << 24;
|
||||||
|
static constexpr uint32_t RESET_CMD = 1u << 25;
|
||||||
|
static constexpr uint32_t RESET_DAT = 1u << 26;
|
||||||
|
|
||||||
|
static constexpr uint32_t XM_BLOCK_COUNT_EN = 1u << 1;
|
||||||
|
static constexpr uint32_t XM_READ = 1u << 4;
|
||||||
|
static constexpr uint32_t XM_MULTI_BLOCK = 1u << 5;
|
||||||
|
|
||||||
|
enum Response : uint32_t {
|
||||||
|
RespNone = 0,
|
||||||
|
Resp48 = 2 | 3u << 3, // R1, R5, R6: CRC and index checked
|
||||||
|
Resp48NoCrc = 2, // R4
|
||||||
|
};
|
||||||
|
static constexpr uint32_t CMD_DATA = 1u << 5;
|
||||||
|
|
||||||
|
// R5 flags: COM_CRC_ERROR, ILLEGAL_COMMAND, ERROR, FUNCTION_NUMBER,
|
||||||
|
// OUT_OF_RANGE.
|
||||||
|
static constexpr uint32_t R5_ERRORS = 0xCB00;
|
||||||
|
|
||||||
|
// CCCR
|
||||||
|
static constexpr uint32_t CCCR_IOEN = 0x02;
|
||||||
|
static constexpr uint32_t CCCR_IORDY = 0x03;
|
||||||
|
static constexpr uint32_t CCCR_INTEN = 0x04;
|
||||||
|
static constexpr uint32_t CCCR_ABORT = 0x06;
|
||||||
|
static constexpr uint32_t CCCR_BUSIF = 0x07;
|
||||||
|
static constexpr uint32_t BUSIF_4BIT = 0x02;
|
||||||
|
static constexpr uint32_t BUSIF_CD_DIS = 0x80;
|
||||||
|
static constexpr uint32_t FBR_BLKSIZE = 0x10; // in function n's FBR at n * 0x100
|
||||||
|
|
||||||
|
static constexpr uint64_t CommandTimeoutMs = 100;
|
||||||
|
static constexpr uint64_t DataTimeoutMs = 500;
|
||||||
|
// CMD53's count field is nine bits.
|
||||||
|
static constexpr uint32_t MaxBlocks = 511;
|
||||||
|
|
||||||
|
// The legacy EMMC clock the firmware sets up; only used when neither the
|
||||||
|
// mailbox nor the capabilities name one. Assuming it high errs towards a
|
||||||
|
// slower bus, never a faster one.
|
||||||
|
static constexpr uint32_t FallbackBaseClockHz = 250000000;
|
||||||
|
|
||||||
|
static volatile uint8_t* g_regs = nullptr;
|
||||||
|
static uint32_t g_version = 0;
|
||||||
|
static uint32_t g_baseClockHz = 0;
|
||||||
|
static uint32_t g_clockHz = 0;
|
||||||
|
static uint32_t g_rca = 0;
|
||||||
|
static bool g_initialized = false;
|
||||||
|
|
||||||
|
static inline uint32_t Read(uint32_t reg) {
|
||||||
|
return *(volatile uint32_t*)(g_regs + reg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void Write(uint32_t reg, uint32_t value) {
|
||||||
|
*(volatile uint32_t*)(g_regs + reg) = value;
|
||||||
|
if (g_clockHz <= 400000) Timekeeping::BusyWaitUs(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool WaitClear(uint32_t reg, uint32_t mask, uint64_t timeoutMs) {
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
while (Read(reg) & mask) {
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Reset(uint32_t which) {
|
||||||
|
Write(REG_CLOCK_CONTROL, Read(REG_CLOCK_CONTROL) | which);
|
||||||
|
return WaitClear(REG_CLOCK_CONTROL, which, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool WaitStatus(uint32_t bits, uint64_t timeoutMs) {
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
for (;;) {
|
||||||
|
uint32_t status = Read(REG_INT_STATUS);
|
||||||
|
if (status & (INT_ERROR | INT_ERROR_MASK)) {
|
||||||
|
Write(REG_INT_STATUS, status & ~INT_CARD);
|
||||||
|
Reset(RESET_CMD | RESET_DAT);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (status & bits) {
|
||||||
|
Write(REG_INT_STATUS, status & bits);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > timeoutMs) {
|
||||||
|
Reset(RESET_CMD | RESET_DAT);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool SendCommand(uint32_t index, uint32_t argument, uint32_t response,
|
||||||
|
uint32_t transferMode = 0, bool data = false) {
|
||||||
|
uint32_t inhibit = PS_CMD_INHIBIT | (data ? PS_DAT_INHIBIT : 0);
|
||||||
|
if (!WaitClear(REG_PRESENT_STATE, inhibit, CommandTimeoutMs)) {
|
||||||
|
Reset(RESET_CMD | RESET_DAT);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Write(REG_INT_STATUS, ~INT_CARD);
|
||||||
|
Write(REG_ARGUMENT, argument);
|
||||||
|
uint32_t command = index << 8 | response | (data ? CMD_DATA : 0);
|
||||||
|
Write(REG_XFER_MODE_CMD, command << 16 | transferMode);
|
||||||
|
return WaitStatus(INT_CMD_COMPLETE, CommandTimeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool SetClock(uint32_t hz) {
|
||||||
|
Write(REG_CLOCK_CONTROL, Read(REG_CLOCK_CONTROL) & ~0xFFFFu);
|
||||||
|
|
||||||
|
uint32_t divider = 0; // base / (2 * divider); 0 = undivided
|
||||||
|
if (hz < g_baseClockHz) {
|
||||||
|
divider = (g_baseClockHz + 2 * hz - 1) / (2 * hz);
|
||||||
|
uint32_t max = g_version >= 2 ? 0x3FF : 0x80;
|
||||||
|
if (divider > max) divider = max;
|
||||||
|
if (g_version < 2) {
|
||||||
|
uint32_t p = 1;
|
||||||
|
while (p < divider) p <<= 1;
|
||||||
|
divider = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint32_t actual = divider ? g_baseClockHz / (2 * divider) : g_baseClockHz;
|
||||||
|
|
||||||
|
uint32_t field = (divider & 0xFF) << 8 | ((divider >> 8) & 0x3) << 6;
|
||||||
|
Write(REG_CLOCK_CONTROL, (Read(REG_CLOCK_CONTROL) & ~0xFFFFu) | field | CLK_INT_EN);
|
||||||
|
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
while (!(Read(REG_CLOCK_CONTROL) & CLK_INT_STABLE)) {
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > 20) return false;
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
Write(REG_CLOCK_CONTROL, Read(REG_CLOCK_CONTROL) | CLK_SD_EN);
|
||||||
|
g_clockHz = actual;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Cmd52(bool write, uint32_t fn, uint32_t addr, uint8_t in, uint8_t* out) {
|
||||||
|
uint32_t arg = (write ? 1u << 31 : 0) | (fn & 7) << 28 | (addr & 0x1FFFF) << 9 | in;
|
||||||
|
if (!SendCommand(52, arg, Resp48)) return false;
|
||||||
|
uint32_t r = Read(REG_RESPONSE);
|
||||||
|
if (r & R5_ERRORS) return false;
|
||||||
|
if (out) *out = (uint8_t)r;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Read8(uint32_t fn, uint32_t addr, uint8_t& value) {
|
||||||
|
return Cmd52(false, fn, addr, 0, &value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Write8(uint32_t fn, uint32_t addr, uint8_t value) {
|
||||||
|
return Cmd52(true, fn, addr, value, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One CMD53: `count` blocks of `blockSize` bytes, or (byte mode) one run
|
||||||
|
// of `blockSize` bytes. `words` is 4-byte aligned.
|
||||||
|
static bool Cmd53(bool write, uint32_t fn, uint32_t addr, bool incr, uint32_t* words,
|
||||||
|
bool blockMode, uint32_t count, uint32_t blockSize) {
|
||||||
|
uint32_t arg = (write ? 1u << 31 : 0) | (fn & 7) << 28 | (blockMode ? 1u << 27 : 0)
|
||||||
|
| (incr ? 1u << 26 : 0) | (addr & 0x1FFFF) << 9
|
||||||
|
| ((blockMode ? count : blockSize) & 0x1FF);
|
||||||
|
uint32_t blocks = blockMode ? count : 1;
|
||||||
|
Write(REG_BLOCK_SIZE_COUNT, blocks << 16 | blockSize);
|
||||||
|
|
||||||
|
uint32_t mode = write ? 0 : XM_READ;
|
||||||
|
if (blockMode) mode |= XM_BLOCK_COUNT_EN | (blocks > 1 ? XM_MULTI_BLOCK : 0);
|
||||||
|
if (!SendCommand(53, arg, Resp48, mode, true)) return false;
|
||||||
|
if (Read(REG_RESPONSE) & R5_ERRORS) {
|
||||||
|
Abort(fn);
|
||||||
|
Reset(RESET_CMD | RESET_DAT);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t perBlock = (blockSize + 3) / 4;
|
||||||
|
for (uint32_t b = 0; b < blocks; b++) {
|
||||||
|
if (!WaitStatus(write ? INT_WRITE_READY : INT_READ_READY, DataTimeoutMs)) {
|
||||||
|
Abort(fn);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < perBlock; i++) {
|
||||||
|
if (write) *(volatile uint32_t*)(g_regs + REG_BUFFER_DATA) = *words++;
|
||||||
|
else *words++ = Read(REG_BUFFER_DATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!WaitStatus(INT_XFER_COMPLETE, DataTimeoutMs)) {
|
||||||
|
Abort(fn);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Transfer(bool write, uint32_t fn, uint32_t addr, bool incr,
|
||||||
|
uint8_t* buffer, uint32_t len) {
|
||||||
|
if (!g_initialized || (len & 3) != 0) return false;
|
||||||
|
uint32_t blockSize = fn == FN2 ? FN2_BLOCK : FN1_BLOCK;
|
||||||
|
|
||||||
|
// The data port moves dwords; callers' buffers are only byte aligned
|
||||||
|
// in general.
|
||||||
|
uint32_t bounce[FN2_BLOCK / 4];
|
||||||
|
while (len > 0) {
|
||||||
|
bool blockMode = len >= blockSize;
|
||||||
|
uint32_t count = blockMode ? len / blockSize : 0;
|
||||||
|
if (count > MaxBlocks) count = MaxBlocks;
|
||||||
|
uint32_t run = blockMode ? count * blockSize : len;
|
||||||
|
if (!blockMode && run > FN2_BLOCK) run = FN2_BLOCK;
|
||||||
|
|
||||||
|
bool aligned = ((uintptr_t)buffer & 3) == 0;
|
||||||
|
if (aligned) {
|
||||||
|
if (!Cmd53(write, fn, addr, incr, (uint32_t*)buffer, blockMode, count,
|
||||||
|
blockMode ? blockSize : run)) return false;
|
||||||
|
} else {
|
||||||
|
// Unaligned: one block (or the byte-mode run) at a time
|
||||||
|
// through the bounce buffer.
|
||||||
|
if (blockMode) { count = 1; run = blockSize; }
|
||||||
|
if (write) memcpy(bounce, buffer, run);
|
||||||
|
if (!Cmd53(write, fn, addr, incr, bounce, blockMode, count,
|
||||||
|
blockMode ? blockSize : run)) return false;
|
||||||
|
if (!write) memcpy(buffer, bounce, run);
|
||||||
|
}
|
||||||
|
buffer += run;
|
||||||
|
if (incr) addr += run;
|
||||||
|
len -= run;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadBlock(uint32_t fn, uint32_t addr, bool incr, uint8_t* buffer, uint32_t len) {
|
||||||
|
return Transfer(false, fn, addr, incr, buffer, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WriteBlock(uint32_t fn, uint32_t addr, bool incr, const uint8_t* buffer, uint32_t len) {
|
||||||
|
return Transfer(true, fn, addr, incr, (uint8_t*)buffer, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Abort(uint32_t fn) {
|
||||||
|
Cmd52(true, FN0, CCCR_ABORT, (uint8_t)fn, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EnableFunction(uint32_t fn, bool enable, uint64_t timeoutMs) {
|
||||||
|
uint8_t ioen = 0;
|
||||||
|
if (!Read8(FN0, CCCR_IOEN, ioen)) return false;
|
||||||
|
uint8_t bit = (uint8_t)(1u << fn);
|
||||||
|
ioen = enable ? (ioen | bit) : (ioen & ~bit);
|
||||||
|
if (!Write8(FN0, CCCR_IOEN, ioen)) return false;
|
||||||
|
if (!enable) return true;
|
||||||
|
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
for (;;) {
|
||||||
|
uint8_t ready = 0;
|
||||||
|
if (Read8(FN0, CCCR_IORDY, ready) && (ready & bit)) return true;
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||||
|
Timekeeping::BusyWaitUs(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EnableCardInterrupts(uint8_t mask) {
|
||||||
|
return Write8(FN0, CCCR_INTEN, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CardInterruptPending() {
|
||||||
|
return g_initialized && (Read(REG_INT_STATUS) & INT_CARD) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsInitialized() { return g_initialized; }
|
||||||
|
|
||||||
|
bool HostInit(uint64_t mmioPhys, uint32_t baseClockHz) {
|
||||||
|
g_initialized = false;
|
||||||
|
if (!g_regs) {
|
||||||
|
Memory::VMM::g_paging->MapMMIO(mmioPhys & ~0xFFFULL, Memory::HHDM(mmioPhys & ~0xFFFULL));
|
||||||
|
g_regs = (volatile uint8_t*)Memory::HHDM(mmioPhys);
|
||||||
|
}
|
||||||
|
g_clockHz = 0;
|
||||||
|
|
||||||
|
if (!Reset(RESET_ALL)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi-SDIO") << "Controller reset timed out";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_version = (Read(REG_VERSION) >> 16) & 0xFF;
|
||||||
|
uint32_t caps = Read(REG_CAPABILITIES);
|
||||||
|
uint32_t capsMhz = g_version >= 2 ? (caps >> 8) & 0xFF : (caps >> 8) & 0x3F;
|
||||||
|
g_baseClockHz = baseClockHz ? baseClockHz
|
||||||
|
: capsMhz ? capsMhz * 1000000 : FallbackBaseClockHz;
|
||||||
|
|
||||||
|
// The radio's 3.3 V supply is not switched by the controller, but the
|
||||||
|
// bus power bit gates the controller's own drivers.
|
||||||
|
Write(REG_HOST_CONTROL, (Read(REG_HOST_CONTROL) & ~0xFF00u) | PWR_330 | PWR_ON);
|
||||||
|
Write(REG_INT_ENABLE, INT_ENABLES);
|
||||||
|
Write(REG_INT_SIGNAL, 0);
|
||||||
|
Write(REG_CLOCK_CONTROL, (Read(REG_CLOCK_CONTROL) & ~0xFF0000u) | TIMEOUT_MAX);
|
||||||
|
|
||||||
|
if (!SetClock(400000)) {
|
||||||
|
KernelLogStream(ERROR, "WiFi-SDIO") << "Clock did not stabilize";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 74 clocks before the first command.
|
||||||
|
Timekeeping::BusyWaitMs(2);
|
||||||
|
|
||||||
|
KernelLogStream(INFO, "WiFi-SDIO") << "Arasan controller at 0x" << base::hex << mmioPhys
|
||||||
|
<< base::dec << ", version " << (uint64_t)(g_version + 1) << ".00, base clock "
|
||||||
|
<< (uint64_t)(g_baseClockHz / 1000000) << " MHz";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CardInit() {
|
||||||
|
// Allow Transfer()/Cmd52 during bring-up; cleared again on failure.
|
||||||
|
g_initialized = true;
|
||||||
|
auto fail = [](const char* what) {
|
||||||
|
KernelLogStream(WARNING, "WiFi-SDIO") << what;
|
||||||
|
g_initialized = false;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
SendCommand(0, 0, RespNone);
|
||||||
|
Timekeeping::BusyWaitMs(2);
|
||||||
|
|
||||||
|
// CMD5 with no voltage first to read the OCR; the radio may still be
|
||||||
|
// coming out of reset, so give it a few tries.
|
||||||
|
uint32_t ocr = 0;
|
||||||
|
bool answered = false;
|
||||||
|
for (int i = 0; i < 10 && !answered; i++) {
|
||||||
|
answered = SendCommand(5, 0, Resp48NoCrc);
|
||||||
|
if (!answered) Timekeeping::BusyWaitMs(10);
|
||||||
|
}
|
||||||
|
if (!answered) return fail("No SDIO card answers CMD5");
|
||||||
|
ocr = Read(REG_RESPONSE) & 0x00FFFFFF;
|
||||||
|
|
||||||
|
uint32_t want = ocr & 0x00300000; // 3.2-3.4 V
|
||||||
|
if (!want) want = ocr & 0x00FF8000;
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
for (;;) {
|
||||||
|
if (!SendCommand(5, want, Resp48NoCrc)) return fail("CMD5 failed");
|
||||||
|
if (Read(REG_RESPONSE) & (1u << 31)) break;
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > 1000) return fail("Card stayed busy (CMD5)");
|
||||||
|
Timekeeping::BusyWaitMs(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SendCommand(3, 0, Resp48)) return fail("CMD3 failed");
|
||||||
|
g_rca = Read(REG_RESPONSE) >> 16;
|
||||||
|
if (!SendCommand(7, g_rca << 16, Resp48)) return fail("CMD7 failed");
|
||||||
|
|
||||||
|
// 4-bit bus; disconnect the card-detect pull-up on DAT3.
|
||||||
|
uint8_t busif = 0;
|
||||||
|
if (!Read8(FN0, CCCR_BUSIF, busif)) return fail("CCCR read failed");
|
||||||
|
busif = (uint8_t)((busif & ~0x03) | BUSIF_4BIT | BUSIF_CD_DIS);
|
||||||
|
if (!Write8(FN0, CCCR_BUSIF, busif)) return fail("Could not select the 4-bit bus");
|
||||||
|
Write(REG_HOST_CONTROL, Read(REG_HOST_CONTROL) | HC_4BIT);
|
||||||
|
|
||||||
|
if (!SetClock(25000000)) return fail("Clock did not stabilize at 25 MHz");
|
||||||
|
|
||||||
|
// Function block sizes (little endian in the FBR).
|
||||||
|
bool ok = Write8(FN0, 1 * 0x100 + FBR_BLKSIZE, (uint8_t)(FN1_BLOCK & 0xFF))
|
||||||
|
&& Write8(FN0, 1 * 0x100 + FBR_BLKSIZE + 1, (uint8_t)(FN1_BLOCK >> 8))
|
||||||
|
&& Write8(FN0, 2 * 0x100 + FBR_BLKSIZE, (uint8_t)(FN2_BLOCK & 0xFF))
|
||||||
|
&& Write8(FN0, 2 * 0x100 + FBR_BLKSIZE + 1, (uint8_t)(FN2_BLOCK >> 8));
|
||||||
|
if (!ok) return fail("Could not set the function block sizes");
|
||||||
|
|
||||||
|
if (!EnableFunction(FN1, true, 1000)) return fail("Function 1 did not become ready");
|
||||||
|
|
||||||
|
KernelLogStream(OK, "WiFi-SDIO") << "SDIO card ready (RCA 0x" << base::hex << (uint64_t)g_rca
|
||||||
|
<< base::dec << ", OCR 0x" << base::hex << (uint64_t)ocr << base::dec << ", "
|
||||||
|
<< (uint64_t)(g_clockHz / 1000) << " kHz, 4-bit)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* BrcmfSdio.hpp
|
||||||
|
* SDIO host on the Raspberry Pi 4/400's Arasan SD controller, for the
|
||||||
|
* CYW4345x Wi-Fi radio wired to it
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// The BCM2711 has two SD controllers: EMMC2 runs the card slot
|
||||||
|
// (Drivers/Storage/Sdhci) and the older Arasan-derived EMMC, routed to GPIO
|
||||||
|
// 34-39, runs the radio's SDIO interface. This is a minimal SDIO host for
|
||||||
|
// the latter: one card, polled, programmed I/O, CMD52 and CMD53 only.
|
||||||
|
//
|
||||||
|
// None of these calls lock; the Broadcom driver above serializes them.
|
||||||
|
|
||||||
|
namespace Drivers::Net::Wifi::Sdio {
|
||||||
|
|
||||||
|
// SDIO functions on the radio.
|
||||||
|
constexpr uint32_t FN0 = 0; // CCCR / FBR
|
||||||
|
constexpr uint32_t FN1 = 1; // backplane
|
||||||
|
constexpr uint32_t FN2 = 2; // WLAN frames
|
||||||
|
|
||||||
|
// Block sizes the card is configured with.
|
||||||
|
constexpr uint32_t FN1_BLOCK = 64;
|
||||||
|
constexpr uint32_t FN2_BLOCK = 512;
|
||||||
|
|
||||||
|
// Map the controller's registers and reset it. `baseClockHz` is the
|
||||||
|
// controller's input clock (0 = take it from the capabilities).
|
||||||
|
bool HostInit(uint64_t mmioPhys, uint32_t baseClockHz);
|
||||||
|
|
||||||
|
// Enumerate the card: CMD0/5/3/7, 4-bit bus at 25 MHz, function 1/2
|
||||||
|
// block sizes, function 1 enabled. Returns false when nothing answers.
|
||||||
|
bool CardInit();
|
||||||
|
|
||||||
|
// CMD52: one byte.
|
||||||
|
bool Read8(uint32_t fn, uint32_t addr, uint8_t& value);
|
||||||
|
bool Write8(uint32_t fn, uint32_t addr, uint8_t value);
|
||||||
|
|
||||||
|
// CMD53: `len` bytes, a multiple of 4. `incr` selects an incrementing
|
||||||
|
// address (backplane memory) over a fixed one (the function 2 FIFO).
|
||||||
|
// Longer transfers are split into block-mode runs plus a byte-mode tail.
|
||||||
|
bool ReadBlock(uint32_t fn, uint32_t addr, bool incr, uint8_t* buffer, uint32_t len);
|
||||||
|
bool WriteBlock(uint32_t fn, uint32_t addr, bool incr, const uint8_t* buffer, uint32_t len);
|
||||||
|
|
||||||
|
// Enable or disable function `fn`'s I/O; waits up to `timeoutMs` for the
|
||||||
|
// card to report it ready.
|
||||||
|
bool EnableFunction(uint32_t fn, bool enable, uint64_t timeoutMs);
|
||||||
|
|
||||||
|
// CCCR interrupt enables (master | functions).
|
||||||
|
bool EnableCardInterrupts(uint8_t mask);
|
||||||
|
|
||||||
|
// Abort a function's transfer in progress (CCCR I/O Abort).
|
||||||
|
void Abort(uint32_t fn);
|
||||||
|
|
||||||
|
// The card is signalling an interrupt on DAT1. One register read, safe
|
||||||
|
// from any context.
|
||||||
|
bool CardInterruptPending();
|
||||||
|
|
||||||
|
bool IsInitialized();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -443,6 +443,12 @@ namespace Drivers::Net::Wifi {
|
|||||||
// Send an Ethernet frame over the air (called by the netif).
|
// Send an Ethernet frame over the air (called by the netif).
|
||||||
bool IwxConnectSendEthernet(const uint8_t* frame, uint32_t len);
|
bool IwxConnectSendEthernet(const uint8_t* frame, uint32_t len);
|
||||||
|
|
||||||
|
// The supplicant's hooks (Wpa.hpp) for this MLME; Wifi.cpp routes them.
|
||||||
|
bool IwxWpaTxEapol(const uint8_t* body, uint32_t len);
|
||||||
|
bool IwxWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher);
|
||||||
|
bool IwxWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||||
|
uint8_t cipher, const uint8_t* rsc);
|
||||||
|
|
||||||
// Sink for decapsulated Ethernet frames, implemented by Wifi.cpp.
|
// Sink for decapsulated Ethernet frames, implemented by Wifi.cpp.
|
||||||
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
|
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
|
||||||
}
|
}
|
||||||
@@ -535,7 +535,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
|
|
||||||
// The supplicant transmits EAPOL frames unencrypted: they are what
|
// The supplicant transmits EAPOL frames unencrypted: they are what
|
||||||
// establishes the key in the first place.
|
// establishes the key in the first place.
|
||||||
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
|
bool IwxWpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||||
return SendDataFrame(g_bssid, ETHERTYPE_EAPOL, body, len, false);
|
return SendDataFrame(g_bssid, ETHERTYPE_EAPOL, body, len, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,14 +545,14 @@ namespace Drivers::Net::Wifi {
|
|||||||
static InstalledKey g_ptkKey = {};
|
static InstalledKey g_ptkKey = {};
|
||||||
static InstalledKey g_gtkKey = {};
|
static InstalledKey g_gtkKey = {};
|
||||||
|
|
||||||
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
bool IwxWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||||
if (!IwxSetKey(tk, tkLen, 0, true, cipher, nullptr)) return false;
|
if (!IwxSetKey(tk, tkLen, 0, true, cipher, nullptr)) return false;
|
||||||
g_ptkKey = { true, 0, cipher, (uint8_t)tkLen };
|
g_ptkKey = { true, 0, cipher, (uint8_t)tkLen };
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
bool IwxWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||||
uint8_t cipher, const uint8_t* rsc) {
|
uint8_t cipher, const uint8_t* rsc) {
|
||||||
if (gtkLen != 16 && gtkLen != 32) return false;
|
if (gtkLen != 16 && gtkLen != 32) return false;
|
||||||
if (!IwxSetKey(gtk, gtkLen, keyIdx, false, cipher, rsc)) return false;
|
if (!IwxSetKey(gtk, gtkLen, keyIdx, false, cipher, rsc)) return false;
|
||||||
g_gtkKey = { true, keyIdx, cipher, (uint8_t)gtkLen };
|
g_gtkKey = { true, keyIdx, cipher, (uint8_t)gtkLen };
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "Wifi.hpp"
|
#include "Wifi.hpp"
|
||||||
#include <Arch/Cpu.hpp>
|
#include <Arch/Cpu.hpp>
|
||||||
#include "Iwx.hpp"
|
#include "Iwx.hpp"
|
||||||
|
#include "Brcmf.hpp"
|
||||||
#include "Wpa.hpp"
|
#include "Wpa.hpp"
|
||||||
#include <Fs/Vfs.hpp>
|
#include <Fs/Vfs.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
@@ -34,6 +35,72 @@ namespace Drivers::Net::Wifi {
|
|||||||
static std::atomic<bool> g_initPending{false};
|
static std::atomic<bool> g_initPending{false};
|
||||||
static bool g_initialized = false;
|
static bool g_initialized = false;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Backend
|
||||||
|
//
|
||||||
|
// Two kinds of adapter sit behind this facade: the Intel AX210 family on
|
||||||
|
// PCIe (Iwx*: the firmware is a SoftMAC, the MLME is ours) and the
|
||||||
|
// Raspberry Pi 4/400's Broadcom/Cypress radio on SDIO (Brcmf*: FullMAC,
|
||||||
|
// the firmware joins by itself). A machine has at most one; whichever
|
||||||
|
// probes first owns the facade. Both use the same scan table, the same
|
||||||
|
// supplicant and the same connection-state numbering (WIFI_CONN_*, which
|
||||||
|
// IwxConnStateId mirrors).
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
enum class Backend : uint8_t { None, Iwx, Brcmf };
|
||||||
|
static Backend g_backend = Backend::None;
|
||||||
|
|
||||||
|
static bool IsBrcmf() { return g_backend == Backend::Brcmf; }
|
||||||
|
|
||||||
|
static bool FwRunning() {
|
||||||
|
return IsBrcmf() ? BrcmfGetState() == BrcmfState::Running
|
||||||
|
: g_iwx.State == IwxFwState::Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool FwFailed() {
|
||||||
|
return IsBrcmf() ? BrcmfGetState() == BrcmfState::Error
|
||||||
|
: g_iwx.State == IwxFwState::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ScanActive() { return IsBrcmf() ? BrcmfScanActive() : g_iwx.ScanActive; }
|
||||||
|
static bool BeginScan() { return IsBrcmf() ? BrcmfStartScan() : IwxStartScan(nullptr); }
|
||||||
|
|
||||||
|
static void AbortScan() {
|
||||||
|
if (IsBrcmf()) BrcmfAbortScan();
|
||||||
|
else IwxAbortScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
// What a blocking wait pumps: the receive path, and for a join the
|
||||||
|
// state machine behind it.
|
||||||
|
static void PumpEvents() {
|
||||||
|
if (IsBrcmf()) BrcmfService();
|
||||||
|
else IwxProcessEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PumpConnection() {
|
||||||
|
if (IsBrcmf()) {
|
||||||
|
BrcmfService();
|
||||||
|
} else {
|
||||||
|
IwxProcessEvents();
|
||||||
|
IwxConnectService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ConnState() { return IsBrcmf() ? BrcmfConnectState() : IwxConnectState(); }
|
||||||
|
|
||||||
|
static void ConnAbort() {
|
||||||
|
if (IsBrcmf()) BrcmfConnectAbort();
|
||||||
|
else IwxConnectAbort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A handshake that got as far as exchanging EAPOL frames and then failed
|
||||||
|
// is almost always a wrong passphrase.
|
||||||
|
static bool JoinFailedOnAuth() {
|
||||||
|
return IsBrcmf() ? BrcmfJoinFailedOnAuth() : WpaGetState() == WpaState::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool LinkUp() { return IsBrcmf() ? BrcmfLinkUp() : IwxLinkUp(); }
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Scan result table
|
// Scan result table
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -141,22 +208,12 @@ namespace Drivers::Net::Wifi {
|
|||||||
uint8_t RsnLen = 0;
|
uint8_t RsnLen = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
|
// The information elements of a beacon or probe response; `privacy` is
|
||||||
// 24-byte MAC header, then timestamp(8) + beacon interval(2) + caps(2).
|
// the capability field's Privacy bit.
|
||||||
constexpr uint32_t HDR = 24;
|
static void ParseIes(const uint8_t* ie, const uint8_t* end, bool privacy, ParsedBeacon* out) {
|
||||||
constexpr uint32_t FIXED = 12;
|
|
||||||
if (len < HDR + FIXED) return false;
|
|
||||||
|
|
||||||
const uint8_t* fixed = frame + HDR;
|
|
||||||
out->BeaconInterval = (uint16_t)(fixed[8] | (fixed[9] << 8));
|
|
||||||
uint16_t caps = (uint16_t)(fixed[10] | (fixed[11] << 8));
|
|
||||||
bool privacy = (caps & 0x0010) != 0;
|
|
||||||
|
|
||||||
bool haveRsn = false, haveWpa = false;
|
bool haveRsn = false, haveWpa = false;
|
||||||
|
|
||||||
const uint8_t* ie = frame + HDR + FIXED;
|
while (ie && ie + 2 <= end) {
|
||||||
const uint8_t* end = frame + len;
|
|
||||||
while (ie + 2 <= end) {
|
|
||||||
uint8_t id = ie[0];
|
uint8_t id = ie[0];
|
||||||
uint8_t ielen = ie[1];
|
uint8_t ielen = ie[1];
|
||||||
if (ie + 2 + ielen > end) break;
|
if (ie + 2 + ielen > end) break;
|
||||||
@@ -200,9 +257,24 @@ namespace Drivers::Net::Wifi {
|
|||||||
else if (privacy) out->Security = WIFI_SEC_WEP;
|
else if (privacy) out->Security = WIFI_SEC_WEP;
|
||||||
else out->Security = WIFI_SEC_OPEN;
|
else out->Security = WIFI_SEC_OPEN;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
|
||||||
|
// 24-byte MAC header, then timestamp(8) + beacon interval(2) + caps(2).
|
||||||
|
constexpr uint32_t HDR = 24;
|
||||||
|
constexpr uint32_t FIXED = 12;
|
||||||
|
if (len < HDR + FIXED) return false;
|
||||||
|
|
||||||
|
const uint8_t* fixed = frame + HDR;
|
||||||
|
out->BeaconInterval = (uint16_t)(fixed[8] | (fixed[9] << 8));
|
||||||
|
uint16_t caps = (uint16_t)(fixed[10] | (fixed[11] << 8));
|
||||||
|
ParseIes(frame + HDR + FIXED, frame + len, (caps & 0x0010) != 0, out);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void StoreScanResult(const uint8_t* bssid, const ParsedBeacon& pb, uint8_t ch,
|
||||||
|
int8_t rssiDbm);
|
||||||
|
|
||||||
// Called by the MVM RX path for every received management frame.
|
// Called by the MVM RX path for every received management frame.
|
||||||
void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
|
void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
|
||||||
int8_t rssiDbm) {
|
int8_t rssiDbm) {
|
||||||
@@ -224,9 +296,21 @@ namespace Drivers::Net::Wifi {
|
|||||||
ParsedBeacon pb;
|
ParsedBeacon pb;
|
||||||
if (!ParseBeacon(frame, len, &pb)) return;
|
if (!ParseBeacon(frame, len, &pb)) return;
|
||||||
|
|
||||||
const uint8_t* bssid = frame + 16; // addr3 of a beacon
|
// addr3 of a beacon
|
||||||
uint8_t ch = pb.Channel ? pb.Channel : channel;
|
StoreScanResult(frame + 16, pb, pb.Channel ? pb.Channel : channel, rssiDbm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One BSS a FullMAC firmware reported from its own scan.
|
||||||
|
void WifiRxBssInfo(const uint8_t* bssid, uint16_t beaconInterval, uint16_t capability,
|
||||||
|
const uint8_t* ies, uint32_t iesLen, uint8_t channel, int8_t rssiDbm) {
|
||||||
|
ParsedBeacon pb;
|
||||||
|
pb.BeaconInterval = beaconInterval;
|
||||||
|
ParseIes(ies, ies ? ies + iesLen : nullptr, (capability & 0x0010) != 0, &pb);
|
||||||
|
StoreScanResult(bssid, pb, pb.Channel ? pb.Channel : channel, rssiDbm);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void StoreScanResult(const uint8_t* bssid, const ParsedBeacon& pb, uint8_t ch,
|
||||||
|
int8_t rssiDbm) {
|
||||||
g_resultLock.Acquire();
|
g_resultLock.Acquire();
|
||||||
|
|
||||||
ScanEntry* slot = nullptr;
|
ScanEntry* slot = nullptr;
|
||||||
@@ -275,7 +359,9 @@ namespace Drivers::Net::Wifi {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
bool Probe(const Pci::PciDevice& dev) {
|
bool Probe(const Pci::PciDevice& dev) {
|
||||||
|
if (g_backend != Backend::None) return false;
|
||||||
if (!IwxProbe(dev)) return false;
|
if (!IwxProbe(dev)) return false;
|
||||||
|
g_backend = Backend::Iwx;
|
||||||
|
|
||||||
// Firmware comes off the ramdisk, which is not mounted during the PCI
|
// Firmware comes off the ramdisk, which is not mounted during the PCI
|
||||||
// scan; hand the rest of the bring-up to the idle loop.
|
// scan; hand the rest of the bring-up to the idle loop.
|
||||||
@@ -283,7 +369,23 @@ namespace Drivers::Net::Wifi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ProbeBroadcomSdio(uint64_t mmioPhys, const char* name) {
|
||||||
|
if (g_backend != Backend::None) return false;
|
||||||
|
if (!BrcmfProbe(mmioPhys)) return false;
|
||||||
|
g_backend = Backend::Brcmf;
|
||||||
|
KernelLogStream(INFO, "WiFi") << "Broadcom SDIO radio on " << name
|
||||||
|
<< "; firmware loads once the root filesystem is up";
|
||||||
|
// Same reason as the PCI path: the firmware is on the root volume.
|
||||||
|
g_initPending.store(true, std::memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static void CompleteInit() {
|
static void CompleteInit() {
|
||||||
|
if (IsBrcmf()) {
|
||||||
|
g_initialized = BrcmfCompleteInit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!IwxReadFirmware()) {
|
if (!IwxReadFirmware()) {
|
||||||
g_iwx.State = IwxFwState::Error;
|
g_iwx.State = IwxFwState::Error;
|
||||||
return;
|
return;
|
||||||
@@ -348,7 +450,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
static constexpr uint64_t RECOVERY_BACKOFF_MS = 5000;
|
static constexpr uint64_t RECOVERY_BACKOFF_MS = 5000;
|
||||||
|
|
||||||
static void ServiceRecovery() {
|
static void ServiceRecovery() {
|
||||||
if (g_iwx.State != IwxFwState::Error || g_recoveryGaveUp) return;
|
if (!FwFailed() || g_recoveryGaveUp) return;
|
||||||
// An adapter that never finished its first bring-up is ServiceDeferredInit's
|
// An adapter that never finished its first bring-up is ServiceDeferredInit's
|
||||||
// problem, not this one.
|
// problem, not this one.
|
||||||
if (!g_initialized) return;
|
if (!g_initialized) return;
|
||||||
@@ -366,7 +468,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
|
|
||||||
// Re-check under the gate: the winner of the race may have just
|
// Re-check under the gate: the winner of the race may have just
|
||||||
// finished a reset that fixed things.
|
// finished a reset that fixed things.
|
||||||
if (g_iwx.State != IwxFwState::Error) return;
|
if (!FwFailed()) return;
|
||||||
|
|
||||||
uint64_t now = Timekeeping::GetMilliseconds();
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
if (g_lastRecoveryMs && now - g_lastRecoveryMs < RECOVERY_BACKOFF_MS) return;
|
if (g_lastRecoveryMs && now - g_lastRecoveryMs < RECOVERY_BACKOFF_MS) return;
|
||||||
@@ -384,6 +486,24 @@ namespace Drivers::Net::Wifi {
|
|||||||
<< " (attempt " << (uint64_t)g_recoveryAttempts << " of "
|
<< " (attempt " << (uint64_t)g_recoveryAttempts << " of "
|
||||||
<< (uint64_t)MAX_RECOVERY_ATTEMPTS << ")";
|
<< (uint64_t)MAX_RECOVERY_ATTEMPTS << ")";
|
||||||
|
|
||||||
|
if (IsBrcmf()) {
|
||||||
|
// A FullMAC radio is reset by power: WL_ON low, then the whole
|
||||||
|
// bring-up (firmware download included) again.
|
||||||
|
g_initialized = false;
|
||||||
|
g_asyncConnect = false;
|
||||||
|
g_scanDeadline = 0;
|
||||||
|
g_lastError = WIFI_ERR_NO_ADAPTER;
|
||||||
|
ClearResults();
|
||||||
|
BrcmfShutdown();
|
||||||
|
CompleteInit();
|
||||||
|
if (g_initialized) {
|
||||||
|
KernelLogStream(OK, "WiFi") << "Adapter recovered";
|
||||||
|
g_recoveryAttempts = 0;
|
||||||
|
g_lastError = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Nothing may be mid-command while the device is torn down. Taking
|
// Nothing may be mid-command while the device is torn down. Taking
|
||||||
// CmdLock is enough: it is what serializes every sender, and the RX
|
// CmdLock is enough: it is what serializes every sender, and the RX
|
||||||
// pump has its own reentrancy guard.
|
// pump has its own reentrancy guard.
|
||||||
@@ -517,8 +637,24 @@ namespace Drivers::Net::Wifi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The facade's own deadlines (async scan and join) and a due reset.
|
||||||
|
static bool FacadeWorkDue(uint64_t now) {
|
||||||
|
if (g_scanDeadline != 0 && (now >= g_scanDeadline || !ScanActive())) return true;
|
||||||
|
if (g_asyncConnect) return true;
|
||||||
|
return FwFailed() && g_initialized && !g_recoveryGaveUp &&
|
||||||
|
(g_lastRecoveryMs == 0 || now - g_lastRecoveryMs >= RECOVERY_BACKOFF_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasPolledWork() {
|
||||||
|
return IsBrcmf() && BrcmfHasWork(Timekeeping::GetMilliseconds());
|
||||||
|
}
|
||||||
|
|
||||||
bool HasDeferredWork() {
|
bool HasDeferredWork() {
|
||||||
if (InitDue()) return true;
|
if (InitDue()) return true;
|
||||||
|
if (IsBrcmf()) {
|
||||||
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
|
return BrcmfHasWork(now) || FacadeWorkDue(now);
|
||||||
|
}
|
||||||
if (!g_iwx.Mmio) return false;
|
if (!g_iwx.Mmio) return false;
|
||||||
if (g_iwx.WorkPending) return true;
|
if (g_iwx.WorkPending) return true;
|
||||||
|
|
||||||
@@ -527,7 +663,29 @@ namespace Drivers::Net::Wifi {
|
|||||||
return HasBlockingWork(now);
|
return HasBlockingWork(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Broadcom radio has no interrupt in use: everything on its bus is
|
||||||
|
// polled, and everything on its bus waits (SDIO transfers, ioctls), so
|
||||||
|
// all of it goes through the blocking gate.
|
||||||
|
static void ServiceBrcmf() {
|
||||||
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
|
bool radioWork = BrcmfHasWork(now);
|
||||||
|
if (!radioWork && !FacadeWorkDue(now)) return;
|
||||||
|
if (LeaveToAp()) return;
|
||||||
|
if (__atomic_test_and_set(&g_blockingBusy, __ATOMIC_ACQUIRE)) return;
|
||||||
|
struct Guard {
|
||||||
|
~Guard() { __atomic_clear(&g_blockingBusy, __ATOMIC_RELEASE); }
|
||||||
|
} guard;
|
||||||
|
|
||||||
|
if (radioWork) BrcmfService();
|
||||||
|
ServiceAsync();
|
||||||
|
ServiceRecovery();
|
||||||
|
}
|
||||||
|
|
||||||
void ServiceEvents() {
|
void ServiceEvents() {
|
||||||
|
if (IsBrcmf()) {
|
||||||
|
ServiceBrcmf();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!g_iwx.Mmio) return;
|
if (!g_iwx.Mmio) return;
|
||||||
|
|
||||||
// The non-blocking half: any core, the BSP included.
|
// The non-blocking half: any core, the BSP included.
|
||||||
@@ -573,7 +731,11 @@ namespace Drivers::Net::Wifi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool IsInitialized() { return g_initialized; }
|
bool IsInitialized() { return g_initialized; }
|
||||||
bool IsPresent() { return g_iwx.State != IwxFwState::Absent; }
|
|
||||||
|
bool IsPresent() {
|
||||||
|
return IsBrcmf() ? BrcmfGetState() != BrcmfState::Absent
|
||||||
|
: g_iwx.State != IwxFwState::Absent;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Public operations
|
// Public operations
|
||||||
@@ -611,26 +773,26 @@ namespace Drivers::Net::Wifi {
|
|||||||
int Scan(WifiNetwork* out, int maxCount, uint32_t timeoutMs) {
|
int Scan(WifiNetwork* out, int maxCount, uint32_t timeoutMs) {
|
||||||
if (!out || maxCount <= 0) return -1;
|
if (!out || maxCount <= 0) return -1;
|
||||||
if (!g_initialized) return -1;
|
if (!g_initialized) return -1;
|
||||||
if (g_iwx.State != IwxFwState::Running) return -1;
|
if (!FwRunning()) return -1;
|
||||||
|
|
||||||
if (timeoutMs < 1000) timeoutMs = 1000;
|
if (timeoutMs < 1000) timeoutMs = 1000;
|
||||||
if (timeoutMs > 20000) timeoutMs = 20000;
|
if (timeoutMs > 20000) timeoutMs = 20000;
|
||||||
|
|
||||||
ClearResults();
|
ClearResults();
|
||||||
|
|
||||||
if (!IwxStartScan(nullptr)) return -1;
|
if (!BeginScan()) return -1;
|
||||||
|
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (g_iwx.ScanActive && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (ScanActive() && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
IwxProcessEvents();
|
PumpEvents();
|
||||||
for (int i = 0; i < 200; i++) Arch::Pause();
|
for (int i = 0; i < 200; i++) Arch::Pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_iwx.ScanActive) {
|
if (ScanActive()) {
|
||||||
IwxAbortScan();
|
AbortScan();
|
||||||
// Drain whatever the firmware queued before the abort landed.
|
// Drain whatever the firmware queued before the abort landed.
|
||||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - t0 < 200) IwxProcessEvents();
|
while (Timekeeping::GetMilliseconds() - t0 < 200) PumpEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
g_scanGeneration++;
|
g_scanGeneration++;
|
||||||
@@ -639,14 +801,14 @@ namespace Drivers::Net::Wifi {
|
|||||||
|
|
||||||
int StartScan(uint32_t timeoutMs) {
|
int StartScan(uint32_t timeoutMs) {
|
||||||
if (!g_initialized) return -1;
|
if (!g_initialized) return -1;
|
||||||
if (g_iwx.State != IwxFwState::Running) return -1;
|
if (!FwRunning()) return -1;
|
||||||
if (g_iwx.ScanActive) return 1; // already sweeping
|
if (ScanActive()) return 1; // already sweeping
|
||||||
|
|
||||||
if (timeoutMs < 1000) timeoutMs = 1000;
|
if (timeoutMs < 1000) timeoutMs = 1000;
|
||||||
if (timeoutMs > 20000) timeoutMs = 20000;
|
if (timeoutMs > 20000) timeoutMs = 20000;
|
||||||
|
|
||||||
ClearResults();
|
ClearResults();
|
||||||
if (!IwxStartScan(nullptr)) return -1;
|
if (!BeginScan()) return -1;
|
||||||
|
|
||||||
g_scanDeadline = Timekeeping::GetMilliseconds() + timeoutMs;
|
g_scanDeadline = Timekeeping::GetMilliseconds() + timeoutMs;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -657,33 +819,47 @@ namespace Drivers::Net::Wifi {
|
|||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
|
|
||||||
out->present = IsPresent() ? 1 : 0;
|
out->present = IsPresent() ? 1 : 0;
|
||||||
out->state = (uint8_t)g_iwx.State;
|
|
||||||
if (!IsPresent()) return -1;
|
if (!IsPresent()) return -1;
|
||||||
|
|
||||||
memcpy(out->mac, g_iwx.Nvm.HwAddr, 6);
|
const char* fwVersion;
|
||||||
out->scanning = g_iwx.ScanActive ? 1 : 0;
|
if (IsBrcmf()) {
|
||||||
out->bands = (uint8_t)((g_iwx.Nvm.Sku24GHz ? 1 : 0)
|
out->state = (uint8_t)BrcmfGetState();
|
||||||
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
|
memcpy(out->mac, g_brcmf.Mac, 6);
|
||||||
out->channels = (uint16_t)g_iwx.ChannelCount;
|
out->bands = (uint8_t)((g_brcmf.Band24 ? 1 : 0) | (g_brcmf.Band5 ? 2 : 0));
|
||||||
out->rxPackets = g_iwx.RxPackets;
|
out->channels = g_brcmf.ChannelCount;
|
||||||
out->txPackets = g_iwx.TxPackets;
|
out->rxPackets = g_brcmf.RxPackets;
|
||||||
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
out->txPackets = g_brcmf.TxPackets;
|
||||||
out->connState = (uint32_t)IwxConnectState();
|
out->fwErrors = g_brcmf.FwErrors;
|
||||||
out->connected = IwxLinkUp() ? 1 : 0;
|
fwVersion = g_brcmf.FwVersion;
|
||||||
|
} else {
|
||||||
|
out->state = (uint8_t)g_iwx.State;
|
||||||
|
memcpy(out->mac, g_iwx.Nvm.HwAddr, 6);
|
||||||
|
out->bands = (uint8_t)((g_iwx.Nvm.Sku24GHz ? 1 : 0)
|
||||||
|
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
|
||||||
|
out->channels = (uint16_t)g_iwx.ChannelCount;
|
||||||
|
out->rxPackets = g_iwx.RxPackets;
|
||||||
|
out->txPackets = g_iwx.TxPackets;
|
||||||
|
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
||||||
|
fwVersion = g_iwx.Fw.Version;
|
||||||
|
}
|
||||||
|
out->scanning = ScanActive() ? 1 : 0;
|
||||||
|
out->connState = (uint32_t)ConnState();
|
||||||
|
out->connected = LinkUp() ? 1 : 0;
|
||||||
out->lastError = (int32_t)g_lastError;
|
out->lastError = (int32_t)g_lastError;
|
||||||
out->scanGeneration = g_scanGeneration;
|
out->scanGeneration = g_scanGeneration;
|
||||||
out->joining = g_asyncConnect ? 1 : 0;
|
out->joining = g_asyncConnect ? 1 : 0;
|
||||||
|
|
||||||
if (IwxConnectState() != (int)IwxConnStateId::Idle) {
|
if (ConnState() != (int)IwxConnStateId::Idle) {
|
||||||
const char* ssid = IwxConnectSsid();
|
const char* ssid = IsBrcmf() ? BrcmfConnectSsid() : IwxConnectSsid();
|
||||||
int k = 0;
|
int k = 0;
|
||||||
for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k];
|
for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k];
|
||||||
out->ssid[k] = '\0';
|
out->ssid[k] = '\0';
|
||||||
memcpy(out->bssid, IwxConnectBssid(), 6);
|
memcpy(out->bssid, IsBrcmf() ? BrcmfConnectBssid() : IwxConnectBssid(), 6);
|
||||||
|
if (IsBrcmf()) out->channel = BrcmfConnectChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i];
|
for (; i < 31 && fwVersion[i]; i++) out->fwVersion[i] = fwVersion[i];
|
||||||
out->fwVersion[i] = '\0';
|
out->fwVersion[i] = '\0';
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -695,24 +871,20 @@ namespace Drivers::Net::Wifi {
|
|||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
IwxProcessEvents();
|
PumpConnection();
|
||||||
IwxConnectService();
|
|
||||||
|
|
||||||
auto state = (IwxConnStateId)IwxConnectState();
|
auto state = (IwxConnStateId)ConnState();
|
||||||
if (state == IwxConnStateId::Connected) return 0;
|
if (state == IwxConnStateId::Connected) return 0;
|
||||||
if (state == IwxConnStateId::Failed) {
|
if (state == IwxConnStateId::Failed) {
|
||||||
// A handshake that got as far as exchanging EAPOL frames and
|
int rc = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||||
// then failed is almost always a wrong passphrase.
|
|
||||||
int rc = WpaGetState() == WpaState::Failed
|
|
||||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
|
||||||
// A rejection seen on the RX path only marks the state; the
|
// A rejection seen on the RX path only marks the state; the
|
||||||
// firmware contexts are still up and have to come back down.
|
// firmware contexts are still up and have to come back down.
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED;
|
if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED;
|
||||||
if (g_iwx.State == IwxFwState::Error) {
|
if (FwFailed()) {
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return WIFI_ERR_FAILED;
|
return WIFI_ERR_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +893,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
Sched::Schedule();
|
Sched::Schedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return WIFI_ERR_TIMEOUT;
|
return WIFI_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,7 +904,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
// scan table is worth a (blocking) sweep to look for it.
|
// scan table is worth a (blocking) sweep to look for it.
|
||||||
static int StartJoin(const char* ssid, const char* password, bool rescan) {
|
static int StartJoin(const char* ssid, const char* password, bool rescan) {
|
||||||
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
|
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
|
||||||
if (g_iwx.State != IwxFwState::Running) return WIFI_ERR_NO_ADAPTER;
|
if (!FwRunning()) return WIFI_ERR_NO_ADAPTER;
|
||||||
|
|
||||||
// Locate the network in the most recent scan results: the firmware
|
// Locate the network in the most recent scan results: the firmware
|
||||||
// contexts need its BSSID and channel, and the connect path needs its
|
// contexts need its BSSID and channel, and the connect path needs its
|
||||||
@@ -809,17 +981,24 @@ namespace Drivers::Net::Wifi {
|
|||||||
if (!havePassword) return WIFI_ERR_NEED_KEY;
|
if (!havePassword) return WIFI_ERR_NEED_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IwxConnectStart(bssid, channel, is5, ssid,
|
bool started = IsBrcmf()
|
||||||
havePassword ? password : nullptr,
|
? BrcmfConnectStart(bssid, channel, ssid,
|
||||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
havePassword ? password : nullptr,
|
||||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
|
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||||
beaconInterval, dtimPeriod)) {
|
security == WIFI_SEC_OPEN ? 0 : rsnIeLen)
|
||||||
// IwxConnectStart already logged the specific reason. Only report
|
: IwxConnectStart(bssid, channel, is5, ssid,
|
||||||
|
havePassword ? password : nullptr,
|
||||||
|
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||||
|
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
|
||||||
|
beaconInterval, dtimPeriod);
|
||||||
|
if (!started) {
|
||||||
|
// The backend already logged the specific reason. Only report
|
||||||
// a security problem when that is genuinely what happened; a radio
|
// a security problem when that is genuinely what happened; a radio
|
||||||
// or firmware failure needs completely different advice.
|
// or firmware failure needs completely different advice.
|
||||||
bool security_ = IwxConnectRefusedForSecurity();
|
bool security_ = IsBrcmf() ? BrcmfConnectRefusedForSecurity()
|
||||||
auto state = (IwxConnStateId)IwxConnectState();
|
: IwxConnectRefusedForSecurity();
|
||||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
auto state = (IwxConnStateId)ConnState();
|
||||||
|
if (state != IwxConnStateId::Idle) ConnAbort();
|
||||||
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
|
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,8 +1014,8 @@ namespace Drivers::Net::Wifi {
|
|||||||
int ConnectAsync(const char* ssid, const char* password) {
|
int ConnectAsync(const char* ssid, const char* password) {
|
||||||
// A join already in flight owns the firmware contexts; tear it down
|
// A join already in flight owns the firmware contexts; tear it down
|
||||||
// rather than stacking a second one on top.
|
// rather than stacking a second one on top.
|
||||||
if (g_asyncConnect || (IwxConnectState() != (int)IwxConnStateId::Idle))
|
if (g_asyncConnect || (ConnState() != (int)IwxConnStateId::Idle))
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
|
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
g_lastError = 0;
|
g_lastError = 0;
|
||||||
@@ -858,7 +1037,7 @@ namespace Drivers::Net::Wifi {
|
|||||||
if (!g_initialized) return -1;
|
if (!g_initialized) return -1;
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
g_lastError = 0;
|
g_lastError = 0;
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,11 +1060,11 @@ namespace Drivers::Net::Wifi {
|
|||||||
uint64_t now = Timekeeping::GetMilliseconds();
|
uint64_t now = Timekeeping::GetMilliseconds();
|
||||||
|
|
||||||
if (g_scanDeadline) {
|
if (g_scanDeadline) {
|
||||||
if (!g_iwx.ScanActive) {
|
if (!ScanActive()) {
|
||||||
g_scanDeadline = 0;
|
g_scanDeadline = 0;
|
||||||
g_scanGeneration++;
|
g_scanGeneration++;
|
||||||
} else if (now >= g_scanDeadline) {
|
} else if (now >= g_scanDeadline) {
|
||||||
IwxAbortScan();
|
AbortScan();
|
||||||
g_scanDeadline = 0;
|
g_scanDeadline = 0;
|
||||||
g_scanGeneration++;
|
g_scanGeneration++;
|
||||||
}
|
}
|
||||||
@@ -893,31 +1072,29 @@ namespace Drivers::Net::Wifi {
|
|||||||
|
|
||||||
if (!g_asyncConnect) return;
|
if (!g_asyncConnect) return;
|
||||||
|
|
||||||
auto state = (IwxConnStateId)IwxConnectState();
|
auto state = (IwxConnStateId)ConnState();
|
||||||
if (state == IwxConnStateId::Connected) {
|
if (state == IwxConnStateId::Connected) {
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
g_lastError = 0;
|
g_lastError = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state == IwxConnStateId::Failed) {
|
if (state == IwxConnStateId::Failed) {
|
||||||
// Same reading as the blocking path: a handshake that exchanged
|
// Same reading as the blocking path.
|
||||||
// EAPOL frames and then failed is almost always a wrong passphrase.
|
g_lastError = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||||
g_lastError = WpaGetState() == WpaState::Failed
|
|
||||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state == IwxConnStateId::Idle || g_iwx.State == IwxFwState::Error) {
|
if (state == IwxConnStateId::Idle || FwFailed()) {
|
||||||
g_lastError = WIFI_ERR_FAILED;
|
g_lastError = WIFI_ERR_FAILED;
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
if (state != IwxConnStateId::Idle) ConnAbort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (now >= g_asyncConnectDeadline) {
|
if (now >= g_asyncConnectDeadline) {
|
||||||
g_lastError = WIFI_ERR_TIMEOUT;
|
g_lastError = WIFI_ERR_TIMEOUT;
|
||||||
g_asyncConnect = false;
|
g_asyncConnect = false;
|
||||||
IwxConnectAbort();
|
ConnAbort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,13 +1104,33 @@ namespace Drivers::Net::Wifi {
|
|||||||
|
|
||||||
static RxCallback g_rxCallback = nullptr;
|
static RxCallback g_rxCallback = nullptr;
|
||||||
|
|
||||||
const uint8_t* GetMacAddress() { return g_iwx.Nvm.HwAddr; }
|
const uint8_t* GetMacAddress() { return IsBrcmf() ? g_brcmf.Mac : g_iwx.Nvm.HwAddr; }
|
||||||
|
|
||||||
bool IsLinkUp() { return g_initialized && IwxLinkUp(); }
|
bool IsLinkUp() { return g_initialized && LinkUp(); }
|
||||||
|
|
||||||
bool SendPacket(const uint8_t* data, uint16_t length) {
|
bool SendPacket(const uint8_t* data, uint16_t length) {
|
||||||
if (!IsLinkUp()) return false;
|
if (!IsLinkUp()) return false;
|
||||||
return IwxConnectSendEthernet(data, length);
|
return IsBrcmf() ? BrcmfSendEthernet(data, length)
|
||||||
|
: IwxConnectSendEthernet(data, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Supplicant hooks (Wpa.hpp): to whichever backend is running the join
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||||
|
return IsBrcmf() ? BrcmfWpaTxEapol(body, len) : IwxWpaTxEapol(body, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||||
|
return IsBrcmf() ? BrcmfWpaInstallPtk(tk, tkLen, cipher)
|
||||||
|
: IwxWpaInstallPtk(tk, tkLen, cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||||
|
uint8_t cipher, const uint8_t* rsc) {
|
||||||
|
return IsBrcmf() ? BrcmfWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc)
|
||||||
|
: IwxWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetRxCallback(RxCallback callback) { g_rxCallback = callback; }
|
void SetRxCallback(RxCallback callback) { g_rxCallback = callback; }
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ namespace Drivers::Net::Wifi {
|
|||||||
// PCI driver-table probe entry point.
|
// PCI driver-table probe entry point.
|
||||||
bool Probe(const Pci::PciDevice& dev);
|
bool Probe(const Pci::PciDevice& dev);
|
||||||
|
|
||||||
|
// The Raspberry Pi 4/400's Broadcom/Cypress radio, on the SDIO
|
||||||
|
// controller (Arasan) at `mmioPhys`. Claims it; the bring-up waits for
|
||||||
|
// the root filesystem like the PCI path.
|
||||||
|
bool ProbeBroadcomSdio(uint64_t mmioPhys, const char* name);
|
||||||
|
|
||||||
// Complete the firmware-dependent bring-up once the ramdisk is mounted.
|
// Complete the firmware-dependent bring-up once the ramdisk is mounted.
|
||||||
// Cheap no-op unless a device is waiting. Called from the idle loop.
|
// Cheap no-op unless a device is waiting. Called from the idle loop.
|
||||||
void ServiceDeferredInit();
|
void ServiceDeferredInit();
|
||||||
@@ -25,6 +30,11 @@ namespace Drivers::Net::Wifi {
|
|||||||
// deadline, or a due recovery attempt needs idle-context servicing.
|
// deadline, or a due recovery attempt needs idle-context servicing.
|
||||||
bool HasDeferredWork();
|
bool HasDeferredWork();
|
||||||
|
|
||||||
|
// A polled adapter (the Broadcom radio has no interrupt in use) is due a
|
||||||
|
// sweep. Cheap and safe from the timer interrupt: the tick uses it to
|
||||||
|
// force a bottom-half pass when the cores never go idle.
|
||||||
|
bool HasPolledWork();
|
||||||
|
|
||||||
bool IsInitialized();
|
bool IsInitialized();
|
||||||
bool IsPresent();
|
bool IsPresent();
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ namespace Drivers::Net::Wifi {
|
|||||||
bool WpaParseApRsn(const uint8_t* ie, uint32_t len, WpaConfig& cfg);
|
bool WpaParseApRsn(const uint8_t* ie, uint32_t len, WpaConfig& cfg);
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Hooks implemented by the MLME (IwxConnect.cpp)
|
// Hooks into the driver running the join (Wifi.cpp routes them to
|
||||||
|
// IwxConnect.cpp or BrcmfConnect.cpp)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
// Transmit an EAPOL frame body (802.1X header included) to the AP.
|
// Transmit an EAPOL frame body (802.1X header included) to the AP.
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
/*
|
||||||
|
* RpiFirmware.cpp
|
||||||
|
* Raspberry Pi VideoCore firmware mailbox (property interface)
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "RpiFirmware.hpp"
|
||||||
|
#include <ACPI/AcpiDevices.hpp>
|
||||||
|
#include <Arch/Cpu.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
#include <Boot/Boot.hpp>
|
||||||
|
#include <Memory/Memmap.hpp>
|
||||||
|
#include <Memory/HHDM.hpp>
|
||||||
|
#include <Memory/Paging.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <Timekeeping/Timer.hpp>
|
||||||
|
|
||||||
|
// Property interface (raspberrypi/firmware wiki, "Mailbox property
|
||||||
|
// interface"): the ARM writes a message buffer's bus address, tagged with
|
||||||
|
// channel 8, to mailbox 1 and reads the same address back from mailbox 0
|
||||||
|
// once the firmware has written its responses into the buffer.
|
||||||
|
//
|
||||||
|
// u32 size, u32 code (0 = request; 0x80000000 = success),
|
||||||
|
// tags: { u32 id, u32 value buffer size, u32 req/resp (bit 31 + length),
|
||||||
|
// value buffer ... }, u32 0 (end tag)
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Drivers::Platform::RpiFirmware {
|
||||||
|
|
||||||
|
// Mailbox registers (offsets from the block ACPI describes, 0x7E00B880)
|
||||||
|
static constexpr uint32_t MBOX_READ = 0x00; // mailbox 0: VC -> ARM
|
||||||
|
static constexpr uint32_t MBOX0_STATUS = 0x18;
|
||||||
|
static constexpr uint32_t MBOX_WRITE = 0x20; // mailbox 1: ARM -> VC
|
||||||
|
static constexpr uint32_t MBOX1_STATUS = 0x38;
|
||||||
|
static constexpr uint32_t STATUS_FULL = 1u << 31;
|
||||||
|
static constexpr uint32_t STATUS_EMPTY = 1u << 30;
|
||||||
|
|
||||||
|
static constexpr uint32_t CHANNEL_PROPERTY = 8;
|
||||||
|
|
||||||
|
static constexpr uint32_t CODE_REQUEST = 0x00000000;
|
||||||
|
static constexpr uint32_t CODE_SUCCESS = 0x80000000;
|
||||||
|
static constexpr uint32_t TAG_RESPONSE = 0x80000000;
|
||||||
|
|
||||||
|
static constexpr uint32_t TAG_GET_BOARD_MAC = 0x00010003;
|
||||||
|
static constexpr uint32_t TAG_SET_CLOCK_STATE = 0x00038001;
|
||||||
|
static constexpr uint32_t TAG_GET_CLOCK_RATE = 0x00030002;
|
||||||
|
static constexpr uint32_t TAG_GET_GPIO_STATE = 0x00030041;
|
||||||
|
static constexpr uint32_t TAG_SET_GPIO_STATE = 0x00038041;
|
||||||
|
static constexpr uint32_t TAG_GET_GPIO_CONFIG = 0x00030043;
|
||||||
|
static constexpr uint32_t TAG_SET_GPIO_CONFIG = 0x00038043;
|
||||||
|
|
||||||
|
// The VideoCore sees the first GiB of SDRAM at bus address 0xC0000000
|
||||||
|
// (the uncached alias), and nothing above it, so the message buffer
|
||||||
|
// must sit below 1 GiB.
|
||||||
|
static constexpr uint64_t VC_BUS_ALIAS = 0xC0000000;
|
||||||
|
static constexpr uint64_t VC_DMA_LIMIT = 0x40000000;
|
||||||
|
|
||||||
|
static constexpr uint64_t TimeoutMs = 200;
|
||||||
|
|
||||||
|
static volatile uint8_t* g_mmio = nullptr;
|
||||||
|
static volatile uint32_t* g_buffer = nullptr; // uncached, below 1 GiB
|
||||||
|
static uint64_t g_bufferPhys = 0;
|
||||||
|
static bool g_probed = false;
|
||||||
|
static kcp::Mutex g_lock;
|
||||||
|
|
||||||
|
static inline uint32_t Read32(uint32_t offset) {
|
||||||
|
return *(volatile uint32_t*)(g_mmio + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void Write32(uint32_t offset, uint32_t value) {
|
||||||
|
*(volatile uint32_t*)(g_mmio + offset) = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A page the VideoCore can reach. The page-frame allocator only manages
|
||||||
|
// the largest run of usable RAM, which on a Pi 4 with the 3 GB limit is
|
||||||
|
// 0x40000000 and up, so it never has one. The usable regions below 1 GiB
|
||||||
|
// are otherwise untouched: take the last page of the highest one.
|
||||||
|
static void* AllocateLowPage(uint64_t& phys) {
|
||||||
|
const auto& map = montauk::boot::Info().memoryMap;
|
||||||
|
Memory::LargestSection pool = Memory::Scan(map);
|
||||||
|
|
||||||
|
uint64_t best = 0;
|
||||||
|
for (uint64_t i = 0; i < map.count; i++) {
|
||||||
|
const auto& r = map.regions[i];
|
||||||
|
if (r.kind != montauk::boot::MemoryKind::Usable || r.base == 0) continue;
|
||||||
|
if (r.base == pool.address) continue; // the allocator's
|
||||||
|
uint64_t end = r.base + r.length;
|
||||||
|
if (end > VC_DMA_LIMIT) end = VC_DMA_LIMIT;
|
||||||
|
if (end < r.base + 0x1000) continue;
|
||||||
|
uint64_t page = (end - 0x1000) & ~0xFFFULL;
|
||||||
|
if (page >= r.base && page > best) best = page;
|
||||||
|
}
|
||||||
|
if (best == 0) return nullptr;
|
||||||
|
|
||||||
|
// Zero it through its cacheable HHDM alias, push the lines out, then
|
||||||
|
// make the alias uncached so neither side needs cache maintenance.
|
||||||
|
void* virt = (void*)Memory::HHDM(best);
|
||||||
|
memset(virt, 0, 0x1000);
|
||||||
|
Arch::InvalidateDataCache(virt, 0x1000);
|
||||||
|
Memory::VMM::g_paging->MapWC(best, (uint64_t)virt);
|
||||||
|
phys = best;
|
||||||
|
return virt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Initialize() {
|
||||||
|
if (g_probed) return g_buffer != nullptr;
|
||||||
|
g_probed = true;
|
||||||
|
|
||||||
|
Hal::AcpiDevices::DeviceInfo info{};
|
||||||
|
if (!Hal::AcpiDevices::FindDeviceByHid("BCM2849", info)) return false;
|
||||||
|
|
||||||
|
Hal::AML::ResourceList resources;
|
||||||
|
uint64_t base = 0, length = 0;
|
||||||
|
if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) ||
|
||||||
|
!resources.GetMemory(base, length) || base == 0) {
|
||||||
|
KernelLogStream(WARNING, "RpiFw") << "Mailbox " << info.Path << ": no register block in _CRS";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t page = base & ~0xFFFULL;
|
||||||
|
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
|
||||||
|
if (((base + 0x40) & ~0xFFFULL) != page) {
|
||||||
|
Memory::VMM::g_paging->MapMMIO(page + 0x1000, Memory::HHDM(page + 0x1000));
|
||||||
|
}
|
||||||
|
g_mmio = (volatile uint8_t*)Memory::HHDM(base);
|
||||||
|
|
||||||
|
uint64_t phys = 0;
|
||||||
|
void* buffer = AllocateLowPage(phys);
|
||||||
|
if (buffer == nullptr) {
|
||||||
|
KernelLogStream(ERROR, "RpiFw") << "No message buffer below 1 GiB";
|
||||||
|
g_mmio = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_buffer = (volatile uint32_t*)buffer;
|
||||||
|
g_bufferPhys = phys;
|
||||||
|
|
||||||
|
KernelLogStream(OK, "RpiFw") << "VideoCore mailbox at 0x" << base::hex << base
|
||||||
|
<< ", buffer 0x" << phys << base::dec;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsPresent() {
|
||||||
|
return Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the message in g_buffer and wait for the firmware's reply.
|
||||||
|
// Caller holds g_lock.
|
||||||
|
static bool Call() {
|
||||||
|
uint32_t message = (uint32_t)((g_bufferPhys | VC_BUS_ALIAS) & ~0xFULL) | CHANNEL_PROPERTY;
|
||||||
|
|
||||||
|
// Drop any stale replies (a previous call that timed out).
|
||||||
|
for (int i = 0; i < 16 && !(Read32(MBOX0_STATUS) & STATUS_EMPTY); i++) {
|
||||||
|
(void)Read32(MBOX_READ);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
while (Read32(MBOX1_STATUS) & STATUS_FULL) {
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > TimeoutMs) return false;
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The buffer is uncached; the fence orders its stores before the
|
||||||
|
// doorbell write the firmware reacts to.
|
||||||
|
Arch::MemoryFence();
|
||||||
|
Write32(MBOX_WRITE, message);
|
||||||
|
|
||||||
|
start = Timekeeping::GetMilliseconds();
|
||||||
|
for (;;) {
|
||||||
|
while (Read32(MBOX0_STATUS) & STATUS_EMPTY) {
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > TimeoutMs) {
|
||||||
|
KernelLogStream(WARNING, "RpiFw") << "Mailbox call timed out";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
uint32_t reply = Read32(MBOX_READ);
|
||||||
|
if (reply == message) break;
|
||||||
|
}
|
||||||
|
Arch::MemoryFence();
|
||||||
|
return g_buffer[1] == CODE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-tag property call. `values` holds valueWords request words and
|
||||||
|
// receives up to valueWords response words. Returns the response length
|
||||||
|
// in bytes, or -1 on failure.
|
||||||
|
static int PropertyCall(uint32_t tag, uint32_t* values, uint32_t valueWords) {
|
||||||
|
if (!Initialize()) return -1;
|
||||||
|
if (valueWords > 16) return -1;
|
||||||
|
|
||||||
|
g_lock.Acquire();
|
||||||
|
uint32_t words = 0;
|
||||||
|
g_buffer[words++] = 0; // size, filled in below
|
||||||
|
g_buffer[words++] = CODE_REQUEST;
|
||||||
|
g_buffer[words++] = tag;
|
||||||
|
g_buffer[words++] = valueWords * 4; // value buffer size
|
||||||
|
g_buffer[words++] = 0; // request
|
||||||
|
uint32_t valueStart = words;
|
||||||
|
for (uint32_t i = 0; i < valueWords; i++) g_buffer[words++] = values[i];
|
||||||
|
g_buffer[words++] = 0; // end tag
|
||||||
|
g_buffer[0] = words * 4;
|
||||||
|
|
||||||
|
// The firmware does not set the response bit for every tag: the
|
||||||
|
// expander GPIO tags answer through value[0] alone and leave the
|
||||||
|
// status word 0. Linux checks only the message code, so do the same
|
||||||
|
// and take the whole value buffer back when the bit is missing; each
|
||||||
|
// caller validates the values it gets.
|
||||||
|
int result = -1;
|
||||||
|
bool answered = Call();
|
||||||
|
uint32_t status = g_buffer[valueStart - 1];
|
||||||
|
if (answered) {
|
||||||
|
uint32_t length = (status & TAG_RESPONSE) ? status & ~TAG_RESPONSE : valueWords * 4;
|
||||||
|
uint32_t copy = (length + 3) / 4;
|
||||||
|
if (copy > valueWords) copy = valueWords;
|
||||||
|
for (uint32_t i = 0; i < copy; i++) values[i] = g_buffer[valueStart + i];
|
||||||
|
result = (int)length;
|
||||||
|
}
|
||||||
|
uint32_t code = g_buffer[1];
|
||||||
|
g_lock.Release();
|
||||||
|
|
||||||
|
if (result < 0) {
|
||||||
|
KernelLogStream(WARNING, "RpiFw") << "Property tag 0x" << base::hex << (uint64_t)tag
|
||||||
|
<< " failed: message code 0x" << (uint64_t)code << base::dec;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetClockRate(uint32_t clockId, uint32_t& hz) {
|
||||||
|
uint32_t v[2] = { clockId, 0 };
|
||||||
|
if (PropertyCall(TAG_GET_CLOCK_RATE, v, 2) < 8 || v[0] != clockId) return false;
|
||||||
|
hz = v[1];
|
||||||
|
return hz != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// State bit 0 = on; the answer echoes the clock ID and its new state
|
||||||
|
// (bit 1 set: no such clock).
|
||||||
|
bool SetClockState(uint32_t clockId, bool on) {
|
||||||
|
uint32_t v[2] = { clockId, on ? 1u : 0u };
|
||||||
|
if (PropertyCall(TAG_SET_CLOCK_STATE, v, 2) < 8 || v[0] != clockId) return false;
|
||||||
|
return (v[1] & 2) == 0 && ((v[1] & 1) != 0) == on;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GPIO tags answer in place of the GPIO number with a status word,
|
||||||
|
// 0 for success (as Linux's raspberrypi-exp-gpio driver checks).
|
||||||
|
bool SetGpioState(uint32_t gpio, bool high) {
|
||||||
|
uint32_t v[2] = { gpio, high ? 1u : 0u };
|
||||||
|
return PropertyCall(TAG_SET_GPIO_STATE, v, 2) >= 4 && v[0] == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// As Linux's rpi_exp_gpio_dir_out: keep the line's polarity, make it an
|
||||||
|
// output driving `high`. A line the firmware still has as an input
|
||||||
|
// refuses SET_GPIO_STATE.
|
||||||
|
bool SetGpioOutput(uint32_t gpio, bool high) {
|
||||||
|
uint32_t polarity = 0;
|
||||||
|
uint32_t cfg[5] = { gpio, 0, 0, 0, 0 }; // gpio, direction, polarity, term_en, term_pull_up
|
||||||
|
if (PropertyCall(TAG_GET_GPIO_CONFIG, cfg, 5) >= 12 && cfg[0] == 0) polarity = cfg[2];
|
||||||
|
|
||||||
|
uint32_t v[6] = { gpio, 1 /* output */, polarity, 0, 0, high ? 1u : 0u };
|
||||||
|
if (PropertyCall(TAG_SET_GPIO_CONFIG, v, 6) < 4 || v[0] != 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetGpioState(uint32_t gpio, bool& high) {
|
||||||
|
uint32_t v[2] = { gpio, 0 };
|
||||||
|
if (PropertyCall(TAG_GET_GPIO_STATE, v, 2) < 8 || v[0] != 0) return false;
|
||||||
|
high = v[1] != 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetBoardMacAddress(uint8_t mac[6]) {
|
||||||
|
uint32_t v[2] = {};
|
||||||
|
if (PropertyCall(TAG_GET_BOARD_MAC, v, 2) < 6) return false;
|
||||||
|
const uint8_t* bytes = (const uint8_t*)v;
|
||||||
|
bool allZero = true;
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
mac[i] = bytes[i];
|
||||||
|
if (bytes[i] != 0) allZero = false;
|
||||||
|
}
|
||||||
|
return !allZero;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* RpiFirmware.hpp
|
||||||
|
* Raspberry Pi VideoCore firmware mailbox (property interface)
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// The VideoCore firmware owns parts of the board the ARM cores cannot reach
|
||||||
|
// directly: the GPIO expander that powers the Wi-Fi/Bluetooth radio, the
|
||||||
|
// clock tree, the board's OTP (serial number, MAC address). The ARM side asks
|
||||||
|
// for them through the property channel of the VideoCore mailbox, which
|
||||||
|
// ACPI describes as BCM2849 (\_SB.GDV0.RPIQ) on the Pi Firmware Task Force
|
||||||
|
// UEFI firmware.
|
||||||
|
//
|
||||||
|
// All calls are synchronous, take a process-context lock, and must not be
|
||||||
|
// made from interrupt context. They return false when there is no mailbox
|
||||||
|
// (any machine but a Pi) or the firmware rejects the request.
|
||||||
|
|
||||||
|
namespace Drivers::Platform::RpiFirmware {
|
||||||
|
|
||||||
|
// Find the mailbox through ACPI and allocate its message buffer.
|
||||||
|
// Idempotent; the other calls initialize on first use.
|
||||||
|
bool Initialize();
|
||||||
|
|
||||||
|
bool IsPresent();
|
||||||
|
|
||||||
|
// Clock identifiers (the firmware's numbering).
|
||||||
|
constexpr uint32_t CLOCK_EMMC = 1; // legacy EMMC (Arasan), runs the Wi-Fi SDIO bus
|
||||||
|
constexpr uint32_t CLOCK_CORE = 4; // VPU core clock, feeds the mini UART
|
||||||
|
|
||||||
|
// Current rate of a firmware clock, in Hz.
|
||||||
|
bool GetClockRate(uint32_t clockId, uint32_t& hz);
|
||||||
|
|
||||||
|
// Gate a firmware clock on or off.
|
||||||
|
bool SetClockState(uint32_t clockId, bool on);
|
||||||
|
|
||||||
|
// The firmware's GPIO expander lines are numbered from 128.
|
||||||
|
constexpr uint32_t EXPGPIO_BASE = 128;
|
||||||
|
constexpr uint32_t EXPGPIO_BT_ON = EXPGPIO_BASE + 0; // Bluetooth power (high = on)
|
||||||
|
constexpr uint32_t EXPGPIO_WL_ON = EXPGPIO_BASE + 1; // Wi-Fi power (high = on)
|
||||||
|
|
||||||
|
bool SetGpioState(uint32_t gpio, bool high);
|
||||||
|
// Configure the line as an output, then drive it (SET_GPIO_CONFIG).
|
||||||
|
bool SetGpioOutput(uint32_t gpio, bool high);
|
||||||
|
bool GetGpioState(uint32_t gpio, bool& high);
|
||||||
|
|
||||||
|
// The board's Ethernet MAC address from OTP, in transmission order
|
||||||
|
// (mac[0] is the first octet of the OUI).
|
||||||
|
bool GetBoardMacAddress(uint8_t mac[6]);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* RpiGpio.cpp
|
||||||
|
* BCM2711 GPIO pin function and pull control
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "RpiGpio.hpp"
|
||||||
|
#include <ACPI/AcpiDevices.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Memory/HHDM.hpp>
|
||||||
|
#include <Memory/Paging.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Drivers::Platform::RpiGpio {
|
||||||
|
|
||||||
|
// BCM2711 ARM Peripherals, chapter 5
|
||||||
|
static constexpr uint32_t REG_GPFSEL0 = 0x00; // 10 pins per register, 3 bits each
|
||||||
|
static constexpr uint32_t REG_PUP_PDN_CNTRL0 = 0xE4; // 16 pins per register, 2 bits each
|
||||||
|
static constexpr uint32_t PinCount = 58;
|
||||||
|
|
||||||
|
static volatile uint8_t* g_mmio = nullptr;
|
||||||
|
static bool g_probed = false;
|
||||||
|
static kcp::Spinlock g_lock;
|
||||||
|
|
||||||
|
static bool Initialize() {
|
||||||
|
if (g_probed) return g_mmio != nullptr;
|
||||||
|
g_probed = true;
|
||||||
|
|
||||||
|
Hal::AcpiDevices::DeviceInfo info{};
|
||||||
|
if (!Hal::AcpiDevices::FindDeviceByHid("BCM2845", info)) return false;
|
||||||
|
|
||||||
|
Hal::AML::ResourceList resources;
|
||||||
|
uint64_t base = 0, length = 0;
|
||||||
|
if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) ||
|
||||||
|
!resources.GetMemory(base, length) || base == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t page = base & ~0xFFFULL;
|
||||||
|
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
|
||||||
|
g_mmio = (volatile uint8_t*)Memory::HHDM(base);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Modify(uint32_t offset, uint32_t mask, uint32_t value) {
|
||||||
|
g_lock.Acquire();
|
||||||
|
volatile uint32_t* reg = (volatile uint32_t*)(g_mmio + offset);
|
||||||
|
*reg = (*reg & ~mask) | (value & mask);
|
||||||
|
g_lock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetFunction(uint32_t pin, Function function) {
|
||||||
|
if (pin >= PinCount || !Initialize()) return false;
|
||||||
|
uint32_t shift = (pin % 10) * 3;
|
||||||
|
Modify(REG_GPFSEL0 + (pin / 10) * 4, 7u << shift, (uint32_t)function << shift);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetPull(uint32_t pin, Pull pull) {
|
||||||
|
if (pin >= PinCount || !Initialize()) return false;
|
||||||
|
uint32_t shift = (pin % 16) * 2;
|
||||||
|
Modify(REG_PUP_PDN_CNTRL0 + (pin / 16) * 4, 3u << shift, (uint32_t)pull << shift);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* RpiGpio.hpp
|
||||||
|
* BCM2711 GPIO pin function and pull control
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// The SoC's own GPIO block (ACPI BCM2845, \_SB.GDV0.GPI0), used to route
|
||||||
|
// pins to the peripheral that drives them. On Linux the device tree's
|
||||||
|
// pinctrl does this; booted through ACPI, nothing does it for us. Only the
|
||||||
|
// BCM2711 (Raspberry Pi 4/400) pull-control layout is implemented.
|
||||||
|
|
||||||
|
namespace Drivers::Platform::RpiGpio {
|
||||||
|
|
||||||
|
enum class Function : uint8_t {
|
||||||
|
Input = 0b000,
|
||||||
|
Output = 0b001,
|
||||||
|
Alt0 = 0b100,
|
||||||
|
Alt1 = 0b101,
|
||||||
|
Alt2 = 0b110,
|
||||||
|
Alt3 = 0b111,
|
||||||
|
Alt4 = 0b011,
|
||||||
|
Alt5 = 0b010,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Pull : uint8_t {
|
||||||
|
None = 0b00,
|
||||||
|
Up = 0b01,
|
||||||
|
Down = 0b10,
|
||||||
|
};
|
||||||
|
|
||||||
|
bool SetFunction(uint32_t pin, Function function);
|
||||||
|
bool SetPull(uint32_t pin, Pull pull);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,488 @@
|
|||||||
|
/*
|
||||||
|
* BtUart.cpp
|
||||||
|
* Interrupt-driven UART to an on-board Bluetooth controller
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "BtUart.hpp"
|
||||||
|
#include <ACPI/GsiRouting.hpp>
|
||||||
|
#include <Arch/Cpu.hpp>
|
||||||
|
#include <Arch/Interrupts.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Memory/HHDM.hpp>
|
||||||
|
#include <Memory/Paging.hpp>
|
||||||
|
#include <Sched/Scheduler.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <Timekeeping/Timer.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Drivers::Serial::BtUart {
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Registers
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Mini UART, offsets from the AUX block (BCM2711 ARM Peripherals 2.2,
|
||||||
|
// with the elinux.org BCM2835 datasheet errata: IER bit 0 enables the
|
||||||
|
// receive interrupt and bit 1 the transmit one, IER bits 3:2 must be set
|
||||||
|
// for interrupts to be raised, LCR needs 0b11 for 8 data bits).
|
||||||
|
static constexpr uint32_t AUX_ENABLES = 0x04;
|
||||||
|
static constexpr uint32_t MU_IO = 0x40;
|
||||||
|
static constexpr uint32_t MU_IER = 0x44;
|
||||||
|
static constexpr uint32_t MU_IIR = 0x48;
|
||||||
|
static constexpr uint32_t MU_LCR = 0x4C;
|
||||||
|
static constexpr uint32_t MU_MCR = 0x50;
|
||||||
|
static constexpr uint32_t MU_LSR = 0x54;
|
||||||
|
static constexpr uint32_t MU_CNTL = 0x60;
|
||||||
|
static constexpr uint32_t MU_BAUD = 0x68;
|
||||||
|
|
||||||
|
static constexpr uint32_t AUX_ENABLE_MU = 1u << 0;
|
||||||
|
static constexpr uint32_t MU_IER_RX = 1u << 0;
|
||||||
|
static constexpr uint32_t MU_IER_TX = 1u << 1;
|
||||||
|
static constexpr uint32_t MU_IER_REQUIRED = 3u << 2;
|
||||||
|
static constexpr uint32_t MU_IIR_FLUSH = 0xC6; // clear both FIFOs, FIFOs on
|
||||||
|
static constexpr uint32_t MU_LCR_8BIT = 0x03;
|
||||||
|
static constexpr uint32_t MU_MCR_RTS = 1u << 1; // RTS pin low (asserted)
|
||||||
|
static constexpr uint32_t MU_LSR_DATA_READY = 1u << 0;
|
||||||
|
static constexpr uint32_t MU_LSR_OVERRUN = 1u << 1;
|
||||||
|
static constexpr uint32_t MU_LSR_TX_SPACE = 1u << 5; // FIFO accepts a byte
|
||||||
|
static constexpr uint32_t MU_LSR_TX_IDLE = 1u << 6; // FIFO empty, shifter idle
|
||||||
|
static constexpr uint32_t MU_CNTL_RXEN = 1u << 0;
|
||||||
|
static constexpr uint32_t MU_CNTL_TXEN = 1u << 1;
|
||||||
|
// Auto RTS at the default level (deassert with 3 FIFO spaces left).
|
||||||
|
// Bit 6 picks the level auto RTS asserts: set = low, clear (the reset
|
||||||
|
// value) = high. A UART's RTS is active low, so it must be set: left
|
||||||
|
// clear, RTS reads "stop" to the controller the moment auto RTS is on.
|
||||||
|
// Bits 3 and 7 are the same for CTS: on a Pi 400, with bit 7 clear the
|
||||||
|
// transmitter held its bytes while the controller drove CTS low (ready).
|
||||||
|
static constexpr uint32_t MU_CNTL_AUTORTS = 1u << 2;
|
||||||
|
static constexpr uint32_t MU_CNTL_AUTOCTS = 1u << 3;
|
||||||
|
static constexpr uint32_t MU_CNTL_RTS_LOW = 1u << 6;
|
||||||
|
static constexpr uint32_t MU_CNTL_CTS_LOW = 1u << 7;
|
||||||
|
|
||||||
|
// PL011 (ARM PrimeCell UART TRM)
|
||||||
|
static constexpr uint32_t PL_DR = 0x00;
|
||||||
|
static constexpr uint32_t PL_FR = 0x18;
|
||||||
|
static constexpr uint32_t PL_IBRD = 0x24;
|
||||||
|
static constexpr uint32_t PL_FBRD = 0x28;
|
||||||
|
static constexpr uint32_t PL_LCRH = 0x2C;
|
||||||
|
static constexpr uint32_t PL_CR = 0x30;
|
||||||
|
static constexpr uint32_t PL_IFLS = 0x34;
|
||||||
|
static constexpr uint32_t PL_IMSC = 0x38;
|
||||||
|
static constexpr uint32_t PL_ICR = 0x44;
|
||||||
|
|
||||||
|
static constexpr uint32_t PL_DR_OE = 1u << 11;
|
||||||
|
static constexpr uint32_t PL_FR_BUSY = 1u << 3;
|
||||||
|
static constexpr uint32_t PL_FR_RXFE = 1u << 4;
|
||||||
|
static constexpr uint32_t PL_FR_TXFF = 1u << 5;
|
||||||
|
static constexpr uint32_t PL_FR_TXFE = 1u << 7;
|
||||||
|
static constexpr uint32_t PL_LCRH_FEN = 1u << 4;
|
||||||
|
static constexpr uint32_t PL_LCRH_8BIT = 3u << 5;
|
||||||
|
static constexpr uint32_t PL_CR_UARTEN = 1u << 0;
|
||||||
|
static constexpr uint32_t PL_CR_TXE = 1u << 8;
|
||||||
|
static constexpr uint32_t PL_CR_RXE = 1u << 9;
|
||||||
|
static constexpr uint32_t PL_CR_RTS = 1u << 11;
|
||||||
|
static constexpr uint32_t PL_CR_RTSEN = 1u << 14;
|
||||||
|
static constexpr uint32_t PL_CR_CTSEN = 1u << 15;
|
||||||
|
static constexpr uint32_t PL_INT_RX = 1u << 4;
|
||||||
|
static constexpr uint32_t PL_INT_TX = 1u << 5;
|
||||||
|
static constexpr uint32_t PL_INT_RT = 1u << 6; // receive timeout
|
||||||
|
static constexpr uint32_t PL_INT_OE = 1u << 10;
|
||||||
|
static constexpr uint32_t PL_IFLS_RX_HALF = 2u << 3;
|
||||||
|
static constexpr uint32_t PL_IFLS_TX_EIGHTH = 0;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// State
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static constexpr uint32_t RX_RING_SIZE = 16384; // powers of two
|
||||||
|
static constexpr uint32_t TX_RING_SIZE = 16384;
|
||||||
|
|
||||||
|
static Kind g_kind = Kind::MiniUart;
|
||||||
|
static volatile uint8_t* g_mmio = nullptr;
|
||||||
|
static uint32_t g_clockHz = 0;
|
||||||
|
static uint32_t g_baud = 0;
|
||||||
|
static bool g_flowControl = false;
|
||||||
|
static bool g_interrupt = false;
|
||||||
|
static bool g_initialized = false;
|
||||||
|
|
||||||
|
// Guards the hardware and the transmit ring: taken by the IRQ handler,
|
||||||
|
// Service() and every producer, with interrupts off.
|
||||||
|
static kcp::Spinlock g_hwLock;
|
||||||
|
|
||||||
|
// Receive ring: produced under g_hwLock, consumed by Read() (one reader).
|
||||||
|
static uint8_t g_rxRing[RX_RING_SIZE];
|
||||||
|
static std::atomic<uint32_t> g_rxHead{0};
|
||||||
|
static std::atomic<uint32_t> g_rxTail{0};
|
||||||
|
|
||||||
|
// Transmit ring: entirely under g_hwLock. g_txCount mirrors the fill for
|
||||||
|
// lock-free readers.
|
||||||
|
static uint8_t g_txRing[TX_RING_SIZE];
|
||||||
|
static uint32_t g_txHead = 0;
|
||||||
|
static uint32_t g_txTail = 0;
|
||||||
|
static std::atomic<uint32_t> g_txCount{0};
|
||||||
|
static bool g_txIrqEnabled = false;
|
||||||
|
|
||||||
|
static Stats g_stats = {};
|
||||||
|
|
||||||
|
static inline uint32_t Read32(uint32_t offset) {
|
||||||
|
return *(volatile uint32_t*)(g_mmio + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void Write32(uint32_t offset, uint32_t value) {
|
||||||
|
*(volatile uint32_t*)(g_mmio + offset) = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FIFO service (caller holds g_hwLock)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static inline void PushRx(uint8_t byte) {
|
||||||
|
uint32_t head = g_rxHead.load(std::memory_order_relaxed);
|
||||||
|
if (head - g_rxTail.load(std::memory_order_acquire) >= RX_RING_SIZE) {
|
||||||
|
g_stats.rxDropped++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_rxRing[head & (RX_RING_SIZE - 1)] = byte;
|
||||||
|
g_rxHead.store(head + 1, std::memory_order_release);
|
||||||
|
g_stats.rxBytes++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool DrainRxLocked() {
|
||||||
|
bool any = false;
|
||||||
|
if (g_kind == Kind::MiniUart) {
|
||||||
|
for (;;) {
|
||||||
|
uint32_t lsr = Read32(MU_LSR);
|
||||||
|
if (lsr & MU_LSR_OVERRUN) g_stats.overruns++;
|
||||||
|
if (!(lsr & MU_LSR_DATA_READY)) break;
|
||||||
|
PushRx((uint8_t)Read32(MU_IO));
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (!(Read32(PL_FR) & PL_FR_RXFE)) {
|
||||||
|
uint32_t data = Read32(PL_DR);
|
||||||
|
if (data & PL_DR_OE) g_stats.overruns++;
|
||||||
|
PushRx((uint8_t)data);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
Write32(PL_ICR, PL_INT_RX | PL_INT_RT | PL_INT_OE);
|
||||||
|
}
|
||||||
|
return any;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetTxIrqLocked(bool on) {
|
||||||
|
if (on == g_txIrqEnabled || !g_interrupt) return;
|
||||||
|
g_txIrqEnabled = on;
|
||||||
|
if (g_kind == Kind::MiniUart) {
|
||||||
|
Write32(MU_IER, MU_IER_RX | MU_IER_REQUIRED | (on ? MU_IER_TX : 0));
|
||||||
|
} else {
|
||||||
|
Write32(PL_IMSC, PL_INT_RX | PL_INT_RT | PL_INT_OE | (on ? PL_INT_TX : 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move queued bytes into the transmit FIFO. The transmit interrupt stays
|
||||||
|
// enabled exactly while bytes remain queued: the PL011's only fires on
|
||||||
|
// crossing its FIFO threshold, so the FIFO must be refilled here first.
|
||||||
|
static void FillTxLocked() {
|
||||||
|
while (g_txHead != g_txTail) {
|
||||||
|
bool space = (g_kind == Kind::MiniUart)
|
||||||
|
? (Read32(MU_LSR) & MU_LSR_TX_SPACE) != 0
|
||||||
|
: (Read32(PL_FR) & PL_FR_TXFF) == 0;
|
||||||
|
if (!space) break;
|
||||||
|
uint8_t byte = g_txRing[g_txTail & (TX_RING_SIZE - 1)];
|
||||||
|
g_txTail++;
|
||||||
|
Write32(g_kind == Kind::MiniUart ? MU_IO : PL_DR, byte);
|
||||||
|
g_stats.txBytes++;
|
||||||
|
}
|
||||||
|
g_txCount.store(g_txHead - g_txTail, std::memory_order_release);
|
||||||
|
if (g_kind == Kind::Pl011) Write32(PL_ICR, PL_INT_TX);
|
||||||
|
SetTxIrqLocked(g_txHead != g_txTail);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TransmitterIdleLocked() {
|
||||||
|
if (g_txHead != g_txTail) return false;
|
||||||
|
if (g_kind == Kind::MiniUart) return (Read32(MU_LSR) & MU_LSR_TX_IDLE) != 0;
|
||||||
|
uint32_t fr = Read32(PL_FR);
|
||||||
|
return (fr & PL_FR_TXFE) && !(fr & PL_FR_BUSY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void HandleInterrupt(uint8_t, bool fromUser) {
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
g_stats.interrupts++;
|
||||||
|
bool received = DrainRxLocked();
|
||||||
|
FillTxLocked();
|
||||||
|
g_hwLock.Release();
|
||||||
|
|
||||||
|
// Received bytes are parsed in process context. When this interrupt
|
||||||
|
// landed in user mode, ask for an idle-context pass at once (as the
|
||||||
|
// timer does for USB) instead of waiting for the core to go idle.
|
||||||
|
if (received && fromUser) Sched::RequestKernelWork();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Baud rate (caller holds g_hwLock, transmitter idle)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static bool WithinTolerance(uint64_t actual, uint32_t wanted) {
|
||||||
|
uint64_t diff = actual > wanted ? actual - wanted : wanted - actual;
|
||||||
|
return diff * 40 <= wanted; // 2.5%
|
||||||
|
}
|
||||||
|
|
||||||
|
// The divisor for `baud`: mini UART, baud = clock / (8 * divisor) (the
|
||||||
|
// register holds divisor - 1); PL011, clock / (16 * baud) in 16.6 fixed
|
||||||
|
// point (IBRD = divisor >> 6, FBRD = divisor & 63).
|
||||||
|
static bool ComputeDivisor(uint32_t baud, uint64_t& divisor) {
|
||||||
|
if (baud == 0 || g_clockHz == 0) return false;
|
||||||
|
if (g_kind == Kind::MiniUart) {
|
||||||
|
divisor = ((uint64_t)g_clockHz + 4ull * baud) / (8ull * baud);
|
||||||
|
if (divisor < 1 || divisor > 0x10000) return false;
|
||||||
|
return WithinTolerance((uint64_t)g_clockHz / (8 * divisor), baud);
|
||||||
|
}
|
||||||
|
divisor = ((uint64_t)g_clockHz * 8 / baud + 1) / 2;
|
||||||
|
if ((divisor >> 6) < 1 || (divisor >> 6) > 0xFFFF) return false;
|
||||||
|
return WithinTolerance((uint64_t)g_clockHz * 4 / divisor, baud);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ProgramBaudLocked(uint32_t baud) {
|
||||||
|
uint64_t divisor = 0;
|
||||||
|
if (!ComputeDivisor(baud, divisor)) return false;
|
||||||
|
|
||||||
|
if (g_kind == Kind::MiniUart) {
|
||||||
|
uint32_t cntl = Read32(MU_CNTL);
|
||||||
|
Write32(MU_CNTL, 0);
|
||||||
|
Write32(MU_BAUD, (uint32_t)(divisor - 1));
|
||||||
|
Write32(MU_CNTL, cntl);
|
||||||
|
} else {
|
||||||
|
uint32_t ibrd = (uint32_t)(divisor >> 6);
|
||||||
|
uint32_t fbrd = (uint32_t)(divisor & 63);
|
||||||
|
uint32_t cr = Read32(PL_CR);
|
||||||
|
Write32(PL_CR, 0);
|
||||||
|
for (int i = 0; i < 100000 && (Read32(PL_FR) & PL_FR_BUSY); i++) Arch::Pause();
|
||||||
|
Write32(PL_LCRH, 0); // flush the FIFOs
|
||||||
|
Write32(PL_IBRD, ibrd);
|
||||||
|
Write32(PL_FBRD, fbrd);
|
||||||
|
Write32(PL_LCRH, PL_LCRH_8BIT | PL_LCRH_FEN); // latches IBRD/FBRD
|
||||||
|
Write32(PL_CR, cr);
|
||||||
|
}
|
||||||
|
g_baud = baud;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Public API
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool Initialize(const Config& config) {
|
||||||
|
if (g_initialized) return true;
|
||||||
|
if (config.mmioPhys == 0 || config.clockHz == 0) return false;
|
||||||
|
|
||||||
|
g_kind = config.kind;
|
||||||
|
g_clockHz = config.clockHz;
|
||||||
|
|
||||||
|
// The mini UART lives 0x40 into the AUX block; accept either base.
|
||||||
|
uint64_t base = config.mmioPhys;
|
||||||
|
if (g_kind == Kind::MiniUart && (base & 0xFF) == MU_IO) base -= MU_IO;
|
||||||
|
uint64_t page = base & ~0xFFFULL;
|
||||||
|
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
|
||||||
|
g_mmio = (volatile uint8_t*)Memory::HHDM(base);
|
||||||
|
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
if (g_kind == Kind::MiniUart) {
|
||||||
|
// AUX_ENABLES also gates SPI1/SPI2; leave their bits alone.
|
||||||
|
Write32(AUX_ENABLES, Read32(AUX_ENABLES) | AUX_ENABLE_MU);
|
||||||
|
Write32(MU_CNTL, 0);
|
||||||
|
Write32(MU_IER, 0);
|
||||||
|
Write32(MU_LCR, MU_LCR_8BIT);
|
||||||
|
Write32(MU_MCR, MU_MCR_RTS);
|
||||||
|
Write32(MU_IIR, MU_IIR_FLUSH);
|
||||||
|
Write32(MU_CNTL, MU_CNTL_RXEN | MU_CNTL_TXEN);
|
||||||
|
} else {
|
||||||
|
Write32(PL_CR, 0);
|
||||||
|
Write32(PL_IMSC, 0);
|
||||||
|
Write32(PL_ICR, 0x7FF);
|
||||||
|
Write32(PL_IFLS, PL_IFLS_RX_HALF | PL_IFLS_TX_EIGHTH);
|
||||||
|
Write32(PL_LCRH, PL_LCRH_8BIT | PL_LCRH_FEN);
|
||||||
|
Write32(PL_CR, PL_CR_UARTEN | PL_CR_TXE | PL_CR_RXE | PL_CR_RTS);
|
||||||
|
}
|
||||||
|
bool baudOk = ProgramBaudLocked(115200);
|
||||||
|
while (DrainRxLocked()) {}
|
||||||
|
g_rxTail.store(g_rxHead.load(std::memory_order_relaxed), std::memory_order_release);
|
||||||
|
g_hwLock.Release();
|
||||||
|
|
||||||
|
if (!baudOk) {
|
||||||
|
KernelLogStream(ERROR, "BtUart") << "115200 baud unreachable from a "
|
||||||
|
<< (uint64_t)g_clockHz << " Hz clock";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.gsi != 0) {
|
||||||
|
uint8_t line = Hal::GsiRouting::LineForGsi(config.gsi, config.levelTriggered,
|
||||||
|
config.activeLow);
|
||||||
|
if (line != 0) {
|
||||||
|
Arch::RegisterIrqHandler(line, HandleInterrupt);
|
||||||
|
g_interrupt = true;
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
if (g_kind == Kind::MiniUart) {
|
||||||
|
Write32(MU_IER, MU_IER_RX | MU_IER_REQUIRED);
|
||||||
|
} else {
|
||||||
|
Write32(PL_IMSC, PL_INT_RX | PL_INT_RT | PL_INT_OE);
|
||||||
|
}
|
||||||
|
g_hwLock.Release();
|
||||||
|
Arch::EnableLegacyIrq(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_initialized = true;
|
||||||
|
KernelLogStream(OK, "BtUart") << (g_kind == Kind::MiniUart ? "Mini UART" : "PL011")
|
||||||
|
<< " at 0x" << base::hex << base << base::dec << ", " << (uint64_t)g_clockHz
|
||||||
|
<< " Hz clock, " << (g_interrupt ? "interrupt driven" : "polled (no interrupt)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsInitialized() { return g_initialized; }
|
||||||
|
bool HasInterrupt() { return g_interrupt; }
|
||||||
|
uint32_t GetBaud() { return g_baud; }
|
||||||
|
Kind GetKind() { return g_kind; }
|
||||||
|
bool FlowControl() { return g_flowControl; }
|
||||||
|
|
||||||
|
bool BaudReachable(uint32_t baud) {
|
||||||
|
uint64_t divisor = 0;
|
||||||
|
return g_initialized && ComputeDivisor(baud, divisor);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetBaud(uint32_t baud) {
|
||||||
|
if (!g_initialized) return false;
|
||||||
|
WaitTxIdle(1000);
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
bool ok = ProgramBaudLocked(baud);
|
||||||
|
g_hwLock.Release();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetFlowControl(bool on) {
|
||||||
|
if (!g_initialized) return;
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
if (g_kind == Kind::MiniUart) {
|
||||||
|
uint32_t cntl = MU_CNTL_RXEN | MU_CNTL_TXEN
|
||||||
|
| (on ? MU_CNTL_AUTORTS | MU_CNTL_RTS_LOW
|
||||||
|
| MU_CNTL_AUTOCTS | MU_CNTL_CTS_LOW : 0);
|
||||||
|
Write32(MU_CNTL, cntl);
|
||||||
|
// With auto RTS off, hold RTS asserted by hand.
|
||||||
|
Write32(MU_MCR, MU_MCR_RTS);
|
||||||
|
} else {
|
||||||
|
uint32_t cr = Read32(PL_CR) & ~(PL_CR_RTSEN | PL_CR_CTSEN);
|
||||||
|
if (on) cr |= PL_CR_RTSEN | PL_CR_CTSEN;
|
||||||
|
Write32(PL_CR, cr | PL_CR_RTS);
|
||||||
|
}
|
||||||
|
g_flowControl = on;
|
||||||
|
g_hwLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeassertRts() {
|
||||||
|
if (!g_initialized) return;
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
if (g_kind == Kind::MiniUart) {
|
||||||
|
Write32(MU_CNTL, MU_CNTL_RXEN | MU_CNTL_TXEN);
|
||||||
|
Write32(MU_MCR, 0);
|
||||||
|
} else {
|
||||||
|
Write32(PL_CR, Read32(PL_CR) & ~(PL_CR_RTSEN | PL_CR_CTSEN | PL_CR_RTS));
|
||||||
|
}
|
||||||
|
g_flowControl = false;
|
||||||
|
g_hwLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Read(uint8_t* buffer, int maxLength) {
|
||||||
|
if (buffer == nullptr || maxLength <= 0) return 0;
|
||||||
|
uint32_t tail = g_rxTail.load(std::memory_order_relaxed);
|
||||||
|
uint32_t head = g_rxHead.load(std::memory_order_acquire);
|
||||||
|
uint32_t available = head - tail;
|
||||||
|
uint32_t n = available < (uint32_t)maxLength ? available : (uint32_t)maxLength;
|
||||||
|
for (uint32_t i = 0; i < n; i++) {
|
||||||
|
buffer[i] = g_rxRing[(tail + i) & (RX_RING_SIZE - 1)];
|
||||||
|
}
|
||||||
|
g_rxTail.store(tail + n, std::memory_order_release);
|
||||||
|
return (int)n;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasRxData() {
|
||||||
|
if (!g_initialized) return false;
|
||||||
|
if (g_rxHead.load(std::memory_order_acquire) != g_rxTail.load(std::memory_order_relaxed))
|
||||||
|
return true;
|
||||||
|
// Polled: nothing fills the ring until Service() runs, so look at
|
||||||
|
// the FIFO itself.
|
||||||
|
if (g_interrupt) return false;
|
||||||
|
return g_kind == Kind::MiniUart ? (Read32(MU_LSR) & MU_LSR_DATA_READY) != 0
|
||||||
|
: (Read32(PL_FR) & PL_FR_RXFE) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlushRx() {
|
||||||
|
if (!g_initialized) return;
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
while (DrainRxLocked()) {}
|
||||||
|
g_rxTail.store(g_rxHead.load(std::memory_order_relaxed), std::memory_order_release);
|
||||||
|
g_hwLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Write(const uint8_t* head, uint32_t headLength,
|
||||||
|
const uint8_t* body, uint32_t bodyLength) {
|
||||||
|
if (body == nullptr) bodyLength = 0;
|
||||||
|
uint32_t length = headLength + bodyLength;
|
||||||
|
if (!g_initialized || length == 0) return length == 0;
|
||||||
|
if (length > TX_RING_SIZE) return false;
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
if (TX_RING_SIZE - (g_txHead - g_txTail) < length) {
|
||||||
|
g_hwLock.Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < headLength; i++) {
|
||||||
|
g_txRing[(g_txHead + i) & (TX_RING_SIZE - 1)] = head[i];
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < bodyLength; i++) {
|
||||||
|
g_txRing[(g_txHead + headLength + i) & (TX_RING_SIZE - 1)] = body[i];
|
||||||
|
}
|
||||||
|
g_txHead += length;
|
||||||
|
FillTxLocked();
|
||||||
|
g_hwLock.Release();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t TxFree() {
|
||||||
|
return TX_RING_SIZE - g_txCount.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t TxPending() {
|
||||||
|
return g_txCount.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WaitTxIdle(uint32_t timeoutMs) {
|
||||||
|
if (!g_initialized) return false;
|
||||||
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
|
for (;;) {
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
FillTxLocked();
|
||||||
|
bool idle = TransmitterIdleLocked();
|
||||||
|
g_hwLock.Release();
|
||||||
|
if (idle) return true;
|
||||||
|
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
|
||||||
|
Arch::Pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Service() {
|
||||||
|
if (!g_initialized) return;
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
DrainRxLocked();
|
||||||
|
FillTxLocked();
|
||||||
|
g_hwLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetStats(Stats& out) {
|
||||||
|
g_hwLock.Acquire();
|
||||||
|
out = g_stats;
|
||||||
|
g_hwLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* BtUart.hpp
|
||||||
|
* Interrupt-driven UART to an on-board Bluetooth controller
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// A byte pipe to a peripheral wired to one of the Broadcom SoC's UARTs --
|
||||||
|
// on the Raspberry Pi 4/400 the Bluetooth half of the CYW4345x radio.
|
||||||
|
// Unlike the console (Uart.cpp: polled, transmit only) this one receives,
|
||||||
|
// so it is interrupt driven: the hard IRQ moves bytes between the FIFOs and
|
||||||
|
// two software rings, and everything else runs in process context.
|
||||||
|
//
|
||||||
|
// Two UARTs can carry it:
|
||||||
|
// - the mini UART (BCM2835 AUX, 8-byte FIFOs, baud from the VPU core
|
||||||
|
// clock), which is where Bluetooth sits with the miniuart-bt overlay
|
||||||
|
// the Pi firmware task force's UEFI ships;
|
||||||
|
// - the PL011 (32-byte FIFOs, fixed 48 MHz reference clock).
|
||||||
|
// Pin routing (GPIO 30-33) is the caller's job.
|
||||||
|
|
||||||
|
namespace Drivers::Serial::BtUart {
|
||||||
|
|
||||||
|
enum class Kind : uint8_t {
|
||||||
|
MiniUart,
|
||||||
|
Pl011,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
Kind kind;
|
||||||
|
uint64_t mmioPhys; // the block ACPI describes (mini UART: the AUX block)
|
||||||
|
uint32_t clockHz; // UART reference clock
|
||||||
|
uint32_t gsi; // 0 = no interrupt, poll from Service()
|
||||||
|
bool levelTriggered;
|
||||||
|
bool activeLow;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Program the UART to 115200 8N1 with RTS asserted and start receiving.
|
||||||
|
bool Initialize(const Config& config);
|
||||||
|
bool IsInitialized();
|
||||||
|
|
||||||
|
// Whether the receive path is interrupt driven. Without it, bytes only
|
||||||
|
// move when Service() runs, so a caller should keep the baud rate low.
|
||||||
|
bool HasInterrupt();
|
||||||
|
|
||||||
|
// Change the baud rate. Waits for queued output to drain first, since
|
||||||
|
// the peer is about to switch too. Returns false if the rate is
|
||||||
|
// unreachable from the reference clock (more than 2.5% off).
|
||||||
|
bool SetBaud(uint32_t baud);
|
||||||
|
bool BaudReachable(uint32_t baud);
|
||||||
|
uint32_t GetBaud();
|
||||||
|
Kind GetKind();
|
||||||
|
|
||||||
|
// Hardware flow control: deassert RTS while the receive FIFO is nearly
|
||||||
|
// full so the peer pauses instead of overrunning it, and hold our
|
||||||
|
// transmitter while the peer deasserts CTS.
|
||||||
|
void SetFlowControl(bool on);
|
||||||
|
bool FlowControl();
|
||||||
|
|
||||||
|
// Drive RTS deasserted (and stop auto RTS) until the next
|
||||||
|
// SetFlowControl. The peer may sample it at power-on: a Broadcom
|
||||||
|
// controller that sees its CTS asserted then boots into autobaud mode.
|
||||||
|
void DeassertRts();
|
||||||
|
|
||||||
|
// Receive ring (single consumer). Read returns the number of bytes copied.
|
||||||
|
int Read(uint8_t* buffer, int maxLength);
|
||||||
|
bool HasRxData();
|
||||||
|
void FlushRx();
|
||||||
|
|
||||||
|
// Queue bytes for transmission: all of them, or none (false) when the
|
||||||
|
// transmit ring lacks room. Safe from several producers at once; each
|
||||||
|
// call's bytes (head, then body) stay contiguous on the wire.
|
||||||
|
bool Write(const uint8_t* head, uint32_t headLength,
|
||||||
|
const uint8_t* body = nullptr, uint32_t bodyLength = 0);
|
||||||
|
uint32_t TxFree();
|
||||||
|
uint32_t TxPending();
|
||||||
|
|
||||||
|
// Wait until everything queued has left the transmitter.
|
||||||
|
bool WaitTxIdle(uint32_t timeoutMs);
|
||||||
|
|
||||||
|
// Move bytes between the FIFOs and the rings from process context: the
|
||||||
|
// whole receive path when there is no interrupt, and a safety net when
|
||||||
|
// there is one.
|
||||||
|
void Service();
|
||||||
|
|
||||||
|
struct Stats {
|
||||||
|
uint64_t rxBytes;
|
||||||
|
uint64_t txBytes;
|
||||||
|
uint64_t rxDropped; // receive ring full
|
||||||
|
uint64_t overruns; // receive FIFO overflowed in hardware
|
||||||
|
uint64_t interrupts;
|
||||||
|
};
|
||||||
|
void GetStats(Stats& out);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
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.
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.
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Executable
+1739
File diff suppressed because it is too large.
Load diff
Executable
+99
@@ -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()
|
||||||
@@ -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(ð[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;
|
||||||
|
}
|
||||||
@@ -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
Reference in new issue
Block a user