ラズピコでRust(9) BME280センサーをSPIでつないでみる

  • 2024.06.14
  • SPI
ラズピコでRust(9) BME280センサーをSPIでつないでみる

皆さん こんにちは。
ポンコツRustacean の moon です。

今回はBME280のセンサーモジュールとラズピコをSPIでつないでみました。

この記事は開発環境を構築することを前提にしています。
環境構築について知りたい方は こちらの記事 をご覧になってください。

このサイトは 書籍 基礎から学ぶ組込みRust を参考にしています。

SPIとは

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

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

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

などの特徴があります。

(最近マスタ、スレーブという言葉は使われないようですが・・・)

SPIモジュール

SPIの温湿度・気圧センサーモジュールは こちら を使いました。

モジュールの資料は 温湿度・気圧センサモジュールキット からご確認ください。

執筆時点では秋月電子にあるセンサーモジュールは在庫が少ないです。

BME280でネット検索すると、いろいろと出てきますので適切なものを選択してください。

できれば I2C と SPI の両方のインターフェースを持っているものが良いと思います。

その理由は両方の接続で遊べるからです (^_^)

BME280センサー

BME280はBOSCH社から提供されているセンサーです。

ドキュメントは BME280 datasheet をご覧になってください。

このセンサー自体はI2CまたはSPIのインターフェースでつなぐことができます。
今回はSPIでつないでみました。

温度、湿度、気圧を測定することができます。
ただしセンサーに個性があるため、個体にキャリブレーションされた値を読んで補正する必要があります。

温度、湿度、気圧を測定できてかつ、キャリブレーション値を書き込まれたものが1,000円程度で購入できるのはお買い得です。

データシートではAPIを使うことが推奨されていますがAPIはC言語で書かれています。
データシートにもシフト演算と浮動小数点を使うコードがC言語で示されています。

今回はデータシートにあるC言語のシフト演算をRustのコードに置き換えてみました。

接続

下表の通りに接続します。

電源・信号 温湿度・気圧センサモジュール ラズピコ
電源(3.3V) 1番 36番ピン(3.3V出力)
GND 2番 18番ピン(GND)
CSB 3番 7番ピン(GPIO5 – SPI0 CS)
SDI 4番 10番ピン(GPIO7 – SPI0 TX)
SDO 5番 6番ピン(GPIO4 – SPI0 RX)
SCK 6番 9番ピン(GPIO6 – SPI0 SCK)

※SDOの信号は4.7kΩの抵抗でプルアップしました。

BMEセンサーモジュールのジャンパーは以下のように設定しました。

J1:開放
J2:開放
J3:開放

詳しくはモジュールのマニュアル及びセンサーのデータシートをご覧になってください。

パッケージの複製を作成する

まず私のGitHubリポジトリにあるRustのひな形をベースに別名でパッケージをつくります。
ひな形はデバッグ環境用のものですから、その複製もデバッグできるようになります。

新しいパッケージ名を rp2040-spi-bme280 として git clone します。
xxxxは皆さんのユーザー名です。
カレントディレクトリを rp2040-spi-bme280 に移します。

今回は元プロジェクトを rp2040-i2c-bme280 にしました。
構成が同じなのでライブラリクレートを作成したりする手間が省けます。

C:\Users\xxxx>cd pprp
C:\Users\xxxx\pprp>git clone https://github.com/moons3925/rp2040-i2c-bme280.git rp2040-spi-bme280
C:\Users\xxxx\pprp>cd rp2040-spi-bme280

上位のCargo.tomlを編集する

rp2040-spi-bme280ディレクトリにある Cargo.toml のnameを rp2040-spi-bme280に変更します。

bme280.rsを作成・編集する

次にrp2040-lib\srcディレクトリ(lib.rsがある場所)に bme280.rs を以下のように編集します。

(1行目の i2c を spi に変更するだけです)

2行目に pub mod spi; を追加するだけです。(pub mod i2c; は残しておく)

BME280はI2CとSPIで接続できるので、どちらを使っても必要になる共通の処理をトレイト Interface で定義しました。

pub mod i2c;
pub mod spi;

