Compare commits
1 commit
master
...
fix/versap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0461f6565b |
17 changed files with 122 additions and 580 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,9 +1,6 @@
|
||||||
# PlatformIO
|
# PlatformIO
|
||||||
.pio/
|
.pio/
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
|
|
||||||
35
README.md
35
README.md
|
|
@ -15,7 +15,6 @@ ATSAMD21G17D mit PlatformIO und dem Arduino-SAMD-Framework.
|
||||||
| LEDs | 20 WS2812B mit Base-/Override-Farbe und 7 Animationsmodi |
|
| LEDs | 20 WS2812B mit Base-/Override-Farbe und 7 Animationsmodi |
|
||||||
| Persistenz | Config und Makros im internen Flash |
|
| Persistenz | Config und Makros im internen Flash |
|
||||||
| Recovery | Werksreset über zwei Tasten |
|
| Recovery | Werksreset über zwei Tasten |
|
||||||
| Firmware-Update | SWD (Atmel-ICE) oder USB (UF2-Bootloader, `bootloader/`) |
|
|
||||||
|
|
||||||
Die drei Fader-Pins sind im Board-Variant definiert, werden von der aktuellen
|
Die drei Fader-Pins sind im Board-Variant definiert, werden von der aktuellen
|
||||||
Firmware aber noch nicht eingelesen.
|
Firmware aber noch nicht eingelesen.
|
||||||
|
|
@ -30,7 +29,7 @@ Firmware aber noch nicht eingelesen.
|
||||||
| Encoder | 4× Quadratur über EIC-Interrupts |
|
| Encoder | 4× Quadratur über EIC-Interrupts |
|
||||||
| LEDs | 20× WS2812B an `PB22` |
|
| LEDs | 20× WS2812B an `PB22` |
|
||||||
| USB | Native USB als HID + CDC Composite Device |
|
| USB | Native USB als HID + CDC Composite Device |
|
||||||
| Programmer | Atmel-ICE/CMSIS-DAP über SWD, oder USB über den UF2-Bootloader |
|
| Programmer | Atmel-ICE/CMSIS-DAP über SWD, standardmäßig ohne Bootloader |
|
||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
|
|
@ -47,30 +46,9 @@ Das Standard-Environment `versapad` baut für `boards/versapad_nobl.json`. Der
|
||||||
Upload wird durch `upload_openocd.py` über das von PlatformIO installierte
|
Upload wird durch `upload_openocd.py` über das von PlatformIO installierte
|
||||||
OpenOCD ausgeführt.
|
OpenOCD ausgeführt.
|
||||||
|
|
||||||
Alternativ lässt sich die App-Firmware nach einem einmaligen
|
Das in `platformio.ini` auskommentierte Bootloader-Environment ist derzeit
|
||||||
SWD-Bootloader-Flash auch über USB aktualisieren, ganz ohne Atmel-ICE:
|
nicht als Produktionsziel unterstützt. Details stehen unter
|
||||||
|
[bekannte Einschränkungen](doc/09_known_limitations.md).
|
||||||
```bash
|
|
||||||
cd bootloader && pio run -e versapad_bootloader --target upload # einmalig
|
|
||||||
pio run -e versapad_usb --target upload # danach jedes App-Update
|
|
||||||
```
|
|
||||||
|
|
||||||
`bootloader/` ist ein eigenständiges PlatformIO-Projekt (eigene
|
|
||||||
`platformio.ini`, kein Arduino-Framework). Für die PlatformIO-IDE-Buttons in
|
|
||||||
VS Code müsste der Ordner separat als eigener Workspace geöffnet werden; über
|
|
||||||
die Kommandozeile reicht `cd bootloader && pio run ...` im selben Fenster.
|
|
||||||
|
|
||||||
Sobald der Bootloader installiert ist, **verweigert `versapad --target
|
|
||||||
upload` den normalen Upload** — dieser Weg schreibt ab `0x0000` und würde den
|
|
||||||
Bootloader sonst kommentarlos überschreiben (`upload_openocd.py` prüft das
|
|
||||||
vorher automatisch). Bauen und Debuggen über `versapad` bleiben uneingeschränkt
|
|
||||||
möglich, nur der Upload ist betroffen. Für App-Updates danach `versapad_usb`
|
|
||||||
verwenden; bewusst zurück zu reinem SWD-Betrieb geht über
|
|
||||||
`pio run -e versapad -t erase-bootloader-and-flash`.
|
|
||||||
|
|
||||||
Details, Speicherlayout und die Bootloader-Aktivierung (kein physischer
|
|
||||||
Reset-Taster auf dieser Platine) stehen in
|
|
||||||
[10_usb_bootloader.md](doc/10_usb_bootloader.md).
|
|
||||||
|
|
||||||
## Laufzeitmodell
|
## Laufzeitmodell
|
||||||
|
|
||||||
|
|
@ -141,13 +119,9 @@ VersaMCU/
|
||||||
|-- AGENTS.md # Kontext und Richtlinien für Coding-LLMs
|
|-- AGENTS.md # Kontext und Richtlinien für Coding-LLMs
|
||||||
|-- README.md
|
|-- README.md
|
||||||
|-- platformio.ini
|
|-- platformio.ini
|
||||||
|-- upload_openocd.py # SWD-Upload-Hook (env:versapad)
|
|
||||||
|-- uf2conv.py # .bin -> .uf2 Konverter (App-Firmware)
|
|
||||||
|-- upload_uf2.py # USB-Upload-Hook (env:versapad_usb)
|
|
||||||
|-- boards/ # PlatformIO-Boarddefinitionen
|
|-- boards/ # PlatformIO-Boarddefinitionen
|
||||||
|-- variants/versapad/ # Pinmapping und Linker-Skripte
|
|-- variants/versapad/ # Pinmapping und Linker-Skripte
|
||||||
|-- doc/ # Architektur- und Protokolldokumentation
|
|-- doc/ # Architektur- und Protokolldokumentation
|
||||||
|-- bootloader/ # UF2-Bootloader (eigenes PlatformIO-Projekt)
|
|
||||||
`-- src/
|
`-- src/
|
||||||
|-- main.cpp
|
|-- main.cpp
|
||||||
|-- CMainController.* # Orchestrierung
|
|-- CMainController.* # Orchestrierung
|
||||||
|
|
@ -183,4 +157,3 @@ Binärverträge, Änderungsregeln und die minimale Verifikation.
|
||||||
- [CDC-Protokoll](doc/07_serial_protocol.md)
|
- [CDC-Protokoll](doc/07_serial_protocol.md)
|
||||||
- [Entwicklung und Einstieg](doc/08_development.md)
|
- [Entwicklung und Einstieg](doc/08_development.md)
|
||||||
- [Bekannte Einschränkungen](doc/09_known_limitations.md)
|
- [Bekannte Einschränkungen](doc/09_known_limitations.md)
|
||||||
- [USB-Bootloader](doc/10_usb_bootloader.md)
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@
|
||||||
"core": "arduino",
|
"core": "arduino",
|
||||||
"variant": "versapad",
|
"variant": "versapad",
|
||||||
"cpu": "cortex-m0plus",
|
"cpu": "cortex-m0plus",
|
||||||
"extra_flags": "-DARDUINO_SAMD_ZERO -DARM_MATH_CM0PLUS -D__SAMD21G18A__",
|
"extra_flags": "-DARDUINO_SAMD_ZERO -DARM_MATH_CM0PLUS -D__SAMD21G17D__",
|
||||||
"f_cpu": "48000000L",
|
"f_cpu": "48000000L",
|
||||||
"hwids": [
|
"hwids": [
|
||||||
["0x239A", "0x0042"]
|
["0x239A", "0x0011"]
|
||||||
],
|
],
|
||||||
"mcu": "samd21g17d",
|
"mcu": "samd21g17d",
|
||||||
"usb_product": "VersaPad v2",
|
"usb_product": "VersaPad v2",
|
||||||
|
|
@ -21,9 +21,14 @@
|
||||||
"name": "VersaPad v2 (USB bootloader)",
|
"name": "VersaPad v2 (USB bootloader)",
|
||||||
"upload": {
|
"upload": {
|
||||||
"maximum_ram_size": 16384,
|
"maximum_ram_size": 16384,
|
||||||
"maximum_size": 121600,
|
"maximum_size": 122880,
|
||||||
|
"disable_flushing": true,
|
||||||
|
"native_usb": true,
|
||||||
"offset": "0x2000",
|
"offset": "0x2000",
|
||||||
"protocol": "custom"
|
"protocol": "sam-ba",
|
||||||
|
"require_upload_port": true,
|
||||||
|
"use_1200bps_touch": true,
|
||||||
|
"wait_for_upload_port": true
|
||||||
},
|
},
|
||||||
"url": "",
|
"url": "",
|
||||||
"vendor": "Custom"
|
"vendor": "Custom"
|
||||||
|
|
|
||||||
5
bootloader/.gitignore
vendored
5
bootloader/.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
.pio
|
|
||||||
.vscode/.browse.c_cpp.db*
|
|
||||||
.vscode/c_cpp_properties.json
|
|
||||||
.vscode/launch.json
|
|
||||||
.vscode/ipch
|
|
||||||
|
|
@ -2,11 +2,9 @@
|
||||||
|
|
||||||
USB-Bootloader für das VersaPad-v2-Makropad. Wird per Atmel-ICE/SWD einmalig
|
USB-Bootloader für das VersaPad-v2-Makropad. Wird per Atmel-ICE/SWD einmalig
|
||||||
auf den ATSAMD21G17D geflasht und belegt `0x0000..0x1FFF` (8 KiB). Danach
|
auf den ATSAMD21G17D geflasht und belegt `0x0000..0x1FFF` (8 KiB). Danach
|
||||||
lässt sich die App-Firmware ohne SWD über USB aktualisieren.
|
lässt sich die App-Firmware ohne SWD über USB aktualisieren: Bootloader-Modus
|
||||||
|
aktivieren (Doppel-Tap Reset), Board erscheint als USB-Laufwerk `VERSABOOT`,
|
||||||
Speicherlayout, Bootloader-Aktivierung, Bedienung und Hardwaretest-Ergebnisse
|
`.uf2`-Datei drauf kopieren.
|
||||||
stehen in [`../doc/10_usb_bootloader.md`](../doc/10_usb_bootloader.md). Diese
|
|
||||||
Datei beschreibt nur das Bootloader-Unterprojekt selbst.
|
|
||||||
|
|
||||||
## Herkunft
|
## Herkunft
|
||||||
|
|
||||||
|
|
@ -25,12 +23,6 @@ und das Node-/Makefile-basierte Build-System — stattdessen ein eigenständiges
|
||||||
PlatformIO-Environment, damit dasselbe Tooling wie für die App-Firmware
|
PlatformIO-Environment, damit dasselbe Tooling wie für die App-Firmware
|
||||||
ausreicht.
|
ausreicht.
|
||||||
|
|
||||||
Für die App-Seite (nicht diesen Bootloader-Build) gibt es unter
|
|
||||||
[`../uf2conv.py`](../uf2conv.py) eine eigene, minimale Python-3-Neuimplemen-
|
|
||||||
tierung des `.bin`→`.uf2`-Konverters (kein Upstream-Code, passendes
|
|
||||||
Blockformat zu `inc/uf2format.h`), eingebunden über
|
|
||||||
[`../upload_uf2.py`](../upload_uf2.py) als `env:versapad_usb`-Upload-Hook.
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -45,26 +37,15 @@ da nur `0x0000..0x1FFF` beschrieben wird)
|
||||||
pio run -e versapad_bootloader --target upload
|
pio run -e versapad_bootloader --target upload
|
||||||
```
|
```
|
||||||
|
|
||||||
## Achtung bei manuellem SWD-Flashen von Bootloader UND App
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
`upload_openocd.py` (App, `env:versapad`) und der Bootloader-Upload (oben)
|
- Flash-Auslastung ~89 % (7292 von 8192 Byte). Wenig Puffer für Änderungen.
|
||||||
sind sicher, weil sie jeweils nur ihren eigenen Bereich anfassen. **Beim
|
- Kein physischer Boot-Pin definiert, nur Doppel-Tap-Reset (RAM-Magic-Wert).
|
||||||
manuellen Debuggen über OpenOCD-Kommandozeile aber Vorsicht:**
|
Ein Hardware-Fallback-Pin wäre für die Wiederherstellung bei kaputter
|
||||||
|
App-Firmware sinnvoll, ist aber noch nicht eingerichtet.
|
||||||
`openocd -c "program datei.elf verify"` **ohne explizite Zieladresse** hat
|
- Kein Status-LED-Feedback im Bootloader-Modus, da die Platine nur eine
|
||||||
sich in dieser Kombination aus OpenOCD-Version/CMSIS-DAP-Adapter/Target-Skript
|
WS2812-Kette (kein einfaches GPIO-LED oder DotStar) hat und der
|
||||||
als unzuverlässig erwiesen — statt die im ELF hinterlegten Sektionsadressen
|
Original-Code dafür nicht ausgelegt ist.
|
||||||
(`0x2000` für die App) zu nutzen, landeten die rohen Datei-Bytes teils direkt
|
- USB_PID `0x0011` ist unverifiziert übernommen (siehe
|
||||||
ab Flash-Adresse `0x0000` und haben damit den frisch geschriebenen Bootloader
|
`../doc/09_known_limitations.md`), noch nicht auf echter Hardware getestet.
|
||||||
sofort wieder überschrieben (bestätigt am 2026-08-05: Byte 0 an Adresse
|
- Ungetestet auf echter Hardware, siehe Branch `feature/usb-bootloader`.
|
||||||
`0x0000` war `0x7f`, der Beginn der ELF-Magic `\x7fELF` — die rohe Datei, kein
|
|
||||||
Firmware-Code). Passierte zuverlässig, wenn Bootloader- und App-Flash im
|
|
||||||
selben OpenOCD-Aufruf kombiniert wurden.
|
|
||||||
|
|
||||||
**Regel für manuelles SWD-Flashen beider Bereiche:** immer die `.bin`-Datei
|
|
||||||
verwenden (nie `.elf`) und die Zieladresse **immer explizit angeben**
|
|
||||||
(`program firmware.bin 0x0 verify` für den Bootloader,
|
|
||||||
`program firmware.bin 0x2000 verify` für die App), und Bootloader- und
|
|
||||||
App-Schreibvorgang **in getrennten OpenOCD-Aufrufen**, nicht in einer
|
|
||||||
gemeinsamen `-c`-Kommandokette. Im Zweifel danach mit `dump_image` in einer
|
|
||||||
frischen, unabhängigen Sitzung verifizieren.
|
|
||||||
|
|
|
||||||
67
bootloader/TESTING.md
Normal file
67
bootloader/TESTING.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Bootloader-Testflash -- Anleitung für Hardware-Zugriff
|
||||||
|
|
||||||
|
Der UF2-Bootloader (siehe [README.md](README.md)) ist fertig gebaut, aber noch
|
||||||
|
nie auf echter Hardware gelaufen. Diese Anleitung ist für jemanden mit
|
||||||
|
Atmel-ICE-Zugriff aufs VersaPad-v2-Board, um das einmal zu testen.
|
||||||
|
|
||||||
|
**Wichtig zur Sicherheit:** Dieser Flash schreibt nur `0x0000..0x1FFF`
|
||||||
|
(8 KiB). Die App-Firmware liegt ab `0x2000` und bleibt unangetastet. Falls
|
||||||
|
etwas schiefgeht, ist das jederzeit per SWD neu beschreibbar, das Board kann
|
||||||
|
dabei nicht dauerhaft "gebrickt" werden, solange der Atmel-ICE-Zugriff
|
||||||
|
funktioniert.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Board per Atmel-ICE/SWD angeschlossen, genau wie beim normalen
|
||||||
|
App-Firmware-Flashen (`pio run -e versapad --target upload` in
|
||||||
|
`doc/08_development.md`)
|
||||||
|
- PlatformIO Core installiert. Falls nicht:
|
||||||
|
```bash
|
||||||
|
pip install -U platformio
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Repo auf diesem Branch auschecken
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.jappel.io/jappel/VersaMCU.git
|
||||||
|
cd VersaMCU
|
||||||
|
git checkout feature/usb-bootloader
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Bootloader bauen und flashen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd bootloader
|
||||||
|
pio run -e versapad_bootloader --target upload
|
||||||
|
```
|
||||||
|
|
||||||
|
Der erste Aufruf lädt automatisch den ARM-Toolchain und OpenOCD herunter
|
||||||
|
(einmalig, braucht Internet). Der Build sollte ohne Fehler durchlaufen
|
||||||
|
(erwartete Größe: ~7,3 von 8 KB Flash).
|
||||||
|
|
||||||
|
## 3. Prüfen, ob der Bootloader läuft
|
||||||
|
|
||||||
|
Nach dem Flash: Board kurz vom USB trennen und wieder anstecken, dann
|
||||||
|
**Reset-Taste zweimal kurz hintereinander drücken** (Doppel-Tap, wie bei
|
||||||
|
Arduino-Zero-artigen Boards üblich).
|
||||||
|
|
||||||
|
Erwartung: Das Board sollte sich als **USB-Massenspeicher-Laufwerk namens
|
||||||
|
`VERSABOOT`** melden (in Windows z. B. im Explorer als neues Laufwerk).
|
||||||
|
|
||||||
|
## 4. Ergebnis zurückmelden
|
||||||
|
|
||||||
|
Bitte notieren und zurückgeben:
|
||||||
|
|
||||||
|
- Ist der Flash-Befehl ohne Fehler durchgelaufen? (ggf. komplette
|
||||||
|
Fehlermeldung kopieren)
|
||||||
|
- Erscheint nach dem Doppel-Tap-Reset ein Laufwerk `VERSABOOT`?
|
||||||
|
- Falls ja: was steht in der Datei `INFO_UF2.TXT` auf dem Laufwerk?
|
||||||
|
- Falls nein: reagiert das Board überhaupt noch normal als App-Firmware
|
||||||
|
(Tasten/LEDs), wenn man es normal per USB ansteckt ohne Doppel-Tap?
|
||||||
|
- Alles, was ungewöhnlich aussieht (USB-Gerät mit falschem Namen, Windows
|
||||||
|
fragt nach Treiber, Laufwerk lässt sich nicht öffnen, etc.)
|
||||||
|
|
||||||
|
Noch NICHT testen: Firmware tatsächlich über das `VERSABOOT`-Laufwerk
|
||||||
|
flashen. Der Firmware-Build erzeugt noch keine `.uf2`-Datei (nur `.bin`
|
||||||
|
für den bisherigen SWD-Weg), das ist der nächste Schritt, nachdem der
|
||||||
|
Bootloader selbst bestätigt läuft.
|
||||||
|
|
@ -15,16 +15,9 @@
|
||||||
#define BOARD_ID "SAMD21G17D-VersaPad-v2"
|
#define BOARD_ID "SAMD21G17D-VersaPad-v2"
|
||||||
|
|
||||||
/* Same VID as the app firmware (platformio.ini), distinct PID so the
|
/* Same VID as the app firmware (platformio.ini), distinct PID so the
|
||||||
* bootloader enumerates as a different USB device than the app.
|
* bootloader enumerates as a different USB device than the app. */
|
||||||
*
|
|
||||||
* NOT 0x0011: that's Adafruit's own Gemma M0 bootloader PID (this file's
|
|
||||||
* upstream template), and Windows machines with an Adafruit driver already
|
|
||||||
* installed silently bind it as a "Adafruit Circuit Playground" COM port
|
|
||||||
* instead of exposing the VERSABOOT mass-storage volume. Confirmed on real
|
|
||||||
* hardware 2026-08-05. 0x0043 is app PID (0x0042) + 1, not a known
|
|
||||||
* third-party assignment. */
|
|
||||||
#define USB_VID 0x239A
|
#define USB_VID 0x239A
|
||||||
#define USB_PID 0x0043
|
#define USB_PID 0x0011
|
||||||
|
|
||||||
/* No plain GPIO status LED on this board, only a WS2812 chain on PB22.
|
/* No plain GPIO status LED on this board, only a WS2812 chain on PB22.
|
||||||
* The bootloader's LED_PIN code just toggles a digital output, that would
|
* The bootloader's LED_PIN code just toggles a digital output, that would
|
||||||
|
|
@ -36,21 +29,4 @@
|
||||||
//#define BOARD_RGBLED_CLOCK_PIN
|
//#define BOARD_RGBLED_CLOCK_PIN
|
||||||
//#define BOARD_RGBLED_DATA_PIN
|
//#define BOARD_RGBLED_DATA_PIN
|
||||||
|
|
||||||
/* Hardware bootloader entry, no dedicated reset/boot button on this PCB.
|
|
||||||
* Reuses the bottom-right Cherry MX key (key_id 24 in the app firmware,
|
|
||||||
* see src/config/pins.h) as a boot-hold key: held while the board resets
|
|
||||||
* or powers up -> stay in the bootloader. Checked in check_start_application()
|
|
||||||
* before the app is even validated, so it works regardless of app firmware
|
|
||||||
* state.
|
|
||||||
*
|
|
||||||
* Pin numbers are PORT-flat (group*32 + pin), matching PINOP()/PINCFG() in
|
|
||||||
* uf2.h: PA15 = 15, PA08 = 8.
|
|
||||||
*
|
|
||||||
* Matrix wiring (src/config/pins.h, src/hal/matrix.cpp): key_id 24 = COL4 x
|
|
||||||
* ROW4. COL4 has an external 10k pull-up to 3V3 (idle HIGH); ROW4 is driven
|
|
||||||
* LOW during the check, matching the app's own scan polarity.
|
|
||||||
*/
|
|
||||||
#define BOOT_KEY_ROW_PIN 15 // PA15 -- ROW4, driven LOW during the check
|
|
||||||
#define BOOT_KEY_COL_PIN 8 // PA08 -- COL4, read back; LOW = key pressed
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -87,38 +87,6 @@ extern int8_t led_tick_step;
|
||||||
#define RESET_CONTROLLER RSTC
|
#define RESET_CONTROLLER RSTC
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(BOOT_KEY_ROW_PIN) && defined(BOOT_KEY_COL_PIN)
|
|
||||||
/**
|
|
||||||
* \brief Check whether the boot-hold key (bottom-right Cherry MX button) is
|
|
||||||
* held down. Drives its matrix row low and reads its matrix column back,
|
|
||||||
* the same polarity the app firmware's own matrix scan uses.
|
|
||||||
*/
|
|
||||||
static bool boot_key_pressed(void) {
|
|
||||||
PORT_PINCFG_Type col_cfg = {0};
|
|
||||||
col_cfg.bit.PMUXEN = false;
|
|
||||||
col_cfg.bit.INEN = true; // external 10k pull-up already on the board
|
|
||||||
|
|
||||||
PORT_PINCFG_Type row_cfg = {0};
|
|
||||||
row_cfg.bit.PMUXEN = false;
|
|
||||||
row_cfg.bit.DRVSTR = true;
|
|
||||||
|
|
||||||
PINCFG(BOOT_KEY_COL_PIN) = col_cfg.reg;
|
|
||||||
PINOP(BOOT_KEY_COL_PIN, DIRCLR); // column stays an input
|
|
||||||
|
|
||||||
PINOP(BOOT_KEY_ROW_PIN, OUTCLR); // pre-set drive level before enabling output
|
|
||||||
PINCFG(BOOT_KEY_ROW_PIN) = row_cfg.reg;
|
|
||||||
PINOP(BOOT_KEY_ROW_PIN, DIRSET); // row -> output, driving low
|
|
||||||
|
|
||||||
for (volatile int i = 0; i < 200; i++) {
|
|
||||||
} // let the row settle through the diode/pull-up RC
|
|
||||||
|
|
||||||
bool pressed = (PINIP(BOOT_KEY_COL_PIN) == 0);
|
|
||||||
|
|
||||||
PINOP(BOOT_KEY_ROW_PIN, DIRCLR); // release row back to high-Z
|
|
||||||
return pressed;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Check the application startup condition
|
* \brief Check the application startup condition
|
||||||
*
|
*
|
||||||
|
|
@ -126,13 +94,6 @@ static bool boot_key_pressed(void) {
|
||||||
static void check_start_application(void) {
|
static void check_start_application(void) {
|
||||||
uint32_t app_start_address;
|
uint32_t app_start_address;
|
||||||
|
|
||||||
#if defined(BOOT_KEY_ROW_PIN) && defined(BOOT_KEY_COL_PIN)
|
|
||||||
if (boot_key_pressed()) {
|
|
||||||
/* Stay in bootloader */
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Check if there is an IO which will hold us inside the bootloader.
|
// Check if there is an IO which will hold us inside the bootloader.
|
||||||
#if defined(HOLD_PIN) && defined(HOLD_STATE)
|
#if defined(HOLD_PIN) && defined(HOLD_STATE)
|
||||||
PORT_PINCFG_Type pincfg = {0};
|
PORT_PINCFG_Type pincfg = {0};
|
||||||
|
|
@ -206,14 +167,6 @@ static void check_start_application(void) {
|
||||||
/* Rebase the vector table base address */
|
/* Rebase the vector table base address */
|
||||||
SCB->VTOR = ((uint32_t)APP_START_ADDRESS & SCB_VTOR_TBLOFF_Msk);
|
SCB->VTOR = ((uint32_t)APP_START_ADDRESS & SCB_VTOR_TBLOFF_Msk);
|
||||||
|
|
||||||
/* Ensure the MSP/VTOR writes are visible before jumping; without these
|
|
||||||
* barriers the app's first fetch can race the pipeline (observed on
|
|
||||||
* real hardware: identical MSP/VTOR/PC values injected by a halted
|
|
||||||
* debugger boot fine, but the bootloader's own running jump hard-faults
|
|
||||||
* every time). */
|
|
||||||
__DSB();
|
|
||||||
__ISB();
|
|
||||||
|
|
||||||
/* Jump to application Reset Handler in the application */
|
/* Jump to application Reset Handler in the application */
|
||||||
asm("bx %0" ::"r"(app_start_address));
|
asm("bx %0" ::"r"(app_start_address));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ pio run -e versapad --target upload
|
||||||
Der Upload nutzt `upload_openocd.py`, das das von PlatformIO installierte
|
Der Upload nutzt `upload_openocd.py`, das das von PlatformIO installierte
|
||||||
OpenOCD mit `interface/cmsis-dap.cfg` und `target/at91samdXX.cfg` startet.
|
OpenOCD mit `interface/cmsis-dap.cfg` und `target/at91samdXX.cfg` startet.
|
||||||
|
|
||||||
Für App-Updates über USB (nach einem einmaligen Bootloader-Flash) gibt es
|
Das in `platformio.ini` nur als Beispiel enthaltene Environment
|
||||||
zusätzlich `env:versapad_usb`, siehe
|
`versapad_usb` ist auskommentiert und mit dem aktuellen NVM-/Linker-Layout
|
||||||
[10_usb_bootloader.md](10_usb_bootloader.md).
|
nicht als unterstützt anzusehen.
|
||||||
|
|
||||||
## Was beim Start passiert
|
## Was beim Start passiert
|
||||||
|
|
||||||
|
|
@ -63,7 +63,6 @@ und NVM-Schreibvorgängen. Es gibt keinen Scheduler und keine Threads.
|
||||||
| LEDs | `05_led_system.md` | `CButton.*`, `hal/ws2812.*` |
|
| LEDs | `05_led_system.md` | `CButton.*`, `hal/ws2812.*` |
|
||||||
| Persistente Config | `06_nvm_config.md` | `config/nvm_config.*`, Linker-Skripte |
|
| Persistente Config | `06_nvm_config.md` | `config/nvm_config.*`, Linker-Skripte |
|
||||||
| Host-Protokoll | `07_serial_protocol.md` | `hal/usb_serial.*`, Controller |
|
| Host-Protokoll | `07_serial_protocol.md` | `hal/usb_serial.*`, Controller |
|
||||||
| USB-Bootloader | `10_usb_bootloader.md` | `bootloader/`, `variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld`, `boards/versapad.json`, `uf2conv.py`, `upload_uf2.py` |
|
|
||||||
|
|
||||||
## Verifikation
|
## Verifikation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,33 +3,19 @@
|
||||||
Diese Liste beschreibt den aktuellen Implementierungsstand nach den
|
Diese Liste beschreibt den aktuellen Implementierungsstand nach den
|
||||||
Robustheitskorrekturen. Sie ist keine Liste bereits umgesetzter Features.
|
Robustheitskorrekturen. Sie ist keine Liste bereits umgesetzter Features.
|
||||||
|
|
||||||
## USB-Bootloader hat einen eingeschränkten Gültigkeitsbereich
|
## Bootloader-Ziel bleibt nicht unterstützt
|
||||||
|
|
||||||
Der UF2-Bootloader (`bootloader/`, App-Environment `env:versapad_usb`,
|
Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich
|
||||||
Details in [10_usb_bootloader.md](10_usb_bootloader.md)) ist auf echter
|
`0x1FB00..0x1FFFF` für Makros und Config. Das auskommentierte
|
||||||
Hardware verifiziert und ergänzt den SWD-Weg, ersetzt ihn aber nicht: der
|
USB-Bootloader-Environment verwendet dagegen weiterhin eine historische
|
||||||
Bootloader selbst muss weiterhin einmalig per Atmel-ICE/SWD geflasht werden.
|
Board-/Linker-Konfiguration und ist nicht als Produktionsziel verifiziert.
|
||||||
|
|
||||||
Verbleibende Einschränkungen:
|
Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den
|
||||||
|
|
||||||
- `uf2conv.py`/`upload_uf2.py` (Repo-Root) suchen das `VERSABOOT`-Laufwerk
|
|
||||||
nur über die Windows-API, keine macOS/Linux-Unterstützung.
|
|
||||||
- Die Bootloader-USB-PID (`0x0043`) ist kollisionsfrei verifiziert, aber kein
|
|
||||||
offiziell registrierter Wert unter Adafruits VID `0x239A`.
|
|
||||||
- Kein Software-Trigger, um aus der laufenden App heraus in den
|
|
||||||
Bootloader-Modus zu wechseln — nur der physische Weg (Kabel ziehen, Taste
|
|
||||||
halten, wieder einstecken).
|
|
||||||
- Bootloader-Flash-Auslastung ~90 % (7364 von 8192 Byte), wenig Puffer für
|
|
||||||
Änderungen am Bootloader selbst.
|
|
||||||
|
|
||||||
Sowohl `boards/versapad_nobl.json` (SWD-Ziel) als auch `boards/versapad.json`
|
|
||||||
(USB-Bootloader-Ziel) benennen die MCU als `samd21g17d`, setzen für den
|
|
||||||
Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der
|
Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der
|
||||||
PlatformIO-Build meldet korrekt 128 KiB physischen Flash, 16 KiB RAM und
|
PlatformIO-Build meldet korrekt 128 KiB physischen Flash, 16 KiB RAM und
|
||||||
129.792 Byte (`versapad_nobl`) beziehungsweise 121.600 Byte
|
129.792 Byte nutzbaren Firmwarebereich. Vor device-spezifischen
|
||||||
(`versapad`, abzüglich 8 KiB Bootloader) nutzbaren Firmwarebereich. Vor
|
Core-Änderungen sollte die historische Makro-Abweichung trotzdem geprüft
|
||||||
device-spezifischen Core-Änderungen sollte die historische Makro-Abweichung
|
werden.
|
||||||
trotzdem geprüft werden.
|
|
||||||
|
|
||||||
## Event-Queue hat eine feste Kapazität
|
## Event-Queue hat eine feste Kapazität
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
# USB-Bootloader (UF2)
|
|
||||||
|
|
||||||
Zweiter Firmware-Update-Weg neben SWD: ein UF2-Bootloader unter
|
|
||||||
`bootloader/` erlaubt, die App-Firmware über USB zu aktualisieren, ohne
|
|
||||||
Atmel-ICE. Der Bootloader selbst wird einmalig per SWD geflasht; danach
|
|
||||||
reicht für App-Updates ein PlatformIO-Befehl.
|
|
||||||
|
|
||||||
## Speicherlayout
|
|
||||||
|
|
||||||
| Bereich | Adresse | Größe | Environment |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Bootloader | `0x00000..0x01FFF` | 8 KiB | `versapad_bootloader` (in `bootloader/`) |
|
|
||||||
| App-Firmware | `0x02000..0x1FAFF` | 118,75 KiB | `versapad_usb` |
|
|
||||||
| NVM (Makros + Config) | `0x1FB00..0x1FFFF` | 1,25 KiB | — |
|
|
||||||
|
|
||||||
Gleiches NVM-Layout wie beim SWD-Ziel `versapad_nobl`
|
|
||||||
(siehe [06_nvm_config.md](06_nvm_config.md)); nur die Firmware-Obergrenze
|
|
||||||
sinkt um die 8 KiB Bootloader. Linkerskript:
|
|
||||||
`variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld`, Boarddatei:
|
|
||||||
`boards/versapad.json`.
|
|
||||||
|
|
||||||
## Bootloader-Einstieg ohne Reset-Taster
|
|
||||||
|
|
||||||
Die Platine hat keinen dedizierten Reset-/Boot-Taster. Statt eines
|
|
||||||
physischen Pins wird die unterste rechte Cherry-MX-Taste (`key_id 24`,
|
|
||||||
COL4 × ROW4, siehe [01_matrix.md](01_matrix.md)) beim Boot geprüft:
|
|
||||||
`check_start_application()` in `bootloader/src/main.c` treibt ROW4 kurz auf
|
|
||||||
LOW und liest COL4 zurück — noch vor der App-Adress-Validierung, funktioniert
|
|
||||||
also auch bei kaputter oder fehlender App-Firmware.
|
|
||||||
|
|
||||||
Bedienung: USB-Kabel ziehen, Taste gedrückt halten, wieder einstecken (Taste
|
|
||||||
weiter halten) → Board bootet in den Massenspeicher-Modus (`VERSABOOT`).
|
|
||||||
Ohne gehaltene Taste startet die App normal.
|
|
||||||
|
|
||||||
## Bauen und Flashen
|
|
||||||
|
|
||||||
Bootloader einmalig per SWD (überschreibt nur `0x0000..0x1FFF`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd bootloader
|
|
||||||
pio run -e versapad_bootloader --target upload
|
|
||||||
```
|
|
||||||
|
|
||||||
Danach App-Firmware per USB (Board vorher wie oben in den Bootloader-Modus
|
|
||||||
versetzen):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pio run -e versapad_usb --target upload
|
|
||||||
```
|
|
||||||
|
|
||||||
Baut die App, wandelt `firmware.bin` über `uf2conv.py` (Repo-Root, eigene
|
|
||||||
minimale Implementierung, kein Upstream-Code) in `firmware.uf2` und kopiert
|
|
||||||
es über `upload_uf2.py` aufs erkannte `VERSABOOT`-Laufwerk. Der Bootloader
|
|
||||||
erkennt den Schreibzugriff und springt selbständig in die neue App.
|
|
||||||
|
|
||||||
`upload_uf2.py` sucht das Laufwerk aktuell nur über die Windows-API
|
|
||||||
(`GetVolumeInformationW`) — keine macOS/Linux-Unterstützung.
|
|
||||||
|
|
||||||
## Schutz gegen versehentliches Überschreiben (env:versapad)
|
|
||||||
|
|
||||||
`env:versapad` (SWD, `boards/versapad_nobl.json`) schreibt die App-Firmware
|
|
||||||
ab `0x0000` und überschreibt damit einen installierten Bootloader
|
|
||||||
kommentarlos — genau das ist am 2026-08-05 während der Entwicklung passiert
|
|
||||||
(zweimal). `upload_openocd.py` prüft das seither vor jedem `versapad`-Upload:
|
|
||||||
|
|
||||||
- vergleicht per `verify_image` gegen die lokal gebaute
|
|
||||||
`bootloader/.pio/build/versapad_bootloader/firmware.bin`
|
|
||||||
- Bootloader erkannt → Upload wird verweigert, mit Hinweis auf
|
|
||||||
`versapad_usb` oder den expliziten Override
|
|
||||||
- lässt sich die Prüfung nicht eindeutig durchführen (z. B. `bootloader/`
|
|
||||||
noch nicht gebaut, oder die SWD-Verbindung antwortet nicht) → wird
|
|
||||||
sicherheitshalber ebenfalls verweigert, nicht durchgelassen
|
|
||||||
- gilt nur für `env:versapad` — `env:versapad_bootloader` nutzt dasselbe
|
|
||||||
Skript (`extra_scripts = ../upload_openocd.py`) und schreibt bewusst immer
|
|
||||||
auf `0x0000`, ungeprüft
|
|
||||||
|
|
||||||
Bewusstes Überschreiben (zurück zu reinem SWD-Betrieb ohne Bootloader):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pio run -e versapad -t erase-bootloader-and-flash
|
|
||||||
```
|
|
||||||
|
|
||||||
## Herkunft und Details
|
|
||||||
|
|
||||||
Der Bootloader ist abgeleitet und stark eingekürzt aus
|
|
||||||
[microsoft/uf2-samdx1](https://github.com/microsoft/uf2-samdx1) (MIT), auf
|
|
||||||
den `SAMD21G17D`-Chip dieses Boards zugeschnitten. Herkunft, Lizenz und
|
|
||||||
vollständige Implementierungsdetails stehen in `bootloader/README.md`.
|
|
||||||
|
|
||||||
## Hardwaretest (2026-08-05) und gefundene Bugs
|
|
||||||
|
|
||||||
Kompletter Weg auf echter Hardware verifiziert: Bootloader-Flash,
|
|
||||||
USB-Enumeration, Tastencheck-Einstieg, App-Firmware per `.uf2` schreiben,
|
|
||||||
automatischer Rücksprung in die neue App. Details, Fehlerbilder und Fixes
|
|
||||||
stehen in `bootloader/README.md` unter "Hardwaretest"; kurz zusammengefasst:
|
|
||||||
|
|
||||||
- **Fehlende Synchronisationsbarriere beim Sprung Bootloader→App.** Der
|
|
||||||
Sprung (`__set_MSP` → `SCB->VTOR` → `bx`) faultete bei jedem
|
|
||||||
eigenständigen Boot. Fix: `__DSB(); __ISB();` vor dem `bx` in
|
|
||||||
`check_start_application()` — von ARM für dieses Pattern vorgeschrieben,
|
|
||||||
fehlte im vendorten Code.
|
|
||||||
- **USB-PID-Kollision.** Die ursprünglich übernommene PID `0x0011` ist
|
|
||||||
Adafruits eigene Gemma-M0-Bootloader-PID; Windows-Rechner mit
|
|
||||||
installiertem Adafruit-Treiber banden das Board fälschlich als
|
|
||||||
`Adafruit Circuit Playground`-COM-Port statt als Massenspeicher. Fix:
|
|
||||||
eigene PID `0x0043`.
|
|
||||||
- **OpenOCD-Falle beim manuellen SWD-Debuggen:** `program datei.elf verify`
|
|
||||||
ohne explizite Zieladresse hat wiederholt den Bootloader mit rohen
|
|
||||||
ELF-Datei-Bytes überschrieben, sobald Bootloader- und App-Flash im selben
|
|
||||||
OpenOCD-Aufruf kombiniert wurden. Bei manuellem Flashen beider Bereiche
|
|
||||||
immer `.bin` mit expliziter Adresse und getrennte OpenOCD-Aufrufe
|
|
||||||
verwenden — Details in `bootloader/README.md`, "Achtung bei manuellem
|
|
||||||
SWD-Flashen".
|
|
||||||
- **`env:versapad` überschreibt den Bootloader kommentarlos.** Ein normaler
|
|
||||||
`pio run -e versapad --target upload` (der alte, gewohnte SWD-Weg für
|
|
||||||
App-Updates) schreibt ab `0x0000` und hat den Bootloader dabei zweimal
|
|
||||||
ohne jede Warnung zerstört. Fix: automatischer Presence-Check in
|
|
||||||
`upload_openocd.py`, siehe "Schutz gegen versehentliches Überschreiben"
|
|
||||||
oben.
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- Bootloader-Flash-Auslastung ~90 % (7364 von 8192 Byte) — wenig Puffer für
|
|
||||||
Änderungen am Bootloader selbst.
|
|
||||||
- Kein Status-LED-Feedback im Bootloader-Modus (nur WS2812-Kette, dafür ist
|
|
||||||
der Original-Code nicht ausgelegt).
|
|
||||||
- USB-PID `0x0043` ist kollisionsfrei verifiziert, aber kein offiziell
|
|
||||||
registrierter Wert unter Adafruits VID `0x239A`.
|
|
||||||
- `upload_uf2.py` ist Windows-only (siehe oben).
|
|
||||||
- Kein Software-Trigger, um aus der laufenden App heraus in den Bootloader
|
|
||||||
zurückzuspringen — nur der physische Kabel-raus/Taste-halten/Kabel-rein-Weg.
|
|
||||||
Wäre über einen neuen CDC-Befehl (siehe
|
|
||||||
[07_serial_protocol.md](07_serial_protocol.md)) nachrüstbar, der dieselbe
|
|
||||||
`DBL_TAP_MAGIC`-RAM-Adresse setzt und `NVIC_SystemReset()` aufruft.
|
|
||||||
|
|
@ -17,7 +17,6 @@ Bereiche stehen ausdrücklich in
|
||||||
| [07_serial_protocol.md](07_serial_protocol.md) | 8-Byte-Protokoll, Config-/Makro-Transfer, ACK/NACK |
|
| [07_serial_protocol.md](07_serial_protocol.md) | 8-Byte-Protokoll, Config-/Makro-Transfer, ACK/NACK |
|
||||||
| [08_development.md](08_development.md) | Setup, Build, Einstieg nach Änderungstyp, Verifikation |
|
| [08_development.md](08_development.md) | Setup, Build, Einstieg nach Änderungstyp, Verifikation |
|
||||||
| [09_known_limitations.md](09_known_limitations.md) | Aktuelle technische Einschränkungen und Risiken |
|
| [09_known_limitations.md](09_known_limitations.md) | Aktuelle technische Einschränkungen und Risiken |
|
||||||
| [10_usb_bootloader.md](10_usb_bootloader.md) | UF2-Bootloader, Speicherlayout, Tastencheck-Einstieg, USB-Flashweg |
|
|
||||||
|
|
||||||
Die Repository-weiten Richtlinien und der kompakte LLM-Kontext stehen in
|
Die Repository-weiten Richtlinien und der kompakte LLM-Kontext stehen in
|
||||||
[`../AGENTS.md`](../AGENTS.md).
|
[`../AGENTS.md`](../AGENTS.md).
|
||||||
|
|
@ -30,4 +29,3 @@ Die Repository-weiten Richtlinien und der kompakte LLM-Kontext stehen in
|
||||||
- Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md)
|
- Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md)
|
||||||
- CDC-Protokoll und Chunk-Zahlen: [07_serial_protocol.md](07_serial_protocol.md)
|
- CDC-Protokoll und Chunk-Zahlen: [07_serial_protocol.md](07_serial_protocol.md)
|
||||||
- bekannte Risiken vor strukturellen Änderungen: [09_known_limitations.md](09_known_limitations.md)
|
- bekannte Risiken vor strukturellen Änderungen: [09_known_limitations.md](09_known_limitations.md)
|
||||||
- Firmware per USB statt SWD flashen: [10_usb_bootloader.md](10_usb_bootloader.md)
|
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,8 @@ upload_protocol = custom
|
||||||
extra_scripts = upload_openocd.py
|
extra_scripts = upload_openocd.py
|
||||||
debug_tool = openocd
|
debug_tool = openocd
|
||||||
|
|
||||||
; ── USB UF2 (nur wenn bootloader/ geflasht ist) ────────────────────────────────
|
; ── USB SAM-BA (nur wenn Bootloader geflasht ist) ─────────────────────────────
|
||||||
; Der eigene Bootloader spricht UF2/Massenspeicher, kein SAM-BA. upload_uf2.py
|
; [env:versapad_usb]
|
||||||
; erzeugt aus firmware.bin ein .uf2 und kopiert es aufs VERSABOOT-Laufwerk.
|
; extends = common
|
||||||
[env:versapad_usb]
|
; board = versapad
|
||||||
extends = common
|
; upload_protocol = sam-ba
|
||||||
board = versapad
|
|
||||||
upload_protocol = custom
|
|
||||||
extra_scripts = upload_uf2.py
|
|
||||||
|
|
|
||||||
83
uf2conv.py
83
uf2conv.py
|
|
@ -1,83 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Minimal .bin -> .uf2 converter for the VersaMCU UF2 bootloader.
|
|
||||||
|
|
||||||
Standalone reimplementation of the block format microsoft/uf2-samdx1's
|
|
||||||
uf2conv.py produces (256-byte payload per 512-byte block); see
|
|
||||||
bootloader/inc/uf2format.h for the struct this has to match on the device
|
|
||||||
side. No external dependencies, Python 3 only.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python uf2conv.py <input.bin> <output.uf2> [--base 0x2000] [--family 0x68ed2b88]
|
|
||||||
"""
|
|
||||||
import argparse
|
|
||||||
import struct
|
|
||||||
|
|
||||||
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
|
|
||||||
UF2_MAGIC_START1 = 0x9E5D5157 # randomly selected, must match the device
|
|
||||||
UF2_MAGIC_END = 0x0AB16F30 # ditto
|
|
||||||
UF2_FLAG_FAMILYID_PRESENT = 0x00002000
|
|
||||||
|
|
||||||
SAMD21_FAMILY_ID = 0x68ED2B88 # bootloader/inc/uf2format.h, #ifdef SAMD21
|
|
||||||
PAYLOAD_SIZE = 256 # bytes per block; matches the upstream uf2conv.py convention
|
|
||||||
|
|
||||||
|
|
||||||
def convert(bin_path: str, uf2_path: str, base_addr: int, family_id: int) -> int:
|
|
||||||
with open(bin_path, "rb") as f:
|
|
||||||
data = f.read()
|
|
||||||
|
|
||||||
# Pad to a whole number of blocks; the bootloader writes payloadSize
|
|
||||||
# bytes per block regardless of how much of it is real firmware.
|
|
||||||
if len(data) % PAYLOAD_SIZE != 0:
|
|
||||||
data += b"\x00" * (PAYLOAD_SIZE - len(data) % PAYLOAD_SIZE)
|
|
||||||
num_blocks = len(data) // PAYLOAD_SIZE
|
|
||||||
|
|
||||||
blocks = []
|
|
||||||
for block_no in range(num_blocks):
|
|
||||||
offset = block_no * PAYLOAD_SIZE
|
|
||||||
chunk = data[offset : offset + PAYLOAD_SIZE]
|
|
||||||
header = struct.pack(
|
|
||||||
"<IIIIIIII",
|
|
||||||
UF2_MAGIC_START0,
|
|
||||||
UF2_MAGIC_START1,
|
|
||||||
UF2_FLAG_FAMILYID_PRESENT,
|
|
||||||
base_addr + offset,
|
|
||||||
PAYLOAD_SIZE,
|
|
||||||
block_no,
|
|
||||||
num_blocks,
|
|
||||||
family_id,
|
|
||||||
)
|
|
||||||
padding = b"\x00" * (476 - PAYLOAD_SIZE)
|
|
||||||
footer = struct.pack("<I", UF2_MAGIC_END)
|
|
||||||
blocks.append(header + chunk + padding + footer)
|
|
||||||
|
|
||||||
with open(uf2_path, "wb") as f:
|
|
||||||
f.write(b"".join(blocks))
|
|
||||||
|
|
||||||
return num_blocks
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument("input", help="Path to the raw firmware .bin")
|
|
||||||
parser.add_argument("output", help="Path to write the .uf2 to")
|
|
||||||
parser.add_argument(
|
|
||||||
"--base",
|
|
||||||
type=lambda s: int(s, 0),
|
|
||||||
default=0x2000,
|
|
||||||
help="Flash base address the .bin was linked for (default: 0x2000, matches "
|
|
||||||
"variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--family",
|
|
||||||
type=lambda s: int(s, 0),
|
|
||||||
default=SAMD21_FAMILY_ID,
|
|
||||||
help="UF2 family ID (default: SAMD21, 0x68ed2b88)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
num_blocks = convert(args.input, args.output, args.base, args.family)
|
|
||||||
print(f"Wrote {args.output}: {num_blocks} blocks, base 0x{args.base:08x}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -2,118 +2,23 @@ Import("env")
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
# Real, already-built bootloader image used as the reference for the presence
|
def upload_via_openocd(source, target, env):
|
||||||
# check below -- a raw synthetic probe blob turned out unreliable with
|
pkg_dir = env.PioPlatform().get_package_dir("tool-openocd")
|
||||||
# verify_image (silent no-op on tiny files), whereas verify_image against a
|
openocd = os.path.join(pkg_dir, "bin", "openocd.exe")
|
||||||
# real firmware .bin has been solid throughout this project's bring-up.
|
scripts = os.path.join(pkg_dir, "scripts")
|
||||||
BOOTLOADER_BIN = os.path.join(
|
firmware = str(source[0]) # .elf path
|
||||||
"bootloader", ".pio", "build", "versapad_bootloader", "firmware.bin"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _openocd_paths(env):
|
|
||||||
pkg_dir = env.PioPlatform().get_package_dir("tool-openocd")
|
|
||||||
return (
|
|
||||||
os.path.join(pkg_dir, "bin", "openocd.exe"),
|
|
||||||
os.path.join(pkg_dir, "scripts"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _run_openocd(env, extra_cmd, capture=False):
|
|
||||||
openocd, scripts = _openocd_paths(env)
|
|
||||||
cmd = [
|
cmd = [
|
||||||
openocd,
|
openocd,
|
||||||
"-s", scripts,
|
"-s", scripts,
|
||||||
"-f", "interface/cmsis-dap.cfg",
|
"-f", "interface/cmsis-dap.cfg",
|
||||||
"-f", "target/at91samdXX.cfg",
|
"-f", "target/at91samdXX.cfg",
|
||||||
"-c", extra_cmd,
|
"-c", 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/"))
|
||||||
]
|
]
|
||||||
|
|
||||||
print(" ".join(cmd))
|
print(" ".join(cmd))
|
||||||
if capture:
|
result = subprocess.run(cmd)
|
||||||
return subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
return subprocess.run(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
def _bootloader_present(env):
|
|
||||||
bootloader_bin = env.subst(
|
|
||||||
os.path.join("$PROJECT_DIR", BOOTLOADER_BIN)
|
|
||||||
)
|
|
||||||
if not os.path.isfile(bootloader_bin):
|
|
||||||
print("WARNING: {} not found (build it with".format(BOOTLOADER_BIN))
|
|
||||||
print("'cd bootloader && pio run -e versapad_bootloader') -- can't check")
|
|
||||||
print("for an installed bootloader, refusing to flash as a precaution.")
|
|
||||||
print("Use 'pio run -e versapad -t erase-bootloader-and-flash' to override.")
|
|
||||||
return True
|
|
||||||
|
|
||||||
result = _run_openocd(
|
|
||||||
env,
|
|
||||||
'init; reset halt; verify_image "{}" 0x0; shutdown'.format(
|
|
||||||
bootloader_bin.replace("\\", "/")
|
|
||||||
),
|
|
||||||
capture=True,
|
|
||||||
)
|
|
||||||
output = result.stdout + result.stderr
|
|
||||||
if "checksum mismatch" in output or "diff " in output:
|
|
||||||
return False # something else is at 0x0000 -- not this bootloader
|
|
||||||
if "halted due to debug-request" in output and "Error" not in output:
|
|
||||||
return True # verify_image is silent on a match; absence of a
|
|
||||||
# mismatch/error after a successful connect means it matched
|
|
||||||
print("WARNING: could not read flash to check for an installed bootloader")
|
|
||||||
print("(inconclusive) -- refusing to flash as a precaution.")
|
|
||||||
print("Use 'pio run -e versapad -t erase-bootloader-and-flash' to override.")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _flash(env, firmware):
|
|
||||||
result = _run_openocd(
|
|
||||||
env, 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/"))
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
env.Exit(1)
|
env.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
def upload_via_openocd(source, target, env):
|
|
||||||
firmware = str(source[0]) # .elf path
|
|
||||||
|
|
||||||
# This script is shared with bootloader/platformio.ini's own upload
|
|
||||||
# (env:versapad_bootloader), which legitimately writes 0x0000 every time
|
|
||||||
# -- the guard below only makes sense for the app-without-bootloader
|
|
||||||
# target (env:versapad).
|
|
||||||
if env["PIOENV"] == "versapad" and _bootloader_present(env):
|
|
||||||
print("=" * 78)
|
|
||||||
print("REFUSING TO FLASH: a UF2 bootloader looks like it's installed at 0x0000.")
|
|
||||||
print("")
|
|
||||||
print("This target (env:versapad) writes the app starting at 0x0000 and")
|
|
||||||
print("would silently overwrite it -- the board would lose its USB flashing")
|
|
||||||
print("path (see doc/10_usb_bootloader.md).")
|
|
||||||
print("")
|
|
||||||
print("Use instead:")
|
|
||||||
print(" pio run -e versapad_usb --target upload # flash over USB, keeps the bootloader")
|
|
||||||
print("or, if you deliberately want to erase the bootloader and go back to")
|
|
||||||
print("standalone SWD-only operation:")
|
|
||||||
print(" pio run -e versapad -t erase-bootloader-and-flash")
|
|
||||||
print("=" * 78)
|
|
||||||
env.Exit(1)
|
|
||||||
|
|
||||||
_flash(env, firmware)
|
|
||||||
|
|
||||||
|
|
||||||
env.Replace(UPLOADCMD=upload_via_openocd)
|
env.Replace(UPLOADCMD=upload_via_openocd)
|
||||||
|
|
||||||
|
|
||||||
def erase_bootloader_and_flash(*_args, **_kwargs):
|
|
||||||
firmware = env.subst(os.path.join("$BUILD_DIR", "${PROGNAME}.elf"))
|
|
||||||
print("Overwriting 0x0000..0x1FAFF -- any installed UF2 bootloader will be erased.")
|
|
||||||
_flash(env, firmware)
|
|
||||||
|
|
||||||
|
|
||||||
env.AddCustomTarget(
|
|
||||||
name="erase-bootloader-and-flash",
|
|
||||||
dependencies=["buildprog"],
|
|
||||||
actions=[erase_bootloader_and_flash],
|
|
||||||
title="Erase bootloader + flash (standalone SWD)",
|
|
||||||
description=(
|
|
||||||
"Unconditionally overwrites 0x0000..0x1FAFF, wiping any installed UF2 "
|
|
||||||
"bootloader. Use only to return to standalone SWD-only operation."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
"""PlatformIO upload hook for env:versapad_usb.
|
|
||||||
|
|
||||||
The board's actual bootloader (bootloader/, UF2/mass-storage) does not speak
|
|
||||||
SAM-BA, so upload_protocol=sam-ba (the board.json default, inherited from an
|
|
||||||
Arduino-Zero-style template) does not work here. This converts the built
|
|
||||||
.bin to .uf2 and copies it onto the VERSABOOT mass-storage volume instead --
|
|
||||||
Windows only for now, matching the rest of this project's dev environment.
|
|
||||||
"""
|
|
||||||
import ctypes
|
|
||||||
import os
|
|
||||||
import string
|
|
||||||
import sys
|
|
||||||
|
|
||||||
Import("env")
|
|
||||||
|
|
||||||
sys.path.insert(0, env.subst("$PROJECT_DIR"))
|
|
||||||
import uf2conv
|
|
||||||
|
|
||||||
VOLUME_LABEL = "VERSABOOT"
|
|
||||||
DRIVE_UNKNOWN = 0
|
|
||||||
DRIVE_NO_ROOT_DIR = 1
|
|
||||||
|
|
||||||
|
|
||||||
def find_versaboot_drive(label=VOLUME_LABEL):
|
|
||||||
for letter in string.ascii_uppercase:
|
|
||||||
root = f"{letter}:\\"
|
|
||||||
drive_type = ctypes.windll.kernel32.GetDriveTypeW(root)
|
|
||||||
if drive_type in (DRIVE_UNKNOWN, DRIVE_NO_ROOT_DIR):
|
|
||||||
continue
|
|
||||||
vol_name_buf = ctypes.create_unicode_buffer(261)
|
|
||||||
fs_name_buf = ctypes.create_unicode_buffer(261)
|
|
||||||
ok = ctypes.windll.kernel32.GetVolumeInformationW(
|
|
||||||
ctypes.c_wchar_p(root),
|
|
||||||
vol_name_buf,
|
|
||||||
ctypes.sizeof(vol_name_buf),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
fs_name_buf,
|
|
||||||
ctypes.sizeof(fs_name_buf),
|
|
||||||
)
|
|
||||||
if ok and vol_name_buf.value == label:
|
|
||||||
return root
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def upload_via_uf2(source, target, env):
|
|
||||||
build_dir = env.subst("$BUILD_DIR")
|
|
||||||
bin_path = os.path.join(build_dir, "firmware.bin")
|
|
||||||
uf2_path = os.path.join(build_dir, "firmware.uf2")
|
|
||||||
|
|
||||||
if not os.path.isfile(bin_path):
|
|
||||||
print(f"error: {bin_path} not found (expected as a normal build product)")
|
|
||||||
env.Exit(1)
|
|
||||||
|
|
||||||
num_blocks = uf2conv.convert(bin_path, uf2_path, base_addr=0x2000, family_id=uf2conv.SAMD21_FAMILY_ID)
|
|
||||||
print(f"Wrote {uf2_path}: {num_blocks} blocks")
|
|
||||||
|
|
||||||
drive = find_versaboot_drive()
|
|
||||||
if drive is None:
|
|
||||||
print(f"error: no drive labeled '{VOLUME_LABEL}' found.")
|
|
||||||
print("Hold the bottom-right Cherry MX key while plugging in USB to enter the bootloader.")
|
|
||||||
env.Exit(1)
|
|
||||||
|
|
||||||
dest = os.path.join(drive, "firmware.uf2")
|
|
||||||
print(f"Copying {uf2_path} -> {dest}")
|
|
||||||
with open(uf2_path, "rb") as src, open(dest, "wb") as dst:
|
|
||||||
dst.write(src.read())
|
|
||||||
print("Upload triggers a reset into the app on the device side; no further action needed.")
|
|
||||||
|
|
||||||
|
|
||||||
env.Replace(UPLOADCMD=upload_via_uf2)
|
|
||||||
|
|
@ -6,8 +6,7 @@ SEARCH_DIR(.)
|
||||||
|
|
||||||
MEMORY
|
MEMORY
|
||||||
{
|
{
|
||||||
rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001DB00 /* 118.75K – Firmware (128K - 8K bootloader - 1.25K NVM) */
|
rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001E000 /* 120K (128K - 8K bootloader) */
|
||||||
nvm (rx) : ORIGIN = 0x0001FB00, LENGTH = 0x00000500 /* 1.25K – Makros + Config, same layout as flash_without_bootloader.ld */
|
|
||||||
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */
|
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue