Dockerで組込みRust!M5Stack(M5Stamp S3)に書き込んでシリアルモニターに出力するところまで


はじめに

WindowsのDocker Desktopを使ってRust開発環境を構築し、M5Stack(ESP32)に書き込んでシリアルモニター出力するまでのメモです。
とりあえず動いた、程度の内容ですので、ご了承ください。
組込みRust環境をDockerで構築することが便利かというと(?)の部分もありますが、始めてしまったので最後までやってみました。

なお、Dockerfile作成をはじめ環境構築手順については生成AIをフル活用しています。

Docker環境構築準備

開発環境をDocker内に構築するため、まずはDocker DesktopをWindowsにインストールします。(Docker Desktopのインストールについては省略します。)
Dockerを使用する利点は、開発環境を容易に再現できることです。しかし、M5StackはUSBケーブルを介してWindowsに接続されるため、Docker内の開発環境と直接通信することはできません。この問題を解決するために、USB接続をWindowsからDocker Desktopが動作するWSL2に中継する設定が必要になります。

USBIPD-WINインストール

下記サイトの情報を参考にUSBIPD-WINをWindowsにインストールします。

Windows パッケージ マネージャーを利用する方法も記載されていますが、私の環境ではうまくいきませんでした。
そのため、インストーラーをダウンロードしてインストールしました。

ひとまず、インストールまで行います。

M5Stamp S3の接続

PCとデバイスをUSBケーブルで接続します。
その際、今回はM5Stamp S3(正確にはM5Capsul)を利用したのですが、このデバイスの場合には書き込みモード(Boot Mode)にするために、表面の BTN0(BOOT_SET)と書かれた小さなボタンを押しながら、リセットボタンを押します。

USB接続をWSL に中継する

Power Shellを管理者モードで起動します。
(Power Shellのアイコン上で右クリックして、管理者モードをクリックします。)

その後、下記コマンドを入力します。

usbipd list

すると、PCに接続されている各種接続情報が表示されます。

PS C:\project> usbipd list
Connected:
BUSID  VID:PID    DEVICE                                                        STATE
2-1    303a:1001  USB シリアル デバイス (COM3), USB JTAG/serial debug unit      Not shared
2-3    04f3:0c55  ELAN WBF Fingerprint Sensor                                   Not shared
2-4    046d:c52b  Logitech USB Input Device, USB 入力デバイス                   Not shared
2-6    5986:211c  HD Webcam                                                     Not shared
2-8    046d:c545  LIGHTSPEED Receiver, USB 入力デバイス                         Not shared
2-10   8087:0026  インテル(R) ワイヤレス Bluetooth(R)                           Not shared

私の環境の場合、BUSID 2-1がデバイスに接続されたUSBシリアルですので、これをWSLに中継します。
そのために、次のコマンドを入力します。

usbipd bind --busid 2-1
usbipd attach --wsl --busid 2-1

この状態でlist表示すると、2-1がattachされています。

BUSID  VID:PID    DEVICE                                                        STATE
2-1    303a:1001  USB シリアル デバイス (COM3), USB JTAG/serial debug unit      Attached
2-3    04f3:0c55  ELAN WBF Fingerprint Sensor                                   Not shared
2-4    046d:c52b  Logitech USB Input Device, USB 入力デバイス                   Not shared
2-6    5986:211c  HD Webcam                                                     Not shared
2-8    046d:c545  LIGHTSPEED Receiver, USB 入力デバイス                         Not shared
2-10   8087:0026  インテル(R) ワイヤレス Bluetooth(R)                           Not shared

Dockerビルド

Dockerfileとdocker-compose.ymlを準備します。

Dockerfile

# Rustの公式イメージをベースとする
FROM rust:latest

