STM32 Rust ベアメタルにGPIOを操作する

STM32 Rust ベアメタルにGPIOを操作する

環境構築の記事では、HAL を使いました。
HAL は Hardware Abstraction Layer (ハードウェア抽象化レイヤ)の略です。
HAL については別の機会にお話しすることにして、今回は HAL を使わずに GPIO を操作してみます。

ベアメタルと言われているレジスタにアクセスする方法です。

開発環境は以下の通りです。

PC:Windows10 OS
Board:STM32Nucleo-F401RE
デバイス:STM32F401RE
エディタ:VSCode
言語:Rust

ボードの情報は こちら からご覧いただけます。
環境構築については こちら をご覧になってください。

プロジェクトをつくる

コマンドプロンプトを起動し、cargo generate と git を使ってSTM32F401用のプロジェクトを作成します。

cargo generate –git https://github.com/rust-embedded/cortex-m-quickstart.git と入力しプロジェクト名を聞いてくるので stm32f401-gpio と入力します。

cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart.git
 Unable to load config file: C:\Users\xxxxx\.cargo\cargo-general.toml
 Project Name : stm32f401-gpio

C:\Users\xxxxx\stm32f401-gpio というフォルダができていることを確認します。

VSCodeを起動する

以下のようにコマンドを入力することで、stm32f401-gpioをカレントディレクトリとしてエディタ(VSCode)が起動します。

cd stm32f401-gpio
code .

C:\Users\xxxxx>cd stm32f401-gpio
C:\Users\xxxxx\stm32f401-gpio>code .

config.tomlの編集

gdbの指定と Cortex-M4F をターゲットに指定します。

ツリーの >.cargo の >部分をクリックすると V .cargo となり、そのツリー下に config.toml というファイルが見えるのでこれをクリックして開きます。

(1)こちらの環境では 8行目に # runner = “arm-none-eabi-gdb -q -x openocd.gdb” があるので、先頭の # をはずします。
(2)こちらの環境では35行目に target = “thumbv7m-none-eabi” # Cortex-M3 があるので、先頭に # をつけます。
前後の行に合わせて # target = … のように記述しておきます。
(3)こちらの環境では37行目に # target = “thumbv7me-none-eabihf” # Cortex-M4F and Cortex-M7F (with FPU) があるので、先頭の # をはずします。

# のある行はコメント(無効)になるようですね。

編集が終わったら Ctrl + s で保存しておきます。

launch.jsonの編集

ここではデバイスの指定を行います。
ツリーの >.vscode の >部分をクリックすると V .vscode となり、そのツリー下に launch.json というファイルが見えるのでこれをクリックして開きます。

“configurations”: の中に {},{} で2つの項目が区切られています。
上の方は “name” が “Debug (QEMU)” となっていて、下の “name” が “Debug (OpenOCD)” です。
QEMUはエミュレーターで、今回は OpenOCD による Debug を行いますので、下の {} 内だけを編集します。

(1)こちらの環境では35行目に #device”: “STM32F303VCT6” とあるので “STM32F401RET6” に変更します。
(2)こちらの環境では38行目に “target/stm32f3x.cfg” とあるので “target/stm32f4x.cfg” に変更します。
(3)こちらの環境では40行目に “svdFile”: “${workspaceRoot}/.vscode/STM32F303.svd” とあるので最後の 303 を 401 に変更します。

編集が終わったら Ctrl + s で保存しておきます。

Cargo.tomlの編集

ツリーの下の方に Cargo.toml があります。

今回はhalを使わずに stm32f4 というクレートを使いますので、それを指定しておきます。
クレートとはライブラリのようなものです。

23~25行を以下のように編集します。
dependencies は依存関係の意味で、こういうクレートの、このバージョンのものを使いますよ とRustに教えてあげます。

今回は HAL を使わないので stm32f4クレートを指定します。

[dependencies.stm32f4]
features = ["stm32f401", "rt"]
version = "0.14"

編集が終わったら Ctrl + s で保存しておきます。

