Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

はじめに

knurling-rs ロゴ

はじめに

本書は、組み込み Rust を始めるためのガイド付きプロジェクトと知識をまとめたものです。

本書の使い方

本書はいくつかのセクションで構成されています。

  • knurling-sessions プロジェクトの手順
  • リファレンス資料
  • 知識
  • トラブルシューティングページ

四半期ごとの knurling-sessions プロジェクトのいずれかをビルドし、その過程でできるだけ多くの組み込みに関する知識を身につけるには、各プロジェクトの手順を順番に進めてください。リファレンス資料セクションには、プロジェクトをゼロから始めるための、より一般的な手順が含まれています。knurling-sessions プロジェクトの手順の一部は、この部分に依存しています。その場合は、該当するセクションへのリンクを示します。知識セクションには、用語集と、組み込みの概念についてより深く学ぶための記事が含まれています。トラブルシューティングページには、プロジェクトのセットアップ時によくある問題に対するヘルプが含まれています。

使用しているアイコンと書式

本書では、さまざまな種類の情報を示すためにアイコンを使用しています。

  • ✅ アクションの呼びかけ
  • ❗️ 警告、特に注意が必要な詳細
  • 🔎 知識。主題をより深く理解するためのものですが、先に進むために完全に理解している必要はありません。
  • 💬 アクセシビリティのための説明

注: このような注には役立つ情報が含まれています

コース資料

Knurling-sessions は、可能な限りインクルーシブであることを目指しています。これは、一部の情報が、たとえば画像とテキストによる説明のように、複数の形式で利用できることを意味します。また、さまざまな種類の情報が一目で視覚的に区別できるように、アイコンも使用しています。対応できていないアクセシビリティ上のニーズがある場合は、お知らせください。

Knurling Sessions 2020 Q4: CO2センサーの構築

このプロジェクトに必要なすべてのハードウェア

最初のKnurling-sessionsプロジェクトでは、CO2センサーを段階的に構築していきます。これは電子工作や組み込みRustに取り組む方法を探る楽しい方法であるだけでなく、最終的には、特に在宅勤務をしている場合に役立つ便利な助っ人にもなります。CO2濃度は、集中力や意思決定能力に大きな影響を与え、それらは思っているよりも早く低下します!私たちが構築するデバイスは、窓を開けるタイミングを知らせてくれます。

最初の1か月は、組み込み開発とRustの基礎を学びます。SCD30センサーは2か月目に実装します。3か月目は、組み込みグラフィックスとディスプレイについて扱います。

サンプルコード

サンプルコードとこの本のソースは、https://github.com/knurling-rs/knurling-session-20q4にあります。

コントリビュート

不明な点がある場合や、この本への提案がある場合は、open an issueするか、PRを送ってください!

準備

この章では、knurling-sessions、標準ハードウェア、インストールガイドに関する情報を扱います。

標準ハードウェア

Knurling Sessions 2020 Q4 では、開発ボードとして nRF52840 Development Kit (DK) を使用することを想定しています。probe-run でサポートされている他のボードを使用することもできますが、提供されている手順に対して自分でいくつかの変更を加える必要がある場合があります。

DK は PC に 1 本の micro-USB ケーブルで接続する必要があります(充電専用ではなくデータケーブルであることを確認してください)。ハードウェアの確認の章で、より詳しい情報を提供します。

追加の周辺機器と部品がいくつか必要になります。全体としては次のとおりです。

部品表

ブロック 1: 組み込み Rust を始める

nrf52840-dk

  • 1x nrf52840-DK(または他の nrf52XXX ボード)
  • 1x RGB-LED(または単色 LED、および/またはオンボード LED)
  • 3x 220 Ohm 抵抗
  • 1x ポテンショメータ
  • 1x ブレッドボード
  • 1x ジャンパーワイヤー(40 本)- ピン to ピン
  • 1x ジャンパーワイヤー(40 本)- ピン to レセプタクル
  • 1x ジャンパーワイヤー(40 本)- レセプタクル to レセプタクル

ブロック 2: CO2 センサーの追加

scd30

: Sensirion ガスセンサーのヘッダーを接続するには、このステップでハンダ付けが必要です。別の方法として、ハンダ付けの代わりに、Adafruit が提供しているような「Hook Probes」を使用できる場合があります。これらは Aliexpress などの Web サイトからまとめて入手できることもよくあります。

  • 1x Sensirion SCD30 CO2 センサー(または他の空気品質センサー)
  • 1x ピンヘッダー(40 個)
  • 1x 圧電ブザー

ブロック 3: Embedded Graphics

waveshare display

: Waveshare ディスプレイのヘッダーを接続するには、このステップでハンダ付けが必要です。別の方法として、ハンダ付けの代わりに、Adafruit が提供しているような「Hook Probes」を使用できる場合があります。これらは Aliexpress などの Web サイトからまとめて入手できることもよくあります。

  • 1x Waveshare 4.2 インチ 白黒 ePaper Display

ハードウェアの確認

nRF52840 Development Kit (DK)

micro USBケーブルの一方の端をボードのUSBコネクタ J2 に接続し、もう一方の端をPCに接続します。

💬 これらの手順では、コンポーネント(スイッチ、ボタン、ピン)が上を向くように、ボードを「水平」に持っていることを前提としています。この状態で、凸形状の短辺が右を向くようにボードを回転させます。左端にUSBコネクタ(J2)が1つ、下端に別のUSBコネクタ(J3)が1つ、右下の角に4つのボタンがあります。

nRF52840 Development Kit (DK) のラベル付き図

DKをPC/ラップトップに接続すると、次のように表示されます。

Windows: リムーバブルUSBフラッシュドライブ(JLINKという名前)として表示され、さらにデバイス マネージャーのポート セクションにUSBシリアル デバイス(COMポート)として表示されます

Linux: lsusb の下にUSBデバイスとして表示されます。このデバイスのVIDは 0x1366、PIDは 0x1015 です。lsusb の出力では 0x プレフィックスは省略されます。

$ lsusb
(..)
Bus 001 Device 014: ID 1366:1015 SEGGER 4-Port USB 2.0 Hub

このデバイスは、/dev ディレクトリにも ttyACM デバイスとして表示されます。

$ ls /dev/ttyACM*
/dev/ttyACM0

macOS: FinderではリムーバブルUSBフラッシュドライブ(JLINKという名前)として表示され、ioreg -p IOUSB -b -n "J-Link" を実行すると「J-Link」という名前のUSBデバイスとしても表示されます。

$ ioreg -p IOUSB -b -n "J-Link"
(...)
  | +-o J-Link@14300000  <class AppleUSBDevice, id 0x10000606a, registered, matched, active, busy 0 $
  |     {
  |       (...)
  |       "idProduct" = 4117
  |       (...)
  |       "USB Product Name" = "J-Link"
  |       (...)
  |       "USB Vendor Name" = "SEGGER"
  |       "idVendor" = 4966
  |       (...)
  |       "USB Serial Number" = "000683420803"
  |       (...)
  |     }
  |

このデバイスは、/dev ディレクトリにも tty.usbmodem<USB Serial Number> として表示されます。

$ ls /dev/tty.usbmodem*
/dev/tty.usbmodem0006834208031

このボードには、動作を設定するためのスイッチがいくつかあります。箱から出した状態の設定が、必要な設定です。上記の手順がうまくいかなかった場合は、Buttonの説明表記が水平になるようにボードを置き、ボード上のスイッチの位置を確認してください。

  • 上端右側の角にあるスイッチSW6は、DEFAULT位置に設定されています。これは2つの可能な位置のうち右側の位置です(nRF = DEFAULT)。
  • ボード中央より少し上かつ右にあるスイッチSW7は、Def.位置に設定されています。これは2つの可能な位置のうち右側の位置です(TRACE = Def.)。このスイッチはKaptonテープで保護されていることに注意してください。
  • 下端左側の角にあるスイッチSW8は、ON位置に設定されています。これは2つの可能な位置のうち左側の位置です(Power = ON)。
  • 左端のUSBコネクタ(J2)の右側にあるスイッチSW9は、VDD位置に設定されています。これは3つの可能な位置のうち中央の位置です(nRF power source = VDD)。
  • 下端左側の角、SW8スイッチの右側にあるスイッチSW10は、OFF位置に設定されています。これは2つの可能な位置のうち左側の位置です(VEXT -> nRF = OFF)。このスイッチはKaptonテープで保護されていることに注意してください。

インストール手順

Rust とツール

基本的な Rust のインストール

https://rustup.rs にアクセスし、指示に従ってください。

Windows: C++ ビルドツールパッケージのオプションコンポーネントは必ずインストールしてください。インストールサイズは最大 2 GB のディスク容量を使用する可能性があります。

probe-run

probe-run は、組み込みアプリをネイティブアプリのように実行できるカスタム Cargo ランナーです。バージョン v0.1.4 以降をインストールしてください:

$ cargo install probe-run

cargo-generate

cargo-generate は、あらかじめ定義された任意のテンプレートから新しい Rust プロジェクトを生成します。次のようにインストールしてください:

$ cargo install cargo-generate

flip-link をインストールします

cargo install flip-link

Rust Analyzer

Visual Studio Code を使用している場合は、開発中の支援として Rust Analyzer をインストールすることをお勧めします。

Windows: git がインストールされていないというメッセージが表示された場合でも、無視して問題ありません!

OS 固有の依存関係

Linux のみ: 非 root ユーザーとして USB デバイスにアクセスする

一部のツールは pkg-configlibudev.pc に依存しています。適切なパッケージがインストールされていることを確認してください。Debian ベースのディストリビューションでは、次を使用できます:

$ sudo apt-get install libudev-dev libusb-1.0-0-dev

非 root ユーザーとして USB デバイスにアクセスするには、次の手順に従ってください:

  1. 表示されている内容で次のファイルを作成します。ファイルを作成するには root 権限が必要です。
$ cat /etc/udev/rules.d/50-knurling.rules
# 非 root ユーザーとして USB デバイスへのアクセスを許可する udev ルール

# nRF52840 Development Kit
ATTRS{idVendor}=="1366", ATTRS{idProduct}=="1015", TAG+="uaccess"
  1. 新しい udev ルールを有効にするために、次のコマンドを実行します
$ sudo udevadm control --reload-rules

Windows では、nRF52840 Development Kit の USB デバイスを WinUSB ドライバーに関連付ける必要があります。

そのためには、(前と同じように)micro-USB ポート J2 を使用して nRF52840 DK を PC に接続し、Zadig ツールをダウンロードして実行します。

Zadig のグラフィカルユーザーインターフェイスで、

  1. 上部の Options メニューから ‘List all devices’ オプションを選択します。

  2. デバイスの(上部)ドロップダウンメニューから “BULK interface (Interface 2)” を選択します

  3. そのデバイスを選択すると、USB ID フィールドに 1366 1015 が表示されるはずです。これは Vendor ID - Product ID のペアです。

  4. ターゲットドライバー(右側)として ‘WinUSB’ を選択します

  5. “Install WinUSB driver” をクリックします。完了までに数分かかる場合があります。

CO2センサープロジェクト

ハードウェアの確認が済んだので、CO2センサーの構築を始めましょう!

このプロジェクトに必要なすべてのハードウェア

ブロック 1: ハードウェアを使った基礎

Hello World

これは、knurling app templatehello.rs の例を、単に hello world をログ出力するだけのものから、nrf52840-DK 上のオンボード LED を点滅させるものへ拡張するためのステップバイステップガイドです。

この実装例はこちらにあります: 1_hello_extended.rs

プロジェクトのセットアップ

プロジェクト名を考え、私たちのガイドに従って app template を生成してください。TODO に適切な情報を入力するのを忘れないでください。

リソースへのアクセスを取得する

  1. 生成された app-template フォルダーで、src/bin/hello.rs に移動します。
  2. 次のモジュールをスコープに取り込みます:
#![allow(unused)]
fn main() {
use nrf52840_hal::{
    self as hal,
    gpio::{p0::Parts as P0Parts, Level},
    Timer,
};
}

nrf52840_hal クレートは Hardware Abstraction Layer (HAL) であり、GPIO ピンやタイマーなど、ボードのリソースにアクセスするのに役立ちます。 別のマイクロコントローラーを使用する場合は、TIMER ペリフェラル、オンボード LED 用のピン、およびピンの Level にアクセスできる必要があります。

  1. fn main() に次の行を追加して、ボードのすべてのペリフェラルへのアクセスを取得します:
#![allow(unused)]
fn main() {
let board = hal::pac::Peripherals::take().unwrap();
}

別のボードを使用する場合は、すべてのペリフェラルにアクセスする方法について、そのクレートのドキュメントを確認してください。

  1. LED を点滅させるにはタイマーが必要です。これは、LED が一定時間オンおよびオフになるためです。タイマーペリフェラルにアクセスするには、次の行を追加します:
#![allow(unused)]
fn main() {
let mut timer = Timer::new(board.TIMER0);
}
  1. オンボード LED を使用したい場合は、それらにアクセスする方法を調べる必要があります。ボードのデータシートを確認して、どの GPIO ピンに接続されているかを調べてください。nrf52840-DK については、情報はこちらにあります。

オンボード LED は P0 ピンの一部です。LED1 は p0.13 です。このピンのグループへのアクセスを取得するには、次の行を追加します:

#![allow(unused)]
fn main() {
let pins = P0Parts::new(board.P0);
}

ライトをオンにする

  1. ピン p0.13 を Low Level のプッシュプル出力として設定します:
#![allow(unused)]
fn main() {
let mut led_1 = pins.p0_13.into_push_pull_output(Level::Low);
}
  1. embedded-hal クレートは、ボードのモデルに依存せずにボードのさまざまなリソースへアクセスするための汎用 API を提供します。これにより開発が容易になり、コードの移植性が高まります。これを使用してピンを high または low に設定したり、Timer の遅延を設定したりします。

これにアクセスするには、Cargo.toml に依存関係として追加します

 # Cargo.toml
 [dependencies]
 cortex-m = "0.6.3"
 cortex-m-rt = "0.6.12"
 # TODO(4) ここに HAL を入力します
 nrf52840-hal = "0.11.0"
+embedded-hal = "0.2.4"

 [features]

そして、hello.rs で、その DelayMsOutputPin トレイトをスコープに取り込み、使用できるようにします:

#![allow(unused)]
fn main() {
use embedded_hal::{
    blocking::delay::DelayMs,
    digital::v2::OutputPin,
};
}
  1. main() 関数に 1000 ミリ秒の遅延を追加します:
#![allow(unused)]
fn main() {
timer.delay_ms(1000u32);
}
  1. プログラムを実行してください!

マイクロコントローラー上の LED1 が 1 秒間点灯するはずです。その後、プログラムは終了します。

LED を点滅させる

  1. ループを開始します:
#![allow(unused)]
fn main() {
loop {

};
}
  1. ループ内に次の行を追加します:
#![allow(unused)]
fn main() {
    led_1.set_high().unwrap();
    timer.delay_ms(1000u32);
    led_1.set_low().unwrap();
    timer.delay_ms(1000u32);
}
  1. プログラムを実行します。

LED1 が継続的に点滅するはずです。

外部 RGB LED

必要なコンポーネント

  • RGB LED 1個
  • 220オーム抵抗 3本
  • ブレッドボード
  • ジャンパーケーブル

❗️❗️❗️ RGB LED にはコモンアノードとコモンカソードがあります。手元の部品箱から取り出しただけの場合は、LED の電球部分が最も長い脚に対してどのように見えるかを比較してください。

コモンアノードかコモンカソードか?

購入リストにある LED はコモンアノード RGB LED です。ここではひとまず、両方のタイプについて手順を示します。

配線

❗️❗️❗️ 配線を始める前に、ブレッドボードと nRF52840-DK がどの電源にも接続されていないことを確認してください。

コモンアノード RGB LED

この構成の配線を示す方法を 2 つ用意しています。

回路図は、どの部品がどのようにつながっているかを示すことに重点を置いています。この種の図では、部品の物理的な見た目や、実際の物理空間で部品がどのように配置されているかは考慮されません。

外部 RGB LED、コモンアノードの回路図

ブレッドボード図は、正しい配線を示しつつ、部品の見た目やブレッドボード上での配置に重点を置いています。

外部 RGB LED、コモンアノードのブレッドボード図

✅ どちらの図が RGB LED をどのように表しているかを比較してください。

✅ ブレッドボード図に従って部品を配線してください。LED の最も長い脚は VDD に接続します。片側に 1 本だけある脚が赤チャンネルで、反対側に青と緑のチャンネルがあります。

✅ ボードをコンピューターに接続する前に、配線を再確認してください。

コモンカソード RGB LED

この構成の配線を示す方法を 2 つ用意しています。

回路図は、どの部品がどのようにつながっているかを示すことに重点を置いています。この種の図では、部品の物理的な見た目や、実際の物理空間で部品がどのように配置されているかは考慮されません。

外部 RGB LED、コモンカソードの回路図

ブレッドボード図は、正しい配線を示しつつ、部品の見た目やブレッドボード上での配置に重点を置いています。

外部 RGB LED、コモンカソードのブレッドボード図

✅ どちらの図が RGB LED をどのように表しているかを比較してください。

✅ ブレッドボード図に従って部品を配線してください。LED の最も長い脚はグラウンドに接続します。片側に 1 本だけある脚が赤チャンネルで、反対側に緑と青のチャンネルがあります。

✅ ボードをコンピューターに接続する前に、配線を再確認してください。

コード

この実装の例は、こちらにあります: 2_hello_external_led.rs

Hello World の例と同じファイルで作業しても、そのコピーで作業してもかまいません。ここでは、p0 ピンにアクセスできるものとします。最初の例では、1 つのピンを設定しました。その 1 つのピンは、オンボード LED のうち 1 つにしかアクセスできないという意味で特別でした。ここでは 3 つの GPIO ピンが必要です。赤用に 1 つ、青用に 1 つ、緑用に 1 つです。

✅ 3 つの GPIO ピン P0.03、P0.04、P0.28 を プッシュプル出力 ピンとして設定してください。初期レベルは、コモンアノードの場合は High、コモンカソードの場合は Low です。

✅ 1000ms の遅延を追加してください。

✅ コードを実行してみてください。何かが点灯した場合は、別のタイプの LED 用のコードを書いたことになります。

各ピンで .set_high().unwrap(); または .set_low().unwrap(); メソッドを使用すると、LED の状態が変わります。

✅ チャンネルをオンまたはオフに切り替える、すべての可能な組み合わせを試してみてください。どのような色を作れますか?

✅ LED が自分で選んだ 2 色の間で点滅するループを作成してください。

内部温度センサー

この実装の例は、こちらにあります: 3_temperature.rs

✅ 準備: コード内でボードとタイマー周辺機器を初期化しておいてください。

ドライバーを作成する必要がある外部センサーを扱い始める前に、まずはボードの内部温度センサーにアクセスします。HAL を見て、周辺機器へのアクセスが詳細にはどのように機能するのか、また Rust でメソッドがどのように機能するのかを詳しく見ていきます。

nrf-hal-common 0.11.1 を開きます

/src/temp.rs を開きます。ここには、ボードの温度センサーとの通信が実装されています。

統合された温度センサーは構造体です: pub struct Temp(TEMP)。外部から呼び出せるようにする必要があるため、public である必要があります。TEMP は peripheral access crate (pac) で定義されている型で、温度センサーのレジスタブロックにアクセスします。impl ブロック内には、Temp に対して定義されたすべてのメソッドがあります。

メソッドは、オブジェクトに結び付けられているという点で関数とは異なります。詳しく見てみましょう。

pub fn new()TEMP を引数として受け取り、Temp を返します。このメソッドは温度センサーのレジスタブロックの所有権を取得します。

✅ コード内で Temp を使用できるようにするには、まずスコープに取り込む必要があります。次の行をコードに追加してください。

#![allow(unused)]
fn main() {
use nrf52840_hal::{
    self as hal,
    Temp,
    Timer,
};
}

board.TEMP を引数として new メソッドを呼び出し、温度センサーのレジスタブロックの所有権を取得します。この変数は mutable である必要があります。

#![allow(unused)]
fn main() {
let mut temp = Temp::new(board.TEMP);
}

これで温度センサーのインスタンスが得られたので、測定を行うことができます。

✅ HAL のコードにある temp.rs に戻ります。

fn measure() は、self への mutable な参照を引数として受け取ります。self は、fn new() で作成された温度センサーのインスタンスです。このメソッドは、すでに測定が開始されている場合はその測定を停止し、新しい測定を開始して、測定が完了するまでプログラムをブロックし、その後で固定小数点数 I30F2 を返します。もう 1 つの選択肢は、fn start_measurement() で測定を開始し、fn read() で測定値を読み取る方法で、これはノンブロッキングに動作します。測定は、レジスタに書き込むことで開始または停止されます。

ここではひとまず、ブロッキングメソッドである fn measure() を使います。

✅ コードに、測定を行う行と、温度値をログ出力する行を追加してください。

#![allow(unused)]
fn main() {
let temperature = temp.measure();
defmt::info!("{:?}", temperature);
}

この構文は、メソッドがオブジェクトに結び付けられていることを反映しています。引数 &mut self はドットの前にあるオブジェクトを指し、括弧の中は空のままです。

この時点でコードを実行すると、コンパイラーエラーが発生します。これは、fn measure() の戻り値の型である I30F2 に対して the trait defmt::Format is not implemented for I30F2 だからです。

fn measure() の後ろに、さらに to_num() メソッドを追加します。このメソッドは固定小数点数を f32 にキャストします。表示できるようにするには、フォーマット文字列で型を指定する必要があります。

#![allow(unused)]
fn main() {
let temperature: f32 = temp.measure().to_num();
defmt::info!("{=f32} °C", temperature);
}

✅ 60 秒ごとに温度を測定して表示するループを初期化します。

メソッドの詳細

このセクションでは、独自のメソッドを記述します。

この実装の例は、こちらにあります: 4_external_led_methods.rs

ここでは、共通アノード RGB LED を使用していると想定します。共通カソード RGB LED を使用する場合、highlow の設定は逆になります。

✅ 外部 RGB LED 用のコードに戻ってください。

各チャンネルの Level を個別に設定する代わりに、3 つのチャンネルすべてを含む型と、RGB LED の振る舞いを定義するメソッドを定義できます。

✅ GPIO とピン設定をスコープに取り込んでください。

#![allow(unused)]
fn main() {
use nrf52840_hal::{
    self as hal,
    gpio::{
        p0::{Parts as P0Parts, P0_03, P0_04, P0_28},
        Level, Output, PushPull,
    },
    Timer,
};
}

fn main() の上に、各チャンネルに 1 つずつ、3 つのフィールドを持つ struct を定義してください。各チャンネルはそれぞれ独自の型を持ちます!

#![allow(unused)]
fn main() {
struct LEDState {
    r: P0_03<Output<PushPull>>,
    b: P0_04<Output<PushPull>>,
    g: P0_28<Output<PushPull>>,
}
}

struct LEDState の下に、その struct 用の impl ブロックを作成してください。

#![allow(unused)]
fn main() {
impl LEDState {
    // 空
}
}

メソッドには 2 種類あります: 静的メソッドインスタンスメソッド です。静的メソッドは一般に、インスタンスのコンストラクターとして使用されます。これらは :: 構文で呼び出されます。インスタンスメソッドはオブジェクトによって呼び出されるため、そのオブジェクトへの参照を引数として持ちます。これらはドット構文で呼び出されます。

impl ブロック内に、struct を構築する静的メソッドを作成してください。メソッドの前半ではピンを設定し、後半では struct を作成して、それを返します。

#![allow(unused)]
fn main() {
fn init(pins: P0Parts) -> LEDState {
    let mut led_red = pins.p0_03.into_push_pull_output(Level::High);
    let mut led_blue = pins.p0_04.into_push_pull_output(Level::High);
    let mut led_green = pins.p0_28.into_push_pull_output(Level::High);

    LEDState {
        r: led_red,
        b: led_blue,
        g: led_green,
    }
}
}

fn main() 内で、ピンを設定している 3 行を、この静的メソッドの呼び出しに置き換えてください。

- let mut led_red = pins.p0_03.into_push_pull_output(Level::High);
- let mut led_blue = pins.p0_04.into_push_pull_output(Level::High);
- let mut led_green = pins.p0_28.into_push_pull_output(Level::High);

+ let mut light = LEDState::init(pins);

これで、LED の振る舞いを制御するさまざまなインスタンスメソッドを定義できるようになりました。例として、1000ms 間隔で LED を赤色光から青色光へ切り替える、このコード片をリファクタリングします:

#![allow(unused)]
fn main() {
loop {
    led_red.set_low().unwrap();
    led_blue.set_high().unwrap();

    timer.delay_ms(1000_u32);

    led_red.set_high().unwrap();
    led_blue.set_low().unwrap();

    timer.delay_ms(1000_u32);
    }
}

impl ブロックに戻ってください。赤チャンネルを low にし、他を high に設定するインスタンスメソッドを定義してください。

#![allow(unused)]
fn main() {
fn red(&mut self) {
    self.r.set_low().unwrap();
    self.g.set_high().unwrap();
    self.b.set_high().unwrap();
}
}

このメソッドは、LEDState のインスタンスの可変参照を引数として受け取ります。&mut selfself: &mut Self の短縮形です。struct のフィールドには . 構文でアクセスできます。

✅ 同じ方法で、青チャンネルを high にし、他を low に設定するメソッドを定義してください。

fn main() に戻り、loop の中の行を対応する関数呼び出しに置き換えてください。

- led_red.set_low().unwrap();
- led_blue.set_high().unwrap();
+ light.red();

  timer.delay_ms(1000_u32);

- led_red.set_high().unwrap();
- led_blue.set_low().unwrap();
+ light.blue();

  timer.delay_ms(1000_u32);

✅ この点滅 loop を、呼び出し可能なメソッドにしてください。

現在、LED 用のピンはハードコードされています。これにより、2 つ目の LED に対してコードを再利用しにくくなっています。この部分のコードをリファクタリングしましょう。

Pin 型と prelude::* モジュールをスコープに取り込んでください。

#![allow(unused)]
fn main() {
use nrf52840_hal::{
    prelude::*, 
    gpio::{
        Level, 
        Output, 
        PushPull, 
        Pin,
    }, 
    Timer,
};
}

✅ struct 定義で、特定のピンを Pin 型に置き換えてください。

#![allow(unused)]
fn main() {
struct LEDColor {
    r: Pin<Output<PushPull>>,
    b: Pin<Output<PushPull>>,
    g: Pin<Output<PushPull>>,
}
}

init メソッドを変更し、受け取るピンが任意の番号付きピンであり、さらに任意の設定でもよいようにしてください。このメソッドは、LEDColor struct をインスタンス化するときに、ピンを high レベルのプッシュプル出力に設定します。

ジェネリック型パラメーター <Mode> に注意してください。これは引数の型宣言で使用できるように、関数名の直後で宣言する必要があります。<Mode> は、未知のピン設定のプレースホルダーです。