const OSRS_H: u8 = 0x3; // 湿度 x4 サンプリング
const CTRL_HUM_WDATA: u8 = OSRS_H;

const OSRS_T: u8 = 0x3; // 温度 x4 サンプリング
const OSRS_P: u8 = 0x3; // 気圧 x4 サンプリング
const MODE: u8 = 0x3; // ノーマルモード
const CTRL_MEAS_WDATA: u8 = OSRS_T << 5 | OSRS_P << 2 | MODE;

const T_SB: u8 = 0x4; // スタンバイ時間 500msec
const FILTER: u8 = 0x2; // フィルター 4
const SPI3W_EN: u8 = 0; // SPI3Wire enable = desable
const CONFIG_WDATA: u8 = T_SB << 5 | FILTER << 2 | SPI3W_EN;

const CTRL_HUM_REG: u8 = 0xf2;
const CTRL_MEAS_REG: u8 = 0xf4;
const CONFIG_REG: u8 = 0xf5;
const ID_REG: u8 = 0xd0;
const ID_CODE: u8 = 0x60;

pub const DEVICE_ADDRESS: u8 = 0x76;

const CALIBRATION_OFFSET_T_P: u8 = 0x88;
const CALIBRATION_OFFSET_H1: u8 = 0xa1;
const CALIBRATION_OFFSET_H2: u8 = 0xe1;
const PRESS_MSB_REG: u8 = 0xf7;

trait Interface {
    fn read_register(&mut self, register: u8) -> u8;
    fn write_register(&mut self, register: u8, value: u8);
    fn write(&mut self, value: &[u8]);
    fn read_trim(&mut self, buffer: &mut [u8; 32]);
    fn read_data(&mut self, buffer: &mut [u8; 8]);
}

bme280ディレクトリにspi.rsを作成する

2024年現在のモジュールの管理は2018年版のエディションが主流ですから、ここでも2018年版で扱います。

ライブラリクレートのクレートルート(親と表現することにします)は lib.rs です。

lib.rs に pub mod bme280; と記述したので、 bme280.rs が子になります。

そのファイルに mod xxx を宣言すると xxx はそのファイルのひとつ下の階層になります。

子の下に孫をつくりたい場合、子の名前と同じディレクトリを作成することになっています。

今回の孫は spi.rs です。
孫にするために、子(この場合 bme280.rs)に pub mod spi; を宣言します。

rp2040_libディレクトリの下のsrcの下にbme280のディレクトリがあります。

そしてそのディレクトリの中に spi.rs を作成します。

(i2c.rsはそのまま残しておいて構いません)

これで親 – 子 – 孫 の関係は以下のようになります。

lib.rs (rp2040_lib) bme280.rs spi.rs

親子関係

子から親、及び孫から子を参照する場合、ひとつ上の階層という意味で super というキーワードを使います。
superは相対的な階層を示すのに使われるキーワードです。

実際のコードで説明します。

spi.rs の先頭に use super::Interface; という記述があります。

super はひとつ上の階層ですから、bme280.rs の Interface (trait) を使えるようにする という意味です。

実際に bme280.rs を見ると trait Interface という記述があります。

super::super とすれば相対的に2つ上の階層を意味します。

それから crate というキーワードがあります。
crate はクレートルートを指す、絶対パスになります。

例えば先ほど i2c.rs で use super::Interface; がありました。
これを絶対パスで記述すると以下のようになります。

use crate::bme280::Interface;

crate はクレートルートでこの場合 lib.rs で、その下の bme280.rs の Interface を使えるようにする という意味で先ほどと同じになります。
それからメソッドの中でも出てきますが、self というキーワードがあります。
これは自身の階層を意味します。

これらクレートやモジュールの概念について理解しておくと他の人が書いたコードが読みやすくなります。

親子孫の3階層をつくって簡単なコードを書いてみると理解できるようになります。
私も最初はさっぱりわからなかったのですけど、最近慣れて来てモジュールの構成について理解することができました。

lib.rs (rp2040_lib) bme280.rs spi.rs
pub mod bme280 を lib.rs に記述する pub mod i2c をbme280.rs に記述する bme280ディレクトリに置く

