今回は 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 におきましたので、よろしければ参考になさってください。
お疲れさまでした。