#![allow(unused)]
fn main() {
pub fn init<Mode>(led_red: Pin<Mode>, led_blue: Pin<Mode>, led_green: Pin<Mode>) -> LEDColor {

    LEDColor {
        r: led_red.into_push_pull_output(Level::High),
        b: led_blue.into_push_pull_output(Level::High),
        g: led_green.into_push_pull_output(Level::High),
    }
}
}

✅ コードが動作するように、fn main() の行を書き換えてください。

#![allow(unused)]
fn main() {
let led_channel_red = pins.p0_03.degrade();
let led_channel_blue = pins.p0_04.degrade();
let led_channel_green = pins.p0_28.degrade();

let mut light = LEDColor::init(led_channel_red, led_channel_blue, led_channel_green);
}

ユーザーの追加 - 入力

このセクションでは、ボタンを動作させて、ハードウェアと対話できるようにすることに焦点を当てます!

この実装例は、こちらにあります: 5_led_with_buttons.rs

ボード上のボタンは、オンボード LED と同じように番号付きのピンです。それらのピンは p0.11p0.12p0.24p0.25 です。

p0 関連を含む gpio モジュールをスコープに取り込み、p0 ピンにアクセスできるようにする行を fn main() に追加してください。

✅ ボタン用の型と静的メソッドを作成してください。この静的メソッドは任意の設定のピンを受け取り、それらをプルアップ入力に変換します。

#![allow(unused)]
fn main() {
pub struct Button(Pin<Input<PullUp>>);

impl Button {
    fn new<Mode>(pin: Pin<Mode>) -> Self {
        Button(pin.into_pullup_input())
    }
}
}

✅ ボタンのインスタンスを作成してください。

#![allow(unused)]
fn main() {
let button_1 = Button::new(pins.p0_11.degrade());
}

効果を得るには、まずボタンの状態を知る必要があります。ボタンは押されているのでしょうか、それとも押されていないのでしょうか?次に、イベントをボタンの状態に結び付ける必要があります。

impl Button ブロック内に、ボタンが押されている場合に true を返すインスタンスメソッドを実装してください。

#![allow(unused)]
fn main() {
pub fn is_pressed(&self) -> bool {
    self.0.is_low().unwrap()
}
}

struct Button には名前付きフィールドがないことに注意してください。関連する型にアクセスするには、0 でインデックス指定します。

fn main() 内に、オンボード LED の 1 つを実装してください。

✅ ボタンが押されているときは LED が点灯し、ボタンが押されていないときは消灯するように、プログラムを書き続けてください。

ユーザー入力の追加 - 応用

ボタンを押して温度の単位を変換する

ユーザー体験は非常に単純です。ボタンが押されている間はプログラムがある処理を行い、ボタンが押されていないときは別の処理を行います。温度の表示方法を切り替えるような、ボタンを押したときに一度だけイベントを発生させたい場合は、これがより複雑になります。

この実装の例はこちらにあります: 8_temp_unit_convert_buttons.rs

✅ 前章のファイルから始めます。

✅ 次のリソースをスコープに取り込みます。

#![allow(unused)]
fn main() {
use nrf52840_hal::{
    self as hal,
    gpio::{p0::Parts as P0Parts, Input, Pin, PullUp},
    prelude::*,
    Temp, 
    Timer,
};
}

温度を定期的に更新しながら、温度を表示する単位を切り替えられるようにしたいとします。プログラムの動作の一部は現在選択されている単位に依存するため、その単位を追跡しておく必要があります。

温度の表示方法には、一般的に Celsius、Kelvin、Fahrenheit の 3 つがあります。これらは同じ概念の 3 つのバリアントなので、この型には enum を使うのが適しています。

struct Button の前に次の enum を追加します。

#![allow(unused)]
fn main() {
enum Unit {
    Fahrenheit,
    Celsius,
    Kelvin,
}
}

センサーは温度を摂氏度で出力します。

fn main() に移動します。ループの前に、現在の表示単位を設定する変数を追加します。

#![allow(unused)]
fn main() {
let mut current_unit = Unit::Celsius;

loop {
    // ...
}

}

struct に対してメソッドを定義できるのと同じように、enum に対してもメソッドを定義できます。

enum Unit に、match 文を含むメソッドを追加します。各 match アームで、温度値を対応する単位へ変換する処理を実装します。

#![allow(unused)]
fn main() {
impl Unit {
    fn convert_temperature(&self, temperature: f32) -> f32 {
        match self {
            Unit::Fahrenheit => {
                // 温度を変換して返す
            },

            Unit::Kelvin => {
                // 温度を変換して返す
            },

            Unit::Celsius => {
                // 温度をそのまま返す
            },
        };
    }
}
}

次に、ボタンを押したときに単位を変更する処理を実装する必要があります。

✅ fn main() に移動します。ループ内で match 文を使い、現在の単位に応じて、ボタンが押された場合に別の単位へ切り替えます。単位が変更されたことを示すログ文を追加します。

#![allow(unused)]
fn main() {
if button_1.is_pressed() {
    current_unit = match current_unit {
        Unit::Fahrenheit => Unit::Kelvin,
        Unit::Kelvin => Unit::Celsius,
        Unit::Celsius => Unit::Fahrenheit,
    };
    defmt::info!("Unit changed");
};
}

✅ プログラムを実行します。ボタンを押すと、連続したログ出力が表示されるはずです。

✅ periodic timer インスタンスを実装します。通常のタイマーの代わりにこのタイマーを使います。

#![allow(unused)]
fn main() {
let mut periodic_timer= Timer::periodic(board.TIMER0);
}

✅ ループ内で、センサーから温度を読み取った後、current_unit に対して convert_temperature メソッドを呼び出し、新しい変数に束縛します。その後に match 文を置き、正しい単位を表示して温度値をログに出力します。

#![allow(unused)]
fn main() {
loop {
    let temperature: f32 = temp.measure().to_num();
    let converted_temp = current_unit.convert_temperature(temperature);

    match current_unit {
        Unit::Fahrenheit => defmt::info!("{=f32} °F", converted_temp),
        Unit::Kelvin => defmt::info!("{=f32} K", converted_temp),
        Unit::Celsius => defmt::info!("{=f32} °C", converted_temp),
    };
    if button_1.is_pressed() {
        // ...
    };       
}
}

✅ プログラムを実行します。

これにより、現在の単位で温度を表示するログ出力が多数発生するはずです。ボタンを 1 回押すと単位が何度も変更されるため、特定の単位へ意図的に変更することは不可能です。

✅ ループの末尾に 100 ms の遅延を追加します。

#![allow(unused)]
fn main() {
loop {
    // ... 

    if button_1.is_pressed() {
        // ...
    };
    periodic_timer.delay_ms(100_u32);        
}
}

✅ プログラムを実行します。

プログラムは一応やりたいことを実行しますが、ユーザー体験はかなりひどいものです。改善しましょう。

ここまでの実装例はこちらにあります: 7_temp_convert_button_noisy.rs

最初のステップは、求める動作をもう少し詳しく定義することです。3 つの要素を見てみましょう。

人間の視点から見たボタンの状態

ボタンは 4 つの状態を取り得ます。

  1. 押されている
  2. 押されていない
  3. 押されている状態から押されていない状態へ遷移中
  4. 押されていない状態から押されている状態へ遷移中

これらの状態をもう少し二値的に定義するには、ボタンが直前にどの位置にあり、今どの位置にあるのかを考えます。

以前現在
1.押されている押されている
2.押されていない押されていない
3.押されている押されていない
4.押されていない押されている

機械の視点から見たボタンの状態

人間の視点では非常に単純に見えますが、ハードウェア上でボタンの状態が何を意味するのかを判断するのは、もう少し複雑です。理論上、ボタンを押すと信号が変化しますが、この変化は多くの場合それほどきれいではなく、むしろノイズを含みます。特にボタンが古くなるとそうなります。この挙動を補正することを、ボタンの debouncing と呼びます。ソフトウェアでは、ボタンの 4 つの状態を追跡するステートマシンを用意し、押されたボタンが押されたボタンとして扱われるのは、導電性のほこりの粒が邪魔をしたことによる突然の信号スパイクではなく、一定時間押されている場合である、と定義することで実現できます。

システム変更の持続性

ボタンを実装するのは、人々がシステムと対話し、ボタンを押すことでシステムの動作を変更できるようにしたいからです。この変更は、ボタンが押されている間だけ存在し、ボタンを離すことで終了するものにすることもできますし、ボタンを押すことで開始され、ボタンを離しても持続するものにすることもできます。

プログラムの動作はどうあるべきか?

ボタンを押すことで、温度を表示する単位を変更したいとします。その変更は、ボタンを離した後も持続する必要があります。単位変換のトリガーイベントとして、ボタンが「押されている」状態から「押されていない」状態へ遷移するタイミングの 1 つを使います。ボタンの遷移を検出するために、プログラムはボタンの過去の状態を追跡します。温度は 1000ms ごとに表示されるべきです。

ボタンの動作を改善する

✅ button struct に、ボタンの過去の状態を bool で追跡するフィールドをもう 1 つ追加します。初期状態は false です。

以前の匿名 struct にフィールドが追加された点に注意してください。この変更は、この struct に対して実装されているメソッドにも反映する必要があります。

#![allow(unused)]
fn main() {
struct Button {
    pin: Pin<Input<PullUp>>,
    was_pressed: bool,
}
}

impl Button ブロックに、信号の立ち上がりエッジを検出するメソッドを追加します。

  • ボタンの現在の状態を読み取る
  • 現在の状態を、ボタンの struct に保存されている過去の状態と比較する。
  • ボタンが押されていたが、現在は押されていない場合に true を返す。
  • ボタンの過去の状態を更新する。
#![allow(unused)]
fn main() {
fn check_rising_edge(&mut self) -> bool {

    let mut rising_edge = false;

    let is_pressed = self.is_pressed();
    // 信号の「立ち上がりエッジ」でのみトリガーする
    // 用語: 「エッジトリガー」
    if self.was_pressed && !is_pressed {
        // 押されていたが、今は押されていない:
        rising_edge = true;
    }
    self.was_pressed = is_pressed;
    rising_edge
}
}

fn main() に移動します。ボタンのピンを mutable として宣言します。is_pressed メソッドを check_rising_edge() に置き換えます。

#![allow(unused)]
fn main() {
let mut button_1 = Button::new(pins.p0_11.degrade()); 

loop {
    // ...
    if button_1.check_rising_edge() {
        // ...
    }
    // ...
}
}

✅ プログラムを実行します。

ボタンをどれだけ長く押しても、単位は一度だけ変更されます。100 ms 以内にボタンを複数回押さなければ、すべての操作が登録されます。しかし、ログ出力はまだ計画より 10 倍多く、ボタンのタイミングも理想的ではありません。

タイミング

人間によるボタン操作をすべて検出し、ボタンの状態を登録するには、ボタンの状態をかなり頻繁に読み取る必要があります。ハードウェアからのノイズを除去するには、約 5 ms ごとにボタンを読み取れば十分です。ここでは、意図的な操作と見なせる十分な長さの立ち上がりエッジを検出したいと考えています。ボタン押下の立ち下がりエッジの後、ボタン解放の立ち上がりエッジに反応することで、その信号が意図的であることをさらに確実にできます。

大まかに見ると、実装は次のようになります。タイマーは 1000 マイクロ秒までカウントアップします。1000 µs が経過するたびに、経過ミリ秒を追跡するカウンターが更新されます。経過ミリ秒数が 5 で割り切れ、かつ立ち上がりエッジが検出された場合、単位が変更されます。経過ミリ秒数が 1000(1 秒)で割り切れるたびに、温度がログに記録されます。

ここでは、カウンターがどの型の符号なし整数であるかが重要です。その型の最大値に達すると問題が発生します。参考までに、u32 のカウンターは 49.7 日後に上限に達し、u64 のカウンターは 267844497 年後に上限に達します。

✅ タイマーインスタンスの後に、経過ミリ秒を追跡する変数を追加します。

#![allow(unused)]
fn main() {
let mut periodic_timer= Timer::periodic(board.TIMER0);
let mut millis: u64 = 0;
}

✅ ループ内で、タイマーを最大値 1000 µs で開始します。ボタンの更新と温度のログ出力の制御フローを実装します。次に、ループの各反復後に経過マイクロ秒のカウンターへ 1 を加算する行を追加します。

#![allow(unused)]
fn main() {
loop {
    periodic_timer.start(1000u32);

    if (millis % 1000) == 0 {
        defmt::info!("Tick (milliseconds): {=u64}", millis);
        // 温度を測定する
        // 温度を表示する
    };
    if (millis % 5) == 0 {
        // ボタンの状態を読み取って更新する
    };

    millis = millis.saturating_add(1);
}
}

✅ コードを実行します。

ループ全体の実行が 1000 µs 未満で完了するため、温度は依然として 1000 ms ごとよりもはるかに頻繁にログ出力されます。つまり、実際にその時間が経過する前に、経過マイクロ秒数が増加しています。プログラムを正しいタイミングにするには、その数を増やす前に、1000 µs が経過するまでループの実行をブロックする必要があります。

cargo.toml ファイルに移動します。

✅ クレート nb = "1.0.0" をインポートします。

✅ プログラムファイルに戻り、そのクレートとその block モジュールをスコープに取り込みます。

#![allow(unused)]
fn main() {
use nb::block;
}

✅ ミリ秒数をインクリメントする前に、次の行を追加します。これにより、ノンブロッキングのカウンターは 1000 µs までカウントアップするまでブロッキングになります。

#![allow(unused)]
fn main() {
block!(periodic_timer.wait()).unwrap();
}

