組み込みRust開発【stm32f3xx-halの使い方】

Hello_Rust_Worldソフト

”stm32f3xx-hal”クレートを使ってSTM32を制御する方法を説明します。このクレートを使えば、STM32F3シリーズを使った組み込みRustのアプリケーションが比較的簡単に作成することが出来ます。

STM32の多くの機能がモジュールになっているので、全てを一度に把握するのは困難かもしれません。使ってみたい機能から少しずつ試してみてください。

この記事を参考にして組み込みRust開発を初めていただければ嬉しいです。

”stm32f3xx-hal” はまだまだ活発に更新されているクレートです。この記事はVer0.7.0を対象としています。

見出しこんな方におすすめの記事
  • 組み込みRust開発に興味のある方
  • Rustの言語仕様が多少は理解できる方
  • Rustの開発環境が構築済の方

Rustの開発環境構築がまだの方は、下のリンクの記事を参考にしてみてください。Rust言語仕様の簡単なトピックスも記載しています。

環境・デバイス

やりたいこと:  stm32f3xx-halを使ってRustでSTM32の開発を行う

ボード:     Nucleo-F303K8

PC:       Windows10

stm32f3xx-halを導入して開発が行えるように設定したプロジェクトをGithubに上げております。こちらも参照してみてください。本記事のサンプルコードも付いています。

tteio/stm32f303k8: Embedded Rust stm32f303 quick start crates. (github.com)

rcc/flash (クロック設定)

何はともあれ、まずは動作クロックの設定を行わなければなりません。これには、rccモジュールとflashモジュールを使用します。

flashモジュールがなぜ必要?と思うかもしれません。これは、flashメモリへアクセスする時のウェイトを再設定する必要があるためです。動作クロックを早くしてウェイトをそのままにしていたら、アクセスミスが発生してしまうかもしれません。

それでは早速、サンプルコードになります。

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();

    let mut flash = dp.FLASH.constrain();
    let mut rcc = dp.RCC.constrain();

    //CLK setting
    let clk = rcc.cfgr
        //.use_hse(8.MHz())     //Use external oscillator
        //.bypass_hse()         //Use external clock signal
        //.enable_css()         //Enable CSS (Clock Security System)
        .hclk(64.MHz())
        .sysclk(64.MHz())
        .pclk1(32.MHz())
        .pclk2(64.MHz())
        .freeze(&mut flash.acr);    //flash access wait setting

7行目までのコードでは、ペリフェラルを取得して、その中からさらにflash,rccを取得します。この辺りの操作は、マイコン周辺機能を使用する場合のお決まり操作です。

その後、rcc.cfgr内の各関数を使用して各種設定をしています。

use_hse()外部クロックの使用と周波数を設定
bypass_hse()外部発振子ではなくクロック入力を有効化
enable_css()Clock Security Systemの有効化
hclk(64.MHz())AHB busの周波数設定
sysclk(64.MHz())コアの周波数設定
pclk1(32.MHz())APB1 busの周波数設定
pclk2(64.MHz())APB2 busの周波数設定
freeze(&mut flash.acr)設定の反映
各関数とその機能表

今回使っているボードでは、外部発振子が付いていないので、”use_hse”, “bypass_hse”, “enable_css”の3つはコメントアウトしています。その他の設定は、内部オシレータ使用時の最高周波数を設定しています。(デバイスは72MHzまで動作できますが、内部オシレータを使うと64MHzまでしか設定できないことに注意)

最後に、freeze関数でここまでの設定の反映を行います。実際にrccやflashのレジスタを操作するのはこの関数の中です。クロックの設定変更をした場合は、必ず最後にfreeze関数をコールしてください。

GPIO (端子機能設定)

組み込みのHelloWorld、Lチカを行う為にも必要なGPIOの設定を行います。これには、gpioモジュールとrccモジュールを使用します。

rccモジュールが必要なのは、使用する周辺機能へのリセットとクロック供給を有効にする為にrccレジスタへの操作が必要だからです。

このrccへの説明は、後の項目でも同じ内容の繰り返しになるので以降は省略します。