クレートは cretes.io から探すことができます。

memory.xの編集

ツリーの下の方に memory.x があります。
デバイスによってFlashメモリーとRAMの容量が異なるので、ここで指定します。

6, 7行目を以下のように編集します。

  FLASH : ORIGIN = 0x08000000, LENGTH = 512K
  RAM : ORIGIN = 0x20000000, LENGTH = 96K

編集が終わったら Ctrl + s で保存しておきます。

svdファイル

svdファイルはデバッグに必要なファイルです。

svdファイルは こちら からダウンロードできます。

解凍して、data\STMicroの中にある STM32F401.svd を C:\Users\xxxxx\stm32f401-gpio\.vscode に保存します。

回路とLチカ

マイコン関連で最初に書くお決まりのコードと言えばLチカですね。
「Lチカって何?」と思われる方のために簡単に説明しておきます。
LはLEDの略、チカはチカチカの略で、LEDをチカチカと点灯消灯を繰り返すことをいいます。
I/Oの制御で最も初歩的なものでプログラムの動作確認に良く使われます。

NUCLEO-F401REの回路図は こちら からダウンロードできますので参考にしてください。

回路図(4/4)より制御できそうなLEDはLD2(緑色)です。

信号名がD13になっていますが回路図(2/4)を見るとPA5につながっていて、これはGPIOポートAのビット5です。
このビットを 0/1 することでLD2をチカチカさせることができそうです。

この回路では PA5 を 1 (High) にすることでLD2が点灯します。

ソースコード

main()関数の外側は、組み込み特有の記述になります。
このように書くものだと考えておけば良いと思います。

#![no_std]
#![no_main]

use panic_halt as _; // you can put a breakpoint on `rust_begin_unwind` to catch panics
use cortex_m_rt::entry;
use stm32f4::stm32f401; // (1)

#[entry]
fn main() -> ! {
    let dp = stm32f401::Peripherals::take().unwrap();       // (2)
    dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());    // (3)
    let gpioa = &dp.GPIOA;                                  // (4)
    gpioa.moder.modify(|_, w| w.moder5().output());         // (5)    
    gpioa.odr.modify(|_, w| w.odr5().high());               // (6)
    gpioa.odr.modify(|_, w| w.odr5().low());                // (7)
    gpioa.odr.modify(|_, w| w.odr5().high());    
    gpioa.odr.modify(|_, w| w.odr5().low());    
    loop {
        // your code goes here
    }
}

コードの解説

このへん にアクセスする例が出ているので、雰囲気はつかめると思います。

最初なので詳しく見ていきます。

(1)stm32f4クレートのstm32f401モジュールを使うという宣言
(2)GPIOを制御するために、まず Peripherals を取得する必要があります。

まず Peripherals を眺めて見てください。

Struct stm32f4::stm32f401::Peripherals と書かれています。

Peripherals は stm32f4というクレートの、stm32f401 というモジュールの構造体です。
この構造体のフィールドに GPIO があるので、まず Peripherals を取得する必要があります。

そして Peripheralsの実装に、pub fn take() -> Option があります。

take()は Option<>型を返すことがわかります。
ここでは unwrap()して Self のみを取り出しています。
Peripherals はシングルトンになっていて、take()は1回目だけが成功するようになっています。
2回目の take() では panic するので注意してください。

(3)最近のマイコンはペリフェラル単位で電源のOn/Offができるようになっているようです。
STM32も低消費電力が売りのArmマイコンですから、ある意味当然でしょうか。
使わないペリフェラルのクロックへの電源供給をOffにすることで消費電力を抑えることができます。

ベリフェラルを扱うにはレジスタについて理解する必要があります。
こちら からリファレンスマニュアル(RM0368)の日本語版をダウンロードしてください。

こちらの6項に RCC の記述があります。Reset & Clock Control の略です。
そしてGPIOAへのクロックの電源供給On/Offの設定はRCCのレジスタである RCC_AHB1ENR で行います。

