Skip to content

About

FreeBSD 15 KLDs for RPi 5 board thermal, fan, ethernet, WiFi+BT?

Topics

Resources

Stars

5 stars

Watchers

5 watching

Forks

Latest commit

 

History

202 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Raspberry Pi 5 FreeBSD Kernel Modules

A set of FreeBSD kernel modules for the Raspberry Pi 5 (BCM2712 SoC + RP1 peripheral chip). Targets FreeBSD 16-CURRENT on arm64.

Modules

The repository builds seven loadable kernel modules (.ko), all built by the default make target.

Drivers, interfaces, and dependencies

.ko module Source(s) Driver registration (parent bus) Kernel interfaces implemented Sysctl tree MODULE_DEPEND Built by default
bcm2712 bcm2712.c none (event-driven MOD_LOAD) exports C API: bcm2712_read_cpu_temp, bcm2712_get_softc, bcm2712_pwm_set_config, bcm2712_pwm_enable, bcm2712_read_fan_rpm hw.bcm2712.* — ✓
rpi5 rpi5.c none (event-driven MOD_LOAD) sysctl-only consumer of bcm2712 exports hw.rpi5.fan.* bcm2712 ✓
rp1_gpio rp1_gpio.c nexus (+ child gpiobus) device_if, bus_if, gpio_if(9), fdt_pinctrl_if, ofw_bus_if (via gpioctl(8)) gpiobus ✓
rp1_eth rp1_eth_cfg.c + rp1_eth.c none (event-driven MOD_LOAD) device_if, bus_if (cfg sysctls); full if_t net driver — rp1eth0 hw.rp1_eth.* bcm2712_pcie ✓
bcm2712_pcie bcm2712_pcie.c acpi device_if (ACPI IRQ shim — routes shared GIC SPI 229 to rp1_eth) — acpi ✓
rp1_pcie2_recon rp1_pcie2_recon.c none (event-driven MOD_LOAD) reconnaissance dump of BCM2712 PCIe2 host-controller state to dmesg + sysctl hw.rp1_pcie2_recon.* — ✓
cyw43455 cyw43455.c + cyw43455_cfg.c, cyw43455_fwil.c, cyw43455_security.c sdiob device_if, net80211 ieee80211_* op table; creates wlanN over parent cyw0 hw.cyw43455.* sdiob, wlan ✓

Dependency graph

            ┌───────────────────────────────────────────────────┐
            │                FreeBSD kernel base                │
            │      acpi   nexus   gpiobus   sdiob   wlan         │
            └───┬───────────┬────────┬─────────┬─────────┬──────┘
                │           │        │         │         │
 bcm2712_pcie         rp1_gpio       │       cyw43455    │
   (acpi)            (nexus +        │       (sdiob +    │
                      gpiobus child) │        wlan)      │
       │                             │                   │
       │   ┌────────────── bcm2712 ◄─┴──── rpi5          │
       │   │                  ▲                          │
       └───┴──────────────────────── rp1_eth             │
                                  (depends only on bcm2712_pcie)

 rp1_pcie2_recon: standalone, no MODULE_DEPEND

Practical loading consequences:

  • kldload rpi5 auto-pulls bcm2712.
  • kldload rp1_eth auto-pulls bcm2712_pcie (which in turn pulls acpi if not already loaded).
  • kldload rp1_gpio auto-pulls gpiobus (and gpioc once a gpiobus child attaches).
  • kldload cyw43455 attaches on the sdiob bus and auto-pulls wlan; it creates the net80211 parent cyw0 from which wlanN is cloned (ifconfig wlan0 create wlandev cyw0).
  • bcm2712_pcie and rp1_pcie2_recon have no module-level dependencies and load standalone.

Note: bcm2712 and rpi5 deliberately avoid the FreeBSD device framework (no DRIVER_MODULE); they attach to nothing and run entirely from MOD_LOAD/MOD_UNLOAD event handlers. This is why they expose a C-symbol API (callable from rpi5 and rp1_eth) rather than a device_if method table.

