Add Pico W and lwIP support
diff --git a/.gitmodules b/.gitmodules
index 4846c9c..a37debe 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,9 @@
 [submodule "tinyusb"]
 	path = lib/tinyusb
 	url = https://github.com/hathach/tinyusb.git
+[submodule "lib/cyw43-driver"]
+	path = lib/cyw43-driver
+	url = https://github.com/georgerobotics/cyw43-driver.git
+[submodule "lib/lwip"]
+	path = lib/lwip
+	url = https://github.com/lwip-tcpip/lwip.git
diff --git a/README.md b/README.md
index db6cef6..26084ca 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,24 @@
 
           ```
 
+   * Or by cloning the SDK locally, but without copying `pico_sdk_import.cmake`:
+       1. `git clone` this Raspberry Pi Pico SDK repository
+       2. Setup a `CMakeLists.txt` like:
+
+           ```cmake
+           cmake_minimum_required(VERSION 3.13)
+ 
+           # initialize the SDK directly
+           include(/path/to/pico-sdk/pico_sdk_init.cmake)
+ 
+           project(my_project)
+ 
+           # initialize the Raspberry Pi Pico SDK
+           pico_sdk_init()
+ 
+           # rest of your project
+ 
+           ```
 1. Write your code (see [pico-examples](https://github.com/raspberrypi/pico-examples) or the [Raspberry Pi Pico C/C++ SDK](https://rptl.io/pico-c-sdk) documentation for more information)
 
    About the simplest you can do is a single source file (e.g. hello_world.c)
diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in
index cc133f8..5dc4be7 100644
--- a/docs/Doxyfile.in
+++ b/docs/Doxyfile.in
@@ -60,4 +60,5 @@
                          __time_critical_func(x) \
                          __not_in_flash(x)= \
                          __no_inline_not_in_flash(x)= \
-                         __attribute__(x)=
+                         __attribute__(x)= \
+                         DOXYGEN_GENERATION=
diff --git a/docs/index.h b/docs/index.h
index c7ef735..a334fc3 100644
--- a/docs/index.h
+++ b/docs/index.h
@@ -56,6 +56,13 @@
  * \defgroup tinyusb_host tinyusb_host
  * @}
  *
+ * \defgroup networking Networking Libraries
+ * Functions for implementing networking
+ * @{
+ * \defgroup pico_lwip pico_lwip
+ * \defgroup pico_cyw43_arch pico_cyw43_arch
+ * @}
+ *
  * \defgroup runtime Runtime Infrastructure
  * Libraries that are used to provide efficient implementation of certain
  * language level and C library functions, as well as CMake INTERFACE libraries
diff --git a/lib/cyw43-driver b/lib/cyw43-driver
new file mode 160000
index 0000000..195dfcc
--- /dev/null
+++ b/lib/cyw43-driver
@@ -0,0 +1 @@
+Subproject commit 195dfcc10bb6f379e3dea45147590db2203d3c7b
diff --git a/lib/lwip b/lib/lwip
new file mode 160000
index 0000000..239918c
--- /dev/null
+++ b/lib/lwip
@@ -0,0 +1 @@
+Subproject commit 239918ccc173cb2c2a62f41a40fd893f57faf1d6
diff --git a/src/boards/include/boards/pico_w.h b/src/boards/include/boards/pico_w.h
new file mode 100644
index 0000000..b4a8bd7
--- /dev/null
+++ b/src/boards/include/boards/pico_w.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+// -----------------------------------------------------
+// NOTE: THIS HEADER IS ALSO INCLUDED BY ASSEMBLER SO
+//       SHOULD ONLY CONSIST OF PREPROCESSOR DIRECTIVES
+// -----------------------------------------------------
+
+// This header may be included by other board headers as "boards/pico.h"
+
+#ifndef _BOARDS_PICO_W_H
+#define _BOARDS_PICO_W_H
+
+// For board detection
+#define RASPBERRYPI_PICO_W
+
+// --- UART ---
+#ifndef PICO_DEFAULT_UART
+#define PICO_DEFAULT_UART 0
+#endif
+#ifndef PICO_DEFAULT_UART_TX_PIN
+#define PICO_DEFAULT_UART_TX_PIN 0
+#endif
+#ifndef PICO_DEFAULT_UART_RX_PIN
+#define PICO_DEFAULT_UART_RX_PIN 1
+#endif
+
+// --- LED ---
+// no PICO_DEFAULT_LED_PIN - LED is on Wireless chip
+// no PICO_DEFAULT_WS2812_PIN
+
+// --- I2C ---
+#ifndef PICO_DEFAULT_I2C
+#define PICO_DEFAULT_I2C 0
+#endif
+#ifndef PICO_DEFAULT_I2C_SDA_PIN
+#define PICO_DEFAULT_I2C_SDA_PIN 4
+#endif
+#ifndef PICO_DEFAULT_I2C_SCL_PIN
+#define PICO_DEFAULT_I2C_SCL_PIN 5
+#endif
+
+// --- SPI ---
+#ifndef PICO_DEFAULT_SPI
+#define PICO_DEFAULT_SPI 0
+#endif
+#ifndef PICO_DEFAULT_SPI_SCK_PIN
+#define PICO_DEFAULT_SPI_SCK_PIN 18
+#endif
+#ifndef PICO_DEFAULT_SPI_TX_PIN
+#define PICO_DEFAULT_SPI_TX_PIN 19
+#endif
+#ifndef PICO_DEFAULT_SPI_RX_PIN
+#define PICO_DEFAULT_SPI_RX_PIN 16
+#endif
+#ifndef PICO_DEFAULT_SPI_CSN_PIN
+#define PICO_DEFAULT_SPI_CSN_PIN 17
+#endif
+
+// --- FLASH ---
+
+#define PICO_BOOT_STAGE2_CHOOSE_W25Q080 1
+
+#ifndef PICO_FLASH_SPI_CLKDIV
+#define PICO_FLASH_SPI_CLKDIV 2
+#endif
+
+#ifndef PICO_FLASH_SIZE_BYTES
+#define PICO_FLASH_SIZE_BYTES (2 * 1024 * 1024)
+#endif
+
+// note the SMSP mode pin is on WL_GPIO1
+// #define PICO_SMPS_MODE_PIN
+
+#ifndef PICO_RP2040_B0_SUPPORTED
+#define PICO_RP2040_B0_SUPPORTED 0
+#endif
+
+#ifndef PICO_RP2040_B1_SUPPORTED
+#define PICO_RP2040_B1_SUPPORTED 0
+#endif
+
+#ifndef CYW43_PIN_WL_HOST_WAKE
+#define CYW43_PIN_WL_HOST_WAKE 24
+#endif
+
+#ifndef CYW43_PIN_WL_REG_ON
+#define CYW43_PIN_WL_REG_ON 23
+#endif
+
+#ifndef CYW43_WL_GPIO_COUNT
+#define CYW43_WL_GPIO_COUNT 3
+#endif
+
+#ifndef CYW43_WL_GPIO_LED_PIN
+#define CYW43_WL_GPIO_LED_PIN 0
+#endif
+
+#endif
diff --git a/src/boards/pico_w.cmake b/src/boards/pico_w.cmake
new file mode 100644
index 0000000..15b901d
--- /dev/null
+++ b/src/boards/pico_w.cmake
@@ -0,0 +1,2 @@
+set(PICO_CYW43_SUPPORTED "1" CACHE INTERNAL "Try to add support for PICO_CYW43")
+include(${CMAKE_CURRENT_LIST_DIR}/generic_board.cmake)
diff --git a/src/common/pico_base/include/pico/error.h b/src/common/pico_base/include/pico/error.h
index fadb45e..a5cbc39 100644
--- a/src/common/pico_base/include/pico/error.h
+++ b/src/common/pico_base/include/pico/error.h
@@ -10,14 +10,18 @@
 #ifndef __ASSEMBLER__
 
 /*!
- * Common return codes from pico_sdk methods that return a status
+ * \brief Common return codes from pico_sdk methods that return a status
+ * \ingroup pico_base
  */
-enum {
+enum pico_error_codes {
     PICO_OK = 0,
     PICO_ERROR_NONE = 0,
     PICO_ERROR_TIMEOUT = -1,
     PICO_ERROR_GENERIC = -2,
     PICO_ERROR_NO_DATA = -3,
+    PICO_ERROR_NOT_PERMITTED = -4,
+    PICO_ERROR_INVALID_ARG = -5,
+    PICO_ERROR_IO = -6,
 };
 
 #endif // !__ASSEMBLER__
diff --git a/src/rp2_common/CMakeLists.txt b/src/rp2_common/CMakeLists.txt
index 4ca55be..0d0f9b9 100644
--- a/src/rp2_common/CMakeLists.txt
+++ b/src/rp2_common/CMakeLists.txt
@@ -58,6 +58,10 @@
     pico_add_subdirectory(tinyusb)
     pico_add_subdirectory(pico_stdio_usb)
 
+    pico_add_subdirectory(cyw43_driver)
+    pico_add_subdirectory(pico_lwip)
+    pico_add_subdirectory(pico_cyw43_arch)
+
     pico_add_subdirectory(pico_stdlib)
 
     pico_add_subdirectory(pico_cxx_options)
diff --git a/src/rp2_common/cyw43_driver/CMakeLists.txt b/src/rp2_common/cyw43_driver/CMakeLists.txt
new file mode 100644
index 0000000..8951df4
--- /dev/null
+++ b/src/rp2_common/cyw43_driver/CMakeLists.txt
@@ -0,0 +1,81 @@
+if (DEFINED ENV{PICO_CYW43_DRIVER_PATH} AND (NOT PICO_CYW43_DRIVER_PATH))
+    set(PICO_CYW43_DRIVER_PATH $ENV{PICO_CYW43_DRIVER_PATH})
+    message("Using PICO_CYW43_DRIVER_PATH from environment ('${PICO_CYW43_DRIVER_PATH}')")
+endif()
+
+set(CYW43_DRIVER_TEST_FILE "src/cyw43.h")
+
+if (NOT PICO_CYW43_DRIVER_PATH)
+    set(PICO_CYW43_DRIVER_PATH ${PICO_SDK_PATH}/lib/cyw43-driver)
+    if (PICO_CYW43_SUPPORTED AND NOT EXISTS ${PICO_CYW43_DRIVER_PATH}/${CYW43_DRIVER_TEST_FILE})
+        message(WARNING "cyw43-driver submodule has not been initialized; Pico W wireless support will be unavailable
+hint: try 'git submodule update --init' from your SDK directory (${PICO_SDK_PATH}).")
+    endif()
+elseif (NOT EXISTS ${PICO_CYW43_DRIVER_PATH}/${CYW43_DRIVER_TEST_FILE})
+    message(WARNING "PICO_CYW43_DRIVER_PATH specified but content not present.")
+endif()
+
+if (EXISTS ${PICO_CYW43_DRIVER_PATH}/${CYW43_DRIVER_TEST_FILE})
+    message("cyw43-driver available at ${PICO_CYW43_DRIVER_PATH}")
+
+    pico_register_common_scope_var(PICO_CYW43_DRIVER_PATH)
+
+    # base driver without our bus
+    add_library(cyw43_driver_base INTERFACE)
+    target_sources(cyw43_driver_base INTERFACE
+            ${PICO_CYW43_DRIVER_PATH}/src/cyw43_ll.c
+            ${PICO_CYW43_DRIVER_PATH}/src/cyw43_stats.c
+            ${PICO_CYW43_DRIVER_PATH}/src/cyw43_lwip.c
+            ${PICO_CYW43_DRIVER_PATH}/src/cyw43_ctrl.c
+            )
+    target_include_directories(cyw43_driver_base INTERFACE
+            ${PICO_CYW43_DRIVER_PATH}/src
+            ${PICO_CYW43_DRIVER_PATH}/firmware
+            )
+
+    # Build the driver for cyw43 for pico w
+
+    # Firmware stuff
+    set(CYW43_FIRMWARE_BIN 43439A0-7.95.49.00.combined)
+    string(REGEX REPLACE [\\\.\-] _ CYW43_FIRMWARE_BIN_ ${CYW43_FIRMWARE_BIN})
+    string(REGEX MATCH [^_]+_?[^_]*_?[^_]*_?[^_]*_?[^_]* CYW43_FIRMWARE_PRETTY ${CYW43_FIRMWARE_BIN_})
+    set(CYW43_FIRMWARE_PRETTY fw_${CYW43_FIRMWARE_PRETTY})
+    set(RESOURCE_SECNAME .big_const)
+    set(RESOURCE_SECFLAGS contents,alloc,load,readonly,data)
+    set(CYW43_FIRMWARE_OBJ ${CMAKE_CURRENT_BINARY_DIR}/cyw43_resource.o)
+
+    add_custom_target(cyw43_firmware_package DEPENDS ${CYW43_FIRMWARE_OBJ})
+
+    # cyw43_resource.o contains the WiFi and BT firmware as a binary blob
+    add_custom_command(
+            OUTPUT ${CYW43_FIRMWARE_OBJ}
+            DEPENDS ${PICO_CYW43_DRIVER_PATH}/firmware/${CYW43_FIRMWARE_BIN}
+            WORKING_DIRECTORY ${PICO_CYW43_DRIVER_PATH}/firmware
+            COMMAND ${CMAKE_OBJCOPY} -I binary -O elf32-littlearm -B arm
+            --readonly-text
+            --rename-section .data=${RESOURCE_SECNAME},${RESOURCE_SECFLAGS}
+            --redefine-sym _binary_${CYW43_FIRMWARE_BIN_}_start=${CYW43_FIRMWARE_PRETTY}_start
+            --redefine-sym _binary_${CYW43_FIRMWARE_BIN_}_end=${CYW43_FIRMWARE_PRETTY}_end
+            --redefine-sym _binary_${CYW43_FIRMWARE_BIN_}_size=${CYW43_FIRMWARE_PRETTY}_size
+            ${CYW43_FIRMWARE_BIN} ${CYW43_FIRMWARE_OBJ}
+    )
+
+    add_library(cyw43_driver_picow INTERFACE)
+    target_sources(cyw43_driver_picow INTERFACE
+            ${CMAKE_CURRENT_LIST_DIR}/cyw43_bus_pio_spi.c
+            )
+    pico_generate_pio_header(cyw43_driver_picow ${CMAKE_CURRENT_LIST_DIR}/cyw43_bus_pio_spi.pio)
+    add_dependencies(cyw43_driver_picow INTERFACE cyw43_firmware_package)
+    target_link_libraries(cyw43_driver_picow INTERFACE
+            ${CYW43_FIRMWARE_OBJ}
+            )
+    target_link_libraries(cyw43_driver_picow INTERFACE
+            cyw43_driver_base
+            pico_stdlib
+            hardware_pio
+            hardware_dma
+            hardware_exception
+            )
+
+    pico_promote_common_scope_vars()
+endif()
diff --git a/src/rp2_common/cyw43_driver/cyw43_bus_pio_spi.c b/src/rp2_common/cyw43_driver/cyw43_bus_pio_spi.c
new file mode 100644
index 0000000..0fd17f4
--- /dev/null
+++ b/src/rp2_common/cyw43_driver/cyw43_bus_pio_spi.c
@@ -0,0 +1,545 @@
+/*
+ * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "pico/stdlib.h"
+#include "hardware/gpio.h"
+#include "hardware/pio.h"
+#include "hardware/clocks.h"
+#include "hardware/structs/iobank0.h"
+#include "hardware/sync.h"
+#include "hardware/dma.h"
+#include "cyw43_bus_pio_spi.pio.h"
+#include "cyw43.h"
+#include "cyw43_internal.h"
+#include "cyw43_spi.h"
+#include "cyw43_debug_pins.h"
+
+#if CYW43_SPI_PIO
+#define WL_REG_ON 23
+#define DATA_OUT_PIN 24u
+#define DATA_IN_PIN 24u
+#define IRQ_PIN 24u
+// #define MONITOR_PIN 3u
+#define CLOCK_PIN 29u
+#define CS_PIN 25u
+#define IRQ_SAMPLE_DELAY_NS 100
+
+#define SPI_PROGRAM_NAME spi_gap01_sample0
+#define SPI_PROGRAM_FUNC __CONCAT(SPI_PROGRAM_NAME, _program)
+#define SPI_PROGRAM_GET_DEFAULT_CONFIG_FUNC __CONCAT(SPI_PROGRAM_NAME, _program_get_default_config)
+#define SPI_OFFSET_END __CONCAT(SPI_PROGRAM_NAME, _offset_end)
+#define SPI_OFFSET_LP1_END __CONCAT(SPI_PROGRAM_NAME, _offset_lp1_end)
+
+#define CLOCK_DIV 2
+#define CLOCK_DIV_MINOR 0
+#define PADS_DRIVE_STRENGTH PADS_BANK0_GPIO0_DRIVE_VALUE_12MA
+
+#if !CYW43_USE_SPI
+#error CYW43_USE_SPI should be true
+#endif
+
+#ifndef NDEBUG
+//#define ENABLE_SPI_DUMPING 1
+#endif
+
+// Set to 1 to enable
+#if ENABLE_SPI_DUMPING //NDEBUG
+#if 0
+#define DUMP_SPI_TRANSACTIONS(A) A
+#else
+static bool enable_spi_packet_dumping; // set to true to dump
+#define DUMP_SPI_TRANSACTIONS(A) if (enable_spi_packet_dumping) {A}
+#endif
+
+static uint32_t counter = 0;
+#else
+#define DUMP_SPI_TRANSACTIONS(A)
+#endif
+
+//#define SWAP32(A) ((((A) & 0xff000000U) >> 8) | (((A) & 0xff0000U) << 8) | (((A) & 0xff00U) >> 8) | (((A) & 0xffU) << 8))
+__force_inline static uint32_t __swap16x2(uint32_t a) {
+    __asm ("rev16 %0, %0" : "+l" (a) : : );
+    return a;
+}
+#define SWAP32(a) __swap16x2(a)
+
+#ifndef CYW43_SPI_PIO_PREFERRED_PIO
+#define CYW43_SPI_PIO_PREFERRED_PIO 1
+#endif
+static_assert(CYW43_SPI_PIO_PREFERRED_PIO >=0 && CYW43_SPI_PIO_PREFERRED_PIO < NUM_PIOS, "");
+
+typedef struct {
+    pio_hw_t *pio;
+    uint8_t pio_func_sel;
+    int8_t pio_offset;
+    int8_t pio_sm;
+    int8_t dma_out;
+    int8_t dma_in;
+} bus_data_t;
+
+static bus_data_t bus_data_instance;
+
+int cyw43_spi_init(cyw43_int_t *self) {
+    // Only does something if CYW43_LOGIC_DEBUG=1
+    logic_debug_init();
+
+    static_assert(NUM_PIOS == 2, "");
+
+    pio_hw_t *pios[2] = {pio0, pio1};
+    uint pio_index = CYW43_SPI_PIO_PREFERRED_PIO;
+    // Check we can add the program
+    if (!pio_can_add_program(pios[pio_index], &SPI_PROGRAM_FUNC)) {
+        pio_index ^= 1;
+        if (!pio_can_add_program(pios[pio_index], &SPI_PROGRAM_FUNC)) {
+            return CYW43_FAIL_FAST_CHECK(-CYW43_EIO);
+        }
+    }
+    assert(!self->bus_data);
+    self->bus_data = &bus_data_instance;
+    bus_data_t *bus_data = (bus_data_t *)self->bus_data;
+    bus_data->pio = pios[pio_index];
+    bus_data->dma_in = -1;
+    bus_data->dma_out = -1;
+
+    static_assert(GPIO_FUNC_PIO1 == GPIO_FUNC_PIO0 + 1, "");
+    bus_data->pio_func_sel = GPIO_FUNC_PIO0 + pio_index;
+    bus_data->pio_sm = (int8_t)pio_claim_unused_sm(bus_data->pio, false);
+    if (bus_data->pio_sm < 0) {
+        cyw43_spi_deinit(self);
+        return CYW43_FAIL_FAST_CHECK(-CYW43_EIO);
+    }
+
+    bus_data->pio_offset = pio_add_program(bus_data->pio, &SPI_PROGRAM_FUNC);
+    pio_sm_config config = SPI_PROGRAM_GET_DEFAULT_CONFIG_FUNC(bus_data->pio_offset);
+
+    sm_config_set_clkdiv_int_frac(&config, CLOCK_DIV, CLOCK_DIV_MINOR);
+    hw_write_masked(&padsbank0_hw->io[CLOCK_PIN],
+                    (uint)PADS_DRIVE_STRENGTH << PADS_BANK0_GPIO0_DRIVE_LSB,
+                    PADS_BANK0_GPIO0_DRIVE_BITS
+    );
+    hw_write_masked(&padsbank0_hw->io[CLOCK_PIN],
+                    (uint)1 << PADS_BANK0_GPIO0_SLEWFAST_LSB,
+                    PADS_BANK0_GPIO0_SLEWFAST_BITS
+    );
+
+    sm_config_set_out_pins(&config, DATA_OUT_PIN, 1);
+    sm_config_set_in_pins(&config, DATA_IN_PIN);
+    sm_config_set_set_pins(&config, DATA_OUT_PIN, 1);
+    sm_config_set_sideset(&config, 1, false, false);
+    sm_config_set_sideset_pins(&config, CLOCK_PIN);
+    sm_config_set_in_shift(&config, false, true, 32);
+    sm_config_set_out_shift(&config, false, true, 32);
+    hw_set_bits(&bus_data->pio->input_sync_bypass, 1u << DATA_IN_PIN);
+    pio_sm_set_config(bus_data->pio, bus_data->pio_sm, &config);
+    pio_sm_set_consecutive_pindirs(bus_data->pio, bus_data->pio_sm, CLOCK_PIN, 1, true);
+    gpio_set_function(DATA_OUT_PIN, bus_data->pio_func_sel);
+    gpio_set_function(CLOCK_PIN, bus_data->pio_func_sel);
+
+    // Set data pin to pull down and schmitt
+    gpio_set_pulls(DATA_IN_PIN, false, true);
+    gpio_set_input_hysteresis_enabled(DATA_IN_PIN, true);
+
+    pio_sm_exec(bus_data->pio, bus_data->pio_sm, pio_encode_set(pio_pins, 1));
+
+    bus_data->dma_out = (int8_t) dma_claim_unused_channel(false);
+    bus_data->dma_in = (int8_t) dma_claim_unused_channel(false);
+    if (bus_data->dma_out < 0 || bus_data->dma_in < 0) {
+        cyw43_spi_deinit(self);
+        return CYW43_FAIL_FAST_CHECK(-CYW43_EIO);
+    }
+    return 0;
+}
+
+void cyw43_spi_deinit(cyw43_int_t *self) {
+    if (self->bus_data) {
+        bus_data_t *bus_data = (bus_data_t *)self->bus_data;
+        if (bus_data->pio_sm >= 0) {
+            if (bus_data->pio_offset != -1)
+                pio_remove_program(bus_data->pio, &SPI_PROGRAM_FUNC, bus_data->pio_offset);
+            pio_sm_unclaim(bus_data->pio, bus_data->pio_sm);
+        }
+        if (bus_data->dma_out >= 0) {
+            dma_channel_unclaim(bus_data->dma_out);
+            bus_data->dma_out = -1;
+        }
+        if (bus_data->dma_in >= 0) {
+            dma_channel_unclaim(bus_data->dma_in);
+            bus_data->dma_in = -1;
+        }
+        self->bus_data = NULL;
+    }
+}
+
+static void cs_set(bool value) {
+    gpio_put(CS_PIN, value);
+}
+
+static __noinline void ns_delay(uint32_t ns) {
+    // cycles = ns * clk_sys_hz / 1,000,000,000
+    uint32_t cycles = ns * (clock_get_hz(clk_sys) >> 16u) / (1000000000u >> 16u);
+    busy_wait_at_least_cycles(cycles);
+}
+
+static void start_spi_comms(cyw43_int_t *self) {
+    bus_data_t *bus_data = (bus_data_t *)self->bus_data;
+    // Pull CS low
+    cs_set(false);
+    gpio_set_function(DATA_OUT_PIN, bus_data->pio_func_sel);
+}
+
+// we need to atomically de-assert CS and enable IRQ
+static void stop_spi_comms(void) {
+    // from this point a positive edge will cause an IRQ to be pending
+    cs_set(true);
+
+    // we need to wait a bit in case the irq line is incorrectly high
+    ns_delay(IRQ_SAMPLE_DELAY_NS);
+}
+
+#if ENABLE_SPI_DUMPING
+static void dump_bytes(const uint8_t *bptr, uint32_t len) {
+    unsigned int i = 0;
+
+    for (i = 0; i < len;) {
+        if ((i & 0x0f) == 0) {
+            printf("\n");
+        } else if ((i & 0x07) == 0) {
+            printf(" ");
+        }
+        printf("%02x ", bptr[i++]);
+    }
+    printf("\n");
+}
+#endif
+
+int cyw43_spi_transfer(cyw43_int_t *self, const uint8_t *tx, size_t tx_length, uint8_t *rx,
+                       size_t rx_length) {
+
+    if ((tx == NULL) && (rx == NULL)) {
+        return CYW43_FAIL_FAST_CHECK(-CYW43_EINVAL);
+    }
+
+    bus_data_t *bus_data = (bus_data_t *)self->bus_data;
+    start_spi_comms(self);
+    if (rx != NULL) {
+        if (tx == NULL) {
+            tx = rx;
+            assert(tx_length && tx_length < rx_length);
+        }
+        DUMP_SPI_TRANSACTIONS(
+                printf("[%lu] bus TX/RX %u bytes rx %u:", counter++, tx_length, rx_length);
+                dump_bytes(tx, tx_length);
+        )
+        assert(!(tx_length & 3));
+        assert(!(((uintptr_t)tx) & 3));
+        assert(!(((uintptr_t)rx) & 3));
+        assert(!(rx_length & 3));
+
+        pio_sm_set_enabled(bus_data->pio, bus_data->pio_sm, false);
+        pio_sm_set_wrap(bus_data->pio, bus_data->pio_sm, bus_data->pio_offset, bus_data->pio_offset + SPI_OFFSET_END - 1);
+        pio_sm_clear_fifos(bus_data->pio, bus_data->pio_sm);
+        pio_sm_set_pindirs_with_mask(bus_data->pio, bus_data->pio_sm, 1u << DATA_OUT_PIN, 1u << DATA_OUT_PIN);
+        pio_sm_restart(bus_data->pio, bus_data->pio_sm);
+        pio_sm_clkdiv_restart(bus_data->pio, bus_data->pio_sm);
+        pio_sm_put(bus_data->pio, bus_data->pio_sm, tx_length * 8 - 1);
+        pio_sm_exec(bus_data->pio, bus_data->pio_sm, pio_encode_out(pio_x, 32));
+        pio_sm_put(bus_data->pio, bus_data->pio_sm, (rx_length - tx_length) * 8 - 1);
+        pio_sm_exec(bus_data->pio, bus_data->pio_sm, pio_encode_out(pio_y, 32));
+        pio_sm_exec(bus_data->pio, bus_data->pio_sm, pio_encode_jmp(bus_data->pio_offset));
+        dma_channel_abort(bus_data->dma_out);
+        dma_channel_abort(bus_data->dma_in);
+
+        dma_channel_config out_config = dma_channel_get_default_config(bus_data->dma_out);
+        channel_config_set_bswap(&out_config, true);
+        channel_config_set_dreq(&out_config, pio_get_dreq(bus_data->pio, 0, true));
+
+        dma_channel_configure(bus_data->dma_out, &out_config, &bus_data->pio->txf[0], tx, tx_length / 4, true);
+
+        dma_channel_config in_config = dma_channel_get_default_config(bus_data->dma_in);
+        channel_config_set_bswap(&in_config, true);
+        channel_config_set_dreq(&in_config, pio_get_dreq(bus_data->pio, 0, false));
+        channel_config_set_write_increment(&in_config, true);
+        channel_config_set_read_increment(&in_config, false);
+        dma_channel_configure(bus_data->dma_in, &in_config, rx + tx_length, &bus_data->pio->rxf[0], rx_length / 4 - tx_length / 4, true);
+
+        pio_sm_set_enabled(bus_data->pio, bus_data->pio_sm, true);
+        __compiler_memory_barrier();
+
+        dma_channel_wait_for_finish_blocking(bus_data->dma_out);
+        dma_channel_wait_for_finish_blocking(bus_data->dma_in);
+
+        __compiler_memory_barrier();
+        memset(rx, 0, tx_length); // make sure we don't have garbage in what would have been returned data if using real SPI
+    } else if (tx != NULL) {
+        DUMP_SPI_TRANSACTIONS(
+                printf("[%lu] bus TX only %u bytes:", counter++, tx_length);
+                dump_bytes(tx, tx_length);
+        )
+        assert(!(((uintptr_t)tx) & 3));
+        assert(!(tx_length & 3));
+        pio_sm_set_enabled(bus_data->pio, bus_data->pio_sm, false);
+        pio_sm_set_wrap(bus_data->pio, bus_data->pio_sm, bus_data->pio_offset, bus_data->pio_offset + SPI_OFFSET_LP1_END - 1);
+        pio_sm_clear_fifos(bus_data->pio, bus_data->pio_sm);
+        pio_sm_set_pindirs_with_mask(bus_data->pio, bus_data->pio_sm, 1u << DATA_OUT_PIN, 1u << DATA_OUT_PIN);
+        pio_sm_restart(bus_data->pio, bus_data->pio_sm);
+        pio_sm_clkdiv_restart(bus_data->pio, bus_data->pio_sm);
+        pio_sm_put(bus_data->pio, bus_data->pio_sm, tx_length * 8 - 1);
+        pio_sm_exec(bus_data->pio, bus_data->pio_sm, pio_encode_out(pio_x, 32));
+        pio_sm_put(bus_data->pio, bus_data->pio_sm, 0);
+        pio_sm_exec(bus_data->pio, bus_data->pio_sm, pio_encode_out(pio_y, 32));
+        pio_sm_exec(bus_data->pio, bus_data->pio_sm, pio_encode_jmp(bus_data->pio_offset));
+        dma_channel_abort(bus_data->dma_out);
+
+        dma_channel_config out_config = dma_channel_get_default_config(bus_data->dma_out);
+        channel_config_set_bswap(&out_config, true);
+        channel_config_set_dreq(&out_config, pio_get_dreq(bus_data->pio, 0, true));
+
+        dma_channel_configure(bus_data->dma_out, &out_config, &bus_data->pio->txf[0], tx, tx_length / 4, true);
+
+        bus_data->pio->fdebug = 1u << PIO_FDEBUG_TXSTALL_LSB;
+        pio_sm_set_enabled(bus_data->pio, 0, true);
+        while (!(bus_data->pio->fdebug & (1u << PIO_FDEBUG_TXSTALL_LSB))) {
+            tight_loop_contents(); // todo timeout
+        }
+        __compiler_memory_barrier();
+        pio_sm_set_enabled(bus_data->pio, bus_data->pio_sm, false);
+        pio_sm_set_consecutive_pindirs(bus_data->pio, bus_data->pio_sm, DATA_IN_PIN, 1, false);
+    } else if (rx != NULL) { /* currently do one at a time */
+        DUMP_SPI_TRANSACTIONS(
+                printf("[%lu] bus TX %u bytes:", counter++, rx_length);
+                dump_bytes(rx, rx_length);
+        )
+        panic_unsupported();
+    }
+    pio_sm_exec(bus_data->pio, bus_data->pio_sm, pio_encode_mov(pio_pins, pio_null)); // for next time we turn output on
+
+    stop_spi_comms();
+    DUMP_SPI_TRANSACTIONS(
+            printf("RXed:");
+            dump_bytes(rx, rx_length);
+            printf("\n");
+    )
+
+    return 0;
+}
+
+// Initialise our gpios
+void cyw43_spi_gpio_setup(void) {
+    // Setup WL_REG_ON (23)
+    gpio_init(WL_REG_ON);
+    gpio_set_dir(WL_REG_ON, GPIO_OUT);
+    gpio_pull_up(WL_REG_ON);
+
+    // Setup DO, DI and IRQ (24)
+    gpio_init(DATA_OUT_PIN);
+    gpio_set_dir(DATA_OUT_PIN, GPIO_OUT);
+    gpio_put(DATA_OUT_PIN, false);
+
+    // Setup CS (25)
+    gpio_init(CS_PIN);
+    gpio_set_dir(CS_PIN, GPIO_OUT);
+    gpio_put(CS_PIN, true);
+}
+
+// Reset wifi chip
+void cyw43_spi_reset(void) {
+    gpio_put(WL_REG_ON, false); // off
+    sleep_ms(20);
+    gpio_put(WL_REG_ON, true); // on
+    sleep_ms(250);
+
+    // Setup IRQ (24) - also used for DO, DI
+    gpio_init(IRQ_PIN);
+    gpio_set_dir(IRQ_PIN, GPIO_IN);
+}
+
+static inline uint32_t make_cmd(bool write, bool inc, uint32_t fn, uint32_t addr, uint32_t sz) {
+    return write << 31 | inc << 30 | fn << 28 | (addr & 0x1ffff) << 11 | sz;
+}
+
+#if CYW43_VERBOSE_DEBUG
+static const char *func_name(int fn) {
+    switch (fn)
+    {
+        case BUS_FUNCTION:
+            return "BUS_FUNCTION";
+        case BACKPLANE_FUNCTION:
+            return "BACKPLANE_FUNCTION";
+        case WLAN_FUNCTION:
+            return "WLAN_FUNCTION";
+        default:
+            return "UNKNOWN";
+    }
+}
+#endif
+
+uint32_t read_reg_u32_swap(cyw43_int_t *self, uint32_t fn, uint32_t reg) {
+    uint32_t buf[2] = {0};
+    assert(fn != BACKPLANE_FUNCTION);
+    buf[0] = SWAP32(make_cmd(false, true, fn, reg, 4));
+    int ret = cyw43_spi_transfer(self, NULL, 4, (uint8_t *)buf, 8);
+    if (ret != 0) {
+        return ret;
+    }
+    return SWAP32(buf[1]);
+}
+
+static inline uint32_t _cyw43_read_reg(cyw43_int_t *self, uint32_t fn, uint32_t reg, uint size) {
+    // Padding plus max read size of 32 bits + another 4?
+    static_assert(WHD_BUS_SPI_BACKPLANE_READ_PADD_SIZE % 4 == 0, "");
+    uint32_t buf32[WHD_BUS_SPI_BACKPLANE_READ_PADD_SIZE/4 + 1 + 1];
+    uint8_t *buf = (uint8_t *)buf32;
+    const uint32_t padding = (fn == BACKPLANE_FUNCTION) ? WHD_BUS_SPI_BACKPLANE_READ_PADD_SIZE : 0; // Add response delay
+    buf32[0] = make_cmd(false, true, fn, reg, size + padding);
+
+    if (fn == BACKPLANE_FUNCTION) {
+        logic_debug_set(pin_BACKPLANE_READ, 1);
+    }
+    int ret = cyw43_spi_transfer(self, NULL, 4, buf, 8 + padding);
+    if (fn == BACKPLANE_FUNCTION) {
+        logic_debug_set(pin_BACKPLANE_READ, 0);
+    }
+
+    if (ret != 0) {
+        return ret;
+    }
+    uint32_t result = buf32[padding > 0 ? 2 : 1];
+    CYW43_VDEBUG("cyw43_read_reg_u%d %s 0x%lx=0x%lx\n", size * 8, func_name(fn), reg, result);
+    return result;
+}
+
+uint32_t cyw43_read_reg_u32(cyw43_int_t *self, uint32_t fn, uint32_t reg) {
+    return _cyw43_read_reg(self, fn, reg, 4);
+}
+
+int cyw43_read_reg_u16(cyw43_int_t *self, uint32_t fn, uint32_t reg) {
+    return _cyw43_read_reg(self, fn, reg, 2);
+}
+
+int cyw43_read_reg_u8(cyw43_int_t *self, uint32_t fn, uint32_t reg) {
+    return _cyw43_read_reg(self, fn, reg, 1);
+}
+
+// This is only used to switch the word order on boot
+int write_reg_u32_swap(cyw43_int_t *self, uint32_t fn, uint32_t reg, uint32_t val) {
+    uint32_t buf[2];
+    // Boots up in little endian so command needs swapping too
+    buf[0] = SWAP32(make_cmd(true, true, fn, reg, 4));
+    buf[1] = SWAP32(val);
+    int ret = cyw43_spi_transfer(self, (uint8_t *)buf, 8, NULL, 0);
+    CYW43_VDEBUG("write_reg_u32_swap %s 0x%lx=0x%lx\n", func_name(fn), reg, val);
+    return ret;
+}
+
+static inline int _cyw43_write_reg(cyw43_int_t *self, uint32_t fn, uint32_t reg, uint32_t val, uint size) {
+    uint32_t buf[2];
+    buf[0] = make_cmd(true, true, fn, reg, size);
+    buf[1] = val;
+    if (fn == BACKPLANE_FUNCTION) {
+        // In case of f1 overflow
+        self->last_size = 8;
+        self->last_header[0] = buf[0];
+        self->last_header[1] = buf[1];
+        self->last_backplane_window = self->cur_backplane_window;
+    }
+
+    if (fn == BACKPLANE_FUNCTION) {
+        logic_debug_set(pin_BACKPLANE_WRITE, 1);
+    }
+
+    int ret = cyw43_spi_transfer(self, (uint8_t *)buf, 8, NULL, 0);
+
+    if (fn == BACKPLANE_FUNCTION) {
+        logic_debug_set(pin_BACKPLANE_WRITE, 0);
+    }
+
+    CYW43_VDEBUG("cyw43_write_reg_u%d %s 0x%lx=0x%lx\n", size * 8, func_name(fn), reg, val);
+    return ret;
+}
+
+int cyw43_write_reg_u32(cyw43_int_t *self, uint32_t fn, uint32_t reg, uint32_t val) {
+    return _cyw43_write_reg(self, fn, reg, val, 4);
+}
+
+int cyw43_write_reg_u16(cyw43_int_t *self, uint32_t fn, uint32_t reg, uint16_t val) {
+    return _cyw43_write_reg(self, fn, reg, val, 2);
+}
+
+int cyw43_write_reg_u8(cyw43_int_t *self, uint32_t fn, uint32_t reg, uint32_t val) {
+    return _cyw43_write_reg(self, fn, reg, val, 1);
+}
+
+#if MAX_BLOCK_SIZE > 0x7f8
+#error Block size is wrong for SPI
+#endif
+
+// Assumes we're reading into spid_buf
+int cyw43_read_bytes(cyw43_int_t *self, uint32_t fn, uint32_t addr, size_t len, uint8_t *buf) {
+    assert(fn != BACKPLANE_FUNCTION || (len <= 64 && (addr + len) <= 0x8000));
+    const uint32_t padding = (fn == BACKPLANE_FUNCTION) ? 4 : 0; // Add response delay
+    size_t aligned_len = (len + 3) & ~3;
+    assert(aligned_len > 0 && aligned_len <= 0x7f8);
+    self->spi_header[padding > 0 ? 0 : 1] = make_cmd(false, true, fn, addr, len + padding);
+    if (fn == WLAN_FUNCTION) {
+        logic_debug_set(pin_WIFI_RX, 1);
+    }
+    int ret = cyw43_spi_transfer(self, NULL, 4, (uint8_t *)&self->spi_header[padding > 0 ? 0 : 1], aligned_len + 4 + padding);
+    if (fn == WLAN_FUNCTION) {
+        logic_debug_set(pin_WIFI_RX, 0);
+    }
+    if (ret != 0) {
+        printf("cyw43_read_bytes error %d", ret);
+        return ret;
+    }
+    if (buf != self->spid_buf) { // avoid a copy in the usual case just to add the header
+        memcpy(buf, self->spid_buf, len);
+    }
+    return 0;
+}
+
+// See whd_bus_spi_transfer_bytes
+// Note, uses spid_buf if src isn't using it already
+// Apart from firmware download this appears to only be used for wlan functions?
+int cyw43_write_bytes(cyw43_int_t *self, uint32_t fn, uint32_t addr, size_t len, const uint8_t *src) {
+    assert(fn != BACKPLANE_FUNCTION || (len <= 64 && (addr + len) <= 0x8000));
+    size_t aligned_len = (len + 3) & ~3u;
+    assert(aligned_len > 0 && aligned_len <= 0x7f8);
+    if (fn == WLAN_FUNCTION) {
+        // Wait for FIFO to be ready to accept data
+        int f2_ready_attempts = 1000;
+        while (f2_ready_attempts-- > 0) {
+            uint32_t bus_status = cyw43_read_reg_u32(self, BUS_FUNCTION, SPI_STATUS_REGISTER);
+            if (bus_status & STATUS_F2_RX_READY) {
+                logic_debug_set(pin_F2_RX_READY_WAIT, 0);
+                break;
+            } else {
+                logic_debug_set(pin_F2_RX_READY_WAIT, 1);
+            }
+        }
+        if (f2_ready_attempts <= 0) {
+            printf("F2 not ready\n");
+            return CYW43_FAIL_FAST_CHECK(-CYW43_EIO);
+        }
+    }
+    if (src == self->spid_buf) { // avoid a copy in the usual case just to add the header
+        self->spi_header[1] = make_cmd(true, true, fn, addr, len);
+        logic_debug_set(pin_WIFI_TX, 1);
+        int res = cyw43_spi_transfer(self, (uint8_t *)&self->spi_header[1], aligned_len + 4, NULL, 0);
+        logic_debug_set(pin_WIFI_TX, 0);
+        return res;
+    } else {
+        // todo: would be nice to get rid of this. Only used for firmware download?
+        assert(src < self->spid_buf || src >= (self->spid_buf + sizeof(self->spid_buf)));
+        self->spi_header[1] = make_cmd(true, true, fn, addr, len);
+        memcpy(self->spid_buf, src, len);
+        return cyw43_spi_transfer(self, (uint8_t *)&self->spi_header[1], aligned_len + 4, NULL, 0);
+    }
+}
+#endif
diff --git a/src/rp2_common/cyw43_driver/cyw43_bus_pio_spi.pio b/src/rp2_common/cyw43_driver/cyw43_bus_pio_spi.pio
new file mode 100644
index 0000000..ea0d195
--- /dev/null
+++ b/src/rp2_common/cyw43_driver/cyw43_bus_pio_spi.pio
@@ -0,0 +1,61 @@
+;
+; Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
+;
+; SPDX-License-Identifier: BSD-3-Clause
+;
+
+.program spi_gap0_sample1
+.side_set 1
+
+; always transmit multiple of 32 bytes
+lp: out pins, 1             side 0
+    jmp x-- lp              side 1
+public lp1_end:
+    set pindirs, 0          side 0
+lp2:
+    in pins, 1              side 1
+    jmp y-- lp2             side 0
+public end:
+
+.program spi_gap01_sample0
+.side_set 1
+
+; always transmit multiple of 32 bytes
+lp: out pins, 1             side 0
+    jmp x-- lp              side 1
+public lp1_end:
+    set pindirs, 0          side 0
+    nop                     side 1
+lp2:
+    in pins, 1              side 0
+    jmp y-- lp2             side 1
+public end:
+
+.program spi_gap010_sample1
+.side_set 1
+
+; always transmit multiple of 32 bytes
+lp: out pins, 1             side 0
+    jmp x-- lp              side 1
+public lp1_end:
+    set pindirs, 0          side 0
+    nop                     side 1
+    nop                     side 0
+lp2:
+    in pins, 1              side 1
+    jmp y-- lp2             side 0
+public end:
+
+.program spi_gap0_sample1_regular
+.side_set 1
+
+; always transmit multiple of 32 bytes
+lp: out pins, 1             side 0
+    jmp x-- lp              side 1
+public lp1_end:
+    set pindirs, 0          side 0
+lp2:
+    in pins, 1              side 1
+    jmp y-- lp2             side 0
+public end:
+
diff --git a/src/rp2_common/hardware_dma/dma.c b/src/rp2_common/hardware_dma/dma.c
index 90fde06..f142b53 100644
--- a/src/rp2_common/hardware_dma/dma.c
+++ b/src/rp2_common/hardware_dma/dma.c
@@ -36,6 +36,12 @@
     hw_claim_clear((uint8_t *) &_claimed, channel);
 }
 
+void dma_unclaim_mask(uint32_t mask) {
+    for(uint i = 0; mask; i++, mask >>= 1u) {
+        if (mask & 1u) dma_channel_unclaim(i);
+    }
+}
+
 int dma_claim_unused_channel(bool required) {
     return hw_claim_unused_from_range((uint8_t*)&_claimed, required, 0, NUM_DMA_CHANNELS-1, "No DMA channels are available");
 }
diff --git a/src/rp2_common/hardware_dma/include/hardware/dma.h b/src/rp2_common/hardware_dma/include/hardware/dma.h
index 8ebd2e5..ec73564 100644
--- a/src/rp2_common/hardware_dma/include/hardware/dma.h
+++ b/src/rp2_common/hardware_dma/include/hardware/dma.h
@@ -88,12 +88,17 @@
 /*! \brief Mark a dma channel as no longer used
  *  \ingroup hardware_dma
  *
- * Method for cooperative claiming of hardware.
- *
  * \param channel the dma channel to release
  */
 void dma_channel_unclaim(uint channel);
 
+/*! \brief Mark multiple dma channels as no longer used
+ *  \ingroup hardware_dma
+ *
+ * \param channel_mask Bitfield of all channels to unclaim (bit 0 == channel 0, bit 1 == channel 1 etc)
+ */
+void dma_unclaim_mask(uint32_t channel_mask);
+
 /*! \brief Claim a free dma channel
  *  \ingroup hardware_dma
  *
diff --git a/src/rp2_common/pico_cyw43_arch/CMakeLists.txt b/src/rp2_common/pico_cyw43_arch/CMakeLists.txt
new file mode 100644
index 0000000..ede03f1
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/CMakeLists.txt
@@ -0,0 +1,66 @@
+if (PICO_CYW43_SUPPORTED) # set by BOARD=pico-w
+    if (TARGET cyw43_driver_picow)
+        message("Enabling build support for Pico W wireless.")
+
+        pico_add_impl_library(pico_cyw43_arch)
+        target_sources(pico_cyw43_arch INTERFACE
+                ${CMAKE_CURRENT_LIST_DIR}/cyw43_arch.c
+                ${CMAKE_CURRENT_LIST_DIR}/cyw43_arch_poll.c
+                ${CMAKE_CURRENT_LIST_DIR}/cyw43_arch_threadsafe_background.c
+                ${CMAKE_CURRENT_LIST_DIR}/cyw43_arch_freertos.c
+                )
+
+        target_include_directories(pico_cyw43_arch INTERFACE
+                ${CMAKE_CURRENT_LIST_DIR}/include)
+
+        target_link_libraries(pico_cyw43_arch INTERFACE
+                pico_unique_id
+                cyw43_driver_picow)
+
+        if (NOT TARGET pico_lwip)
+            message(WARNING "lwIP is not available; Full Pico W wireless support will be unavailable too")
+        else()
+            add_library(pico_cyw43_arch_lwip_poll INTERFACE)
+            target_link_libraries(pico_cyw43_arch_lwip_poll INTERFACE
+                    pico_cyw43_arch
+                    pico_lwip
+                    pico_lwip_nosys)
+            target_compile_definitions(pico_cyw43_arch_lwip_poll INTERFACE
+                    CYW43_LWIP=1
+                    PICO_CYW43_ARCH_POLL=1
+                    )
+
+            add_library(pico_cyw43_arch_lwip_threadsafe_background INTERFACE)
+            target_link_libraries(pico_cyw43_arch_lwip_threadsafe_background INTERFACE
+                    pico_cyw43_arch
+                    pico_lwip
+                    pico_lwip_nosys)
+            target_compile_definitions(pico_cyw43_arch_lwip_threadsafe_background INTERFACE
+                    CYW43_LWIP=1
+                    PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1
+                    )
+
+            add_library(pico_cyw43_arch_lwip_sys_freertos INTERFACE)
+            target_link_libraries(pico_cyw43_arch_lwip_sys_freertos INTERFACE
+                    pico_cyw43_arch
+                    pico_lwip
+                    pico_lwip_contrib_freertos)
+            target_compile_definitions(pico_cyw43_arch_lwip_sys_freertos INTERFACE
+                    CYW43_LWIP=1
+                    LWIP_PROVIDE_ERRNO=1
+                    PICO_CYW43_ARCH_FREERTOS=1
+                    )
+        endif()
+
+        add_library(pico_cyw43_arch_none INTERFACE)
+        target_link_libraries(pico_cyw43_arch_none INTERFACE pico_cyw43_arch)
+        target_compile_definitions(pico_cyw43_arch_none INTERFACE
+                CYW43_LWIP=0
+                PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1 # none still uses threadsafe_background to make gpio use easy
+                )
+    endif()
+endif()
+
+if (PICO_CYW43_DRIVER_PATH AND EXISTS "${PICO_CYW43_DRIVER_PATH}")
+    pico_add_doxygen(${PICO_CYW43_DRIVER_PATH}/src)
+endif()
\ No newline at end of file
diff --git a/src/rp2_common/pico_cyw43_arch/cyw43_arch.c b/src/rp2_common/pico_cyw43_arch/cyw43_arch.c
new file mode 100644
index 0000000..c328d6e
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/cyw43_arch.c
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "pico/unique_id.h"
+#include "cyw43.h"
+#include "pico/cyw43_arch.h"
+#include "cyw43_ll.h"
+#include "cyw43_stats.h"
+
+#if CYW43_ARCH_DEBUG_ENABLED
+#define CYW43_ARCH_DEBUG(...) printf(__VA_ARGS__)
+#else
+#define CYW43_ARCH_DEBUG(...) ((void)0)
+#endif
+
+static uint32_t country_code = PICO_CYW43_ARCH_DEFAULT_COUNTRY_CODE;
+
+void cyw43_arch_enable_sta_mode() {
+    assert(cyw43_is_initialized(&cyw43_state));
+    cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_STA, true, cyw43_arch_get_country_code());
+}
+
+void cyw43_arch_enable_ap_mode(const char *ssid, const char *password, uint32_t auth) {
+    assert(cyw43_is_initialized(&cyw43_state));
+    cyw43_wifi_ap_set_ssid(&cyw43_state, strlen(ssid), (const uint8_t *) ssid);
+    if (password) {
+        cyw43_wifi_ap_set_password(&cyw43_state, strlen(password), (const uint8_t *) password);
+        cyw43_wifi_ap_set_auth(&cyw43_state, auth);
+    } else {
+        cyw43_wifi_ap_set_auth(&cyw43_state, CYW43_AUTH_OPEN);
+    }
+    cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, true, cyw43_arch_get_country_code());
+}
+
+#if CYW43_ARCH_DEBUG_ENABLED
+// Return a string for the wireless state
+static const char* status_name(int status)
+{
+    switch (status) {
+    case CYW43_LINK_DOWN:
+        return "link down";
+    case CYW43_LINK_JOIN:
+        return "joining";
+    case CYW43_LINK_NOIP:
+        return "no ip";
+    case CYW43_LINK_UP:
+        return "link up";
+    case CYW43_LINK_FAIL:
+        return "link fail";
+    case CYW43_LINK_NONET:
+        return "network fail";
+    case CYW43_LINK_BADAUTH:
+        return "bad auth";
+    }
+    return "unknown";
+}
+#endif
+
+int cyw43_arch_wifi_connect_async(const char *ssid, const char *pw, uint32_t auth) {
+    if (!pw) auth = CYW43_AUTH_OPEN;
+    // Connect to wireless
+    return cyw43_wifi_join(&cyw43_state, strlen(ssid), (const uint8_t *)ssid, pw ? strlen(pw) : 0, (const uint8_t *)pw, auth, NULL, CYW43_ITF_STA);
+}
+
+// Connect to wireless, return with success when an IP address has been assigned
+int cyw43_arch_wifi_connect_until(const char *ssid, const char *pw, uint32_t auth, absolute_time_t until) {
+    int err = cyw43_arch_wifi_connect_async(ssid, pw, auth);
+    if (err) return err;
+
+    int status = CYW43_LINK_UP + 1;
+    while(status >= 0 && status != CYW43_LINK_UP) {
+        int new_status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA);
+        if (new_status != status) {
+            status = new_status;
+            CYW43_ARCH_DEBUG("connect status: %s\n", status_name(status));
+        }
+        // in case polling is required
+        cyw43_arch_poll();
+        best_effort_wfe_or_timeout(until);
+        if (time_reached(until)) {
+            return PICO_ERROR_TIMEOUT;
+        }
+    }
+    return status == CYW43_LINK_UP ? 0 : status;
+}
+
+int cyw43_arch_wifi_connect_blocking(const char *ssid, const char *pw, uint32_t auth) {
+    return cyw43_arch_wifi_connect_until(ssid, pw, auth, at_the_end_of_time);
+}
+
+int cyw43_arch_wifi_connect_timeout_ms(const char *ssid, const char *pw, uint32_t auth, uint32_t timeout_ms) {
+    return cyw43_arch_wifi_connect_until(ssid, pw, auth, make_timeout_time_ms(timeout_ms));
+}
+
+// todo maybe add an #ifdef in cyw43_driver
+uint32_t storage_read_blocks(__unused uint8_t *dest, __unused uint32_t block_num, __unused uint32_t num_blocks) {
+    // shouldn't be used
+    panic_unsupported();
+}
+
+// Generate a mac address if one is not set in otp
+void cyw43_hal_generate_laa_mac(__unused int idx, uint8_t buf[6]) {
+    CYW43_DEBUG("Warning. No mac in otp. Generating mac from board id\n");
+    pico_unique_board_id_t board_id;
+    pico_get_unique_board_id(&board_id);
+    memcpy(buf, &board_id.id[2], 6);
+    buf[0] &= (uint8_t)~0x1; // unicast
+    buf[0] |= 0x2; // locally administered
+}
+
+// Return mac address
+void cyw43_hal_get_mac(__unused int idx, uint8_t buf[6]) {
+    // The mac should come from cyw43 otp.
+    // This is loaded into the state after the driver is initialised
+    // cyw43_hal_generate_laa_mac is called by the driver to generate a mac if otp is not set
+    memcpy(buf, cyw43_state.mac, 6);
+}
+
+uint32_t cyw43_arch_get_country_code(void) {
+    return country_code;
+}
+
+int cyw43_arch_init_with_country(uint32_t country) {
+    country_code = country;
+    return cyw43_arch_init();
+}
+
+void cyw43_arch_gpio_put(uint wl_gpio, bool value) {
+    invalid_params_if(CYW43_ARCH, wl_gpio >= CYW43_WL_GPIO_COUNT);
+    cyw43_gpio_set(&cyw43_state, (int)wl_gpio, value);
+}
+
+bool cyw43_arch_gpio_get(uint wl_gpio) {
+    invalid_params_if(CYW43_ARCH, wl_gpio >= CYW43_WL_GPIO_COUNT);
+    bool value = false;
+    cyw43_gpio_get(&cyw43_state, (int)wl_gpio, &value);
+    return value;
+}
diff --git a/src/rp2_common/pico_cyw43_arch/cyw43_arch_freertos.c b/src/rp2_common/pico_cyw43_arch/cyw43_arch_freertos.c
new file mode 100644
index 0000000..88dde6e
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/cyw43_arch_freertos.c
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include <stdio.h>
+
+#include "pico/cyw43_arch.h"
+
+#include "hardware/gpio.h"
+#include "hardware/irq.h"
+#include "hardware/sync.h"
+
+#include "cyw43_stats.h"
+
+#if CYW43_LWIP
+#include <lwip/tcpip.h>
+#endif
+
+#if PICO_CYW43_ARCH_FREERTOS
+
+// FreeRTOS includes
+#include "FreeRTOS.h"
+#include "timers.h"
+#include "semphr.h"
+
+#if NO_SYS
+#error example_cyw43_arch_frertos_sys requires NO_SYS=0
+#endif
+
+#ifndef CYW43_TASK_PRIORITY
+#define CYW43_TASK_PRIORITY ( tskIDLE_PRIORITY + 4)
+#endif
+
+#ifndef CYW43_SLEEP_CHECK_MS
+#define CYW43_SLEEP_CHECK_MS 50 // How often to run lwip callback
+#endif
+
+#define CYW43_GPIO_IRQ_HANDLER_PRIORITY 0x40
+
+static void signal_cyw43_task(void);
+
+#if !LWIP_TCPIP_CORE_LOCKING_INPUT
+static SemaphoreHandle_t cyw43_mutex;
+#endif
+static TimerHandle_t timer_handle;
+static TaskHandle_t cyw43_task_handle;
+static volatile bool cyw43_task_should_exit;
+static SemaphoreHandle_t cyw43_worker_ran_sem;
+static uint8_t cyw43_core_num;
+
+// Called in low priority pendsv interrupt only to do lwip processing and check cyw43 sleep
+static void periodic_worker(__unused TimerHandle_t handle)
+{
+#if CYW43_USE_STATS
+    static uint32_t counter;
+    if (counter++ % (30000 / LWIP_SYS_CHECK_MS) == 0) {
+        cyw43_dump_stats();
+    }
+#endif
+
+    CYW43_STAT_INC(LWIP_RUN_COUNT);
+    if (cyw43_poll) {
+        if (cyw43_sleep > 0) {
+            if (--cyw43_sleep == 0) {
+                signal_cyw43_task();
+            }
+        }
+    }
+}
+
+void cyw43_await_background_or_timeout_us(uint32_t timeout_us) {
+    // if we are called from within an IRQ, then don't wait (we are only ever called in a polling loop)
+    assert(!portCHECK_IF_IN_ISR());
+    xSemaphoreTake(cyw43_worker_ran_sem, pdMS_TO_TICKS(timeout_us / 1000));
+}
+
+// GPIO interrupt handler to tell us there's cyw43 has work to do
+static void gpio_irq_handler(void)
+{
+    uint32_t events = gpio_get_irq_event_mask(CYW43_PIN_WL_HOST_WAKE);
+    if (events & GPIO_IRQ_LEVEL_HIGH) {
+        // As we use a high level interrupt, it will go off forever until it's serviced
+        // So disable the interrupt until this is done. It's re-enabled again by CYW43_POST_POLL_HOOK
+        // which is called at the end of cyw43_poll_func
+        gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, false);
+        signal_cyw43_task();
+        CYW43_STAT_INC(IRQ_COUNT);
+    }
+}
+
+// Low priority interrupt handler to perform background processing
+static void cyw43_task(__unused void *param) {
+    do {
+        ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
+        if (cyw43_task_should_exit) break;
+        cyw43_thread_enter();
+        if (cyw43_poll) cyw43_poll();
+        cyw43_thread_exit();
+        xSemaphoreGive(cyw43_worker_ran_sem);
+        __sev(); // it is possible regular code is waiting on a WFE on the other core
+    } while (true);
+}
+
+static void tcpip_init_done(void *param) {
+    xSemaphoreGive((SemaphoreHandle_t)param);
+}
+
+int cyw43_arch_init(void) {
+    cyw43_core_num = get_core_num();
+#if configUSE_CORE_AFFINITY && configNUM_CORES > 1
+    TaskHandle_t task_handle = xTaskGetCurrentTaskHandle();
+    UBaseType_t affinity = vTaskCoreAffinityGet(task_handle);
+    // we must bind the main task to one core during init
+    vTaskCoreAffinitySet(task_handle, 1 << portGET_CORE_ID());
+#endif
+#if !LWIP_TCPIP_CORE_LOCKING_INPUT
+    cyw43_mutex = xSemaphoreCreateRecursiveMutex();
+#endif
+    cyw43_init(&cyw43_state);
+    cyw43_worker_ran_sem = xSemaphoreCreateBinary();
+
+#if CYW43_LWIP
+    SemaphoreHandle_t init_sem = xSemaphoreCreateBinary();
+    tcpip_init(tcpip_init_done, init_sem);
+    xSemaphoreTake(init_sem, portMAX_DELAY);
+#endif
+
+    timer_handle = xTimerCreate(    "cyw43_sleep_timer",       // Just a text name, not used by the kernel.
+                                    pdMS_TO_TICKS(CYW43_SLEEP_CHECK_MS),
+                                    pdTRUE,        // The timers will auto-reload themselves when they expire.
+                                    NULL,
+                                    periodic_worker);
+
+    if (!timer_handle) {
+        return PICO_ERROR_GENERIC;
+    }
+
+    gpio_add_raw_irq_handler_with_order_priority(IO_IRQ_BANK0, gpio_irq_handler, CYW43_GPIO_IRQ_HANDLER_PRIORITY);
+    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, true);
+    irq_set_enabled(IO_IRQ_BANK0, true);
+
+    cyw43_task_should_exit = false;
+    xTaskCreate(cyw43_task, "CYW43 Worker", configMINIMAL_STACK_SIZE, NULL, CYW43_TASK_PRIORITY, &cyw43_task_handle);
+#if configUSE_CORE_AFFINITY && configNUM_CORES > 1
+    // the cyw43 task mus tbe on the same core so it can restore IRQs
+    vTaskCoreAffinitySet(cyw43_task_handle, 1 << portGET_CORE_ID());
+#endif
+
+#if configUSE_CORE_AFFINITY && configNUM_CORES > 1
+    vTaskCoreAffinitySet(task_handle, affinity);
+#endif
+
+    return PICO_OK;
+}
+
+void cyw43_arch_deinit(void) {
+    assert(cyw43_core_num == get_core_num());
+    if (timer_handle) {
+        xTimerDelete(timer_handle, 0);
+        timer_handle = 0;
+    }
+    if (cyw43_task_handle) {
+        cyw43_task_should_exit = true;
+        signal_cyw43_task();
+    }
+    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, false);
+    gpio_remove_raw_irq_handler(IO_IRQ_BANK0, gpio_irq_handler);
+}
+
+void cyw43_post_poll_hook(void) {
+    assert(cyw43_core_num == get_core_num());
+    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, true);
+}
+
+// This is called in the gpio and low_prio_irq interrupts and on either core
+static void signal_cyw43_task(void) {
+    if (cyw43_task_handle) {
+        if (portCHECK_IF_IN_ISR()) {
+            vTaskNotifyGiveFromISR(cyw43_task_handle, NULL);
+        } else {
+            xTaskNotifyGive(cyw43_task_handle);
+        }
+    }
+}
+
+void cyw43_schedule_internal_poll_dispatch(void (*func)(void)) {
+    assert(func == cyw43_poll);
+    signal_cyw43_task();
+}
+
+static int nesting;
+// Prevent background processing in pensv and access by the other core
+// These methods are called in pensv context and on either core
+// They can be called recursively
+void cyw43_thread_enter(void) {
+    // Lock the other core and stop low_prio_irq running
+    assert(!portCHECK_IF_IN_ISR());
+#if LWIP_TCPIP_CORE_LOCKING_INPUT
+    // we must share their mutex otherwise we can get deadlocks with two different recursive mutexes
+    LOCK_TCPIP_CORE();
+#else
+    xSemaphoreTakeRecursive(cyw43_mutex, portMAX_DELAY);
+#endif
+    nesting++;
+}
+
+#ifndef NDEBUG
+void cyw43_thread_lock_check(void) {
+    // Lock the other core and stop low_prio_irq running
+#if LWIP_TCPIP_CORE_LOCKING_INPUT
+    assert(xSemaphoreGetMutexHolder(lock_tcpip_core.mut) == xTaskGetCurrentTaskHandle());
+#else
+    assert(xSemaphoreGetMutexHolder(cyw43_mutex) == xTaskGetCurrentTaskHandle());
+#endif
+}
+#endif
+
+// Re-enable background processing
+void cyw43_thread_exit(void) {
+    // Run low_prio_irq if needed
+    --nesting;
+#if LWIP_TCPIP_CORE_LOCKING_INPUT
+    // we must share their mutex otherwise we can get deadlocks with two different recursive mutexes
+    UNLOCK_TCPIP_CORE();
+#else
+    xSemaphoreGiveRecursive(cyw43_mutex);
+#endif
+
+    if (!nesting && cyw43_task_handle != xTaskGetCurrentTaskHandle())
+        signal_cyw43_task();
+}
+
+void cyw43_delay_ms(uint32_t ms) {
+    assert(!portCHECK_IF_IN_ISR());
+    vTaskDelay(pdMS_TO_TICKS(ms));
+}
+
+void cyw43_delay_us(uint32_t us) {
+    if (us >= 1000) {
+        cyw43_delay_ms(us / 1000);
+    } else {
+        vTaskDelay(1);
+    }
+}
+
+void cyw43_arch_poll() {
+}
+
+#endif
\ No newline at end of file
diff --git a/src/rp2_common/pico_cyw43_arch/cyw43_arch_poll.c b/src/rp2_common/pico_cyw43_arch/cyw43_arch_poll.c
new file mode 100644
index 0000000..e71f86a
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/cyw43_arch_poll.c
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include <stdio.h>
+#include "hardware/gpio.h"
+#include "hardware/irq.h"
+#include "pico/sem.h"
+#include "pico/cyw43_arch.h"
+#include "cyw43_stats.h"
+
+#if PICO_CYW43_ARCH_POLL
+#include <lwip/init.h>
+#include "lwip/timeouts.h"
+
+#if CYW43_LWIP && !NO_SYS
+#error PICO_CYW43_ARCH_POLL requires lwIP NO_SYS=1
+#endif
+
+#define CYW43_GPIO_IRQ_HANDLER_PRIORITY 0x40
+
+#ifndef NDEBUG
+uint8_t cyw43_core_num;
+#endif
+
+bool cyw43_poll_required;
+
+// GPIO interrupt handler to tell us there's cyw43 has work to do
+static void gpio_irq_handler(void)
+{
+    uint32_t events = gpio_get_irq_event_mask(CYW43_PIN_WL_HOST_WAKE);
+    if (events & GPIO_IRQ_LEVEL_HIGH) {
+        // As we use a high level interrupt, it will go off forever until it's serviced
+        // So disable the interrupt until this is done. It's re-enabled again by CYW43_POST_POLL_HOOK
+        // which is called at the end of cyw43_poll_func
+        gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, false);
+        // also clear the force bit which we use to programmatically cause this handler to fire (on the right core)
+        io_irq_ctrl_hw_t *irq_ctrl_base = get_core_num() ?
+                                          &iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl;
+        hw_clear_bits(&irq_ctrl_base->intf[CYW43_PIN_WL_HOST_WAKE/8], GPIO_IRQ_LEVEL_HIGH << (4 * (CYW43_PIN_WL_HOST_WAKE & 7)));
+        cyw43_schedule_internal_poll_dispatch(cyw43_poll);
+        CYW43_STAT_INC(IRQ_COUNT);
+    }
+}
+
+void cyw43_post_poll_hook(void) {
+    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, true);
+}
+
+int cyw43_arch_init(void) {
+#ifndef NDEBUG
+    cyw43_core_num = (uint8_t)get_core_num();
+#endif
+    cyw43_init(&cyw43_state);
+    static bool done_lwip_init;
+    if (!done_lwip_init) {
+        lwip_init();
+        done_lwip_init = true;
+    }
+    gpio_add_raw_irq_handler_with_order_priority(IO_IRQ_BANK0, gpio_irq_handler, CYW43_GPIO_IRQ_HANDLER_PRIORITY);
+    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, true);
+    irq_set_enabled(IO_IRQ_BANK0, true);
+    return 0;
+}
+
+void cyw43_arch_deinit(void) {
+    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, false);
+    gpio_remove_raw_irq_handler(IO_IRQ_BANK0, gpio_irq_handler);
+    cyw43_deinit(&cyw43_state);
+}
+
+
+void cyw43_schedule_internal_poll_dispatch(__unused void (*func)(void)) {
+    cyw43_poll_required = true;
+}
+
+void cyw43_arch_poll(void)
+{
+    CYW43_STAT_INC(LWIP_RUN_COUNT);
+    sys_check_timeouts();
+    if (cyw43_poll) {
+        if (cyw43_sleep > 0) {
+            // todo check this; but we don't want to advance too quickly
+            static absolute_time_t last_poll_time;
+            absolute_time_t current = get_absolute_time();
+            if (absolute_time_diff_us(last_poll_time, current) > 1000) {
+                if (--cyw43_sleep == 0) {
+                    cyw43_poll_required = 1;
+                }
+                last_poll_time = current;
+            }
+        }
+        // todo graham i removed this because otherwise polling can do nothing during connect.
+        //  in the polling only case, the caller is responsible for throttling how often they call anyway.
+        //  The alternative would be to have the call to this function from the init set the poll_required flag first
+//        if (cyw43_poll_required) {
+            cyw43_poll();
+//            cyw43_poll_required = false;
+//        }
+    }
+}
+
+#ifndef NDEBUG
+void cyw43_thread_check() {
+    if (__get_current_exception() || get_core_num() != cyw43_core_num) {
+        panic("cyw43_thread_lock_check failed");
+    }
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/src/rp2_common/pico_cyw43_arch/cyw43_arch_threadsafe_background.c b/src/rp2_common/pico_cyw43_arch/cyw43_arch_threadsafe_background.c
new file mode 100644
index 0000000..a3dde74
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/cyw43_arch_threadsafe_background.c
@@ -0,0 +1,319 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include <stdio.h>
+
+#include "pico/cyw43_arch.h"
+#include "pico/mutex.h"
+#include "pico/sem.h"
+
+#include "hardware/gpio.h"
+#include "hardware/irq.h"
+
+#include "cyw43_stats.h"
+
+#if CYW43_LWIP
+#include <lwip/init.h>
+#include "lwip/timeouts.h"
+#endif
+
+// note same code
+#if PICO_CYW43_ARCH_THREADSAFE_BACKGROUND
+
+#if PICO_CYW43_ARCH_THREADSAFE_BACKGROUND && CYW43_LWIP && !NO_SYS
+#error PICO_CYW43_ARCH_THREADSAFE_BACKGROUND requires lwIP NO_SYS=1
+#endif
+#if PICO_CYW43_ARCH_THREADSAFE_BACKGROUND && CYW43_LWIP && MEM_LIBC_MALLOC
+#error MEM_LIBC_MALLOC is incompatible with PICO_CYW43_ARCH_THREADSAFE_BACKGROUND
+#endif
+// todo right now we are now always doing a cyw43_dispatch along with a lwip one when hopping cores in low_prio_irq_schedule_dispatch
+
+#ifndef CYW43_SLEEP_CHECK_MS
+#define CYW43_SLEEP_CHECK_MS 50 // How often to run lwip callback
+#endif
+static alarm_id_t periodic_alarm = -1;
+
+static inline uint recursive_mutex_enter_count(recursive_mutex_t *mutex) {
+    return mutex->enter_count;
+}
+
+static inline lock_owner_id_t recursive_mutex_owner(recursive_mutex_t *mutex) {
+    return mutex->owner;
+}
+
+#define CYW43_GPIO_IRQ_HANDLER_PRIORITY 0x40
+
+enum {
+    CYW43_DISPATCH_SLOT_CYW43 = 0,
+    CYW43_DISPATCH_SLOT_ADAPTER,
+    CYW43_DISPATCH_SLOT_ENUM_COUNT
+};
+#ifndef CYW43_DISPATCH_SLOT_COUNT
+#define CYW43_DISPATCH_SLOT_COUNT CYW43_DISPATCH_SLOT_ENUM_COUNT
+#endif
+
+typedef void (*low_prio_irq_dispatch_t)(void);
+static void low_prio_irq_schedule_dispatch(size_t slot, low_prio_irq_dispatch_t f);
+
+static uint8_t cyw43_core_num;
+#ifndef NDEBUG
+static bool in_low_priority_irq;
+#endif
+static uint8_t low_priority_irq_num;
+static bool low_priority_irq_missed;
+static low_prio_irq_dispatch_t low_priority_irq_dispatch_slots[CYW43_DISPATCH_SLOT_COUNT];
+static recursive_mutex_t cyw43_mutex;
+semaphore_t cyw43_irq_sem;
+
+// Called in low priority pendsv interrupt only to do lwip processing and check cyw43 sleep
+static void periodic_worker(void)
+{
+#if CYW43_USE_STATS
+    static uint32_t counter;
+    if (counter++ % (30000 / LWIP_SYS_CHECK_MS) == 0) {
+        cyw43_dump_stats();
+    }
+#endif
+
+    CYW43_STAT_INC(LWIP_RUN_COUNT);
+#if CYW43_LWIP
+    sys_check_timeouts();
+#endif
+    if (cyw43_poll) {
+        if (cyw43_sleep > 0) {
+            if (--cyw43_sleep == 0) {
+                low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, cyw43_poll);
+            }
+        }
+    }
+}
+
+// Regular callback to get lwip to check for timeouts
+static int64_t periodic_alarm_handler(__unused alarm_id_t id, __unused void *user_data)
+{
+    // Do lwip processing in low priority pendsv interrupt
+    low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_ADAPTER, periodic_worker);
+    return CYW43_SLEEP_CHECK_MS * 1000;
+}
+
+void cyw43_await_background_or_timeout_us(uint32_t timeout_us) {
+    // if we are called from within an IRQ, then don't wait (we are only ever called in a polling loop)
+    if (!__get_current_exception()) {
+        sem_acquire_timeout_us(&cyw43_irq_sem, timeout_us);
+    }
+}
+
+// GPIO interrupt handler to tell us there's cyw43 has work to do
+static void gpio_irq_handler(void)
+{
+    uint32_t events = gpio_get_irq_event_mask(CYW43_PIN_WL_HOST_WAKE);
+    if (events & GPIO_IRQ_LEVEL_HIGH) {
+        // As we use a high level interrupt, it will go off forever until it's serviced
+        // So disable the interrupt until this is done. It's re-enabled again by CYW43_POST_POLL_HOOK
+        // which is called at the end of cyw43_poll_func
+        gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, false);
+        // also clear the force bit which we use to progratically cause this handler to fire (on the right core)
+        io_irq_ctrl_hw_t *irq_ctrl_base = get_core_num() ?
+                                          &iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl;
+        hw_clear_bits(&irq_ctrl_base->intf[CYW43_PIN_WL_HOST_WAKE/8], GPIO_IRQ_LEVEL_HIGH << (4 * (CYW43_PIN_WL_HOST_WAKE & 7)));
+        low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, cyw43_poll);
+        CYW43_STAT_INC(IRQ_COUNT);
+    }
+}
+
+// Low priority interrupt handler to perform background processing
+static void low_priority_irq_handler(void) {
+    assert(cyw43_core_num == get_core_num());
+    if (recursive_mutex_try_enter(&cyw43_mutex, NULL)) {
+        if (recursive_mutex_enter_count(&cyw43_mutex) != 1) {
+            low_priority_irq_missed = true;
+            CYW43_STAT_INC(PENDSV_DISABLED_COUNT);
+        } else {
+            CYW43_STAT_INC(PENDSV_RUN_COUNT);
+#ifndef NDEBUG
+            in_low_priority_irq = true;
+#endif
+            for (size_t i = 0; i < count_of(low_priority_irq_dispatch_slots); i++) {
+                if (low_priority_irq_dispatch_slots[i] != NULL) {
+                    low_prio_irq_dispatch_t f = low_priority_irq_dispatch_slots[i];
+                    low_priority_irq_dispatch_slots[i] = NULL;
+                    f();
+                }
+            }
+#ifndef NDEBUG
+            in_low_priority_irq = false;
+#endif
+        }
+        recursive_mutex_exit(&cyw43_mutex);
+    } else {
+        CYW43_STAT_INC(PENDSV_DISABLED_COUNT);
+        low_priority_irq_missed = true;
+    }
+    sem_release(&cyw43_irq_sem);
+}
+
+static bool low_prio_irq_init(uint8_t priority) {
+    assert(get_core_num() == cyw43_core_num);
+    int irq = user_irq_claim_unused(false);
+    if (irq < 0) return false;
+    low_priority_irq_num = (uint8_t) irq;
+    irq_set_exclusive_handler(low_priority_irq_num, low_priority_irq_handler);
+    irq_set_enabled(low_priority_irq_num, true);
+    irq_set_priority(low_priority_irq_num, priority);
+    return true;
+}
+
+static void low_prio_irq_deinit(void) {
+    if (low_priority_irq_num > 0) {
+        irq_set_enabled(low_priority_irq_num, false);
+        irq_remove_handler(low_priority_irq_num, low_priority_irq_handler);
+        user_irq_unclaim(low_priority_irq_num);
+        low_priority_irq_num = 0;
+    }
+}
+
+int cyw43_arch_init(void) {
+    cyw43_core_num = get_core_num();
+    recursive_mutex_init(&cyw43_mutex);
+    cyw43_init(&cyw43_state);
+    sem_init(&cyw43_irq_sem, 0, 1);
+
+    // Start regular lwip callback to handle timeouts
+    periodic_alarm = add_alarm_in_us(CYW43_SLEEP_CHECK_MS * 1000, periodic_alarm_handler, NULL, true);
+    if (periodic_alarm < 0) {
+        return PICO_ERROR_GENERIC;
+    }
+
+    gpio_add_raw_irq_handler_with_order_priority(IO_IRQ_BANK0, gpio_irq_handler, CYW43_GPIO_IRQ_HANDLER_PRIORITY);
+    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, true);
+    irq_set_enabled(IO_IRQ_BANK0, true);
+
+#if CYW43_LWIP
+    lwip_init();
+#endif
+    // start low priority handler (no background work is done before this)
+    bool ok = low_prio_irq_init(PICO_LOWEST_IRQ_PRIORITY);
+    if (!ok) {
+        cyw43_arch_deinit();
+        return PICO_ERROR_GENERIC;
+    }
+    return PICO_OK;
+}
+
+void cyw43_arch_deinit(void) {
+    if (periodic_alarm >= 0) {
+        cancel_alarm(periodic_alarm);
+        periodic_alarm = -1;
+    }
+    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, false);
+    gpio_remove_raw_irq_handler(IO_IRQ_BANK0, gpio_irq_handler);
+    low_prio_irq_deinit();
+}
+
+void cyw43_post_poll_hook(void) {
+    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, true);
+}
+
+// This is called in the gpio and low_prio_irq interrupts and on either core
+static void low_prio_irq_schedule_dispatch(size_t slot, low_prio_irq_dispatch_t f) {
+    assert(slot < count_of(low_priority_irq_dispatch_slots));
+    low_priority_irq_dispatch_slots[slot] = f;
+    if (cyw43_core_num == get_core_num()) {
+        //on same core, can dispatch directly
+        irq_set_pending(low_priority_irq_num);
+    } else {
+        // on wrong core, so force via GPIO IRQ which itself calls this method for the CYW43 slot.
+        // since the CYW43 slot always uses the same function, this is fine with the addition of an
+        // extra (but harmless) CYW43 slot call when another SLOT is invoked.
+        // We could do better, but would have to track why the IRQ was called.
+        io_irq_ctrl_hw_t *irq_ctrl_base = cyw43_core_num ?
+                                          &iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl;
+        hw_set_bits(&irq_ctrl_base->intf[CYW43_PIN_WL_HOST_WAKE/8], GPIO_IRQ_LEVEL_HIGH << (4 * (CYW43_PIN_WL_HOST_WAKE & 7)));
+    }
+}
+
+void cyw43_schedule_internal_poll_dispatch(void (*func)(void)) {
+    low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, func);
+}
+
+// Prevent background processing in pensv and access by the other core
+// These methods are called in pensv context and on either core
+// They can be called recursively
+void cyw43_thread_enter(void) {
+    // Lock the other core and stop low_prio_irq running
+    recursive_mutex_enter_blocking(&cyw43_mutex);
+}
+
+#ifndef NDEBUG
+void cyw43_thread_lock_check(void) {
+    // Lock the other core and stop low_prio_irq running
+    if (recursive_mutex_enter_count(&cyw43_mutex) < 1 || recursive_mutex_owner(&cyw43_mutex) != lock_get_caller_owner_id()) {
+        panic("cyw43_thread_lock_check failed");
+    }
+}
+#endif
+
+// Re-enable background processing
+void cyw43_thread_exit(void) {
+    // Run low_prio_irq if needed
+    if (1 == recursive_mutex_enter_count(&cyw43_mutex)) {
+        // note the outer release of the mutex is not via cyw43_exit in the low_priority_irq case (it is a direct mutex exit)
+        assert(!in_low_priority_irq);
+//        if (low_priority_irq_missed) {
+//            low_priority_irq_missed = false;
+            if (low_priority_irq_dispatch_slots[CYW43_DISPATCH_SLOT_CYW43]) {
+                low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, cyw43_poll);
+            }
+//        }
+    }
+    recursive_mutex_exit(&cyw43_mutex);
+}
+
+
+static void cyw43_delay_until(absolute_time_t until) {
+    // sleep can be called in IRQs, so there's not much we can do there
+    if (__get_current_exception()) {
+        busy_wait_until(until);
+    } else {
+        sleep_until(until);
+    }
+}
+
+void cyw43_delay_ms(uint32_t ms) {
+    cyw43_delay_until(make_timeout_time_ms(ms));
+}
+
+void cyw43_delay_us(uint32_t us) {
+    cyw43_delay_until(make_timeout_time_us(us));
+}
+
+void cyw43_arch_poll() {
+    // should not be necessary
+//    if (cyw43_poll) {
+//        low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, cyw43_poll);
+//    }
+}
+
+#if !CYW43_LWIP
+static void no_lwip_fail() {
+    panic("You cannot use IP with pico_cyw43_arch_none");
+}
+void cyw43_cb_tcpip_init(cyw43_t *self, int itf) {
+}
+void cyw43_cb_tcpip_deinit(cyw43_t *self, int itf) {
+}
+void cyw43_cb_tcpip_set_link_up(cyw43_t *self, int itf) {
+    no_lwip_fail();
+}
+void cyw43_cb_tcpip_set_link_down(cyw43_t *self, int itf) {
+    no_lwip_fail();
+}
+void cyw43_cb_process_ethernet(void *cb_data, int itf, size_t len, const uint8_t *buf) {
+    no_lwip_fail();
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/src/rp2_common/pico_cyw43_arch/include/cyw43_configport.h b/src/rp2_common/pico_cyw43_arch/include/cyw43_configport.h
new file mode 100644
index 0000000..7827c81
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/include/cyw43_configport.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+// This header is included by cyw43_driver to setup its environment
+
+#ifndef _CYW43_CONFIGPORT_H
+#define _CYW43_CONFIGPORT_H
+
+#include "pico.h"
+
+#ifdef PICO_CYW43_ARCH_HEADER
+#include __XSTRING(PICO_CYW43_ARCH_HEADER)
+#else
+#if PICO_CYW43_ARCH_POLL
+#include "pico/cyw43_arch/arch_poll.h"
+#elif PICO_CYW43_ARCH_THREADSAFE_BACKGROUND
+#include "pico/cyw43_arch/arch_threadsafe_background.h"
+#elif PICO_CYW43_ARCH_FREERTOS
+#include "pico/cyw43_arch/arch_freertos.h"
+#else
+#error must specify support pico_cyw43_arch architecture type or set PICO_CYW43_ARCH_HEADER
+#endif
+#endif
+
+#ifndef CYW43_HOST_NAME
+#define CYW43_HOST_NAME "PicoW"
+#endif
+
+#ifndef CYW43_GPIO
+#define CYW43_GPIO 1
+#endif
+
+#ifndef CYW43_LOGIC_DEBUG
+#define CYW43_LOGIC_DEBUG 0
+#endif
+
+#ifndef CYW43_USE_OTP_MAC
+#define CYW43_USE_OTP_MAC 1
+#endif
+
+#ifndef CYW43_NO_NETUTILS
+#define CYW43_NO_NETUTILS 1
+#endif
+
+#ifndef CYW43_IOCTL_TIMEOUT_US
+#define CYW43_IOCTL_TIMEOUT_US 1000000
+#endif
+
+#ifndef CYW43_USE_STATS
+#define CYW43_USE_STATS 0
+#endif
+
+// todo should this be user settable?
+#ifndef CYW43_HAL_MAC_WLAN0
+#define CYW43_HAL_MAC_WLAN0 0
+#endif
+
+#ifndef STATIC
+#define STATIC static
+#endif
+
+#ifndef CYW43_USE_SPI
+#define CYW43_USE_SPI 1
+#endif
+
+#ifndef CYW43_SPI_PIO
+#define CYW43_SPI_PIO 1
+#endif
+
+#endif
\ No newline at end of file
diff --git a/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch.h b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch.h
new file mode 100644
index 0000000..8d6a628
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch.h
@@ -0,0 +1,328 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef _PICO_CYW43_ARCH_H
+#define _PICO_CYW43_ARCH_H
+
+#include "pico.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "cyw43.h"
+#include "cyw43_country.h"
+
+/**
+ * \defgroup cyw43_driver cyw43_driver
+ * \ingroup pico_cyw43_arch
+ * \brief Driver used for Pico W wireless
+*/
+
+/**
+ * \defgroup cyw43_ll cyw43_ll
+ * \ingroup cyw43_driver
+ * \brief Low Level CYW43 driver interface
+*/
+
+/** \file pico/cyw43_arch.h
+ *  \defgroup pico_cyw43_arch pico_cyw43_arch
+ *
+ * Architecture for integrating the CYW43 driver (for the wireless on Pico W) and lwIP (for TCP/IP stack) into the SDK. It is also necessary for accessing the on-board LED on Pico W
+ *
+ * Both the low level \c cyw43_driver and the lwIP stack require periodic servicing, and have limitations
+ * on whether they can be called from multiple cores/threads.
+ *
+ * \c pico_cyw43_arch attempts to abstract these complications into several behavioral groups:
+ *
+ * * \em 'poll' - This not multi-core/IRQ safe, and requires the user to call \ref cyw43_arch_poll periodically from their main loop
+ * * \em 'thread_safe_background' - This is multi-core/thread/task safe, and maintenance of the driver and TCP/IP stack is handled automatically in the background
+ *
+ * As of right now, lwIP is the only supported TCP/IP stack, however the use of \c pico_cyw43_arch is intended to be independent of
+ * the particular TCP/IP stack used (and possibly Bluetooth stack used) in the future. For this reason, the integration of lwIP
+ * is handled in the base (\c pico_cyw43_arch) library based on the #define \ref CYW43_LWIP used by the \c cyw43_driver.
+ *
+ * Whilst you can use the \c pico_cyw43_arch library directly and  specify \ref CYW$#_LWIP (and other defines) yourself, several
+ * other libraries are made available to the build which aggregate the defines and other dependencies for you:
+ *
+ * * \b pico_cyw43_arch_lwip_poll - For using the RAW lwIP API (in `NO_SYS=1` mode) without any background processing or multi-core/thread safety.
+ *
+ *    The user must call \ref pico_cyw43_poll periodically from their main loop.
+ *
+ *    This wrapper library:
+ *    - Sets \c CYW43_LWIP=1 to enable lwIP support in \c pico_cyw43_arch and \c cyw43_driver.
+ *    - Sets \c PICO_CYW43_ARCH_POLL=1 to select the polling behavior.
+ *    - Adds the \c pico_lwip as a dependency to pull in lwIP.
+ *
+ * * \b pico_cyw43_arch_lwip_threadsafe_background - For using the RAW lwIP API (in `NO_SYS=1` mode) with multi-core/thread safety, and automatic servicing of the \c cyw43_driver and
+ * lwIP in background.
+ *
+ *    Calls into the \c cyw43_driver high level API (cyw43.h) may be made from either core or from lwIP callbacks, however calls into lwIP (which
+ * is not thread-safe) other than those made from lwIP callbacks, must be bracketed with \ref cyw43_arch_lwip_begin and \ref cyw43_arch_lwip_end. It is fine to bracket
+ * calls made from within lwIP callbacks too; you just don't have to.
+ *
+ *    \note lwIP callbacks happen in a (low priority) IRQ context (similar to an alarm callback), so care should be taken when interacting
+ *    with other code.
+ *
+ *    This wrapper library:
+ *    - Sets \c CYW43_LWIP=1 to enable lwIP support in \c pico_cyw43_arch and \c cyw43_driver
+ *    - Sets \c PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1 to select the thread-safe/non-polling behavior.
+ *    - Adds the pico_lwip as a dependency to pull in lwIP.
+ *
+ *
+ *    This library \em can also be used under the RP2040 port of FreeRTOS with lwIP in `NO_SYS=1` mode (allowing you to call \c cyw43_driver APIs
+ * from any task, and to call lwIP from lwIP callbacks, or from any task if you bracket the calls with \ref cyw43_arch_lwip_begin and \ref cyw43_arch_lwip_end. Again, you should be
+ * careful about what you do in lwIP callbacks, as you cannot call most FreeRTOS APIs from within an IRQ context. Unless you have good reason, you should probably
+ * use the full FreeRTOS integration (with `NO_SYS=0`) provided by \c pico_cyw43_arch_lwip_sys_freertos.
+ *
+ * * \b pico_cyw43_arch_lwip_sys_freertos - For using the full lwIP API including blocking sockets in OS (`NO_SYS=0`) mode, along with with multi-core/task/thread safety, and automatic servicing of the \c cyw43_driver and
+ * the lwIP stack.
+ *
+ *    This wrapper library:
+ *    - Sets \c CYW43_LWIP=1 to enable lwIP support in \c pico_cyw43_arch and \c cyw43_driver.
+ *    - Sets \c PICO_CYW43_ARCH_FREERTOS=1 to select the NO_SYS=0 lwip/FreeRTOS integration
+ *    - Sets \c LWIP_PROVIDE_ERRNO=1 to provide error numbers needed for compilation without an OS
+ *    - Adds the \c pico_lwip as a dependency to pull in lwIP.
+ *    - Adds the lwIP/FreeRTOS code from lwip-contrib (in the contrib directory of lwIP)
+ *
+ *    Calls into the \c cyw43_driver high level API (cyw43.h) may be made from any task or from lwIP callbacks, but not from IRQs. Calls into the lwIP RAW API (which is not thread safe)
+ *    must be bracketed with \ref cyw43_arch_lwip_begin and \ref cyw43_arch_lwip_end. It is fine to bracket calls made from within lwIP callbacks too; you just don't have to.
+ *
+ *    \note this wrapper library requires you to link FreeRTOS functionality with your application yourself.
+ *
+ * * \b pico_cyw43_arch_none - If you do not need the TCP/IP stack but wish to use the on-board LED.
+ *
+ *    This wrapper library:
+ *    - Sets \c CYW43_LWIP=0 to disable lwIP support in \c pico_cyw43_arch and \c cyw43_driver
+ */
+
+// PICO_CONFIG: PARAM_ASSERTIONS_ENABLED_CYW43_ARCH, Enable/disable assertions in the pico_cyw43_arch module, type=bool, default=0, group=pico_cyw43_arch
+#ifndef PARAM_ASSERTIONS_ENABLED_CYW43_ARCH
+#define PARAM_ASSERTIONS_ENABLED_CYW43_ARCH 0
+#endif
+
+// PICO_CONFIG: CYW43_ARCH_DEBUG_ENABLED, Enable/disable some debugging output in the pico_cyw43_arch module, type=bool, default=1 in debug builds, group=pico_cyw43_arch
+#ifndef CYW43_ARCH_DEBUG_ENABLED
+#ifndef NDEBUG
+#define CYW43_ARCH_DEBUG_ENABLED 1
+#else
+#define CYW43_ARCH_DEBUG_ENABLED 0
+#endif
+#endif
+
+// PICO_CONFIG: PICO_CYW43_ARCH_DEFAULT_COUNTRY_CODE, Default country code for the cyw43 wireless driver, default=CYW43_COUNTRY_WORLDWIDE, group=pico_cyw43_arch
+#ifndef PICO_CYW43_ARCH_DEFAULT_COUNTRY_CODE
+#define PICO_CYW43_ARCH_DEFAULT_COUNTRY_CODE CYW43_COUNTRY_WORLDWIDE
+#endif
+
+/*!
+ * \brief Initialize the CYW43 architecture
+ * \ingroup pico_cyw43_arch
+ *
+ * This method initializes the `cyw43_driver` code and initializes the lwIP stack (if it
+ * was enabled at build time). This method must be called prior to using any other \c pico_cyw43_arch,
+ * \cyw43_driver or lwIP functions.
+ *
+ * \note this method initializes wireless with a country code of \c PICO_CYW43_ARCH_DEFAULT_COUNTRY_CODE
+ * which defaults to \c CYW43_COUNTRY_WORLDWIDE. Worldwide settings may not give the best performance; consider
+ * setting PICO_CYW43_ARCH_DEFAULT_COUNTRY_CODE to a different value or calling \ref cyw43_arch_init_with_country
+ * \return 0 if the initialization is successful, an error code otherwise \see pico_error_codes
+ */
+int cyw43_arch_init(void);
+
+/*!
+ * \brief Initialize the CYW43 architecture for use in a specific country
+ * \ingroup pico_cyw43_arch
+ *
+ * This method initializes the `cyw43_driver` code and initializes the lwIP stack (if it
+ * was enabled at build time). This method must be called prior to using any other \c pico_cyw43_arch,
+ * \cyw43_driver or lwIP functions.
+ *
+ * \param country the country code to use (see \ref CYW43_COUNTRY_)
+ * \return 0 if the initialization is successful, an error code otherwise \see pico_error_codes
+ */
+int cyw43_arch_init_with_country(uint32_t country);
+
+/*!
+ * \brief Enables Wi-Fi STA (Station) mode.
+ * \ingroup pico_cyw43_arch
+ *
+ * This enables the Wi-Fi in \emStation mode such that connections can be made to other Wi-Fi Access Points
+ */
+void cyw43_arch_enable_sta_mode(void);
+
+/*!
+ * \brief Enables Wi-Fi AP (Access point) mode.
+ * \ingroup pico_cyw43_arch
+ *
+ * This enables the Wi-Fi in \em Access \em Point mode such that connections can be made to the device by  other Wi-Fi clients
+ * \param ssid the name for the access point
+ * \param password the password to use or NULL for no password.
+ * \param auth the authorization type to use when the password is enabled. Values are \ref CYW43_AUTH_WPA_TKIP_PSK,
+ *             \ref CYW43_AUTH_WPA2_AES_PSK, or \ref CYW43_AUTH_WPA2_MIXED_PSK (see \ref CYW43_AUTH_)
+ */
+void cyw43_arch_enable_ap_mode(const char *ssid, const char *password, uint32_t auth);
+
+/*!
+ * \brief De-initialize the CYW43 architecture
+ * \ingroup pico_cyw43_arch
+ *
+ * This method de-initializes the `cyw43_driver` code and de-initializes the lwIP stack (if it
+ * was enabled at build time). Note this method should always be called from the same core (or RTOS
+ * task, depending on the environment) as \ref cyw43_arch_init.
+ */
+void cyw43_arch_deinit(void);
+
+/*!
+ * \brief Attempt to connect to a wireless access point, blocking until the network is joined or a failure is detected.
+ * \ingroup pico_cyw43_arch
+ *
+ * \param ssid the network name to connect to
+ * \param password the network password or NULL if there is no password required
+ * \param auth the authorization type to use when the password is enabled. Values are \ref CYW43_AUTH_WPA_TKIP_PSK,
+ *             \ref CYW43_AUTH_WPA2_AES_PSK, or \ref CYW43_AUTH_WPA2_MIXED_PSK (see \ref CYW43_AUTH_)
+ *
+ * \return 0 if the initialization is successful, an error code otherwise \see pico_error_codes
+ */
+int cyw43_arch_wifi_connect_blocking(const char *ssid, const char *pw, uint32_t auth);
+
+/*!
+ * \brief Attempt to connect to a wireless access point, blocking until the network is joined, a failure is detected or a timeout occurs
+ * \ingroup pico_cyw43_arch
+ *
+ * \param ssid the network name to connect to
+ * \param password the network password or NULL if there is no password required
+ * \param auth the authorization type to use when the password is enabled. Values are \ref CYW43_AUTH_WPA_TKIP_PSK,
+ *             \ref CYW43_AUTH_WPA2_AES_PSK, or \ref CYW43_AUTH_WPA2_MIXED_PSK (see \ref CYW43_AUTH_)
+ *
+ * \return 0 if the initialization is successful, an error code otherwise \see pico_error_codes
+ */
+int cyw43_arch_wifi_connect_timeout_ms(const char *ssid, const char *pw, uint32_t auth, uint32_t timeout);
+
+/*!
+ * \brief Start attempting to connect to a wireless access point
+ * \ingroup pico_cyw43_arch
+ *
+ * This method tells the CYW43 driver to start connecting to an access point. You should subsequently check the
+ * status by calling \ref cyw43_wifi_link_status.
+ *
+ * \param ssid the network name to connect to
+ * \param password the network password or NULL if there is no password required
+ * \param auth the authorization type to use when the password is enabled. Values are \ref CYW43_AUTH_WPA_TKIP_PSK,
+ *             \ref CYW43_AUTH_WPA2_AES_PSK, or \ref CYW43_AUTH_WPA2_MIXED_PSK (see \ref CYW43_AUTH_)
+ *
+ * \return 0 if the scan was started successfully, an error code otherwise \see pico_error_codes
+ */
+int cyw43_arch_wifi_connect_async(const char *ssid, const char *pw, uint32_t auth);
+
+/*!
+ * \brief Return the country code used to initialize cyw43_arch
+ * \ingroup pico_cyw43_arch
+ *
+ * \return the country code (see \ref CYW43_COUNTRY_)
+ */
+uint32_t cyw43_arch_get_country_code(void);
+
+/*!
+ * \brief Set a GPIO pin on the wireless chip to a given value
+ * \ingroup pico_cyw43_arch
+ * \note this method does not check for errors setting the GPIO. You can use the lower level \ref cyw43_gpio_set instead if you wish
+ * to check for errors.
+ *
+ * \param wl_gpio the GPIO number on the wireless chip
+ * \param value true to set the GPIO, false to clear it.
+ */
+void cyw43_arch_gpio_put(uint wl_gpio, bool value);
+
+/*!
+ * \brief Read the value of a GPIO pin on the wireless chip
+ * \ingroup pico_cyw43_arch
+ * \note this method does not check for errors setting the GPIO. You can use the lower level \ref cyw43_gpio_get instead if you wish
+ * to check for errors.
+ *
+ * \param wl_gpio the GPIO number on the wireless chip
+ * \return true if the GPIO is high, false otherwise
+ */
+bool cyw43_arch_gpio_get(uint wl_gpio);
+
+/*!
+ * \brief Perform any processing required by the \c cyw43_driver or the TCP/IP stack
+ * \ingroup pico_cyw43_arch
+ *
+ * This method must be called periodically from the main loop when using a
+ * \em polling style \c pico_cyw43_arch (e.g. \c pico_cyw43_arch_lwip_poll ). It
+ * may be called in other styles, but it is unnecessary to do so.
+ */
+void cyw43_arch_poll(void);
+
+/*!
+ * \fn cyw43_arch_lwip_begin
+ * \brief Acquire any locks required to call into lwIP
+ * \ingroup pico_cyw43_arch
+ *
+ * The lwIP API is not thread safe. You should surround calls into the lwIP API
+ * with calls to this method and \ref cyw43_arch_lwip_end. Note these calls are not
+ * necessary (but harmless) when you are calling back into the lwIP API from an lwIP callback.
+ * If you are using single-core polling only (pico_cyw43_arch_poll) then these calls are no-ops
+ * anyway it is good practice to call them anyway where they are necessary.
+ *
+ * \sa cyw43_arch_lwip_end
+ * \sa cyw43_arch_lwip_protect
+ */
+
+/*!
+ * \fn void cyw43_arch_lwip_end(void)
+ * \brief Release any locks required for calling into lwIP
+ * \ingroup pico_cyw43_arch
+ *
+ * The lwIP API is not thread safe. You should surround calls into the lwIP API
+ * with calls to \ref cyw43_arch_lwip_begin and this method. Note these calls are not
+ * necessary (but harmless) when you are calling back into the lwIP API from an lwIP callback.
+ * If you are using single-core polling only (pico_cyw43_arch_poll) then these calls are no-ops
+ * anyway it is good practice to call them anyway where they are necessary.
+ *
+ * \sa cyw43_arch_lwip_begin
+ * \sa cyw43_arch_lwip_protect
+ */
+
+/*!
+ * \fn int cyw43_arch_lwip_protect(int (*func)(void *param), void *param)
+ * \brief sad Release any locks required for calling into lwIP
+ * \ingroup pico_cyw43_arch
+ *
+ * The lwIP API is not thread safe. You can use this method to wrap a function
+ * with any locking required to call into the lwIP API. If you are using
+ * single-core polling only (pico_cyw43_arch_poll) then there are no
+ * locks to required, but it is still good practice to use this function.
+ *
+ * \param func the function ta call with any required locks held
+ * \param param parameter to pass to \c func
+ * \return the return value from \c func
+ * \sa cyw43_arch_lwip_begin
+ * \sa cyw43_arch_lwip_end
+ */
+
+/*!
+ * \fn void cyw43_arch_lwip_check(void)
+ * \brief Checks the caller has any locks required for calling into lwIP
+ * \ingroup pico_cyw43_arch
+ *
+ * The lwIP API is not thread safe. You should surround calls into the lwIP API
+ * with calls to \ref cyw43_arch_lwip_begin and this method. Note these calls are not
+ * necessary (but harmless) when you are calling back into the lwIP API from an lwIP callback.
+ *
+ * This method will assert in debug mode, if the above conditions are not met (i.e. it is not safe to
+ * call into the lwIP API)
+ *
+ * \sa cyw43_arch_lwip_begin
+ * \sa cyw43_arch_lwip_protect
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_common.h b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_common.h
new file mode 100644
index 0000000..3cd5aa9
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_common.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef _PICO_CYW43_ARCH_ARCH_COMMON_H
+#define _PICO_CYW43_ARCH_ARCH_COMMON_H
+
+#include "pico.h"
+#include "pico/time.h"
+#include "hardware/gpio.h"
+#include "pico/error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Note, these are negated, because cyw43_driver negates them before returning!
+#define CYW43_EPERM            (-PICO_ERROR_NOT_PERMITTED) // Operation not permitted
+#define CYW43_EIO              (-PICO_ERROR_IO) // I/O error
+#define CYW43_EINVAL           (-PICO_ERROR_INVALID_ARG) // Invalid argument
+#define CYW43_ETIMEDOUT        (-PICO_ERROR_TIMEOUT) // Connection timed out
+
+#define CYW43_NUM_GPIOS        CYW43_WL_GPIO_COUNT
+
+#define cyw43_hal_pin_obj_t uint
+
+// get the number of elements in a fixed-size array
+#define CYW43_ARRAY_SIZE(a) count_of(a)
+
+static inline uint32_t cyw43_hal_ticks_us(void) {
+    return time_us_32();
+}
+
+static inline uint32_t cyw43_hal_ticks_ms(void) {
+    return to_ms_since_boot(get_absolute_time());
+}
+
+static inline int cyw43_hal_pin_read(cyw43_hal_pin_obj_t pin) {
+    return gpio_get(pin);
+}
+
+static inline void cyw43_hal_pin_low(cyw43_hal_pin_obj_t pin) {
+    gpio_clr_mask(1 << pin);
+}
+
+static inline void cyw43_hal_pin_high(cyw43_hal_pin_obj_t pin) {
+    gpio_set_mask(1 << pin);
+}
+
+#define CYW43_HAL_PIN_MODE_INPUT           (GPIO_IN)
+#define CYW43_HAL_PIN_MODE_OUTPUT          (GPIO_OUT)
+
+#define CYW43_HAL_PIN_PULL_NONE            (0)
+#define CYW43_HAL_PIN_PULL_UP              (1)
+#define CYW43_HAL_PIN_PULL_DOWN            (2)
+
+static inline void cyw43_hal_pin_config(cyw43_hal_pin_obj_t pin, uint32_t mode, uint32_t pull, __unused uint32_t alt) {
+    assert((mode == CYW43_HAL_PIN_MODE_INPUT || mode == CYW43_HAL_PIN_MODE_OUTPUT) && alt == 0);
+    gpio_set_dir(pin, mode);
+    gpio_set_pulls(pin, pull == CYW43_HAL_PIN_PULL_UP, pull == CYW43_HAL_PIN_PULL_DOWN);
+}
+
+void cyw43_hal_get_mac(int idx, uint8_t buf[6]);
+
+void cyw43_hal_generate_laa_mac(int idx, uint8_t buf[6]);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
diff --git a/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_freertos.h b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_freertos.h
new file mode 100644
index 0000000..342a85c
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_freertos.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef _EXAMPLE_CYW43_ARCH_ARCH_FREERTOS_SYS_H
+#define _EXAMPLE_CYW43_ARCH_ARCH_FREERTOS_SYS_H
+
+#include "pico/cyw43_arch/arch_common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void cyw43_thread_enter(void);
+void cyw43_thread_exit(void);
+
+#define CYW43_THREAD_ENTER cyw43_thread_enter();
+#define CYW43_THREAD_EXIT cyw43_thread_exit();
+#ifndef NDEBUG
+void cyw43_thread_lock_check(void);
+#define cyw43_arch_lwip_check() cyw43_thread_lock_check()
+#define CYW43_THREAD_LOCK_CHECK cyw43_arch_lwip_check();
+#else
+#define cyw43_arch_lwip_check() ((void)0)
+#define CYW43_THREAD_LOCK_CHECK
+#endif
+
+void cyw43_await_background_or_timeout_us(uint32_t timeout_us);
+// todo not 100% sure about the timeouts here; MP uses __WFI which will always wakeup periodically
+#define CYW43_SDPCM_SEND_COMMON_WAIT cyw43_await_background_or_timeout_us(1000);
+#define CYW43_DO_IOCTL_WAIT cyw43_await_background_or_timeout_us(1000);
+
+void cyw43_delay_ms(uint32_t ms);
+void cyw43_delay_us(uint32_t us);
+
+void cyw43_schedule_internal_poll_dispatch(void (*func)(void));
+
+void cyw43_post_poll_hook(void);
+#define CYW43_POST_POLL_HOOK cyw43_post_poll_hook();
+
+static inline void cyw43_arch_lwip_begin(void) {
+    cyw43_thread_enter();
+}
+static inline void cyw43_arch_lwip_end(void) {
+    cyw43_thread_exit();
+}
+
+static inline int cyw43_arch_lwip_protect(int (*func)(void *param), void *param) {
+    cyw43_arch_lwip_begin();
+    int rc = func(param);
+    cyw43_arch_lwip_end();
+    return rc;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_poll.h b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_poll.h
new file mode 100644
index 0000000..bfaea7f
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_poll.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef _PICO_CYW43_ARCH_ARCH_POLL_H
+#define _PICO_CYW43_ARCH_ARCH_POLL_H
+
+#include "pico/cyw43_arch/arch_common.h"
+
+#include <assert.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define CYW43_THREAD_ENTER
+#define CYW43_THREAD_EXIT
+#ifndef NDEBUG
+
+void cyw43_thread_check(void);
+
+#define cyw43_arch_lwip_check() cyw43_thread_check()
+#define CYW43_THREAD_LOCK_CHECK cyw43_arch_lwip_check();
+#else
+#define cyw43_arch_lwip_check() ((void)0)
+#define CYW43_THREAD_LOCK_CHECK
+#endif
+
+#define CYW43_SDPCM_SEND_COMMON_WAIT cyw43_poll_required = true;
+#define CYW43_DO_IOCTL_WAIT cyw43_poll_required = true;
+
+#define cyw43_delay_ms sleep_ms
+#define cyw43_delay_us sleep_us
+
+void cyw43_schedule_internal_poll_dispatch(void (*func)(void));
+
+void cyw43_post_poll_hook(void);
+
+extern bool cyw43_poll_required;
+
+#define CYW43_POST_POLL_HOOK cyw43_post_poll_hook();
+#endif
+
+#ifndef DOXYGEN_GENERATION // multiple definitions in separate headers seems to confused doxygen
+#define cyw43_arch_lwip_begin() ((void)0)
+#define cyw43_arch_lwip_end() ((void)0)
+
+static inline int cyw43_arch_lwip_protect(int (*func)(void *param), void *param) {
+    return func(param);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_threadsafe_background.h b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_threadsafe_background.h
new file mode 100644
index 0000000..713fcc1
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_threadsafe_background.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef _PICO_CYW43_ARCH_ARCH_THREADSAFE_BACKGROUND_H
+#define _PICO_CYW43_ARCH_ARCH_THREADSAFE_BACKGROUND_H
+
+#include "pico/cyw43_arch/arch_common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void cyw43_thread_enter(void);
+
+void cyw43_thread_exit(void);
+
+#define CYW43_THREAD_ENTER cyw43_thread_enter();
+#define CYW43_THREAD_EXIT cyw43_thread_exit();
+#ifndef NDEBUG
+
+void cyw43_thread_lock_check(void);
+
+#define cyw43_arch_lwip_check() cyw43_thread_lock_check()
+#define CYW43_THREAD_LOCK_CHECK cyw43_arch_lwip_check();
+#else
+#define cyw43_arch_lwip_check() ((void)0)
+#define CYW43_THREAD_LOCK_CHECK
+#endif
+
+void cyw43_await_background_or_timeout_us(uint32_t timeout_us);
+// todo not 100% sure about the timeouts here; MP uses __WFI which will always wakeup periodically
+#define CYW43_SDPCM_SEND_COMMON_WAIT cyw43_await_background_or_timeout_us(1000);
+#define CYW43_DO_IOCTL_WAIT cyw43_await_background_or_timeout_us(1000);
+
+void cyw43_delay_ms(uint32_t ms);
+
+void cyw43_delay_us(uint32_t us);
+
+void cyw43_schedule_internal_poll_dispatch(void (*func)(void));
+
+void cyw43_post_poll_hook(void);
+
+#define CYW43_POST_POLL_HOOK cyw43_post_poll_hook();
+
+static inline void cyw43_arch_lwip_begin(void) {
+    cyw43_thread_enter();
+}
+
+static inline void cyw43_arch_lwip_end(void) {
+    cyw43_thread_exit();
+}
+
+static inline int cyw43_arch_lwip_protect(int (*func)(void *param), void *param) {
+    cyw43_arch_lwip_begin();
+    int rc = func(param);
+    cyw43_arch_lwip_end();
+    return rc;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/rp2040_usb_device_enumeration.c b/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/rp2040_usb_device_enumeration.c
index ad9e822..503cd12 100644
--- a/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/rp2040_usb_device_enumeration.c
+++ b/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/rp2040_usb_device_enumeration.c
@@ -17,12 +17,12 @@
 #define LS_K   0b10
 #define LS_SE1 0b11
 
+#if PICO_RP2040_B0_SUPPORTED || PICO_RP2040_B1_SUPPORTED
 static void hw_enumeration_fix_wait_se0(void);
 static void hw_enumeration_fix_force_ls_j(void);
 static void hw_enumeration_fix_finish(void);
 
 void rp2040_usb_device_enumeration_fix(void) {
-#if PICO_RP2040_B0_SUPPORTED || PICO_RP2040_B1_SUPPORTED
     // Actually check for B0/B1 h/w
     if (rp2040_chip_version() == 1) {
         // After coming out of reset, the hardware expects 800us of LS_J (linestate J) time
@@ -36,7 +36,6 @@
         // Wait SE0 phase will call force ls_j phase which will call finish phase
         hw_enumeration_fix_wait_se0();
     }
-#endif
 }
 
 static inline uint8_t hw_line_state(void) {
@@ -146,3 +145,9 @@
     // Restore the pad ctrl value
     padsbank0_hw->io[dp] = pad_ctrl_prev;
 }
+
+#else
+void rp2040_usb_device_enumeration_fix(void) {
+    // nothing to do
+}
+#endif
\ No newline at end of file
diff --git a/src/rp2_common/pico_lwip/CMakeLists.txt b/src/rp2_common/pico_lwip/CMakeLists.txt
new file mode 100644
index 0000000..7b1a8a7
--- /dev/null
+++ b/src/rp2_common/pico_lwip/CMakeLists.txt
@@ -0,0 +1,278 @@
+if (DEFINED ENV{PICO_LWIP_PATH} AND (NOT PICO_LWIP_PATH))
+    set(PICO_LWIP_PATH $ENV{PICO_LWIP_PATH})
+    message("Using PICO_LWIP_PATH from environment ('${PICO_LWIP_PATH}')")
+endif ()
+
+set(LWIP_TEST_PATH "src/Filelists.cmake")
+if (NOT PICO_LWIP_PATH)
+    set(PICO_LWIP_PATH ${PROJECT_SOURCE_DIR}/lib/lwip)
+#    if (NOT EXISTS ${PICO_LWIP_PATH}/${LWIP_TEST_PATH})
+#        message(WARNING "LWIP submodule has not been initialized; Pico W wireless support will be unavailable
+#hint: try 'git submodule update --init' from your SDK directory (${PICO_SDK_PATH}).")
+#    endif()
+elseif (NOT EXISTS ${PICO_LWIP_PATH}/${LWIP_TEST_PATH})
+    message(WARNING "PICO_LWIP_PATH specified but content not present.")
+endif()
+
+if (EXISTS ${PICO_LWIP_PATH}/${LWIP_TEST_PATH})
+    message("lwIP available at ${PICO_LWIP_PATH}")
+
+    # argh... wanted to use this, but they dump stuff into the source tree, which breaks parallel builds
+    #set(LWIP_DIR ${PICO_LWIP_PATH})
+    #include(${PICO_LWIP_PATH}/src/Filelists.cmake)
+
+    pico_register_common_scope_var(PICO_LWIP_PATH)
+
+    # The minimum set of files needed for lwIP.
+    add_library(pico_lwip_core INTERFACE)
+    target_sources(pico_lwip_core INTERFACE
+            ${PICO_LWIP_PATH}/src/core/init.c
+            ${PICO_LWIP_PATH}/src/core/def.c
+            ${PICO_LWIP_PATH}/src/core/dns.c
+            ${PICO_LWIP_PATH}/src/core/inet_chksum.c
+            ${PICO_LWIP_PATH}/src/core/ip.c
+            ${PICO_LWIP_PATH}/src/core/mem.c
+            ${PICO_LWIP_PATH}/src/core/memp.c
+            ${PICO_LWIP_PATH}/src/core/netif.c
+            ${PICO_LWIP_PATH}/src/core/pbuf.c
+            ${PICO_LWIP_PATH}/src/core/raw.c
+            ${PICO_LWIP_PATH}/src/core/stats.c
+            ${PICO_LWIP_PATH}/src/core/sys.c
+            ${PICO_LWIP_PATH}/src/core/altcp.c
+            ${PICO_LWIP_PATH}/src/core/altcp_alloc.c
+            ${PICO_LWIP_PATH}/src/core/altcp_tcp.c
+            ${PICO_LWIP_PATH}/src/core/tcp.c
+            ${PICO_LWIP_PATH}/src/core/tcp_in.c
+            ${PICO_LWIP_PATH}/src/core/tcp_out.c
+            ${PICO_LWIP_PATH}/src/core/timeouts.c
+            ${PICO_LWIP_PATH}/src/core/udp.c
+            ${CMAKE_CURRENT_LIST_DIR}/random.c
+            )
+    target_include_directories(pico_lwip_core INTERFACE
+            ${PICO_LWIP_PATH}/src/include)
+
+    add_library(pico_lwip_core4 INTERFACE)
+    target_sources(pico_lwip_core4 INTERFACE
+            ${PICO_LWIP_PATH}/src/core/ipv4/acd.c
+            ${PICO_LWIP_PATH}/src/core/ipv4/autoip.c
+            ${PICO_LWIP_PATH}/src/core/ipv4/dhcp.c
+            ${PICO_LWIP_PATH}/src/core/ipv4/etharp.c
+            ${PICO_LWIP_PATH}/src/core/ipv4/icmp.c
+            ${PICO_LWIP_PATH}/src/core/ipv4/igmp.c
+            ${PICO_LWIP_PATH}/src/core/ipv4/ip4_frag.c
+            ${PICO_LWIP_PATH}/src/core/ipv4/ip4.c
+            ${PICO_LWIP_PATH}/src/core/ipv4/ip4_addr.c
+            )
+
+    add_library(pico_lwip_core6 INTERFACE)
+    target_sources(pico_lwip_core6 INTERFACE
+            ${PICO_LWIP_PATH}/src/core/ipv6/dhcp6.c
+            ${PICO_LWIP_PATH}/src/core/ipv6/ethip6.c
+            ${PICO_LWIP_PATH}/src/core/ipv6/icmp6.c
+            ${PICO_LWIP_PATH}/src/core/ipv6/inet6.c
+            ${PICO_LWIP_PATH}/src/core/ipv6/ip6.c
+            ${PICO_LWIP_PATH}/src/core/ipv6/ip6_addr.c
+            ${PICO_LWIP_PATH}/src/core/ipv6/ip6_frag.c
+            ${PICO_LWIP_PATH}/src/core/ipv6/mld6.c
+            ${PICO_LWIP_PATH}/src/core/ipv6/nd6.c
+            )
+
+    # APIFILES: The files which implement the sequential and socket APIs.
+    add_library(pico_lwip_api INTERFACE)
+    target_sources(pico_lwip_api INTERFACE
+            ${PICO_LWIP_PATH}/src/api/api_lib.c
+            ${PICO_LWIP_PATH}/src/api/api_msg.c
+            ${PICO_LWIP_PATH}/src/api/err.c
+            ${PICO_LWIP_PATH}/src/api/if_api.c
+            ${PICO_LWIP_PATH}/src/api/netbuf.c
+            ${PICO_LWIP_PATH}/src/api/netdb.c
+            ${PICO_LWIP_PATH}/src/api/netifapi.c
+            ${PICO_LWIP_PATH}/src/api/sockets.c
+            ${PICO_LWIP_PATH}/src/api/tcpip.c
+            )
+
+    # Files implementing various generic network interface functions
+    add_library(pico_lwip_netif INTERFACE)
+    target_sources(pico_lwip_netif INTERFACE
+            ${PICO_LWIP_PATH}/src/netif/ethernet.c
+            ${PICO_LWIP_PATH}/src/netif/bridgeif.c
+            ${PICO_LWIP_PATH}/src/netif/bridgeif_fdb.c
+            ${PICO_LWIP_PATH}/src/netif/slipif.c
+            )
+
+    # 6LoWPAN
+    add_library(pico_lwip_sixlowpan INTERFACE)
+    target_sources(pico_lwip_sixlowpan INTERFACE
+            ${PICO_LWIP_PATH}/src/netif/lowpan6_common.c
+            ${PICO_LWIP_PATH}/src/netif/lowpan6.c
+            ${PICO_LWIP_PATH}/src/netif/lowpan6_ble.c
+            ${PICO_LWIP_PATH}/src/netif/zepif.c
+            )
+
+    # PPP
+    add_library(pico_lwip_ppp INTERFACE)
+    target_sources(pico_lwip_ppp INTERFACE
+            ${PICO_LWIP_PATH}/src/netif/ppp/auth.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/ccp.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/chap-md5.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/chap_ms.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/chap-new.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/demand.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/eap.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/ecp.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/eui64.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/fsm.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/ipcp.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/ipv6cp.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/lcp.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/magic.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/mppe.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/multilink.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/ppp.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/pppapi.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/pppcrypt.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/pppoe.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/pppol2tp.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/pppos.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/upap.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/utils.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/vj.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/polarssl/arc4.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/polarssl/des.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/polarssl/md4.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/polarssl/md5.c
+            ${PICO_LWIP_PATH}/src/netif/ppp/polarssl/sha1.c
+            )
+
+    # SNMPv3 agent
+    add_library(pico_lwip_snmp INTERFACE)
+    target_sources(pico_lwip_snmp INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_asn1.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_core.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_mib2.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_mib2_icmp.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_mib2_interfaces.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_mib2_ip.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_mib2_snmp.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_mib2_system.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_mib2_tcp.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_mib2_udp.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_snmpv2_framework.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_snmpv2_usm.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_msg.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmpv3.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_netconn.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_pbuf_stream.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_raw.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_scalar.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_table.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_threadsync.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmp_traps.c
+            )
+
+    # HTTP server + client
+    add_library(pico_lwip_http INTERFACE)
+    target_sources(pico_lwip_http INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/http/altcp_proxyconnect.c
+            ${PICO_LWIP_PATH}/src/apps/http/fs.c
+            ${PICO_LWIP_PATH}/src/apps/http/http_client.c
+            ${PICO_LWIP_PATH}/src/apps/http/httpd.c
+            )
+
+    # MAKEFSDATA HTTP server host utility
+    add_library(pico_lwip_makefsdata INTERFACE)
+    target_sources(pico_lwip_makefsdata INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/http/makefsdata/makefsdata.c
+            )
+
+    # iperf
+    add_library(pico_lwip_iperf INTERFACE)
+    target_sources(pico_lwip_iperf INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/lwiperf/lwiperf.c
+            )
+
+    # SMTP client
+    add_library(pico_lwip_smtp INTERFACE)
+    target_sources(pico_lwip_smtp INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/smtp/smtp.c
+            )
+
+    # SNTP client
+    add_library(pico_lwip_sntp INTERFACE)
+    target_sources(pico_lwip_sntp INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/sntp/sntp.c
+            )
+
+    # MDNS responder
+    add_library(pico_lwip_mdns INTERFACE)
+    target_sources(pico_lwip_mdns INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/mdns/mdns.c
+            ${PICO_LWIP_PATH}/src/apps/mdns/mdns_out.c
+            ${PICO_LWIP_PATH}/src/apps/mdns/mdns_domain.c
+            )
+
+    # NetBIOS name server
+    add_library(pico_lwip_netbios INTERFACE)
+    target_sources(pico_lwip_netbios INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/netbiosns/netbiosns.c
+            )
+
+    # TFTP server files
+    add_library(pico_lwip_tftp INTERFACE)
+    target_sources(pico_lwip_tftp INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/tftp/tftp.c
+            )
+
+    # MQTT client files
+    add_library(pico_lwip_mbedtls INTERFACE)
+    target_sources(pico_lwip_mbedtls INTERFACE
+            ${PICO_LWIP_PATH}/src/apps/altcp_tls/altcp_tls_mbedtls.c
+            ${PICO_LWIP_PATH}/src/apps/altcp_tls/altcp_tls_mbedtls_mem.c
+            ${PICO_LWIP_PATH}/src/apps/snmp/snmpv3_mbedtls.c
+            )
+
+
+    # All LWIP files without apps
+    add_library(pico_lwip INTERFACE)
+    target_link_libraries(pico_lwip INTERFACE
+        pico_lwip_core
+        pico_lwip_core4
+        pico_lwip_core6
+        pico_lwip_api
+        pico_lwip_netif
+        pico_lwip_sixlowpan
+        pico_lwip_ppp
+    )
+
+    # our arch/cc.h
+    add_library(pico_lwip_arch INTERFACE)
+    target_include_directories(pico_lwip_arch INTERFACE
+            ${CMAKE_CURRENT_LIST_DIR}/include)
+
+    # our nosys impl
+    add_library(pico_lwip_nosys INTERFACE)
+    target_sources(pico_lwip_nosys INTERFACE
+            ${CMAKE_CURRENT_LIST_DIR}/nosys.c
+            )
+    target_link_libraries(pico_lwip_nosys INTERFACE
+            pico_lwip_arch)
+
+
+    if (NOT PICO_LWIP_CONTRIB_PATH)
+        set(PICO_LWIP_CONTRIB_PATH ${PICO_LWIP_PATH}/contrib)
+    endif()
+    pico_register_common_scope_var(PICO_LWIP_CONTRIB_PATH)
+
+    # Make lwip_contrib_freertos library, with the FreeRTOS/lwIP code from lwip-contrib
+    add_library(pico_lwip_contrib_freertos INTERFACE)
+    target_sources(pico_lwip_contrib_freertos INTERFACE
+            ${PICO_LWIP_CONTRIB_PATH}/ports/freertos/sys_arch.c
+            )
+    target_include_directories(pico_lwip_contrib_freertos INTERFACE
+            ${PICO_LWIP_CONTRIB_PATH}/ports/freertos/include
+            )
+    target_link_libraries(pico_lwip_contrib_freertos INTERFACE
+            pico_lwip_arch)
+
+    pico_promote_common_scope_vars()
+endif()
diff --git a/src/rp2_common/pico_lwip/doc.h b/src/rp2_common/pico_lwip/doc.h
new file mode 100644
index 0000000..ede60b6
--- /dev/null
+++ b/src/rp2_common/pico_lwip/doc.h
@@ -0,0 +1,44 @@
+/**
+ * \defgroup pico_lwip pico_lwip
+ * \brief Wrapper libraries for <a href="https://savannah.nongnu.org/projects/lwip/lwIP">lwIP</a>
+ *
+ * The following libraries are provided that contain the equivalent lwIP functionality groups:
+ *
+ * * \c \b pico_lwip_core - 
+ * * \c \b pico_lwip_core4 - 
+ * * \c \b pico_lwip_core6 - 
+ * * \c \b pico_lwip_netif -
+ * * \c \b pico_lwip_sixlowpan - 
+ * * \c \b pico_lwip_ppp -
+ * * \c \b pico_lwip_api -
+ *
+ * The following libraries are provided that contain the equivalent lwIP application support:
+ *
+ * * \c \b pico_lwip_snmp -
+ * * \c \b pico_lwip_http -
+ * * \c \b pico_lwip_makefsdata - 
+ * * \c \b pico_lwip_iperf - 
+ * * \c \b pico_lwip_smtp - 
+ * * \c \b pico_lwip_sntp - 
+ * * \c \b pico_lwip_mdns - 
+ * * \c \b pico_lwip_netbios - 
+ * * \c \b pico_lwip_tftp - 
+ * * \c \b pico_lwip_mbedtls -
+ *
+ * The SDK Provides a common set of functionality in \c \p pico_lwip which aggregates:
+ *
+ * * \c \b pico_lwip_core -
+ * * \c \b pico_lwip_core4 -
+ * * \c \b pico_lwip_core6 -
+ * * \c \b pico_lwip_netif -
+ * * \c \b pico_lwip_sixlowpan -
+ * * \c \b pico_lwip_ppp -
+ *
+ * The following additional libraries are provided:
+ *
+ * * \c \b pico_lwip - Aggregates the lwIP RAW API: \c \b pico_lwip_core, \c \b pico_lwip_core4, \c \b pico_lwip_core6, \c \b pico_lwip_api, \c \b pico_lwip_netif, \c \b pico_lwip_sixlowpan and \c \b pico_lwip_ppp. It does
+ * not include \c \b pico_lwip_api, which requires NO_SYS=0. You should include the latter separately if you want it.
+ *
+ * * \c \b pico_lwip_arch - lwIP required compiler adapters. This is not included in \c \b pico_lwip in case you wish to replace them.
+ * * \c \b pico_lwip_nosys - basic stub functions for NO_SYS mode.
+ */
diff --git a/src/rp2_common/pico_lwip/include/arch/cc.h b/src/rp2_common/pico_lwip/include/arch/cc.h
new file mode 100644
index 0000000..447e4d2
--- /dev/null
+++ b/src/rp2_common/pico_lwip/include/arch/cc.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2001-2003 Swedish Institute of Computer Science.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ *    derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+ * SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+ * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+ * OF SUCH DAMAGE.
+ *
+ * This file is part of the lwIP TCP/IP stack.
+ *
+ * Author: Adam Dunkels <adam@sics.se>
+ *
+ */
+#ifndef __CC_H__
+#define __CC_H__
+
+#if NO_SYS
+// todo really we should just not allow SYS_LIGHTWEIGHT_PROT for nosys mode (it doesn't do anything anyway)
+typedef int sys_prot_t;
+#endif
+
+/* define compiler specific symbols */
+#if defined (__ICCARM__)
+
+#define PACK_STRUCT_BEGIN
+#define PACK_STRUCT_STRUCT
+#define PACK_STRUCT_END
+#define PACK_STRUCT_FIELD(x) x
+#define PACK_STRUCT_USE_INCLUDES
+
+#elif defined (__CC_ARM)
+
+#define PACK_STRUCT_BEGIN __packed
+#define PACK_STRUCT_STRUCT
+#define PACK_STRUCT_END
+#define PACK_STRUCT_FIELD(x) x
+
+#elif defined (__GNUC__)
+
+#define PACK_STRUCT_BEGIN
+#define PACK_STRUCT_STRUCT __attribute__ ((__packed__))
+#define PACK_STRUCT_END
+#define PACK_STRUCT_FIELD(x) x
+
+#elif defined (__TASKING__)
+
+#define PACK_STRUCT_BEGIN
+#define PACK_STRUCT_STRUCT
+#define PACK_STRUCT_END
+#define PACK_STRUCT_FIELD(x) x
+
+#endif
+
+#define LWIP_PLATFORM_ASSERT(x) do { if(!(x)) while(1); } while(0)
+
+unsigned int pico_lwip_rand(void);
+#ifndef LWIP_RAND
+// Use ROSC based random number generation, more for the fact that rand() may not be seeded, than anything else
+#define LWIP_RAND pico_lwip_rand
+#endif
+#endif /* __CC_H__ */
diff --git a/src/rp2_common/pico_lwip/nosys.c b/src/rp2_common/pico_lwip/nosys.c
new file mode 100644
index 0000000..4e7605d
--- /dev/null
+++ b/src/rp2_common/pico_lwip/nosys.c
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "lwip/init.h"
+#include "pico/time.h"
+
+#if NO_SYS
+/* lwip has provision for using a mutex, when applicable */
+sys_prot_t sys_arch_protect(void) {
+    return 0;
+}
+
+void sys_arch_unprotect(sys_prot_t pval) {
+    (void) pval;
+}
+
+/* lwip needs a millisecond time source, and the TinyUSB board support code has one available */
+uint32_t sys_now(void) {
+    return to_ms_since_boot(get_absolute_time());
+}
+
+#endif
+
diff --git a/src/rp2_common/pico_lwip/random.c b/src/rp2_common/pico_lwip/random.c
new file mode 100644
index 0000000..2c4cdf8
--- /dev/null
+++ b/src/rp2_common/pico_lwip/random.c
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "pico.h"
+#include "hardware/structs/rosc.h"
+
+static uint8_t pico_lwip_random_byte(int cycles) {
+    static uint8_t byte;
+    assert(cycles >= 8);
+    assert(rosc_hw->status & ROSC_STATUS_ENABLED_BITS);
+    for(int i=0;i<cycles;i++) {
+        // picked a fairly arbitrary polynomial of 0x35u - this doesn't have to be crazily uniform.
+        byte = ((byte << 1) | rosc_hw->randombit) ^ (byte & 0x80u ? 0x35u : 0);
+        // delay a little because the random bit is a little slow
+        busy_wait_at_least_cycles(30);
+    }
+    return byte;
+}
+
+unsigned int pico_lwip_rand(void) {
+    uint32_t value = 0;
+    for (int i = 0; i < 4; i++) {
+        value = (value << 8u) | pico_lwip_random_byte(32);
+    }
+    return value;
+}
\ No newline at end of file
diff --git a/src/rp2_common/tinyusb/CMakeLists.txt b/src/rp2_common/tinyusb/CMakeLists.txt
index ea919cd..8978788 100644
--- a/src/rp2_common/tinyusb/CMakeLists.txt
+++ b/src/rp2_common/tinyusb/CMakeLists.txt
@@ -53,5 +53,6 @@
                 PROPERTIES
                 COMPILE_FLAGS "-Wno-stringop-overflow -Wno-array-bounds")
     endfunction()
+
     pico_promote_common_scope_vars()
 endif()
diff --git a/test/kitchen_sink/CMakeLists.txt b/test/kitchen_sink/CMakeLists.txt
index 3487fbb..a201991 100644
--- a/test/kitchen_sink/CMakeLists.txt
+++ b/test/kitchen_sink/CMakeLists.txt
@@ -41,11 +41,6 @@
     pico_unique_id
     pico_util
 )
-# todo this is full of warnings atm
-#if (TARGET tinyusb_device)
-#    target_include_directories(kitchen_sink_libs INTERFACE ${CMAKE_CURRENT_LIST_DIR})
-#    target_link_libraries(kitchen_sink_libs INTERFACE tinyusb_device)
-#endif()
 
 add_library(kitchen_sink_options INTERFACE)
 
@@ -63,7 +58,7 @@
         -Wcast-qual
         -Wfloat-equal
         -Wmissing-format-attribute
-        -Wconversion
+        #-Wconversion
         -Wsign-compare
         $<$<COMPILE_LANGUAGE:C>:-Wstrict-prototypes>
 
@@ -100,6 +95,13 @@
     # TinyUSB itself, so we have to guard against TinyUSB not being present with the above if
     suppress_tinyusb_warnings()
 endif()
+if (TARGET pico_lwip)
+    set_source_files_properties(
+            ${PICO_LWIP_PATH}/src/core/ipv4/ip4_frag.c
+            PROPERTIES
+            COMPILE_FLAGS "-Wno-null-dereference")
+
+endif()
 target_link_libraries(kitchen_sink_extra_stdio kitchen_sink_libs kitchen_sink_options)
 pico_add_extra_outputs(kitchen_sink_extra_stdio)
 pico_enable_stdio_usb(kitchen_sink_extra_stdio 1)
@@ -119,3 +121,25 @@
 target_link_libraries(kitchen_sink_cpp kitchen_sink_libs kitchen_sink_options)
 pico_set_program_name(kitchen_sink_cpp "Wombat tentacles CPP")
 pico_add_extra_outputs(kitchen_sink_cpp)
+
+if (TARGET pico_cyw43_arch)
+    # for lwipopts.h
+    add_executable(kitchen_sink_lwip_poll ${CMAKE_CURRENT_LIST_DIR}/kitchen_sink.c)
+    target_link_libraries(kitchen_sink_lwip_poll kitchen_sink_libs kitchen_sink_options)
+    pico_add_extra_outputs(kitchen_sink_lwip_poll)
+    target_link_libraries(kitchen_sink_lwip_poll
+            pico_cyw43_arch_lwip_poll)
+    # for lwipopts.h
+    target_include_directories(kitchen_sink_lwip_poll PRIVATE
+            ${CMAKE_CURRENT_LIST_DIR})
+
+    add_executable(kitchen_sink_lwip_background ${CMAKE_CURRENT_LIST_DIR}/kitchen_sink.c)
+    target_link_libraries(kitchen_sink_lwip_background kitchen_sink_libs kitchen_sink_options)
+    pico_add_extra_outputs(kitchen_sink_lwip_background)
+    target_link_libraries(kitchen_sink_lwip_background
+            pico_cyw43_arch_lwip_threadsafe_background)
+    # for lwipopts.h
+    target_include_directories(kitchen_sink_lwip_background PRIVATE
+            ${CMAKE_CURRENT_LIST_DIR})
+
+endif()
diff --git a/test/kitchen_sink/kitchen_sink.c b/test/kitchen_sink/kitchen_sink.c
index 62fe03a..a49654d 100644
--- a/test/kitchen_sink/kitchen_sink.c
+++ b/test/kitchen_sink/kitchen_sink.c
@@ -47,6 +47,9 @@
 #include "pico/sync.h"
 #include "pico/time.h"
 #include "pico/unique_id.h"
+#if LIB_PICO_CYW43_ARCH
+#include "pico/cyw43_arch.h"
+#endif
 
 #include "hardware/structs/adc.h"
 #include "hardware/structs/bus_ctrl.h"
diff --git a/test/kitchen_sink/lwipopts.h b/test/kitchen_sink/lwipopts.h
new file mode 100644
index 0000000..58b704a
--- /dev/null
+++ b/test/kitchen_sink/lwipopts.h
@@ -0,0 +1,14 @@
+#ifndef _LWIPOPTS_H
+#define _LWIPOPTS_H
+
+// dummy lwip opts to allow compilation
+
+#define NO_SYS                      1
+#define LWIP_DHCP                   1
+#define LWIP_RAW                    1
+#define LWIP_NETIF_HOSTNAME         1
+#define LWIP_NETIF_STATUS_CALLBACK  1
+#define LWIP_DNS                    1
+#define LWIP_SOCKET                 0
+#define LWIP_NETCONN                0
+#endif
\ No newline at end of file