ペリフェラルのレジスタには
ペリフェラルズ.ペリフェラル.そのレジスタ.function() のようにしてアクセスします。

function には read, write_with_zero, reset, write, modify があり、ここでは modifyを使っています。
レジスタの一部を変更したいので、write よりは modify を使う方が良さそうです。

上のレジスタの説明で、赤枠部分に rw という記述があります。
これは読み書きできるビットであることを示していて、その場合 modify を使うことができます。

modify()はクロージャを使うことができ、レジスタを読んだ結果を判断しながら書いたり、一部だけを書いたりすることができます。
今回は読む必要もないので、引数の指定を |_,w| としています。

_ はRustのワイルドカードで、読む必要がない場合には、このように書きます。
読んでから書きたい場合には |r,w| とします。

RCC の AHB1ENR レジスタの GPIOAEN にアクセスするためのメソッドは こちら で確認することができます。

そしてその GPIOAEN ビットを操作するためのメソッドは こちら で確認することができます。

以下のように RCC, AHB1ENR, GPIOAEN を 1 にして GPIOAのクロックを有効にすることで GPIOA を使うことができるようになります。
enabled()メソッドを使うことで実現しています。

dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());

(4)まずペリフェラルのGPIOAを取得します。
(5)リファレンスマニュアルの8項にGPIOの記述があります。
いくつかのレジスタがあるのですが(リセット時の状態を考慮すると)モードレジスタに書き込みを行うだけで良さそうです。
他にどんなレジスタがあるのか眺めておくと良いでしょう。

以下はモードレジスタの説明です。PA5のリセット時の状態は入力ですから、汎用出力に設定する必要があります。

GPIOの動作モードは、
入力、汎用出力、オルタネート機能モード、アナログモードの4種類があります。
ここでは汎用出力を使うので、output()メソッドを使っています。

GPIOA の MODE レジスタの MODER5 にアクセスするためのメソッドは こちら で確認することができます。

そしてその MODER5 ビットを操作するためのメソッドは こちら で確認することができます。

以下のコードで GPIOA, MODE, MODER5 に 01(1)を書いて、GPIOAを出力機能で使うことができるようになります。
ここでは、outout()メソッドを使って出力ピンに設定しました。
bits(1)メソッドにより直接値を書いても設定可能ですが、output()の方がわかりやすいですね。

gpioa.moder.modify(|_, w| w.moder5().output());

使う機能に応じたメソッドを使うことでレジスタに値を書いてくれます。

入力: input()
汎用出力: output()
オルタネート機能: alternat()
アナログ: analog()

オルタネートは UART とか SPI 等、アナログや入出力以外を使う場合に設定する機能です。

(6),(7)はGPIOポートに 1/0 を書く処理です。
出力データの書き込みには ODR を使います。

GPIOA の ODR レジスタの ODR5 にアクセスするためのメソッドは こちら で確認することができます。

そしてその ODR5 ビットを操作するためのメソッドは こちら で確認することができます。

以下のコードで GPIOA, ODR, ODR5 に 1/0 を書いて、LEDを点灯/消灯させることができます。
ここでは、high()及びlow()メソッドを使って出力ピンに設定しました。
set_bit(), clear_bit()でも設定可能ですが、hihi(), low()の方がわかりやすいですね。

gpioa.odr.modify(|_, w| w.odr5().high()); // 点灯
gpioa.odr.modify(|_, w| w.odr5().low()); // 消灯

それでは F5キーでプログラムを動作させた後、F10キーでステップ実行していきLD2(緑色)の点滅を確認してみてください。

デバッグモードは OpenOCD に設定する必要があるので、エラーが出る場合には確認してみてください。

Rustを学び始めてまだ日が浅いせいか、C言語で動かした時以上に感動が大きかったです。

いかがでしたか?
皆さんの環境では、うまく動作しましたか?

今回のプロジェクトを stm32f401-gpio におきましたので、よろしければ参考になさってください。

お疲れさまでした。

GPIOカテゴリの最新記事