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

  • 2024.06.07
  • I2C
ラズピコでRust(8) BME280センサーをI2Cでつないでみる

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

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

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

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

I2Cとは

主にプリント基板上でマイコンとデバイスを接続するのにつかわれる通信方式です。

・フィリップス(現NXP)セミコンダクターズが開発した通信方式
・SCLとSDAの2本の通信ラインを使って双方向で通信が可能
・通信速度は100k, 1Mbits/secなどいくつか選択肢がある
・マスタとスレーブが存在し、マルチマスタでの構築も可能

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

個人的にはコントローラーとデバイスが良いのではないかと思っています。

I2Cの詳しい仕様については こちら を見て頂ければと思います。

I2Cモジュール

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

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

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

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

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

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

BME280センサー

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

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

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

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

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

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

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

接続

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

電源・信号 温湿度・気圧センサモジュール ラズピコ
電源(3.3V) 1番 36番ピン(3.3V出力)
GND 2番 18番ピン(GND)
CSB 3番
SDA 4番 4番ピン(I2C1 SDA)
GND 5番 18番ピン(GND)
SCL 6番 5番ピン(I2C1 SCL)

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

J1:短絡(SDAを4.7kでプルアップ)
J2:短絡(SCLを4.7kでプルアップ)
J3:短絡(デバイスのCSBピンを3.3Vに接続する)

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

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

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

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

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

ライブラリクレートの作成

書籍の105ページ以降に書かれている方法でライブラリクレートを作成し、rp2040_libというライブラリクレートの下にbme280関連のモジュールを配置します。
rp2040とlibの間は _ (アンダースコア)であることに注意してください。

まず以下のコマンドでライブラリクレートを作成し、VSCodeを起動します。
このコマンドは rp2040-i2c-bme280 ディレクトリで実行する必要があります。
xxxxは皆さんのユーザー名です。

C:\Users\xxxx\pprp\rp2040-i2c-bme280>cargo new --lib rp2040_lib
C:\Users\xxxx\pprp\rp2040-i2c-bme280>code .

上位のCargo.tomlを編集する

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

rp2040-i2c-bme280ディレクトリにある Cargo.toml のdependencies を以下の構成にします。

cortex-m = "0.7.7"
cortex-m-rt = "0.7.3"
embedded-hal = "0.2.7"
panic-halt = "0.2.0"
rp-pico = "0.8.0"
rp2040-hal = "0.9"
fugit = "0.3.7"
critical-section = "1.1.2"
nb = "1.0.0"
rp2040_lib = { path = "rp2040_lib" }

これでライブラリクレートのクレートルートの名称は rp2040_lib になります。
ライブラリクレートのクレートルートのファイルは rp2040_lib\src にある lib.rs になります。

下位のCargo.tomlを編集する

rp2040-lib\src ディレクトリにある Cargo.toml を以下のように編集します。
(dependencies にクレートをxxxつ追加する)

[package]
name = "rp2040_lib"
version = "0.1.0"
edition = "2021"

[dependencies]
rp2040-hal = "0.9"
embedded-hal = "0.2.7"
nb = "1.1.0"

lib.rsを編集する

rp2040-libディレクトリにある lib.rs を以下のように編集します。
(元コード(テストコード)は削除します)

#![no_std]
#![no_std]

pub mod bme280;
pub mod my_macro;

bme280.rsを作成・編集する

rp2040-lib\srcディレクトリ(lib.rsがある場所)に bme280 というフォルダを作成します。

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

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

pub mod i2c;

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ディレクトリとi2c.rsを作成する

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

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

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

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

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

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

rp2040_libディレクトリの下のsrcの下にbme280のディレクトリを作成します。

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

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

lib.rs (rp2040_lib) bme280.rs i2c.rs

親子関係

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

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

i2c.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 i2c.rs
pub mod bme280 を lib.rs に記述する pub mod i2c をbme280.rs に記述する bme280ディレクトリに置く

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

i2c.rsを編集します

use super::Interface;

