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

  • 2022.03.19
  • SPI
STM32 Rust ベアメタルにSPIを使ってみる

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

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

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

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

SPIとは

SPIはシリアル・ペリフェラル・インターフェースの略です。
I2Cと同じようにマイコンと周辺デバイスを(一般的には)プリント基板上で通信するための手段です。

I2Cではデータバスが1本で送信と受信が兼用になっていますが、SPIは別々です。
別ということは全2重通信(送信と受信を同時に行うこと)が可能なので、その分通信速度は速いです。
(SPIの方が速い要因はそれだけではないと思いますけれど・・・)

・モトローラが開発した通信方式
・SCK, MISO, MOSI, SSの4本の通信ラインを使って双方向で通信が可能
・通信速度はI2Cに比べて高速
・マスタとスレーブが存在する

などの特徴があります。

SPIについては こちら を見て頂ければと思います。

スレーブとなるターゲット

STM32(をマスタ)で制御し、スレーブのデバイスは以下のものを使ってみました。

気圧センサーモジュール

価格は600円でネットを使って購入しました。

メーカーはSTマイクロです。デバイスのデータシートは こちら です。

気圧センサーのプロトコル

気圧センサーの読み書きプロトコルの図です。

クロックのアイドル状態は H (CPOL=1) です。
またデータのサンプリングは2番目のエッジ(立ち上がりエッジ)を使います。(CPHA=1)

また、送受信のラインが別になっているので一般的なモトローラ形式のSPIフォーマットになります。

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

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

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

PA4~7にSPI1の機能を割り当てられることがわかります。
ということで今回は PA4~7 の端子を使うことにします。

ただし SPI1_NSS という信号は制御がやりにくいようなので、PA4端子はGPIOA4として使うことにします。

SPI1の機能を使うために GPIOA5~7のモードレジスタでオルタネートの設定を行い、もうひとつオルタネート機能下位レジスタの AFRL5,6,7 に AF5 を設定します。

AF5 はテーブル9の AF05 を指しています。

オルタネート機能レジスタには上位と下位があり、GPIOx0-7 (xはA, B, C…) に対しては下位に、GPIOx8-15 に対しては上位の方に設定を行います。

どの端子にどのペリフェラルや機能を割り当てるのか、回路を設計する上でテーブル9は重要な存在になります。

接続

下表のとおりに接続します。

このモジュールは I2C か SPI のどちらで接続するのか選択することができます。
今回はSPIで接続するので5番ピンをチップセレクトの信号とつなぎます。
割り込みは使わないのでINT端子は接続していません。

なおSPIで通信する場合、裏面のJ1, J2の半田ジャンパーはオープンにします。
(10kΩのプルアップ抵抗がつながることになるので、それでも動くとは思いますが・・・)

プロジェクトをつくる

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

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

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

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

VSCodeを起動する

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

cd stm32f401-spi
code .

C:\Users\xxxxx>cd stm32f401-spi
C:\Users\xxxxx\stm32f401-spi>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"

(注意)今回の追加

8行目に cortex-m = “0.6.0” があるので Delayを使うために “0.7” にバージョンを上げておきます。

編集が終わったら Ctrl + s で保存しておきます。

クレートは cretes.io から探すことができます。

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-spi\.vscode に保存します。

ソースコード

main()関数の外側は、組み込み特有の記述になります。
このように書くものだと考えておけば良いと思います。

#![no_std]
#![no_main]

const WHO_AM_I: u8 = 0x0f;  // デバイス確認用のコマンド
const CTRL_REG1: u8 = 0x20; // コントロールレジスタ1
const WAKE_UP: u8 = 0x90;   // デバイスを起こすためのコマンド
const P_ADRS: u8 = 0x28;    // 気圧読み込み用のアドレス
const LPS25HB_DEVICE_CODE: u8 = 0xbd;

use panic_halt as _; // you can put a breakpoint on `rust_begin_unwind` to catch panics
use cortex_m_rt::entry;
use cortex_m::delay;    // Delayを使う
use stm32f4::stm32f401;

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

    let mut dp = stm32f401::Peripherals::take().unwrap();   // デバイス用Peripheralsの取得
    let cp = cortex_m::peripheral::Peripherals::take().unwrap();    // cortex-m Peripheralsの取得
    let mut delay = delay::Delay::new(cp.SYST, 84000000_u32);   // Delayの生成
    clock_init(&dp);    // クロック関連の初期化
    gpioa5_init(&dp);   // GPIOAの初期化
    spi1_init(&dp);     // SPI1の初期化
    lps25hb_init(&mut dp);  // LPS25HBの初期化
    loop {
        delay.delay_ms(5_u32);           // delay 2000msec
        lps25hb_select(&dp);
        lps25hb_send(&mut dp, (P_ADRS | 0xc0) as u16);
        let l = lps25hb_send(&mut dp, 0);
        let m = lps25hb_send(&mut dp, 0);
        let h = lps25hb_send(&mut dp, 0);
        lps25hb_deselect(&dp);
        let mut press = h << 16 | m << 8 | l;
        press >>= 12;   // 1/4096
    }
}

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

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

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

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

    dp.RCC.cfgr.modify(|_,w| w.sw().pll());     // (19)sysclk = PLL
    while !dp.RCC.cfgr.read().sws().is_pll() {  // (20)SWS システムクロックソースがPLLになるまで待つ
    }
