Skip to content

rp23xx: add CYW43439 wifi driver and Pimoroni Pico Plus 2 W board#19533

Open
casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:rp23xx-cyw43439-pimoroni-pico-plus-2-w
Open

rp23xx: add CYW43439 wifi driver and Pimoroni Pico Plus 2 W board#19533
casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:rp23xx-cyw43439-pimoroni-pico-plus-2-w

Conversation

@casaroli

@casaroli casaroli commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

RP2350 had no wireless support, although the bcmf FullMAC driver and its gSPI
bus binding have been in tree since the Raspberry Pi Pico W port. This adds the
missing transport for rp23xx and a board that uses it.

arch/arm/rp23xx: Add gSPI driver for the Infineon CYW43439. Provides the
gspi_dev_t operations that bcmf_gspi_initialize() expects, driving the
chip's half-duplex gSPI bus with a six-instruction PIO program and DMA in both
directions. It is a port of rp2040_cyw43439.c; three differences in the RP2350
hardware and its NuttX support had to be handled:

  • The GPIO_STATUS register no longer has an OEFROMPERI field, so the wait
    for the PIO program to release the shared data line watches OETOPAD. No
    override is configured on that pad, so the two fields carry the same value.
  • rp23xx GPIO interrupt modes are enumerated as indices rather than as the
    bitmask rp2040 uses, so passing the rp2040 constant would have selected the
    wrong trigger.
  • RP23XX_PIO_NUM is three, and the DREQ definitions live in
    hardware/rp23xx_dreq.h.

The data and clock lines are also pulled down explicitly rather than left on
the reset default. The data line doubles as the chip's active-high interrupt
request and is serviced with a level-high interrupt, so it must read low
whenever the CYW43439 has released it. Finally the state machine returned by
rp23xx_pio_claim_unused_sm() is collected in a signed temporary before being
stored, so the "none available" test is not defeated by an unsigned comparison.

boards/arm/rp23xx: Add support for the Pimoroni Pico Plus 2 W. A Pico
form-factor board built around the RP2350B — the 80-pin part with 48 GPIOs —
with 16MB of flash, 8MB of PSRAM and a Raspberry Pi RM2 module for 2.4GHz WiFi.
The RM2 houses a CYW43439 wired to the same pins the Pico W uses: GPIO 23 power
enable, 24 shared gSPI data and interrupt, 25 chip select, 29 clock.

Three configurations: nsh and usbnsh without the wireless chip, and wifi,
which brings up wlan0 in station mode with WAPI, a DHCP client and ping.

The firmware and CLM blob are linked in from the pico-sdk at build time exactly
as on the Pico W, selected by CONFIG_CYW43439_FIRMWARE_BIN_PATH. pico-sdk 2.x
no longer ships that blob as a binary, only as a C header, so the board
documentation covers converting it back. rp23xx_firmware.c is placed in the
common source directory so a future board with the same chip reuses it, and
unlike rp2040 the blob is staged for the CMake build as well as for Make.

Two board details drove design decisions:

  • The only LED is on GPIO 0 of the CYW43439, not on an RP2350 pin, so
    driving it means an iovar request over the gSPI bus. That is fine for
    CONFIG_USERLED but not for CONFIG_ARCH_LEDS, whose board_autoled_on()
    is called from interrupt handlers and from assertion handling. A build
    selecting CONFIG_ARCH_LEDS is therefore rejected with an explicit message
    rather than silently doing nothing. The LED also only responds once wlan0
    has been brought up, because that is when the chip's firmware is downloaded.
  • The BOOT button is also wired to GPIO 45, so it reads as an ordinary user
    button once NuttX is running. Its internal pull-up is enabled deliberately:
    erratum RP2350-E9 means a floating Bank 0 input on RP2350 A2 leaks around
    120uA and settles near 2.2V, which the internal pull-down cannot overcome.

Impact

  • New feature. No existing board enables IEEE80211_INFINEON_CYW43439, so
    nothing changes for current users; with it disabled neither new source file
    is compiled.
  • Arch: arm (rp23xx / RP2350). New chip source built only under
    CONFIG_IEEE80211_INFINEON_CYW43439.
  • Boards: one new board, pimoroni-pico-plus-2-w. The rp23xx common
    Make.defs and CMakeLists.txt gain a block for rp23xx_firmware.c, also
    guarded by CONFIG_IEEE80211_INFINEON_CYW43439, so other rp23xx boards are
    unaffected.
  • Build process: a wifi build needs the CYW43439 blob available at
    CONFIG_CYW43439_FIRMWARE_BIN_PATH. If it is absent a placeholder is used so
    the tree still compiles, matching the existing rp2040 behaviour; the wireless
    chip then does not come up. No blob is added to the repository.
  • No new external dependencies and no API or ABI changes beyond the new Kconfig
    symbols.
  • Documentation: a new board page under
    Documentation/platforms/arm/rp23xx/boards/.

