Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b145d96
Add SAMD51 port with the Intech Studio Grid bootloader boards
SukuWc Oct 16, 2026
0b89790
samd51: add NEOPIXEL_POWER_STATE like the espressif boards
SukuWc Oct 19, 2026
cfc2459
samd51: follow the UF2 Board-ID format with a revision token
SukuWc Oct 19, 2026
ecb44d2
samd51: count NeoPixel timing in cycles
SukuWc Oct 20, 2026
ced86cf
samd51: derive the application flash size from the board
SukuWc Oct 20, 2026
2d7eb06
samd51: reject a bootloader image with an even reset vector
SukuWc Oct 20, 2026
5edf587
Merge pull request #10 from adafruit/master
SukuWc Oct 20, 2026
26786ba
Merge master into samd51-port
hathach Oct 26, 2026
dc9ff8f
samd51: address review findings on flash bounds, page writes and app …
hathach Oct 27, 2026
98083d4
samd51: share the vector table check and bound app entries by usable …
hathach Oct 27, 2026
094cf44
samd51: bound new bootloader reset entry by its image and fix Debug u…
hathach Oct 27, 2026
a892dd6
Merge remote-tracking branch 'origin/master' into samd51-port
hathach Oct 31, 2026
927409c
samd51: share one linker script body, define the flash size once, add…
hathach Oct 31, 2026
1e2480f
samd51: take the flash size from the device header, let the linker ch…
hathach Oct 31, 2026
0869137
samd51: name the boards Grid and Wrid in USB and the board list
hathach Oct 31, 2026
3c42aa1
samd51: add Metro M4 Express with double tap, J-Link target and HIL f…
hathach Nov 1, 2026
fe59557
supported_boards: add Metro M4 Express
hathach Nov 1, 2026
8f44299
samd51: re-enable DWT in the cycle counter waits
hathach Nov 1, 2026
a68bb97
hil-contract: find the Microchip DFP startup's vector table, exceptio…
hathach Nov 1, 2026
9b470ab
samd51: name the initialised data output section .data
hathach Nov 1, 2026
ea2f479
hil: keep the rig job running past files another user left in its wor…
hathach Nov 1, 2026
04befa8
hil: add the Metro M4 Express on ci.lan, pending re-entry commissioning
hathach Nov 1, 2026
41d6909
hil: commission the Metro M4 Express on ci.lan, retained-register re-…
hathach Nov 1, 2026
d94b2e6
hil: simplify the workspace clean and the pending-board check
hathach Nov 1, 2026
1cda275
hil-contract: date the Metro M4 Express's commissioning apart from th…
hathach Nov 2, 2026
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'],
'samd51': ['intech_grid'],
'stm32f3': ['stm32f303disco'],
'stm32f4': ['stm32f411ve_discovery', 'feather_stm32f405_express'],
'stm32h5': ['stm32h563_nucleo'],
Expand Down
3 changes: 2 additions & 1 deletion .claude/skills/hil-contract/scripts/board_facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
EM_ARM, EM_RISCV = 40, 243
ABS32 = {(EM_ARM, SHT_REL): 2, (EM_RISCV, SHT_RELA): 1} # R_ARM_ABS32, R_RISCV_32
STACK = 0x1000 # the fixture's initial SP is ram + STACK (hil_test.fixture)
VECTOR_NAMES = ('__isr_vector', '__Vectors', 'g_pfnVectors') # the CMSIS startups' vector table, aliases included
# the CMSIS startups' vector table, aliases included, and the Microchip DFP startups' exception_table
VECTOR_NAMES = ('__isr_vector', '__Vectors', 'g_pfnVectors', 'exception_table')
MALFORMED = (struct.error, IndexError, ValueError, UnicodeDecodeError, KeyError)
STRINGS = {'hil_board_id', 'hil_flash_size_expr'} # board_facts.c's char arrays, bytes whatever their size