それでは、サンプルコードを見ていきます。入出力ポートの設定と簡単な制御をしています。サンプルコードのLEDはNucleoボード上に実装されていますが、スイッチはありません。試される場合は、ピンヘッダーにスイッチを接続してください。

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let mut rcc = dp.RCC.constrain();

    //GPIO setting
    let mut gpiob = dp.GPIOB.split(&mut rcc.ahb);
    let mut led = 
        gpiob
        .pb3
        .into_push_pull_output(&mut gpiob.moder, &mut gpiob.otyper);
    let button =
        gpiob
        .pb0
        .into_pull_up_input(&mut gpiob.moder, &mut gpiob.pupdr);

    loop {
        if button.is_low().unwrap() {
            led.set_high().unwrap();
        }
        else{
            led.set_low().unwrap();
        }
    }
}

まずは7行目のコードで、gpiob変数にsplit関数でペリフェラルからGPIOのBブロックを切り出します。この関数の引数でRCC.AHB関連のレジスタを渡してGPIOBへのクロック供給などを開始しています。

8行目からloop関数までのコードが使用するポートの取得と機能設定になります。ここでは、PB3をLED点灯用に出力端子に、PB0をスイッチ状態確認用にプルアップ付入力端子に設定しています。各末尾の”into_****()”関数で端子のモードを設定しています。設定に使える関数は下記のようになっています。

into_input()入力端子(プルアップ/プルダウンの設定は変更しない)
into_floating_input()入力端子(プルアップ/プルダウン無し)
into_pull_up_input()入力端子(プルアップ)
into_pull_down_input()入力端子(プルダウン)
into_push_pull_output()出力端子(プッシュプル)※通常のHigh,Lo出力
into_open_drain_output()出力端子(オープンドレイン)
into_analog()アナログ入力端子
set_speed()出力端子のスルーレート指定
into_af*_****()オルタネート機能を設定
端子のモード設定関数表

各関数によって、引数のレジスタが異なっているのに注意してください。詳細は”stm32f3xx-hal”のgpio.rsモジュールを見てみてください。

オルタネート機能の設定とは、GPIO以外のペリフェラル機能の設定になります。例えば、PA2ポートをUART送信機能にしたい場合は、”gpioa.pa2.into_af7_push_pull()”という関数をコールします。

端子によって設定できるAFのナンバー、AFに割り当てられている機能が異なります。マイコンのデータシートを参照しながら設定してください。

入力端子の状態は、下記の関数で確認できます。結果はResult<bool,error>型でかえってくるのでunwrap()して取得してください。

  • is_low()
  • is_high()

出力端子の状態は、下記の関数で確認できます。こちらも返り値がResult<bool,error>型なのでunwrap()をしておいてください。

  • set_low()
  • set_high()
  • toggle()

また、使用頻度は多くないかもしれませんが、出力状態の確認用関数もあります。これも結果はResultなので、※以下略。

  • is_set_high()
  • is_set_low()

Serial(UART)

外部機器との通信や、デバッグに便利なSerial(以降UART)機能の初期設定と使用方法を説明します。

初期設定

UARTの設定を全て終わらせるには少々手順が必要です。通信速度の指定や端子にUART機能を割り付ける為に、ここまでに説明したrcc, gpioモジュールも使用します。

rccの設定においては、特別なことは必要ありません。rcc/flash (クロック設定)のコードと設定をそのまま流用します。これで、”clk“変数を参照してUART通信速度を計算するための情報を得ることが出来ます。

次に、シリアル送受信用のポート設定をします。今回は、TX:PA2 RX:PA15 とします。Nucleo-F303K8 において、このポートはST-Linkに接続されていて、ST-Link接続用USBケーブル経由でPCと接続できます。UARTとして使用する為にはGPIOにてオルタネート機能に設定します。データシートでオルタネートの番号を確認しつつ、下記のようにコードを記述します。

    let mut gpioa = dp.GPIOA.split(&mut rcc.ahb);
    let uart_pins = (
        gpioa.pa2.into_af7_push_pull(
            &mut gpioa.moder, 
            &mut gpioa.otyper, 
            &mut gpioa.afrl
        ),  //tx
        gpioa.pa15.into_af7_push_pull(
            &mut gpioa.moder,
            &mut gpioa.otyper, 
            &mut gpioa.afrh
        ), //rx
    );

最後にUARTモジュールの設定を行います。使うUART機能、ピン、ボーレート、クロック、APBレジスタを引数で設定します。これで初期設定完了です。

    let mut debug = Serial::new(
        dp.USART2, 
        uart_pins, 
        9600.Bd(), 
        clk, 
        &mut rcc.apb1
    );