Building

Prerequisites:

  • FreeBSD kernel sources at /usr/src/sys (the build pulls interface headers like device_if.h, bus_if.h, pwmbus_if.h, acpi_if.h directly from system .m files).
  • A working make and toolchain (base FreeBSD).
make                          # build all modules
make bcm2712                  # build single module
make rpi5
make rp1_gpio
make rp1_eth                  # builds rp1_eth_cfg.c + rp1_eth.c → rp1_eth.ko
make bcm2712_pcie
make cyw43455                 # SDIO WiFi FullMAC driver → cyw43455.ko

sudo make install             # install all .ko files into /boot/modules
sudo make install-rpi5        # individual installs also available
sudo make install-rp1_gpio

BUILDING.md has the full reference for build flags and per-module Makefiles.

Loading

sudo make load                    # all modules
sudo kldload rpi5                 # auto-loads bcm2712
sudo kldload /boot/modules/rp1_gpio.ko   # GPIO / pinctrl controller

make status                       # show kldstat + sysctl summary
sudo make unload                  # rpi5 first, then bcm2712

Note: rp1_gpio is installed to /boot/modules/ (not /boot/kernel/), so use the full path with kldload.

To auto-load at boot, add to /boot/loader.conf:

rpi5_load="YES"           # auto-loads bcm2712
rp1_gpio_load="YES"
bcm2712_pcie_load="YES"   # ACPI IRQ shim required by rp1_eth
rp1_eth_load="YES"        # auto-loads bcm2712_pcie; creates rp1eth0
cyw43455_load="YES"       # auto-loads wlan; SDIO WiFi (cyw0)

Firmware note: cyw43455 reads its firmware, NVRAM, and CLM blob from /boot/firmware/cyw43455/ at attach time (make install-cyw43455 places them there). Without those files the SDIO bring-up fails.

Sysctl Interface

hw.bcm2712.* — common BCM2712 hardware

OID Type Description
hw.bcm2712.debug RW int Verbose debug logging (0 = off, 1 = on)
hw.bcm2712.thermal.cpu_temp RD int CPU temperature — stored as deciKelvin, displayed by sysctl(8) as Celsius (e.g. 47.9C)

hw.rpi5.fan.* — cooling-fan thermal management

OID Type Default Description
hw.rpi5.fan.temp0 RW uint 50000 Level 1 trigger threshold (milli-°C = 50.000 °C)
hw.rpi5.fan.temp1 RW uint 60000 Level 2 trigger threshold
hw.rpi5.fan.temp2 RW uint 67500 Level 3 trigger threshold
hw.rpi5.fan.temp3 RW uint 75000 Level 4 trigger threshold
hw.rpi5.fan.temp{0..3}_hyst RW uint 5000 Per-level hysteresis (milli-°C)
hw.rpi5.fan.speed0 RW uint 75 Level 1 PWM duty (0–255 → 0–100 %)
hw.rpi5.fan.speed1 RW uint 125 Level 2 PWM duty
hw.rpi5.fan.speed2 RW uint 175 Level 3 PWM duty
hw.rpi5.fan.speed3 RW uint 250 Level 4 PWM duty
hw.rpi5.fan.cpu_temp RD uint — Latest sampled CPU temperature (milli-°C)
hw.rpi5.fan.current_state RD uint — Active fan level (0–4)
hw.rpi5.fan.rpm RD uint — RP1 PWM1 offset 0x3C (CHAN2_PHASE); firmware-preloaded static value — not live fan RPM

The rpi5 module polls cpu_temp once per second (1 Hz callout) and selects a level using the thresholds and per-level hysteresis. The selected PWM duty is driven on GPIO45 via RP1 PWM1 channel 3 (inverted polarity: duty=0 = fan off).

Level Fan state when cpu_temp ≥ PWM duty (0–255) Approx. duty
0 idle (< temp0, 50 °C) speed0 (75) 29 %
1 temp0 (50 °C) speed0 (75) 29 %
2 temp1 (60 °C) speed1 (125) 49 %
3 temp2 (67.5 °C) speed2 (175) 69 %
4 temp3 (75 °C) speed3 (250) 98 %