孫をつくる場合には、子と同じ名前のディレクトリを作成し孫のファイルをそのディレクトリに配置する必要があります。

spi.rsを編集します

use super::Interface;

use embedded_hal::blocking::spi::transfer::Default;
use embedded_hal::blocking::spi::write::Default as OtherDefault;
use embedded_hal::digital::v2::OutputPin;

use embedded_hal::prelude::_embedded_hal_blocking_spi_Transfer;
use embedded_hal::prelude::_embedded_hal_blocking_spi_Write;

use crate::bme280::ID_CODE;
use crate::bme280::ID_REG;

use crate::bme280::CALIBRATION_OFFSET_H1;
use crate::bme280::CALIBRATION_OFFSET_H2;
use crate::bme280::CALIBRATION_OFFSET_T_P;
use crate::bme280::CONFIG_REG;
use crate::bme280::CONFIG_WDATA;
use crate::bme280::CTRL_HUM_REG;
use crate::bme280::CTRL_HUM_WDATA;
use crate::bme280::CTRL_MEAS_REG;
use crate::bme280::CTRL_MEAS_WDATA;
use crate::bme280::PRESS_MSB_REG;

pub struct BME280<IF, GPIO>
where
    IF: Default<u8> + OtherDefault<u8>,
    GPIO: OutputPin,
{
    interface: SPIInterface<IF, GPIO>,
    buffer: [u8; 32],
    buffer2: [u8; 8],
    dig_t1: u16,
    dig_t2: i16,
    dig_t3: i16,
    dig_p1: u16,
    dig_p2: i16,
    dig_p3: i16,
    dig_p4: i16,
    dig_p5: i16,
    dig_p6: i16,
    dig_p7: i16,
    dig_p8: i16,
    dig_p9: i16,
    dig_h1: i8,
    dig_h2: i16,
    dig_h3: i8,
    dig_h4: i16,
    dig_h5: i16,
    dig_h6: i8,
    pub temp_raw: u32,
    pub pres_raw: u32,
    pub humi_raw: u32,
    t_fine: i32,
}