使用方法

UARTを使用するには、”embedded-hal ver0.2.5”のSerialモジュールに用意されている、”Write”, “Read”クレートで読み書きを行う事となります。また、次のDMAの項ではDMAを使ったシリアル送受信も紹介していますので、そちらも参照してみてください。

Write

UARTからデータ出力するには、”write()“関数で1バイトのデータ送信を行います。ただし、この関数は以前にコールされたwrite関数による送信中の場合にはエラーを返すので少々使いにくいです。

そこで、前回のデータ送信の終了を待ってから次の送信を始めるブロッキングな送信をする方法があります。それぞれの使い方は下記のようになっています。

use stm32f3xx_hal::nb;

//ブロッキング無し1バイトwrite
debug.write('A' as u8).unwrap();

//ブロッキング有り1バイトwrite
nb::block!(debug.write('A' as u8)).unwrap();

//ブロッキング有り文字列write
debug.bwrite_all(b"Sample.\r\n").unwrap();

Read

UART受信したデータを読み出すには”read()”関数を使います。この関数は、データ未受信の場合は”Err(nb::Error::WouldBlock)”を戻り値として返します。また、受信エラーが発生している場合は対応したエラーを返します。

下のサンプルコードは、read()関数の全ての戻り値に対応した処理を記述したコードになります。正しくデータ受信があった時にはデータのオウム返しを行い、未受信の場合は何もしない、その他エラーでは対応したエラーの報知をしています。

match debug.read() {
    Ok(s) => nb::block!(debug.write(s)).unwrap(),
    Err(error) => {
        match error {
            nb::Error::Other(e) => {
                match e {
                    hal::serial::Error::Framing => 
                        debug.bwrite_all(b"Framing error.\r\n").unwrap(),
                    hal::serial::Error::Noise => 
                        debug.bwrite_all(b"Noise error.\r\n").unwrap(),
                    hal::serial::Error::Overrun => 
                        debug.bwrite_all(b"Overrun error.\r\n").unwrap(),
                    hal::serial::Error::Parity => 
                        debug.bwrite_all(b"Parity error.\r\n").unwrap(),
                    _ => {},
                };
            },
            nb::Error::WouldBlock => {},
        };
    },
};

DMA(Serial)

“stm32f3xx-hal”クレートにはDMAモジュールが用意されています。しかし、DMA単独で使用することは少ないかもしれません。UART、SPI、I2Cなどの通信機能と一緒に使う事が多いのではないでしょうか。

“stm32f3xx-hal” では、UARTモジュールにDMAを使った機能実装がされています。ここでは、UARTモジュール内のDMAを使った機能を紹介します。

他のDMAと親和性の高いモジュールにDMAを実装する予定があるようですが、現状はまだ対応されていません。

初期設定

Serialを初期設定するまでの手順については、Serial(UART)の項と同じです。rcc,gpio,serialモジュールの設定をSerialの項を参照して設定してください。

Serialの初期化が終わったら、下記のようにserial(サンプルコードではdebugと命名しています)をtxとrxに分割します。また、dma1からSerialモジュールに対応するDMAのチャンネルを切り出してきます。これで、DMA転送を行うリソースの準備が整います。

    let (tx, rx) = debug.split();

    let dma1 = dp.DMA1.split(&mut rcc.ahb);
    let rx_dma = dma1.ch6;
    let tx_dma = dma1.ch7;

使用方法

SerialモジュールのDMA転送機能付きの送受信関数を使用する場合には、データを保存しておくstaticなバッファが必要になってきます。これは、cortex-mクレート内にあるsingletonマクロを使って生成します。

データの受信には”read_exact()”関数、送信には” write_all ()”関数を使用します。これらの関数にバッファとDMAチャンネルを引数で渡すことによって、DMAが起動します。引数は参照ではないので引数の所有権は関数内に移動します。いったんDMAが起動すると再度同じDMAを起動することはできません。所有権はDMA終了時に返ってきます。

DMA起動後の処理は戻り値のTransfer構造体で行います。その中でも”wait()”関数は、特に使用頻度が高そうです。DMAが終了するまでこの関数内で待ってくれます。終了時は、引数で渡したバッファやDMAの所有権が戻り値でかえってきます。繰り返しDMAを行う場合は所有権を受け取っておきましょう。

下のサンプルコードでは、DMA受信データを送信しなおす処理をDMAを使って実現しています。

use cortex_m::singleton;

//dma buf
let rx_buf = singleton!(: [u8; 1] = [0; 1]).unwrap();
let tx_buf = singleton!(: [u8; 1] = [0; 1]).unwrap();

//wait receive
let debug_receive = rx.read_exact( rx_buf, rx_dma);
let (rx_buf,rx_dma, rx) = debug_receive.wait();

//send receive data
tx_buf[0] = rx_buf[0]; 
let debug_send = tx.write_all(tx_buf, tx_dma);
let (tx_buf,tx_dma, tx) = debug_send.wait();

I2C

初期設定

I2Cの初期設定は、UARTに似ています。rcc, gpio, i2c モジュールを使用します。

rccの設定は、 rcc/flash (クロック設定)のコードと設定をそのまま流用します。

次にI2Cポートの設定です。今回はSCL:PB6 SDA:PB7となり、これらをオルタネート機能に設定します。データシートでオルタネートの番号を確認しつつ、下記のようにコードを記述します。

let mut gpiob = dp.GPIOB.split(&mut rcc.ahb);
let i2c_pins = (
    gpiob.pb6.into_af4_open_drain(
        &mut gpiob.moder,
        &mut gpiob.otyper,
        &mut gpiob.afrl
    ),
    gpiob.pb7.into_af4_open_drain(
        &mut gpiob.moder,
        &mut gpiob.otyper,
        &mut gpiob.afrl
    )
);

最後にI2Cのインスタンス化をします。 使うI2C機能、ピン、周波数、クロック、APBレジスタを引数で設定します。これで初期設定完了です。

let mut i2c = i2c::I2c::new(
    dp.I2C1,
    i2c_pins,
    100_000.Hz(),
    clk,
    &mut rcc.apb1,
);

使用方法

I2Cデバイスへアクセスする関数は、”read()”、”write()”、”write_read()”の3種類があります。

read()”関数は、デバイスアドレスと読み出したいデータを保存する配列の参照を渡します。配列の長さが、読み出すバイト数になります。

write()”関数は、デバイスアドレスと書き込みたいデータを保存した配列の参照を渡します。こちらも配列の長さが書き込むバイト数になります。

write_read()”関数は、writeしてからreadを行う複合関数です。 I2Cデバイスには、データを読み出す時に、レジスタアドレスを書き込んでからリード動作を行う事になっているものがよくあります。このようなデバイスのレジスタを読み出す時に便利な関数になっています。

下に、サンプルコードを記載しています。”write_read()”関数のサンプルでは、エラー発生時の処理も記載してみました。このサンプルコードで使ったI2Cデバイスは、Microchip製のDAC MCP4726 になります。

let i2c_buf: [u8; 2] = [0x02, 0x00];
let mut i2c_read_buf: [u8; 6] = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05];

//write
i2c.write(0x60, &i2c_buf).unwrap();

//read
i2c.read(0x60, &mut i2c_read_buf).unwrap();

//write_read with err check
match i2c.write_read(0x60, &i2c_buf, &mut i2c_read_buf){
    Ok(()) => {},
    Err(e) =>{
        match e {
            i2c::Error::Arbitration =>
                debug.bwrite_all(b"Arbitration error.\r\n").unwrap(),
            i2c::Error::Bus =>
                debug.bwrite_all(b"Bus error.\r\n").unwrap(),
            i2c::Error::Busy =>
                debug.bwrite_all(b"Busy error.\r\n").unwrap(),
            i2c::Error::Nack =>
                debug.bwrite_all(b"Nack error.\r\n").unwrap(),
            _ => {}
        }
    }
}

SPI

初期設定

SPIの初期設定は、UARTやI2Cに似ています。rcc, gpio, spi モジュールを使用します。

rccの設定は、 rcc/flash (クロック設定)のコードと設定をそのまま流用します。

次に、SPIに使うポートの設定を行います。 今回はSCK:PA5 MISO:PA6 MOSI:PA7となり、これらをオルタネート機能に設定します。さらに、多くのデバイスでは、SS(CS)端子の制御も必要になります。 今回はSS:PA4を出力端子に設定して、High出力にしておきます。