The fan runs at the level-0 idle speed (PWM speed0 = 75) even below the temp0 threshold. Use temp0 / speed0 to set the baseline idle duty.

Note on hw.rpi5.fan.rpm: The RP1 datasheet names offset 0x3C CHAN2_PHASE (channel-2 counter phase-offset preload). Hardware testing on RPi 5 confirmed it reads ~10169 regardless of PWM duty cycle, channel enable state, or fan speed; CHAN2 is not enabled in GLOBAL_CTRL. The value is firmware-preloaded static data, not a live tachometer reading. It is exposed read-only for diagnostic inspection.

Testing Fan Threshold Control

The easiest way to verify transitions is to lower a threshold below the current CPU temperature, wait one poll interval (≤ 2 s), and read current_state:

# Read current CPU temp
sysctl hw.rpi5.fan.cpu_temp       # e.g. 47400 = 47.4 °C

# Force state 1 (low speed): lower temp0 below current temp
sudo sysctl hw.rpi5.fan.temp0=45000
sleep 2
sysctl hw.rpi5.fan.current_state  # expect 1

# Force state 2 (medium speed)
sudo sysctl hw.rpi5.fan.temp1=44000
sleep 2
sysctl hw.rpi5.fan.current_state  # expect 2

# Force state 3 (high speed)
sudo sysctl hw.rpi5.fan.temp2=46000
sleep 2
sysctl hw.rpi5.fan.current_state  # expect 3

# Restore defaults
sudo sysctl hw.rpi5.fan.temp0=50000 hw.rpi5.fan.temp1=60000 hw.rpi5.fan.temp2=67500
sleep 2
sysctl hw.rpi5.fan.current_state  # returns to 0 once CPU cools below 50 °C

Fan audibly responds within one thermal poll cycle. The rpm register will not change — it holds the firmware-preloaded static value.

Persist changes in /etc/sysctl.conf, e.g.:

hw.rpi5.fan.temp0=45000
hw.rpi5.fan.speed0=90

hw.rp1_eth.* — Ethernet driver (M3 complete — rp1eth0 production NIC)

Subtree Contents
hw.rp1_eth.mac_addr.* OTP-derived MAC address (string + raw uints)
hw.rp1_eth.cfg.* eth_cfg PHY-glue register window (status, GPIO state, RD-only)
hw.rp1_eth.gem.* GEM core register snapshot (diagnostic, RD-only)
hw.rp1_eth.mac_drv.rxbufs RW int — RX descriptor ring size
hw.rp1_eth.mac_drv._rxoverruns, _rxnobufs, _rxdmamapfails, _txfull, _txdmamapfails, _txdefrags RD counters — driver-side error tallies
hw.rp1_eth.mac_drv.stats.tx_{bytes,frames,under_runs} RD — GEM hardware TX counters
hw.rp1_eth.mac_drv.stats.rx_{bytes,frames,frames_fcs_errs,overrun_errs,…} RD — GEM hardware RX counters

The driver also creates an if_t named rp1eth0 that participates normally in ifconfig(8), the routing table, BPF, and netgraph.

hw.cyw43455.* — CYW43455 SDIO WiFi