use embedded_hal::blocking::i2c::Read;
use embedded_hal::blocking::i2c::Write;
use embedded_hal::blocking::i2c::WriteRead;

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: Read + Write + WriteRead> {
    interface: I2CInterface<IF>,
    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<IF: Read + Write + WriteRead> BME280<IF> {
    pub fn new(
        i2c: IF,
        address: u8,
    ) -> Self {
        Self {
            interface: I2CInterface { i2c, address },
            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 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 mut t = self.calibration_temperature(self.temp_raw as i32);
        let mut h = self.calibration_humidity(self.humi_raw as i32);
        let mut p = self.calibration_pressure(self.pres_raw as i32);
        (t, h, p)
    }
}

pub struct I2CInterface<IF>
where
    IF: Read + Write + WriteRead,
{
    pub i2c: IF,
    pub address: u8,
}

impl<IF> Interface for I2CInterface<IF>
where
    IF: Read + Write + WriteRead,
{
    fn read_register(&mut self, register: u8) -> u8 {
        let mut buffer: [u8; 1] = [0; 1];
        let _ = self.i2c.write_read(self.address, &[register], &mut buffer);
        buffer[0]
    }
    fn write_register(&mut self, register: u8, value: u8) {
        let _ = self.i2c.write(self.address, &[register, value]);
    }
    fn write(&mut self, value: &[u8]) {
        let _ = self.i2c.write(self.address, &value);
    }

    fn read_trim(&mut self, buffer: &mut [u8; 32]) {
        let _ = self.i2c.write(self.address, &[CALIBRATION_OFFSET_T_P]);
        let _ = self.i2c.read(self.address, &mut buffer[0..24]);

        let _ = self.i2c.write(self.address, &[CALIBRATION_OFFSET_H1]);
        let _ = self.i2c.read(self.address, &mut buffer[24..25]);

        let _ = self.i2c.write(self.address, &[CALIBRATION_OFFSET_H2]);
        let _ = self.i2c.read(self.address, &mut buffer[25..32]);
    }
    fn read_data(&mut self, buffer: &mut [u8; 8]) {
        let _ = self.i2c.write(self.address, &[PRESS_MSB_REG]);
        let _ = self.i2c.read(self.address, &mut buffer[0..8]);
    }
}

launch.jsonを編集する

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

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

main.rsを編集する

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

#![no_std]
#![no_main]

use fugit::RateExtU32;
use hal::pac;
use hal::uart::{DataBits, StopBits, UartConfig};
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::bme280::i2c::BME280;
use rp2040_lib::bme280::DEVICE_ADDRESS;

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

use rp2040_hal::gpio::bank0::Gpio2;
use rp2040_hal::gpio::bank0::Gpio3;
use rp2040_hal::gpio::FunctionI2c;
use rp2040_hal::gpio::Pin;
use rp2040_hal::gpio::PullDown;
use rp2040_hal::i2c::I2C;
use rp2040_hal::pac::I2C1;

#[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();

    let mut i2c = hal::I2C::i2c1(
        pac.I2C1,
        pins.gpio2.into_function(), // sda
        pins.gpio3.into_function(), // scl
        400.kHz(),
        &mut pac.RESETS,
        125_000_000.Hz(),
    );

    let mut bme280 = BME280::<
        I2C<
            I2C1,
            (
                Pin<Gpio2, FunctionI2c, PullDown>,
                Pin<Gpio3, FunctionI2c, PullDown>,
            ),
        >,
    >::new(i2c, DEVICE_ADDRESS);

    // 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 (mut temp, mut humi, mut 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秒毎に温度、湿度、気圧の値が表示されれば成功です。

解説

概略説明します。

マクロ

「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 を入れてあげることでパスが通ります。

トレイト

I2CInterfaceという構造体にInterfaceトレイトを実装しています。
更に I2Cのembedded-halの Read, Write, WriteRead トレイトで制約をかけています。
ラズピコのHALの構造体にはこれらが実装されているので、そのまま使うことができます。

impl<IF> Interface for I2CInterface<IF>

例えば rp2040-hal-0.9.0\src\i2c\controller.rs でこれらトレイトの実装の確認ができます。

SPIInterfaceという構造体をつくり、Interfaceトレイトを実装し、このトレイトのメソッドを実装することで今度はSPIで接続できるだろうという考えです。

BME280

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

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

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

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

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

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

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

GitHubにアップ

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

コードの出来はともかく今回のプロジェクトは初心者の方が構成を学ぶには良い教材になるのではないかと思っています。

私もまだまだ初心者ですけれど・・・

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

I2Cカテゴリの最新記事