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
1 change: 1 addition & 0 deletions .claude/skills/build/scripts/check_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
'lpc55': ['lpcxpresso55s69'],
'maxim': ['max32666fthr'],
'mimxrt10xx': ['imxrt1064_evk', 'imxrt1170_evk'],
'rp2350': ['intechstudio_grid_rp2350'],
'samd51': ['intech_grid'],
'stm32f3': ['stm32f303disco'],
'stm32f4': ['stm32f411ve_discovery', 'feather_stm32f405_express'],
Expand Down
4 changes: 2 additions & 2 deletions .claude/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ def test_app_nothing_builds_is_uncovered(self):

def test_top_level_app_selects_ports_whose_backend_compiles_it(self):
ports = {cb.port_of(b) for b, _ in classify('apps/blinky/src/blinky.c')[2]}
self.assertEqual(ports, {'espressif', 'lpc55', 'maxim', 'mimxrt10xx', 'samd51', 'stm32f3', 'stm32f4', 'stm32h5',
'stm32h7'})
self.assertEqual(ports, {'espressif', 'lpc55', 'maxim', 'mimxrt10xx', 'rp2350', 'samd51', 'stm32f3', 'stm32f4',
'stm32h5', 'stm32h7'})

def test_a_ports_cmake_files_select_its_cmake_build(self):
for port in ('kinetis_k32l2', 'stm32h7', 'stm32l4'):
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
- 'lpc55'
- 'maxim'
- 'mimxrt10xx'
- 'rp2350'
- 'samd51'
- 'stm32f3'
- 'stm32f4'
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Not all features are implemented for all MCUs, following is supported MCUs and i
| LPC55 | ✔ | ✔ | | | ✔ | |
| MAX32 | ✔ | ✔ | ✔ | | | |
| iMXRT | ✔ | ✔ | ✔ | | ✔ | |
| RP2350 | ✔ | ✔ | ✔ | | ✔ | |
| SAMD51 | ✔ | | ✔ | ✔ | ✔ | |
| STM32F3 | ✔ | ✔ | ✔ | ✔ | ✔ | |
| STM32F4 | ✔ | ✔ | ✔ | ✔ | ✔ | |
Expand Down
2 changes: 1 addition & 1 deletion ports/family_support.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ function(family_add_app NAME SOURCE_DIR)
-DARTIFACT_PATH=${ARTIFACT_PATH}
-DPython_EXECUTABLE=${Python_EXECUTABLE}
)
foreach (VAR LOG LOGGER CFLAGS_CLI TRACE_ETM LTO_ON)
foreach (VAR LOG LOGGER CFLAGS_CLI TRACE_ETM LTO_ON PICO_SDK_PATH PICO_PLATFORM LD_FLASH_BOOT_SIZE)
if (DEFINED ${VAR})
# LIST_SEPARATOR below turns | back into ; so a list value stays one argument
string(REPLACE ";" "|" VALUE "${${VAR}}")
Expand Down
51 changes: 51 additions & 0 deletions ports/rp2350/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
cmake_minimum_required(VERSION 3.20)

# family.cmake (included by family_support.cmake) pulls in pico_sdk_init.cmake, which must happen before project()
include(${CMAKE_CURRENT_LIST_DIR}/../family_support.cmake)

project(tinyuf2 C CXX ASM)
set(CMAKE_EXECUTABLE_SUFFIX .elf)

# pico-sdk's toolchain file does not set CMAKE_SIZE, which family_configure_common runs after the build
if (NOT CMAKE_SIZE)
string(REGEX REPLACE "gcc$" "size" CMAKE_SIZE "${CMAKE_C_COMPILER}")
endif ()

pico_sdk_init()

#------------------------------------
# TinyUF2
#------------------------------------
add_executable(tinyuf2
board_flash.c
boards.c
${TOP}/lib/tinyusb/src/portable/raspberrypi/rp2040/dcd_rp2040.c
${TOP}/lib/tinyusb/src/portable/raspberrypi/rp2040/rp2040_usb.c
)