OID Type Description
hw.cyw43455.chip_id RD u16 SDIO chip ID (expect 0x4345)
hw.cyw43455.chip_rev RD u8 Chip revision (expect 6)
hw.cyw43455.firmware_version RD string Firmware build string (e.g. 7.45.265 …)
hw.cyw43455.rx_ok_count RD u64 Successful F2 (CMD53) reads since attach
hw.cyw43455.rx_eio_count RD u64 F2 reads that returned EIO
hw.cyw43455.rx_eagain_count RD u64 F2 reads bounced by gate or header check
hw.cyw43455.rx_last_ok_ticks RD int ticks of last successful F2 read
hw.cyw43455.rx_last_eio_ticks RD int ticks of last F2 EIO
hw.cyw43455.rx_data_frames RD u64 SDPCM channel-2 frames delivered to net80211
hw.cyw43455.rx_data_bytes RD u64 Total bytes delivered on channel 2
hw.cyw43455.rx_eapol_frames RD u64 EAPOL frames (EtherType 0x888E) delivered up
hw.cyw43455.tx_data_frames RD u64 Frames handed to cyw_transmit
hw.cyw43455.tx_eapol_frames RD u64 TX subset with EtherType 0x888E
hw.cyw43455.tx_eapol_bytes RD u64 TX EAPOL byte total
hw.cyw43455.tx_hdr_debug RW int Dump SDPCM/BDC headers for each TX data frame when nonzero
hw.cyw43455.fw_wsec RD int Live wsec iovar GET (security mode; 4 = AES_CCM)
hw.cyw43455.fw_wpa_auth RD int Live wpa_auth iovar GET (128 = WPA2-PSK)
hw.cyw43455.fw_auth RD int Live auth iovar GET (0 = Open System)

The loader tunable hw.cyw43455.probe_fwsup=1 (set via /boot/loader.conf or kenv before kldload) runs a one-shot read-only FWSUP-capability probe at first WLC_UP; see doc/cyw43455.md §16.8.

WiFi (cyw43455) — WPA2-PSK status

cyw43455 is a FullMAC SDIO driver for the Cypress/Infineon CYW43455 (chip 0x4345 rev 6, firmware 7.45.265). The firmware does not implement an internal supplicant (sup_wpa returns BCME_UNSUPPORTED), so WPA2-PSK runs in host-supplicant mode: wpa_supplicant on the host owns the 4-way handshake, and the driver installs the resulting PTK/GTK via the wsec_key iovar.

Working today (5 GHz and 2.4 GHz): WPA2-PSK association → wpa_state=COMPLETED, CCMP pairwise/group, and bidirectional unicast + multicast data flow verified on a clean boot. TX SDPCM/BDC framing and PTK/GTK key install are proven byte-for-byte against Linux brcmfmac. 2.4 GHz association succeeds against the correct BSS; the firmware retries an initial E_AUTH status=2 (TIMEOUT) and clears it on its own. See doc/cyw43455.md §16.9 (5 GHz resolution) and §16.10 (2.4 GHz) for the test results.

BSS-selection note: the driver joins whatever BSS net80211 selects (vap->iv_bss->ni_bssid); it does not independently lock a BSSID. A bssid= in wpa_supplicant.conf must belong to a BSS whose beaconed SSID matches ssid=, or net80211 may select a different BSS (e.g. a same-SSID AP on the other band).

sudo kldload cyw43455
sudo ifconfig wlan0 create wlandev cyw0
sudo ifconfig wlan0 up
sudo wpa_supplicant -B -i wlan0 -c /path/to/wpa_supplicant.conf

Reload caution: rapid kldunload/kldload cycling can leave the SDIO core in a dirty state (unexpected chip ID 0xffff → SDIO attach failed: 6). Allow a few seconds to settle, or prefer a cold boot, before re-testing.

GPIO (rp1_gpio) — Milestone 1

The rp1_gpio module attaches the RP1's GPIO / Pinctrl controller to the FreeBSD gpio_if(9) and gpiobus(4) framework. It locates the hardware via an FDT walk (no ACPI node required) and maps the three register banks via pmap_mapdev_attr.

rp1_gpio0: <RP1 GPIO / Pinctrl Controller>
rp1_gpio0: IO_BANK@... RIO@... PADS@... (IRQ chain: deferred to M4)
gpiobus0: <GPIO bus> on rp1_gpio0
gpioc0: <GPIO controller> at pins 0-53 on gpiobus0

After loading, use gpioctl(8) with the /dev/gpioc0 device:

gpioctl -lv /dev/gpioc0          # list all 54 pins with names and levels
gpioctl -f /dev/gpioc0 45        # read GPIO 45 level (0 or 1)
gpioctl -f /dev/gpioc0 45 1      # drive GPIO 45 high

