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 f80fbd436..84a1bc0df 100644 --- a/universal/CMakeLists.txt +++ b/universal/CMakeLists.txt @@ -20,10 +20,15 @@ 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(oneValueArgs SOURCE_TARGET PADDING PACKADDR) + 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) @@ -41,6 +46,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) @@ -100,13 +112,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 @@ -117,7 +151,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 @@ -135,8 +169,13 @@ 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 + # 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..9b4f3ede5 --- /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 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**. + +## 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 + - Ditto + +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 appears at the start of the +flash address space, and executes from there. + +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 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/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..754170919 --- /dev/null +++ b/universal/blink_universal/blink_universal.c @@ -0,0 +1,85 @@ +/** + * 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 + +enum BOARD_TYPE { + BOARD_TYPE_PICO, // Pico-series board + BOARD_TYPE_PICO_W, // Pico W-series board + 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); + 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) { + // Pico W-series board + return BOARD_TYPE_PICO_W; + } else if (value == 0) { + // Pico-series board + return BOARD_TYPE_PICO; + } else { + // Unknown board + return BOARD_TYPE_UNKNOWN; + } +} + +// Perform initialisation +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 + // 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, 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); + } +} + +int main() { + 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, board_type); + sleep_ms(LED_DELAY_MS); + pico_set_led(false, board_type); + 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