STM32 Rust halでSPIを使ってみる

  • 2022.03.26
  • SPI
STM32 Rust halでSPIを使ってみる

今回はhalを使ってSPIを操作してみます。

前回ベアメタルで気圧モジュールと接続しましたが、今回はhalを使ってみます。
SPI、気圧モジュール、接続方法などについては 前回の記事 を参照してください。

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

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

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

halとは

halとは Hardware Abstraction Layer の略で日本語では「ハードウエァ抽象化層」です。
halはハードウェアごとの仕様の違いを吸収し、ユーザーが同じ方法で取り扱えるようにする役割をもつ階層になります。

ベアメタルでレジスタに書く方法に比べると抽象化されているので、コードを書きやすく見やすいので保守の面でも良さそうです。

embedded_hal と stm32f4xx_hal

組み込み用のhalクレートして embedded_hal が基礎としてあり、その上に各メーカー等の hal があります。
こちらで今使っているマイコンに対しては stm32f4xx_hal を使います。

プロジェクトをつくる

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

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

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

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

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

VSCodeを起動する

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

cd stm32f401-spi2
code .

C:\Users\xxxxx>cd stm32f401-spi2
C:\Users\xxxxx\stm32f401-spi2>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-spi2\.vscode に保存します。

ソースコード

#![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 stm32f4xx_hal::gpio::{GpioExt};
use stm32f4xx_hal::{prelude::*, gpio::*};
use stm32f4xx_hal::gpio::gpioa::PA4;
use stm32f4xx_hal::gpio::gpioa::PA5;
use stm32f4xx_hal::gpio::gpioa::PA6;
use stm32f4xx_hal::gpio::gpioa::PA7;
use stm32f4xx_hal::rcc::RccExt;
use stm32f4xx_hal::time;
use stm32f4xx_hal::stm32::SPI1;
use stm32f4xx_hal::spi::*;

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;

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

    let dp = stm32f4xx_hal::pac::Peripherals::take().unwrap();
    let gpioa = dp.GPIOA.split();   // GPIOAのclockも有効にしてくれる (AHBENRレジスタ)
    let mut cs = DigitalOut::new(gpioa.pa4);
    let sck = gpioa.pa5.into_alternate_af5();   // afrl, modeレジスタを設定してくれる
    let miso = gpioa.pa6.into_alternate_af5();   // afrl, modeレジスタを設定してくれる
    let mosi = gpioa.pa7.into_alternate_af5();   // afrl, modeレジスタを設定してくれる

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

    let mode = Mode { polarity: Polarity::IdleHigh, phase: Phase::CaptureOnSecondTransition };  // SPIのモード
    let hz = time::Hertz(1000_000u32);  // SPIのクロック

    lps25hb_deselect(&mut cs);  // CS=Highにしておく

    let mut spi = Spi::spi1(
        dp.SPI1,
        (sck, miso, mosi),
        mode,
        hz,
        clks,
    );  // SPIの生成
    lps25hb_init(&mut spi, &mut cs);    // LPS25HBの初期化
    loop {
        let mut data: [u8; 4] = [P_ADRS | 0xc0, 0, 0, 0];
        lps25hb_select(&mut cs);
        lps25hb_send_buf(&mut spi, &mut data);
        lps25hb_deselect(&mut cs);
        let mut press = (data[3] as u32) << 16_u32 | (data[2] as u32) << 8_u32 | data[1] as u32;
        press >>= 12_i32;   // 1/4096
    }
}

fn lps25hb_init(spi: &mut Spi<SPI1, (PA5<Alternate<AF5>>, PA6<Alternate<AF5>>, PA7<Alternate<AF5>>)>, cs: &mut DigitalOut) -> bool {

    lps25hb_select(cs);
    lps25hb_send(spi, WHO_AM_I | 0x80);     // WHO_AM_I コマンドを送る
    let res = lps25hb_send(spi, 0u8);   // 返事を読む
    lps25hb_deselect(cs);

    lps25hb_select(cs);
    lps25hb_send(spi, CTRL_REG1);           // CTRLREG1
    lps25hb_send(spi, WAKE_UP);             // 起床を指示
    lps25hb_deselect(cs);
    if res == LPS25HB_DEVICE_CODE { // デバイスコードが返ること
        return true;
    }
    false
}

fn lps25hb_select(cs: &mut DigitalOut) {    // CS=Low
    cs.select();
}

fn lps25hb_deselect(cs: &mut DigitalOut) {  // CS=High
    cs.deselect();
}

fn lps25hb_send(spi: &mut Spi<SPI1, (PA5<Alternate<AF5>>, PA6<Alternate<AF5>>, PA7<Alternate<AF5>>)>, data: u8) -> u8 {
    while !spi.is_txe() {}
    spi.send(data).unwrap();    // 送って
    while !spi.is_rxne() {}
    spi.read().unwrap() // 読む
}

fn lps25hb_send_buf(spi: &mut Spi<SPI1, (PA5<Alternate<AF5>>, PA6<Alternate<AF5>>, PA7<Alternate<AF5>>)>, data: &mut [u8]) {
    spi.transfer(data).unwrap();    // 送って読む
}

struct DigitalOut { // GPIO出力用の構造体
    pin: PA4<Output<PushPull>>
}

impl DigitalOut {
    fn new(pin: PA4<Input<Floating>>) -> DigitalOut {
        DigitalOut { pin: pin.into_push_pull_output() }
    }
    fn deselect(&mut self) {
        self.pin.set_high().unwrap();
    }
    fn select(&mut self) {
        self.pin.set_low().unwrap();
    }    
}

コード概要

要所にコメントを入れたので参考にしてください。
DigitalOutというGPIO出力用の構造体を定義してみました。

気圧センサーの初期化:
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バイトの値をオートインクリメントで読み出しています。

Spi::transfer()というメソッドを使うと、読み込み用のコマンドを送信した結果を受信することができます。
配列の戻り値 data[0]は意味のない値です。
data[1],[2],[3]に要求した値が返ってきます。

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

本日の測定結果:

3バイトの合成値: 4195217(dec) = 4003c7(hex)
これを 12ビット右シフトすると 400(hex) = 1024[hPa]

となりました。

動作させてみる

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

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

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

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

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

お疲れさまでした。

SPIカテゴリの最新記事