A set of FreeBSD kernel modules for the Raspberry Pi 5 (BCM2712 SoC + RP1 peripheral chip). Targets FreeBSD 16-CURRENT on arm64.
The repository builds seven loadable kernel modules (.ko), all built by
the default make target.
.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 |
✓ |
┌───────────────────────────────────────────────────┐
│ 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 rpi5auto-pullsbcm2712.kldload rp1_ethauto-pullsbcm2712_pcie(which in turn pullsacpiif not already loaded).kldload rp1_gpioauto-pullsgpiobus(andgpioconce agpiobuschild attaches).kldload cyw43455attaches on thesdiobbus and auto-pullswlan; it creates the net80211 parentcyw0from whichwlanNis cloned (ifconfig wlan0 create wlandev cyw0).bcm2712_pcieandrp1_pcie2_reconhave no module-level dependencies and load standalone.
Note:
bcm2712andrpi5deliberately avoid the FreeBSD device framework (noDRIVER_MODULE); they attach to nothing and run entirely fromMOD_LOAD/MOD_UNLOADevent handlers. This is why they expose a C-symbol API (callable fromrpi5andrp1_eth) rather than adevice_ifmethod table.
Prerequisites:
- FreeBSD kernel sources at
/usr/src/sys(the build pulls interface headers likedevice_if.h,bus_if.h,pwmbus_if.h,acpi_if.hdirectly from system.mfiles). - A working
makeand 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_gpioBUILDING.md has the full reference for build flags and per-module
Makefiles.
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 bcm2712Note:
rp1_gpiois installed to/boot/modules/(not/boot/kernel/), so use the full path withkldload.
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:
cyw43455reads its firmware, NVRAM, and CLM blob from/boot/firmware/cyw43455/at attach time (make install-cyw43455places them there). Without those files the SDIO bring-up fails.
| 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) |
| 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 0x3CCHAN2_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.
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 °CFan 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
| 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.
| 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.
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. Abssid=inwpa_supplicant.confmust belong to a BSS whose beaconed SSID matchesssid=, 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.confReload caution: rapid
kldunload/kldloadcycling 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.
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 highPin 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_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, mapeth_cfg(PHY clock-mux / reset glue), drive PHY reset GPIO, exposehw.rp1_eth.cfg.*sysctls and a link-state observation tick. No network attach. - M2 — polled mode (complete): forks
if_cgem, attachesrp1eth0to 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-checksCGEM_INT_STATUSand forwards tocgem_intr_filter. The filter defers RX/TX work to ataskqueue_fastswi task.
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:
- When a real SPI 229 edge coincidentally arrives (USB poll),
bcm2712_pcie_filterreadsCGEM_INT_STATUS; if GEM bits are set it callscgem_intr_filter, which masks all GEM interrupts, storesintr_pending, enqueuescgem_intr_task, and cancelsgem_poll. - After
cgem_intr_taskfinishes processing the batch (RX deliver + TX reclaim), it re-enables GEM interrupt sources and arms a 5 msgem_pollcallout (callout_init(CALLOUT_MPSAFE)). gem_pollpollsCGEM_INTR_STATdirectly at interrupt priority withsc_mtxheld. If GEM bits are set it masks, setsintr_pending, and re-enqueues the task (which re-armsgem_pollon 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.
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.
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.
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 onlyThe 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 exerciserp1_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.
bcm2712.c,bcm2712_var.h— BCM2712 thermal + RP1 PWM (pwmbus)rpi5.c— Pi 5 fan controller,hw.rpi5.fan.*sysctlsrp1_gpio.c,rp1_gpio_var.h— RP1 GPIO / Pinctrl M1rp1_eth.c,rp1_eth_cfg.c,rp1_eth_var.h,rp1_eth_hw.hbcm2712_pcie.c,bcm2712_pcie.h— M3 interrupt routercyw43455.c,cyw43455_var.h— CYW43455 attach, sysctls, SDIO probecyw43455_{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 installdoc/cyw43455.md— WiFi bring-up log; §16.8 FWSUP probe, §16.9 WPA2 resolutionMakefile— single consolidated build for all modulesdoc/INTEGRATION_GUIDE.md— architecture and integration detailsBUILDING.md— detailed build/install referencetools/— diagnostic shell scripts (see above)
BSD 2-Clause (SPDX-License-Identifier: BSD-2-Clause).