impl BME280<IF, GPIO>
where
    IF: Default<u8> + OtherDefault<u8>,
    GPIO: OutputPin,
{
    pub fn new(spi: IF, cs: GPIO) -> Self {
        Self {
            interface: SPIInterface { spi, cs },
            buffer: [0; 32],
            buffer2: [0; 8],
            dig_t1: 0,
            dig_t2: 0,
            dig_t3: 0,
            dig_p1: 0,
            dig_p2: 0,
            dig_p3: 0,
            dig_p4: 0,
            dig_p5: 0,
            dig_p6: 0,
            dig_p7: 0,
            dig_p8: 0,
            dig_p9: 0,
            dig_h1: 0,
            dig_h2: 0,
            dig_h3: 0,
            dig_h4: 0,
            dig_h5: 0,
            dig_h6: 0,
            temp_raw: 0,
            pres_raw: 0,
            humi_raw: 0,
            t_fine: 0,
        }
    }
    pub fn init(&mut self) -> bool {
        let _ = self.interface.cs.set_high(); // デバイスを非選択にしておく

        let id = self.interface.read_register(ID_REG);
        if id != ID_CODE {
            return false;
        }

        self.interface.write_register(CTRL_HUM_REG, CTRL_HUM_WDATA);
        self.interface
            .write_register(CTRL_MEAS_REG, CTRL_MEAS_WDATA);
        self.interface.write_register(CONFIG_REG, CONFIG_WDATA);
        self.read_trim();
        true
    }
    pub fn read_trim(&mut self) {
        let _ = self.interface.read_trim(&mut self.buffer);

        self.dig_t1 = (self.buffer[1] as u16) << 8 | self.buffer[0] as u16;
        self.dig_t2 = (self.buffer[3] as i16) << 8 | self.buffer[2] as i16;
        self.dig_t3 = (self.buffer[5] as i16) << 8 | self.buffer[4] as i16;

        self.dig_p1 = (self.buffer[7] as u16) << 8 | self.buffer[6] as u16;
        self.dig_p2 = (self.buffer[9] as i16) << 8 | self.buffer[8] as i16;
        self.dig_p3 = (self.buffer[11] as i16) << 8 | self.buffer[10] as i16;
        self.dig_p4 = (self.buffer[13] as i16) << 8 | self.buffer[12] as i16;
        self.dig_p5 = (self.buffer[15] as i16) << 8 | self.buffer[14] as i16;
        self.dig_p6 = (self.buffer[17] as i16) << 8 | self.buffer[16] as i16;
        self.dig_p7 = (self.buffer[19] as i16) << 8 | self.buffer[18] as i16;
        self.dig_p8 = (self.buffer[21] as i16) << 8 | self.buffer[20] as i16;
        self.dig_p9 = (self.buffer[23] as i16) << 8 | self.buffer[22] as i16;

        self.dig_h1 = self.buffer[24] as i8;
        self.dig_h2 = (self.buffer[26] as i16) << 8 | self.buffer[25] as i16;
        self.dig_h3 = self.buffer[27] as i8;

        self.dig_h4 = (self.buffer[28] as i16) << 4 | (self.buffer[29] as i16) & 0xf;
        self.dig_h5 = (self.buffer[29] as i16) << 4 | ((self.buffer[30] as i16) >> 4) & 0xf;
        self.dig_h6 = self.buffer[31] as i8;
    }
    pub fn read_data(&mut self) {
        let _ = self.interface.read_data(&mut self.buffer2);
        self.pres_raw = (self.buffer2[0] as u32) << 12
            | (self.buffer2[1] as u32) << 4
            | (self.buffer2[2] as u32) >> 4;
        self.temp_raw = (self.buffer2[3] as u32) << 12
            | (self.buffer2[4] as u32) << 4
            | (self.buffer2[5] as u32) >> 4;
        self.humi_raw = (self.buffer2[6] as u32) << 8 | self.buffer2[7] as u32;
    }
    pub fn read_register(&mut self, register: u8) -> u8 {
        self.interface.read_register(register)
    }

    pub fn calibration_temperature(&mut self, adc_t: i32) -> f64 {
        let var1: i32;
        let var2: i32;
        let t: i32;
        var1 = (((adc_t >> 3) - ((self.dig_t1 as i32) << 1)) * (self.dig_t2 as i32)) >> 11;
        var2 = ((adc_t >> 4) - (self.dig_t1 as i32) * ((adc_t >> 4) - (self.dig_t1 as i32)) >> 12)
            * ((self.dig_t3 as i32) >> 14);
        self.t_fine = var1 + var2;
        t = (self.t_fine * 5 + 128) >> 8;
        (t as f64) / 100.0
    }

    pub fn calibration_humidity(&mut self, adc_h: i32) -> f64 {
        let mut v_x1_u32r: i32;
        v_x1_u32r = self.t_fine - 76800i32;
        v_x1_u32r = (((adc_h << 14)
            - (((self.dig_h4 as i32) << 20) - ((self.dig_h5 as i32) * v_x1_u32r))
            + 16384i32)
            >> 15)
            * (((((((v_x1_u32r * (self.dig_h6 as i32)) >> 10)
                * (((v_x1_u32r * (self.dig_h3 as i32)) >> 11) + 32768i32))
                >> 10)
                + 2097152i32)
                * (self.dig_h2 as i32)
                + 8192)
                >> 14);
        v_x1_u32r = v_x1_u32r
            - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * (self.dig_h1 as i32)) >> 4);
        if v_x1_u32r < 0 {
            v_x1_u32r = 0;
        }
        if v_x1_u32r > 419430400 {
            v_x1_u32r = 419430400;
        }
        ((v_x1_u32r >> 12) as f64) / 1024.0
    }

    pub fn calibration_pressure(&mut self, adc_p: i32) -> f64 {
        let mut var1: i64;
        let mut var2: i64;
        let mut p: i64;
        var1 = (self.t_fine as i64) - 128000;
        var2 = var1 * var1 * (self.dig_p6 as i64);
        var2 = var2 + ((var1 * (self.dig_p5 as i64)) << 17);
        var2 = var2 + ((self.dig_p4 as i64) << 35);
        var1 = (var1 * var1 * ((self.dig_p3 as i64) >> 8)) + ((var1 * (self.dig_p2 as i64)) << 12);
        var1 = ((((1 as i64) << 47) + var1) * (self.dig_p1 as i64)) >> 33;
        if var1 == 0 {
            return 0.0;
        }
        p = 1048576 - (adc_p as i64);
        p = (((p << 31) - var2) * 3125) / var1;
        var1 = ((self.dig_p9 as i64) * (p >> 13) * (p >> 13)) >> 25;
        var2 = ((self.dig_p8 as i64) * p) >> 19;
        p = ((p + var1 + var2) >> 8) + ((self.dig_p7 as i64) << 4);
        ((p / 256) as f64) / 100.0
    }
    pub fn get_elements(&mut self) -> (f64, f64, f64) {
        let t = self.calibration_temperature(self.temp_raw as i32);
        let h = self.calibration_humidity(self.humi_raw as i32);
        let p = self.calibration_pressure(self.pres_raw as i32);
        (t, h, p)
    }
}

