diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ca3a52e..d83205d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,11 +1,13 @@ FROM mcr.microsoft.com/devcontainers/python:3.10 -# System packages for MicroPython firmware build and board communication +# System packages for MicroPython and DAPLink firmware builds, board communication RUN apt-get update && apt-get install -y --no-install-recommends \ gcc-arm-none-eabi \ libnewlib-arm-none-eabi \ openocd \ udev \ + ccache \ + ninja-build \ && rm -rf /var/lib/apt/lists/* # udev rules for STeaMi board (DAPLink / STM32) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 28d22eb..ebcd65f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,3 +85,58 @@ jobs: "steami-micropython-firmware-${VERSION}.hex" \ "steami-micropython-firmware-${VERSION}.bin" \ --clobber + + daplink-firmware: + name: Build and attach DAPLink firmware + needs: release + if: needs.release.outputs.new-release-published == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.RELEASE_APP_ID }} + private_key: ${{ secrets.RELEASE_APP_SECRET }} + + - uses: actions/checkout@v4 + with: + ref: v${{ needs.release.outputs.new-release-version }} + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends ccache ninja-build python3-venv + + - name: Clone DAPLink (needed for venv cache key) + run: make .build/DAPLink + + - name: Cache gcc-arm-none-eabi 10.3-2021.10 + uses: actions/cache@v4 + with: + path: .build/gcc-arm-none-eabi-10.3-2021.10 + key: gcc-arm-none-eabi-10.3-2021.10-linux-x86_64 + + - name: Cache DAPLink Python venv + uses: actions/cache@v4 + with: + path: .build/DAPLink/venv + key: daplink-venv-${{ runner.os }}-${{ hashFiles('.build/DAPLink/requirements.txt') }} + + - name: Build DAPLink firmware + run: make daplink-firmware + + - name: Attach DAPLink firmware to release + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + VERSION="v${{ needs.release.outputs.new-release-version }}" + BUILD_DIR=".build/DAPLink/projectfiles/make_gcc_arm/stm32f103xb_steami32_if/build" + cp "${BUILD_DIR}/stm32f103xb_steami32_if_crc.bin" "steami-daplink-firmware-${VERSION}.bin" + cp "${BUILD_DIR}/stm32f103xb_steami32_if_crc.hex" "steami-daplink-firmware-${VERSION}.hex" + gh release upload "$VERSION" \ + "steami-daplink-firmware-${VERSION}.bin" \ + "steami-daplink-firmware-${VERSION}.hex" \ + --clobber diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b4593d..cb3f68c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,7 +119,9 @@ For local development (without dev container): * Python 3.10+ * Node.js 22+ (for husky, commitlint, lint-staged, semantic-release) -* `arm-none-eabi-gcc` toolchain (for `make micropython-firmware`) +* `arm-none-eabi-gcc` toolchain (for `make micropython-firmware`; `make daplink-firmware` ignores the system toolchain and downloads its own pinned 10.3-2021.10 build) +* `ccache` and `ninja-build` (for `make daplink-firmware`) +* For `make daplink-firmware`: Linux x86_64, Linux aarch64, or Intel macOS only (the pinned toolchain is not published for Apple Silicon or Windows — use the dev container on those platforms) * `pyocd` (for `make micropython-deploy`, installed via `pip install -e ".[flash]"`) * OpenOCD (optional, for `make micropython-deploy-openocd`) * `mpremote` (installed via `pip install -e ".[test]"`) @@ -129,7 +131,7 @@ Then run `make setup` to install all dependencies and git hooks. This creates a ## Dev Container -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. +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. 1. Open the repository in VS Code 2. When prompted, click **Reopen in Container** (or use the command palette: *Dev Containers: Reopen in Container*) @@ -191,9 +193,11 @@ make bump PART=major # major: v1.1.0 → v2.0.0 The STeaMi board has two distinct firmwares: - **MicroPython firmware** — runs on the STM32WB55 main MCU and exposes the drivers from this repository -- **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) +- **DAPLink firmware** — runs on the STM32F103 interface chip and provides the I2C bridge, mass-storage, and CMSIS-DAP debug interface -This section covers the **MicroPython firmware** only. The drivers in this repository are "frozen" into it. The Makefile automates cloning, building, and flashing: +The drivers in this repository are "frozen" into the **MicroPython firmware**. The Makefile automates cloning, building, and flashing both firmwares. + +### MicroPython firmware ```bash make micropython-firmware # Clone micropython-steami (if needed), link local drivers, build @@ -215,6 +219,27 @@ Use `make micropython-firmware` for normal rebuilds from the existing local clon All these tools are included in the dev container. For local development, see the [Prerequisites](#prerequisites) section. +### DAPLink firmware + +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`. + +DAPLink consists of **two parts**: + +- **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. +- **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. + +```bash +make daplink-firmware # Clone steamicc/DAPLink and build stm32f103xb_steami32_if +make daplink-update # Refresh the DAPLink clone +make daplink-deploy # Flash DAPLink interface firmware (default: usb mass-storage) +make daplink-deploy-usb # Flash DAPLink interface firmware via MAINTENANCE volume +make daplink-clean # Clean DAPLink build artifacts +``` + +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. + +**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. + ## Notes * Keep implementations simple and readable diff --git a/Makefile b/Makefile index f374819..deccc2f 100644 --- a/Makefile +++ b/Makefile @@ -136,7 +136,9 @@ micropython-deploy-openocd: $(MPY_DIR) ## Flash MicroPython firmware via OpenOCD .PHONY: micropython-deploy-usb micropython-deploy-usb: $(MPY_DIR) ## Flash MicroPython firmware via DAPLink USB mass-storage - @$(PYTHON) scripts/deploy_usb.py $(STM32_DIR)/build-$(BOARD)/firmware.bin + @$(PYTHON) scripts/deploy_usb.py \ + --build-target micropython-firmware \ + $(STM32_DIR)/build-$(BOARD)/firmware.bin # --- Deprecated targets (ambiguous since DAPLink build is also planned) --- # Replaced by explicit micropython-* / daplink-* targets to avoid confusion @@ -145,7 +147,15 @@ micropython-deploy-usb: $(MPY_DIR) ## Flash MicroPython firmware via DAPLink USB define DEPRECATED_FIRMWARE @echo "Error: 'make $(1)' is ambiguous. Use one of:"; \ echo " make micropython-$(2) (MicroPython firmware)"; \ -echo " make daplink-$(2) (DAPLink firmware, see #377)"; \ +echo " make daplink-$(2) (DAPLink firmware)"; \ +exit 1 +endef + +# Variant for short names whose DAPLink counterpart does not exist yet +# (daplink-deploy-pyocd / daplink-deploy-openocd are tracked in #388). +define DEPRECATED_MICROPYTHON_ONLY +@echo "Error: 'make $(1)' has been renamed. Use:"; \ +echo " make micropython-$(2) (MicroPython firmware)"; \ exit 1 endef @@ -159,9 +169,9 @@ firmware-clean: deploy: $(call DEPRECATED_FIRMWARE,deploy,deploy) deploy-pyocd: - $(call DEPRECATED_FIRMWARE,deploy-pyocd,deploy-pyocd) + $(call DEPRECATED_MICROPYTHON_ONLY,deploy-pyocd,deploy-pyocd) deploy-openocd: - $(call DEPRECATED_FIRMWARE,deploy-openocd,deploy-openocd) + $(call DEPRECATED_MICROPYTHON_ONLY,deploy-openocd,deploy-openocd) deploy-usb: $(call DEPRECATED_FIRMWARE,deploy-usb,deploy-usb) @@ -191,6 +201,79 @@ micropython-clean: ## Clean MicroPython firmware build artifacts $(MAKE) -C $(STM32_DIR) BOARD=$(BOARD) clean; \ fi +# --- DAPLink firmware --- +# These targets manage the DAPLink **interface firmware** only (the second +# stage of DAPLink, flashed at 0x08002000). The bootloader (first stage, +# flashed at 0x08000000) is installed once at the factory and is not +# managed here. A future `daplink-deploy-bootloader` target could be added +# if needed, but it requires an external SWD probe and is rarely necessary. + +$(DAPLINK_DIR): + @echo "Cloning DAPLink into $(CURDIR)/$(DAPLINK_DIR)..." + @mkdir -p $(dir $(CURDIR)/$(DAPLINK_DIR)) + git clone --branch $(DAPLINK_BRANCH) $(DAPLINK_REPO) $(CURDIR)/$(DAPLINK_DIR) + +$(DAPLINK_GCC_DIR)/bin/arm-none-eabi-gcc: + @set -e + @if [ -z "$(DAPLINK_GCC_ARCHIVE)" ]; then \ + echo "Error: no prebuilt gcc-arm-none-eabi $(DAPLINK_GCC_VERSION) for $(DAPLINK_GCC_HOST_OS)/$(DAPLINK_GCC_HOST_ARCH)."; \ + echo "Supported by this target: Linux x86_64, Linux aarch64, macOS Intel."; \ + echo "Other platforms: install the toolchain manually and override DAPLINK_GCC_DIR,"; \ + echo "or build inside the dev container."; \ + exit 1; \ + fi + @echo "Downloading gcc-arm-none-eabi $(DAPLINK_GCC_VERSION) for $(DAPLINK_GCC_HOST_OS)/$(DAPLINK_GCC_HOST_ARCH)..." + @mkdir -p $(BUILD_DIR) + curl -fL -o $(BUILD_DIR)/gcc-arm-none-eabi.tar.bz2 "$(DAPLINK_GCC_URL)" + tar -xjf $(BUILD_DIR)/gcc-arm-none-eabi.tar.bz2 -C $(BUILD_DIR) + rm -f $(BUILD_DIR)/gcc-arm-none-eabi.tar.bz2 + +# Sentinel: re-runs pip install whenever DAPLink's requirements.txt changes +# (e.g. after `make daplink-update`). The order-only prerequisite on +# $(DAPLINK_DIR) guarantees the clone happens first on a fresh checkout, so +# requirements.txt exists by the time make checks it. +$(DAPLINK_DIR)/venv/.installed: $(DAPLINK_DIR)/requirements.txt | $(DAPLINK_DIR) + @echo "Setting up DAPLink Python virtualenv..." + @if [ ! -x "$(DAPLINK_DIR)/venv/bin/python" ]; then \ + $(PYTHON) -m venv $(DAPLINK_DIR)/venv; \ + fi + $(DAPLINK_DIR)/venv/bin/pip install -r $(DAPLINK_DIR)/requirements.txt + @touch $@ + +.PHONY: daplink-firmware +daplink-firmware: $(DAPLINK_DIR) $(DAPLINK_GCC_DIR)/bin/arm-none-eabi-gcc $(DAPLINK_DIR)/venv/.installed ## Build DAPLink interface firmware for the STeaMi STM32F103 + @echo "Building DAPLink target $(DAPLINK_TARGET) with gcc-arm-none-eabi $(DAPLINK_GCC_VERSION)..." + cd $(CURDIR)/$(DAPLINK_DIR) && \ + PATH="$(CURDIR)/$(DAPLINK_GCC_DIR)/bin:$(CURDIR)/$(DAPLINK_DIR)/venv/bin:$$PATH" \ + ./venv/bin/python tools/progen_compile.py -t make_gcc_arm $(DAPLINK_TARGET) + @echo "DAPLink firmware ready: $(DAPLINK_BUILD_DIR)/$(DAPLINK_TARGET)_crc.bin" + +.PHONY: daplink-update +daplink-update: $(DAPLINK_DIR) ## Update the DAPLink clone + @set -e + @echo "Updating DAPLink..." + git -C $(CURDIR)/$(DAPLINK_DIR) fetch origin + git -C $(CURDIR)/$(DAPLINK_DIR) checkout $(DAPLINK_BRANCH) + git -C $(CURDIR)/$(DAPLINK_DIR) pull --ff-only + +.PHONY: daplink-deploy +daplink-deploy: daplink-deploy-usb ## Flash DAPLink interface firmware (default: usb mass-storage) + +.PHONY: daplink-deploy-usb +daplink-deploy-usb: $(DAPLINK_DIR) ## Flash DAPLink interface firmware via MAINTENANCE USB mass-storage + @echo "Note: the board must be in MAINTENANCE mode." + @echo "Power on the board with the RESET button held until the MAINTENANCE volume appears." + @echo "" + @$(PYTHON) scripts/deploy_usb.py --label MAINTENANCE \ + --build-target daplink-firmware \ + $(DAPLINK_BUILD_DIR)/$(DAPLINK_TARGET)_crc.bin + +.PHONY: daplink-clean +daplink-clean: ## Clean DAPLink firmware build artifacts + @if [ -d "$(DAPLINK_DIR)" ]; then \ + rm -rf $(DAPLINK_DIR)/projectfiles; \ + fi + # --- Hardware --- .PHONY: repl diff --git a/env.mk b/env.mk index 54f24d9..364dde8 100644 --- a/env.mk +++ b/env.mk @@ -1,10 +1,44 @@ export PATH := $(CURDIR)/node_modules/.bin:$(PATH) PORT ?= /dev/ttyACM0 -# Firmware build configuration +BUILD_DIR ?= .build + +# MicroPython firmware build configuration MICROPYTHON_REPO ?= https://github.com/steamicc/micropython-steami.git MICROPYTHON_BRANCH ?= stm32-steami-rev1d-final BOARD ?= STEAM32_WB55RG -BUILD_DIR ?= .build MPY_DIR ?= $(BUILD_DIR)/micropython-steami STM32_DIR ?= $(MPY_DIR)/ports/stm32 + +# DAPLink firmware build configuration +DAPLINK_REPO ?= https://github.com/steamicc/DAPLink.git +DAPLINK_BRANCH ?= release_letssteam +DAPLINK_DIR ?= $(BUILD_DIR)/DAPLink +DAPLINK_TARGET ?= stm32f103xb_steami32_if +DAPLINK_BUILD_DIR ?= $(DAPLINK_DIR)/projectfiles/make_gcc_arm/$(DAPLINK_TARGET)/build + +# DAPLink requires gcc-arm-none-eabi 10.3-2021.10. System toolchains >= 11.3 +# produce code that overflows m_text (see DAPLink docs/DEVELOPERS-GUIDE.md and +# ARMmbed/DAPLink#1043). The toolchain is downloaded once into BUILD_DIR. +# +# ARM publishes 10.3-2021.10 binaries for: x86_64 Linux, aarch64 Linux, and +# Intel macOS. Apple Silicon and Windows are NOT supported by this target — +# users on those platforms must install the toolchain manually and override +# DAPLINK_GCC_DIR / DAPLINK_GCC_URL, or build inside the dev container. +DAPLINK_GCC_VERSION ?= 10.3-2021.10 +DAPLINK_GCC_DIR ?= $(BUILD_DIR)/gcc-arm-none-eabi-$(DAPLINK_GCC_VERSION) + +DAPLINK_GCC_HOST_OS := $(shell uname -s) +DAPLINK_GCC_HOST_ARCH := $(shell uname -m) +ifeq ($(DAPLINK_GCC_HOST_OS),Linux) + ifeq ($(DAPLINK_GCC_HOST_ARCH),x86_64) + DAPLINK_GCC_ARCHIVE ?= gcc-arm-none-eabi-$(DAPLINK_GCC_VERSION)-x86_64-linux.tar.bz2 + else ifeq ($(DAPLINK_GCC_HOST_ARCH),aarch64) + DAPLINK_GCC_ARCHIVE ?= gcc-arm-none-eabi-$(DAPLINK_GCC_VERSION)-aarch64-linux.tar.bz2 + endif +else ifeq ($(DAPLINK_GCC_HOST_OS),Darwin) + ifeq ($(DAPLINK_GCC_HOST_ARCH),x86_64) + DAPLINK_GCC_ARCHIVE ?= gcc-arm-none-eabi-$(DAPLINK_GCC_VERSION)-mac.tar.bz2 + endif +endif +DAPLINK_GCC_URL ?= https://developer.arm.com/-/media/Files/downloads/gnu-rm/$(DAPLINK_GCC_VERSION)/$(DAPLINK_GCC_ARCHIVE) diff --git a/scripts/deploy_usb.py b/scripts/deploy_usb.py index 93a2afd..d6aafe0 100644 --- a/scripts/deploy_usb.py +++ b/scripts/deploy_usb.py @@ -1,30 +1,34 @@ -"""Deploy MicroPython firmware to a STeaMi board via DAPLink USB mass-storage. +"""Deploy a firmware binary to a STeaMi board via DAPLink USB mass-storage. -Detects the STeaMi volume by its label across Linux, macOS, and Windows, +Detects the target volume by its label across Linux, macOS, and Windows, copies the firmware .bin to it, and lets DAPLink auto-reset the target. +The default label is ``STeaMi`` (normal mode, used for MicroPython firmware). +For DAPLink firmware updates, the board must be in maintenance mode (boot +with the RESET button held) and the volume label is ``MAINTENANCE``. + Usage: python scripts/deploy_usb.py path/to/firmware.bin + python scripts/deploy_usb.py --label MAINTENANCE path/to/daplink.bin """ +import argparse import os import platform import shutil import subprocess import sys -VOLUME_LABEL = "STeaMi" - -def find_steami_linux(): - """Find STeaMi mount point on Linux via findmnt. +def find_volume_linux(label): + """Find the mount point of a labelled volume on Linux via findmnt. - Returns the mount path, or ``None`` if the board is not mounted + Returns the mount path, or ``None`` if the volume is not mounted or ``findmnt`` is not available. """ try: result = subprocess.run( - ["findmnt", "-n", "-o", "TARGET", "-S", "LABEL=" + VOLUME_LABEL], + ["findmnt", "-n", "-o", "TARGET", "-S", "LABEL=" + label], capture_output=True, text=True, check=False, @@ -37,22 +41,22 @@ def find_steami_linux(): return None -def find_steami_macos(): - """Find STeaMi mount point on macOS. +def find_volume_macos(label): + """Find the mount point of a labelled volume on macOS. - Returns ``/Volumes/STeaMi`` if the board is mounted, or ``None``. + Returns ``/Volumes/