Pin 45 maps to bank 2, index 11 (FAN_PWM on the standard Pi 5 board — drives the fan PWM control line via PWM1 channel 3 in ALT0 mode). Pin 29 is FAN_TACH (tachometer input, not yet wired to an interrupt counter). Symbolic names are populated from the FDT gpio-line-names property.

M1 does not expose a sysctl tree or handle interrupts. Those are scheduled for M2 (pinctrl function-select) and M4 (per-pin edge/level IRQs).

RP1 Ethernet (rp1_eth) — Milestones

rp1_eth is a fork of sys/dev/cadence/if_cgem.c adapted for the Pi 5 GEM_GXL MAC behind the RP1 PCIe2 outbound window. Development followed three milestones; all three are now complete and rp1eth0 is the production Ethernet interface.

  • M1 — rp1_eth_cfg.c (complete): FDT walk, map eth_cfg (PHY clock-mux / reset glue), drive PHY reset GPIO, expose hw.rp1_eth.cfg.* sysctls and a link-state observation tick. No network attach.
  • M2 — polled mode (complete): forks if_cgem, attaches rp1eth0 to the network stack, RX/TX driven by a 1 Hz link-poll callout; interrupt infrastructure wired.
  • M3 — interrupt-driven (complete): GIC SPI 229 (shared with xhci0/1) is routed through a small ACPI shim (bcm2712_pcie) that filter-checks CGEM_INT_STATUS and forwards to cgem_intr_filter. The filter defers RX/TX work to a taskqueue_fast swi task.

Interrupt architecture and gem_poll fallback

GIC SPI 229 is permanently asserted by USB host-controller activity (xhci0/1), which prevents the RP1 MSIx IACK mechanism from generating per-packet MSI edges for the GEM vector (RP1_INT_ETH = 6). Empirical verification: SPI 229 delta of 88 counts for 20 pings = pure USB background rate (~4.4/s); zero GEM-originated edges.

The M3 interrupt path therefore operates as follows:

  1. When a real SPI 229 edge coincidentally arrives (USB poll), bcm2712_pcie_filter reads CGEM_INT_STATUS; if GEM bits are set it calls cgem_intr_filter, which masks all GEM interrupts, stores intr_pending, enqueues cgem_intr_task, and cancels gem_poll.
  2. After cgem_intr_task finishes processing the batch (RX deliver + TX reclaim), it re-enables GEM interrupt sources and arms a 5 ms gem_poll callout (callout_init(CALLOUT_MPSAFE)).
  3. gem_poll polls CGEM_INTR_STAT directly at interrupt priority with sc_mtx held. If GEM bits are set it masks, sets intr_pending, and re-enqueues the task (which re-arms gem_poll on exit). If nothing is pending it re-arms itself for the next 5 ms window.

The 5 ms polling interval delivers 1.6 ms min / 5.4 ms avg / 11 ms max RTT to a local gateway — a 160× improvement over the USB-poll-rate baseline (499 ms min / 1623 ms avg / 2814 ms max) measured before M3.

bcm2712_pcie KPI

The shim publishes a C API (bcm2712_pcie.h) used by rp1_eth:

/* Register/deregister rp1_eth's GEM interrupt filter. Safe to call
   before or after bcm2712_pcie0 attaches (module-level storage). */
void bcm2712_pcie_register_rp1_intr(driver_filter_t *filter, void *arg);
void bcm2712_pcie_deregister_rp1_intr(void);

/* Write MSIx IACK=1 for GEM vector 6 (exported; currently unused by
   rp1_eth — IACK cannot generate per-packet edges on this platform). */
void bcm2712_pcie_gem_iack(void);

filter / arg are stored as volatile uintptr_t and updated via atomic_store_rel_ptr / atomic_load_acq_ptr so the filter never needs a spin lock — required because FreeBSD arm64 interrupt dispatch nests filter handlers inside an implicit critical section.

ACPI / DSDT requirement