下記のようなコードになります。

let mut gpioa = dp.GPIOA.split(&mut rcc.ahb);
let mut ss = gpioa
    .pa4
    .into_push_pull_output(&mut gpioa.moder, &mut gpioa.otyper);
ss.set_high().unwrap();//ss is low active

let sck = gpioa
    .pa5
    .into_af5_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl);
let miso = gpioa
    .pa6
    .into_af5_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl);
let mosi = gpioa
    .pa7
    .into_af5_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl);

次に、spiモジュールの設定を行います。まずは、デバイスの通信モードを0~3から決定します。各モードの設定内容は、”embedded-hal::spi“に記載がありますので、参照して設定してみて下さい。次に、通信周波数についてもデバイス仕様を確認して決めておいてください。

これらが決まったら、spiの設定を行います。サンプルコードは下記になります。

let spi_mode = Mode {
    polarity: Polarity::IdleLow,
    phase: Phase::CaptureOnFirstTransition,
};

let mut spi = Spi::spi1(
    dp.SPI1,
    (sck, miso, mosi),
    spi_mode,    //MODE0
    3u32.MHz().try_into().unwrap(),//3MHz
    clocks,
    &mut rcc.apb2,
);

使用方法

spiモジュールは、 “embedded-hal::spi“の”read”、”send”クレートを実装しており、これが通信の基本関数になります。ただし、この2つのクレートは、1バイトのノンブロッキング送受信(通信の終了を待たない、通信中にコールされるとエラーを返す)なので少々使いづらいです。

ブロッキング付き複数バイト通信機能の”write”、”transfer”関数を使うのが便利です。

write”関数は、引数で送信データ配列の参照を受け取って送信、MISOデータを廃棄する送信専用関数です。”transfer”関数は、 引数で送信データ配列の参照を受け取って送信、 MISOデータ配列を引数の参照先に上書きし、返り値としても返す関数になります。

この2つの関数を使ってSPI通信を行うサンプルコードを下記に示します。使っているデバイスはMCP23S17というMicrochip社のIO拡張チップです。このデバイスの初期化を行い、拡張ポート(A0)のボタン状態を確認して拡張ポート(A1)のLEDを点滅させています。

通信関数をコールする前後で、SSポート(Lowアクティブ)を制御しています。この動作はspiモジュールでは行ってくれません。GPIOポートで制御してください。

let init_data: [u8;4] = [0x40, 0x00, 0x01, 0x00];//ADD000, A0=input, A1=output
ss.set_low().unwrap();
spi.write(&init_data).unwrap();
ss.set_high().unwrap();

let mut write_data = [0x40, 0x14, 0x00, 0x00];
loop {
    asm::delay(2_000_000);
    let mut read_data = [0x41, 0x12, 0x00, 0x00];
    ss.set_low().unwrap();
    spi.transfer(&mut read_data).unwrap();
    ss.set_high().unwrap();

    if read_data[2] & 0x01 == 0x01 {
        write_data[2] = 0x02;
    }
    else{
        write_data[2] = 0x00;
    }
    ss.set_low().unwrap();
    spi.write(&write_data).unwrap();
    ss.set_high().unwrap();
}

ADC

初期設定

初期設定を行うのに、rcc/adc/gpioのモジュールを使用します。

rccの設定は、 rcc/flash (クロック設定)のコードと設定をそのまま流用します。

次に、ペリフェラルからADCを設定して切り出します。ここでは、ADC1を有効にしました。STM32においてはADC1とADC2が密接しており、一部の機能を共有しています。(ADC3とADC4も同様です。)ですので、ADC1を初期化するには、ADC1とADC1_2の2つが必要になります。

最後に、ADCとして使用するGPIOポートをアナログ入力ポートに設定します。

初期設定のサンプルコードを下記に記載します。

let mut adc1_2 = dp.ADC1_2;
let mut adc1 = adc::Adc::adc1(
    dp.ADC1, 
    &mut adc1_2, 
    &mut rcc.ahb,
    adc::CkMode::default(),
    clk
);
let mut vr = gpioa.pa0.into_analog(&mut gpioa.moder, &mut gpioa.pupdr);

使用方法

ADCの値を取得するには、”read”関数を使用します。

let vr_data: u16 = adc1.read(&mut vr).unwrap();