Testing

Host: macOS 15 (arm64), Arm GNU Toolchain 14.2.Rel1.

Hardware: Pimoroni Pico Plus 2 W, RP2350 A2 in QFN80 (confirmed with
picotool info). Flashed over SWD with a Raspberry Pi Debug Probe (probe-rs);
console on UART0 at 115200 through the probe's UART bridge.

SSIDs and BSSIDs below are masked; they were real values in the session.

Builds

All three configurations built from a clean tree, each reaching LD: nuttx:

nsh:    OK   2934352 bytes elf, uf2 303616
usbnsh: OK   3001124 bytes elf, uf2 313856
wifi:   OK   4658096 bytes elf, uf2 931840

tools/checkpatch.sh -f passes on every added C and header file.

Blob embedding verified in the linked ELF: g_cyw43439_clm_blob_image sits
exactly 224256 bytes after g_cyw43439_firmware_image (the 224190-byte
firmware rounded up to a 256-byte boundary) and begins with the CLM BLOB
magic, so the real blob is linked rather than the placeholder.

Base configuration, on hardware

nsh> uname -a
NuttX  12.2.1 4fa5487470 Jul 25 2026 arm pimoroni-pico-plus-2-w
nsh> free
      total       used       free    maxused    maxfree  nused  nfree name
     518064       6992     511072       7360     510656     25      2 Umem
nsh> ls /dev
/dev:
 buttons
 console
 null
 ttyS0
 userleds
 zero

Wireless, on hardware

wlan0 is registered with the MAC read from the chip:

nsh> ifconfig
wlan0	Link encap:Ethernet HWaddr 28:cd:c1:ff:d3:be at UP mtu 576
	inet addr:0.0.0.0 DRaddr:10.0.0.1 Mask:255.255.255.0

Scan:

nsh> wapi scan wlan0
bssid / frequency / signal level / encode / ssid
xx:xx:xx:xx:xx:xx	2412	-85	0800	<neighbour-1>
xx:xx:xx:xx:xx:xx	2412	-82	0800	<neighbour-2>
xx:xx:xx:xx:xx:xx	2457	-59	0800	<my-ssid>

Association, DHCP and connectivity:

nsh> wapi mode wlan0 2
nsh> wapi psk wlan0 "<passphrase>" 3 2
nsh> wapi essid wlan0 <my-ssid> 1
nsh> wapi show wlan0
wlan0 Configuration:
Frequency: 2457
  Channel: 10
    ESSID: <my-ssid>
     Flag: WAPI_ESSID_ON
     Mode: WAPI_MODE_MANAGED
       AP: xx:xx:xx:xx:xx:xx
  BitRate: 72000
  TxPower: 31
    Sense: -55
  Country: XX

nsh> renew wlan0
nsh> ifconfig
lo	Link encap:Local Loopback at RUNNING mtu 1518
	inet addr:127.0.0.1 DRaddr:127.0.0.1 Mask:255.0.0.0

wlan0	Link encap:Ethernet HWaddr 28:cd:c1:ff:d3:be at RUNNING mtu 576
	inet addr:192.168.0.118 DRaddr:192.168.0.1 Mask:255.255.255.0

nsh> ping -c 3 8.8.8.8
PING 8.8.8.8 56 bytes of data
56 bytes from 8.8.8.8: icmp_seq=0 time=20.0 ms
56 bytes from 8.8.8.8: icmp_seq=1 time=10.0 ms
56 bytes from 8.8.8.8: icmp_seq=2 time=10.0 ms
3 packets transmitted, 3 received, 0% packet loss, time 3030 ms
rtt min/avg/max/mdev = 10.000/13.333/20.000/4.714 ms

nsh> ping -c 2 google.com
PING 142.251.142.142 56 bytes of data
56 bytes from 142.251.142.142: icmp_seq=0 time=10.0 ms
56 bytes from 142.251.142.142: icmp_seq=1 time=10.0 ms
2 packets transmitted, 2 received, 0% packet loss, time 2020 ms
rtt min/avg/max/mdev = 10.000/10.000/10.000/0.000 ms

DNS resolution working confirms the full path through the driver in both
directions, not just ICMP to a literal address.

The association sequence was re-run after every change to the firmware staging
layout and after rebasing onto current master, with the same result.

LED and button, on hardware

The LED on CYW43439 GPIO 0 was driven with the leds example and observed
blinking on the board:

