Rustで組み込みプログラミングの第一歩、LチカとHello Worldを試してみた

組み込みに向いていると言われるRustで、Lチカを試してみました。環境構築から、サンプルをビルド、実機にダウンロードして実行するまでの一通りを説明します。
2019.08.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

最近社内ではRustがちょっと流行ってきていて、社内勉強会を開催したりしています。

一方、社内の研究開発として、組み込みのプロトタイピングをやったりしています(たとえばDevelopersIO Cafeでも使っています)。

Rustは、システムプログラミングに向いているとも言われています。Rustで組み込みするのも一興かと思いますので、実際に試してみました。

やることは、組み込みのHello WorldであるLチカです。

stm32-rs

Rustの組み込み関係の状況を調べてみると、STM32というベンダーのチップを使ったツールチェインやライブラリの整備が、だいぶん進んできているようです。こちらのサイトThe Embedded Rust Bookで詳しく解説が提供されていたりもします。

実装の方法は各種あるようですが、githubのこちらのアカウントstm32-rsに集積されているものが良さそうに見えます。今回はこれを試してみることにしました。

ターゲット

ターゲットは、手元にあったNucleo-F103というボードを使用します。このボードはSTM32F103というチップが載っています。デバッグアダプタも付いていて安価です。

Nucleoでなくても、STM32ベースのハードウェア(Discoveryシリーズや、Blue Pillなどいろいろあります)であれば、同様のことが可能だと思います。

ツールの準備

すでにRust環境は手元にあるかもしれません。インストール方法はこちらにあるとおりですが、念のため載せておきます。とても簡単ですね。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

Rustのクロスコンパイル環境

STM32F103は、ARMのCortex-M3というアーキテクチャです。このアーキテクチャ用に、Rustのクロスコンパイル環境を適切に準備する必要があります。Cortex-M3の場合は、thumbv7m-none-eabiがターゲット名となります。rustupコマンドでターゲットを追加しておきます。

rustup target add thumbv7m-none-eabi

もし、Cortex-M0というアーキテクチャ(STM32F0xxが該当します)であれば、v7mの代わりにv6mを追加しておきます。

rustup target add thumbv6m-none-eabi

利用可能なターゲットの一覧は、次のように取得できます。

$ rustc --print target-list
aarch64-fuchsia
aarch64-linux-android
...中略...
x86_64-unknown-redox
x86_64-unknown-uefi

以前はRustで組み込みを試すにはnightlyに切り替える必要があったようですが、現在はstableのままで大丈夫なようです。お手元のrust環境にそのまま共存して使えます。

クロスツールのインストール

ARM用のバイナリファイルを取り扱うために、クロスツールを用意する必要があります。brew でインストールできます。

brew install armmbed/formulae/gcc-arm-none-eabi

openocdのインストール

Nucleoで走らせるためにはopenocdというツールを使用します。MacOSの場合は下記のようにして用意します。

brew install openocd

ソース

Lチカといっても、いろんな方法があります。適切な方法を見極めるのも目的の一つです。リポジトリを眺めてみたところ、HAL (Hardware Abstraction Layer)が整備されていましたので、今回はこれを使用してみます。

HALとは、ハードウェアの抽象化レイヤです。HALがあることによって、チップを変更しても修正は最小限で済ませることができるようになります。STM32はたくさんの種類があり、目的に応じて選択することができますが、HALがあることにより差異を吸収してくれます。

HAL以外にも、ボード固有のレイヤ、あるいはもっと低レベルの、チップのレジスタを直接操作するなどの方法がありますが、今回はHALを使ってみることにしました。HALのリポジトリ内にサンプルがたくさん用意されているので簡単に試せたというのが最大の理由ですが。

リポジトリをgithubからcloneします。

git clone git@github.com:stm32-rs/stm32f1xx-hal.git
cd stm32f1xx-hal

サンプルを小改造する

サンプルは、リポジトリのexampleディレクトリに用意されています。Lチカだけでも2種類あります。

  • blinky.rs
  • blinky_rtc.rs

しかしサンプルは、Nucleoボード用に作成されているわけではありませんので、ちょっとだけ修正する必要があります。

サンプルでは、LEDがGPIOのポートCの13番ピン(C13)に接続されていることが前提のようです。ところがNucleoはGPIO Aの5番に接続されています。これに合わせてソース example/blinky.rsを下記のdiffのように書き換えます。

-    // Acquire the GPIOC peripheral
-    let mut gpioc = dp.GPIOC.split(&mut rcc.apb2);
+    // Acquire the GPIOA peripheral
+    let mut gpioa = dp.GPIOA.split(&mut rcc.apb2);

     // Configure gpio C pin 13 as a push-pull output. The `crh` register is passed to the function
     // in order to configure the port. For pins 0-7, crl should be passed instead.
-    let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
+    let mut led = gpioa.pa5.into_push_pull_output(&mut gpioa.crl);
     // Configure the syst timer to trigger an update every second
     let mut timer = Timer::syst(cp.SYST, 1.hz(), clocks);

ビルドする

まずはビルドしてみましょう。

example内にサンプルはたくさん入っていますが、cargoの引数に与えるだけで簡単に選んでビルドできます。また、チップの名称をfeaturesとして引数で渡します。これはstm32f1xx-halクレート固有の事項です。Cargo.tomlのfeaturesセクションで確認することができます。

ビルドの初回は依存クレートをダウンロードしたり、それらのコンパイルで少し時間がかかります。

cd stm32f103-hal
cargo build --example blinky --release --features=stm32f103

実機で動かす

さて、ビルドが成功したら、次は実機で動かしてみます。

接続