pub struct SPIInterface<IF, GPIO>
where
    IF: Default<u8> + OtherDefault<u8>,
    GPIO: OutputPin,
{
    pub spi: IF,
    pub cs: GPIO,
}

impl<IF, GPIO> Interface for SPIInterface<IF, GPIO>
where
    IF: Default<u8> + OtherDefault<u8>,
    GPIO: OutputPin,
{
    fn read_register(&mut self, register: u8) -> u8 {
        let _ = self.cs.set_low();
        let mut buf: [u8; 1] = [register];
        let _ = self.spi.write(&buf);
        let mut value: u8 = 0;
        if let Ok(n) = self.spi.transfer(&mut buf) {
            value = n[0];
        }
        let _ = self.cs.set_high();
        value
    }
    fn write_register(&mut self, register: u8, value: u8) {
        let _ = self.cs.set_low();
        let _ = self.spi.write(&[(register & 0x7f), value]);
        let _ = self.cs.set_high();
    }
    fn write(&mut self, value: &[u8]) {
        let _ = self.spi.write(&value);
    }

    fn read_trim(&mut self, buffer: &mut [u8; 32]) {
        let mut buf: [u8; 1] = [CALIBRATION_OFFSET_T_P];
        let _ = self.cs.set_low();
        let _ = self.spi.write(&buf);
        for i in 0..24 {
            if let Ok(n) = self.spi.transfer(&mut buf) {
                buffer[i] = n[0];
            }
        }
        let _ = self.cs.set_high();
        let _ = self.cs.set_low();
        let mut buf: [u8; 1] = [CALIBRATION_OFFSET_H1];
        let _ = self.spi.write(&buf);
        for i in 24..25 {
            if let Ok(n) = self.spi.transfer(&mut buf) {
                buffer[i] = n[0];
            }
        }
        let _ = self.cs.set_high();
        let _ = self.cs.set_low();
        let mut buf: [u8; 1] = [CALIBRATION_OFFSET_H2];
        let _ = self.spi.write(&buf);
        for i in 25..32 {
            if let Ok(n) = self.spi.transfer(&mut buf) {
                buffer[i] = n[0];
            }
        }
        let _ = self.cs.set_high();
    }
    fn read_data(&mut self, buffer: &mut [u8; 8]) {
        let mut buf: [u8; 1] = [PRESS_MSB_REG];
        let _ = self.cs.set_low();
        let _ = self.spi.write(&buf);
        for i in 0..8 {
            if let Ok(n) = self.spi.transfer(&mut buf) {
                buffer[i] = n[0];
            }
        }
        let _ = self.cs.set_high();
    }
}

launch.jsonを編集する

.vscode にある launch.json を編集します。
executable: の名称を rp2040-i2c-bme280 から rp2040-spi-bme280 に変更します。

"executable": "./target/thumbv6m-none-eabi/debug/rp2040-spi-bme280",

