STM32 Rust halを使ってUARTを抽象化してみる

STM32 Rust halを使ってUARTを抽象化してみる

今回はhalを使ってUARTを抽象化してみます。

前回ベアメタル系の記事を書いたので、その halバージョンです。
概要については 前回の記事 をご覧になってください。

halにSerial構造体があります。
前回パリティエラーなどのエラー検出処理を書きましたが、2022年3月時点でSerial構造体には基本的なメソッドしかありませんでした。

こちらでつくるプロジェクトから見ると Serial構造体は外部クレートの一部です。
そのクレートに対してトレイトを使ってエラー処理を追加してみます。

またエラー処理のコード量はわずかですが、その部分を別ファイルに置いて部品化してみます。

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

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-uart2 と入力します。

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

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

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

VSCodeを起動する

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

cd stm32f401-uart2
code .

C:\Users\xxxxx>cd stm32f401-uart2
C:\Users\xxxxx\stm32f401-uart2>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 があります。

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

今回は hal を使うので stm32f4xx_halクレートを指定します。
新しいバージョンではエラーが出ることがあるので今回は “0.9” を選択しました。

[dependencies.stm32f4xx-hal]
features = ["stm32f401", "rt"]
version = "0.9"

編集が終わったら 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-uart2\.vscode に保存します。

ライブラリクレートを追加する

ファイルを分割するのに必要な作業になります。
ファイル分割の方法は、いろいろあると思うのですが今回はライブラリクレートを使ってみます。

まず、カレントディレクトリを stm32f401-uart2 にします。
例.

C:\Users\xxxxx>cd stm32f401-uart2
C:\Users\xxxxx\stm32f401-uart2>

cargo new –lib stm32lib と入力してライブラリクレートをつくります。

C:\Users\xxxxx\stm32f401-uart2>cargo new --lib stm32lib
    Created libral `stm32lib` package

stm32libディレクトリがつくられ、その下に src/lib.rs というファイルがつくられます。

lib.rs を以下のように編集して保存します。

#![no_std]
pub mod uart;

このファイルは、単に橋渡しをするためだけのものです。

Cargo.toml

ライブラリクレートをつくったので、Cargo.toml は2つ存在します。

まず上の階層の Cargo.toml にstm32libのパスを指定し、その存在を教えます。

[dependencies]
stm32lib = { path = "stm32lib" } 

次に stm32libにある Cargo.toml には以下のように hal を追加します。
uartを使うために必要です。

[dependencies.stm32f4xx-hal]
features = ["stm32f401", "rt"]
version = "0.9"

モジュール uart.rs

次に lib.rsと同じ階層(ディレクトリ)に uart.rs というファイルをつくり以下のように編集して保存します。
lib.rsに記述した “pub mod uart;” はこのファイルを指しています。

Rustにはクラスがないので、トレイトを使って継承を実現します。
トレイトで追加したメソッドをSerial構造体のメソッドであるかのように使うことができます。
トレイトを使ってエラー処理を記述します。
traitの前に pub キーワードをつけて、外からも使えるようにしておきます。

ここでは定義だけを記述して、部品として使えるようにしておけばスッキリしますね。

use stm32f4xx_hal::serial::Serial;
use stm32f4xx_hal::serial::Instance;
use stm32f4xx_hal::serial::Pins;

pub trait ErrorDetect {
    fn is_pe(&self) -> bool;    // パリティエラー
    fn is_fe(&self) -> bool;    // フレーミングエラー
    fn is_ore(&self) -> bool;   // オーバーランエラー
}

impl ErrorDetect for Serial
where
    PINS: Pins,
    USART: Instance,
{
    fn is_pe(&self) -> bool {
        unsafe { (*USART::ptr()).sr.read().pe().bit_is_set() }
    }
    fn is_fe(&self) -> bool {
        unsafe { (*USART::ptr()).sr.read().fe().bit_is_set() }
    }
    fn is_ore(&self) -> bool {
        unsafe { (*USART::ptr()).sr.read().ore().bit_is_set() }
    }
}