# the WS2812 PIO program
pico_generate_pio_header(tinyuf2 ${CMAKE_CURRENT_LIST_DIR}/ws2812.pio)

# The bootloader links into the first LD_FLASH_BOOT_SIZE bytes of flash only. pico_override_flash_size() cannot be used
# for that: it also defines PICO_FLASH_SIZE_BYTES, which makes flash_range_erase/program hard_assert() on every offset
# beyond the bootloader, i.e. on every application write. So only the linker region is overridden.
set(PICO_FLASH_SIZE_BYTES_STRING "${LD_FLASH_BOOT_SIZE}")
configure_file(${PICO_SDK_PATH}/src/rp2_common/pico_standard_link/pico_flash_region.template.ld
${CMAKE_CURRENT_BINARY_DIR}/tinyuf2/pico_flash_region.ld)
pico_add_linker_script_override_path(tinyuf2 ${CMAKE_CURRENT_BINARY_DIR}/tinyuf2 FILES pico_flash_region.ld)

# drop the SDK runtime parts the bootloader does not use (default alarm pool, printf, float/double, panic, ...)
pico_minimize_runtime(tinyuf2)

# see __wrap_irq_add_shared_handler() in boards.c
target_link_options(tinyuf2 PUBLIC "LINKER:--wrap=irq_add_shared_handler")

# the RP2040 and RP2350 share one TinyUSB device driver (OPT_MCU_RP2040)
family_configure_tinyuf2(tinyuf2 OPT_MCU_RP2040)
family_gen_uf2(tinyuf2 ${UF2_FAMILY_ID})

#------------------------------------
# Application
#------------------------------------
family_add_app(blinky ${TOP}/apps/blinky)
family_add_app(update-tinyuf2 ${TOP}/apps/self_update SELF_UPDATE)
88 changes: 88 additions & 0 deletions ports/rp2350/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# TinyUF2 for RP2350