main.rsを編集する

main.rsを以下の通りに編集します。

#![no_std]
#![no_main]

use fugit::RateExtU32;
use hal::pac;
use hal::uart::{DataBits, StopBits, UartConfig};
use rp2040_hal::spi::Enabled;
use rp2040_hal::Clock;
use rp_pico::entry;

use embedded_hal::digital::v2::OutputPin;

use panic_halt as _;
use rp2040_hal as hal;

use rp2040_lib::my_macro::UART_TRANSMITTER;
use rp2040_lib::print;
use rp2040_lib::println;

use rp2040_lib::bme280::spi::BME280;

use rp2040_hal::gpio::bank0::Gpio4;
use rp2040_hal::gpio::bank0::Gpio5;
use rp2040_hal::gpio::bank0::Gpio6;
use rp2040_hal::gpio::bank0::Gpio7;
use rp2040_hal::gpio::FunctionSio;
use rp2040_hal::gpio::Pin;
use rp2040_hal::gpio::PullDown;
use rp2040_hal::gpio::{FunctionSpi, SioOutput};

use crate::pac::SPI0;
use rp2040_hal::Spi;

#[entry]
fn main() -> ! {
    let mut pac = pac::Peripherals::take().unwrap();
    let core = pac::CorePeripherals::take().unwrap();
    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
    let clocks = hal::clocks::init_clocks_and_plls(
        rp_pico::XOSC_CRYSTAL_FREQ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());

    let sio = hal::Sio::new(pac.SIO);

    let pins = rp_pico::Pins::new(
        // (8-6-7)
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    let uart_pins = (pins.gpio0.reconfigure(), pins.gpio1.reconfigure());
    let uart = hal::uart::UartPeripheral::new(pac.UART0, uart_pins, &mut pac.RESETS)
        .enable(
            UartConfig::new(9600.Hz(), DataBits::Eight, None, StopBits::One),
            clocks.peripheral_clock.freq(),
        )
        .unwrap();

    let (_, uart_tx) = uart.split();

    critical_section::with(|_| unsafe {
        UART_TRANSMITTER = Some(uart_tx);
    });

    let mut led_pin = pins.led.into_push_pull_output();

    // These are implicitly used by the spi driver if they are in the correct mode
    let spi_mosi = pins.gpio7.into_function::();
    let spi_miso = pins.gpio4.into_function::();
    let spi_sclk = pins.gpio6.into_function::();
    let spi = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (spi_mosi, spi_miso, spi_sclk));

    let cs = pins.gpio5.into_push_pull_output();

    // Exchange the uninitialised SPI driver for an initialised one
    let spi = spi.init(
        &mut pac.RESETS,
        clocks.peripheral_clock.freq(),
        10.MHz(),
        embedded_hal::spi::MODE_0,
    );

    let mut bme280 = BME280::<
        Spi<
            Enabled,
            SPI0,
            (
                Pin<Gpio7, FunctionSpi, PullDown>,
                Pin<Gpio4, FunctionSpi, PullDown>,
                Pin<Gpio6, FunctionSpi, PullDown>,
            ),
        >,
        Pin<Gpio5, FunctionSio<SioOutput>, PullDown>,
    >::new(spi, cs);

    // DeviceのIDコード(0x60)を正しく読めれば成功としている
    if bme280.init() {
        println!("BME280 initialization successful.");
        println!("BME280 ID = 0x60.\r\n");
    } else {
        println!("BME280 initialization failed.\r\n");
    }

    loop {
        bme280.read_data();

        let (temp, humi, pres) = bme280.get_elements();

        println!("T = {:.2} ℃", temp);
        println!("H = {:.2} %", humi);
        println!("P = {:.2} hPa\r\n", pres);

        led_pin.set_high().unwrap();
        delay.delay_ms(200);
        led_pin.set_low().unwrap();
        delay.delay_ms(800);
    }
}

PC側の準備

通信の確認用として Tera Term を使います。

インストール後、 Tera Term を起動し、シリアルポートでUSBシリアル変換モジュールのCOMポートを選択します。

もしCOMポートが複数ある場合には、USBシリアル変換モジュールのケーブルをはずしてデバイスマネージャーでCOMポート番号を確認しておきます。

起動後、設定メニューのシリアルポートから以下の設定を行います。

スピート : 9600
データ : 8 bit
パリティ : なし
ストップビット : 1
フロー制御 : none

これらの値は送受信する相手と合わせておく必要があります。

改行コードを設定する

設定 – 端末 で改行コードを LF に設定します。

ビルドして実行する

Run – Start Debugging (F5) からプログラムを実行します。

初期化成功のメッセージの後、約1秒毎に温度、湿度、気圧の値が表示されれば成功です。

解説

概略説明します。

マクロ

(i2cの記事で書いた内容と同じです)

「println!マクロをUARTで実装する」の println! マクロ について説明します。
「println!マクロをUARTで実装する」では main.rs にマクロを書いていて、せっかくなので今回は rp2040_lib 配下にマクロのファイルも置いてみました。
my_macro.rs というファイルです。

モジュールの話を上でしましたが、マクロはまたそれと少し違いがありとまどったので書き留めておきます。

my_macro.rsを見て頂くとわかるのですが、今回のマクロは print! と println! の2つです。

マクロは クレートルート名::マクロ名 と記述することで参照が可能になります。

main.rs で使うので、main.rs に

use rp2040_lib::print;
use rp2040_lib::println;

と記述し、

lib.rs に

pub mod my_macro; と記述すれば使うことができます。

中林さんが書かれた ベアメタルテクニック – print!マクロ のコードに若干手を加えて動かすことができました。

これに初期化したUARTを割り当てて使います。

それから_print()関数自体はマクロと違い通常のスコープになるため、ライブラリクレート下に配置した今回は print マクロ本体にも以下のように手を加えました。

#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => ($crate::my_macro::_print(format_args!($($arg)*)));
}

$crate は常にマクロが定義されているクレートルートになります。
従って $crate と _print()の間に ::my_macro を入れてあげることでパスが通ります。

トレイト

SPIInterfaceという構造体にInterfaceトレイトを実装しています。
更に SPIのembedded-halの Default トレイトで制約をかけています。

私は気がつかないで苦労したのですが、ラズピコのHALの構造体では以下のように空の impl ブロックを指定しているためデフォルトの実装が採用されます。

impl<D: SpiDevice, P: ValidSpiPinout<D>> spi::write::Default<$type> for Spi<Enabled, D, P, $nr> {}
impl<D: SpiDevice, P: ValidSpiPinout<D>> spi::transfer::Default<$type> for Spi<Enabled, D, P, $nr> {}

デフォルトの実装は embedded-hal::blocking::spi::write と embedded-hal::blocking::spi::transfer に書かれているのでご確認ください。

それから transfer と write 両方で Default を使っているので、同時に使うと重複のエラーが出ます。
そのため以下のように、write の方は OtherDefault に名前を代えて対応しました。

use embedded_hal::blocking::spi::transfer::Default;
use embedded_hal::blocking::spi::write::Default as OtherDefault;

BME280

スイッチサイエンス社が以下の記事を書いています。

BME280搭載 温湿度・気圧センサモジュールの使い方

日本語でわかりやすいので、こちらを参考にさせて頂きました。

流れとしては、最初に3つのレジスタに設定を書き込んだ後、キャリブレーションの値を読みます。
後は温度、湿度、気圧のデータを読み、キャリブレーション値から計算した値を println! マクロを使って UART出力します。

メソッドの処理は大体このサイトに従って書きました。

変更点はサンプリング数を増やしてスタンバイ時間を半分(500msec)にしたところです。

計算式が非常に長いので、C言語 -> Rust の変換にひと手間かかりました。

GitHubにアップ

GitHubの rp2040-spi-bme280 にプロジェクトをアップしましたので参考になさってください。

i2cのプロジェクトで I2C / SPI を交換して動かせるように構成しておいたので今回は比較的スムーズに移植ができました。

長々とお付き合い頂きましてありがとうございます。
お疲れさまでした。

SPIカテゴリの最新記事