Expand Down
5 changes: 4 additions & 1 deletion .claude/skills/hil-contract/scripts/hil_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,14 @@
# 0x1FF00000 stays outside, and its option bytes are registers only.
# MAX32665/6: 1 MiB internal flash at 0x10000000, two 512 KiB blocks (MAX32665/MAX32666 UG Rev 3, 3.3.1); the
# information block at 0x10800000 stays outside.
# ATSAMD51J19: 512 KiB internal flash at 0 (DS60001507N Table 1-2 p18, Figure 8-1 p48); the NVM user page at
# 0x00804000 stays outside.
FLASH_LIMITS = {'LPC55S69_M33_0': (0, 0x9D800), 'MIMXRT1062xxx6A': (0x60000000, 0x60800000),
'MIMXRT1011DAE5A': (0x60000000, 0x60800000), 'MIMXRT1064xxx6A': (0x70000000, 0x70400000),
'stm32f303vc': (0x08000000, 0x08040000), 'stm32f407vg': (0x08000000, 0x08100000),
'stm32f411ve': (0x08000000, 0x08080000), 'stm32h563zi': (0x08000000, 0x08200000),
'stm32h743zi': (0x08000000, 0x08200000), 'max32665': (0x10000000, 0x10100000)}
'stm32h743zi': (0x08000000, 0x08200000), 'max32665': (0x10000000, 0x10100000),
'ATSAMD51J19': (0, 0x80000)}
Comment thread
hathach marked this conversation as resolved.
Resolved
# by a board's kind, its firmware and battery: a J-Link, ST-LINK or openocd Cortex-M board, or an Espressif one whatever
# transport installs it (board['flasher'])
STEPS = {'jlink': ('install', 'dfu', 'copy', 'app-ran', 'reenter')}
Expand Down
4 changes: 4 additions & 0 deletions .claude/test/test_board_facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ def test_without_a_count_or_a_sized_table_the_vectors_refuse(self):
self.b.boot_syms[3] = ('__isr_vector', BOOT + 4, 0, '.isr_vector') # unsized, and no .isr_vector there
self.refused('no vector count in the device header and no sized vector table')

def test_the_microchip_dfp_vector_table_is_found(self):
self.b.boot_syms[3] = ('exception_table', BOOT, len(self.b.vectors), '.isr_vector')
self.assertEqual(self.read()['vectors'], 98)