✅ プログラムを実行します。ボタンを押すのを楽しんでください!

すべてを統合する

LED を快適温度インジケーターとして使用する

あなたは以下を学びました。

  • RGB LED の点灯と配線。
  • 温度センサーの使用。
  • ユーザー入力の実装

個人的に快適だと感じる温度付近を、異なる光の色で示すプログラムを構築してください。

この実装例はこちらにあります: 9_comfy_temp_indicator.rs

あなたが最も快適だと感じる温度は何度ですか? 最も快適だと感じる 2 度 (°C) の範囲を定義してください。その範囲の上下 2 度までの温度は許容範囲であり、この 6 度の範囲外の温度は暑すぎる、または寒すぎます。各ゾーンに信号色を割り当ててください。範囲は自由に調整してかまいません。

✅ LED のこの動作を最後のプログラムに統合してください。

1 つのファイルに多くのコードを書いてきました。これにより、全体が把握しにくくなり、コードの再利用も難しくなります。再利用する可能性が高いコードをモジュールに入れてリファクタリングしましょう。

src/ の中に dk_button という名前の新しいフォルダーを作成してください。

dk_button の中に mod.rs という名前のファイルを作成してください。

struct Button の定義とその impl ブロックを src/bin/thermometer から dk_button/mod.rs に移動してください。

✅ 必要なすべてのモジュールをスコープに取り込んでください。

✅ 他のファイルからアクセスできるように、すべてのメソッドとすべての struct または enum の定義の前に pub を追加してください。

scr/lib.rs に次の行を追加して、このモジュールをエクスポートしてください。

#![allow(unused)]
fn main() {
pub mod dk_button;
}

src/bin/comfy_temp_indicator で、dk_button モジュールをスコープに取り込んでください。

#![allow(unused)]
fn main() {
use knurling_session_20q4::{
    dk_button, 
};
}

✅ ボタンをインスタンス化するための静的メソッドを呼び出している行を変更し、そのメソッドが dk_button モジュールから呼び出されるようにしてください。

#![allow(unused)]
fn main() {
let mut button_1 = dk_button::Button::new(pins.p0_11.degrade());
}

✅ 同じ方法で、LED 関連のコード用に rgb_led モジュールを作成し、単位変換用に number_representations モジュールを作成してください。単位変換のメソッドは、所有権が不要なため、temperature への参照だけを受け取るように変更するのが理にかなっています。

ブロック 2: 独自のドライバーを書く

Hello, Sensor!

大まかに言うと、これから作成するドライバーは、バイト列の形式でさまざまなコマンドをセンサーに送信できるようになります。コマンドに応じて、センサーはプロセスを開始または終了したり、データを返したりします。SCD30 は 3 つの異なるプロトコルを使用できますが、ここでは I2C を使用します。

この実装の例は、こちらで確認できます: 10_scd_30_log_v.rs

配線

SCD30 の配線用ブレッドボード図

前提条件

プログラム内で、次のリソースにアクセスできるようにしてください。

  • Timer
  • P0 Pins
  • 1 LED

I2C リソースのセットアップ

使用するリソースは twim と呼ばれます。Twim と I2C は同一のプロトコルで、違いは後者が商標登録されており、前者はそうではないという点です。

✅ 次のモジュールをスコープに取り込みます。

#![allow(unused)]
fn main() {

// ボードペリフェラルへのアクセス:
use nrf52840_hal::{
    self as hal,
    gpio::{
        p0::{
            Parts as P0Parts
        }, 
        Level,
    },
    prelude::*,
    Temp, 
    Timer,
    twim::{self, Twim, Error, Instance},
};
}

fn main() で、2 つのピンを floating として設定します。1 つはデータ信号(SDA)用、もう 1 つはクロック信号(SCL)用です。

#![allow(unused)]
fn main() {
let scl = pins.p0_30.degrade();
let sda = pins.p0_31.degrade();
}

✅ ピンを twim::Pins としてインスタンス化します。

#![allow(unused)]
fn main() {
let pins = twim::Pins { scl, sda };
}

Twim インスタンスを作成します。このメソッドは 3 つの引数を取ります: TWIM ペリフェラル、pins、周波数です。

#![allow(unused)]
fn main() {
let i2c = Twim::new(board.TWIM0, pins, twim::Frequency::K100);
}

✅ プログラムの最後に点滅ループを追加します。これは、プログラムが実行中であることを視覚的に出力する方法です。

#![allow(unused)]
fn main() {
loop {
        timer.delay(250_000);
        led_1.set_high().unwrap();
        timer.delay(250_000);
        led_1.set_low().unwrap();
    }
}

✅ プログラムを実行します。LED が点滅するはずです。

I2C インスタンスを構築したので、センサーのインターフェイスに接続する必要があります。そのためには、センサーのアドレスが必要です。

Interface Description でセンサーのアドレスを見つけ、fn main() の上にグローバル変数 DEFAULT_ADDRESS として追加します。

回答
```rust
pub const DEFAULT_ADDRESS: u8 = 0x61;
```

✅ センサー用のモジュール scd30 を作成します。 src/scd30/mod.rs 内に、Twim<T> の型エイリアスとして匿名構造体を作成します。

#![allow(unused)]
fn main() {
pub struct SCD30<T: Instance>(Twim<T>);

impl<T> SCD30<T> where T: Instance {
    /// impl ブロック
}
}

<T> とは何でしょうか?

I2C は Twim<T> という型を持っており、T はジェネリック型パラメーターで、struct 内で定義する必要があります。ジェネリック型 <T> が関数引数の型宣言の一部である場合、関数名の直後に指定する必要があります。その struct に対してメソッドを実装する場合も、<T> を指定して定義する必要がありますが、これは impl ブロックの開始行で行います。

impl ブロック内に、SCD30 のインスタンスを返す静的メソッドを作成します。

#![allow(unused)]
fn main() {
impl<T> SCD30<T> where T: Instance {

    pub fn init(i2c2: Twim<T>) -> Self {
        SCD30(i2c2)
    }

    /// その他のメソッド
}
}

次に、センサーインスタンス上で使用できるメソッドを作成します。このメソッドの目的は、センサーのファームウェアバージョン番号を読み取れるようにするコマンドをセンサーに書き込むことです。これを行うには、センサーの Interface Description でいくつかの情報を見つける必要があります。

✅ ファームウェアバージョンを読み取るための I2C コマンドを見つけます。

回答
I2C `0xD100`

✅ 実際にセンサーへ書き込む必要があるメッセージシーケンスを見つけます。

回答
Start 0xC2 0xD1 0x00 Stop

Start シンボルの後には、これが *write* メッセージであることを示すバイト `0xC2` があります。このバイトは、センサーのアドレス 0x61 を 1 ビット左シフトしたものです。これはこれから使用する `write()` メソッドにすでに実装されているため、今は無視できます。 

✅ センサーから読み取られるメッセージを見つけます。実際の情報内容の長さは何バイトですか?

回答 読み取られるメッセージ:
Start 0xC3 0x03 0x42 0xF3 Stop

*read* バイトに注目してください。これもセンサーのアドレスから 1 ビットシフトしただけのものです。*read* バイトの後には、3 バイトと Stop シンボルがあります。3 バイトのうち、最初がメジャーバージョン番号、2 番目がマイナーバージョン番号、最後が CRC バイトです。CRC は cyclic redundancy check の略で、生データの偶発的な変更を検出します。したがって、実際の情報の長さは 2 バイトです。

`read()` メソッドは、*read* バイトに続くすべてのバイトのバイト配列を返します。 

✅ 例にある 16 進数のバイト表現からバージョン番号を計算します。

回答
|0x03|0x42|
|----|----|
|3   |66  |

したがって、この例のバージョン番号は 3.66 です。

このメソッドについて詳しく見ていきます。他のすべてのメソッドは、このテーマのバリエーションにすぎないためです。

SCD30 の impl ブロックにメソッドを追加します。

#![allow(unused)]
fn main() {
pub fn get_firmware_version(&mut self) -> Result<[u8; 2], Error> {
    let command:[u8; 2] = [0xd1, 0x00];
    let mut rd_buffer = [0u8; 2];
        
    self.0.write(DEFAULT_ADDRESS, &command)?;
    self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?;

    let major = u8::from_be(rd_buffer[0]);
    let minor = u8::from_be(rd_buffer[1]);
        
    Ok([major, minor]) 
}
}

このメソッドは self への可変参照を受け取り、Error バリアントと、2 つの符号なし 8 ビット整数の array を含む ok バリアントを持つ Result 型を返します。

関数本体の最初の行では、センサーに送信されるコマンドを含む 2 つの u8array を作成します。次に、0 で初期化された 2 つの u8 を含む空の読み取りバッファを作成します。これは、返されるバイトのうち最初の 2 バイトだけが必要だからです。CRC バイトは省くことができます。

次に、SCD30 上で write() メソッドを呼び出します。このメソッドは、アドレスとコマンドへの参照を引数として取ります。その後、アドレスと読み取りバッファへの可変参照を引数として read() メソッドを呼び出します。

最後の処理は、返されたバイトを 10 進数に変換し、それらを配列として返すことです。

✅ プログラムファイルに移動し、scd30 モジュールをスコープに取り込みます。

#![allow(unused)]
fn main() {
use knurling_session_20q4::scd30;
}

fn main() でセンサーインスタンス上のメソッドを呼び出し、センサーのファームウェアバージョンをログに記録します。

#![allow(unused)]
fn main() {
let firmware_version = sensor.get_firmware_version().unwrap();
defmt::info!("Firmware Version: {:u8}.{:u8}", firmware_version[0], firmware_version[1]);
}

プログラムを実行します。LED が点滅している間に、ログ出力としてバージョン番号が得られるはずです。

おめでとうございます!ハードウェアドライバーの最初の部分を作成しました!

計測を開始する

❗️❗️❗️ defmt の更新

先週、defmt v0.1.0 の crates.io 版が crates.io でリリースされました。プロジェクトを github 版の defmt から crates.io 版へ移行するための便利なguideを用意しました。app-template をベースにした新しいプロジェクトは、今後自動的に crates.io 版を使用します。

古い手順のリファクタリング:

すべてをまとめるの章と Hello Sensor の章を読み、コードをモジュールに分けてください


ファームウェアのバージョン番号を読み取ってログに記録し、通信が正しくセットアップされていることを確認したら、計測を開始します。

この実装の例は、こちらで確認できます: 11_scd_30_measure.rs

Interface Description のセクション 1.4.1 に進んでください。

連続計測をトリガーするために提供する必要があるメッセージコンポーネントは何ですか?

回答
0x00 コマンドバイト
0x10 コマンドバイト
0x00 引数: 周囲気圧
0x00 引数: 周囲気圧
0x81 CRC バイト

開始記号と停止記号は write メソッドによって自動的に提供されます。

このメッセージにはコマンドだけでなく、周囲気圧の値を設定できる引数も含まれています。

周囲気圧の役割

気圧は温度とともに、定義された空間内にどれだけの気体分子が存在するかを決定します。圧力が上がると分子の数は増え、圧力が下がると減ります。CO2 に対するセンサーの出力単位は ppm、つまり parts per million であり、空気全体に含まれる 100 万個の粒子(原子または分子)のうち、一定数が二酸化炭素分子であることを意味します。

非常に正確なセンサー読み取り値が必要な場合、周囲気圧の値は別のセンサーから取得するべきです。職場や教室向けの空気質モニターを作る場合は、値をハードコードすれば十分です。海面での標準気圧は 1013.25 mbar です。標高の高い場所に住んでいる場合は、地域の気象台で値を確認してください。

このチュートリアルでは、ベルリンの現在値である 1020 mbar を使用します。

連続計測を開始する

src/scd30/mod.rs に進んでください。impl SCD30 ブロック内に、引数として &mut selfpressure: u16 を受け取り、バリアント ()Error を持つ Result 型を返す新しい関数を追加します。

この関数内で、送信するメッセージの長さである 5 個の u8 バイト用の可変配列を定義します。引数バイトと crc バイトは 0x00 のままにします。

#![allow(unused)]
fn main() {
pub fn start_continuous_measurement(&mut self, pressure: u16) -> Result<(), Error> {
    let mut command: [u8; 5] = [0x00, 0x10, 0x00, 0x00, 0x00];
    // ...
    Ok(())
}
}

次に、引数をコマンドに埋め込みます。センサー通信はビッグエンディアンのバイト順で動作します。

u16 値をビッグエンディアンのバイト列に変換してください。返されたスライスに含まれる値を、コマンド内のそれぞれの位置に代入します。

#![allow(unused)]
fn main() {
let argument_bytes = &pressure.to_be_bytes();

command[2] = argument_bytes[0];
command[3] = argument_bytes[1];
}

CRC バイトの計算

2 バイトより長いメッセージを送信する場合、検証のために 2 バイトごとに CRC バイトを送信する必要があります。これらは引数バイトから計算する必要があります。

cargo.toml に進み、次の依存関係を追加してください。

#![allow(unused)]
fn main() {
crc_all = "0.2.0"
}

src/scd30/mod.rs に戻り、モジュールをスコープに取り込みます。

#![allow(unused)]
fn main() {
use crc_all::Crc;
}

crc_all のドキュメントを確認してください。インスタンスメソッド Crc::<u8>::new() にはどの引数が必要ですか? センサーの Interface Description のセクション 1.1.3 に進み、すべての引数を埋められるか確認してください。

回答
|引数|情報|
|-|-|
|poly: u8|0x31|
|width: uszise|8|
|init: u8|0xff|
|xorout: u8|0x00|
|reflect: bool|false|

