STM32 Rust halと組み合わせてUARTの受信割り込みを使ってみた

STM32 Rust halと組み合わせてUARTの受信割り込みを使ってみた

今回は hal と組み合わせてUARTの受信割り込みを使ってみました。

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

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

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

プロジェクトをつくる

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

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

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

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

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

VSCodeを起動する

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

cd stm32f401-uart5
code .

C:\Users\xxxxx>cd stm32f401-uart5
C:\Users\xxxxx\stm32f401-uart5>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 があります。
依存関係を以下のように記述します。

[dependencies]
embedded-hal = "0.2"
cortex-m = "0.7"
cortex-m-rt = "0.7"
panic-halt = "0.2.0"
stm32lib = { path = "stm32lib" }

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

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

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

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

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

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

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

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

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

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

#![no_std]
pub mod uart;

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

stm32libにつくられたCargo.tomlを編集する

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

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

モジュール 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<USART, PINS> ErrorDetect for Serial<USART, PINS>
where
    PINS: Pins<USART>,
    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;   // write!()マクロを使えるようにする
use stm32lib::uart::ErrorDetect;    // 追加するトレイトを使えるようにする
use stm32f4xx_hal::prelude::*;  // MHz()
use cortex_m::delay;    // Delayを使う

use stm32f4xx_hal::pac::*;
use cortex_m::interrupt::*;
use core::cell::RefCell;
use core::ops::DerefMut;    // deref_mut()
use stm32f4xx_hal::serial::*;   // USART2
use stm32f4xx_hal::gpio::*; // Pin

const BUFFER_SIZE: usize = 256;

static UART: Mutex<RefCell<Option<
    Serial<USART2, (PA2, PA3)>
>>> = Mutex::new(RefCell::new(None));

static RDATA: Mutex<RefCell<Option<
    Receiver<>
>>> = Mutex::new(RefCell::new(None));

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

    let dp = stm32f4xx_hal::pac::Peripherals::take().unwrap();
    let cp = cortex_m::peripheral::Peripherals::take().unwrap();    // Delayを使うので
    let gpioa = dp.GPIOA.split();   // GPIOAのclockも有効にしてくれる (AHBENRレジスタ)
    let bps = Bps(115_200_u32); // 通信速度
    let seri_config = config::Config {  // 通信パラメーターの設定
        baudrate: bps,
        wordlength: config::WordLength::DataBits8,  // 実際には7ビット
        parity: config::Parity::ParityEven,
        stopbits: config::StopBits::STOP1,
        dma: config::DmaConfig::None,
    };

    let rcc = dp.RCC.constrain();
    let clks = rcc
        .cfgr
        .use_hse(8.MHz())   // 外部クロックを使う
        .bypass_hse_oscillator()    // 矩形波を使う(水晶振動子でなく発信器を使う)
        .sysclk(84.MHz())
        .pclk1(42.MHz())
        .pclk2(84.MHz())
        .freeze();

    let mut delay = delay::Delay::new(cp.SYST, 84000000_u32);

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

    let receiver = Receiver::new();

    unsafe {
        cortex_m::peripheral::NVIC::unmask(interrupt::USART2);
    }
    cortex_m::interrupt::free(|cs| RDATA.borrow(cs).replace(Some(receiver)));

    cortex_m::interrupt::free(|cs| UART.borrow(cs).replace(Some(serial)));

    cortex_m::interrupt::free(|cs| {
        if let Some(ref mut serial) =
            UART.borrow(cs).borrow_mut().deref_mut()
        {
            serial.listen(Event::Rxne);
            writeln!(serial, "hellow UART receive interrupt world.\r\n").unwrap();                
        }
    });

    loop {
        delay.delay_ms(1000_u32);   // 1000msec遅延
        cortex_m::interrupt::free(|cs| {
            if let Some(ref mut receiver) =
                RDATA.borrow(cs).borrow_mut().deref_mut()
            {
                while receiver.readable() {
                    let c = receiver.get();
                    if let Some(ref mut serial) =
                        UART.borrow(cs).borrow_mut().deref_mut()
                    {
                        serial.write(c).unwrap();
                    }
                }
            }
        });
    }
}