//  SYSCLK = 16MHz * 1/M * N * 1/P
//  SYSCLK = 16MHz * 1/16 * 336 * 1/4 = 84MHz
//  APB2 = 84MHz (SPI1 pclk1)
}

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

    dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());    // (21)GPIOAのクロックを有効にする
    dp.GPIOA.moder.modify(|_, w| w.moder4().output());      // (22)GPIOA4を汎用出力に設定    

    dp.GPIOA.moder.modify(|_, w| w.moder5().alternate());   // (22)GPIOA5をオルタネートに設定    
    dp.GPIOA.afrl.modify(|_, w| w.afrl5().af5());           // (23)GPIOA5をAF5に設定    
    dp.GPIOA.moder.modify(|_, w| w.moder6().alternate());   // (22)GPIOA6をオルタネートに設定    
    dp.GPIOA.afrl.modify(|_, w| w.afrl6().af5());           // (23)GPIOA6をAF5に設定    
    dp.GPIOA.moder.modify(|_, w| w.moder7().alternate());   // (22)GPIOA7をオルタネートに設定    
    dp.GPIOA.afrl.modify(|_, w| w.afrl7().af5());           // (23)GPIOA7をAF5に設定    

    lps25hb_deselect(dp);   // CS=High
}

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

    // SPI1のクロックイネーブル機能は APB2 にある
    dp.RCC.apb2enr.modify(|_,w| w.spi1en().enabled());          // (24)SPI1のクロックを有効にする
    dp.SPI1.cr1.modify(|_, w| w.ssm().set_bit());
    dp.SPI1.cr1.modify(|_, w| w.ssi().set_bit());
    dp.SPI1.cr1.modify(|_, w| w.br().div16());          // 84MHz/16=5.25MHz
    dp.SPI1.cr1.modify(|_, w| w.cpha().second_edge());
    dp.SPI1.cr1.modify(|_, w| w.cpol().idle_high());
    dp.SPI1.cr1.modify(|_, w| w.mstr().master());
    dp.SPI1.cr1.modify(|_, w| w.spe().enabled());
}

fn lps25hb_init(dp: &mut stm32f401::Peripherals) -> bool {

    lps25hb_select(dp);
    lps25hb_send(dp, (WHO_AM_I | 0x80).into());  // WHO_AM_I コマンドを送る
    let res = lps25hb_send(dp, 0u16);  // 読む
    lps25hb_deselect(dp);

    lps25hb_select(dp);
    lps25hb_send(dp, (CTRL_REG1).into());   // CTRLREG1
    lps25hb_send(dp, (WAKE_UP).into());     // 起床を指示
    lps25hb_deselect(dp);
    if res == LPS25HB_DEVICE_CODE.into() {
        return true;    // デバイスコードが返ってくれば true
    }
    false
}

fn lps25hb_select(dp: &stm32f401::Peripherals) {    // CS=Low
    dp.GPIOA.odr.modify(|_, w| w.odr4().low());
}

fn lps25hb_deselect(dp: &stm32f401::Peripherals) {  // CS=High
    dp.GPIOA.odr.modify(|_, w| w.odr4().high());
}

fn lps25hb_send(dp: &mut stm32f401::Peripherals, data: u16) -> u32 {
    while dp.SPI1.sr.read().txe().is_not_empty() {}
    dp.SPI1.dr.write(|w| w.dr().bits(data));    // 書いて
    while dp.SPI1.sr.read().rxne().is_empty() {}
    dp.SPI1.dr.read().bits()    // 読む
}

コードの解説

詳しくはデバイスのデータシートをご覧になってください。
SPI1のクロックソースはAPB2になります。
デバイスのSPIクロックは最大で10MHzなので、APB2を16分周して5.25MHzで動かしてみました。

lps25hb_init()

デバイスに対してコマンドを書きますが、コマンドに0x80を OR した値を書くとリード動作になり、次にダミーデータ(0)を送りながら値を読むことができます。
コマンドに0xc0を OR した値を書くと、やはりリード動作で、かつ読み込むレジスタのアドレスが自動的にインクリメントします。

戻り値で判定はしていませんが lps25hb_init()で true を返せは通信できていると判断して良さそうです。
WHO_AM_I に 0x80 を OR した値を送り、デバイスコードの読み込み指示を行っています。
その後 CTRL_REG1 には WAKE_UP(0x90) を書きパワーダウンを解除し、1Hzの出力データレートを設定しています。

loop{}内では P_ADRS に 0xc0 を OR した値を送っています。
これで3バイトの値をオートインクリメントで読み出しています。

press >>= 12; した値が気圧[hPa]として求められます。

動作させてみる

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

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

プログラムを動作させた後、停止させてステップ実行しながら press の値を確認してみます。
こちらでは press の値を 1023 で読むことができました。

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

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

お疲れさまでした。

SPIカテゴリの最新記事