bcm2712_pcie matches an ACPI _HID of "BCM2712" placed inside the RP1B scope. The default Pi 5 DSDT does not declare it; an override (/boot/acpi_dsdt.aml) supplies the device with two MMIO resources (GEM MAC + eth_cfg) and the shared interrupt (GSI 261 / GIC SPI 229). Without the override bcm2712_pcie will not attach and rp1_eth will not receive interrupts (link and traffic still function via gem_poll).

All development has been performed on RPi5 firmware configured to provide BOTH ACPI and full DeviceTree support.

Diagnostic Tools

Repeatable diagnostics live in tools/ as shell scripts so each one can be granted execute permission once and reused:

# rp1_gpio
sh tools/rp1_gpio_dump.sh 45        # dump CTRL/STATUS/PADS regs for GPIO 45 (FAN_PWM)

# rp1_eth
sh tools/rp1_eth_status.sh           # snapshot of module state + cfg regs
sh tools/rp1_eth_link_watch.sh       # live link-state monitor (Ctrl-C to stop)
sudo sh tools/rp1_eth_fdt_dump.sh    # dump FDT ethernet/phy/gpio nodes
sudo sh tools/rp1_eth_load.sh        # build + install + load + status
sudo sh tools/rp1_eth_load.sh --reload   # unload + reload without rebuild
sudo sh tools/rp1_eth_load.sh --unload   # unload only

The fan-control wrapper script:

chmod +x rpi5_fan_control_integrated.sh
./rpi5_fan_control_integrated.sh --check          # module status
./rpi5_fan_control_integrated.sh --diagnostics    # full diagnostics
./rpi5_fan_control_integrated.sh --monitor        # real-time temperature/fan
./rpi5_fan_control_integrated.sh --test           # PWM hardware exercise

Device Tree Requirements

rp1_gpio and rp1_eth_cfg locate their hardware by walking the FDT exported via /dev/openfirm; no matching ACPI node is required. The system boots with ACPI but the Pi 5 firmware also publishes a full FDT, which these drivers consume directly.

The PWM channel used by the fan controller requires:

&RP1 {
    pwm0: pwm@e0000 {
        compatible = "raspberrypi,rp1-pwm";
        reg = <0xe0000 0x2800>;
        #pwm-cells = <2>;
        status = "okay";
    };
};

rp1_eth_cfg walks /soc/... for cdns,pi5-gem (or compatible) and its associated PHY / eth_cfg glue. M3 additionally requires the ACPI DSDT override described above.

Files of Interest

  • bcm2712.c, bcm2712_var.h — BCM2712 thermal + RP1 PWM (pwmbus)
  • rpi5.c — Pi 5 fan controller, hw.rpi5.fan.* sysctls
  • rp1_gpio.c, rp1_gpio_var.h — RP1 GPIO / Pinctrl M1
  • rp1_eth.c, rp1_eth_cfg.c, rp1_eth_var.h, rp1_eth_hw.h
  • bcm2712_pcie.c, bcm2712_pcie.h — M3 interrupt router
  • cyw43455.c, cyw43455_var.h — CYW43455 attach, sysctls, SDIO probe
  • cyw43455_{sdio,sdpcm,fwil,fw,cfg,scan,events,security}.c — SDIO transport, SDPCM/BCDC framing, firmware IOVAR layer, firmware load, net80211 glue, scanning, event handling, and WPA2 key install
  • doc/cyw43455.md — WiFi bring-up log; §16.8 FWSUP probe, §16.9 WPA2 resolution
  • Makefile — single consolidated build for all modules
  • doc/INTEGRATION_GUIDE.md — architecture and integration details
  • BUILDING.md — detailed build/install reference
  • tools/ — diagnostic shell scripts (see above)

License

BSD 2-Clause (SPDX-License-Identifier: BSD-2-Clause).

About

FreeBSD 15 KLDs for RPi 5 board thermal, fan, ethernet, WiFi+BT?

Topics

Resources

Stars

5 stars

Watchers

5 watching

Forks

Releases

Contributors

Languages