#[interrupt]
fn USART2() {
    cortex_m::interrupt::free(|cs| {
        if let Some(ref mut serial) =
            UART.borrow(cs).borrow_mut().deref_mut()
        {
            if serial.is_rx_not_empty() {
                if serial.is_pe() {
                    let _ = serial.read();  // 読み捨てる
                }
                else if serial.is_fe() {
                    let _ = serial.read();  // 読み捨てる
                }
                else if serial.is_ore() {
                    let _ = serial.read();  // 読み捨てる
                }
                else if let Ok(c) = serial.read() {
                    if let Some(ref mut receiver) =
                    RDATA.borrow(cs).borrow_mut().deref_mut()
                    {
                        receiver.put(c);
                    }
                }
            }
        }
    });
}

struct Receiver {
    buf: [u8; BUFFER_SIZE],
    rp: usize,
    wp: usize
}

impl Receiver {
    const fn new() -> Self {
        Receiver { buf: [0; BUFFER_SIZE], rp: 0, wp: 0 }
    }
    fn readable(&self) -> bool {
        if self.rp == self.wp {
            return false;
        }
        true
    }
    fn put(&mut self, c: u8) {
        self.buf[self.wp] = c;
        self.wp = (self.wp + 1) % BUFFER_SIZE;
    }
    fn get(&mut self) -> u8 {
        let c = self.buf[self.rp];
        self.rp = (self.rp + 1) % BUFFER_SIZE;
        c
    }
}

コード概要

今回追加した機能について概略説明しておきます。
排他処理を意識したコードを書いてみました。
(今回のコードでは、おおげさな感じがしていますけれど・・・)

main()スレッドと割り込み処理を2つのスレッドと考えると、排他制御を考えないといけません。
どちらからもアクセス可能にするためにはstaticな変数を使うことになります。

Rustではstatic mutな変数は unsafe になるので、そうならないように RefCellを使いました。
RefCellは一時的にミュータブルな参照にすることができます。

それから排他制御用に Mutex を使いました。
interrupt::free(|cs| {…}) とすることで、Mutexのlock()は意識しなくて良さそうです。

この辺りの処理は 基礎から学ぶ組込みRust を参考にさせて頂きました。

割り込みを有効にするには、NVICとペリフェラルの個々の要因の2つについて設定します。

cortex_m::peripheral::NVIC::unmask(interrupt::USART2);

serial.listen(Event::Rxne);

の2か所です。
更におおげさになってしまうのですが、受信バッファーの構造体も MutexとRefCellでラップしました。
Receiver構造体の wp を排他処理する必要がありそうなので、どうせなら “unsafeなし” づくしのコードでいこうかと。

STM32のUARTにはFiFoバッファがありませんので、256バイトのバッファーにデータを貯蓄できるようにしてみました。
loop {} の中でデータがあるか確認し、エコーバックするようにしてみました。

ビルドエラー対策

原因が良くわかっていないのですが、こちらの環境では stm32f401xx-hal のバージョンを0.13に上げてビルドするとエラーが出るようになってしまいました。
たまたま バージョン0.13 でうまく動作した stm32f401-i2c3 プロジェクトがあるので、そのプロジェクトの Cargo.lock ファイルを本プロジェクトのルートディレクトリにコピーしたところエラーが解消されました。

クレート間の相性があるようです。
プロジェクトを構成する側に問題があるように思いますが、ひとまずこれを回避策としておきます。
ご自身で書いたソースコードに関係のないエラーが出る場合には試してみてください。

今回のプロジェクトを stm32f401-uart5 におきましたので、Cargo.lockファイルをこちらからダウンロードしてお使い頂く方法でも良いと思います。

PC側の設定

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

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

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

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

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

動作させてみる

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

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

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

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

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

お疲れさまでした。

UARTカテゴリの最新記事