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

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

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

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

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

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

UARTとは

UARTとは、Universal Asynchronous Receiver Transmitter の略で、日本語では汎用非同期送受信機です。

STM32のペリフェラルにはUSARTがあり、UARTはその機能の一部になります。
(シリーズによっては USART, UART の両方が存在するデバイスもあります)

USARTの S は syonchronous 同期のことで、UARTはその同期の機能を取り除いたものを指しています。
同期にはクロックが必要ですが、非同期通信にクロックという信号線はありません。

非同期式は一般的に、調歩同期式と呼ばれています。
いくつかの制御信号がありますが省略されて、送信、受信とGNDの3本で通信することが多いです。

UARTは調歩同期式の通信を行うためにマイコンに内蔵されているペリフェラル(周辺機能)と考えておけば良いでしょう。

ハードウェア的なインターフェースは、

RS-232C
RS-422
RS-485
USB-シリアル

などがあります。

ブロック図

今回はUARTを使ってPCと通信してみます。
NucleoボードのマイコンのUARTはデバッグ用のマイコンにつながっていますが、VCP(Virtual COM Port : 仮想COMポート)を使うことで、あたかもPCと直接通信しているように考えることができます。
デバッグ用のUSBケーブルを1本つなぐだけでデバッグとUARTによる通信を兼用できるので便利です。

図にあるように、VCPを使うためにはマイコンの PA2 と PA3 ピンを使います。

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

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

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

今回使うGPIOA2(PA2)には USART2_TX が、GPSIA3(PA3)には USART2_RX の機能を割り当てることができます。

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

AF7 はテーブル9の AF07 を指しています。

オルタネート機能レジスタには上位と下位があり、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-uart と入力します。

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

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

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

VSCodeを起動する

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

cd stm32f401-uart
code .

C:\Users\xxxxx>cd stm32f401-uart
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-uart\.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 stm32f4::stm32f401;

#[entry]
fn main() -> ! {
    let dp = stm32f401::Peripherals::take().unwrap();   // (1)デバイス用Peripheralsの取得
    clock_init(&dp);    // (2)クロック関連の初期化
    gpioa2a3_init(&dp); // (3)GPIOAの初期化
    usart2_init(&dp);   // (4)usart2の初期化
    loop {
        while dp.USART2.sr.read().rxne().bit() {	// (5)
            if dp.USART2.sr.read().pe().bit() {	// (6)parity error
                let err = b"\r\nDetected parity error.\r\n";    // (7)エラー検出時に送信する文字列
                let _ = dp.USART2.dr.read().bits(); // (8)読み捨てる
                for c in err.iter() {   // (9)文字列の送信処理
                    let data: u32 = *c as u32;
                    dp.USART2.dr.write( |w| unsafe { w.bits(data) });
                    while !dp.USART2.sr.read().txe().bit() {}   // (10)送り終わるまで待つ
                }
            }
            else if dp.USART2.sr.read().fe().bit() {	// (11)framing error
                let err = b"\r\nDetected framing error.\r\n";
                let _ = dp.USART2.dr.read().bits();
                for c in err.iter() {
                    let data: u32 = *c as u32;
                    dp.USART2.dr.write( |w| unsafe { w.bits(data) });
                    while !dp.USART2.sr.read().txe().bit() {}
                }
            }
            else if dp.USART2.sr.read().ore().bit() {	// (12)overrun error
                let err = b"\r\nDetected overrun error.\r\n";
                let _ = dp.USART2.dr.read().bits();
                for c in err.iter() {
                    let data: u32 = *c as u32;
                    dp.USART2.dr.write( |w| unsafe { w.bits(data) });
                    while !dp.USART2.sr.read().txe().bit() {}
                }
            }
            else {	// (13)no error
                dp.USART2.dr.write( |w| unsafe { w.bits(dp.USART2.dr.read().bits()) });	// echo back
                while !dp.USART2.sr.read().txe().bit() {}
            }
        }
    }
}

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

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

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

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

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

//  SYSCLK = 16MHz * 1/M * N * 1/P
//  SYSCLK = 16MHz * 1/16 * 336 * 1/4 = 84MHz
//  APB1 = 42MHz (USTAR2 pclk1)

}

fn gpioa2a3_init(dp: &stm32f401::Peripherals) {
    dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());    // (22)GPIOAのクロックを有効にする
    dp.GPIOA.moder.modify(|_, w| w.moder2().alternate());   // (23)GPIOA2をオルタネートに設定    
    dp.GPIOA.moder.modify(|_, w| w.moder3().alternate());   // (24)GPIOA3をオルタネートに設定    
    dp.GPIOA.afrl.modify(|_, w| w.afrl2().af7());           // (25)GPIOA2をAF7に設定    
    dp.GPIOA.afrl.modify(|_, w| w.afrl3().af7());           // (26)GPIOA3をAF7に設定
    
    // GPIOA2 = USART2 Tx
    // GPIOA3 = USART2 Rx
}

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

    // 通信速度: 115,200
    // データ長: 7ビット
    // パリティ: 偶数(EVEN)
    // ストップビット: 1ビット

    dp.RCC.apb1enr.modify(|_,w| w.usart2en().enabled());    // (27)USART2のクロックを有効にする
    dp.USART2.cr1.modify(|_, w| w.te().enabled());  // (28)送信有効
    dp.USART2.cr1.modify(|_, w| w.re().enabled());  // (29)受信有効
    dp.USART2.cr1.modify(|_, w| w.pce().enabled()); // (30)パリティチェック有効
    dp.USART2.cr1.modify(|_, w| w.ue().enabled());  // (31)USART有効

// 以下のようにまとめて書くこともできる
// dp.USART2.cr1.modify(|_, w| w.te().enabled().re().enabled().pce().enabled().ue().enabled());

    dp.USART2.brr.modify(|_, w| w.div_mantissa().bits(22)); // (32)ボーレート(整数部)
    dp.USART2.brr.modify(|_, w| w.div_fraction().bits(12)); // (33)ボーレート(小数部)

//  bps = pclk1 / 16 * USARTDIV
//  USARTDIV = 22 + 12/16 = 22.75
//  bps = 42M / 16 * 22.75 = 42M / 364 =115384
//  誤差 115384/115200 = 1.001597222
}

コード概要

適度にコメントを入れましたので、参考にしてください。
パリティを付加する仕様にして、エラーを検出できるようにしてみました。
初期化の後、loop()でPCからデータが来るのを待ちます。
受信したら、まずエラーがないかを確認しています。
エラーを検出したら受信した文字を読み捨てて、文字列を返すようにしてみました。
エラーがない場合に、受信した文字をそのまま返しています。

(32),(33)ボーレートの計算がややこしいです。
コメントを追記しておきました。

PC側の設定

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

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

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

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

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

動作させてみる

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

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

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

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

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

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

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

お疲れさまでした。

UARTカテゴリの最新記事