From c364b9414da5510583b13d6d13f30a8289e1a808 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Mon, 23 Mar 2026 15:00:14 +0000 Subject: [PATCH 1/6] Make universal blink example work with W boards Detects between W and non-W boards using the ADC, as described in "Connecting to the Internet with Raspberry Pi Pico W-series" This method should work for all boards powered from VSYS, which should cover all standard use cases for the blink example --- universal/CMakeLists.txt | 16 +- universal/blink_universal/CMakeLists.txt | 18 ++ universal/blink_universal/blink_universal.c | 66 +++++++ universal/wrapper/CMakeLists.txt | 7 + .../wrapper/boards_universal/universal.h | 170 ++++++++++++++++++ 5 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 universal/blink_universal/CMakeLists.txt create mode 100644 universal/blink_universal/blink_universal.c create mode 100644 universal/wrapper/boards_universal/universal.h diff --git a/universal/CMakeLists.txt b/universal/CMakeLists.txt index f80fbd436..2be32c7d8 100644 --- a/universal/CMakeLists.txt +++ b/universal/CMakeLists.txt @@ -21,7 +21,7 @@ include(ExternalProject) # The build will output a TARGET.bin file which can be written using picotool, and a # TARGET.uf2 file which can be dragged and dropped onto the device in BOOTSEL mode function (add_universal_target TARGET SOURCE) - set(oneValueArgs SOURCE_TARGET PADDING PACKADDR) + set(oneValueArgs SOURCE_TARGET PADDING PACKADDR BOARD_RP2040 BOARD_RP2350) set(multiValueArgs PLATFORMS) cmake_parse_arguments(PARSE_ARGV 2 PARSED "" "${oneValueArgs}" "${multiValueArgs}") @@ -41,6 +41,13 @@ function (add_universal_target TARGET SOURCE) if (PARSED_PLATFORMS) set(PLATFORMS ${PARSED_PLATFORMS}) endif() + # these could be set as CMake variables, so only override if explicitly passed as arguments + if (PARSED_BOARD_RP2040) + set(PICO_BOARD_RP2040 ${PARSED_BOARD_RP2040}) + endif() + if (PARSED_BOARD_RP2350) + set(PICO_BOARD_RP2350 ${PARSED_BOARD_RP2350}) + endif() # rp2040 must come first, as that has checksum requirements at the start of the binary list(FIND PLATFORMS "rp2040" idx) if (idx GREATER 0) @@ -135,8 +142,11 @@ add_universal_target(hello_universal # blink binary add_universal_target(blink_universal - ${CMAKE_CURRENT_LIST_DIR}/../blink - SOURCE_TARGET blink + ${CMAKE_CURRENT_LIST_DIR}/blink_universal + SOURCE_TARGET blink_universal + BOARD_RP2040 universal + BOARD_RP2350 universal + PLATFORMS "rp2040;rp2350-arm-s" # Skip RISC-V, as wifi firmware takes up lots of space ) # nuke binary - is no_flash, so needs to be sent to SRAM on RP2040 diff --git a/universal/blink_universal/CMakeLists.txt b/universal/blink_universal/CMakeLists.txt new file mode 100644 index 000000000..7192989a8 --- /dev/null +++ b/universal/blink_universal/CMakeLists.txt @@ -0,0 +1,18 @@ +if (NOT PICO_BOARD STREQUAL "universal") + message(FATAL_ERROR "PICO_BOARD for blink_universal must be set to 'universal', not '${PICO_BOARD}'") + return() +endif() + +add_executable(blink_universal + blink_universal.c + ) + +# pull in common dependencies +target_link_libraries(blink_universal pico_stdlib hardware_adc) +target_link_libraries(blink_universal pico_cyw43_arch_none) + +# create map/bin/hex file etc. +pico_add_extra_outputs(blink_universal) + +# add url via pico_set_program_url +example_auto_set_url(blink_universal) diff --git a/universal/blink_universal/blink_universal.c b/universal/blink_universal/blink_universal.c new file mode 100644 index 000000000..82fa8cb41 --- /dev/null +++ b/universal/blink_universal/blink_universal.c @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include "hardware/adc.h" + +#ifndef LED_DELAY_MS +#define LED_DELAY_MS 250 +#endif + +bool detect_is_w_using_adc(void) { + adc_init(); + adc_gpio_init(PICO_VSYS_PIN); + adc_select_input(PICO_VSYS_PIN - ADC_BASE_PIN); + const float conversion_factor = 3.3f / (1 << 12); + uint16_t result = adc_read(); + float voltage = result * conversion_factor; + + gpio_init(PICO_DEFAULT_LED_PIN); + gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_IN); + bool value = gpio_get(PICO_DEFAULT_LED_PIN); + + if (value == 0 && voltage < 0.1) { + return true; + } else { + return false; + } +} + +// Perform initialisation +int pico_led_init(bool is_w) { + if (is_w) { + return cyw43_arch_init(); + } else { + // A device like Pico that uses a GPIO for the LED will define PICO_DEFAULT_LED_PIN + // so we can use normal GPIO functionality to turn the led on and off + gpio_init(PICO_DEFAULT_LED_PIN); + gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT); + return PICO_OK; + } +} + +// Turn the led on or off +void pico_set_led(bool led_on, bool is_w) { + if (is_w) { + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on); + } else { + gpio_put(PICO_DEFAULT_LED_PIN, led_on); + } +} + +int main() { + bool is_w = detect_is_w_using_adc(); + int rc = pico_led_init(is_w); + hard_assert(rc == PICO_OK); + while (true) { + pico_set_led(true, is_w); + sleep_ms(LED_DELAY_MS); + pico_set_led(false, is_w); + sleep_ms(LED_DELAY_MS); + } +} diff --git a/universal/wrapper/CMakeLists.txt b/universal/wrapper/CMakeLists.txt index 548f9310e..b5eb4839e 100644 --- a/universal/wrapper/CMakeLists.txt +++ b/universal/wrapper/CMakeLists.txt @@ -29,6 +29,13 @@ elseif(PICO_BOARD_RP2350 AND (PICO_PLATFORM MATCHES rp2350)) set(PICO_BOARD ${PICO_BOARD_RP2350}) endif() +# Add universal board header dir +if (DEFINED ENV{PICO_BOARD_HEADER_DIRS}) + set(PICO_BOARD_HEADER_DIRS $ENV{PICO_BOARD_HEADER_DIRS}) + message("Using PICO_BOARD_HEADER_DIRS from environment ('${PICO_BOARD_HEADER_DIRS}')") +endif() +set(PICO_BOARD_HEADER_DIRS ${PICO_BOARD_HEADER_DIRS} ${CMAKE_CURRENT_LIST_DIR}/boards_universal) + # Pull in SDK (must be before project) include(${PICO_EXAMPLES_PATH}/pico_sdk_import.cmake) diff --git a/universal/wrapper/boards_universal/universal.h b/universal/wrapper/boards_universal/universal.h new file mode 100644 index 000000000..e2a2aba3f --- /dev/null +++ b/universal/wrapper/boards_universal/universal.h @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2026 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 +// ----------------------------------------------------- + +#ifndef _BOARDS_UNIVERSAL_H +#define _BOARDS_UNIVERSAL_H + +pico_board_cmake_set(PICO_CYW43_SUPPORTED, 1) + +// For board detection +#define RASPBERRYPI_UNIVERSAL + +// --- RP2350 VARIANT --- +#define PICO_RP2350A 1 + +// --- 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 --- +#ifndef PICO_DEFAULT_LED_PIN +#define PICO_DEFAULT_LED_PIN 25 +#endif +// 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 + +pico_board_cmake_set_default(PICO_FLASH_SIZE_BYTES, (2 * 1024 * 1024)) +#ifndef PICO_FLASH_SIZE_BYTES +#define PICO_FLASH_SIZE_BYTES (2 * 1024 * 1024) +#endif +// Drive high to force power supply into PWM mode (lower ripple on 3V3 at light loads) +#define PICO_SMPS_MODE_PIN 23 + +#ifndef PICO_RP2040_B0_SUPPORTED +#define PICO_RP2040_B0_SUPPORTED 1 +#endif + +#ifndef PICO_RP2040_B1_SUPPORTED +#define PICO_RP2040_B1_SUPPORTED 1 +#endif + +pico_board_cmake_set_default(PICO_RP2350_A2_SUPPORTED, 1) +#ifndef PICO_RP2350_A2_SUPPORTED +#define PICO_RP2350_A2_SUPPORTED 1 +#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 + +// Drive high to force power supply into PWM mode (lower ripple on 3V3 at light loads) +// As this is a CYW43 pin you can do this by calling cyw43_gpio_set +#ifndef CYW43_WL_GPIO_SMPS_PIN +#define CYW43_WL_GPIO_SMPS_PIN 1 +#endif + +// If CYW43_WL_GPIO_VBUS_PIN is defined then a CYW43 GPIO has to be used to read VBUS. +// This can be passed to cyw43_arch_gpio_get to determine if the device is battery powered. +// PICO_VBUS_PIN and CYW43_WL_GPIO_VBUS_PIN should not both be defined. +#ifndef CYW43_WL_GPIO_VBUS_PIN +#define CYW43_WL_GPIO_VBUS_PIN 2 +#endif + +// If CYW43_USES_VSYS_PIN is defined then CYW43 uses the VSYS GPIO (defined by PICO_VSYS_PIN) for other purposes. +// If this is the case, to use the VSYS GPIO it's necessary to ensure CYW43 is not using it. +// This can be achieved by wrapping the use of the VSYS GPIO in cyw43_thread_enter / cyw43_thread_exit. +#ifndef CYW43_USES_VSYS_PIN +#define CYW43_USES_VSYS_PIN 1 +#endif + +// The GPIO Pin used to read VBUS to determine if the device is battery powered. +#ifndef PICO_VBUS_PIN +#define PICO_VBUS_PIN 24 +#endif + +// The GPIO Pin used to monitor VSYS. Typically you would use this with ADC. +// There is an example in adc/read_vsys in pico-examples. +#ifndef PICO_VSYS_PIN +#define PICO_VSYS_PIN 29 +#endif + +// cyw43 SPI pins can't be changed at runtime +#ifndef CYW43_PIN_WL_DYNAMIC +#define CYW43_PIN_WL_DYNAMIC 0 +#endif + +// gpio pin to power up the cyw43 chip +#ifndef CYW43_DEFAULT_PIN_WL_REG_ON +#define CYW43_DEFAULT_PIN_WL_REG_ON 23u +#endif + +// gpio pin for spi data out to the cyw43 chip +#ifndef CYW43_DEFAULT_PIN_WL_DATA_OUT +#define CYW43_DEFAULT_PIN_WL_DATA_OUT 24u +#endif + +// gpio pin for spi data in from the cyw43 chip +#ifndef CYW43_DEFAULT_PIN_WL_DATA_IN +#define CYW43_DEFAULT_PIN_WL_DATA_IN 24u +#endif + +// gpio (irq) pin for the irq line from the cyw43 chip +#ifndef CYW43_DEFAULT_PIN_WL_HOST_WAKE +#define CYW43_DEFAULT_PIN_WL_HOST_WAKE 24u +#endif + +// gpio pin for the spi clock line to the cyw43 chip +#ifndef CYW43_DEFAULT_PIN_WL_CLOCK +#define CYW43_DEFAULT_PIN_WL_CLOCK 29u +#endif + +// gpio pin for the spi chip select to the cyw43 chip +#ifndef CYW43_DEFAULT_PIN_WL_CS +#define CYW43_DEFAULT_PIN_WL_CS 25u +#endif + +#endif From ab408f8d5395069dfae91009fcb646943b35e2d7 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Mon, 23 Mar 2026 15:39:00 +0000 Subject: [PATCH 2/6] Reduce size by making it only a universal UF2 Halves the size of the UF2 file, and halves the size in flash --- universal/CMakeLists.txt | 43 ++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/universal/CMakeLists.txt b/universal/CMakeLists.txt index 2be32c7d8..02e68aeb2 100644 --- a/universal/CMakeLists.txt +++ b/universal/CMakeLists.txt @@ -21,9 +21,10 @@ include(ExternalProject) # The build will output a TARGET.bin file which can be written using picotool, and a # TARGET.uf2 file which can be dragged and dropped onto the device in BOOTSEL mode function (add_universal_target TARGET SOURCE) + set(zeroValueArgs SEPARATE_RP2040) set(oneValueArgs SOURCE_TARGET PADDING PACKADDR BOARD_RP2040 BOARD_RP2350) set(multiValueArgs PLATFORMS) - cmake_parse_arguments(PARSE_ARGV 2 PARSED "" "${oneValueArgs}" "${multiValueArgs}") + cmake_parse_arguments(PARSE_ARGV 2 PARSED "${zeroValueArgs}" "${oneValueArgs}" "${multiValueArgs}") set(SOURCE_TARGET ${TARGET}) if (PARSED_SOURCE_TARGET) @@ -107,13 +108,35 @@ function (add_universal_target TARGET SOURCE) message(FATAL_ERROR "Cannot link universal binary without picotool") endif() - # Link the binaries for different platforms into a single block loop, with - # appropriate rolling window deltas. This creates a universal binary file, - # which will run on any of the platforms when loaded using picotool. - add_custom_target(${TARGET}_combined - COMMAND picotool link ${COMBINED} ${BINS} --pad ${PADDING} - DEPENDS ${DEPS} - ) + list(FIND PLATFORMS "rp2040" idx) + if (idx EQUAL 0 AND PARSED_SEPARATE_RP2040) + # Don't include RP2040 bin in the combined BIN, just include it in the RP2040 UF2 + # POP_FRONT only added in CMake 3.15, so use GET and REMOVE_AT instead + list(GET BINS 0 RP2040_BIN) + list(REMOVE_AT BINS 0) + set(RP2040_COMBINED ${RP2040_BIN}) + else() + set(RP2040_COMBINED ${COMBINED}) + endif() + + list(LENGTH BINS BINS_COUNT) + if (BINS_COUNT GREATER 1) + # Link the binaries for different platforms into a single block loop, with + # appropriate rolling window deltas. This creates a universal binary file, + # which will run on any of the platforms when loaded using picotool. + add_custom_target(${TARGET}_combined + COMMAND picotool link ${COMBINED} ${BINS} --pad ${PADDING} + DEPENDS ${DEPS} + ) + else() + # Only one binary left, so no picotool link is needed - just copy instead + # This could be the case if only building for rp2040 and rp2350-arm-s, + # with SEPARATE_RP2040 set + add_custom_target(${TARGET}_combined + COMMAND ${CMAKE_COMMAND} -E copy ${BINS} ${COMBINED} + DEPENDS ${DEPS} + ) + endif() # Create UF2s targeting the absolute and rp2040 family IDs, then combine these # into a single universal UF2. This is required as there isn't a single family @@ -124,7 +147,7 @@ function (add_universal_target TARGET SOURCE) DEPENDS ${TARGET}_combined ) add_custom_target(${TARGET}_rp2040_uf2 - COMMAND picotool uf2 convert ${COMBINED} ${BINDIR}/rp2040.uf2 --family rp2040 --offset ${PACKADDR} + COMMAND picotool uf2 convert ${RP2040_COMBINED} ${BINDIR}/rp2040.uf2 --family rp2040 --offset ${PACKADDR} DEPENDS ${TARGET}_combined ) add_custom_target(${TARGET}_uf2 @@ -146,7 +169,7 @@ add_universal_target(blink_universal SOURCE_TARGET blink_universal BOARD_RP2040 universal BOARD_RP2350 universal - PLATFORMS "rp2040;rp2350-arm-s" # Skip RISC-V, as wifi firmware takes up lots of space + SEPARATE_RP2040 PLATFORMS "rp2040;rp2350-arm-s" # Skip RISC-V and keep RP2040 separate, as wifi firmware takes up lots of space ) # nuke binary - is no_flash, so needs to be sent to SRAM on RP2040 From 00af391f33bc6a69be042008bb30375ffc5ff3ae Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Wed, 25 Mar 2026 16:24:52 +0000 Subject: [PATCH 3/6] Add function docs, add separate universal README, and fixup main README Also switch to board_type instead of is_w so the functions are clearer --- README.md | 6 ++- universal/CMakeLists.txt | 8 ++- universal/README.md | 55 +++++++++++++++++++++ universal/blink_universal/blink_universal.c | 41 ++++++++++----- 4 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 universal/README.md diff --git a/README.md b/README.md index f43a7339a..eda6d39bb 100644 --- a/README.md +++ b/README.md @@ -408,12 +408,14 @@ App|Description These are examples of how to build universal binaries which run on RP2040, and RP2350 Arm & RISC-V. These require you to set `PICO_ARM_TOOLCHAIN_PATH` and `PICO_RISCV_TOOLCHAIN_PATH` to appropriate paths, to ensure you have compilers for both architectures. +These are designed for dragging & dropping onto a device, so may not load as expected when using `picotool`. +See the separate [README](universal/README.md) for more details of how these work. App|Description ---|--- -[blink_universal](universal/CMakeLists.txt#L126) | Same as the [blink](blink) example, but universal. +[blink_universal](universal/blink_universal) | A universal blink which works for all Pico-series and Pico W-series boards. [hello_universal](universal/hello_universal) | The obligatory Hello World program for Pico (USB and serial output). On RP2350 it will reboot to the other architecture after every 10 prints. -[nuke_universal](universal/CMakeLists.txt#L132) | Same as the [nuke](flash/nuke) example, but universal. On RP2350 runs as a packaged SRAM binary, so it is written to flash and copied to SRAM by the bootloader. +[nuke_universal](universal/CMakeLists.txt) | Same as the [nuke](flash/nuke) example, but universal. ### USB Device diff --git a/universal/CMakeLists.txt b/universal/CMakeLists.txt index 02e68aeb2..84a1bc0df 100644 --- a/universal/CMakeLists.txt +++ b/universal/CMakeLists.txt @@ -20,6 +20,10 @@ include(ExternalProject) # # The build will output a TARGET.bin file which can be written using picotool, and a # TARGET.uf2 file which can be dragged and dropped onto the device in BOOTSEL mode +# +# If SEPARATE_RP2040 is set, the RP2040 binary will not be included in the block loop, +# so the RP2040 UF2 file will only contain the RP2040 binary, and the RP2350 UF2 file will +# contain the combined binary excluding the RP2040 binary. function (add_universal_target TARGET SOURCE) set(zeroValueArgs SEPARATE_RP2040) set(oneValueArgs SOURCE_TARGET PADDING PACKADDR BOARD_RP2040 BOARD_RP2350) @@ -169,7 +173,9 @@ add_universal_target(blink_universal SOURCE_TARGET blink_universal BOARD_RP2040 universal BOARD_RP2350 universal - SEPARATE_RP2040 PLATFORMS "rp2040;rp2350-arm-s" # Skip RISC-V and keep RP2040 separate, as wifi firmware takes up lots of space + # Skip RISC-V and keep RP2040 separate, as wifi firmware takes up lots of space + PLATFORMS "rp2040;rp2350-arm-s" + SEPARATE_RP2040 ) # nuke binary - is no_flash, so needs to be sent to SRAM on RP2040 diff --git a/universal/README.md b/universal/README.md new file mode 100644 index 000000000..4ff05a0d7 --- /dev/null +++ b/universal/README.md @@ -0,0 +1,55 @@ +# Universal Examples + +These examples show ways to load the same code onto different chips, and package +it in such a way that the bootrom only executes the code compatible with that chip. + +## Universal Binary vs Universal UF2 + +There is a difference between a **Universal Binary** and a **Universal UF2**, +for the purposes of these examples: +- A **Universal Binary** is a `.bin` file that can be loaded into flash (or sram) and executed, +allowing RP2040 and RP2350 (Arm & RISC-V) to run from identical flash contents. +- A **Universal UF2** is multiple individual `.uf2` files with different family IDs +concatenated together to create a single `.uf2` file. When dragged & dropped onto a device, +only the file with a family ID corresponding to that device will be loaded onto it, and the +rest of the files will be ignored. + +A **Universal Binary** can be packaged into a UF2 file for loading onto a device. However, +as there isn't a common family ID between RP2040 and RP2350, you would have to package it into a **Universal UF2** with two copies (using `rp2040` and `absolute` family IDs), thus creating a **Universal UF2** of a **Universal Binary**. + +## How Universal Binaries work + +Universal binaries must be recognised by both the RP2040 and RP2350 bootroms. Therefore, they need the following structure for flash binaries: +- RP2040 boot2 + - Required by the RP2040 bootrom +- RP2040 binary containing an embedded block + - The embedded block contains an `IGNORED` item due to RP2350-E13, but you can use an RP2040 + `IMAGE_DEF` item instead if not using RP2350-A2 chips +- RP2350 Arm binary containing an embedded block + - In addition to the RP2350 `IMAGE_DEF` item, this embedded block contains a + `ROLLING_WINDOW_DELTA` item to translate this binary to the start of flash for execution +- RP2350 RISC-V binary containing an embedded block + - Same as the Arm one + +All of the embedded blocks are linked into one big block loop. + +These are then booted by the respective bootroms: +- **RP2040** - sees the boot2 at the start and uses that to execute the RP2040 binary, as +RP2040 has no support for embedded blocks. +- **RP2350** - sees the block loop and parses it to find the correct embedded block to boot +from (Arm vs RISC-V). It then translates the flash address according to the +`ROLLING_WINDOW_DELTA` so that the binary containing that embedded block is at the start of the +flash address space, and executes from there. + +For no_flash binaries the RP2040 boot2 is omitted as the bootrom just executes from the start +of SRAM, and instead of `ROLLING_WINDOW_DELTA` items the RP2350 binaries use `LOAD_MAP` items, +to copy the code in SRAM to the correct location for execution rather than using address +translation. + +## How you should use them + +For most use cases, **Universal UF2s** are the best option to use. They will only load into +flash the code that runs on that device. The [blink_universal](blink_universal) example uses a +Universal UF2 for that reason, as the Wi-Fi firmware is quite large. **Universal Binaries** +are only currently useful when the commonality of having a single `.bin` file for programming +outweighs the disadvantage of the extra flash usage. diff --git a/universal/blink_universal/blink_universal.c b/universal/blink_universal/blink_universal.c index 82fa8cb41..44609355b 100644 --- a/universal/blink_universal/blink_universal.c +++ b/universal/blink_universal/blink_universal.c @@ -12,7 +12,21 @@ #define LED_DELAY_MS 250 #endif -bool detect_is_w_using_adc(void) { +enum BOARD_TYPE { + BOARD_TYPE_PICO, + BOARD_TYPE_PICO_W, + BOARD_TYPE_UNKNOWN, +}; + +// Detects if PICO_VSYS_PIN is actually connected to the VSYS voltage divider, +// to determine the board type. +// Also checks that the LED pin is low, which should be the case for both +// Pico-series and Pico W-series boards. +// This will work provided that the board is being powered from VSYS (i.e. it +// is using the onboard voltage regulator). +// This method is documented in section 2.4 of Connecting to the Internet with +// Raspberry Pi Pico W-series (https://pip.raspberrypi.com/documents/RP-008257-DS). +enum BOARD_TYPE detect_board_type(void) { adc_init(); adc_gpio_init(PICO_VSYS_PIN); adc_select_input(PICO_VSYS_PIN - ADC_BASE_PIN); @@ -25,15 +39,20 @@ bool detect_is_w_using_adc(void) { bool value = gpio_get(PICO_DEFAULT_LED_PIN); if (value == 0 && voltage < 0.1) { - return true; + // Pico W-series board + return BOARD_TYPE_PICO_W; + } else if (value == 0) { + // Pico-series board + return BOARD_TYPE_PICO; } else { - return false; + // Unknown board + return BOARD_TYPE_UNKNOWN; } } // Perform initialisation -int pico_led_init(bool is_w) { - if (is_w) { +int pico_led_init(enum BOARD_TYPE board_type) { + if (board_type == BOARD_TYPE_PICO_W) { return cyw43_arch_init(); } else { // A device like Pico that uses a GPIO for the LED will define PICO_DEFAULT_LED_PIN @@ -45,8 +64,8 @@ int pico_led_init(bool is_w) { } // Turn the led on or off -void pico_set_led(bool led_on, bool is_w) { - if (is_w) { +void pico_set_led(bool led_on, enum BOARD_TYPE board_type) { + if (board_type == BOARD_TYPE_PICO_W) { cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on); } else { gpio_put(PICO_DEFAULT_LED_PIN, led_on); @@ -54,13 +73,13 @@ void pico_set_led(bool led_on, bool is_w) { } int main() { - bool is_w = detect_is_w_using_adc(); - int rc = pico_led_init(is_w); + enum BOARD_TYPE board_type = detect_board_type(); + int rc = pico_led_init(board_type); hard_assert(rc == PICO_OK); while (true) { - pico_set_led(true, is_w); + pico_set_led(true, board_type); sleep_ms(LED_DELAY_MS); - pico_set_led(false, is_w); + pico_set_led(false, board_type); sleep_ms(LED_DELAY_MS); } } From bd98af8ac7fc57afe89602373dbe5277ce96294a Mon Sep 17 00:00:00 2001 From: will-v-pi <108662275+will-v-pi@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:14:08 +0000 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Andrew Scheller Co-authored-by: will-v-pi <108662275+will-v-pi@users.noreply.github.com> --- universal/README.md | 14 +++++++------- universal/blink_universal/blink_universal.c | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/universal/README.md b/universal/README.md index 4ff05a0d7..9b4f3ede5 100644 --- a/universal/README.md +++ b/universal/README.md @@ -11,8 +11,8 @@ for the purposes of these examples: allowing RP2040 and RP2350 (Arm & RISC-V) to run from identical flash contents. - A **Universal UF2** is multiple individual `.uf2` files with different family IDs concatenated together to create a single `.uf2` file. When dragged & dropped onto a device, -only the file with a family ID corresponding to that device will be loaded onto it, and the -rest of the files will be ignored. +only the portion of the file with a family ID corresponding to that device will be processed, and the +rest of the file will be ignored. A **Universal Binary** can be packaged into a UF2 file for loading onto a device. However, as there isn't a common family ID between RP2040 and RP2350, you would have to package it into a **Universal UF2** with two copies (using `rp2040` and `absolute` family IDs), thus creating a **Universal UF2** of a **Universal Binary**. @@ -29,7 +29,7 @@ Universal binaries must be recognised by both the RP2040 and RP2350 bootroms. Th - In addition to the RP2350 `IMAGE_DEF` item, this embedded block contains a `ROLLING_WINDOW_DELTA` item to translate this binary to the start of flash for execution - RP2350 RISC-V binary containing an embedded block - - Same as the Arm one + - Ditto All of the embedded blocks are linked into one big block loop. @@ -38,18 +38,18 @@ These are then booted by the respective bootroms: RP2040 has no support for embedded blocks. - **RP2350** - sees the block loop and parses it to find the correct embedded block to boot from (Arm vs RISC-V). It then translates the flash address according to the -`ROLLING_WINDOW_DELTA` so that the binary containing that embedded block is at the start of the +`ROLLING_WINDOW_DELTA` so that the binary containing that embedded block appears at the start of the flash address space, and executes from there. -For no_flash binaries the RP2040 boot2 is omitted as the bootrom just executes from the start +For no_flash binaries the RP2040 boot2 is omitted as the RP2040 bootrom just executes from the start of SRAM, and instead of `ROLLING_WINDOW_DELTA` items the RP2350 binaries use `LOAD_MAP` items, to copy the code in SRAM to the correct location for execution rather than using address translation. ## How you should use them -For most use cases, **Universal UF2s** are the best option to use. They will only load into -flash the code that runs on that device. The [blink_universal](blink_universal) example uses a +For most use cases, **Universal UF2s** are the best option to use. They will only load the +code that runs on that device into flash. The [blink_universal](blink_universal) example uses a Universal UF2 for that reason, as the Wi-Fi firmware is quite large. **Universal Binaries** are only currently useful when the commonality of having a single `.bin` file for programming outweighs the disadvantage of the extra flash usage. diff --git a/universal/blink_universal/blink_universal.c b/universal/blink_universal/blink_universal.c index 44609355b..754170919 100644 --- a/universal/blink_universal/blink_universal.c +++ b/universal/blink_universal/blink_universal.c @@ -13,8 +13,8 @@ #endif enum BOARD_TYPE { - BOARD_TYPE_PICO, - BOARD_TYPE_PICO_W, + BOARD_TYPE_PICO, // Pico-series board + BOARD_TYPE_PICO_W, // Pico W-series board BOARD_TYPE_UNKNOWN, }; From b2625a97bc1871d9597e3d4859e9631cc7f96108 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Thu, 14 May 2026 14:53:10 +0100 Subject: [PATCH 5/6] Fix copy error when disconnecting on Windows with RP2040 Put last RP2040 UF2 block at the end of the file, so the transfer doesn't complete before the RP2350 UF2 has been fully copied (and ignored) --- universal/CMakeLists.txt | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/universal/CMakeLists.txt b/universal/CMakeLists.txt index 84a1bc0df..1cbd7cc77 100644 --- a/universal/CMakeLists.txt +++ b/universal/CMakeLists.txt @@ -146,18 +146,31 @@ function (add_universal_target TARGET SOURCE) # into a single universal UF2. This is required as there isn't a single family # ID which is accepted by both RP2040 and RP2350. Instead, the 2 UF2 files are # concatenated together and the device ignores the part not targeting it. - add_custom_target(${TARGET}_rp2350_uf2 - COMMAND picotool uf2 convert ${COMBINED} ${BINDIR}/rp2350.uf2 --family absolute --offset ${PACKADDR} + add_custom_target(${TARGET}_absolute_uf2 + COMMAND picotool uf2 convert ${COMBINED} ${BINDIR}/absolute.uf2 --family absolute --offset ${PACKADDR} DEPENDS ${TARGET}_combined ) add_custom_target(${TARGET}_rp2040_uf2 COMMAND picotool uf2 convert ${RP2040_COMBINED} ${BINDIR}/rp2040.uf2 --family rp2040 --offset ${PACKADDR} DEPENDS ${TARGET}_combined ) - add_custom_target(${TARGET}_uf2 - COMMAND ${CMAKE_COMMAND} -E cat ${BINDIR}/rp2040.uf2 ${BINDIR}/rp2350.uf2 > ${BINDIR}/${TARGET}.uf2 - DEPENDS ${TARGET}_rp2350_uf2 ${TARGET}_rp2040_uf2 - ) + + if (CMAKE_HOST_UNIX) + # Can use tail and truncate to make a better UF2 file + add_custom_target(${TARGET}_uf2 + COMMAND tail -c 512 ${BINDIR}/rp2040.uf2 > ${BINDIR}/rp2040.end.uf2 + COMMAND cat ${BINDIR}/rp2040.uf2 > ${BINDIR}/rp2040.start.uf2 + COMMAND truncate --size=-512 ${BINDIR}/rp2040.start.uf2 + COMMAND cat ${BINDIR}/rp2040.start.uf2 ${BINDIR}/absolute.uf2 ${BINDIR}/rp2040.end.uf2 > ${BINDIR}/${TARGET}.uf2 + DEPENDS ${TARGET}_absolute_uf2 ${TARGET}_rp2040_uf2 + ) + else() + # Not guaranteed to have tail or truncate, so + add_custom_target(${TARGET}_uf2 + COMMAND ${CMAKE_COMMAND} -E cat ${BINDIR}/rp2040.uf2 ${BINDIR}/absolute.uf2 > ${BINDIR}/${TARGET}.uf2 + DEPENDS ${TARGET}_absolute_uf2 ${TARGET}_rp2040_uf2 + ) + endif() add_dependencies(${TARGET} ${TARGET}_combined ${TARGET}_uf2) endfunction() From 58e3256aac8c1114e4c512a361eec1d09c367b8f Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Thu, 14 May 2026 15:57:45 +0100 Subject: [PATCH 6/6] Improve comments --- universal/CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/universal/CMakeLists.txt b/universal/CMakeLists.txt index 1cbd7cc77..41983ac33 100644 --- a/universal/CMakeLists.txt +++ b/universal/CMakeLists.txt @@ -156,7 +156,10 @@ function (add_universal_target TARGET SOURCE) ) if (CMAKE_HOST_UNIX) - # Can use tail and truncate to make a better UF2 file + # If you just concatenate the files, then on Windows you get an alarming 'write error' dialog when copying + # large UF2s, because the device reboots too soon. Moving the final RP2040 UF2 block to the end of the file + # fixes this, as it won't reboot until the final block is received. This fix requires `tail` and `truncate`, + # so only use it when building on Unix-like systems. add_custom_target(${TARGET}_uf2 COMMAND tail -c 512 ${BINDIR}/rp2040.uf2 > ${BINDIR}/rp2040.end.uf2 COMMAND cat ${BINDIR}/rp2040.uf2 > ${BINDIR}/rp2040.start.uf2 @@ -165,7 +168,8 @@ function (add_universal_target TARGET SOURCE) DEPENDS ${TARGET}_absolute_uf2 ${TARGET}_rp2040_uf2 ) else() - # Not guaranteed to have tail or truncate, so + # Not guaranteed to have `tail` or `truncate`, so just concatenate the files as before - this still loads + # the file onto the device correctly. add_custom_target(${TARGET}_uf2 COMMAND ${CMAKE_COMMAND} -E cat ${BINDIR}/rp2040.uf2 ${BINDIR}/absolute.uf2 > ${BINDIR}/${TARGET}.uf2 DEPENDS ${TARGET}_absolute_uf2 ${TARGET}_rp2040_uf2