Skip to content

Commit 35a319d

Browse files
committed
tooling: Add make daplink-firmware and make daplink-deploy targets.
1 parent e563980 commit 35a319d

5 files changed

Lines changed: 158 additions & 49 deletions

File tree

.devcontainer/Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
FROM mcr.microsoft.com/devcontainers/python:3.10
22

3-
# System packages for MicroPython firmware build and board communication
3+
# System packages for MicroPython and DAPLink firmware builds, board communication
44
RUN apt-get update && apt-get install -y --no-install-recommends \
55
gcc-arm-none-eabi \
66
libnewlib-arm-none-eabi \
77
openocd \
88
udev \
9+
ccache \
10+
ninja-build \
911
&& rm -rf /var/lib/apt/lists/*
1012

1113
# udev rules for STeaMi board (DAPLink / STM32)

CONTRIBUTING.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ For local development (without dev container):
119119

120120
* Python 3.10+
121121
* Node.js 22+ (for husky, commitlint, lint-staged, semantic-release)
122-
* `arm-none-eabi-gcc` toolchain (for `make micropython-firmware`)
122+
* `arm-none-eabi-gcc` toolchain (for `make micropython-firmware` and `make daplink-firmware`)
123+
* `ccache` and `ninja-build` (for `make daplink-firmware`)
123124
* `pyocd` (for `make micropython-deploy`, installed via `pip install -e ".[flash]"`)
124125
* OpenOCD (optional, for `make micropython-deploy-openocd`)
125126
* `mpremote` (installed via `pip install -e ".[test]"`)
@@ -129,7 +130,7 @@ Then run `make setup` to install all dependencies and git hooks. This creates a
129130

130131
## Dev Container
131132

132-
A dev container is available for VS Code (local Docker only, not GitHub Codespaces). It includes all prerequisites out of the box: Python 3.10, Node.js 22, ruff, pytest, mpremote, pyOCD, arm-none-eabi-gcc, OpenOCD, and the GitHub CLI.
133+
A dev container is available for VS Code (local Docker only, not GitHub Codespaces). It includes all prerequisites out of the box: Python 3.10, Node.js 22, ruff, pytest, mpremote, pyOCD, arm-none-eabi-gcc, OpenOCD, ccache, ninja-build, and the GitHub CLI.
133134

134135
1. Open the repository in VS Code
135136
2. When prompted, click **Reopen in Container** (or use the command palette: *Dev Containers: Reopen in Container*)
@@ -191,9 +192,11 @@ make bump PART=major # major: v1.1.0 → v2.0.0
191192
The STeaMi board has two distinct firmwares:
192193

193194
- **MicroPython firmware** — runs on the STM32WB55 main MCU and exposes the drivers from this repository
194-
- **DAPLink firmware** — runs on the STM32F103 interface chip and provides the I2C bridge, mass-storage, and CMSIS-DAP debug interface (build targets planned in #377)
195+
- **DAPLink firmware** — runs on the STM32F103 interface chip and provides the I2C bridge, mass-storage, and CMSIS-DAP debug interface
195196

196-
This section covers the **MicroPython firmware** only. The drivers in this repository are "frozen" into it. The Makefile automates cloning, building, and flashing:
197+
The drivers in this repository are "frozen" into the **MicroPython firmware**. The Makefile automates cloning, building, and flashing both firmwares.
198+
199+
### MicroPython firmware
197200

198201
```bash
199202
make micropython-firmware # Clone micropython-steami (if needed), link local drivers, build
@@ -215,6 +218,27 @@ Use `make micropython-firmware` for normal rebuilds from the existing local clon
215218

216219
All these tools are included in the dev container. For local development, see the [Prerequisites](#prerequisites) section.
217220

221+
### DAPLink firmware
222+
223+
DAPLink is the firmware running on the STM32F103 interface chip. It provides the USB mass-storage, CMSIS-DAP debug interface, and the I2C bridge used by `daplink_bridge` / `daplink_flash` / `steami_config`.
224+
225+
DAPLink consists of **two parts**:
226+
227+
- **Bootloader** (first stage, flashed at `0x08000000`) — installed once at the factory, rarely updated. It provides the MAINTENANCE mode used to update the interface firmware. Updating the bootloader requires an external SWD probe and is not covered by these targets.
228+
- **Interface firmware** (second stage, flashed at `0x08002000`) — the part that contains the I2C bridge, mass-storage, debug interface, and is updated routinely. This is what the `daplink-*` Makefile targets manage.
229+
230+
```bash
231+
make daplink-firmware # Clone steamicc/DAPLink and build stm32f103xb_steami32_if
232+
make daplink-update # Refresh the DAPLink clone
233+
make daplink-deploy # Flash DAPLink interface firmware (default: usb mass-storage)
234+
make daplink-deploy-usb # Flash DAPLink interface firmware via MAINTENANCE volume
235+
make daplink-clean # Clean DAPLink build artifacts
236+
```
237+
238+
The DAPLink source is cloned from [steamicc/DAPLink](https://github.com/steamicc/DAPLink) into `.build/DAPLink/` (gitignored). A Python virtualenv is created automatically inside the clone for the progen build tool.
239+
240+
**Maintenance mode:** to flash the DAPLink interface firmware, the board must be in maintenance mode. Power on the board with the RESET button held until a `MAINTENANCE` USB volume appears (instead of the usual `STeaMi` volume). The `make daplink-deploy-usb` target then copies the firmware to that volume and the board reboots automatically with the new interface firmware.
241+
218242
## Notes
219243

220244
* Keep implementations simple and readable

Makefile

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,55 @@ micropython-clean: ## Clean MicroPython firmware build artifacts
191191
$(MAKE) -C $(STM32_DIR) BOARD=$(BOARD) clean; \
192192
fi
193193

194+
# --- DAPLink firmware ---
195+
# These targets manage the DAPLink **interface firmware** only (the second
196+
# stage of DAPLink, flashed at 0x08002000). The bootloader (first stage,
197+
# flashed at 0x08000000) is installed once at the factory and is not
198+
# managed here. A future `daplink-deploy-bootloader` target could be added
199+
# if needed, but it requires an external SWD probe and is rarely necessary.
200+
201+
$(DAPLINK_DIR):
202+
@echo "Cloning DAPLink into $(CURDIR)/$(DAPLINK_DIR)..."
203+
@mkdir -p $(dir $(CURDIR)/$(DAPLINK_DIR))
204+
git clone --branch $(DAPLINK_BRANCH) $(DAPLINK_REPO) $(CURDIR)/$(DAPLINK_DIR)
205+
206+
.PHONY: daplink-firmware
207+
daplink-firmware: $(DAPLINK_DIR) ## Build DAPLink interface firmware for the STeaMi STM32F103
208+
@set -e
209+
@if [ ! -d "$(DAPLINK_DIR)/venv" ]; then \
210+
echo "Setting up DAPLink Python virtualenv..."; \
211+
$(PYTHON) -m venv $(DAPLINK_DIR)/venv; \
212+
$(DAPLINK_DIR)/venv/bin/pip install -r $(DAPLINK_DIR)/requirements.txt; \
213+
fi
214+
@echo "Building DAPLink target $(DAPLINK_TARGET)..."
215+
cd $(CURDIR)/$(DAPLINK_DIR) && \
216+
./venv/bin/python tools/progen_compile.py -t make_gcc_arm $(DAPLINK_TARGET)
217+
@echo "DAPLink firmware ready: $(DAPLINK_BUILD_DIR)/$(DAPLINK_TARGET)_crc.bin"
218+
219+
.PHONY: daplink-update
220+
daplink-update: $(DAPLINK_DIR) ## Update the DAPLink clone
221+
@set -e
222+
@echo "Updating DAPLink..."
223+
git -C $(CURDIR)/$(DAPLINK_DIR) fetch origin
224+
git -C $(CURDIR)/$(DAPLINK_DIR) checkout $(DAPLINK_BRANCH)
225+
git -C $(CURDIR)/$(DAPLINK_DIR) pull --ff-only
226+
227+
.PHONY: daplink-deploy
228+
daplink-deploy: daplink-deploy-usb ## Flash DAPLink interface firmware (default: usb mass-storage)
229+
230+
.PHONY: daplink-deploy-usb
231+
daplink-deploy-usb: ## Flash DAPLink interface firmware via MAINTENANCE USB mass-storage
232+
@echo "Note: the board must be in MAINTENANCE mode."
233+
@echo "Power on the board with the RESET button held until the MAINTENANCE volume appears."
234+
@echo ""
235+
@$(PYTHON) scripts/deploy_usb.py --label MAINTENANCE $(DAPLINK_BUILD_DIR)/$(DAPLINK_TARGET)_crc.bin
236+
237+
.PHONY: daplink-clean
238+
daplink-clean: ## Clean DAPLink firmware build artifacts
239+
@if [ -d "$(DAPLINK_DIR)" ]; then \
240+
rm -rf $(DAPLINK_DIR)/projectfiles; \
241+
fi
242+
194243
# --- Hardware ---
195244

196245
.PHONY: repl

env.mk

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
export PATH := $(CURDIR)/node_modules/.bin:$(PATH)
22
PORT ?= /dev/ttyACM0
33

4-
# Firmware build configuration
4+
BUILD_DIR ?= .build
5+
6+
# MicroPython firmware build configuration
57
MICROPYTHON_REPO ?= https://github.com/steamicc/micropython-steami.git
68
MICROPYTHON_BRANCH ?= stm32-steami-rev1d-final
79
BOARD ?= STEAM32_WB55RG
8-
BUILD_DIR ?= .build
910
MPY_DIR ?= $(BUILD_DIR)/micropython-steami
1011
STM32_DIR ?= $(MPY_DIR)/ports/stm32
12+
13+
# DAPLink firmware build configuration
14+
DAPLINK_REPO ?= https://github.com/steamicc/DAPLink.git
15+
DAPLINK_BRANCH ?= release_letssteam
16+
DAPLINK_DIR ?= $(BUILD_DIR)/DAPLink
17+
DAPLINK_TARGET ?= stm32f103xb_steami32_if
18+
DAPLINK_BUILD_DIR ?= $(DAPLINK_DIR)/projectfiles/make_gcc_arm/$(DAPLINK_TARGET)/build

scripts/deploy_usb.py

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
1-
"""Deploy MicroPython firmware to a STeaMi board via DAPLink USB mass-storage.
1+
"""Deploy a firmware binary to a STeaMi board via DAPLink USB mass-storage.
22
3-
Detects the STeaMi volume by its label across Linux, macOS, and Windows,
3+
Detects the target volume by its label across Linux, macOS, and Windows,
44
copies the firmware .bin to it, and lets DAPLink auto-reset the target.
55
6+
The default label is ``STeaMi`` (normal mode, used for MicroPython firmware).
7+
For DAPLink firmware updates, the board must be in maintenance mode (boot
8+
with the RESET button held) and the volume label is ``MAINTENANCE``.
9+
610
Usage:
711
python scripts/deploy_usb.py path/to/firmware.bin
12+
python scripts/deploy_usb.py --label MAINTENANCE path/to/daplink.bin
813
"""
914

15+
import argparse
1016
import os
1117
import platform
1218
import shutil
1319
import subprocess
1420
import sys
1521

16-
VOLUME_LABEL = "STeaMi"
17-
1822

19-
def find_steami_linux():
20-
"""Find STeaMi mount point on Linux via findmnt.
23+
def find_volume_linux(label):
24+
"""Find the mount point of a labelled volume on Linux via findmnt.
2125
22-
Returns the mount path, or ``None`` if the board is not mounted
26+
Returns the mount path, or ``None`` if the volume is not mounted
2327
or ``findmnt`` is not available.
2428
"""
2529
try:
2630
result = subprocess.run(
27-
["findmnt", "-n", "-o", "TARGET", "-S", "LABEL=" + VOLUME_LABEL],
31+
["findmnt", "-n", "-o", "TARGET", "-S", "LABEL=" + label],
2832
capture_output=True,
2933
text=True,
3034
check=False,
@@ -37,22 +41,22 @@ def find_steami_linux():
3741
return None
3842

3943

40-
def find_steami_macos():
41-
"""Find STeaMi mount point on macOS.
44+
def find_volume_macos(label):
45+
"""Find the mount point of a labelled volume on macOS.
4246
43-
Returns ``/Volumes/STeaMi`` if the board is mounted, or ``None``.
47+
Returns ``/Volumes/<label>`` if the volume is mounted, or ``None``.
4448
"""
45-
path = "/Volumes/" + VOLUME_LABEL
49+
path = "/Volumes/" + label
4650
if os.path.isdir(path):
4751
return path
4852
return None
4953

5054

51-
def _find_steami_windows_powershell():
52-
"""Find STeaMi drive letter via PowerShell Get-Volume (preferred)."""
55+
def _find_volume_windows_powershell(label):
56+
"""Find a labelled volume drive letter via PowerShell Get-Volume."""
5357
ps_cmd = (
5458
"Get-Volume | Where-Object FileSystemLabel -eq '"
55-
+ VOLUME_LABEL
59+
+ label
5660
+ "' | Select-Object -First 1 -ExpandProperty DriveLetter"
5761
)
5862
try:
@@ -71,15 +75,15 @@ def _find_steami_windows_powershell():
7175
return None
7276

7377

74-
def _find_steami_windows_wmic():
75-
"""Find STeaMi drive letter via legacy wmic (fallback for older Windows)."""
78+
def _find_volume_windows_wmic(label):
79+
"""Find a labelled volume drive letter via legacy wmic (fallback)."""
7680
try:
7781
result = subprocess.run(
7882
[
7983
"wmic",
8084
"logicaldisk",
8185
"where",
82-
"VolumeName='" + VOLUME_LABEL + "'",
86+
"VolumeName='" + label + "'",
8387
"get",
8488
"DeviceID",
8589
"/value",
@@ -99,58 +103,80 @@ def _find_steami_windows_wmic():
99103
return None
100104

101105

102-
def find_steami_windows():
103-
"""Find STeaMi drive letter on Windows.
106+
def find_volume_windows(label):
107+
"""Find a labelled volume drive letter on Windows.
104108
105109
Tries PowerShell Get-Volume first (works on all modern Windows),
106110
falls back to wmic for older systems where PowerShell is unavailable.
107-
Returns the drive path (e.g. ``E:\\``), or ``None`` if the board is
111+
Returns the drive path (e.g. ``E:\\``), or ``None`` if the volume is
108112
not mounted or neither tool is available.
109113
"""
110-
return _find_steami_windows_powershell() or _find_steami_windows_wmic()
114+
return _find_volume_windows_powershell(label) or _find_volume_windows_wmic(label)
111115

112116

113-
def find_steami():
114-
"""Detect the STeaMi USB volume across platforms.
117+
def find_volume(label):
118+
"""Detect a USB volume by its filesystem label across platforms.
115119
116120
Returns the mount path as a string (e.g. ``/media/user/STeaMi``,
117-
``/Volumes/STeaMi``, or ``E:\\``) when a volume with label ``STeaMi``
118-
is found, or ``None`` if the board is not mounted (or the detection
121+
``/Volumes/STeaMi``, or ``E:\\``) when a volume with the given label
122+
is found, or ``None`` if the volume is not mounted (or the detection
119123
tool — findmnt, PowerShell, wmic — is not available on the system).
120124
121125
Exits with an error on unsupported operating systems.
122126
"""
123127
system = platform.system()
124128
if system == "Linux":
125-
return find_steami_linux()
129+
return find_volume_linux(label)
126130
if system == "Darwin":
127-
return find_steami_macos()
131+
return find_volume_macos(label)
128132
if system == "Windows":
129-
return find_steami_windows()
133+
return find_volume_windows(label)
130134
print("Error: unsupported OS: " + system, file=sys.stderr)
131135
sys.exit(1)
132136

133137

134138
def main():
135-
if len(sys.argv) != 2:
136-
print("Usage: deploy_usb.py <firmware.bin>", file=sys.stderr)
137-
sys.exit(1)
139+
parser = argparse.ArgumentParser(
140+
description="Deploy a firmware binary to a STeaMi board via USB mass-storage.",
141+
)
142+
parser.add_argument(
143+
"firmware",
144+
help="Path to the firmware .bin file to deploy.",
145+
)
146+
parser.add_argument(
147+
"--label",
148+
default="STeaMi",
149+
help=(
150+
"Filesystem label of the target volume. "
151+
"Use 'STeaMi' (default) for MicroPython firmware updates "
152+
"or 'MAINTENANCE' for DAPLink firmware updates "
153+
"(the board must be powered on with RESET held to enter maintenance mode)."
154+
),
155+
)
156+
args = parser.parse_args()
138157

139-
firmware = sys.argv[1]
140-
if not os.path.isfile(firmware):
141-
print("Error: firmware binary not found: " + firmware, file=sys.stderr)
142-
print("Run 'make micropython-firmware' first.", file=sys.stderr)
158+
if not os.path.isfile(args.firmware):
159+
print("Error: firmware binary not found: " + args.firmware, file=sys.stderr)
160+
print("Build the firmware first.", file=sys.stderr)
143161
sys.exit(1)
144162

145-
mount = find_steami()
163+
mount = find_volume(args.label)
146164
if not mount or not os.path.isdir(mount):
147165
print(
148-
"Error: STeaMi board not found (no volume with label '"
149-
+ VOLUME_LABEL
150-
+ "').",
166+
"Error: no volume with label '" + args.label + "' found.",
151167
file=sys.stderr,
152168
)
153-
print("Check that the board is connected and mounted.", file=sys.stderr)
169+
if args.label == "MAINTENANCE":
170+
print(
171+
"To enter DAPLink maintenance mode, power on the board with the "
172+
"RESET button held until the MAINTENANCE volume appears.",
173+
file=sys.stderr,
174+
)
175+
else:
176+
print(
177+
"Check that the board is connected and mounted.",
178+
file=sys.stderr,
179+
)
154180
if platform.system() == "Windows":
155181
print(
156182
"On Windows, this requires PowerShell (Get-Volume) or wmic.",
@@ -159,7 +185,7 @@ def main():
159185
sys.exit(1)
160186

161187
print("Copying firmware to " + mount + "...")
162-
shutil.copy(firmware, mount)
188+
shutil.copy(args.firmware, mount)
163189

164190
# Best-effort flush on Unix (no-op on Windows)
165191
if hasattr(os, "sync"):

0 commit comments

Comments
 (0)