STM32 Rust ベアメタルにタイマー割り込みを使う

STM32 Rust ベアメタルにタイマー割り込みを使う

今回はタイマー割り込みを使って1秒毎にLチカしてみます。
前回と同じベアメタルと言われているレジスタにアクセスする方法を使います。

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

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

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

クロックについて

タイマーを使うにはSTM32のクロック系統について理解しておく必要があります。
以前私が書いたC言語環境の記事になりますが こちら をご覧いただくとクロックの概要がつかめると思います。
よろしかったら参考になさってください。

プロジェクトをつくる

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

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

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-timer

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

xxxxx は皆さまのユーザー名です。

VSCodeを起動する

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

cd stm32f401-timer
code .

C:\Users\xxxxx>cd stm32f401-timer
C:\Users\xxxxx\stm32f401-timer>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-timer\.vscode に保存します。

ソースコード

main()関数の外側は、組み込み特有の記述になります。
このように書くものだと考えておけば良いと思います。
ただし今回は割り込みを使うために次の1行を追記しました。

use stm32f4::stm32f401::interrupt;
#![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;
use stm32f4::stm32f401::interrupt;  // (1)割り込みを使う

#[entry]
fn main() -> ! {
    unsafe {
        stm32f401::NVIC::unmask(stm32f401::interrupt::TIM2);    // (2)TIM2 NVIC割り込み許可
    }
    let dp = stm32f401::Peripherals::take().unwrap();   // (3)Peripheralsの取得

    // PLLSRC = HSI (default)
    dp.RCC.pllcfgr.modify(|_, w| w.pllp().div4());  // (4)P=4
    dp.RCC.pllcfgr.modify(|_, w| unsafe { w.plln().bits(336) });    // (5)N=336
    // PLLM = 16 (default)

    dp.RCC.cfgr.modify(|_, w| w.ppre1().div2());    // (6) APB1 PSC = 1/2

    dp.RCC.cr.modify(|_, w| w.pllon().on());    // (7)PLL On
    while dp.RCC.cr.read().pllrdy().is_not_ready() {    // (8)安定するまで待つ
        // PLLがロックするまで待つ (PLLRDY)
    }

    // データシートのテーブル15より
    dp.FLASH.acr.modify(|_,w| w.latency().bits(2));    // (9)レイテンシの設定: 2ウェイト (3.3V, 84MHz)

    dp.RCC.cfgr.modify(|_,w| w.sw().pll()); // (10)sysclk = PLL
    while !dp.RCC.cfgr.read().sws().is_pll() {  // (11)SWS システムクロックソースがPLLになるまで待つ
    }

    dp.RCC.apb1enr.modify(|_,w| w.tim2en().enabled());  // (12)TIM2のクロックを有効にする

    let tim2 = &dp.TIM2;    // (13)TIM2の取得
    tim2.psc.modify(|_, w| unsafe { w.bits(84 - 1) });   // (14)プリスケーラの設定
    tim2.arr.modify(|_, w| unsafe { w.bits(1000000 - 1) }); // (15)ロードするカウント値
    tim2.dier.modify(|_, w| w.uie().enabled()); // (16)更新割り込み有効
    tim2.cr1.modify(|_, w| w.cen().enabled());  // (17)カウンタ有効

    dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());    // (18)GPIOAのクロックを有効にする
    let gpioa = &dp.GPIOA;                                  // (19)GPIOAの取得
    gpioa.moder.modify(|_, w| w.moder5().output());         // (20)GPIOA5を出力に設定    
    loop {
        // your code goes here
    }
}

#[interrupt]    // (21)割り込みの指定
fn TIM2() {     // (22)TIM2割り込みハンドラ
    unsafe {
        let dp = stm32f401::Peripherals::steal();   // (23)Peripheralsの取得
        dp.TIM2.sr.modify(|_, w| w.uif().clear());  // (24)更新フラグのクリア
        dp.GPIOA.odr.modify(|r, w| w.odr5().bit(r.odr5().is_low()));    // (25)GPIOA5の出力を反転する
    }
}

概要説明

それぞれのコードが何をやっているのか、コメントをつけたので参考になさってください。
多くのレジスタの設定を行っているので、さすがに図を貼るのはやめました(笑)
必要に応じてリファレンスマニュアル RM0368 を参照してください。

リファレンスマニュアルは こちら からダウンロードできます。

クロック

クロックツリーはリファレンスマニュアル RM0368 の図12をご覧になってください。

今回はSYSCLKを 84MHz に設定してみます。
高速で動かすためにはPLLが必須になります。
クロック源に HSI を選択し、これをPLLに入力し、SYSCLK をつくります。
HSIは16MHz固定です。
クロックツリーより SYSCLK は以下の式で求めることができます。

SYSCLK = 16M * 1/M * N * 1/P [Hz]

M=16, N=336, P=4 とすれば、SYSCLK=84MHz となります。

消費電力を無視すれば、このクロックが最強最速です。(HSI使用時)

(6)今回は使いませんが、APB1(低速ペリフェラル用クロック)は42MHzを超えないようにする必要があるために、プリスケーラーで1/2して84から42MHzに落としています。(使っていないので検証はしていません)

割り込みタイマー

32ビットのカウンタを持つTIM2を使ってみることにしました。
84MHzのクロックをTIM2のプリスケーラーで1MHzに落とし、1000000カウントすれば1秒タイマーをつくることができます。
そうすると計算が簡単ですね。
(16ビットだとカウンタが不足してしまいます)

(14)TIM2のプリスケーラー 84 の設定値は -1 する必要があります。
(15)TIM2のカウンタ 1000000 の設定値は -1 する必要があります。
これらは最初のカウントで値がゼロになるために -1 した値を設定する必要があります。

(23)Peripheralsのtake()はできないので、steal()を使っています。
実際のシステムでは排他制御を考慮したコードを書く必要があると思います。

TIM2 という割り込みハンドラ名は stm32f4::stm32f401::Interrupt から引っ張ってきました。
他の割り込み要因も、こちらを参考にすれば良いと思います。

各割り込みは、NVICで割り込みを許可するだけではダメで、それぞれのペリフェラルの割り込み要因も有効にする必要があります。
もちろん、それに付随するペリフェラルの設定も行わなければなりません。

(25)GPIOA5の出力を反転しています。
modify を |r, w| で使うサンプルになります。

フラッシュメモリー

(9)周波数を上げたので、フラッシュメモリーのアクセスにウェイトを入れる必要があります。
電源電圧3.3V、 84MHzで動作させる場合、ウェイトを2つ入れる必要があります。
このコードがないと動きません。
これはデータシートのテーブル15で確認することができます。

動作させてみる

それでは F5キーでプログラムを起動した後、entry で停止したらもう一度F5キーを押してプログラムを動作させてみます。
1秒周期でLD2(緑)が点滅すれば成功です。

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

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

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

お疲れさまでした。

Timerカテゴリの最新記事