Port for the RP2350 (Arm Cortex-M33, secure) built on [pico-sdk](https://github.com/raspberrypi/pico-sdk). The bootloader runs
behind the RP2350 bootrom, which finds its `IMAGE_DEF` in flash and starts it: the bootrom USB bootloader (BOOTSEL) stays
available as recovery whatever is in flash.

Boards:

| Board | Notes |
| --- | --- |
| `intechstudio_grid_rp2350` | 16 WS2812 on GPIO6 (PIO0), volume `GRIDRP2350` |
| `intechstudio_wrid_rp2350` | Same hardware and USB IDs as `intechstudio_grid_rp2350`, volume `WRIDRP2350`, to tell the two bootloaders apart |

## Build

pico-sdk is not a submodule: `tools/get_deps.py` fetches the pinned version into `lib/mcu/raspberrypi/pico-sdk`.

```
python3 tools/get_deps.py rp2350
cmake -B build -G Ninja -DBOARD=intechstudio_grid_rp2350 ports/rp2350
cmake --build build
```

Use another pico-sdk with `-DPICO_SDK_PATH=<path>` or the `PICO_SDK_PATH` environment variable. The build also needs a host
compiler, for the SDK's `pioasm`.

## Flash layout

| Address | Content |
| --- | --- |
| `0x10000000` | TinyUF2, 32 KB region (`LD_FLASH_BOOT_SIZE` in `board.cmake`) |
| `0x10008000` | Application |

The application must be linked at `0x10008000`, which `app.cmake` does for the applications built here. A UF2 file of an
application uses the family ID `0xe48bff59` (`RP2350_ARM_S`) and absolute flash addresses. To create one from a `.bin`:

```
uf2conv.py -c -b 0x10008000 -f 0xe48bff59 firmware.bin
```

The bootloader refuses UF2 blocks below the application start or past the end of the flash, and erases a 4 KB sector only
when a block to write does not fit in blank flash, writing back the blocks of that sector it knows to be good.

The bootloader image is about 17 KB, so the region could shrink: 16 KB was not reached with pico-sdk, TinyUSB's mass storage
class and the flash driver. What was tried: `pico_minimize_runtime()` and replacing `irq_add_shared_handler()` (both used,
about 3.4 KB saved) and LTO (does not link with the Arm GNU 13.3 and Debian 14.2 toolchains).

## Flashing the bootloader

Hold BOOTSEL while plugging in, then copy `_bin/<board>/tinyuf2.uf2` to the BOOTSEL drive. Updating a running bootloader is done with
the self-update below. SWD flash targets are not set up.

## Entering the bootloader

- Double tap reset: the LEDs show purple for 0.5 s after reset, a second reset within that time stays in the bootloader.
- The bootloader also stays when the application is missing or invalid: the stack pointer in the first vector must point into
SRAM and the reset vector must be a Thumb address inside the application area.
- From an application: `board_reboot_to_bootloader()`.

The double tap uses the `DOUBLE_TAP` flag of POWMAN, armed from the start of the bootloader until it starts the application or
enters DFU mode. A RUN pin reset powers down RAM and also resets the POWMAN scratch registers, which are only kept over watchdog
and software resets: the application request therefore uses `powman_hw->scratch[0]`, the reset button the flag.

## Self-update

`update-tinyuf2.uf2` is built next to the bootloader in `_bin/<board>/apps`. Copy it to the bootloader drive: it runs from the
application area, rewrites the bootloader and erases itself, then the new bootloader starts. The application area is
overwritten, so copy the application again afterwards.

The bootloader's first sector holds the `IMAGE_DEF` the bootrom looks for. The update erases it first and writes it last, so an
interrupted update leaves no bootable mix of old and new code: the bootrom then starts its USB bootloader, from where
`tinyuf2.uf2` can be copied again. An image is only written when its vector table is valid, it fits the boot area and holds an
`IMAGE_DEF` start marker in its first 4 KB.

## Not implemented

- Write protection: `board_flash_protect_bootloader()` does nothing. UF2 writes cannot reach the bootloader, but the flash
itself is not protected against an application. It needs the block protect bits of the flash chip, whose type is not known yet.
- `BOARD_FLASH_SIZE` in `board.cmake` is the assumed 2 MB, to be confirmed for the board.
- Applications for the RISC-V cores and the non-secure Arm family are not supported.

## Notes

- pico-sdk's own TinyUSB support is switched off in `family.cmake`: its glue defines functions with the names of this repo's
`family_support.cmake` and replaces them. TinyUF2 builds `lib/tinyusb`, and CMake prints a warning that SDK USB support is unavailable.
- `pico_override_flash_size()` must not be used for the boot region: it also defines `PICO_FLASH_SIZE_BYTES`, and the SDK's
`flash_range_erase()`/`flash_range_program()` assert on every offset beyond it.
- Flash writes keep interrupts off, as the flash leaves XIP mode meanwhile.
20 changes: 20 additions & 0 deletions ports/rp2350/app.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
include_guard(GLOBAL)

# applications are standalone pico-sdk projects: pico_sdk_init() must come after their project() and before their targets
enable_language(CXX)
if (NOT CMAKE_SIZE)
string(REGEX REPLACE "gcc$" "size" CMAKE_SIZE "${CMAKE_C_COMPILER}")
endif ()
pico_sdk_init()

function(family_configure_app TARGET)
family_configure_common(${TARGET})

# boards.c is compiled into the application too, it drives the WS2812
pico_generate_pio_header(${TARGET} ${FAMILY_PATH}/ws2812.pio)

# link behind the bootloader: the flash region starts at the application start
configure_file(${FAMILY_PATH}/linker/pico_flash_region_app.template.ld
${CMAKE_CURRENT_BINARY_DIR}/${TARGET}/pico_flash_region.ld @ONLY)
pico_add_linker_script_override_path(${TARGET} ${CMAKE_CURRENT_BINARY_DIR}/${TARGET} FILES pico_flash_region.ld)
endfunction()
Loading
Loading