Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
63 changes: 51 additions & 12 deletions universal/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
55 changes: 55 additions & 0 deletions universal/README.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions universal/blink_universal/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
85 changes: 85 additions & 0 deletions universal/blink_universal/blink_universal.c
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 7 additions & 0 deletions universal/wrapper/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading
Loading