USBでホストPCと接続します。Nucleoは今は珍しくなったUSB mini-Bです。ケーブルを確保しておきましょう。USBを接続したら、USBドライブとしてマウントされます。これはそのまま放置です。

ターミナルを一つ開いてopenocdを起動します。このとき、ターゲットに合わせて設定ファイルを指定します。設定ファイルはopenocdとともに/usr/local/share/openocd/scriptsにインストールされています。一度どんなものが用意されているか眺めてみると良いかもしれません。

Nucleoは、STLink-V2/1というインターフェースなので、これ用の設定ファイルを指定します。またターゲットに合わせてstm32f1xを指定します。

openocd -f interface/stlink-v2-1.cfg -f target/stm32f1x.cfg

エラーが出ずに、そのまま待機状態になることを確認します。

もしエラーが出て、プロンプトに戻ってきてしまう場合は、ファームウェアをアップデートする必要があるかもしれません。方法は後述します。

実行

openocdを動作状態にしたまま、cargoコマンドでrunします。通常通りのビルドが終わった後にgdbが起動します。

cargo run --example blinky --release --features=stm32f103
...中略...
    Finished release [optimized + debuginfo] target(s) in 3m 22s
     Running `arm-none-eabi-gdb target/thumbv7m-none-eabi/release/examples/blinky`
GNU gdb (GNU Tools for Arm Embedded Processors 7-2018-q2-update) 8.1.0.20180315-git
Copyright (C) 2018 Free Software Foundation, Inc.
...中略...
Reading symbols from target/thumbv7m-none-eabi/release/examples/blinky...done.
core::ptr::read_volatile (src=<optimized out>) at /rustc/3f55461efb25b3c8b5c5c3d829065cb032ec953b/src/libcore/ptr/mod.rs:920
920	/rustc/3f55461efb25b3c8b5c5c3d829065cb032ec953b/src/libcore/ptr/mod.rs: No such file or directory.
semihosting is enabled
Loading section .vector_table, size 0x400 lma 0x8000000
Loading section .text, size 0x392 lma 0x8000400
Loading section .rodata, size 0x58 lma 0x8000794
Start address 0x800050c, load size 2026
Transfer rate: 9 KB/sec, 675 bytes/write.
DefaultPreInit () at /Users/xxx/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.10/src/lib.rs:564
564	pub unsafe extern "C" fn DefaultPreInit() {}
(gdb)

自動的にopenocdと接続が行われ、バイナリがボードにロード(フラッシュメモリへの書き込み)が完了して、途中で実行が止まった状態となります。openocdへの接続やボードへのロードが自動でされるのは、.gdbinitファイルがプロジェクト内に用意されているためです。プロジェクトによっては用意されていないこともありますので、仕組みは把握しておきましょう。

gdbの下記のコマンドを実行します。

(gdb) continue

そうすると、ロードしたコードが実行されます。

無事LEDが点滅するのが確認できました!

semihostでHello World

ARMの組み込み系には、semihostという仕組みがあります。これを使うと、実機側で生成したテキストによるログをホスト側で表示することができます。

hello.rsというサンプルは、semihostによる Hello World です。こんなソースコードです。

//! Prints "Hello, world" on the OpenOCD console

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use panic_semihosting as _;

use stm32f1xx_hal as _;
use cortex_m_semihosting::hprintln;

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    hprintln!("Hello, world!").unwrap();
    loop {}
}

先ほどのblink.rsと同じように実行してみます。

cargo run --example hello --release --features=stm32f103
...
(gdb) continue

そうすると、openocd側のターミナルを確認すると、openocdの出力に混じってHello, World!が表示されているのがわかります。

Info : accepting 'gdb' connection on tcp/3333
Info : device id = 0x20036410
Info : flash size = 128kbytes
undefined debug reason 7 - target needs reset
semihosting is enabled
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x0800050c msp: 0x20005000, semihosting
Info : Padding image section 0 with 18 bytes
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x2000003a msp: 0x20005000, semihosting
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000520 msp: 0x20005000, semihosting
Info : halted: PC: 0x08000772
Hello, world!

これを使うと、ログを吐いたり、デバッグが便利にできそうです。

openocdがうまく動かない時は

実は当初、openocdを起動したときに、エラーが発生していました。これはボードのファームウェアのバージョンが古いせいでした。

こんなエラーです。このメッセージでは何が悪いのかよくわかりません。

$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f1x.cfg
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
in procedure 'init'
in procedure 'ocd_bouncer'

この原因は、ファームウェアのバージョンが古いせいでした。Nucleoのファームウェアのアップデータは、STMicroelectronicsのサイトに用意されていますので、ダウンロードしてきます。

ダウンロードしたZipファイルを解凍すると、アップデータが2種類入っています。一方はWindows専用のバイナリ、もう一つがAllPlatformsで、Linux/MacOS/Windowsなどどのプラットフォームでもアップデートが可能です。ただしJavaの実行環境が必要となります。

ボードをUSBで接続し、ターミナルから下記を実行します。

cd stsw-link007/AllPlatforms
java -jar STLinkUpgrade.jar

画面が現れますので、Upgradeボタンを押してアップデートします。

アップデートに成功した後は、こんな感じでopenocdが正常に起動するようになります。

$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f1x.cfg
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v34 API v2 SWIM v25 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 3.251613
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints

まとめ

開発ボードであるNucleo-F103を使って、RustでLチカを試してみました。stm32-rsとして整備されているHALとそのサンプルを使って簡単に試すことができました。

またsemihostを使って、伝統的なテキスト版Hello Worldも試せました。シリアルポート等を使わずにデバッグメッセージを出せるので、いろいろと捗りそうです。

参考