pub fn start_continuous_measurement() の中で、集めた情報を使って新しい crc バイトをインスタンス化します。この変数は可変である必要があります。pressure 値で crc バイトを更新し、そのバイトをコマンド配列内の位置に代入します。コマンドをセンサーに送信します。

#![allow(unused)]
fn main() {
let mut crc = Crc::<u8>::new(0x31, 8, 0xff, 0x00, false);

crc.update(&pressure.to_be_bytes());
command[4] = crc.finish();

self.0.write(DEFAULT_ADDRESS, &command)?;
}

✅ プログラムファイルに移動します。fn main() で周囲気圧を設定し、計測を開始してください!

#![allow(unused)]
fn main() {
// 1020_u16 を地域の値に置き換えてください
let pressure = 1020_u16;


// ...
sensor.start_continuous_measurement(pressure).unwrap();

loop {
    ///...
}
}

✅ プログラムを実行してください。LED が点滅するはずです。

電源投入後、センサーはデータを読み取れるようになるまで約 2 秒かかります。値を提供するだけでなく、センサーはデータがまだ準備できているかどうかの情報も提供できます。

src/scd30/mod.rs で、data_ready メソッドを実装してください。コマンドと読み取りバッファの長さについては、インターフェース説明を確認してください。

回答
```rust
pub fn data_ready(&mut self) -> Result<bool, Error> {
let command: [u8; 2] = [0x02, 0x02];
let mut rd_buffer = [0u8; 3];

self.0.write(DEFAULT_ADDRESS, &command)?;
self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?;

Ok(u16::from_be_bytes([rd_buffer[0], rd_buffer[1]]) == 1)
}
```

✅ プログラムファイルで、点滅ループの前に新しいループを開き、データが準備できているかどうかをセンサーから継続的に読み取るようにします。メソッドが true を返したら “Data ready.” を出力するログ文を追加します。その後、ループを抜けます。