def test_blinkys_vector_table_must_sit_at_the_app_start(self):
self.b.app_base = APP + 0x1000
self.refused('blinky.elf starts at 0x8011000')
Expand Down
3 changes: 2 additions & 1 deletion .claude/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +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', 'stm32f3', 'stm32f4', 'stm32h5', 'stm32h7'})
self.assertEqual(ports, {'espressif', 'lpc55', 'maxim', 'mimxrt10xx', '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 @@ -75,6 +75,7 @@ jobs:
- 'lpc55'
- 'maxim'
- 'mimxrt10xx'
- 'samd51'
- 'stm32f3'
- 'stm32f4'
- 'stm32h5'
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 | ✔ | ✔ | ✔ | | ✔ | |
| SAMD51 | ✔ | | ✔ | ✔ | ✔ | |
| STM32F3 | ✔ | ✔ | ✔ | ✔ | ✔ | |
| STM32F4 | ✔ | ✔ | ✔ | ✔ | ✔ | |
| STM32H5 | ✔ | ✔ | ✔ | ✔ | | |
Expand Down
29 changes: 29 additions & 0 deletions ports/samd51/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.20)

include(${CMAKE_CURRENT_LIST_DIR}/../family_support.cmake)

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

#------------------------------------
# TinyUF2
#------------------------------------
add_executable(tinyuf2
board_flash.c
boards.c
${TOP}/lib/tinyusb/src/portable/microchip/samd/dcd_samd.c
)
target_link_options(tinyuf2 PUBLIC
"LINKER:--script=${CMAKE_CURRENT_LIST_DIR}/linker/samd51_boot.ld"
"LINKER:--script=${CMAKE_CURRENT_LIST_DIR}/linker/samd51_common.ld"
)

family_configure_tinyuf2(tinyuf2 OPT_MCU_SAMD51)
family_flash_openocd(tinyuf2)
family_flash_jlink(tinyuf2 hex)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

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

Port for SAMD51. Boards:

| Board | Notes |
| --- | --- |
| `intech_grid` | Intech Studio Grid modules (SAMD51N20A), volume `GRID26` |
| `intech_wrid` | Same hardware and USB IDs as `intech_grid`, volume `WRID26`, to tell the two bootloaders apart |
| `metro_m4_express` | Adafruit Metro M4 Express (SAMD51J19A), volume `METROM4BOOT` |

## Flash layout

| Address | Content |
| --- | --- |
| `0x00000000` | TinyUF2, 16 KB, write protected with BOOTPROT |
| `0x00004000` | Application |

With the default 16 KB boot size the application must be linked at `0x4000`. To create a UF2 image from a `.bin`
file:

```
uf2conv.py -c -b 0x4000 -f 0x55114460 firmware.bin
```

UF2 blocks must carry 256 bytes at a 256-byte aligned address, as uf2conv.py writes them. Other blocks are skipped yet
counted as written (#629), so such a file "completes" with parts of the application missing.

## Entering the bootloader

- Grid and Wrid: hold the button (`PC11`, active low) while the board resets or is plugged in.
- Metro M4 Express: double tap reset. An application enters it by writing `0xf01669ef` to the last RAM word and
resetting, as with uf2-samdx1.
- The bootloader also stays when the application is missing or invalid.

## 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 invalidates itself, then the new bootloader starts. The
application area is overwritten, so copy the application again afterwards.

- The bootloader protects itself on every boot: if the BOOTPROT fuse in the user page does not match the 16 KB boot
size, it is rewritten and the board resets once. The self-update lifts the protection until the next reset
(`SBPDIS`) to write the new bootloader.
- Rewriting the BOOTPROT fuse erases the user page before it is programmed again. A power cut in between leaves the
user page blank, and the bootloader then leaves it alone: restore it with an SWD probe. The user page holds the
factory BOD12 calibration (bits 25:15), which exists nowhere else: read out and keep the user page of each board
before its first boot with this bootloader.
- Updating between `intech_grid` and `intech_wrid` works the same way, the drive label shows which one is installed.
- A power cut while the bootloader area is erased, or a write that still fails after three tries, leaves the board
without a bootloader: recover it with an SWD probe. To program the bootloader over SWD, first remove the protection
with `atsame5 bootloader 0` in OpenOCD and reset.

## Debugging

- `BOD33` is set to 2.7 V at start. The default level is too low and flash writes can store zeros.
- The application usually takes over the SWD pins. Hold the button while powering up so the bootloader runs and SWD
stays usable.
- The bootloader must fit in 16 KB. A Debug build or logging (`-DLOG=1 -DLOGGER=RTT`) does not: the Debug build type
increases the boot size to 64 KB, and the application then has to be linked at `0x10000` and converted with
`uf2conv.py -c -b 0x10000 -f 0x55114460 firmware.bin`. The port has no UART: `LOG` without a `LOGGER` selection
prints nothing.
- Flash with openocd and a CMSIS-DAP probe: `cmake --build build --target tinyuf2-openocd`. It lifts the bootloader
protection first, the bootloader protects itself again at boot.
- Erratum 2.6.10 (DS80000748, silicon revisions A, D and F): a power-up can corrupt the reset vector fetch. Its
workaround sets the user page fuses WDT Enable to 1 and WDT Period to 0; the bootloader does not set them. With them
set, the watchdog runs from reset until `board_init` disables it.
9 changes: 9 additions & 0 deletions ports/samd51/app.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
include_guard(GLOBAL)

function(family_configure_app TARGET)
family_configure_common(${TARGET})
target_link_options(${TARGET} PUBLIC
"LINKER:--script=${CMAKE_CURRENT_FUNCTION_LIST_DIR}/linker/samd51_app.ld"
"LINKER:--script=${CMAKE_CURRENT_FUNCTION_LIST_DIR}/linker/samd51_common.ld"
)
endfunction()
Loading
Loading