STM32 Rust ベアメタルにPWMを使ってみる

  • 2022.02.12
  • PWM
STM32 Rust ベアメタルにPWMを使ってみる

今回はPWM出力を使ってみます。
ベアメタルと言われているレジスタにアクセスする方法を使います。

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

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

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

PWMとは

PWMとは、Pulse Width Modulation の略です。日本語ではパルス幅変調といいます。
I/Oポートからパルスを出力する機能です。
パルス幅を変えることでモーターを制御したり、LEDの明るさ調節を行ったりすることができます。

今回はボードに載っている緑色のLED(LD2)がたまたまPWM出力ポート(TIM2_CH1)につながっているので、それを使ってみます。
GPIOA5の先にLD2がつながっているので、この端子を使うことになります。

STM32のオルタネート機能とは

GPIO端子に汎用入出力以外の機能を割り当てる場合、動作モードをオルタネートに設定する必要があります。
データシートを見れば、端子にどの機能を割り当てることができるのか確認することができます。
(全ての機能を自由に端子に割り当てることはできません)

下表はSTM32F401REデータシート テーブル9 の一部抜粋です。

例えば今回使うGPIOA5(PA5)には PWM出力が可能な TIM2_CH1 の機能があります。

この機能を使うために GPIOA5のモードレジスタでオルタネートの設定を行い、もうひとつオルタネート機能下位レジスタの AFRL5 に AF1 を設定します。

AF1 はテーブル9の AF01 を指しています。

例えば同じ端子(PA5)にSPI1のクロック(SPI1_SCK)を割り当てたい場合には、オルタネート機能下位レジスタの AFRL5 に AF5 を設定すれば良いことになります。

オルタネート機能レジスタには上位と下位があり、GPIOx0-7 (xはA, B, C…) に対しては下位に、GPIOx8-15 に対しては上位の方に設定を行います。

どの端子にどのペリフェラルや機能を割り当てるのか、回路を設計する上でテーブル9は重要な存在になります。

プロジェクトをつくる

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

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

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

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

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

VSCodeを起動する

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

cd stm32f401-pwm
code .

C:\Users\xxxxx>cd stm32f401-pwm
C:\Users\xxxxx\stm32f401-pwm>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"

もうひとつ、今回も cortex-mクレートの Delay を使うので、そのバージョンを “0.7” に上げておきます。

[dependencies]
cortex-m = “0.7”

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

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-pwm\.vscode に保存します。

ソースコード

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 cortex_m::delay;    // (1)Delayを使う
use stm32f4::stm32f401;

#[entry]
fn main() -> ! {

    let dp = stm32f401::Peripherals::take().unwrap();   // (2)デバイス用Peripheralsの取得
    let cp = cortex_m::peripheral::Peripherals::take().unwrap();    // (3)cortex-m Peripheralsの取得
    let mut delay = delay::Delay::new(cp.SYST, 84000000_u32);   // (4)Delayの生成
    clock_init(&dp);    // (5)クロック関連の初期化
    tim2_init(&dp);     // (6)TIM2の初期化
    gpioa5_init(&dp);   // (7)GPIOAの初期化
    tim2_start(&dp);    // (8)PWM スタート
    loop {
        tim2_change_duty(&dp, 500_u32);     // (9)Duty を 50% に変更する
        delay.delay_ms(2000_u32);           // (10)delay 2000msec
        tim2_change_duty(&dp, 100_u32);     // (11)High(LED On)の期間を 10% に変更して暗くする
        delay.delay_ms(1000_u32);           // (12)delay 1000msec
    }
}

fn clock_init(dp: &stm32f401::Peripherals) {

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

    dp.RCC.cfgr.modify(|_, w| w.ppre1().div2());        // (15) APB1 PSC = 1/2
    dp.RCC.cr.modify(|_, w| w.pllon().on());            // (16)PLL On
    while dp.RCC.cr.read().pllrdy().is_not_ready() {    // (17)安定するまで待つ
        // PLLがロックするまで待つ (PLLRDY)
    }

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

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

fn gpioa5_init(dp: &stm32f401::Peripherals) {
    dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());    // (21)GPIOAのクロックを有効にする
    dp.GPIOA.moder.modify(|_, w| w.moder5().alternate());   // (22)GPIOA5をオルタネートに設定    
    dp.GPIOA.afrl.modify(|_, w| w.afrl5().af1());           // (23)GPIOA5をAF1に設定    
}

fn tim2_init(dp: &stm32f401::Peripherals) {
    dp.RCC.apb1enr.modify(|_,w| w.tim2en().enabled());          // (24)TIM2のクロックを有効にする
    dp.TIM2.psc.modify(|_, w| unsafe { w.bits(84 - 1) });       // (25)プリスケーラの設定

    // 周波数はここで決める
    dp.TIM2.arr.modify(|_, w| unsafe { w.bits(1000 - 1) });     // (26)ロードするカウント値
    dp.TIM2.ccmr1_output().modify(|_, w| w.oc1m().pwm_mode1()); // (27)出力比較1 PWMモード1

    // Duty比はここで決まる
    dp.TIM2.ccr1.modify(|_, w| unsafe { w.bits(500 - 1) });     // (28)キャプチャ比較モードレジスタ1
}

fn tim2_start(dp: &stm32f401::Peripherals) {
    dp.TIM2.cr1.modify(|_, w| w.cen().enabled());   // (29)カウンタ有効
    dp.TIM2.ccer.modify(|_, w| w.cc1e().set_bit()); // (30)キャプチャ比較1出力イネーブル
}

// duty : 1 ~ 1000
// 値が 0 ~ 999 の範囲で設定されるように制限しておく

fn tim2_change_duty(dp: &stm32f401::Peripherals, duty: u32) {
    let config;
    if duty == 0 {
        config = 1;
    }
    else if duty > 1000 {
        config = 1000;
    }
    else {
        config = duty;
    }
    dp.TIM2.ccr1.modify(|_, w| unsafe { w.bits(config - 1) });     // (31)キャプチャ比較モードレジスタ1
}

コード概要

gpioa5_init()で上で説明したオルタネート機能の設定を行っています。
(22)GPIOA5をオルタネートに設定
(23)GPIOA5をAF1に設定 (オルタネート機能下位レジスタへの設定)

tim2_init()でTIM2のCH1にPWM出力の設定を行っています。
(26)ARRレジスタに周波数の設定を行います。
ここでは 1kHz に設定しています。
(28)CCR1レジスタに書く値でデューティー比が決まります。
tim2_start()でPWM出力を開始します。

(9)LEDが点灯する GPIOA5=Hignの期間を50% に設定します。
(11)LEDが点灯する GPIOA5=Hignの期間を10% に設定します。

動作させてみる

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

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

期待した周期、デューティー比になっているか波形を確認してみました。良さそうです。

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

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

お疲れさまでした。

PWMカテゴリの最新記事