{
  "$type": "com.whtwnd.blog.entry",
  "blobs": [
    {
      "blobref": {
        "$type": "blob",
        "ref": {
          "$link": "bafkreigp7544sic47hb34c2o6i3t54kkgxu3uniafp6ariwvykf55gvbfm"
        },
        "mimeType": "image/jpeg",
        "size": 197689
      },
      "name": "blackpill-running.jpg"
    },
    {
      "blobref": {
        "$type": "blob",
        "ref": {
          "$link": "bafkreic4bwy2y7ypz2ehkibozpb5opoiqobroay5bi6aqru4kv2xp56frm"
        },
        "mimeType": "image/jpeg",
        "size": 210622
      },
      "name": "picoprobe-blackpill-wiring.jpg"
    }
  ],
  "content": "*[日本語](https://whtwnd.com/demiplus.bsky.social/3mnhdboppcu2x)*\n\n## Overview\n\nI set up an embedded development environment on Docker using Embassy (an async embedded Rust framework) for the STM32F4 microcontroller, and documented the steps from environment setup to LED blinking.\n\nTested on Ubuntu 24.04.\n\n![STM32F411 Black Pill running](https://bsky.social/xrpc/com.atproto.sync.getBlob?did=did:plc:4hlhginxpua4s2qb6dxfiaz4&cid=bafkreigp7544sic47hb34c2o6i3t54kkgxu3uniafp6ariwvykf55gvbfm)\n\n---\n\n## 1. About Embassy\n\nMy starting point was learning that you can do embedded programming in Rust and thinking it would be interesting to apply the philosophy of \"catch potential bugs at compile time and write safe code\" to systems that control hardware.\n\nFreeRTOS (C/C++) is a well-known OS for hardware control with MCUs. Embassy, however, does **not** use a preemptive scheduler like FreeRTOS — it uses **cooperative scheduling**, where tasks yield control at `.await` points. This means \"other tasks won't run unless a task voluntarily yields.\"\n\nFor example, in a mobile robot control loop, sensor reading, motor control, and command waiting would all be structured to periodically `.await` via `Timer::after_millis()` and similar. I had a preconception that non-preemptive tasks would be problematic for mobile robot control software, but I concluded that as long as each task is designed to yield appropriately, Embassy can handle control loops just fine.\n\n### Note\n\nEmbassy also supports **interrupt-based multi-priority executors**, which allow preemption at `.await` points between different priority levels. This article uses a single-priority configuration.\n\n---\n\n## 2. Environment Used\n\n### 2.1 Hardware\n\n- **Target board**: STM32F411CEU6 (Black Pill)\n  - Specs: 100MHz Cortex-M4F, 512KB Flash, 128KB RAM\n\n- **Debugger**: Raspberry Pi Pico (picoprobe firmware) — CMSIS-DAP compatible probe\n  - USB ID: `2e8a:000c`\n  - Also tested ST-Link V2 clone, but picoprobe is recommended due to issues described later\n\n### 2.2 Software Components\n\n| Component | Version | Purpose |\n|-----------|---------|---------|\n| Ubuntu | 24.04 | Host OS |\n| Rust | 1.92 | Programming language |\n| embassy-executor | 0.9 | Async task executor |\n| embassy-time | 0.5 | Time management |\n| embassy-stm32 | 0.5 | HAL for STM32 |\n| probe-rs-tools | 0.30 | Flash/debug tooling |\n| Docker | 24+ | Development container |\n| defmt | 1.0 | Embedded logging |\n\n---\n\n## 3. Preparing the Host Environment\n\n### 3.1 Host Environment\n\n- Docker / Docker Compose already installed\n- Ubuntu 24.04\n\n### 3.2 Setting Up udev Rules\n\nTo use the debugger without root privileges, configure udev rules. Register both picoprobe and ST-Link V2.\nReference: [Probe Setup | probe-rs](https://probe.rs/docs/getting-started/probe-setup/)\n\n[/etc/udev/rules.d/99-stlink.rules]\n\n```bash\n# ST-Link V2\nATTRS{idVendor}==\"0483\", ATTRS{idProduct}==\"3748\", MODE=\"0666\", GROUP=\"plugdev\"\n# ST-Link V2-1\nATTRS{idVendor}==\"0483\", ATTRS{idProduct}==\"374b\", MODE=\"0666\", GROUP=\"plugdev\"\n# ST-Link V3\nATTRS{idVendor}==\"0483\", ATTRS{idProduct}==\"374e\", MODE=\"0666\", GROUP=\"plugdev\"\nATTRS{idVendor}==\"0483\", ATTRS{idProduct}==\"374f\", MODE=\"0666\", GROUP=\"plugdev\"\n\n# Raspberry Pi Debugprobe / picoprobe (CMSIS-DAP)\nATTRS{idVendor}==\"2e8a\", ATTRS{idProduct}==\"000c\", MODE=\"0666\", GROUP=\"plugdev\"\n```\n\n```bash\n# Reload udev rules\nsudo udevadm control --reload-rules\nsudo udevadm trigger\n```\n\n### 3.3 Adding to the plugdev Group\n\n```bash\n# Check if current user belongs to plugdev\ngroups | grep plugdev\n\n# If not, add the user (requires logout/login)\nsudo usermod -aG plugdev $USER\n```\n\n---\n\n## 4. Building the Docker Development Environment\n\n### 4.1 Directory Structure\n\nThe project is organized as follows:\n\n```\nstm32-embassy/\n├── Dockerfile\n├── docker-compose.yml\n└── projects/\n    └── led-blink/\n        ├── Cargo.toml\n        ├── rust-toolchain.toml\n        ├── .cargo/\n        │   └── config.toml\n        └── src/\n            └── main.rs\n```\n\n### 4.2 Dockerfile Highlights\n\n- Base image: `rust:1.92-bookworm`\n- Install build dependencies (pkg-config, libusb, etc.)\n  - Reference: [Installation | probe-rs](https://probe.rs/docs/getting-started/installation/)\n- Add Rust target `thumbv7em-none-eabihf`\n  - Reference: [Hardware - The Embedded Rust Book](https://docs.rust-embedded.org/book/start/hardware.html)\n- Install `probe-rs-tools` and `flip-link`\n- Run as a non-root user\n\n```dockerfile\nFROM rust:1.92-bookworm\n\nENV DEBIAN_FRONTEND=noninteractive\n\n# Build tools and dependencies\nRUN apt-get update && apt-get install -y \\\n    pkg-config \\\n    libusb-1.0-0-dev \\\n    libudev-dev \\\n    gdb-multiarch \\\n    openocd \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Rust target for Cortex-M4F (STM32F4)\nRUN rustup target add thumbv7em-none-eabihf\nRUN rustup component add rust-src rust-analyzer\n\n# Embedded Rust tools\nRUN cargo install probe-rs-tools --locked\nRUN cargo install flip-link --locked\n\n# Create developer user\nARG UID=1000\nARG GID=1000\nRUN groupadd -g ${GID} developer && \\\n    useradd -m -u ${UID} -g ${GID} -G dialout,plugdev developer\n\n# Copy cargo and rustup to developer's home\nRUN cp -r /usr/local/cargo /home/developer/.cargo && \\\n    cp -r /usr/local/rustup /home/developer/.rustup && \\\n    chown -R developer:developer /home/developer/.cargo /home/developer/.rustup\n\nUSER developer\nENV CARGO_HOME=/home/developer/.cargo\nENV RUSTUP_HOME=/home/developer/.rustup\nENV PATH=\"${CARGO_HOME}/bin:${PATH}\"\n\nWORKDIR /projects\n\nCMD [\"/bin/bash\"]\n```\n\n### 4.3 docker-compose.yml Highlights\n\n- Volume mount design:\n  - Mount `./projects` into the container at `/projects`\n  - Persist Cargo cache in a named volume (without this, builds are slow every time)\n- USB device (debugger) passthrough:\n  - `/dev/bus/usb` in `volumes` (bind mount): required for libusb to traverse the device tree upward\n  - `/dev/bus/usb` in `devices`: declares the device node to Docker\n  - Both are needed. With only `devices`, probe-rs fails with \"Could not determine a suitable packet size\"\n- `privileged: true` is required\n\n```yaml\nservices:\n  embassy-dev:\n    build:\n      context: .\n      args:\n        UID: ${UID:-1000}\n        GID: ${GID:-1000}\n    image: stm32f4-embassy-dev\n    container_name: stm32-embassy-dev\n    volumes:\n      - ./projects:/projects\n      - cargo-cache:/home/developer/.cargo/registry\n      - cargo-git:/home/developer/.cargo/git\n      - rustup-toolchains:/home/developer/.rustup\n      - /dev/bus/usb:/dev/bus/usb   # required for libusb device tree traversal\n    devices:\n      - /dev/bus/usb:/dev/bus/usb\n    privileged: true\n    tty: true\n    stdin_open: true\n    working_dir: /projects\n    environment:\n      - TERM=xterm-256color\n\nvolumes:\n  cargo-cache:\n  cargo-git:\n  rustup-toolchains:\n```\n\n### 4.4 Building the Image\n\n```bash\n# Basic build command\ndocker compose build\n\n# Customize UID/GID if needed\nUID=$(id -u) GID=$(id -g) docker compose build\n```\n\nThe first build takes 5–10 minutes. Subsequent builds are faster due to caching.\n\n---\n\n## 5. Project Structure\n\n### 5.1 Cargo.toml\n\nRole of each dependency crate:\n- `embassy-executor`: Async task runner\n- `embassy-time`: Time management (`Timer::after_millis`, etc.)\n- `embassy-stm32`: HAL for STM32 (target chip specified via `features`)\n- `defmt` / `defmt-rtt`: Lightweight logging over RTT\n- `panic-probe`: defmt output on panic\n- `cortex-m` / `cortex-m-rt`: Cortex-M runtime\n\n```toml\n[package]\nname = \"led-blink\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nembassy-executor = { version = \"0.9\", features = [\"arch-cortex-m\", \"executor-thread\"] }\nembassy-time = { version = \"0.5\", features = [\"tick-hz-32_768\"] }\nembassy-stm32 = { version = \"0.5\", features = [\n    \"stm32f411ce\",\n    \"time-driver-any\",\n    \"memory-x\",\n]}\n\ndefmt = \"1.0\"\ndefmt-rtt = \"1.0\"\npanic-probe = { version = \"1.0\", features = [\"print-defmt\"] }\ncortex-m = { version = \"0.7\", features = [\"critical-section-single-core\"] }\ncortex-m-rt = \"0.7\"\n\n[profile.dev]\nopt-level = 1\n\n[profile.release]\ndebug = 2\nlto = true\nopt-level = \"s\"\ncodegen-units = 1   # single unit + LTO for maximum optimization\n```\n\n### 5.2 .cargo/config.toml\n\n- Specify the build target\n- Configure the probe-rs runner (**picoprobe preferred**)\n- Linker settings:\n  - `flip-link`: Stack overflow detection linker wrapper. Places static variables at the start of RAM and grows the stack upward, causing an immediate HardFault on overflow\n  - `link.x` / `defmt.x`: Linker scripts for cortex-m-rt / defmt\n- Set log level via the `DEFMT_LOG` environment variable\n\n```toml\n[target.thumbv7em-none-eabihf]\n# picoprobe (CMSIS-DAP) - no BOOT0 manipulation needed\nrunner = \"probe-rs run --chip STM32F411CEUx --probe 2e8a:000c\"\n# ST-Link V2 - requires BOOT0+RESET on every flash\n# runner = \"probe-rs run --chip STM32F411CEUx --connect-under-reset\"\n\n[target.'cfg(all(target_arch = \"arm\", target_os = \"none\"))']\nrustflags = [\n    \"-C\", \"linker=flip-link\",\n    \"-C\", \"link-arg=-Tlink.x\",\n    \"-C\", \"link-arg=-Tdefmt.x\",\n]\n\n[build]\ntarget = \"thumbv7em-none-eabihf\"\n\n[env]\nDEFMT_LOG = \"debug\"\n```\n\n### 5.3 rust-toolchain.toml\n\nPinning the toolchain keeps the environment consistent.\n\n```toml\n[toolchain]\nchannel = \"1.92\"\ncomponents = [\"rust-src\", \"rust-analyzer\"]\ntargets = [\"thumbv7em-none-eabihf\"]\n```\n\n---\n\n## 6. LED Blink Sample (led-blink)\n\n### 6.1 Full Code\n\n<!-- Reference: official sample https://github.com/embassy-rs/embassy/blob/main/examples/stm32f4/src/bin/blinky.rs -->\n\n```rust\n#![no_std]\n#![no_main]\n\nuse defmt::*;\nuse embassy_executor::Spawner;\nuse embassy_stm32::gpio::{Level, Output, Speed};\nuse embassy_time::Timer;\nuse {defmt_rtt as _, panic_probe as _};\n\n#[embassy_executor::main]\nasync fn main(_spawner: Spawner) {\n    let config = embassy_stm32::Config::default();\n    let p = embassy_stm32::init(config);\n    info!(\"Embassy STM32F4 LED Blink started!\");\n\n    // PC13: onboard LED (active low)\n    let mut led = Output::new(p.PC13, Level::High, Speed::Low);\n\n    loop {\n        led.toggle();\n        info!(\"LED toggled\");\n        Timer::after_millis(1000).await;\n    }\n}\n```\n\n### 6.2 Differences from the Official Sample\n\nBased on the official [blinky.rs (STM32F4)](https://github.com/embassy-rs/embassy/blob/main/examples/stm32f4/src/bin/blinky.rs), adapted for the Black Pill:\n\n| Item | Official blinky.rs | This main.rs | Reason |\n|------|-------------------|-------------|--------|\n| Initialization | `init(Default::default())` | Separate `Config::default()` variable passed to `init(config)` | Easier to add comments |\n| LED pin | `PB7` | `PC13` | Black Pill onboard LED is on PC13 |\n| Toggle method | Alternating `set_high()` / `set_low()` | `toggle()` | More concise |\n| Blink interval | 300ms × 2 (High/Low separately) | 1000ms | Easier to see at 1-second intervals |\n| Log | Separate `\"high\"` / `\"low\"` output | Single `\"LED toggled\"` | Simplified |\n\nThe structure (`#![no_std]`, `#![no_main]`, imports, macros) is identical to the official sample — only hardware-specific differences and minor readability tweaks.\n\n### 6.3 Code Highlights\n\n- `#![no_std]` / `#![no_main]`: Required for bare-metal (no OS) environments\n- `embassy_stm32::init(config)` initializes peripherals and manages pin ownership\n- `enable_debug_during_sleep` is **not needed** (no issue with picoprobe)\n  - Even with ST-Link V2 clone, this setting has no effect (see the Troubleshooting section)\n- `Output::new(p.PC13, Level::High, Speed::Low)`: Active Low, so initial state is High (LED off)\n- `loop { ... Timer::after_millis(1000).await; }`: Async sleep — CPU sleeps in WFE during this time\n- `info!(\"LED toggled\")`: defmt log output over RTT\n\n### 6.4 picoprobe Wiring\n\npicoprobe operates with just **3 SWD wires**. If the Black Pill is powered via USB, power lines are not needed.\n\n![picoprobe - Black Pill wiring diagram](https://bsky.social/xrpc/com.atproto.sync.getBlob?did=did:plc:4hlhginxpua4s2qb6dxfiaz4&cid=bafkreic4bwy2y7ypz2ehkibozpb5opoiqobroay5bi6aqru4kv2xp56frm)\n\n| Pico GPIO | Pico Pin | Black Pill | Description |\n|-----------|----------|------------|-------------|\n| GP2 | Pin 4 | PA14 (SWCLK) | SWD clock |\n| GP3 | Pin 5 | PA13 (SWDIO) | SWD data |\n| GND | Pin 38 etc. | GND | Ground |\n| 3V3 | Pin 36 | 3V3 | 3.3V power |\n\n### 6.5 Build and Run\n\n```bash\n# Start the container\ndocker compose run --rm embassy-dev\n\n# Navigate to the led-blink project directory and build\ncd led-blink\ncargo build --release\n\n# Flash & run (completes in one step if picoprobe is connected)\ncargo run --release\n```\n\nAfter running, the LED blinks at 1-second intervals and defmt logs appear in the terminal:\n\n```\nINFO  Embassy STM32F4 LED Blink started!\nINFO  LED toggled\nINFO  LED toggled\n...\n```\n\n---\n\n## 7. Debugging and Logging\n\n### 7.1 Log Output with defmt\n\ndefmt is a lightweight logging system that transfers logs to the host via RTT (Real-Time Transfer).\nWhile `cargo run` is executing, probe-rs automatically forwards RTT output to the terminal.\n\n```rust\nuse defmt::*;\n\ninfo!(\"Value: {}\", some_value);     // INFO level\nwarn!(\"Something happened\");        // WARN level\nerror!(\"Error: {}\", err_code);      // ERROR level\n```\n\nLog level is configured via `DEFMT_LOG` in `.cargo/config.toml`:\n\n```toml\n[env]\nDEFMT_LOG = \"debug\"   # trace / debug / info / warn / error\n```\n\n### 7.2 Basic probe-rs Operations\n\n```bash\n# List connected probes\nprobe-rs list\n\n# Check version (-v is verbose flag, use --version instead)\nprobe-rs --version\n\n# Verify connection to target\nprobe-rs info --chip STM32F411CEUx --probe 2e8a:000c\n```\n\n---\n\n## 8. Troubleshooting\n\n### ST-Link V2 Clone + embassy 0.9: JtagNoDeviceConnected\n\nI initially planned to use an ST-Link V2 clone as the programmer.\nHowever, flashing failed from the second attempt onward.\n\n**Symptom**:\n\n- First `cargo run --release` succeeds\n- Second and subsequent attempts (after entering WFE sleep) fail with `JtagNoDeviceConnected`\n\n**Root Cause**:\n\nIn embassy-executor 0.9, the idle implementation changed from WFI to **WFE**.\nOn the STM32F411, during WFE sleep, the **AHB bus matrix is disabled**, which drops the SWD connection.\n\nSetting `enable_debug_during_sleep = true` (`DBGMCU_CR = 0x7`) has no effect, and there appears to be no fundamental workaround for ST-Link V2 clones.\n\n**Solution (Recommended): Switch to picoprobe**\n\nUse a Raspberry Pi Pico flashed with the \"picoprobe\" firmware as the programmer.\n\nWith picoprobe (CMSIS-DAP), probe-rs directly controls SWD at the bit level and can re-initialize the DP via an SWD line reset sequence even during WFE sleep.\n\nSimply specify `--probe 2e8a:000c` in the `runner` in `.cargo/config.toml`.\n\n**Workaround: BOOT0 Button (when using ST-Link V2)**\n\nRequired before each flash:\n\n1. Hold the **BOOT0** button on the Black Pill\n2. Press and release the RESET button\n3. Release BOOT0\n4. Run `cargo run --release`\n\n### Recovery Mode\n\nIf the board becomes unresponsive with `JtagNoDeviceConnected` for any reason, use the same BOOT0 procedure above:\n\n- Set BOOT0 pin HIGH and reset → boots into system memory bootloader\n- The probe can then write to Flash in this state\n\n### Docker: \"Could not determine a suitable packet size\"\n\nThis occurs when `/dev/bus/usb:/dev/bus/usb` is removed from `volumes` in `docker-compose.yml`.\n\n**Symptom**:\n\n```\nError: Failed to open probe: Failed to open the debug probe.\nCaused by:\n    Could not determine a suitable packet size for this probe.\n```\n\n**Root Cause**:\n\nlibusb (used internally by probe-rs) traverses the USB device tree upward when opening a device file.\nThe `devices` entry only passes the device node itself — not the full tree — so libusb cannot access the device hierarchy.\n\n**Solution**:\n\nSpecify `/dev/bus/usb:/dev/bus/usb` in both `volumes` and `devices` (see the configuration in section 4.3).\nNote: `Couldn't get parent device` shown during `probe-rs list` or `probe-rs info` is a non-fatal libusb warning and does not affect connectivity.\n\n### Reconnecting picoprobe USB Fixes It\n\nIf `cargo run --release` suddenly starts failing, picoprobe may be in an unstable state.\nUnplugging and replugging the picoprobe USB cable on the host side usually resolves it.\n\n---\n\n## 9. Summary\n\n- Successfully built a reproducible embedded Rust development environment combining Embassy and Docker\n- Confirmed the basics of async programming (async/await) with an LED blink example\n- **Recommend picoprobe as the debugger**: ST-Link V2 clones are incompatible with the WFE sleep used by embassy 0.9 and later\n- defmt RTT logging is extremely convenient and works like `printf` debugging\n\n---\n\n## References\n\n- [Embassy Book](https://embassy.dev/book/)\n- [Embassy Official Documentation](https://embassy.dev/)\n- [embassy/examples/stm32f4/src/bin/blinky.rs](https://github.com/embassy-rs/embassy/blob/main/examples/stm32f4/src/bin/blinky.rs) (Official STM32F4 sample)\n- [Getting Started · embassy-rs/embassy Wiki](https://github.com/embassy-rs/embassy/wiki/Getting-Started)\n- [The Embedded Rust Book](https://docs.rust-embedded.org/book/)\n- [Hardware - The Embedded Rust Book](https://docs.rust-embedded.org/book/start/hardware.html) (target triple explanation)\n- [Installation | probe-rs](https://probe.rs/docs/getting-started/installation/) (pkg-config / libusb dependencies)\n- [Probe Setup | probe-rs](https://probe.rs/docs/getting-started/probe-setup/) (udev rule setup)\n- [probe-rs](https://probe.rs/)\n- [defmt](https://defmt.ferrous-systems.com/)\n- [STM32F411 Reference Manual](https://www.st.com/resource/en/reference_manual/rm0383-stm32f411xce-advanced-armbased-32bit-mcus-stmicroelectronics.pdf)\n- [Jeff McBride: Errors using RTT and WFI (2025)](https://jeffmcbride.net/blog/2025/05/22/rtt-errors-with-wfi/) — Explanation of the bus matrix disable mechanism",
  "createdAt": "2026-06-05T02:40:48.000Z",
  "theme": "github-light",
  "title": "STM32F4 + Embassy(Rust) + Docker: Building an Embedded Rust Dev Environment",
  "visibility": "public"
}