DELAY

delayモジュールは、指定した時間の経過を待つ機能を提供しています。この機能は、Cortex-Mシリーズがコアの機能として持っている”SysTick”を使って実装されています。

dekayの提供する機能は、時間が経過するまでは他の処理を実行しない非常にシンプルなmのです。より高機能な時間管理の機能が欲しい場合は、Timerを使うことになります。

初期設定

delayモジュールは、rccとsystick機能を使います。

rccの設定は、rcc/flash (クロック設定)のコードと設定をそのまま流用します。これで、”clk“変数を参照して遅延時間を計算するための情報を得ることが出来ます。

systickを使うには、”cortex-m”クレートからコアのペリフェラルを持ってきます。”stm32f3xx_hal”のペリフェラルではありませんので、注意してください。

これら2つを用いて、delayのインスタンス化を行うコードは下記のようになります。

use cortex_m::peripheral;
use stm32f3xx_hal::delay;

fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let mut flash = dp.FLASH.constrain();
    let mut rcc = dp.RCC.constrain();

    //CLK setting
    let clk = rcc.cfgr
        //.use_hse(8.MHz())     //Use external oscillator
        //.bypass_hse()         //Use external clock signal
        //.enable_css()         //Enable CSS (Clock Security System)
        .hclk(64.MHz())
        .sysclk(64.MHz())
        .pclk1(32.MHz())
        .pclk2(64.MHz())
        .freeze(&mut flash.acr);    //flash access wait setting

    //delay setting
    let core = peripheral::Peripherals::take().unwrap();
    let mut blocking = delay::Delay::new(core.SYST, clk);

使用方法

delay関数には、msとusの2種類があります。また、それぞれの関数において引数がu8~u32までのバージョンがあります。これらのコールして指定時間のdelayを行います。

    loop {
        led.toggle().unwrap();
        blocking.delay_ms(500u32);
    }

timer/interrupt

ここで紹介するtimerは、embedded_hal::timer モジュールの実装になります。カウントダウンタイマーを使って、定期的に割り込みやDMA起動をおこないます。STM32のタイマーペリフェラルの全機能を網羅したモジュールという事ではありません。

timerだけでは役に立たないので、ここでは割り込み機能についても簡単に説明します。

初期設定

timerモジュールは、rcc機能を使います。 rcc/flash (クロック設定)のコードと設定をそのまま流用します。このrccモジュール使って、timerを初期化します。

まずは、使うタイマー機能(TIM6)を指定し、割り込み周期(10回/s)を設定します。ここで、タイマーの動作もスタートします。

その後、”listen“関数を使って、割り込みを有効にします。

let mut period_timer =
    timer::Timer::tim6(dp.TIM6, 10.Hz(), clk, &mut rcc.apb1);
period_timer.listen(timer::Event::Update);

使用方法

初期設定にて、timerのカウントダウン割り込みが発生するようになりました。ここでは、この割り込み内の処理でLEDを点滅させてみます。

割り込み関数では、LEDポートの反転と、割り込みフラグのクリアを行います。メイン関数内で処理化をして、割り込み関数で使用する関係で、LEDやTIM6の変数はグローバル変数を使用しなければなりません。また、排他処理も必要なので、Mutexも使用します。下記のようにグローバル変数を定義します。

use core::cell::RefCell;
use cortex_m::asm;
use cortex_m::interrupt::Mutex;
use cortex_m::peripheral::NVIC;
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f3xx_hal::{gpio, 
                    gpio::PushPull,
                    interrupt,
                    pac::{self, TIM6},
                    prelude::*,
                    timer
                };

type LedPin = gpio::gpiob::PB3<gpio::Output<PushPull>>;
static LED: Mutex<RefCell<Option<LedPin>>> = 
    Mutex::new(RefCell::new(None));

type TimerT = timer::Timer<TIM6>;
static P_TIMER: Mutex<RefCell<Option<TimerT>>> = 
    Mutex::new(RefCell::new(None));

グローバル変数を用意したら、初期化済のled, period_timer変数をグローバル変数に渡します。(各初期化コードは省略)その後、TIM6の割り込みを有効にします。ここの処理はunsafeになることを注意してください。