# 必要なパッケージのインストール
RUN apt-get update && apt-get install -y \
    git \
    wget \
    curl \
    python3 \
    python3-pip \
    python3-venv \
    build-essential \
    cmake \
    ninja-build \
    libudev-dev \
    pkg-config \
    libssl-dev \
    usbutils \
    screen \
    nano \
    && rm -rf /var/lib/apt/lists/*

# sccacheのインストール
RUN cargo install sccache

# 環境変数の設定
ENV RUSTC_WRAPPER=/usr/local/cargo/bin/sccache
ENV SCCACHE_DIR=/root/.cache/sccache

# ビルド生成物をワークディレクトリから外す
ENV CARGO_BUILD_TARGET_DIR=/tmp/target

# cargo-generateのインストール
RUN cargo install cargo-generate

# Rustのリンカーのためのプロキシーツール
RUN cargo install ldproxy

# シリアルモニター
RUN cargo install espmonitor

# RUN cargo install cargo-espflash
RUN cargo install espflash

# espupのインストール
RUN cargo install espup
RUN espup install

# ワークディレクトリの設定
WORKDIR /workspace

docker-compose.ym

version: '3.8'

services:
  rust-esp32:
    build: .
    volumes:
      - .:/workspace
    environment:
      - USER=root
    devices:
      - /dev/ttyACM0:/dev/ttyACM0
    command: /bin/bash -c "source ~/export-esp.sh && tail -f /dev/null"
    stdin_open: true
    tty: true
    

devicesの/dev/ttyACM0を設定している2行は環境ごとに異なると可能性がありますので、最初はコメントアウトしておく必要があるかもしれません。

この2ファイルを準備したあと、下記コマンドでビルドしDockerを起動します。

docker-compose up -d
docker-compose exec rust-esp32 bash 

Dockerが起動したら、先ほどコメントアウトしたポート部分に該当するポートを下記コマンドで調べて、修正します。

dmesg | grep tty

修正したら、Dockerを再起動してください。

Rustサンプルプログラム

次のコマンドを入力しサンプルプログラムをビルドします。

Project Nameを尋ねられるため、適当に入力してください。
今回は「test」にしました。

cargo generate --vcs none --git https://github.com/esp-rs/esp-idf-template cargo

また、MCU選択画面が出ますので、接続したデバイスの該当するMUCを選択してください。

?  Which MCU to target? ›
  esp32
  esp32c2
  esp32c3
  esp32c6
  esp32h2
  esp32s2
❯ esp32s3

すると、入力したProject Nameのディレクトリが作成されます。

移動します。

cd test

ビルドします。

cargo build --release

releaseオプションを付けると早くビルドできるそうです。

デバイスへの書き込み

次のコマンドを入力します。

espflash flash /tmp/target/xtensa-esp32s3-espidf/release/test --list-all-ports

xtensa-esp32s3-espidf/release/testの部分は、プロジェクト名やMCUによって変わりますので、都度、変更してください。

targetディレクトリをtmpに移動したのは、ビルド高速化のためです。

Dockerfile内の環境変数で設定しています。

上記コマンドを実行すると、書き込むポートを指定する画面が表示されますので、書き込むポートで「y」を選択して書き込んでください。

root@037d5ee57cd7:/workspace/prog/test# espflash flash /tmp/target/xtensa-esp32s3-espidf/release/test --list-all-ports
✔ Use serial port '/dev/ttyACM0'? · yes
[2024-04-02T15:12:05Z INFO ] Serial port: '/dev/ttyACM0'
[2024-04-02T15:12:05Z INFO ] Connecting...
[2024-04-02T15:12:05Z INFO ] Using flash stub
Chip type:         esp32s3 (revision v0.2)
Crystal frequency: 40 MHz
Flash size:        8MB
Features:          WiFi, BLE
MAC address:       48:27:e2:e3:a8:b4
App/part. size:    395,520/1,048,576 bytes, 37.72%
[2024-04-02T15:12:06Z INFO ] Segment at address '0x0' has not changed, skipping write
[2024-04-02T15:12:06Z INFO ] Segment at address '0x8000' has not changed, skipping write
[2024-04-02T15:12:06Z INFO ] Segment at address '0x10000' has not changed, skipping write
[2024-04-02T15:12:06Z INFO ] Flashing has completed!

これで書き込みが完了しました。

このあと、デバイスをリセットします。すると、USB接続が切れてしまいますので、再度、USB接続をアタッチします。

usbipd attach --wsl --busid 2-1

シリアルモニタ(espmonitor)で受信

シリアルモニタで受信します。

espmonitor /dev/ttyACM0

受信したデータは次の通り。最後から2行目に「Hello, world!」が表示されています。

root@037d5ee57cd7:/workspace/prog/test# espmonitor /dev/ttyACM0
ESPMonitor 0.10.0

Commands:
    CTRL+R    Reset chip
    CTRL+C    Exit

Opening /dev/ttyACM0 with speed 115200
Resetting device... done
ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0x28 (SPI_FAST_FLASH_BOOT)
Saved PC:0x4037815a
SPIWP:0xee
mode:DIO, clock div:2
load:0x3fce3818,len:0x16f8
load:0x403c9700,len:0x4
load:0x403c9704,len:0xc00
load:0x403cc700,len:0x2eb0
entry 0x403c9908
I (27) boot: ESP-IDF v5.1-beta1-378-gea5e0ff298-dirt 2nd stage bootloader
I (28) boot: compile time Jun  7 2023 08:07:32
I (29) boot: Multicore bootloader
I (33) boot: chip revision: v0.2
I (36) boot.esp32s3: Boot SPI Speed : 40MHz
I (41) boot.esp32s3: SPI Mode       : DIO
I (46) boot.esp32s3: SPI Flash Size : 4MB
I (51) boot: Enabling RNG early entropy source...
I (56) boot: Partition Table:
I (60) boot: ## Label            Usage          Type ST Offset   Length
I (67) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (74) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (82) boot:  2 factory          factory app      00 00 00010000 00100000
I (89) boot: End of partition table
I (94) esp_image: segment 0: paddr=00010020 vaddr=3c040020 size=12544h ( 75076) map
I (121) esp_image: segment 1: paddr=0002256c vaddr=3fc90800 size=027f4h ( 10228) load
I (124) esp_image: segment 2: paddr=00024d68 vaddr=40374000 size=0b2b0h ( 45744) load
I (140) esp_image: segment 3: paddr=00030020 vaddr=42000020 size=3e304h (254724) map
I (204) esp_image: segment 4: paddr=0006e32c vaddr=4037f2b0 size=014d4h (  5332) load
I (211) boot: Loaded app from partition at offset 0x10000
I (211) boot: Disabling RNG early entropy source...
I (224) cpu_start: Multicore app
I (224) cpu_start: Pro cpu up.
I (224) cpu_start: Starting app cpu, entry point is 0x40375a58
I (0) cpu_start: App cpu up.
I (242) cpu_start: Pro cpu start user code
I (242) cpu_start: cpu freq: 160000000 Hz
I (243) cpu_start: Application information:
I (245) cpu_start: Project name:     libespidf
I (251) cpu_start: App version:      1
I (255) cpu_start: Compile time:     Apr  2 2024 14:09:01
I (261) cpu_start: ELF file SHA256:  0000000000000000...
I (267) cpu_start: ESP-IDF:          v5.1.3
I (272) cpu_start: Min chip rev:     v0.0
I (277) cpu_start: Max chip rev:     v0.99
I (281) cpu_start: Chip rev:         v0.2
I (286) heap_init: Initializing. RAM available for dynamic allocation:
I (294) heap_init: At 3FC93900 len 00055E10 (343 KiB): DRAM
I (300) heap_init: At 3FCE9710 len 00005724 (21 KiB): STACK/DRAM
I (306) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM
I (312) heap_init: At 600FE010 len 00001FD8 (7 KiB): RTCRAM
I (320) spi_flash: detected chip: gd
I (323) spi_flash: flash io: dio
W (327) spi_flash: Detected size(8192k) larger than the size in the binary image header(4096k). Using the size in the binary image header.
W (341) pcnt(legacy): legacy driver is deprecated, please migrate to `driver/pulse_cnt.h`
W (349) timer_group: legacy driver is deprecated, please migrate to `driver/gptimer.h`
I (358) sleep: Configure to isolate all GPIO pins in sleep state
I (364) sleep: Enable automatic switching of GPIO sleep configuration
I (372) app_start: Starting scheduler on CPU0
I (376) app_start: Starting scheduler on CPU1
I (376) main_task: Started on CPU0
I (386) main_task: Calling app_main()
I (386) test: Hello, world!
I (396) main_task: Returned from app_main()

USB接続解除

USB接続をWSLに接続したままだと、WindowsからUSBデバイスにアクセスできないため解除しておきます。

usbipd detach --busid 2-1
usbipd unbind -a