#![allow(unused)]
fn main() {
loop {
    if sensor.data_ready().unwrap() {
        defmt::info!("Data ready.");
        break
    }
}

✅ プログラムを実行してください。“Data ready” というログ出力が表示されるはずです。

センサーデータの読み取りとログ出力

センサーは 3 つの値を返します。1 つは二酸化炭素濃度、1 つは温度、1 つは湿度です。Interface Description のセクション 1.5 で、センサーがデータに使用する数値型を確認してください。

回答
センサーが返す値は、ビッグエンディアン形式の浮動小数点数です。

src/scd30/mod.rs に進んでください。各値に対応するフィールドを持つ新しい struct 定義を追加します。

#![allow(unused)]
fn main() {
pub struct SensorData {
    pub co2: f32,
    pub temperature: f32,
    pub humidity: f32,
}

pub const DEFAULT_ADDRESS: u8 = 0x61;
pub struct SCD30<T: Instance>(Twim<T>);

impl<T> SCD30<T>
where
    T: Instance,
{
    // ...
}
}

impl SCD30 ブロック内に、新しいメソッドを追加します。

#![allow(unused)]
fn main() {
pub fn read_measurement(&mut self) -> Result<SensorData, Error> {
    // ...
    Ok(data)
}
}
  • コマンドと読み取りバッファーの長さについては、Interface Descriptionを確認してください。
  • SensorData のインスタンスを作成します。
  • 関連するバイトを f32 値に変換します。ビッグエンディアンのバイト列から f32 への変換については、std ドキュメントを確認してください。
  • データを返します。
解答
#![allow(unused)]
fn main() {
pub fn read_measurement(&mut self) -> Result<SensorData, Error> {
    let command: [u8; 2] = [0x03, 0x00];
    let mut rd_buffer = [0u8; 18];

    self.0.write(DEFAULT_ADDRESS, &command)?;
    self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?;

    let data = SensorData {
        co2: f32::from_bits(u32::from_be_bytes([
            rd_buffer[0],
            rd_buffer[1],
            rd_buffer[3],
            rd_buffer[4],
        ])),
        temperature: f32::from_bits(u32::from_be_bytes([
            rd_buffer[6],
            rd_buffer[7],
            rd_buffer[9],
            rd_buffer[10],
        ])),
        humidity: f32::from_bits(u32::from_be_bytes([
            rd_buffer[12],
            rd_buffer[13],
            rd_buffer[15],
            rd_buffer[16],
        ])),
    };
    Ok(data)
}
}

✅ プログラムファイル内の点滅ループの中で、このメソッドを呼び出し、値とその単位をログに追加してください。

#![allow(unused)]
fn main() {
loop {
    let result = sensor.get_measurement().unwrap();

    let co2 = result.co2;
    let temp = result.temperature;
    let humidity = result.humidity;

    defmt::info!("
        CO2 {=f32} ppm
        Temperature {=f32} °C
        Humidity {=f32} %
        ", co2, temp, humidity
    );

    // LEDを点滅させる
}
}

✅ プログラムを実行すると、ログ出力に3つの値が表示されるはずです。

オプションの課題:

  • センサーの工場出荷時キャリブレーションはかなり優れていますが、センサーのキャリブレーション方法を調べ、必要なメソッドを実装することもできます。
  • 高度補正メソッドを実装し、圧力補正の代わりに使用してください。
  • センサーから取得した相対湿度値から絶対湿度を計算してください。
  • 露点を計算してください。
  • センサーの Interface Description に記載されている残りのメソッドを実装してください。

赤色警報!

この章は、nrf-hal の次のリリースで Pulse Width Modulation (PWM) を使用するようにリファクタリングされます

CO2 測定デバイスは、周囲の二酸化炭素濃度を測定して表示できるようになりました。この章では、さらにインタラクティブ性を追加します。このデバイスは値を表示するだけでなく、その量を分類し、それに応じて動作します。空気中の二酸化炭素について、通常レベル、警告レベル、上限値を定義します。すべてのレベルは LED で示され、上限値ではブザーによる音響警告も行われます。

配線

ブザーには 2 本の脚があり、一方をグラウンドに、もう一方をピンに接続します。複数のデバイスをボードに接続する場合は、それらすべてが同じグラウンドに接続されていることを確認してください。 この図では、ケーブルが交差しないようにピンが変更されています。以前のピン配置のままでもかまいません。

センサー、LED、ブザーの配線

実装

ブザー

ブザーは単純な仕組みで動作します。ピンとグラウンドに接続されています。何らかの電圧変化があると、ブザーが鳴ります。この変化の周波数が低いほど、ブザー音の音程は低くなります。

✅ ブザー用のモジュールを作成してください。次のメソッドを持つ型を作成してください。 * init: ピンを受け取り、初期レベル low の push pull output モードに設定します。 * high: ピンを high にします。 * low: ピンを low にします。 * buzz: 各切り替えの間に 1 ミリ秒の間隔を置いて、ピンを low と high に設定します。異なる間隔の長さを試してみてください。

fn main() でセンサーを初期化するときに、動作確認のためにブザーを 500 ms 鳴らしてください。

CO2 アラート

150 年以上にわたり、二酸化炭素濃度は室内空気の質を示す指標と見なされてきました。室内に人がいると二酸化炭素濃度が上昇し、それに伴って人からのその他の排出物(微生物やその他のガス)の濃度も上昇し、空気の質が低下します。二酸化炭素は測定しやすい代替指標であるだけでなく、このガス自体も認知機能に大きな影響を与え、濃度が高くなると健康にも影響します。空気の質に関してはさまざまな規制があります。自動換気を規定する DIN 13779 では、800 ppm 未満の値を最高の空気質を示すもの、800 ppm から 1000 ppm の値を中程度の品質、1000 ppm から 1400 ppm の値をやや不十分な品質、1400 ppm を超える値を低品質と見なしています。1000 ppm を超える値を低品質と見なす 150 年前の Pettenkofer number は、その根拠については時代遅れと見なされていますが、それでも有用な数値です。現在の推奨事項では、1000 ppm 未満の値をかなり安全、1000 ppm から 2000 ppm の値を感知できるレベル、2000 ppm を超える値を非衛生的と見なしています。コロナウイルスに感染する可能性を下げるためには、値を 1000 ppm 未満に保つことが強く推奨されます。

✅ センサーで達成したいことに応じて、CO2 の警告レベルと上限値を何にしますか?

✅ CO2 アラート用の新しいモジュールを作成してください。これには次のものが含まれます。

  • CO2 の警告レベルを定義します。
  • CO2 の上限値を定義します。
  • 現在の CO2 レベルがアラートかどうかを判定する関数を含みます。 値が
    • 警告レベル未満の場合、LED は緑です。
    • 警告レベルを超え、上限値未満の場合、LED は黄色です。
    • 上限値を超えた場合、LED は赤になり、ブザーが鳴ります。

ヘルプ:

モジュール内で他のモジュールにアクセスするには:

#![allow(unused)]
fn main() {
use crate::rgb_led::LEDColor;
use crate::buzzer::Buzzer;
}

ユーザーエクスペリエンス

次の実験を行ってください。二酸化炭素センサーが 2000 ppm を超える濃度を検出し、窓を開けた場合、濃度が 400 から 500 ppm の間のベースラインに戻るまでにどれくらい時間がかかりますか?

おそらく、予想より長い時間がかかります。その間ずっとブザーを鳴らし続けるのは煩わしく、目的を果たしません。人々は単に完全にオフにしてしまうからです。ベースラインに達したときに視覚的な信号だけでなく音響信号もあるのは理にかなっています。冬に長時間窓を開けたままにするのはエネルギーの無駄だからです。ここでは、設計に多くの自由度があります。

✅ 独自の信号スキームを考えてください。発生し得るイベントは何ですか?それらについてどのように通知されたいですか?

いくつかのアイデアを示します。

イベント:

  • 湿度の値が低い(暖房を多く使う冬に関連)
  • 二酸化炭素のベースライン(500 ppm 未満)
  • 非常に高い二酸化炭素レベル
  • 湿度の値が高い(暖房されていない部屋でのみ関連)
  • サーモスタットで制御されたヒーターがなく、省エネルギーをしたい場合の温度アラート。

通知:

  • 通知は常にオンである必要はありません。リマインダーとして機能し、一定時間が経過しても状況が変わらない場合に再度オンにできます
  • LED は点滅させたり、常時点灯させたりでき、すべて異なる色にできます
  • ブザーは異なる周波数で鳴らしたり、1 つの信号内で周波数を変えたりできます。
  • 低い周波数は警告感が弱いものの信号としては機能し、高い周波数は緊急感を生み出します。

その他のアイデア:

  • スヌーズボタン
  • データが未準備/準備完了であることを示すインジケーター

✅ あなたのスキームを実装してください!

パニック!

エラーには、回復可能なエラーと回復不能なエラーの 2 種類があります。回復不能なエラーとは、システムをそれ以上動作できない状態にしてしまうエラーです。今日は、このエラーに焦点を当てます。

発生するエラーをシミュレートするために、コードにエラーを入れます(後で元に戻します)。

src/scd30/mod.rs に移動します。DEFAULT_ADDRESS0x61 から 0x62 に変更します。

#![allow(unused)]
fn main() {
pub const DEFAULT_ADDRESS: u8 = 0x62;
}

この変更により、センサーと開発ボード間の通信ができなくなります。

開発ボードが最初にセンサーと通信しようとするのは、センサーのファームウェア番号を読み取るときです。このメソッドを見てみましょう。

#![allow(unused)]
fn main() {
pub fn get_firmware_version(&mut self) -> Result<[u8; 2], Error> {
        let command: [u8; 2] = [0xd1, 0x00];
        let mut rd_buffer = [0u8; 2];

        self.0.write(DEFAULT_ADDRESS, &command)?;
        self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?;

        let major = u8::from_be(rd_buffer[0]);
        let minor = u8::from_be(rd_buffer[1]);

        Ok([major, minor])
    }
}

このメソッドは Result 型を返します。操作の結果に応じて、Result には異なる値が入ります。操作が成功した場合、センサーのファームウェア番号である [u8; 2] 配列が入ります。操作が成功しなかった場合、write() または read() メソッドからのエラーが伝播されて返されます。

プログラムではこのメソッドを呼び出し、unwrap() を追加します。

#![allow(unused)]
fn main() {
let firmware_version_result = sensor.get_firmware_version().unwrap();
}

unwrap() を使用すると、成功時には値を返し、エラー時にはプログラムをパニックさせます。これは、エラーの発生が想定されない場合、またはそのエラーがいずれにせよ回復不能な場合にのみ有用です。今回のケースではこの両方が当てはまるとしても、パニックを手動で処理することにはいくつかの利点があります。そのいくつかを見てみましょう。

✅ プログラムを実行します。

次のような表示になるはずです。

0.000000 ERROR panicked at 'called `Result::unwrap()` on an `Err` value: AddressNack', src/bin/13_scd_30_error_handling.rs:61:28
└─ panic_probe::print_defmt::print @ /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:140
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
      <exception entry>
   1: __udf
   2: cortex_m::asm::udf
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.3/src/asm.rs:105
   3: rust_begin_unwind
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:75
   4: core::panicking::panic_fmt
        at /rustc/5c1f21c3b82297671ad3ae1e8c942d2ca92e84f2/src/libcore/panicking.rs:101
   5: core::option::expect_none_failed
        at /rustc/5c1f21c3b82297671ad3ae1e8c942d2ca92e84f2/src/libcore/option.rs:1272
   6: _13_scd_30_error_handling::__cortex_m_rt_main
   7: main
        at src/bin/13_scd_30_error_handling.rs:19
   8: ResetTrampoline
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   9: Reset
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550
The terminal process "/bin/bash '-c', 'cargo run --package knurling-session-20q4 --bin 13_scd_30_error_handling'" terminated with exit code: 134.

Terminal will be reused by tasks, press any key to close it.

最初の行では、unwrap() が呼び出されたときにパニックが発生したこと、およびそれが発生した行が通知されています。メッセージの残りを読んでも、それ以上の情報は分かりませんが、7: で、これが main() の内部で発生したことが分かります。

✅ 次の行を置き換えます。

#![allow(unused)]
fn main() {
let firmware_version = sensor.get_firmware_version().unwrap();
}

次のコードブロックに置き換えます。

#![allow(unused)]
fn main() {
let firmware_version_result = sensor.get_firmware_version();

let firmware_version = match firmware_version_result {
    Ok(version_number) => version_number,

    Err(error) => {
        panic!("Error getting firmware version: {:?}", error)
    }
};
}

unwrap() を呼び出す代わりに、matchResult を処理します。エラーの場合、何が起こるかをこちらで決められるようになります。それでもパニックを呼び出すことはできます。

✅ プログラムを実行します。

次のような表示になるはずです。

0.000000 ERROR panicked at 'Error getting firmware version: AddressNack', src/bin/13_scd_30_error_handling.rs:67:13
└─ panic_probe::print_defmt::print @ /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:140
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
      <exception entry>
   1: __udf
   2: cortex_m::asm::udf
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.3/src/asm.rs:105
   3: rust_begin_unwind
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:75
   4: core::panicking::panic_fmt
        at /rustc/5c1f21c3b82297671ad3ae1e8c942d2ca92e84f2/src/libcore/panicking.rs:101
   5: _13_scd_30_error_handling::__cortex_m_rt_main
        at src/bin/13_scd_30_error_handling.rs:67
   6: main
        at src/bin/13_scd_30_error_handling.rs:19
   7: ResetTrampoline
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   8: Reset
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550
The terminal process "/bin/bash '-c', 'cargo run --package knurling-session-20q4 --bin 13_scd_30_error_handling'" terminated with exit code: 134.

Terminal will be reused by tasks, press any key to close it.

エラーメッセージにこちらの部分を追加することで、将来のユーザーはコード行を確認しなくても、プログラムが失敗した理由に加えて、何が失敗したのかも分かるようになります。カスタムエラーメッセージは便利ですが、エラー発生箇所の位置は正確とは言えません。パニックを呼び出した場所はエラーが発生した場所と同じではなく、現在引用されているコード行は panic! が呼び出された行だからです。しかし、これをさらに改善できます!

✅ 上に示したコードブロックを次の行に置き換えます。

#![allow(unused)]
fn main() {
let firmware_version = sensor.get_firmware_version()
    .unwrap_or_else(|error| {
        panic!("Error getting firmware version: {:?}", error)
    });
}

unwrap() を呼び出す代わりに、unwrap_or_else() を呼び出します。unwrap() がエラーの場合にパニックするのに対し、unwrap_or_else() は引数としてクロージャを受け取ることができ、match 文で Result を処理する場合と同じ追加機能を提供できます。

✅ プログラムを実行します。

次のような表示になるはずです。

0.000000 ERROR panicked at 'Error getting firmware version: AddressNack', src/bin/13_scd_30_error_handling.rs:63:5
└─ panic_probe::print_defmt::print @ /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:140
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
      <exception entry>
   1: __udf
   2: cortex_m::asm::udf
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.3/src/asm.rs:105
   3: rust_begin_unwind
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:75
   4: core::panicking::panic_fmt
        at /rustc/5c1f21c3b82297671ad3ae1e8c942d2ca92e84f2/src/libcore/panicking.rs:101
   5: _13_scd_30_error_handling::__cortex_m_rt_main::{{closure}}
        at src/bin/13_scd_30_error_handling.rs:63
   6: core::result::Result<T,E>::unwrap_or_else
   7: _13_scd_30_error_handling::__cortex_m_rt_main
        at src/bin/13_scd_30_error_handling.rs:61
   8: main
        at src/bin/13_scd_30_error_handling.rs:19
   9: ResetTrampoline
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
  10: Reset
        at /Users/tanks/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550
The terminal process "/bin/bash '-c', 'cargo run --package knurling-session-20q4 --bin 13_scd_30_error_handling'" terminated with exit code: 134.

Terminal will be reused by tasks, press any key to close it.

Resultmatch で処理する解決策と比べると、少なくともスタックバックトレースでは、単に panic! が呼び出された場所だけでなく、エラーが発生した正確な場所も確認できます。

より詳細なエラーメッセージは便利ですが、私たちはログ出力用のホストマシンなしで実行できるはずの組み込みデバイス上でプログラムしています。独自のパニックハンドラを書くことで、このような場合のための「エラーメッセージ」を提供できます。存在しないホストへログメッセージを送っても役に立たないためです。

scr/rgb_led/mod.rs に移動してください。次のメソッドを追加します。

#![allow(unused)]
fn main() {
pub fn error_blink_red(&mut self, timer: &mut Timer<TIMER0, OneShot>) {
    for _i in 0 ..10 {
        self.red();
        timer.delay_ms(200_u32);
        self.off();
        timer.delay_ms(200_u32);
    }
}
}

呼び出されると、LED は赤色で比較的高速に 10 回点滅します。

panic! を呼び出す直前に、RGB LED でこのメソッドを呼び出してください。

#![allow(unused)]
fn main() {
let firmware_version = sensor.get_firmware_version()
    .unwrap_or_else(|error| {
    led_indicator.error_blink_red(&mut timer);
        panic!("Error getting firmware version: {:?}", error)
    });
}

✅ コードを実行してください。

プログラムがパニックする前に、RGB LED が数回赤く点滅するようになります。これにより、何か問題が発生し、デバイスを再起動する必要があること、またはさらなる診断のためにホストへ接続する必要があることをユーザーに知らせます。

src/scd30/mod.rs に移動してください。DEFAULT_ADDRESS を元の値 0x61 に戻します。

#![allow(unused)]
fn main() {
pub const DEFAULT_ADDRESS: u8 = 0x61;
}

電子ペーパーディスプレイ

Hello, e-Paper ディスプレイ!

センサープロジェクトに e-Paper ディスプレイを追加する前に、まずは単体で試してみます。1_hello_extended.rs を基にした新しいファイルから始めてください。

配線

✅ 開発ボードからすべてのジャンパーワイヤーを取り外します。ただし、ブレッドボードはそのままにしておきます。

✅ ePaper ディスプレイの以下のワイヤーを、それぞれ対応するピンに接続します。

名前ピン
vccvdd
gndgnd
dinp1.01
clkp1.02
csオレンジp1.03
dcp1.04
rstp1.05
busyp1.06

コード

Cargo.toml

✅ 次の依存関係を cargo.toml に追加します。

#![allow(unused)]
fn main() {
epd-waveshare = "0.4.0"
embedded-graphics = "0.6.2"
}

SPIM のインスタンス化

spimp1 モジュールをスコープに取り込みます。

#![allow(unused)]
fn main() {
// ボード周辺機能へのアクセス:
use nrf52840_hal::{
    self as hal,
    gpio::{p0::Parts as P0Parts, p1::Parts as P1Parts, Level},
    prelude::*,
    spim::{self, Spim},
    Timer,
};
}

✅ ピンを次のように設定します。

#![allow(unused)]
fn main() {
let din = pins_1.p1_01.into_push_pull_output(Level::Low).degrade();
let clk = pins_1.p1_02.into_push_pull_output(Level::Low).degrade();
let cs = pins_1.p1_03.into_push_pull_output(Level::Low);
let dc = pins_1.p1_04.into_push_pull_output(Level::Low);
let rst = pins_1.p1_05.into_push_pull_output(Level::Low);
let busy = pins_1.p1_06.into_floating_input();
}

din はデータライン、clk はクロックです。どちらもフローティングである必要があります。 cs は chip select の略で、dc は data/command control pin、rst は reset の略です。これらはすべて、初期レベルが Low のプッシュプル出力でなければなりません。 busy は入力として設定されます。これは、ディスプレイがビジーかどうかを通信できるチャネルです。

SPI プロトコルは、クロックを持つという点で I2C と似ていますが、周辺デバイスとの間で送受信されるデータには MISOMOSI という 2 つの異なるチャネルを使用します。データはディスプレイに送信されるだけで、ディスプレイからは送信されないため、ここでは MOSI ラインのみを使用します。

✅ SPIM ピンを設定し、SPIM 周辺機能の新しいインスタンスを作成します。

#![allow(unused)]
fn main() {
let spi_pins = spim::Pins {
    sck: clk,
    miso: None,
    mosi: Some(din),
};


let mut spi = Spim::new(board.SPIM3, spi_pins, spim::Frequency::K500, spim::MODE_0, 0);
}

✅ プログラムを実行して、ビルドできることを確認します。この時点では、ディスプレイは何もしないはずです。

ePaper ディスプレイのインスタンス化

✅ 次のモジュールをスコープに取り込みます。

#![allow(unused)]
fn main() {
use epd_waveshare::{
    epd4in2::*,
    graphics::Display,
    prelude::*,
};
}

✅ タイマーのインスタンスを追加します。 ✅ 4.2 インチ E Paper ディスプレイの新しいインスタンスを作成します。 ✅ デフォルトのディスプレイを追加します。

#![allow(unused)]
fn main() {
// ePaper をインスタンス化
let mut delay = Timer::new(board.TIMER1);
let mut epd4in2 = EPD4in2::new(&mut spi, cs, busy, dc, rst, &mut delay).unwrap();

let mut display = Display4in2::default();
}

✅ プログラムを実行して、ビルドできることを確認します。この時点で、e-paper ディスプレイは何度か暗くなってから明るく戻ります。

ePaper ディスプレイへの描画

✅ 次のモジュールをスコープに取り込みます。

#![allow(unused)]
fn main() {
use embedded_graphics::{
    geometry::Point,
    pixelcolor::BinaryColor,
    prelude::*,
    primitives::{ Circle, Triangle },
    style::PrimitiveStyle,
};
}

✅ waveshare e-Paper ディスプレイは二値カラーシステムを持っています。色は On または Off のどちらかです。On は黒を意味し、Off は白を意味します。

ディスプレイに描画する方法の 1 つは、プリミティブな図形を使用することです。この crate は、円、三角形、長方形、線を提供しています。それぞれは重要な点と距離によって定義されます。たとえば、円は中心と半径によって定義されます。

各図形は塗りつぶすことも、エッジの周囲の線だけで表すこともできます。ディスプレイのコンテンツを定義するたびに、ディスプレイバッファーに追加する必要があります。これは draw メソッドで行います。

✅ 次の図形定義、2 つの円と 1 つの三角形をプログラムに追加します。

#![allow(unused)]
fn main() {
let c1 = Circle::new(Point::new(171, 110), 30)
    .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
    .draw(&mut display);

let c2 = Circle::new(Point::new(229, 110), 30)
    .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
    .draw(&mut display);

let t1 = Triangle::new(Point::new(259, 120), Point::new(141, 120), Point::new(200, 200))
    .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
    .draw(&mut display);
}

draw() メソッドを使うだけでは、実際に画面に何かを表示するには不十分です。図形をディスプレイに表示するには、spi 接続を介してフレームを更新し、フレームを表示する必要があります。

✅ 次の行をコードに追加します。

#![allow(unused)]
fn main() {
epd4in2.update_frame(&mut spi, &display.buffer()).unwrap();
epd4in2.display_frame(&mut spi)
    .expect("新しいグラフィックのフレームを表示");
}

✅ コードを実行します。2 つの円と 1 つの三角形で構成されたシンボルが表示されるはずです。

センサーデータを表示する

このセクションでは、センサーデータを ePaper ディスプレイに表示します。ディスプレイには、大きめのフォントでタイトル「Air Quality」を表示し、その下に数値、その値、単位を小さめのフォントで表示します。

この機能を 12_scd_30_alert.rs から実装していきます。

配線

✅ ブレッドボードの + ワイヤーを開発ボードの VDD に、- ワイヤーを GND に再接続します。

✅ その他のすべてのケーブルを、開発ボード上のそれぞれ対応するピンに接続します。

✅ ePaper Display は十分な電流を得るために、専用の電源が必要です。もう一方の VDDGND に接続します。

ブレッドボードと ePaper ディスプレイを別々の VDD/GND に配線する

SPIM をインスタンス化する

これは前章の繰り返しです。 自分で試して、どれだけ学んだか確認してみましょう!方法を覚えていない場合は、前のに戻ってください。

✅ コードを実行して、すべてビルドできることを確認します。ディスプレイには新しいものは何も表示されないはずです。

静的テキストを表示する

✅ 新しいモジュール display_helper を追加します。

display_helper/mod.rs 内で、次のリソースをスコープに取り込みます。

#![allow(unused)]
fn main() {
use epd_waveshare::{
    epd4in2::*,
};
use embedded_graphics::{
    egtext, 
    fonts::{Font12x16, Font24x32, Text},
    geometry::Point,
    pixelcolor::BinaryColor,
    prelude::*,
    style::TextStyle,
    text_style, 
};
}

最初のステップは、タイトルや表示される測定値の名前など、静的なままのテキストを書くことです。この関数は、ディスプレイ型である Display4in2 を可変引数として受け取り、それを返します。

組み込みフォントを使った基本的なテキストは、基本図形と似た方法で追加されます。ディスプレイ上に配置するには、次のものが必要です。

  • 文字列
  • テキストが始まる位置の左上座標

この場合、シンプルなビットマップフォントの 6 種類のサイズと、スタイルとして色をオンまたはオフにする選択肢があります。タイトル用のフォントは、残りのテキスト用のフォントより大きくします。

✅ 次の関数をモジュールに追加します。

#![allow(unused)]
fn main() {
pub fn draw_text (mut display: Display4in2 ) -> Display4in2 {
    Text::new("Air Quality", Point::new(20, 30))
        .into_styled(TextStyle::new(Font24x32, BinaryColor::On))
        .draw(&mut display).unwrap();

    Text::new("Carbon Dioxide:", Point::new(20, 90))
        .into_styled(TextStyle::new(Font12x16, BinaryColor::On))
        .draw(&mut display).unwrap();
    
    Text::new("Temperature:", Point::new(20, 130))
        .into_styled(TextStyle::new(Font12x16, BinaryColor::On))
        .draw(&mut display).unwrap();

    Text::new("Humidity:", Point::new(20, 170))
        .into_styled(TextStyle::new(Font12x16, BinaryColor::On))
        .draw(&mut display).unwrap();
    
    display
}
}

✅ メインファイルに移動します。

✅ 測定用の loop の中に、ディスプレイのインスタンスを追加し、その後に fn draw_text の呼び出しを追加します。

#![allow(unused)]
fn main() {
let display = Display4in2::default();

let display = display::draw_text(display);
}

✅ ディスプレイがバッファに書き込まれた内容を実際に表示するようにするため、フレームを更新して表示する次の行を追加します。

#![allow(unused)]
fn main() {
epd4in2.update_frame(&mut spi, &display.buffer()).unwrap();
epd4in2.display_frame(&mut spi).expect("display frame new graphics"); 
}

✅ プログラムを実行します。タイトルに続いて 3 行のテキストが表示されるはずです。

動的テキストを表示する

静的テキストは比較的単純ですが、プログラムの実行中に変化すると想定される値をフォーマット文字列を使って表示するのは、[no_std] 環境では動的メモリ割り当てがないため、少し複雑です。フォーマット文字列を使用するために、固定サイズの配列と文字列を提供する arrayvec クレートを使います。

cargo.toml[dependencies] セクションに次の行を追加します。

#![allow(unused)]
fn main() {
arrayvec = {version = "0.5.2", default-features = false }
}

display_helper/mod.rs 内で、次のリソースをスコープに取り込みます。

#![allow(unused)]
fn main() {
use arrayvec::ArrayString;
use core::fmt::Write;
}

✅ 次の関数をモジュールに追加します。

#![allow(unused)]
fn main() {
pub fn draw_numbers (value: f32, unit: &str, position: (i32, i32), mut display: Display4in2 ) -> Display4in2 {
    
    // 内容

    display

}
}

pub fn draw_numbers は、測定値の値、その単位、測定値を表示する位置の top_left 座標、および display を引数として受け取ります。ディスプレイが返されます。

次のステップは、書き込みバッファ buf として固定サイズの ArrayString を作成することです。このバッファは、フォーマットされた動的データを保存するために必要です。表示したい文字数以上の文字を格納できる必要があります。

✅ 書き込みバッファを作成し、write! マクロを使って、小数点以下 2 桁までの値と単位を含むフォーマット文字列を、書き込みバッファ buf に書き込みます。

#![allow(unused)]
fn main() {
let mut buf = ArrayString::<[_; 12]>::new();

write!(&mut buf, "{:.2} {}", value, unit).expect("Failed to write to buffer");
}

次に、egtext! マクロを使って、テキストを display バッファに書き込みます。

✅ 次の行を pub fn draw_numbers に追加します。

#![allow(unused)]
fn main() {
egtext!(
    text = &buf,
    top_left = position,
    style = text_style!(
        font = Font12x16,
        text_color = BinaryColor::On,
    )
)
.draw(&mut display).unwrap();
}

✅ メインプログラムファイルに移動します。

fn main() の直前に、次の定数を定義します。

#![allow(unused)]
fn main() {
const CO2_POSITION: (i32, i32) = (220, 90);
const CO2_UNIT: &str = "ppm";

const TEMP_POSITION: (i32, i32) = (220, 130);
const TEMP_UNIT: &str = "°C";

const HUMIDITY_POSITION: (i32, i32) = (220, 170);
const HUMIDITY_UNIT: &str = "%";
}

これらの定数は、数値を表示する位置とその単位を設定します。

fn main 内の測定用の loop の中で、センサーから値を読み取った後に、各測定値について fn draw_numbers を呼び出します。

#![allow(unused)]
fn main() {
let display = display_helper::draw_numbers(co2, CO2_UNIT, CO2_POSITION, display);
let display = display_helper::draw_numbers(temp, TEMP_UNIT, TEMP_POSITION, display);
let display = display_helper::draw_numbers(humidity, HUMIDITY_UNIT, HUMIDITY_POSITION, display);
}

✅ プログラムを実行します。ePaper には、静的テキストの横にタイトル、数値、単位が表示されるはずです。

ePaper ディスプレイに表示されたデータ

ディスプレイは非常に頻繁に、とても派手に更新されます。これを減らすために、ループの最後の遅延を変更します。ePaper ディスプレイは各更新に約 4 秒必要なので、それより頻繁に測定しても意味がありません。

✅ 遅延を 2000 ms から 30000ms に変更します。

#![allow(unused)]
fn main() {
timer.delay_ms(30000_u32);
led_1.set_high().unwrap();
timer.delay_ms(30000_u32);
led_1.set_low().unwrap();
}

✅ プログラムを実行します。同じ出力が表示されるはずですが、画面は 1 分ごとにのみ更新されます。

ディスプレイは更新時にまだ点滅しますが、この点やその他のより見た目に関する問題は、1 月に提供されるこのプロジェクトの最後の更新で対処されます。

参考資料

プロジェクトのセットアップ

この章には、ツールのインストールとプロジェクトのセットアップに関するガイドが含まれています。

defmt のセットアップ

セットアップを簡単にするために、Cargo プロジェクトテンプレートを作成しました。 セットアップ手順は次のとおりです。

  1. probe-run の v0.1.4(またはそれ以降)をインストールします。これは、組み込みアプリをネイティブアプリであるかのように実行できる、カスタム Cargo runner です。
$ cargo install probe-run
  1. cargo-generate でプロジェクトテンプレートを初期化するか、コピーを取得して Cargo.toml を手動で初期化します。
$ cargo generate \
    --git https://github.com/knurling-rs/app-template \
    --branch main \
    --name my-app

cargo-generate を使用しない場合は、Cargo.tomlauthor フィールドと name フィールドを手動で入力する必要があります。

 # Cargo.toml
 [package]
-# authors = ["{{authors}}"]
-# name = "{{project-name}}"
+name = "my-app"

その後、テンプレートには TODO がいくつかあります。 ripgrep を使って TODO という語を検索し(rg TODO .cargo .)、それらを見つけることができますが、このブログ記事ではそのすべてを順を追って説明します。

  1. probe-run --list-chips からチップを選び、それを .cargo/config.toml に入力します。

たとえば、私たちのワークショップのいずれかで使用した nRF52840 Development Kit を持っている場合は、{{chip}}nRF52840_xxAA に置き換えます。

 # .cargo/config.toml
 [target.'cfg(all(target_arch = "arm", target_os = "none"))']
-runner = "probe-run --chip {{chip}} --defmt"
+runner = "probe-run --chip nRF52840_xxAA --defmt"
  1. .cargo/config.toml のコンパイルターゲットを調整します。

nRF52840 チップでは、thumbv7em-none-eabihf ターゲットを使用します。

 # .cargo/config.toml
 [build]
-target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
-# target = "thumbv7m-none-eabi"    # Cortex-M3
-# target = "thumbv7em-none-eabi"   # Cortex-M4 and Cortex-M7 (no FPU)
-# target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
+target = "thumbv7em-none-eabihf" # Cortex-M4F(FPU あり)

まだ行っていない場合は、上記のターゲット用の rust-std コンポーネントをインストールします。

$ rustup target add thumbv7em-none-eabihf
  1. HAL を依存関係として追加します。

nRF52840 では、nrf52840-hal を使用します。

 # Cargo.toml
 [dependencies]
-# some-hal = "1.2.3"
+nrf52840-hal = "0.11.0"
  1. HAL を選択したので、src/lib.rs の HAL インポートを修正します。
 // my-app/src/lib.rs
-// use some_hal as _; // memory layout
+use nrf52840_hal as _; // メモリレイアウト

Hello defmt

これで、初めての defmt 対応アプリケーションを cargo-run する準備がすべて整いました! src/bin ディレクトリにはいくつかの例があります。

// my-app/src/bin/hello.rs

#![no_main]
#![no_std]

use my_app as _; // グローバルロガー + パニック時の振る舞い + メモリレイアウト

#[cortex_m_rt::entry]
fn main() -> ! {
    defmt::info!("Hello, world!");

    my_app::exit()
}

このプログラムを cargo run すると、おなじみの “Hello, world!” 出力が生成されます。

$ # `rb` は `run --bin` のエイリアスです
$ cargo rb hello
    Finished dev [optimized + debuginfo] target(s) in 0.03s
flashing program ..
DONE
resetting device
0.000000 INFO Hello, world!
(..)

$ echo $?
0

または、代わりに VS code + Rust-Analyzer を使用している場合は、src/bin/hello.rs ファイルを開き、以前のブログ記事で実演したように “Run” ボタンをクリックできます。

詳細については、defmt bookを確認してください。

プロジェクトをゼロから開始する

nRF52840 を例として、組み込みプロジェクトをゼロから開始する方法を示します。ただし、このガイドはこの開発ボードに限定されません。

マイクロコントローラーを特定する

最初のステップは、使用するマイクロコントローラーを特定することです。必要となるマイクロコントローラーに関する情報は次のとおりです。

1. プロセッサーアーキテクチャとサブアーキテクチャ。

この情報は、デバイスのデータシートまたはマニュアルに記載されているはずです。nRF52840 の場合、プロセッサーは ARM Cortex-M4 コアです。この情報をもとに、互換性のあるコンパイルターゲットを選択する必要があります。rustup target list を実行すると、サポートされているすべてのコンパイルターゲットが表示されます。

$ rustup target list
(..)
thumbv6m-none-eabi
thumbv7em-none-eabi
thumbv7em-none-eabihf
thumbv7m-none-eabi
thumbv8m.base-none-eabi
thumbv8m.main-none-eabi
thumbv8m.main-none-eabihf

コンパイルターゲットは通常、$ARCHITECTURE-$VENDOR-$OS-$ABI という形式で命名されます。ただし、$VENDOR フィールドは省略されることがあります。マイクロコントローラーのようなベアメタルおよび no_std ターゲットでは、多くの場合 $OS フィールドに none が使用されます。$ABI フィールドが hf で終わる場合、出力される ELF が hardfloat Application Binary Interface (ABI) を使用することを示します。

上記の thumb ターゲットは、現在サポートされているすべての ARM Cortex-M ターゲットです。次の表は、コンパイルターゲットと ARM Cortex-M プロセッサーの対応関係を示しています。

コンパイルターゲットプロセッサー
thumbv6m-none-eabiARM Cortex-M0, ARM Cortex-M0+
thumbv7m-none-eabiARM Cortex-M3
thumbv7em-none-eabiARM Cortex-M4, ARM Cortex-M7
thumbv7em-none-eabihfARM Cortex-M4F, ARM Cortex-M7F
thumbv8m.base-none-eabiARM Cortex-M23
thumbv8m.main-none-eabiARM Cortex-M33, ARM Cortex-M35P
thumbv8m.main-none-eabihfARM Cortex-M33F, ARM Cortex-M35PF

ARM Cortex-M ISA には後方互換性があるため、たとえば thumbv6m-none-eabi ターゲットを使用してプログラムをコンパイルし、それを ARM Cortex-M4 マイクロコントローラー上で実行できます。これは動作しますが、thumbv7em-none-eabi を使用した方がパフォーマンスは向上するため(コンパイラーが ARMv7-M 命令を生成するため)、そちらを優先すべきです。

❗️ 選択したコンパイルターゲットを Rust ツールチェーンに追加する必要があります。

$ rustup +stable target add thumbv7em-none-eabihf

2. メモリレイアウト。

特に、デバイスがどれだけの Flash と RAM メモリを持っているか、またそのメモリがどのアドレスで公開されているかを特定する必要があります。この情報は、デバイスのデータシートまたはリファレンスマニュアルに記載されています。

nRF52840 の場合、この情報は Product Specification のセクション 4.2(図 2)にあります。 内容は次のとおりです。

  • アドレス範囲 0x0000_0000 - 0x0010_0000 にわたる 1 MB の Flash。
  • アドレス範囲 0x2000_0000 - 0x2004_0000 にわたる 256 KB の RAM。

knurling の app-template

これらすべての情報があれば、ターゲットデバイス向けのプログラムをビルドできるようになります。

私たちは、cortex-m-quickstart テンプレートをベースにした app-template という Cargo プロジェクトテンプレートを作成しました。これは、すべての knurling ツールをそのまま使用する ARM Cortex-M アーキテクチャ向けの新しいプロジェクトを開始できるものです。

🔎 その他のアーキテクチャについては、rust-embedded organization による他のプロジェクトテンプレートを確認してください。

❗️ インストール手順で案内されているとおり、probe-runcargo-generate がインストールされていることを確認してください。

独自のプロジェクトをセットアップするために app-template を使用する推奨方法は、cargo-generate ツールを使うことです。

$ cargo generate \
    --git https://github.com/knurling-rs/app-template \
    --branch main \
    --name co2sensor

❗️ cargo-generate ツールは libgit2(C ライブラリ)に依存しているため、Windows ではインストールが難しい場合があります。別の選択肢として、GitHub から app-template のスナップショットをダウンロードし、そのスナップショットの Cargo.toml にあるプレースホルダーを埋める方法があります。

テンプレートを使用してプロジェクトを作成したら、前の 2 つのステップで収集したデバイス固有の情報を入力する必要があります。

変更が必要なものはすべて、ファイル内で TODO としてもマークされています。

  1. .cargo/config.toml にチップを入力します。
 # .cargo/config.toml
 [target.'cfg(all(target_arch = "arm", target_os = "none"))']
-runner = "probe-run --chip {{chip}} --defmt"
+runner = "probe-run --chip nRF52840_xxAA --defmt"
  1. .cargo/config.toml のコンパイルターゲットを調整します。
 # .cargo/config.toml
 [build]
-target = "thumbv6m-none-eabi"    # Cortex-M0 および Cortex-M0+
-# target = "thumbv7m-none-eabi"    # Cortex-M3
-# target = "thumbv7em-none-eabi"   # Cortex-M4 および Cortex-M7(FPU なし)
-# target = "thumbv7em-none-eabihf" # Cortex-M4F および Cortex-M7F(FPU あり)
+target = "thumbv7em-none-eabihf" # Cortex-M4F(FPU あり)
  1. Cargo.toml で、適切な HAL を依存関係として追加します。
 # Cargo.toml
 [dependencies]
-# some-hal = "1.2.3"
+nrf52840-hal = "0.11.0"
  1. HAL を選択したので、src/lib.rs の HAL インポートを修正します。
 // my-app/src/lib.rs
-// use some_hal as _; // メモリレイアウト
+use nrf52840_hal as _; // メモリレイアウト
  1. cargo build が動作することを確認します。
$ cd co2sensor
$ cargo build
   Compiling co2sensor v0.1.0 (/Users/ferrous/co2sensor)
    Finished dev [optimized + debuginfo] target(s) in 0.65s

おめでとうございます!co2sensor/ のサンプルコードをターゲットデバイス向けに正常にクロスコンパイルできました。

rust-embedded organization の下に特定のアーキテクチャ向けのテンプレートやサポートの兆候がない場合は、embedonomicon に従って新しいアーキテクチャのサポートを自分でブートストラップできます。

プログラムのフラッシュ書き込み

ターゲットデバイスにプログラムを書き込むには、開発ボードにオンボードデバッガーが搭載されている場合はそれを特定する必要があります。または、開発ボードが何らかのコネクター経由で JTAG または SWD インターフェースを公開している場合は、外部デバッガーを選択します。

ハードウェアデバッガーが probe-rs プロジェクトでサポートされている場合(たとえば J-Link、ST-Link、CMSIS-DAP など)、cargo-flashcargo-embed のような probe-rs ベースのツールを使用できます。これは nRF52840 DK の場合に当てはまります。このボードにはオンボード J-Link プローブがあります。

デバッガーが probe-rs でサポートされていない場合は、OpenOCD またはベンダー提供のソフトウェアを使用してボードにプログラムを書き込む必要があります。

ボードが JTAG、SWD、または同様のインターフェイスを公開していない場合、そのマイクロコントローラーには標準ファームウェアの一部としてブートローダーが搭載されている可能性があります。その場合、チップにプログラムを書き込むには dfu-util、または nrfutil のようなベンダー固有のツールを使用する必要があります。これは nRF52840 Dongle の場合に当てはまります。

出力を取得する

probe-rs がサポートするプローブのいずれかを使用している場合は、rtt-target ライブラリを使用して cargo-embed 上でテキスト出力を取得できます。例で使用したロギング機能は、rtt-target クレートを使用して実装されています。

そうでない場合、またはボード上にデバッガーがない場合は、ボードからテキスト出力を取得する前に HAL を追加する必要があります。

ハードウェア抽象化レイヤー (HAL) を追加する

これで、うまくいけばプログラムを実行し、その出力を取得できるようになっています。デバイスのハードウェア機能を使用するには、依存関係のリストに HAL を追加する必要があります。crates.iolib.rsawesome embedded Rust は HAL を探すのに適した場所です。

HAL を見つけたら、その API docsexamples を通じて API に慣れておくとよいでしょう。特にペリフェラルの初期化や設定に関して、HAL が常にまったく同じ API を公開しているとは限りません。ただし、ほとんどの HAL は embedded-hal トレイトを実装しています。これらのトレイトにより、HAL と ドライバー クレートの間で相互運用が可能になります。これらのドライバークレートは、I2C や SPI のようなインターフェイスを介して、センサー、アクチュエーター、無線機器などの外部デバイスとやり取りする機能を提供します。

使用しているデバイス向けの HAL が存在しない場合は、自分で構築する必要があります。これは通常、まず svd2rust ツールを使用して System View Description (SVD) ファイルから Peripheral Access Crate (PAC) を生成することで行います。PAC は、デバイス上のレジスタを変更するための低レベルだが型安全な API を公開します。PAC が用意できたら、crates.io にある多数の HAL のいずれかを参考にできます。そのほとんどは、svd2rust によって生成された PAC の上に実装されています。

こんにちは、💡

自分のプロジェクトをゼロからセットアップできたので、HAL だけを使用して DK のオンボード LED の 1 つを点灯させ、いろいろ試し始めることができます。その際に役立つかもしれないヒントをいくつか挙げます。

  • Nordic Infocenter では、どの LED がどのピンに接続されているかを確認できます。

LED の点滅 今後の展望

Probe-run

Probe-run

VSCode向けの rust-analyzer プラグインは、すべてのテストまたは main() 関数の上に便利な小さな ▶ Run ボタンを表示してくれます。これにより、エディターから直接コードを実行できます。 しかし残念ながら、組み込みプロジェクトではこれはそのままでは動作しません。▶ Run をクリックすると rust-analyzer は cargo run を呼び出しますが、cargo 自体は組み込みターゲット上でアプリケーションをフラッシュして実行する方法を知らないためです。

ただし、Rust-Analyzer は probe-run、組み込み開発向けのカスタム cargo ランナー とシームレスに統合できます。 probe-run は cargo サブコマンドではなく cargo ランナーなので、必要なのは cargo run が呼び出されたときに代わりに probe-run を使用するよう設定を変更することだけです。その後は、ネイティブプロジェクトと同じように ▶ Run ボタンを使用できます。

この設定方法を示すために、cortex-m-quickstart をベースにしたプロジェクト、具体的には私たちの 初心者向け組み込みトレーニング のコード例を設定してみましょう。

まず、Rust-Analyzerprobe-run がインストールされていることを確認してください。

$ cargo install probe-run

次に、私たちのチップがサポートされているかを確認する必要があります。これにより、後で設定で使用するバリアント名もわかります。

$ probe-run --list-chips
(..)
        STM32F107VB
        STM32F107VC
nrf52 series
    Variants:
        nRF52810_xxAA
        nRF52811_xxAA
        nRF52832_xxAA
        nRF52832_xxAB
        nRF52840_xxAA
nrf51 series
(..)

nRF52840 Development Kit 向けに例をビルドしたいので、選択するバリアントは nRF52840_xxAA です。

これで、プロジェクトの .cargo/config または .cargo/config.toml ファイルで、ボード向けにビルドされた実行可能ファイルを実行するときに使用するデフォルトの runner として probe-run を設定できます。

[target.thumbv7em-none-eabi]
runner = "probe-run --chip nRF52840_xxAA"
#         ^^^^^^^^^        ^^^^^^^^^^^^^

[build]
target = "thumbv7em-none-eabi" # = ARM Cortex-M4

これで完了です。これで通常どおり Run ボタンを使用できます。

知識

この章には、組み込みRustにおけるさまざまな概念を説明する記事と、用語集が含まれています。

記事

[std] と [no_std]?

プログラムの先頭行にある #![no_std] 属性は、そのプログラムが標準ライブラリである std クレートを使用しないことを示します。代わりに、標準ライブラリのサブセットであり、基盤となるオペレーティングシステム(OS)に依存しない core ライブラリを使用します。これは完全にプラットフォームに依存せず、上流ライブラリ、システムライブラリ、libc を必要としません。これは、ロードされる最初のコードである環境では必要です。その結果、core ライブラリは std ライブラリで利用可能なすべての機能を提供するわけではありません。

コレクション

core ライブラリは Vec、String、HashMap を提供しません。これらは動的メモリアロケータ(ヒープ割り当て)を必要としますが、core はそれを提供しないためです。

他のクレートを使用しない場合、配列タプルのような、コンパイル時にサイズが分かっている型に制限されます。

長さに関してもう少し柔軟に扱える別の型として、スライスがあります。スライスは、連続したメモリに格納された要素のリストへの参照です。スライスを作成する方法の 1 つは、連続したメモリに格納された固定サイズの要素リストである配列への参照を取ることです。

#![allow(unused)]
fn main() {
// スタックに割り当てられた配列
let array: [u8; 3] = [0, 1, 2];

let ref_to_array: &[u8; 3] = &array;
let slice: &[u8] = &array;
}

sliceref_to_array は同じ方法で構築されますが、型は異なります。ref_to_array はメモリ上では単一のポインタ(32 ビットプラットフォームでは 1 ワード / 4 バイト)として表現されます。slice はポインタ + 長さ(32 ビットプラットフォームでは 2 ワード / 8 バイト)として表現されます。

スライスはコンパイル時ではなく実行時に長さを追跡するため、任意の長さのメモリ領域を参照できます。

#![allow(unused)]
fn main() {
let array1: [u8; 3] = [0, 1, 2];
let array2: [u8; 4] = [0, 1, 2, 3];

let mut slice: &[u8] = &array1;
log::info!("{:?}", slice); // length = 3

// ここで別の配列を指す
slice = &array2;
log::info!("{:?}", slice); // length = 4
}

この問題に対処する別の可能性:

heapless crate は、動的メモリ割り当てを必要とせず、固定された最大ストレージサイズを持つ、static に適したデータ構造を提供します。

参考資料:

The embedded Rust Book

用語集

HAL

HAL は Hardware Abstraction Layer(ハードウェア抽象化レイヤー)の略です。HAL は、プログラムがハードウェアリソースにアクセスするためのインターフェースを提供するルーチンの集合です。

GPIO

GPIO は General Purpose Input Output(汎用入出力)の略です。GPIO はプログラム可能なデジタル、または場合によってはアナログの信号ピンであり、他のシステムやデバイスへのインターフェースとして使用できます。

ピン設定

フローティング

フローティングピンは、VCC にもグラウンドにも接続されていません。電圧は残留電圧と一致します。

プッシュプル出力

プッシュプル出力として設定されたピンは、高電圧と低電圧を切り替えることができます。

オープンドレイン出力

オープンドレイン出力は、「未接続」と「グラウンドに接続」の間で切り替わります。

プルアップ入力

プルアップ入力として設定されたピンは、外部ソースによって上書きされない限り、VCC に設定されます。この設定により、ピンがフローティングになることを防ぎ、システム内のノイズの原因となることを防止します。

プロトコル

I2C

I2C プロトコルには 2 本の信号線があり、1 本はデータ用(SDA)、もう 1 本はクロック信号用(SCL)です。I2C トランザクションは 1 つ以上のメッセージで構成されます。各メッセージはスタートシンボルで始まります。メッセージは write または read のいずれかであり、次のビットによって示されます。その後に、バイト形式の実際のメッセージが続きます。メッセージはストップシンボルで終了します。 クロック信号は指定された周波数で立ち上がり、立ち下がります。

多くのデバイスを同じ I2C バスに接続でき、I2C アドレスを指定することで特定のデバイスにメッセージを送信できます。

トラブルシューティング

git defmt から安定版 defmt への移行

2020-11-11 より前にこの本に取り組み始めた人は、defmt ロギングフレームワークの不安定な git 版を使用しています。 2020-11-11 に、defmt の安定版が crates.io で利用可能になりました。 まだ git 版を使っている場合は、crates.io 版へ移行することをお勧めします! 方法は次のとおりです:

  1. app-template プロジェクトで、ルートの Cargo.toml を以下のように変更します:
 [workspace]
 members = ["testsuite"]

-[dependencies.defmt]
-git = "https://github.com/knurling-rs/defmt"
-branch = "main"
-
-[dependencies.defmt-rtt]
-git = "https://github.com/knurling-rs/defmt"
-branch = "main"
-
-[dependencies.panic-probe]
-git = "https://github.com/knurling-rs/probe-run"
-branch = "main"
-
 [dependencies]
+defmt = "0.1.0"
+defmt-rtt = "0.1.0"
+panic-probe = { version = "0.1.0", features = ["print-defmt"] }
 cortex-m = "0.6.4"
 cortex-m-rt = "0.6.13"
  1. app-template プロジェクトで、testsuite/Cargo.toml も以下のように変更します:
 name = "test"
 harness = false

-[dependencies.defmt]
-git = "https://github.com/knurling-rs/defmt"
-branch = "main"
-
-[dependencies.defmt-rtt]
-git = "https://github.com/knurling-rs/defmt"
-branch = "main"
-
-[dependencies.panic-probe]
-git = "https://github.com/knurling-rs/probe-run"
-branch = "main"
-# より完全なテスト出力のために `print-defmt` フィーチャーを有効にする
-features = ["print-defmt"]
-
 [dependencies]
+defmt = "0.1.0"
+defmt-rtt = "0.1.0"
+panic-probe = { version = "0.1.0", features = ["print-defmt"] }
 cortex-m = "0.6.3"
 cortex-m-rt = "0.6.12"
  1. 最後に、probe-run バージョン v0.1.4(またはそれ以降)をインストールします
$ cargo install probe-run -f

これでプロジェクトでの作業を再開できます!