#[entry]
fn main() -> ! {
    /*RCC & LED & TIM6 config code*/
   
    // Move the ownership of the led and period_timer to global
    cortex_m::interrupt::free(|cs| *LED.borrow(cs).borrow_mut() = Some(led));
    cortex_m::interrupt::free(|cs| *P_TIMER.borrow(cs).borrow_mut() = Some(period_timer));

    unsafe { NVIC::unmask(interrupt::TIM6_DACUNDER) };

    loop {
        asm::delay(2_000_000);
    }
}

次に割り込み関数内の処理を記述します。関数名は、stm32f3xx_halにて決められています。Mutexからグローバル変数を借用して必要な処理を行います。タイマー割り込みフラグのクリアをお忘れなく。忘れてしまうと、割り込み関数を抜けた直後に再度割り込みが発生してしまいます。

#[interrupt]
fn TIM6_DACUNDER(){
    cortex_m::interrupt::free(|cs| {
        // Clear interrupt flag
        P_TIMER.borrow(cs)
        .borrow_mut()
        .as_mut()
        .unwrap()
        .clear_update_interrupt_flag();

        // Toggle the LED
        LED.borrow(cs)
            .borrow_mut()
            .as_mut()
            .unwrap()
            .toggle()
            .unwrap();
    })
}

PWM

STM32のタイマー機能を使ってPWM波形を出力する機能になります。タイマー機能は周期割り込みの発生などにも使用できますが、こちらはtimerモジュールとして用意されています。

pwmモジュールは、rccとgpio機能を使います。 rcc/flash (クロック設定)のコードと設定をそのまま流用します。このrccモジュール使ってpwmを初期化します。 その後、生成したpwmを出力する為にオルタネートのプッシュプル出力に設定したgpioポートをpwmモジュールに割り当てます。

ここまで設定出来たら、デューティを設定して、出力を有効にすればpwm出力が始まります。サンプルコードを元に詳しく説明します。

//GPIO setting
let mut gpiob = dp.GPIOB.split(&mut rcc.ahb);
let pwm_out = 
    gpiob
    .pb3
    .into_af1_push_pull(&mut gpiob.moder, &mut gpiob.otyper, &mut gpiob.afrl);

//Timer setting
let tim2_channels = pwm::tim2(
    dp.TIM2, 
    16_000, //arr reg value
    10.Hz(), 
    &clk
);
let mut tim2_ch2 = tim2_channels.1.output_to_pb3(pwm_out);

//set duty 20%
tim2_ch2.set_duty(tim2_ch2.get_max_duty() / 5);

//enable pwm
tim2_ch2.enable();

サンプルコードでは、pwm出力用のポートはPB3(NucleoボードでLEDが付いています)にしています。このポートをオルタネート1のプッシュプル出力に設定しました。

PB3ポートでpwm出力できるのはTIM2のチャンネル2になります。こちらを設定して変数に切り出します。”tim2()”関数でarrレジスタ値と周期の設定がありますが、このarrレジスタ設定値には注意してください。設定値によっては、指定した周期が生成できない可能性があります。この辺り、今一度STM32のタイマー機能の項やレジスタ機能をよくご確認ください。

set_duty()”関数ではHigh出力Dutyを設定します。引数内の”get_max_duty()”関数は、arrレジスタの値を取得しています。これを5で割った値を引数として渡すことで20%のDuty波形になります。

最後に”enable()“関数でpwm出力を開始します。

WATCHDOG

マイコンの暴走などから復帰するための機能です。このモジュールは、STM32に内蔵されている独立型ウォッチドッグを使用しています。

設定と使い方は比較的簡単です。ウォッチドッグのインスタンス化を行い、デバッグ中に機能停止するかどうかを指定し、タイムアウト時間を指定しながらスタートをします。ウォッチドッグのタイマクリアを行うには、”feed()“関数を実行します。feed->餌を与えるって、おしゃれな表現ですね。

サンプルコードは下記になります。デバッグ用のシリアルから任意の入力があったらfeedを実行しています。

//instance watchdog 
let mut iwdg = watchdog::IndependentWatchDog::new(dp.IWDG);

//stop on debug
iwdg.stop_on_debug(&dp.DBGMCU, true);

//Start wdt 3s periods
iwdg.start(3000.milliseconds());

loop {
    match debug.read() {
        Ok(_) => iwdg.feed(),//clear wdt
        Err(_) => {},
    };
}

コメント