nsh> leds
led_daemon (pid# 3): Running
led_daemon: Opening /dev/userleds
led_daemon: Supported LEDs 0x01
led_daemon: LED set 0x01

The BOOT button on GPIO 45 was read through /dev/buttons with the buttons
example, pressing it by hand:

nsh> buttons
button_daemon: Opening /dev/buttons
button_daemon: Supported BUTTONs 0x01
Sample = 1

Not tested

  • usbnsh was build-tested only. RP2350 USB device support is still marked
    experimental and the CDC/ACM console corrupts data after a few commands on
    any rp23xx board, which is pre-existing and unrelated to this change.
  • Bluetooth. The RM2 module supports it but the bcmf driver does not.
  • The RISC-V (Hazard3) build of this board.

@github-actions github-actions Bot added Area: Build system Arch: arm Issues related to ARM (32-bit) architecture Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces. Board: arm labels Jul 25, 2026
@casaroli
casaroli force-pushed the rp23xx-cyw43439-pimoroni-pico-plus-2-w branch from 5c1d144 to 49dfc06 Compare July 25, 2026 10:51
@acassis
acassis requested a review from xiaoxiang781216 July 25, 2026 11:36
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

acassis
acassis previously approved these changes Jul 25, 2026
casaroli added 2 commits July 25, 2026 15:53
Provide the transport half of the bcmf FullMAC driver on RP2350, so that
boards carrying an Infineon CYW43439 (or the Raspberry Pi RM2 module that
houses one) can bring up a wlan0 interface.  The chip's half-duplex gSPI bus
is driven by a six-instruction PIO program with DMA in both directions, and
the driver fills in the gspi_dev_t operations that bcmf_gspi_initialize()
expects.

This is a port of the equivalent rp2040 driver.  Three differences in the
RP2350 hardware and its NuttX support had to be accounted for:

  - The GPIO_STATUS register no longer has an OEFROMPERI field, so the wait
    for the PIO program to release the shared data line watches OETOPAD
    instead.  No override is configured on that pad, so the two fields carry
    the same value.

  - rp23xx GPIO interrupt modes are enumerated as indices rather than as the
    bitmask rp2040 uses, so passing the rp2040 constant would have selected
    the wrong trigger.

  - RP23XX_PIO_NUM is three, and the DREQ definitions live in
    hardware/rp23xx_dreq.h.

The data and clock lines are also explicitly pulled down rather than left on
the reset default.  The data line doubles as the chip's active-high interrupt
request and is serviced with a level-high interrupt, so it must read low
whenever the CYW43439 has released it.

Finally, the state machine returned by rp23xx_pio_claim_unused_sm() is
collected in a signed temporary before being stored, so that the "no state
machine available" test is not defeated by an unsigned comparison.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
The Pimoroni Pico Plus 2 W is a Pico form-factor board built around the
RP2350B, the 80-pin part with 48 GPIOs, carrying 16MB of flash, 8MB of PSRAM
and a Raspberry Pi RM2 module for 2.4GHz WiFi.  The RM2 houses an Infineon
CYW43439, wired to the same pins the Raspberry Pi Pico W uses: GPIO 23 for
power enable, 24 for the shared gSPI data and interrupt line, 25 for chip
select and 29 for the clock.

Three configurations are provided: nsh and usbnsh without the wireless chip,
and wifi, which brings up wlan0 in station mode with WAPI, a DHCP client and
ping.  The CYW43439 firmware and CLM blob are linked in from the pico-sdk at
build time, as on the Pico W; CONFIG_CYW43439_FIRMWARE_BIN_PATH selects the
file and the board documentation covers converting it out of the C header that
pico-sdk 2.x ships in place of the binary.  rp23xx_firmware.c is placed in the
common source directory so that a future board with the same chip can reuse
it, and unlike rp2040 the blob is staged for the CMake build as well.

Two board details are worth calling out:

  - The only LED is wired to GPIO 0 of the CYW43439 rather than to a pin of
    the RP2350, so driving it means an iovar request over the gSPI bus.  That
    works for CONFIG_USERLED but not for CONFIG_ARCH_LEDS, whose
    board_autoled_on() is called from interrupt handlers and from assertion
    handling, so a build selecting CONFIG_ARCH_LEDS is rejected with an
    explicit message.  The LED also only responds once wlan0 has been brought
    up, because that is when the chip's firmware is downloaded.

  - The BOOT button is also wired to GPIO 45, so it can be read as an ordinary
    user button once NuttX is running.  Its internal pull-up is enabled
    deliberately: erratum RP2350-E9 means a floating Bank 0 input on RP2350 A2
    leaks enough current to settle around 2.2V, which the internal pull-down
    cannot overcome.

Tested on the board with a Raspberry Pi Debug Probe: nsh and wifi boot on
UART0, wlan0 reports the MAC read from the chip, wapi scan lists access
points, and association to a WPA2 network followed by DHCP gives a working
route with ping succeeding to both a literal address and a resolved name.
The LED and the BOOT button were confirmed by hand.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@casaroli
casaroli force-pushed the rp23xx-cyw43439-pimoroni-pico-plus-2-w branch from 49dfc06 to 0bd195b Compare July 25, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Area: Build system Board: arm Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants