ESP32 + RustでHello world

## 3行まとめ

* RustでもESP32のファームウェアを開発できる。
* LLVM、`clang`、`rustc`のコンパイルはなかなか大変。（時間的な意味で）
* 組み込みRustに未来を感じる。

## 1. 概要

　[技術書典7](https://techbookfest.org/event/tbf07)で頒布されていた『[M5Stackで始める組み込みRust](https://booth.pm/ja/items/1569213)』という技術同人誌を読みました。
以前よりマイコンを使った電子工作を行っており、組み込み領域でもRustを使ってみたいと思っていましたが、それなりに環境が整ったみたいなので試してみることにしました。

　結論から言えば、無事にRustからシリアルコンソールに「Hello world!」の文字を出力することができました。

## 2. 環境

　検証に用いた環境は以下の通りです。

* OS: macOS Mojave 10.14.6 (18G103)
* Docker CE: 19.03.2
* llvm-xtensa: `757e18f722dbdcd98b8479e25041b1eee1128ce9`
* clang-xtensa: `248d9ce8765248d953c3e5ef4022fb350bbe6c51`
* rust-xtensa: `b365cff41a60df8fd5f1237ef71897edad0375dd`
* ボード: 中国のサイトで購入したWROOM-32開発ボード

```text
mac$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.6
BuildVersion:	18G103

mac$ docker version
Client: Docker Engine - Community
 Version:           19.03.2
 API version:       1.40
 Go version:        go1.12.8
 Git commit:        6a30dfc
 Built:             Thu Aug 29 05:26:49 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.2
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.8
  Git commit:       6a30dfc
  Built:            Thu Aug 29 05:32:21 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.6
  GitCommit:        894b81a4b802e4eb2a91d1ce216b8817763c29fb
 runc:
  Version:          1.0.0-rc8
  GitCommit:        425e105d5a03fabd737a126ad93d62a9eeede87f
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
```

## 3. ESP32について

　ESP32は、個人の電子工作でも頻繁に用いられる安価かつ多機能なマイコン（マイクロコントローラ）です。
通常はESP32系チップが内蔵されたWROOM-32モジュールという形で使われることが多く、Wi-Fi、Bluetoothなどを内蔵しています。いわゆる「技適」の認定も行われているため、日本国内でも問題なく使用できます。

　ESP32のコア（CPU）は、Tensilica社のXtensa LX6であり、組み込み分野での採用が多いARM系ではありません。
ESP32の開発元である中国Espressif Systems社がRustのバックエンドであるLLVMのXtensaサポートを公開したことにより、ESP32上でRustを動かす道が開かれました。

## 4. LLVM、`clang`、`rustc`のビルド

　ESP32上では当然セルフビルドなどは行えないため、x86_64環境のホストでクロスビルドする必要があります。
そのために、Xtensa向けのLLVM、`clang`、`rustc`をビルドする必要があります。手順は前述の書籍に詳しいですが、少々煩雑なため`Dockerfile`を用意しました。参考にどうぞ。
マルチステージ、かつ並列ビルドを行いますので、それらに対応したバージョンのDockerが必要です。

```Dockerfile
#
# A stage for preparing to fetch data from Git and URL.
#
FROM ubuntu:18.04 AS ubuntu-for-fetch

RUN apt-get update \
  && DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \
    ca-certificates \
    curl \
    git-core \
  && rm --recursive --force /var/lib/apt/lists/*

#
# A stage fetching llvm-xtensa.
#
FROM ubuntu-for-fetch AS fetch-llvm-xtensa

RUN git clone https://github.com/espressif/llvm-xtensa.git /root/repo/llvm-xtensa \
  && cd /root/repo/llvm-xtensa/ \
  && git checkout 757e18f722dbdcd98b8479e25041b1eee1128ce9

#
# A stage fetching clang-xtensa.
#
FROM ubuntu-for-fetch AS fetch-clang-xtensa

RUN git clone https://github.com/espressif/clang-xtensa.git /root/repo/clang-xtensa \
  && cd /root/repo/clang-xtensa/ \
  && git checkout 248d9ce8765248d953c3e5ef4022fb350bbe6c51

#
# A stage fetching rust-xtensa.
#
FROM ubuntu-for-fetch AS fetch-rust-xtensa

RUN git clone https://github.com/MabezDev/rust-xtensa.git /root/repo/rust-xtensa \
  && cd /root/repo/rust-xtensa/ \
  && git checkout b365cff41a60df8fd5f1237ef71897edad0375dd

#
# A stage fetching esp-idf.
#
FROM ubuntu-for-fetch AS fetch-esp-idf

RUN ARCHIVE_NAME=xtensa-esp32-elf-linux64-1.22.0-80-g6c4433a-5.2.0.tar.gz \
  && curl -o /root/${ARCHIVE_NAME} https://dl.espressif.com/dl/${ARCHIVE_NAME} \
  && tar zxfv /root/${ARCHIVE_NAME} -C /opt \
  && rm /root/${ARCHIVE_NAME}
RUN git clone --branch v3.3 --depth 1 https://github.com/espressif/esp-idf.git /opt/esp-idf \
  && rm --recursive --force /opt/esp-idf/.git

#
# A stage building llvm-xtensa.
#
FROM ubuntu:18.04 AS build-llvm-xtensa

RUN apt-get update \
  && DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \
    build-essential \
    ca-certificates \
    cmake \
    curl \
    git-core \
    ninja-build \
    python \
  && rm --recursive --force /var/lib/apt/lists/*

COPY --from=fetch-llvm-xtensa /root/repo/llvm-xtensa /root/repo/llvm-xtensa
COPY --from=fetch-clang-xtensa /root/repo/clang-xtensa /root/repo/llvm-xtensa/tools/clang
RUN mkdir --parents /root/build/llvm-xtensa \
  && cd /root/build/llvm-xtensa/ \
  && cmake \
    -D CMAKE_INSTALL_PREFIX="/opt/llvm-xtensa" \
    -D CMAKE_BUILD_TYPE="Release" \
    -D LLVM_TARGETS_TO_BUILD="X86;Xtensa" \
    -G "Ninja" \
    /root/repo/llvm-xtensa \
  && ninja install

COPY --from=fetch-rust-xtensa /root/repo/rust-xtensa /root/repo/rust-xtensa
RUN mkdir --parents /opt/rust-xtensa \
  && cd /root/repo/rust-xtensa/ \
  && ./configure \
    --llvm-root /root/build/llvm-xtensa \
    --prefix /opt/rust-xtensa \
  && ./x.py build \
  && ./x.py install
RUN cp --recursive /root/repo/rust-xtensa/src /opt/rust-xtensa

#
# A stage for development.
#
FROM ubuntu:18.04 AS devel

RUN apt-get update \
  && DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \
    bison \
    ca-certificates \
    curl \
    flex \
    gcc \
    git-core \
    gperf \
    libncurses-dev \
    make \
    python \
    python-cryptography \
    python-future \
    python-pip \
    python-pyparsing \
    python-serial \
    python-setuptools \
  && rm --recursive --force /var/lib/apt/lists/*

COPY --from=build-llvm-xtensa /opt/rust-xtensa /opt/rust-xtensa
COPY --from=fetch-esp-idf /opt/xtensa-esp32-elf /opt/xtensa-esp32-elf
COPY --from=fetch-esp-idf /opt/esp-idf /opt/esp-idf

SHELL ["/bin/bash", "-c"]
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --no-modify-path
RUN source $HOME/.cargo/env \
  && rustup toolchain link xtensa /opt/rust-xtensa \
  && rustup default xtensa \
  && rustup toolchain list
RUN source $HOME/.cargo/env \
  && cargo install cargo-xbuild

ENV PATH /opt/xtensa-esp32-elf/bin:${PATH}
ENV IDF_PATH /opt/esp-idf
ENV XARGO_RUST_SRC /opt/rust-xtensa/src
RUN echo 'source $HOME/.cargo/env' >> ~/.bashrc
```

　上記の`Dockerfile`の`devel`ステージを以下の手順でビルドします。ビルドには30分〜2時間ほど掛かりますので気長に待ちましょう。

```text
mac$ DOCKER_BUILDKIT=1 docker image build \
  --target devel \
  --tag ${USER}/rust-xtensa \
  rust-xtensa
```

　本当はすべてmacOS環境に閉じたかったのですが、macOS上のDocker環境では、なぜかLLVMのビルドの途中で固まってしまうため、仕方なく別のLinux環境でビルドしてDockerイメージを転送しました。

## 5. 「Hello world!」のビルド

　上記でビルドしたDockerイメージを用い、「Hello world!」を出力するRustコードをビルドします。
前述の書籍のコードはGitHub上で公開されていますので、今回はそちらをそのまま使いました。

```text
mac$ docker container run --interactive --tty --rm \
  --volume $(pwd):/workspace \
  ${USER}/rust-xtensa \
  /bin/bash

docker$ cd /workspace/
docker$ git clone https://github.com/ciniml/esp32-rust-examples.git
docker$ cd esp32-rust-examples/hello_world/
docker$ make defconfig
docker$ make
```

　`make -j4`のように並列ビルドを有効にするとビルドに失敗するケースがあったため、上記では並列なしでビルドしています。並列ビルドする場合も、`make`コマンドを再び実行すると成功することが多かったです。

## 6. ESP32への転送

　Docker for MacはUSBデバイスのマウントをサポートしていないため、ビルドしたファームウェアの書き込み（転送）はmacOS上（Dockerの外側）で行いました。

　書き込み先デバイス（`/dev/tty.SLAB_USBtoUART`）はボード（書き込み装置）により異なります。適宜調整してください。

```text
mac$ pip install esptool
mac$ cd esp32-rust-examples/hello_world/
mac$ esptool.py \
  --chip esp32 \
  --port /dev/tty.SLAB_USBtoUART \
  --baud 115200 \
  --before default_reset \
  --after hard_reset \
  write_flash \
  -z \
  --flash_mode dio \
  --flash_freq 40m \
  --flash_size detect \
  0x1000 build/bootloader/bootloader.bin \
  0x10000 build/hello-world.bin \
  0x8000 build/partitions_singleapp.bin
```

## 7. 動作確認

　今回書き込んだRustコードは、「Hello world!」の文字列をシリアル通信（標準出力）に出力し、1秒後に再起動するという単純なものです。
`screen`コマンドを用いてシリアル通信の内容を確認する例は以下の通りです。

```text
mac$ screen /dev/tty.SLAB_USBtoUART 115200
...
ets Jun  8 2016 00:22:57

rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0018,len:4
load:0x3fff001c,len:6264
load:0x40078000,len:11624
load:0x40080400,len:6648
entry 0x40080764
I (29) boot: ESP-IDF v3.3 2nd stage bootloader
I (29) boot: compile time 07:39:47
I (29) boot: Enabling RNG early entropy source...
I (33) boot: SPI Speed      : 40MHz
I (38) boot: SPI Mode       : DIO
I (42) boot: SPI Flash Size : 4MB
I (46) boot: Partition Table:
I (49) boot: ## Label            Usage          Type ST Offset   Length
I (56) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (64) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (71) boot:  2 factory          factory app      00 00 00010000 00100000
I (79) boot: End of partition table
I (83) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x07dc8 ( 32200) map
I (103) esp_image: segment 1: paddr=0x00017df0 vaddr=0x3ffb0000 size=0x01e9c (  7836) load
I (107) esp_image: segment 2: paddr=0x00019c94 vaddr=0x40080000 size=0x00400 (  1024) load
I (112) esp_image: segment 3: paddr=0x0001a09c vaddr=0x40080400 size=0x05f74 ( 24436) load
I (131) esp_image: segment 4: paddr=0x00020018 vaddr=0x400d0018 size=0x12a28 ( 76328) map
I (158) esp_image: segment 5: paddr=0x00032a48 vaddr=0x40086374 size=0x01ca0 (  7328) load
I (167) boot: Loaded app from partition at offset 0x10000
I (167) boot: Disabling RNG early entropy source...
I (168) cpu_start: Pro cpu up.
I (171) cpu_start: Application information:
I (176) cpu_start: Project name:     hello-world
I (181) cpu_start: App version:      caf544c
I (186) cpu_start: Compile time:     Sep 29 2019 07:41:53
I (192) cpu_start: ELF file SHA256:  ac30c2c78aa66f96...
I (198) cpu_start: ESP-IDF:          v3.3
I (203) cpu_start: Starting app cpu, entry point is 0x40080ea0
I (195) cpu_start: App cpu up.
I (214) heap_init: Initializing. RAM available for dynamic allocation:
I (221) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (227) heap_init: At 3FFB2EC8 len 0002D138 (180 KiB): DRAM
I (233) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
I (239) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
I (246) heap_init: At 40088014 len 00017FEC (95 KiB): IRAM
I (252) cpu_start: Pro cpu start user code
I (270) cpu_start: Chip Revision: 1
W (271) cpu_start: Chip revision is higher than the one configured in menuconfig. Suggest to upgrade it.
I (274) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
Hello world!
Hello, from Rust
```

　C言語で出力した`Hello world!`、Rustから出力した`Hello, from Rust`が確認できました。

## 8. 参考

* 書籍『[M5Stackで始める組み込みRust](https://booth.pm/ja/items/1569213)』
* [espressif/llvm-xtensa: Work-in-progress version of LLVM for Xtensa. See also https://github.com/espressif/clang-xtensa](https://github.com/espressif/llvm-xtensa)
* [espressif/clang-xtensa: Work-in-progress version of Clang for Xtensa. See also https://github.com/espressif/llvm-xtensa](https://github.com/espressif/clang-xtensa)
* [MabezDev/rust-xtensa: Rust for the xtensa architecture. Built in targets for the ESP32 and ESP8266](https://github.com/MabezDev/rust-xtensa)
* [Rust on the ESP and how to get started — ctron's blog](https://dentrassi.de/2019/06/16/rust-on-the-esp-and-how-to-get-started/)