ソースコード main.rs

#![no_std]
#![no_main]

use embedded_hal::prelude::_embedded_hal_serial_Write;
use panic_halt as _; // you can put a breakpoint on `rust_begin_unwind` to catch panics
use cortex_m_rt::entry;
use stm32f4xx_hal::serial::{Serial, config};
use stm32f4xx_hal::gpio::{GpioExt};
use stm32f4xx_hal::rcc::RccExt;
use stm32f4xx_hal::time::Bps;

use embedded_hal::serial::Read;
use core::fmt::Write;   // (1)write!()マクロを使えるようにする
use stm32lib::uart::ErrorDetect;    // (2)追加するトレイトを使えるようにする

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

    let dp = stm32f4xx_hal::pac::Peripherals::take().unwrap();
    let gpioa = dp.GPIOA.split();   // GPIOAのclockも有効にしてくれる (AHBENRレジスタ)
    let pa2 = gpioa.pa2.into_alternate_af7();   // afrl, modeレジスタを設定してくれる
    let pa3 = gpioa.pa3.into_alternate_af7();   // afrl, modeレジスタを設定してくれる

    let bps = Bps(115_200_u32); // (3)通信速度

    let seri_config = config::Config {  // (4)通信パラメーターの設定
        baudrate: bps,
        wordlength: config::WordLength::DataBits8,  // 実際には7ビット
        parity: config::Parity::ParityEven,
        stopbits: config::StopBits::STOP1,
        dma: config::DmaConfig::None,
    };

    let rcc = dp.RCC.constrain();   // (5)RCCの取得
    let clks = rcc.cfgr.freeze();   // (6)各clockの設定

    let mut serial = Serial::usart2(
        dp.USART2,
        (pa2, pa3),
        seri_config,
        clks,
    ).unwrap(); // (5)Serial構造体の生成

    loop {
        while !serial.is_rxne() {}
        if serial.is_pe() {
            let _ = serial.read();  // 読み捨てる
            write!(serial, "\r\nParity error {}", "detected.\r\n").unwrap();
        }
        else if serial.is_fe() {
            let _ = serial.read();  // 読み捨てる
            write!(serial, "\r\nFraming error {}", "detected.\r\n").unwrap();
        }
        else if serial.is_ore() {
            let _ = serial.read();  // 読み捨てる
            write!(serial, "\r\nOver run error {}", "detected.\r\n").unwrap();
        }
        else if let Ok(c) = serial.read() {
            while !serial.is_txe() {}
            serial.write(c).unwrap();
        }
    }
}

コード概要

各所にコメントを入れたので参考にしてください。
(1)の宣言で write!マクロが使えるようになります。
引数内に”{}”のプレースホルダを使うことができます。
(5),(6)でclock系の初期化が行われています。
ステップ実行でメソッドの中に入っていくことができますので、お勉強できそうですね。

clockは HSI , 16MHz に設定されているようです。

ベアメタルに書いた場合に比べると、見さすさでは hal に軍配が上がるのではないでしょうか?
通信速度の設定など、見通しが良いですね。

PC側の設定

PCのソフトには Tera Term を使いますのでダウンロードしてインストールしておきます。

新しい接続で、シリアルを選択し、Virtual COM Port のあるポートを選択します。

次に、設定-シリアルポートから通信パラメーターを以下のとおりに設定します。

続いて、設定-端末でローカルエコーにチェックを入れます。
これで送信した文字が表示されます。

USBケーブルでPCとボートを接続しておきます。

動作させてみる

それでは F5キーでプログラムを起動した後、entry で停止したらもう一度F5キーを押してプログラムを動作させてみます。

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

PCのTera Termで何かキーを押し、ボートから戻ってくれば成功です。
正常に戻ってくれば、2文字ずつTera Termに表示されます。

パリティエラーを起こしてみる

通信パラメータの設定で、パリティを ODD (奇数)に設定し、文字を送ってみます。
以下のようにエラーが検出されます。

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

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

お疲れさまでした。

UARTカテゴリの最新記事