今回、STM32F401を搭載した開発ボード、STM32F401 Nucleo-64を入手しました。マイコンは、ARM Cortex-M4をCPUコアとした、かなり高機能なものです。
ペリフェラルも豊富で、開発環境も、STMより、STM32CUBE IDEが無償で使えます。これで使ってみた感じも、相当使いでも良かったのですが・・・
そういえば、RUSTで、ベアメタル環境の整備が一番進んでいるのも、ARMと聞いたような気が・・・というわけで、一度、RUSTでこのボードを動かしてみようと思い立ってやってみました。
どうにかこうにか、Lチカまではなんとかなったので、そこまでのメモです。
開発環境
開発用PCのOS
今回の開発環境としてのPCには、ubuntu19.10のLinux環境を使用しています。基本的な開発環境は、ほぼ入れてあるので、もしかすると、生のOSから比較するとここに記載していないものもあるかもしれません。適時、判断ください。
RUSTの基本環境
RUSTの基本的な開発環境は、整っているものとします。(この言語、実は、初めてとっかかるとかなり癖のある言語です。ここで、基本的な開発環境から説明が必要な方ですと、多分、挫折します。言語入門の参考文献としては、Rustの日本語ドキュメントにある、プログラミング言語 Rust, 2nd Edition/ The Rust Programming Language, Second Editionが参考になるでしょう。これ一冊でほぼ入門書がいらない程度の内容を誇ります。ただし、難易度は、何か他の言語を一応使えるレベルの人向けです。)
さて、その上で、いくつかの追加が必要です。
いろいろなところを見ると、nightly環境が必須と書いてあるところも多いのですが、基本Lチカまでなら、stable環境で開発可能でした。
今回試した、RUSTのバージョンは、
$ rustc -V
rustc 1.42.0 (b8cedc004 2020-03-09)
となります。
さらに、
- binutils-arm-none-eabi(リンカ等として)
- gdb-multiarch(デバッガ。GDBの多環境バージョン。)
- openocd(デバッガをボードに接続するためのインターフェース。GDBサーバー)
がツールとして必要となります。これらは、このままの名前でapt installでインストール可能です。
RUSTの追加環境として、
$ rustup target add thumbv7em-none-eabihf
$ cargo install cargo-binutils
$ rustup component add llvm-tools-preview
$ cargo install cargo-generate
が必要となります。
openocdの接続テスト
ここで、openocdが動くかどうかテストしておきます。調べていると、udevルールの設定が必要(一般ユーザーで接続を可能とするため。)という記事もあったのですが、私の場合、何もせずにすんなり行ってしまいました。
まず、ボードをUSBで開発PCに接続します。
その後、次のようにopenocdを起動した時、画面のようになれば、正常です。
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f4x.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: 2000 kHz
adapter_nsrst_delay: 100
none separate
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : clock speed 1800 kHz
Info : STLINK v2 JTAG v36 API v2 SWIM v26 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 3.275313
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
なお、コマンドの引数のファイル2つですが、/usr/share/openocd/scriptsの下にあります。ターゲットボードが違う場合は、ここから適合するものを探して書き換えてください。
ちなみに、実行すると、端末をブロックします。開発中は、端末を一枚、このコマンドを実行したままで、置いておくことになります。
プログラム制作
テンプレートのダウンロード
STM32マイコンには、OSなんて良いものはついていません。普通、OSのサポート無しでプログラムは動くことになります。専用の統合開発環境と、HALを使うと、このあたりをずいぶんと吸収してくれます。
まったく最初からプログラムを作ろうとすると、ターゲットになるCPUの設定のみならず、リンカの設定を触り、メモリー配置まで自分でちゃんと設定する必要があります。また、rustにコンパイル手順とリンク手順を示すことも必要です。かくして、結構な"おまじない"の数々が必要です。
でも、幸いなことに、STM32に関しては、このあたりのおまじないを全部唱えていてくれるテンプレートが存在します。このテンプレートを利用するために、cargo-generateを導入しました。このコマンドは、cargo newのテンプレート版とでもいうべきものです。
テンプレートは、https://github.com/rust-embedded/cortex-m-quickstartにあります。これ、このままgit cloneで使っても問題ありません。cargo generateを使うと、プロジェクトのディレクトリを作って、Cargo.tomlのプロジェクト名をついでに設定してくれます。ちょっと楽です。
$cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
Project Name: test0
Creating project called `test0`...
Done! New project created /home/mito/develop/stm32/rust/test0
こんな感じで、プロジェク名を聞いてきてくれるので、入力すると、この設定だけ済ませてくれます。
では、test0ディレクトリに入りましょう。最初から、結構な数のファイルが作成されています。
Cargo.tomlの確認
いくつか、自分のボード環境に合わせて修正が必要なところがあります。
列挙のような感じで示します。
まずは、おなじみのCargo.toml。
見てみると、cargo newで作ったものより、たくさんの項目があります。
ほとんど、触る必要はありません。
ひとつだけ、項目を追加します。
[package]
authors = ["hogehoge"]
edition = "2018"
readme = "README.md"
name = "test0"
version = "0.1.0"
[dependencies]
cortex-m = "0.6.0"
cortex-m-rt = "0.6.10"
cortex-m-semihosting = "0.3.3"
panic-halt = "0.2.0"
[dependencies.stm32f4]
version = "0.11.0"
features = ["stm32f401", "rt"]
# this lets you use `cargo fix`!
[[bin]]
name = "lchikawithswitch"
test = false
bench = false
[profile.release]
codegen-units = 1 # better optimizations
debug = true # symbols are nice and they don't increase the size on Flash
lto = true # better optimizations
これが、完成形です。[dependencies.stm32f4]のブロックを追加します。
このクレートは、stm32f401を使うためのハードウェアレジスタへのアクセス手段を提供してくれるモジュールです。これがないと、I/Oレジスタへのアクセスをアドレスからの逆参照で行うことになり、unsafeの山が出来上がります。
また、最初から入っているcortex-mと、cortex-m-rtに関しては、このテンプレートを作成した方と同じ方が作成された、cortex-m系列のCPUへアクセスするための基本的な手続きと、初期化手続きが収められています。
profile.releaseにdebug=trueが指定されています。stm32でプログラムを作成する際、デバッグはリモートで行うため、デバッグ情報はボードには一切転送されません。そして、これをつけておくと、releaseバージョンでも、リモートでのシンボリックデバッグが可能になります。コスト無しで実施できますので、trueにしておきましょう。
(2020年5月23日更新:stm32f401モジュールのバージョンが0.11.0に上がっていました。いくつかのunsafeが消えたほか、割込み定義が不足していた部分が改定されています。こちらの文書も、0.11.0に変更しておきます。)
memory.x
プロジェクトトップのディレクトリにあります。
このファイルは、リンカに、ボードのCPUのメモリー配置情報を知らせるためのものです。自分のボードのCPUに合わせて、正確に指定する必要があります。CPUのデータシートをwww.st.comから入手して、memory mapの項目を探せば必要な情報はでてきます。メモリーマップから、次の2つのアドレスを探し出してください。
- flash memoryのスタートアドレス。及び、サイズ。
- SRAM のスタートアドレス。及び、サイズ。
今回の事例の場合は、STM32F401が対象となりますので、この数字は、
- Flash memory : 0x0800 0000 - 0x0807 FFFF (size 512K)
- SRAM : 0x2000 0000 - 0x2001 7FFF (size 96K)
となります。これをmemory.xに指定します。
MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
/* TODO Adjust these memory regions to match your device memory layout */
/* These values correspond to the LM3S6965, one of the few devices QEMU can emulate */
FLASH : ORIGIN = 0x08000000, LENGTH = 512K
RAM : ORIGIN = 0x20000000, LENGTH = 96K
}
サイズに関しては、始点と終点を引き算するか、データシートの一番最初の要約のところにも書いてあります。
.cargo/config
タイトルの通り、プロジェクトトップの隠しフォルダに、configファイルがあります。このファイルは、必要に応じたところのコメントアウトをつけ外しするだけ済むようにすでに作成されています。
まずは、完成形のファイルの内容から。
[target.thumbv7m-none-eabi]
# uncomment this to make `cargo run` execute programs on QEMU
# runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# uncomment ONE of these three option to make `cargo run` start a GDB session
# which option to pick depends on your system
# runner = "arm-none-eabi-gdb -q -x openocd.gdb"
runner = "gdb-multiarch -q -x openocd.gdb"
# runner = "gdb -q -x openocd.gdb"
rustflags = [
# LLD (shipped with the Rust toolchain) is used as the default linker
"-C", "link-arg=-Tlink.x",
# if you run into problems with LLD switch to the GNU linker by commenting out
# this line
# "-C", "linker=arm-none-eabi-ld",
# if you need to link to pre-compiled C libraries provided by a C toolchain
# use GCC as the linker by commenting out both lines above and then
# uncommenting the three lines below
# "-C", "linker=arm-none-eabi-gcc",
# "-C", "link-arg=-Wl,-Tlink.x",
# "-C", "link-arg=-nostartfiles",
]
[build]
# Pick ONE of these compilation targets
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
修正点と、ポイントです。
まず、一番下の方の[build]セクションで、ターゲットボードのCPUを選択します。今回の事例だと、Cortex-M4FのFPU有りとなります。このコメントアウトを削除し、代わりに、デフォルトになっていた、Cortex-M3の行をコメントアウトします。
次に二番目の長い名前のセクションです。ここに、runnerの3行が全てコメントアウトされています。これは、cargo runを実行した時に、何を実行するかの指定です。私の場合ですと、gdb-multiarchをインストールしましたので、2番めのコメントアウトを外しました。これで、cargo runを実行すると、
コンパイル→GDBの起動→プログラムのロード→実行して即時ブレーク
までの一連の手順を自動で実行してくれます。
openocd.cfg
先程、テストでopenocdを起動して接続まで実施しました。この時のコマンドラインオプションの内容が書いてあります。これを修正しておくと、$ openocd -f openocd.cfg
の一発で接続できるようになります。
# Sample OpenOCD configuration for the STM32F3DISCOVERY development board
# Depending on the hardware revision you got you'll have to pick ONE of these
# interfaces. At any time only one interface should be commented out.
# Revision C (newer revision)
source [find interface/stlink-v2-1.cfg]
# Revision A and B (older revisions)
# source [find interface/stlink-v2.cfg]
source [find target/stm32f4x.cfg]
修正点は、sourceの二行を先のテストで使ったファイル名に書き換えるだけです。
ドキュメント生成
雛形のsrc/main.rsは、すでにテンプレートに入っています。また、exampleには参考になるファイルがいくつか入っています。
ここで、一度、docの生成を行っておきます。
理由は、stm32f4クレートなんですが、crates.io/cratesから検索をかければ、クレートは見つかるんですが、APIドキュメントがアップされていません。これ非常に困ります。でも、ソースにはちゃんと完全なドキュメントコメントがついています。ですから、自分で作っておくことにします。
作り始めてからでも出来ます。できますが・・・ソースにエラーがあると、ドキュメントも作成できません。素のままならば、ちゃんとコンパイルも通る形ですので、今のうちというわけです。
$ cargo doc
ライブラリーを全部ダウンロードしてきますので、少々時間がかかります。おとなしく待ちましょう。
出来上がったドキュメントは、プロジェクトルートから、target/thumbv7em-none-eabihf/doc/test0/index.html
をブラウザで開けば、参照できます。左側のCratesの欄に、stm32f4のリンクが有りますので、stm32f401クレートの使い方は、ここから調べましょう。
stm32f401のリファレンスマニュアル
stmの日本法人のページで、https://www.stmcu.jp/design/document/reference_manual_j/に、stm32f401の日本語のリファレンスマニュアルが有ります。プログラムを組むときには、隣に常に置いておくファイルですので、ダウンロードしておきましょう。
Lチカ(点灯のみ)のプログラム作成。
今回は、PC8(CN10の2番端子)に、LEDを接続します。適当な電流制限抵抗を通して、LEDを接続しておいてください。
src/main.rsをまず示します。
#![no_std]
#![no_main]
// pick a panicking behavior
extern crate panic_halt; // you can put a breakpoint on `rust_begin_unwind` to catch panics
// extern crate panic_abort; // requires nightly
// extern crate panic_itm; // logs messages over ITM; requires ITM support
// extern crate panic_semihosting; // logs messages to the host stderr; requires a debugger
use cortex_m_rt::entry;
use stm32f4::stm32f401;
#[entry]
fn main() -> ! {
// stm32f401モジュールより、ペリフェラルの入り口となるオブジェクトを取得する。
let perip = stm32f401::Peripherals::take().unwrap();
// GPIOCポートの電源投入(クロックの有効化)
perip.RCC.ahb1enr.modify(|_,w| w.gpiocen().set_bit());
// gpio初期化(PC8を出力に指定)
let gpioc = &perip.GPIOC;
gpioc.moder.modify(|_,w| w.moder8().output());
loop {
gpioc.bsrr.write(|w| w.bs8().set());
}
}
おまじないの部分とスケルトンは、すでにファイルが入っていますので、これを編集します。
おまじないの部分で、use cortex_m::asm;
は、今は削除して大丈夫です。コメントアウトしても良いでしょう。
また、stm32f4クレートを使用しますので、use stm32f4::stm32f401;
を追加します。
最初に、#![no_std]
と、#![no_main]
があります。これは、ようするに、プログラムがベアメタルであることの宣言です。つまり、OSのサポートが必要な標準ライブラリが一切使えないことを意味します。一番痛いのは、メモリー管理機構となるでしょう。平たく言えば、Vectorが使えません。メモリー管理を自前で準備すれば、使えるようになります。
本当の最低限のライブラリ機構は、使用できます。これは、coreと呼ばれます。Rustのマニュアルにも、coreクレートとして説明が有ります。stdクレートのサブセットです。
後、当然のごとく、ディスプレイがないのでprintln!も使えるわけ無いですね。こちらは、cortex-m-semihostingクレートを使うと、openocdを端末としてデバッグの際に使うことが出来ます。
後、プログラムがパニックした時に、どうするかの指定が、最初の方にある// pick a panicking behavior
で始まる数行です。この中で、ひとつだけコメントアウトを外します。デフォルトは、panic_halt、つまり、黙って止まります。
no_mainと言っちゃったので、入り口の指定と、mainに入る前の最低限の起動準備が必要です。これには、スタックの準備や割り込みベクタテーブルの準備などが有ります。これを一式でやってくれるのが、#[entry]
の一言です。cortex_m_rtクレートがこれを担ってくれます。
本体は、最低限のLEDの点灯です。
同時に、IOを制御する一般手順ともなります。
まず、GPIOを利用するためには、
- GPIOポートへのクロックの有効化(RCC_AHB1ENRのGPIOCENビットをオン)
- GPIOポートのモード(入出力方向)の指定(GPIOC_MODERのMODER8[1:0]を01に設定)
- GPIOポートの出力レジスタへの書き込み(GPIOC_BSRRのBS8ビットをオン。オフはBR8ビットをオン)
の手順を踏むことになります。
ずいぶんと仰々しい関数群の山ですが、この関数たち、殆どは、型の整合をRUSTに対して約束し、本来unsafeなメモリーアドレスの逆参照を正しく保証するためだけに有ります。そして、コンパイル結果は、RUSTのゼロコスト安全性を象徴するかのように、殆どがメモリーへの書き込みコードだけに翻訳されます。
まずは、入り口になるオブジェクトを取得します。stm32f401::Peripherals::take().unwrap()
の部分です。これは、peripheralsオブジェクトがプログラム内で唯一つ取得されることを保証しているビルド関数です。全ての操作は、このオブジェクトから始まります。Peripherals構造体のドキュメントを見ると、stm32f401に用意されている全てのペリフェラルのレジスタへの構造体をメンバーとして持っています。
最初に、GPIOCの電源投入です。STM32では、省電力を図るため、デフォルトでほぼすべてのIOへのクロックの供給を停止しています。というわけで、まずは、クロックの供給を開始する必要が有ります。
クロックを有効化するためには、RCC_AHB1ENRレジスタのgpiocenフィールドをセットすることになります。このレジスタの説明は、SMT32F401リファレンスの「リセット及びクロック制御」の章に有ります。
perip.RCC.ahb1enr.modify(|_,w| w.gpiocen().set_bit());
が、ビットの操作を行う一般形式ともなります。RCCのahb1enrレジスタまでは、素直に構造体のメンバーです。ahb1enrメンバーは、modify・write・readの各関数をサポートし、各々の関数でレジスタの更新・書き込み・読み出しを行うライター・リーダーを用意してくれます。先程作ったドキュメントをブラウザで開き、stm32f4→stm32f401とたどってみましょう。Structsの項にあるのが、レジスタの全てです。RCCの中を見てみましょう。ptr()関数をサポートし、この関数の戻り値は、RegisterBlockへのポインターとなっています。RCC.****
で直接この戻り値へのアクセスが可能です。RegisterBlockの中身を見てみます。各種のレジスタが、構造体のフィールドとして用意されています。今回は、ahb1enrレジスタを操作するので、そこを見てみます。トレイトが3つサポートされています。そのうち、ReadableとWritableが書き込み、読み出しをサポートしています。このトレイトの説明のリンクは、私の今のバージョンでは残念ながら死んでいます。左のモジュール欄から、ahblenrを参照しましょう。Type Definitionsの中に、Wがあります。この構造体が、書き込みをサポートしています。たくさんのジェネリック構造体を個別にインプリメントしていてAHB1ENR用には、このレジスタにあるフィールドへの書き込み構造体へのアクセス関数が用意されています。この構造体がクロージャーに渡されるwの本体です。今回は、gpiocenをセットするので、GPIOCEN_Wを見てみましょう。フィールドのセットのための関数が用意されています。今回は、enabled()を呼び出すだけで済みそうです。
ひとつだけ詳細に書いてみまみましたが、このやり方で、ほとんど全ての構造体、ひいては、ほとんど全てのレジスタへのアクセス方法をたどることが出来ます。ひとつだけ注意事項。実は、write関数の方は、引数がwrite(|w| w.*****)
でアクセスできます。少し短く、誘惑に駆られるのですが、フィールドを更新する際には、modify(|r,w| w.****)
の形式を使う必要が有ります。ここではまりました。writeを使うと、同じレジスタの書き込み対象以外の全てのフィールドがリセットされてしまいます。
後は、同様に、追ってみてください。GPIOC_BSRRだけは、書き込み専用レジスタなので、modify関数ではなく、write関数を使います。
コンパイルからロード・実行
コンパイル
コンパイルは、普通と同様に、cargo build --release
でできます。--releaseを省略すれば、いつものようにデバッグバージョンが出来上がります。
今回は、リリースバージョンで、続きの手順を進めます。
ボードへの書き込み
さて、ボードへの書き込みです。先のテスト起動のopenocdが立ち上がっていれば、そのままにしておきます。終了しているなら、新しい端末ウィンドウを起動し、プロジェクトルートまで移動します。
そして、openocd -openocd.cfg
とすると、openocdの起動と接続が完了するはずです。このウィンドウはこのままにして、作業用の端末に戻ります。
$ telnet localhost 4444
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
>
と、telnetを実行して先のopenocdに接続を行います。
この状態で、更に
> reset halt
Unable to match requested speed 2000 kHz, using 1800 kHz
Unable to match requested speed 2000 kHz, using 1800 kHz
adapter speed: 1800 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000324 msp: 0x20018000
> flash write_image erase target/thumbv7em-none-eabihf/release/test0
auto erase enabled
device id = 0x10016433
flash size = 512kbytes
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x20000046 msp: 0x20018000
wrote 16384 bytes from file target/thumbv7em-none-eabihf/release/test0 in 0.612251s (26.133 KiB/s)
> reset run
Unable to match requested speed 2000 kHz, using 1800 kHz
Unable to match requested speed 2000 kHz, using 1800 kHz
adapter speed: 1800 kHz
>
と3つのコマンドを実行します。reset halt
でCPUを停止し、flash write_image erase プログラムバイナリのパス
でCPUのフラッシュへの書き込みを実行します。そして、reset run
で、プログラムの実行を開始します。今回は、チカリとLEDが点灯して、それで終わりです。
プログラムのロードの手抜き。
プログラムのロードが少し面倒です。別ウィンドウでopenocdが必要になりますし、コマンドもパスのおかげで長くなってしまいます。(まぁ、一度打てば次からは↑キーで履歴が使えますが・・・)
せめて、コマンド一発で、ロードができるようにopenocdのスクリプトを組んでみます。
# OpenOCD STM32 cfg with Flash proc
telnet_port 4444
gdb_port 3333
source [ find interface/stlink-v2-1.cfg ]
set WORKAREASIZE 0x5000
source [ find target/stm32f4x.cfg ]
proc flash_elf {ELF_FILENAME} {
reset
halt
flash write_image erase $ELF_FILENAME
verify_image $ELF_FILENAME
echo "flash program complete. reset and run."
reset run
exit
}
init
これをプロジェクトルートに置いておきます。
このスクリプトを使うときは、別ウィンドウのopenocdは、^cで終了しておいてください。直接openocdをこちらで起動するので、2重起動となりエラーがでます。
何故か、たまに失敗します。失敗しても、もう一度やれば、大概、転送OKになります。ただし、マイコンボードのフラッシュアクセスの関係でCPUをフォルトさせるとこのコマンドエラーになります。何か、設定が足りないのだとは思いますが・・・。この場合は、openocdへのtelnet接続から手動でやると、うまくいきました。まぁ、手抜きなので9割成功すれば良しとしておきます。
起動は、
$ prog=target/thumbv7em-none-eabihf/release/test0
$ openocd -f flash.cfg -c "flash_elf $prog"
とこれで、OKです。progシェル変数を使った理由は、-cの後では、ファイル名のTAB補完が効きません。変数への代入のときには、効きます。という入力上の都合です。どうせ、このパスは何度も打つことになりますし、同じ端末上では一度、変数セットするだけで次回からはopenocdの実行だけになり便利です。
デバッガの利用
デバッガの利用は、少しおまじない手順があるので、openocd.gdbというファイルがプロジェクトルートにあるので、それを使うのが便利です。これを便利に使うために、.cargo/configのrunnerを設定しました。
まずは、別端末でプロジェクトルートにいき、openocdを立ち上げておきます。
後は、いつものようにcargo run
でOKです。リリースバージョンで実行するなら、--releaseをつけられます。コンパイルからボードへの書き込み、リセットと実行まで済ませて、実行直後にブレークをかけて停止します。
ちなみに、mainにブレークポイントが設定された状態になっているので、continueの実行で、mainまで一気に実行できます。
この後の手順はGDBの使い方を別のところで探してください。わたしも、説明できるほど詳しくありません(苦笑)
ここまでの参考文献
stm32で、rustを試してみようという、動機になった記事です。とてもわかり易い記事でしたが、細かいところで、現状のRUSTに追いついていないところもあり、このメモを書くことにしました。
rustの日本語に翻訳されたドキュメントが有ります。
組み込み環境でrustを使う際のドキュメントです。かなり内容も詳しいです。
STM32のリファレンスの日本語があります。ただ、翻訳されていない文書も多々有り、それは本家の、www.st.comで英文を取ってくるしか無いです。
つたない文章ですが、参考になれば、幸いです。