The Book
環境構築
チュートリアル(入門)
F-Rust (超初心者入門)
Rustで組み込み開発を始めよう!Baker link. Devで簡単スタート
※この画像のプログラムはRustで書かれています
Baker link. Devとは?
Raspberry Pi財団が独自に開発したARM Cortex M0+デュアルコアのRP2040マイコンをベースに、デバッガを搭載したRust言語学習用開発ボードです。
Baker link. Devの特長
- 強力なデバッガ💪: Raspberry Pi Debug Probeと同等のデバッガを搭載し、効率的なデバッグが可能です。
- 配線要らずのチュートリアル📚: LEDやボタンの配線を一切せずに本チュートリアルを開始できます。初心者でも安心して学べます。
- 簡単セットアップ🚀: Freeで提供しているBaker link. Envを利用することで、Dockerコンテナ上で動作するポータブルな開発環境を簡単に構築し、すぐに開発を開始できます。
Baker link. Devは、スイッチサイエンスでお買い求めいただけます。 リンク
具体的なBaker link. Devの使用例
Baker link. Devを使えば、LEDの点灯からセンサーの読み取りまで、Rustでの組み込み開発を実践的に学ぶことができます。例えば、以下のようなことに挑戦できます:
- LEDの点滅
- 温度センサーのデータ取得🌡
- モーターの制御⚙
- ディスプレイの表示
- I2C、SPIインターフェスのデバイスとの通信
- etc..
さあ、Baker link. DevでRustの組み込み開発を始めましょう!
寄付のお願い
Baker link.にご興味を持っていただきありがとうございます。本プロジェクトは、Baker link. Labのメンバーの皆様からの温かいご支援によって成り立っています。この度、Baker link. Devの製作および販売においては、Baker linkプロジェクトやRust言語、そしてそれを用いた組み込み開発の普及を最優先としたため、利益を追求しておりません。私たちの活動にご関心をお寄せいただけるだけでも大変ありがたく存じますが、もしご寄付を賜ることができましたら、プロジェクトのさらなる発展と技術の普及に大きな力となります。
皆様のご支援が、未来の技術革新を支える礎となります。どうか、私たちの夢を共に実現するために、ご協力をお願い申し上げます。何卒よろしくお願い申し上げます。
開発環境
本チュートリアルでは、Baker link. Envを使ったポータブルなRust環境で組み込み開発を行なって行きます。 Baker link. Envの詳細はこちら
開発環境構成イメージ図
Baker link. Envは、次のツールを必要とします。
- Docker ( Rancher Desktop by SUSE )
- Visual Studio Code
- probe-rs
開発環境構築の流れ
%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#BB2528', 'primaryTextColor': '#fff', 'primaryBorderColor': '#7C0000', 'lineColor': '#F8B229', 'secondaryColor': '#006100', 'tertiaryColor': '#fff' } } }%% flowchart LR Output([Rancher DesktopとVisual Studio Codeのインストール]) Input([probe-rsのインストール]) Interrup([Baker link. Envのインストール]) Output --> Input --> Interrup
Rancher DesktopとVisual Stdio Codeのインストール
Rancher Desktop、Visual Studio Codeは、公式リンクのインストーラーでインストールできます。
またprobe-rsは、OSによってインストール方法を異なります。
probe-rsのインストール
Windowsの場合
- PowerShell(管理者)を起動して次のコマンドを実行して実行権限を取得します。
コマンド実行後に
Y
を入力してEnter
を押してください。
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
- 実行権限を取得した後に次のコマンドを実行してprobe-rsをインストールします。
irm https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.ps1 | iex
Macの場合
次のコマンドをターミナルで実行すればprobe-rsがインストールされます。
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh
Baker link. Envのインストール
Release PageにBaker link. EnvのWindowsとMacのインストーラーがあります。
.exeがWindows用、.dmgがMac用のインストーラーです。 対象のインストーラーをダウンロードして、実行してください。
「壊れているため開けません」の対処法(Mac)
MacでBaker link. Envを起動した際に発生する「壊れているため開けません」のエラーへの対処方法について説明します。
ターミナルで次のコマンドを実行してください。
xattr -d com.apple.quarantine "/Applications/Baker link. Env.app"
※このコマンドの実行は、一度だけで問題ありません。
この問題を回避するためのコマンドを紹介しましたが、根本的な解決策として、証明書の導入を検討しています。これにより、恐らくこのターミナル操作なしでアプリを実行できます。 また、寄付を通じてプロジェクトを支援していただけると、証明書の導入やさらなる開発に役立てることができます。 ご支援をお待ちしております🙇♂️
Baker link. Envの使い方
Baker link. Envは、3つの機能があります。
- プロジェクトを作成する機能
- DAP Serverを起動する機能
- Log機能
これらの機能は、プロジェクト作成、DAP Serverの起動の順序で行います。もし、動作がおかしいなと思った時は出力されるLogをご確認ください。
本開発環境では、Rancher Desktopの起動が必須です。
Baker link. Envを起動すると自動でRancher Desktopが起動する仕様になっています。 もし起動していなかったら、Rancher Desktopのアプリのアイコンをクリックして起動状態にしてください。
プロジェクトの作成
-
Baker link. Envの
Create Project
のProject name
に好きなプロジェクト名を入力します。 -
create
をクリックして、プロジェクトの作成先のフォルダーを選択します。(
Visual Stduio Code open
にチェックが入っているとプロジェクト作成後に、VS Codeが自動起動します。) -
VS Codeの起動直後に左下の
コンテナーで再度開く
をクリックしてください。
DAP Severの起動&デバック
プロジェクトがDev Containerで立ち上がったら、次はBaker link. Devを接続し、probe-rsのDAP Serverを起動させ、デバックを動作させます。
- PCとBaker link. DevをUSBで接続してください。
- 接続したらBaker link. EnvのDAP Serverの
Run
クリックしてServerを起動してください。
F5
を押すと.vscode/launch.json
のprobe-run
という設定が動作します。 もう一度F5
を押すと、プログラムが実行されます。 VS Code上でログが表示されていることも確認できるかと思います。
今画面に見えているのは、3色のLEDが光りながらログに何色が点灯しているか表示するプログラムです。
チュートリアルの流れ
組み込みシステムでは、通常のソフトウェアの概念にプラスして、入力/出力(I/O)と割り込み処理があります。 本チュートリアルでは、出力、入力、割り込みの順に解説します。
%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#BB2528', 'primaryTextColor': '#fff', 'primaryBorderColor': '#7C0000', 'lineColor': '#F8B229', 'secondaryColor': '#006100', 'tertiaryColor': '#fff' } } }%% flowchart LR Output([① 出力(3色点灯)]) Input([② 入力(ボタン入力)]) Interrup([③ 割り込み(入力割り込み)]) Output --> Input --> Interrup
コードのフォルダー構成
作成されたプロジェクトのフォルダー構成は以下の通りです。 (他にもファイルがありますが、必要なファイルだけ記載しています)
.
|-- Cargo.toml # Cargoによるライブラリのインストールを管理するためのファイル
|-- examples # LEDやButtonの使い方の参考コード
| |-- traffic_light.rs
| |-- traffic_light_button.rs
| `-- traffic_light_button_irq.rs
|-- memory.x # 書き込み先のチップのメモリマップ設定ファイル
└── src # ソースコードフォルダ
└── main.rs # mainソースコードファイル
コードを書くときは、src内のファイルを編集することになります。 それ以外は設定ファイルなので、ビルド設定を変更したいときに編集します。
出力 3色点灯光(Lチカ) コードの解読
本チャプターのコード: examples/traffic_light.rs
もしRasberry Pi Picoを利用している場合は、LEDを配線してください。
※Baker link. Envの場合は、配線不要です。何も気にせずに、コーディングをお楽しみください。
コードの読解
src/main.rsがRustのコードになります。 コードの構成は、以下のように分かれています。
- ①マクロの宣言
- ②useの宣言
- ③bootローダー関連
- ④定数
- ⑤main関数
- ⑥プログラム開始ログ
- ⑦各設定のinit
- ⑧無限loop、ログ出力、LED PinのON/OFF、delay処理
次の節から、①〜⑧の順に説明します。
// ①マクロの宣言 #![no_std] #![no_main] // ②useの宣言 use defmt::*; use defmt_rtt as _; use panic_probe as _; use rp2040_hal as hal; use hal::pac; use embedded_hal::delay::DelayNs; use embedded_hal::digital::OutputPin; // ③bootローダー関連 #[link_section = ".boot2"] #[used] pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; // ④定数 const XTAL_FREQ_HZ: u32 = 12_000_000u32; // ⑤main関数 #[rp2040_hal::entry] fn main() -> ! { // ⑥プログラム開始ログ info!("Program start!"); // ⑦各設定のintit let mut pac = pac::Peripherals::take().unwrap(); let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); let clocks = hal::clocks::init_clocks_and_plls( XTAL_FREQ_HZ, pac.XOSC, pac.CLOCKS, pac.PLL_SYS, pac.PLL_USB, &mut pac.RESETS, &mut watchdog, ) .ok() .unwrap(); let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); let sio = hal::Sio::new(pac.SIO); let pins = hal::gpio::Pins::new( pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS, ); let mut green_led = pins.gpio22.into_push_pull_output(); let mut orange_led = pins.gpio21.into_push_pull_output(); let mut red_led = pins.gpio20.into_push_pull_output(); // ⑧無限loop、ログ出力、LED、PinのON/OFF、delay処理 loop { info!("green"); green_led.set_high().unwrap(); timer.delay_ms(2000); green_led.set_low().unwrap(); info!("orange"); for _ in 1..4 { orange_led.set_high().unwrap(); timer.delay_ms(500); orange_led.set_low().unwrap(); timer.delay_ms(500); } orange_led.set_low().unwrap(); info!("red"); red_led.set_high().unwrap(); timer.delay_ms(2000); red_led.set_low().unwrap(); } }
main.rsは、最初に呼ばれるコードです。 その他のコード(例えばsrc/sub.rs)はまたmain.rs以外の別のコードから呼ばれる可能性もありますが辿っていけば最後はこのmain.rsから呼ばれることになります。
①マクロ宣言
このコード部分の話
#![allow(unused)] #![no_std] #![no_main] fn main() { }
マクロ(#![no_std]
と#![no_main]
)
Rustのマクロは、コンパイル時に処理されます。
コンパイルする時にコードを上書きしてくれるイメージです。
これは、C言語の#define
に少し似ています。
書き方は、#![macro_name]
です。
他にも関数のような使い方ができるマクロもあり、info!(argument)
がそれに当たります。
そして、最初の2行に#![no_std]
と#![no_main]
が記載されているかと思いますが、これもマクロです。
#![no_std]
#![no_std]
は、Rustのstdクレートの代わりにcoreクレートをリンクします。
このstdクレートは、WindowsやLinuxといったOS上で動作するソフトウェアに対して利用できます。
一方で、今回の組み込みソフトウェアでは、OSなしのベアメタル環境になるめstdクレートが利用できません。
そのため、OSなしの環境でも動作するcoreクレートに切り替える必要があるわけです。
#![no_main]
#![no_main]
は、Rustコンパイラの通常のエントリポイント(main関数)を利用しないことを意味します。
通常のmain関数を利用しない代わりに、今回のコードでは#[rp2040_hal::entry]
というマクロを宣言しています。
つまり、RP2040専用のエントリポイントを利用しているということです。
少し難しい説明でしたが、ベアメタル(OSなし)の環境ではstdクレートとmain関数が利用できないため、#![no_std]
と#![no_main]
を宣言する必要があると覚えておけば初めは問題ないです。
②use宣言
このコード部分の話
#![allow(unused)] fn main() { use defmt::*; use defmt_rtt as _; use panic_probe as _; use rp2040_hal as hal; use hal::pac; use embedded_hal::delay::DelayNs; use embedded_hal::digital::OutputPin; }
use
大雑把に言えば「use
=ライブラリ(クレート)宣言の省略」です。
以降、その意味についてと利用の仕方について説明します。
他の言語だと外部のライブラリの呼び出しはinclude
やimport
といった宣言したりします。
Rustは、これらのライブラリ呼び出し宣言をせずとも、クレート名::利用したい関数
(例:cortex_m::delay::Delay::new()
)といったように::
を利用すれば参照したいクレート(ライブラリ)の関数を呼び出すことができます。
しかし、これの欠点は::
で繋いでいくと文字数が増えて読みづらくなることです。
そこで、use
の出番です。
use
を利用すれば、先頭の単語を省略して関数等を利用できます。
use
の詳細は、こちらのページに記載しています。
③bootローダー関連
このコード部分の話
#![allow(unused)] fn main() { #[link_section = ".boot2"] #[used] pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H }
bootローダーとは
このLチカのプログラムは、bootローダーというものを動作させる必要があります。
bootローダーとは、電源投入直後に動作するプログラムでのことで、組み込みシステムではよく出てきます。
初めはrp2040のチップの中にある書き換え不可能なブートローダーが実行されます。
その後2段階目のブートローダーとしてrp2040_boot2::BOOT_LOADER_GENERIC_03H
が呼ばれることになります。
このrp2040_boot2
は、RP2040のRust専用のブートローダーでUartなどの周辺回路(ペリフェラル)の初期化処理を行っています。
#[link_section = ".boot2"]
対象のシンボルを.boot2
というセクションに配置します。
.boot2
は、memory.x
というファイルに記載されています。
// memory.x
MEMORY {
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
RAM : ORIGIN = 0x20000000, LENGTH = 256K
}
EXTERN(BOOT2_FIRMWARE)
SECTIONS {
/* ### Boot loader */
.boot2 ORIGIN(BOOT2) :
{
KEEP(*(.boot2));
} > BOOT2
} INSERT BEFORE .text;
これは、どのメモリーの何番地に何を入れるかの設定になります。
これを見てみると.boot2
は、0x10000000から0x10000099まで入ることが分かります。
#[used]
ファイルに静的に保持するための宣言です。
次のpub static BOOT2:[u8;256] = rp2040_boot2::BOOT_LOADER_GENRIC_03H
をBOOT2
をプログラムに静的に保持してくれます。
④定数(const)
このコード部分の話
#![allow(unused)] fn main() { const XTAL_FREQ_HZ: u32 = 12_000_000u32; }
const
const
による宣言はコンパイル時にインラインの定数として扱われるようになります。
つまり、次のようにconst
を宣言しておけば、
#![allow(unused)] fn main() { const XTAL_FREQ_HZ: u32 = 12_000_000u32; }
次のコードが
#![allow(unused)] fn main() { fn sample() -> u32{ let num: u32 = XTAL_FREQ_HZ + 1; return num } }
コンパイル時には、次のコードになるという意味です。
#![allow(unused)] fn main() { fn sample() -> u32{ let num: u32 = 12_000_000u32 + 1; return num } }
⑤main関数
このコード部分の話
#[rp2040_hal::entry] fn main() -> !{ ... }
main関数
Rustはmain.rsの中に記載されているmain関数がはじめに呼ばれる関数です。 他の関数は、このmain関数から呼ばれたりmain関数が読んだ関数がまた次の関数を呼ぶことで呼ばれるようになります。
ちなみにWindowsやMacなどのOS上で動作するアプリを作成する際のmain関数の場合は次の通りです。
fn main{ ... }
これに比べるとRP2040のmain関数は#[rp2040_hal::entry]
があったり、-> !
があります。
#[rp2040_hal::entry]
#[rp2040_hal::entry]
の次の関数をRP2040のプログラムのmain関数として扱うことができます。
-> !
!
は値を返さない関数であることを示しています。
つまり、永遠にreturnをしないと言うことを明示しています。
⑥プログラム開始ログ
このコード部分の話
#![allow(unused)] fn main() { info!("Program start!"); }
info!
PC側にログを出力するための関数です。
このinfo!("Program start!")
は実行すると、Visual Stadio Codeのターミナル画面にProgram start
と出力されます。
また今回のコードの中に記載されてないですが、変数の中身の出力もできます。
たとえば次の通りに、i32の値を出力できます。
#![allow(unused)] fn main() { let cnt = 10; info!("cnt: {}", cnt); }
変数の中身を確認するのに大変便利なので、コードを状況を調べるのに活用してみてください。
⑦各設定のinit
このコード部分の話
#![allow(unused)] fn main() { let mut pac = pac::Peripherals::take().unwrap(); let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); let clocks = hal::clocks::init_clocks_and_plls( XTAL_FREQ_HZ, pac.XOSC, pac.CLOCKS, pac.PLL_SYS, pac.PLL_USB, &mut pac.RESETS, &mut watchdog, ) .ok() .unwrap(); let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); let sio = hal::Sio::new(pac.SIO); let pins = hal::gpio::Pins::new( pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS, ); let mut green_led = pins.gpio22.into_push_pull_output(); let mut orange_led = pins.gpio21.into_push_pull_output(); let mut red_led = pins.gpio20.into_push_pull_output(); }
ペリフェラルオブジェクトの取得
#![allow(unused)] fn main() { let mut pac = pac::Peripherals::take().unwrap(); }
この宣言で、ペリフェラルへのアクセスを簡単にしてくれるペリフェラルオブジェクトを::take
メソッドで取得しています。
ペリフェラルとは、マイコンに内蔵された装置のことを指しています。 RP2040では、次のペリフェラルがあります。
- GPIO:入出力
- UART:シリアル通信
- SPI:チップ間の通信
- I2C:チップ間の通信
- PWM:PWMの出力
- USBコントローラー:USBの通信
- PIO:プログラマブルな入出力
ウォッチドックオブジェクトのインスタンス
#![allow(unused)] fn main() { let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); }
この宣言で、ウォッチドックへのアクセスを簡単にしてくれるウォッチドックオブジェクトを::new
メソッドでインスタンスしています。
また::new
メソッドの引数でpac.WATCHDOG
を渡してます。
これは、ペリフェラルオブジェクトの一部であるWATCHODG
利用してウォッチドックオブジェクトを生成していることを意味してます。
クロックオブジェクトの初期化&取得
#![allow(unused)] fn main() { let clocks = hal::clocks::init_clocks_and_plls( XTAL_FREQ_HZ, pac.XOSC, pac.CLOCKS, pac.PLL_SYS, pac.PLL_USB, &mut pac.RESETS, &mut watchdog, ) .ok() .unwrap(); }
この宣言で、クロックオブジェクトの初期化(pllsも初期化)と取得を::init_clocks_and_plls
メソッドで行っっています。
`
timerの初期化
#![allow(unused)] fn main() { let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); }
マイコンは、CPUを動作させるクロックを用いて時間測定しており、1クロックでカウントアップする回路のカウント数×1クロックの時間(クロックサイクル時間)で計算しています。
Timer
は、この時間に関係する処理を簡易化しており、Timer
の初期化でclocks
を引数で渡しています。
また何秒遅らせる(delay)かといった処理も、このTimer
で行うことができます。
SIOの初期化
#![allow(unused)] fn main() { let sio = hal::Sio::new(pac.SIO); }
専用のペリフェラルを利用しているため、pac.SIO
を引数で渡して初期化しています。
GPIOの初期化
#![allow(unused)] fn main() { let pins = hal::gpio::Pins::new( pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS, ); }
GPIOの初期化をしています。この後のLEDへの出力設定をするには必要な設定になります。
GPIOの出力設定(LED)
#![allow(unused)] fn main() { let mut green_led = pins.gpio22.into_push_pull_output(); let mut orange_led = pins.gpio21.into_push_pull_output(); let mut red_led = pins.gpio20.into_push_pull_output(); }
GPIOの20、21、22をpush-pullのoutputで設定しています。
今回は、信号機のように赤、オレンジ、緑を光らせるため、変数名をred_led
、orange_led
、green_led
にして見やすくしました。
GPIO(General Purpose Input/Output)は、汎用入出力ピンのことです。 これらのピンは、マイクロコントローラやシングルボードコンピュータなどで使用され、入力または出力として設定できます。
⑧無限loop、ログ出力、LED PinのON/OFF、delay処理
このコード部分の話
#![allow(unused)] fn main() { loop { info!("green"); green_led.set_high().unwrap(); timer.delay_ms(2000); green_led.set_low().unwrap(); info!("orange"); for _ in 1..4 { orange_led.set_high().unwrap(); timer.delay_ms(500); orange_led.set_low().unwrap(); timer.delay_ms(500); } orange_led.set_low().unwrap(); info!("red"); red_led.set_high().unwrap(); timer.delay_ms(2000); red_led.set_low().unwrap(); } }
Loop
loop
は、文字通り無限に繰り返されるという意味です。
今回のコードでは、break
がないのでloop{}
の中の処理が無限に繰り返されます。
LEDのHigh、Low
#![allow(unused)] fn main() { green_led.set_high().unwrap(); ... green_led.set_low().unwrap(); }
.set_high()
で、GPIOをHighにします。
unwrap()
.set_high()
の後ろに.unwrap()
がついていますが、これりは意味があります。
set_high()
は、返り値にResult
を持つため次のようにエラー処理を書く必要があります。
#![allow(unused)] fn main() { match green_led.set_high(){ Ok(_) => {}, Err(e) => { // Error }, }; }
重要な箇所においては、このエラー処理は適切に記述するべきだと思います。
しかし、カジュアルにコーディングを楽しみたい、それほど重要でない場所にはunwrap()
という文法を用いることで、Ok
の時はその中身を取り出してくれます。(これがunwrapの意味です)
一方で、Err
の時はpanicになるので使い方には注意が必要です。
タイマーのDelay
#![allow(unused)] fn main() { timer.delay_ms(500); }
500msで待つ処理です。文字通りです。
for文
#![allow(unused)] fn main() { for _ in 1..4 { ... } }
for
は、決まった回数繰り返すという文法です。
このfor
は、1..4
つまり1,2,3(4より小さいという意味です)の値を_
に入れて繰り返し処理をするというものです。
また_
は、その値を使わずに_
に入れておくという意味になりますので、今回は3回処理を繰り返すというfor
になります。
3色点灯(Lチカ)のまとめ
動作させたのは、緑のLEDを2秒点灯、オレンジのLEDを3回点滅、赤のLEDを2秒間点灯を無限に繰り返すプログラムでした。
お気づきかと思いますが、これは時間式の信号機に似ている処理になります。
次の章では、入力処理について学ぶためにこの信号機の処理にボタン入力を加えていきます。
入力 ボタン入力
本チャプターのコード: examples/traffic_light_button.rs
先ほどの時差式信号のプログラムにButtonの処理を加えてコードです。 追加されたのは、次の2つです。
- ①Buttonの設定
- ②Buttonの状態確認(input)
次の節から、①、②を説明します。
#![no_std] #![no_main] use defmt::*; use defmt_rtt as _; use panic_probe as _; use rp2040_hal as hal; use hal::pac; use embedded_hal::delay::DelayNs; use embedded_hal::digital::{InputPin, OutputPin}; // bootloader code #[link_section = ".boot2"] #[used] pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; const XTAL_FREQ_HZ: u32 = 12_000_000u32; #[rp2040_hal::entry] fn main() -> ! { info!("Program start!"); let mut pac = pac::Peripherals::take().unwrap(); let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); let clocks = hal::clocks::init_clocks_and_plls( XTAL_FREQ_HZ, pac.XOSC, pac.CLOCKS, pac.PLL_SYS, pac.PLL_USB, &mut pac.RESETS, &mut watchdog, ) .ok() .unwrap(); let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); let sio = hal::Sio::new(pac.SIO); let pins = hal::gpio::Pins::new( pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS, ); // LED:GPIO22(Green), GPIO21(orange), GPIO20(RED) let mut green_led = pins.gpio22.into_push_pull_output(); let mut orange_led = pins.gpio21.into_push_pull_output(); let mut red_led = pins.gpio20.into_push_pull_output(); // ①Buttonの設定 let mut button = pins.gpio23.into_pull_up_input(); loop { info!("red"); red_led.set_high().unwrap(); timer.delay_ms(2000); // ②Buttonの状態確認(input) if button.is_low().unwrap() { red_led.set_low().unwrap(); info!("green"); green_led.set_high().unwrap(); timer.delay_ms(2000); green_led.set_low().unwrap(); info!("orange"); for _ in 1..4 { orange_led.set_high().unwrap(); timer.delay_ms(500); orange_led.set_low().unwrap(); timer.delay_ms(500); } orange_led.set_low().unwrap(); } } }
①Buttonの設定
このコード部分の話
#![allow(unused)] fn main() { let mut button = pins.gpio23.into_pull_up_input(); }
GPIOの設定(入力)
GPIO23をPull-Upの入力設定をしています。
Pull-Upで設定すると、Buttonを押した時(オープン)にLowになり、押してない時(クローズ)にHighになります。
Pull-UpとPull-Downで抵抗の繋ぎ方が違います。
Pull-Upは、スイッチがオープン(非接触)のときに、入力を高い状態(通常は電源電圧)に引き上げるために使用されます。これにより、スイッチがオープンのときに入力が不確定な状態になるのを防ぎます。
一方、Pull-Downは、スイッチがオープンのときに、入力を低い状態(通常はグラウンド)に引き下げるために使用されます。これにより、スイッチがオープンのときに入力が不確定な状態になるのを防ぎます。
②Buttonの状態確認
このコード部分の話
#![allow(unused)] fn main() { if button.is_low().unwrap() { ... } }
is_low()
is_low()
は、button
の入力がLowの時にTrueを返します。
つまりButtonを押した時に{}
の処理が実行されます。
set_high()
の時にも説明しましたが、unwrap()
はErrorの処理を省略しResult
の中身のbool値だけを取り出す関数です。
ボタン入力のまとめ
LEDの点灯からのButtonの入力確認は簡単だったと思います。
しかし、このプログラムには信号機として利用するには少し不便な欠陥があります。
それは、2秒間周期で一瞬しかButtonを検知されないことです。
#![allow(unused)] fn main() { loop { info!("red"); red_led.set_high().unwrap(); timer.delay_ms(2000); if button.is_low().unwrap() { // この処理が2秒周期で一瞬しかやってこない ... } } }
この処理だと、Buttonを押したタイミングでたままた検知されるか、Buttonを長押ししないと反応しないようになっています。
押した時にif..{}
の処理をしたいですよね?
次の章では、そんな希望を叶えてくれる割り込みについて解説します。
割り込み 入力割り込みのコードの解読
本チャプターのコード: examples/traffic_light_button.rs
前章から追加されたのは、次の2つです。
- ①LED、Buttonの型、グローバル変数宣言
- ②GIPOの設定(割り込み)
- ③変数を格納
- ④割り込み設定の登録
- ⑤何もしない無限ループ
- ⑥割り込み処理
次の節から、①〜⑥を説明します。
#![no_std] #![no_main] use defmt::*; use defmt_rtt as _; use embedded_hal::delay::DelayNs; use embedded_hal::digital::OutputPin; use hal::pac::interrupt; use panic_probe as _; use rp2040_hal as hal; // bootloader code #[link_section = ".boot2"] #[used] pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; const XTAL_FREQ_HZ: u32 = 12_000_000u32; // ①LED、Buttonの型、グローバル変数宣言 type GreenLedPin = hal::gpio::Pin<hal::gpio::bank0::Gpio22, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>; type RedLedPin = hal::gpio::Pin<hal::gpio::bank0::Gpio20, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>; type OrangeLedPin = hal::gpio::Pin<hal::gpio::bank0::Gpio21, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>; type ButtonPin = hal::gpio::Pin<hal::gpio::bank0::Gpio23, hal::gpio::FunctionSioInput, hal::gpio::PullUp>; type DelayTimer = hal::Timer; type LedAndButton = (GreenLedPin, RedLedPin, OrangeLedPin, ButtonPin, DelayTimer); static GLOBAL_PINS: critical_section::Mutex<core::cell::RefCell<Option<LedAndButton>>> = critical_section::Mutex::new(core::cell::RefCell::new(None)); #[rp2040_hal::entry] fn main() -> ! { info!("Program start!"); let mut pac = hal::pac::Peripherals::take().unwrap(); let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); let clocks = hal::clocks::init_clocks_and_plls( XTAL_FREQ_HZ, pac.XOSC, pac.CLOCKS, pac.PLL_SYS, pac.PLL_USB, &mut pac.RESETS, &mut watchdog, ) .ok() .unwrap(); let timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); let sio = hal::Sio::new(pac.SIO); let pins = hal::gpio::Pins::new( pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS, ); // LED:GPIO22(Green), GPIO21(orange), GPIO20(RED) let green_led = pins.gpio22.into_push_pull_output(); let orange_led = pins.gpio21.into_push_pull_output(); let mut red_led = pins.gpio20.into_push_pull_output(); red_led.set_high().unwrap(); // ②GIPOの設定(割り込み) // Button:GPIO23 let button = pins.gpio23.into_pull_up_input(); button.set_interrupt_enabled(hal::gpio::Interrupt::EdgeLow, true); // ③変数を格納 critical_section::with(|cs| { GLOBAL_PINS .borrow(cs) .replace(Some((green_led, red_led, orange_led, button, timer))); }); // ④割り込み設定の登録 unsafe { hal::pac::NVIC::unmask(hal::pac::Interrupt::IO_IRQ_BANK0); } // ⑤何もしない無限ループ // info!("red"); loop { cortex_m::asm::wfi(); } } // ⑥割り込み処理 #[hal::pac::interrupt] fn IO_IRQ_BANK0() { static mut LED_AND_BUTTON: Option<LedAndButton> = None; if LED_AND_BUTTON.is_none() { critical_section::with(|cs| { *LED_AND_BUTTON = GLOBAL_PINS.borrow(cs).take(); }); } if let Some(gpios) = LED_AND_BUTTON { let (green_led, red_led, orange_led, button, timer) = gpios; if button.interrupt_status(hal::gpio::Interrupt::EdgeLow) { info!("Button pressed"); red_led.set_low().unwrap(); info!("green"); green_led.set_high().unwrap(); timer.delay_ms(2000); green_led.set_low().unwrap(); info!("orange"); for _ in 1..4 { orange_led.set_high().unwrap(); timer.delay_ms(500); orange_led.set_low().unwrap(); timer.delay_ms(500); } orange_led.set_low().unwrap(); info!("red"); red_led.set_high().unwrap(); button.clear_interrupt(hal::gpio::Interrupt::EdgeLow); } } }
プログラムの大枠
プログラムの大枠は、次の通りです。
graph TD A[main] --> C[mainの何もしない無限ループ] C -->|"GPIO割り込み(Button割り込み)"| B[IO_IRQ_BANK0] B -->|IO_IRQ_BANK0の処理完了| C
main
関数は、Pinの設定、割り込み設定を行い最後に何もしない無限ループで終わります。
Button割り込みがなければ、常に何もしない無限ループで待機し続けます。
Button割り込みがあった時に、IO_IRQ_BANK0
関数が実行され、LEDが点灯し処理終了後にmain
関数の何もしない無限ループに戻ります。
つまり割り込みがない限りは、何もしないプログラムになります。 また前章のボタンの入力をmain関数で検知する処理と比較して、GPIO割り込みというハードウェアとして備わっている機能に任せることができため、高速化つ確実にButtonを検知できます。
関数間でやり取りするためのグローバル変数の必要性
本章の処理では、main
関数とIO_IRQ_BANK0
関数の2つの関数がそれぞれ独立して実行されます。
Rustでは、main
関数から別の関数を実行する場合は所有権を渡してPinの参照変更をし、LEDを光らせます。
今回の場合は、IO_IRQ_BANK0
関数をmain
関数が呼び出すのではなく、GPIO割り込み(Button割り込み)が呼び出し実行します。
そのため、main
関数から所有権をIO_IRQ_BANK0
に所有権を貸すことができません。
そこで次のようなグローバルな変数を宣言して、関数間の所有権と変数の参照変更をできるようにしています。
#![allow(unused)] fn main() { static GLOBAL_PINS: critical_section::Mutex<core::cell::RefCell<Option<LedAndButton>>> = critical_section::Mutex::new(core::cell::RefCell::new(None)); }
①LED、Buttonの型、グローバル変数宣言
このコード部分の話
#![allow(unused)] fn main() { type GreenLedPin = hal::gpio::Pin<hal::gpio::bank0::Gpio22, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>; type RedLedPin = hal::gpio::Pin<hal::gpio::bank0::Gpio20, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>; type OrangeLedPin = hal::gpio::Pin<hal::gpio::bank0::Gpio21, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>; type ButtonPin = hal::gpio::Pin<hal::gpio::bank0::Gpio23, hal::gpio::FunctionSioInput, hal::gpio::PullUp>; type DelayTimer = hal::Timer; type LedAndButton = (GreenLedPin, RedLedPin, OrangeLedPin, ButtonPin, DelayTimer); static GLOBAL_PINS: critical_section::Mutex<core::cell::RefCell<Option<LedAndButton>>> = critical_section::Mutex::new(core::cell::RefCell::new(None)); }
グローバルな変数
#![allow(unused)] fn main() { static GLOBAL_PINS: critical_section::Mutex<core::cell::RefCell<Option<LedAndButton>>> = critical_section::Mutex::new(core::cell::RefCell::new(None)); }
GLOBAL_PINS
は、関数間でPin情報の変数を参照変更するためのグローバルな変数です。
関数を超えて変数の所有権、参照、変更のやり取りをするために、static
とcritical_section
のMutex
、RefCell
を利用します。
critical_section
は、並行処理で必要とされるグローバルな変数を制御するのに役立つ組み込み用のクレートです。
Mutex
、RefCell
は、std
が提供していますが、std_no
環境である組み込みでは利用することができません。
そこでcritical_section
が代わりにMutex
とRefCell
を提供してくれています。
ジェネリック<T>
hal::gpio::Pin<>
の<>
は、ジェネリックと言われるもので、構造体の中で異なる複数の型の値を保持できる定義です。
#![allow(unused)] fn main() { struct Container<T>{ value: T } }
T
が任意の型になります。
hal::gpio::Pin<hal::gpio::bank0::Gpio22, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>
は、hal::gpio::Pin<>
というPinの型で、hal::gpio::bank0::Gpio22
、hal::gpio::FunctionSioOutput
、hal::gpio::PullDown
の3つを指定しており、GPIO22、SioOutput、PullDownのPin設定であることを意味しています。
type
#![allow(unused)] fn main() { type GreenLedPin = hal::gpio::Pin<...>; }
これはhal::gpio::Pin<...>
をGreenLedPin
という名前に省略するよという意味です。
このtypeを定義することで、グローバル変数の記述が読みやすくなります。
static
static
は、静的変数を定義するために使われます。静的変数はプログラムの全体で一度だけ初期化され、プログラムの終了まで存在します。
Rustでは、static変数自体は変更不可能(イミュータブル)ですが、内部のデータが変更可能(ミュータブル)にすることが可能です。
GLOBAL_PINS
は、critical_section::Mutex
とcore::cell::RefCell
を使って内部のデータを安全に変更できるようにしています。
critical_section::Mutex
Mutex
は、変更可能(ミュータブル)な変数として扱うためのものです。
そして、複数の関数から安全にアクセスできるようにするためのロック機構が備わっており、同時に複数の関数がデータにアクセスすることを防ぎます。
core::cell::RefCell
RefCell
は、実行時に変更可能(ミュータブル)な借用をチェックするためもので、同じスコープ内での複数の変更可能(ミュータブル)な参照を可能にします。
Option
変数でNone
を扱うためのものです。
次のようにi32
をOption
でラップすることで、None
を入れることができます。
#![allow(unused)] fn main() { let val: Opiton<i32> = None }
②GIPOの設定(割り込み)
このコード部分の話
#![allow(unused)] fn main() { let button = pins.gpio23.into_pull_up_input(); button.set_interrupt_enabled(hal::gpio::Interrupt::EdgeLow, true); }
set_interrupt_enabled
GPIOの割り込みを有効にする関数です。
EdgeLow
は検出する信号のパターンを表し、第二引数のtrue
は有効にすることを示しています。
③変数を格納
このコード部分の話
#![allow(unused)] fn main() { critical_section::with(|cs| { GLOBAL_PINS .borrow(cs) .replace(Some((green_led, red_led, orange_led, button, timer))); }); }
critical_setcion::with
critical_setcion::with
は、critical_section::Mutex
であるGLOBAL_PINS
の変数の操作をするための関数です。
この|cs|{...}
の中のやり取りは、他のスレッドからブロックできます。
一連のLEDとButtonのpinの設定がされた、green_led
、red_led
、orange_led
、button
、timer
を格納しています。
またcs
は、Critical Sectionの略です。変数のためcs
でないaa
でも動作します。
クロージャ(|cs|{})
|cs|{}
は、関数を引数として渡せるクロージャと言われるもので、cs
が関数の引数の役割を果たします。
他の言語では、関数を別で宣言して::with(function_name)
のようにして関数名を書いて渡したりしますが、このクロージャを利用することでfunction_name
が不要になり、1つ関数名を考えなくて良くなります。
今回のような小規模なプログラムではメリットを感じないですが、大規模なプログラムになると全体の設計を把握して関数名を決定する必要があるため、1つ考える関数名が減るのはありがたいものです。
borrow
所有権限を借用することを示しています。 Rustでは、所有権を持つことで変数の値の変更することができるので、この操作が必要になります。
replace
値を置き換えることを意味しています。
ここでは、None
からSome((green_led, red_led, orange_led, button, timer))
に置き換えています。
Some
GLOBAL_PINS
は、Option
の変数でした。Option
にした理由は、None
を利用したかったためです。Some
は、Option
で設定した変数に格納する際、宣言するものです。
逆に次のコードでは変数に格納数ことはできません。
#![allow(unused)] fn main() { .replace((green_led, red_led, orange_led, button, timer)); }
④割り込み設定の登録
このコード部分の話
#![allow(unused)] fn main() { unsafe { hal::pac::NVIC::unmask(hal::pac::Interrupt::IO_IRQ_BANK0); } }
unsafe
rustの借用等の安全な文法を無視して、コードを書くことを明示しています。
あまり利用しないようが良いのですが、NVICへの登録には必要になります。
NVIC::unmask
IO_IRQ_BANK0
とは、IO割り込みのBANK0を意味しており、
#![allow(unused)] fn main() { hal::pac::NVIC::unmask(hal::pac::Interrupt::IO_IRQ_BANK0); }
では、NVICにIO_IRQ_BANK0
を登録することを意味しています。
NVIC(Nested Vectored Interrupt Controller)は、主にARM Cortex-Mシリーズのマイクロコントローラに搭載されている割り込みコントローラです。NVICは、システム内で発生する複数の割り込みを効率的に管理し、優先順位に基づいて処理を行います。
⑤何もしない無限ループ
このコード部分の話
#![allow(unused)] fn main() { loop { cortex_m::asm::wfi(); } }
cortex_m::asm::wfi
ARM Cortex-Mプロセッサのアセンブリ命令WFI(Wait For Interrupt)を呼び出します。 この命令は、プロセッサを低電力モードに移行させ、次の割り込みが発生するまで待機させるのに使用します。
つまり、このloop
の処理は「省エネな状態で無限に割り込みを待ちづつける」という意味になります。
⑥割り込み処理
このコード部分の話
#![allow(unused)] fn main() { #[hal::pac::interrupt] fn IO_IRQ_BANK0() { static mut LED_AND_BUTTON: Option<LedAndButton> = None; if LED_AND_BUTTON.is_none() { critical_section::with(|cs| { *LED_AND_BUTTON = GLOBAL_PINS.borrow(cs).take(); }); } if let Some(gpios) = LED_AND_BUTTON { let (green_led, red_led, orange_led, button, timer) = gpios; if button.interrupt_status(hal::gpio::Interrupt::EdgeLow) { info!("Button pressed"); red_led.set_low().unwrap(); info!("green"); green_led.set_high().unwrap(); timer.delay_ms(2000); green_led.set_low().unwrap(); info!("orange"); for _ in 1..4 { orange_led.set_high().unwrap(); timer.delay_ms(500); orange_led.set_low().unwrap(); timer.delay_ms(500); } orange_led.set_low().unwrap(); info!("red"); red_led.set_high().unwrap(); button.clear_interrupt(hal::gpio::Interrupt::EdgeLow); } } } }
hal::pac::interrupt
関数を割り込みの関数として表すマクロです。
次のIO_IRQ_BANK0
を割り込みの関数として扱うことを意味しています。
IO_IRQ_BANK0
IO割り込みとして予約されている関数です。
割り込み登録をしたIO_IRQ_BANK0
に紐づく関数です。このように割り込みの関数は、割り込みによって関数名が決められています。
割り込み関数内での借用
#![allow(unused)] fn main() { static mut LED_AND_BUTTON: Option<LedAndButton> = None; }
LED_AND_BUTTON
がstatic
な変数として用意されます。
static
は、プログラムの最初に初期化されてからプログラムが終了するまで残り続ける変数を意味しています。
そしてIO_IRQ_BANK0
の中で宣言されるということは、この関数が呼び出され終了した後もLED_AND_BUTTON
は残り続け、再びIO_IRQ_BANK0
が呼び出されると残り続けているLED_AND_BUTTON
がそのまま再利用されます。
#![allow(unused)] fn main() { if LED_AND_BUTTON.is_none() { critical_section::with(|cs| { *LED_AND_BUTTON = GLOBAL_PINS.borrow(cs).take(); }); } }
IO_IRQ_BANK0
内で宣言したLED_AND_BUTTON
がNone
だった時に、Critical Sectionを借用し、GLOBAL_PINS
をLED_AND_BUTTON
に入れる処理です。
LED_AND_BUTTON
の前に*
がついていますが、これはC言語のポインターに近い概念です。
LED_AND_BUTTON
は、static mut
になっていますがRustが自動的に&mut
と解釈するため、これをmut
として扱う(&
を外す)ために*
が必要になります。
割り込みされたか確認
#![allow(unused)] fn main() { if button.interrupt_status(hal::gpio::Interrupt::EdgeLow) {...} }
IO_IRQ_BANK0
は、GPIOの割り込みがあったと時に動作し、それがどのGPIOのPinであったか認識することはできません。
このif
文は、button
のPinでEdgeLow
の割り込みがあったか確認をしています。
もし割り込みがあった場合は、{...}
の処理を実行します。
割り込み情報のクリア
#![allow(unused)] fn main() { button.clear_interrupt(hal::gpio::Interrupt::EdgeLow); }
clear_interrupt
でbutton
のPinに割り込みがあった情報をクリアします。
今回は、割り込みを検出するPinが1つしなかったため、必要性を感じないですが、複数の割り込みを検知するPinがあった場合、このclear_interrupt
をしないとすべてのGPIO割り込みでbutton
のPinの処理が実行されてしまいます。
そのため、このclear_interrupt
の処理は必須になります。
入力割り込みのまとめ
いつのタイミングで押しても動作するボタン式信号機が完成しました。
割り込みは、組み込み開発以外であまり馴染みがありませんが、リアルタイム制御をする上で非常に大切なシステムになります。 今回説明したIO割り込みの他にも、タイマー割り込みという時間で割り込み処理を行うものもあります。 ぜひ、ご自身で調べて活用してみてください。
F-Rust
本チャプターでは、 Rust言語初心者のBaker link. Devユーザーが学習を兼ねて、 Baker link. Devで試したコードを紹介します。 以降は、Rust言語初心者のBaker link. Devユーザーの有志で作成されています。 また、記入内容の誤植、訂正、加筆を求めます。
定数
Rustでは、定数(constant)はconst
キーワードを使用して宣言されます。定数は値が固定されており、プログラムの実行中に変更することはできません。定数は、関数の外側やモジュール、構造体、impl
ブロック内で定義できます。
定数の宣言
定数を宣言するには、const
キーワードを使用し、型を明示的に指定する必要があります。以下は基本的な定数の宣言例です。
#![allow(unused)] fn main() { const PI: f64 = 3.141592653589793; }
static変数
Rustでは、static
キーワードを使用して静的変数を宣言します。静的変数はプログラムのライフタイム全体で存在し、グローバルにアクセス可能です。static
変数は、特定の値を一度だけ初期化し、その後は変更せずに使用する場合に便利です。
static
変数の宣言
static
変数を宣言するには、static
キーワードを使用し、型を明示的に指定する必要があります。以下は基本的なstatic
変数の宣言例です。
#![allow(unused)] fn main() { static GREETING: &str = "Hello, world!"; }
この例では、GREETINGという名前の静的変数が宣言され、値は文字列リテラル"Hello, world!"に設定されています。型は&str(文字列スライス)です。
static変数の使用例
静的変数はプログラム全体で使用することができます。以下は、静的変数を使用してメッセージを表示する例です。
static GREETING: &str = "Hello, world!"; fn main() { println!("{}", GREETING); }
この例では、main関数外で宣言した静的変数GREETINGを関数内で使用してメッセージを表示しています
可変なstatic変数
Rustでは、static変数を可変(mutable)にすることもできますが、その場合はunsafeブロック内で操作する必要があります。以下は可変なstatic変数の例です。
static mut COUNTER: i32 = 0; fn main() { unsafe { COUNTER += 1; println!("COUNTER: {}", COUNTER); } }
この例では、COUNTERという可変な静的変数が宣言され、unsafeブロック内でその値を変更しています。
メモリ安全性とデータ競合
Rustは、メモリ安全性を保証するために、所有権と借用のルールを厳格に適用します。 しかし、可変なstatic変数はこれらのルールを回避するため、複数のスレッドから同時にアクセスされる可能性があります。 これにより、データ競合や未定義動作が発生するリスクがあります。
unsafeの必要性
可変なstatic変数を使用する場合、Rustのコンパイラはその安全性を保証できません。 そのため、プログラマが手動で安全性を保証する必要があります。 これを示すために、unsafeブロック内で操作を行う必要があります。 unsafeブロックは、プログラマがこのコードの安全性を確認し、責任を持つことを意味します。
static変数を可変にする場合、コンパイラがメモリの安全性を保証できないようですね。 プログラマがunsafeなのはわかってる!!と明示的に書く必要がルール的にあるのですね!
定数とstatic変数の違い
Rustでは、定数とstatic
変数はどちらも固定された値を持つことができますが、それぞれに異なる特性と用途があります。
定数(const
)
定数は、const
キーワードを使用して宣言されます。定数はコンパイル時に評価され、プログラムの実行中に変更することはできません。
特徴
- コンパイル時に評価:定数の値はコンパイル時に決定されます。
- 変更不可:一度設定された値は変更できません。
- スコープ:定数は宣言されたスコープ内でのみ有効です。
static変数 (static
)
static変数は、static
キーワードを使用して宣言されます。static変数はプログラムのライフタイム全体で存在し、グローバルにアクセス可能です。
特徴
- プログラムのライフタイム全体で存在:static変数はプログラムが終了するまでメモリに保持されます。
- グローバルアクセス:static変数はプログラム全体からアクセス可能です。
- 可変性:static変数はmutキーワードを使用して可変にすることができますが、その場合はunsafeブロック内で操作する必要があります。
定数に割り当てられた値はコンパイル時にインラインに展開されるそうです。 一方で、static変数はインラインに展開されず、 static変数を使用する関数内などから値を参照されるようです。 仮に、割り当てる値のメモリ量が大きかったり、プログラム内に1つしか存在しないように値を保持したい場合は、 static変数を用いる方が良いと理解しました。
不変な変数
Rustでは、変数はデフォルトで不変(immutable)です。これは、一度値を割り当てた変数の値を変更できないことを意味します。不変な変数を使用することで、コードの安全性と予測可能性が向上します。
不変な変数の宣言
変数を不変にするには、特別なキーワードは必要ありません。以下のようにlet
キーワードを使用して変数を宣言します。
fn main() { let x = 5; println!("The value of x is: {}", x); }
このコードでは、不変な変数x
は値5
を持ちますが、その後に値を変更することはできません。
試しに、値を再割り当てしてみました。
不変な変数への値の再割り当て
fn main() { let x = 5; println!("The value of x is: {}", x); x = 6; // エラー: 不変変数に再代入できません }
このコードをコンパイルすると、以下のようなエラーが表示されます。
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
可変な変数
Rustでは、変数はデフォルトで不変(immutable)ですが、mut
キーワードを使用することで可変(mutable)にすることができます。可変な変数を使用することで、変数の値を後から変更することが可能になります。
可変な変数の宣言
可変な変数を宣言するには、let
キーワードの後にmut
キーワードを追加します。
fn main() { let mut x = 5; println!("The value of x is: {}", x); x = 6; // 問題なく再代入できます println!("The value of x is: {}", x); }
このコードでは、変数x
は最初に値5
を持ち、その後に値6
に変更されます。
可変な変数の注意点
- スコープ:可変な変数は宣言されたスコープ内でのみ有効です。
- 競合状態:マルチスレッド環境では、可変な変数の使用により競合状態が発生する可能性があります。そのため、適切な同期機構を使用する必要があります。
スコープ
Rustでは、スコープ(scope)は変数や関数などの要素が有効である範囲を指します。スコープを理解することで、プログラムの構造や変数のライフタイムをよりよく管理できます。
スコープの基本
Rustでは、{}
で囲まれた領域がスコープを形成します。スコープ内で宣言された変数は、そのスコープ内でのみ有効です。
fn main() { let x = 5; // xはこのスコープ内で有効 { let y = 10; // yはこの内側のスコープ内で有効 println!("x: {}, y: {}", x, y); // xとyは両方ともここで使用可能 } // println!("y: {}", y); // エラー: yはこのスコープでは無効 }
グローバルスコープとローカルスコープ
- グローバルスコープ:プログラム全体で有効なスコープです。通常、関数外で宣言された定数や関数が含まれます。
- ローカルスコープ:特定のブロックや関数内でのみ有効なスコープです。
const GLOBAL_CONST: i32 = 100; // グローバルスコープ fn main() { let local_var = 50; // ローカルスコープ println!("Global: {}, Local: {}", GLOBAL_CONST, local_var); }
スコープのネスト
スコープはネストすることができ、内側のスコープから外側のスコープの変数にアクセスできますが、その逆はできません。
fn main() { let outer = "outer"; { let inner = "inner"; println!("Outer: {}, Inner: {}", outer, inner); // 両方の変数にアクセス可能 } // println!("Inner: {}", inner); // エラー: innerはこのスコープでは無効 }
シャドーイング
Rustでは、同じ名前の変数を再度宣言することができます。これをシャドーイング(shadowing)と呼びます。
fn main() { let x = 5; let x = x + 1; // シャドーイング { let x = x * 2; // 内側のスコープで再度シャドーイング println!("Inner x: {}", x); // Inner x: 12 } println!("Outer x: {}", x); // Outer x: 6 }
定数と変数
先の不変な変数ですが、お気づきでしょうか。 不変なら変数と名乗るべきでないのではないか!と。 そもそもここまでの説明だけだと、不変な変数の存在意義は定数と同じです。 では、不変な変数は定数なのか?まず定数についてまとめます。
定数は以下のように定義されます。
#![allow(unused)] fn main() { const MAX_VALUE: u32 = 10; println!("最大値は{}", MAX) }
定数はconst
で宣言し、値の型も必ず指定する必要があります。
型については、以降のセクションで紹介しますので、
今は値がどんな値かを記す必要があると思ってください。
この場合だと、u32
は32ビットの符号なし整数です。
変数と定数(constants)の違い
では、変数と定数の違いは何でしょうか。 まず定数は、グローバルスコープでも定義できます。 スコープとは、ある変数や関数、定数などを参照できる範囲のことです。 具体的な例を以下に示します。
~~~ // ④定数 const XTAL_FREQ_HZ: u32 = 12_000_000u32; // ⑤main関数 #[rp2040_hal::entry] fn main() -> ! { // ⑥プログラム開始ログ info!("Program start!"); // ⑦各設定のintit let mut pac = pac::Peripherals::take().unwrap(); let clocks = hal::clocks::init_clocks_and_plls( XTAL_FREQ_HZ, pac.XOSC, pac.CLOCKS, pac.PLL_SYS, pac.PLL_USB, &mut pac.RESETS, &mut watchdog, ~~~
所有権とは
Rustの所有権システムは、メモリ管理を安全かつ効率的に行うための独自の仕組みです。以下に所有権の基本的な概念を説明します。
所有権の基本ルール
- 各値は所有者と呼ばれる変数に対応している: Rustの各値は、所有者と呼ばれる変数に対応しています。所有者が存在する限り、その値は有効です。
- 一度に存在できる所有者は1つだけ: 値の所有権は一度に1つの変数だけが持つことができます。所有権が移動すると、元の所有者はその値を使用できなくなります。
- 所有者がスコープを抜けると値は破棄される: 所有者がスコープを抜けると、その値は自動的にメモリから解放されます。これにより、メモリリークを防ぎます。
所有権の移動
所有権は変数間で移動することができます。 所有権が移動すると、元の変数はその値を使用できなくなります。
以下のコードでは、s1
が"hello"
の所有権を持っていましたが、let s2 = s1;
によってその所有権がs2
に移動しました。
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; // s1からs2に所有権が移動 println!("{}", s2); // 出力: hello }
その結果、s1
はもう有効ではなくなり、使用できません。
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; // s1からs2に所有権が移動 println!("{}", s1); // エラー: s1はもう有効ではない }
所有権の移動は、メモリの安全性を確保するためのRustの仕組みです。これにより、同じメモリ領域を複数の変数が同時に所有することによるメモリリークやデータ競合を防ぎます。
スタックとヒープ
Rustでは、メモリ管理が非常に重要であり、スタックとヒープという2つの主要なメモリ領域を使用します。
スタック
- 特徴: スタックはLIFO(Last In, First Out)のデータ構造で、関数の呼び出し時に使用されます。関数の引数やローカル変数がスタックに積まれ(push)、関数の終了時にスタックから取り除かれます(pop)。
- 利点: スタックはデータの取り扱いが高速で、メモリの確保と解放が自動的に行われます。データのサイズが固定されているため、効率的です。
- 制約: スタックはサイズが固定されており、動的なメモリ確保ができません。大きなデータや長期間保持するデータには向いていません。
コンパイル時にサイズがわからなかったり、サイズが可変のデータはヒープに格納することができます。
ヒープ
- 特徴: ヒープは動的にメモリを確保するための領域で、データのサイズが可変です。実行時に必要なメモリを確保し、使用後に解放します。
- 利点: ヒープは動的なメモリ管理が可能で、大きなデータや長期間保持するデータに適しています。サイズが事前にわからないデータにも対応できます。
- 制約: ヒープはメモリの確保と解放が手動で行われるため、スタックよりも遅くなります。また、メモリリークや断片化のリスクがあります。
fn main() { // スタック上に確保される変数 let x = 5; // ヒープ上に確保されるString型 let mut s = String::from("Hello"); s.push_str(", world!"); println!("{}", s); }
参照と借用
参照
所有権を移動せずに値を参照することができます。
借用には不変参照(&T
)と可変参照(&mut T
)があります。
不変参照(&T
)
値を変更できない参照です。複数の不変参照を同時に持つことができます。
#![allow(unused)] fn main() { let s = String::from("hello"); let r1 = &s; // 不変参照 let r2 = &s; // 不変参照 println!("{} and {}", r1, r2); // 出力: hello and hello }
可変参照(&mut T
)
値を変更できる参照です。 ただし、可変参照は一度に一つしか持つことができません。
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &mut s; // 可変参照 r1.push_str(", world"); println!("{}", r1); // 出力: hello, world println!("{}", s); // 出力: hello, world }
借用
関数の引数に参照を取ることを借用と呼びます。
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // '{}'の長さは、{}です println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
借用のルール
-
不変参照と可変参照を同時に持つことはできない
同じスコープ内で不変参照と可変参照を同時に持つことはできません。これにより、データ競合を防ぎます。
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &s; // 不変参照 let r2 = &mut s; // エラー: 不変参照が存在する間は可変参照を持てない }
-
可変参照は一度に一つだけ
可変参照は一度に一つしか持つことができません。これにより、データの一貫性を保ちます。
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &mut s; // 可変参照 let r2 = &mut s; // エラー: 可変参照は一度に一つだけ }
-
可変参照は一度に一つだけ
借用はそのスコープ内でのみ有効です。スコープを抜けると借用は無効になります。
let r; { let s = String::from("hello"); r = &s; // エラー: sのスコープを抜けるとrは無効になる println!("{}", r) } println!("{}", r) // エラー: スコープを抜けたためrは無効
借用の利点
- メモリ安全性: 借用により、所有権を移動せずにデータを安全に操作できます。
- 効率性: 借用を使用することで、不要なデータのコピーを避け、効率的にメモリを使用できます。
スライス
スライス型(slice)は、Rustで配列やベクタの一部を参照するための型です。 スライスは所有権を持たず、元のデータを参照するだけなので、メモリの効率的な利用が可能です。
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = &s1; // s2は&s1の文字列スライス }
このコードでは、s1はString型であり、ヒープに格納された可変長の文字列データを持っています。 一方、s2はs1の参照であり、文字列スライス型(&str)というスライス型です。 s2を通じてs1のデータを読み取ることができます。
特徴
-
参照型
スライスは常に参照であり、元のデータの一部を指し示します。
&[T]
や&mut [T]
の形で定義されます。 -
不変スライスと可変スライス
- 不変スライス:
&[T]
。元のデータを変更できません。 - 可変スライス:
&mut [T]
。元のデータを変更できます。
- 不変スライス:
-
動的サイズ
スライスは動的にサイズを変更でき、配列やベクタの一部を切り出して使用できます。
#![allow(unused)] fn main() { // 配列から不変スライスを生成 let arr = [1, 2, 3, 4, 5]; let slice = &arr[1..4]; // [2, 3, 4] // ベクタから可変スライスを生成 let mut vec = vec![1, 2, 3, 4, 5]; let slice_mut = &mut vec[1..4]; // [2, 3, 4] slice_mut[0] = 10; // スライスを介して元のベクタを変更 }
スライスの利点
-
効率的なメモリ使用
スライスは元のデータを参照するだけなので、メモリのコピーが発生しません。
-
柔軟性
配列やベクタの一部を簡単に操作でき、コードの再利用性が高まります。
-
安全性
Rustの所有権システムと借用規則により、スライスを使用してもメモリの安全性が保たれます。
使用例
fn main() { let arr = [1, 2, 3, 4, 5]; let slice = &arr[1..4]; // [2, 3, 4] println!("{:?}", slice); // 出力: [2, 3, 4] let mut vec = vec![1, 2, 3, 4, 5]; let slice_mut = &mut vec[1..4]; // [2, 3, 4] slice_mut[0] = 10; // スライスを介して元のベクタを変更 println!("{:?}", vec); // 出力: [1, 10, 3, 4, 5] }
基本のデータ型
スカラー型
単一の値を持つ型です。Rustには以下のスカラー型があります。
- 整数型: 符号付き(i8, i16, i32, i64, i128, isize)と符号なし(u8, u16, u32, u64, u128, usize)の整数型があります。
- 浮動小数点数型: f32とf64の2種類があります。
- 論理値型: 真偽値を表すbool型があります。値はtrueまたはfalseです。
- 文字型: Unicodeのスカラ値を表すchar型があります。シングルクォートで囲まれた一文字を表します。
複合型
複数の値を持つ型です。以下のような複合型があります。
- タプル型: 異なる型の値をまとめて一つのタプルとして扱います。例: (i32, f64, char)
- 配列型: 同じ型の値を固定長の配列として扱います。例: [i32; 5]は5つのi32型の値を持つ配列です。
- ベクタ型: 動的にサイズを変更できる可変長の配列です。例: Vec
。
文字列型
- String型: String型は、ヒープに格納された可変の文字列データを表します。文字列の内容を変更したり、追加したりすることができます
- 文字列スライス型 (
&str
):文字列スライスは、既存の文字列データを参照する不変のビューです。 文字列スライス自体は変更できません。 文字列スライスは、文字列データの一部を効率的に参照するために使用されます。
総称型
ジェネリック型とも呼ばれ、型を抽象化して再利用性を高めます。例: Option
スカラー型
単一の値を持つ型です。 Rustには以下のスカラー型があります。
符号付き整数(i8, i16, i32, i64, i128, isize)
符号付き整数型は、正の数と負の数の両方を表現できます。以下に各型の範囲とサンプルコードを示します。
- i8: 8ビット符号付き整数。
- i16: 16ビット符号付き整数。
- i32: 32ビット符号付き整数。
- i64: 64ビット符号付き整数。
- i128: 128ビット符号付き整数。
- isize: ポインタサイズ符号付き整数。範囲はアーキテクチャ依存(32ビットまたは64ビット)。
fn main() { let a: i8 = -128; let b: i16 = 32767; let c: i32 = -2147483648; let d: i64 = 9223372036854775807; let e: i128 = -170141183460469231731687303715884105728; let f: isize = 42; // 32ビットまたは64ビットの範囲内 println!("i8: {}, i16: {}, i32: {}, i64: {}, i128: {}, isize: {}", a, b, c, d, e, f); }
符号なし整数(u8, u16, u32, u64, u128, usize)
符号なし整数型は、正の数のみを表現します。以下に各型の範囲とサンプルコードを示します。
- u8: 8ビット符号なし整数。
- u16: 16ビット符号なし整数。
- u32: 32ビット符号なし整数。
- u64: 64ビット符号なし整数。
- u128: 128ビット符号なし整数。
- usize: ポインタサイズ符号なし整数。範囲はアーキテクチャ依存(32ビットまたは64ビット)。
fn main() { let a: u8 = 255; let b: u16 = 65535; let c: u32 = 4294967295; let d: u64 = 18446744073709551615; let e: u128 = 340282366920938463463374607431768211455; let f: usize = 42; // 32ビットまたは64ビットの範囲内 println!("u8: {}, u16: {}, u32: {}, u64: {}, u128: {}, usize: {}", a, b, c, d, e, f); }
浮動小数点型
Rustには2種類の浮動小数点型があります。これらはIEEE-754標準に準拠しています。
- f32: 32ビット浮動小数点数。単精度浮動小数点数とも呼ばれます。
- f64: 64ビット浮動小数点数。倍精度浮動小数点数とも呼ばれます。
f32
32ビット浮動小数点数は、約7桁の精度を持ちます。以下にf32型のサンプルコードを示します。
fn main() { let a: f32 = 3.14; let b: f32 = 2.71828; let c: f32 = 1.41421; println!("f32 values: a = {}, b = {}, c = {}", a, b, c); }
f64
64ビット浮動小数点数は、約15桁の精度を持ちます。以下にf64型のサンプルコードを示します。
fn main() { let a: f64 = 3.141592653589793; let b: f64 = 2.718281828459045; let c: f64 = 1.4142135623730951; println!("f64 values: a = {}, b = {}, c = {}", a, b, c); }
浮動小数点の演算
浮動小数点型を使った基本的な演算も可能です。以下にいくつかの例を示します。
fn main() { let x: f64 = 1.0; let y: f64 = 3.0; let sum = x + y; let difference = x - y; let product = x * y; let quotient = x / y; println!("Sum: {}", sum); println!("Difference: {}", difference); println!("Product: {}", product); println!("Quotient: {}", quotient); }
bool型(論理値型)
真偽値を表すbool型です。値はtrueまたはfalseです。
fn main() { let is_true: bool = true; let is_false: bool = false; println!("True: {}, False: {}", is_true, is_false); }
char型(文字型)
Unicodeのスカラ値を表すchar型です。シングルクォートで囲まれた一文字を表します。
fn main() { let letter: char = 'A'; let emoji: char = '😊'; println!("Letter: {}, Emoji: {}", letter, emoji); }
複合型
複合型は、複数の値をまとめて一つの型として扱うことができるデータ型です。Rustには主に以下の複合型があります。
タプル型
タプル型は、異なる型の値をまとめて一つのグループとして扱います。タプルはカンマ区切りの値を丸括弧で囲んで作成します。 また、タプルから値を取り出すには、パターンマッチングを使用して分解することができます。
fn main() { let tuple: (i32, f64, char) = (42, 3.14, 'a'); let (x, y, z) = tuple; // パターンマッチングで値を取り出す println!("Tuple: ({}, {}, {})", x, y, z); }
タプルから特定の要素を取り出すには、インデックスも使用できます。インデックスは0から始まります。
fn main() { let tuple: (i32, f64, char) = (42, 3.14, 'a'); println!("First element: {}", tuple.0); println!("Second element: {}", tuple.1); println!("Third element: {}", tuple.2); }
配列型
配列型は、同じ型の値を固定長の配列として扱います。配列は角括弧で囲んで作成します。
ベクタを生成するには、vec!
マクロやVec::new
メソッドを使用します。
fn main() { let array: [i32; 3] = [1, 2, 3]; println!("Array: {:?}", array); println!("First element: {}", array[0]); println!("Second element: {}", array[1]); println!("Third element: {}", array[2]); }
ベクタ型
ベクタ型(Vec<T>
)は、動的にサイズを変更できる可変長の配列です。
- 可変長: 実行時にサイズを変更できます。
- ヒープメモリ: 要素はヒープに格納されます。
- 型安全: ベクタ内のすべての要素は同じ型でなければなりません。
#![allow(unused)] fn main() { // vec!マクロを使用 let v = vec![1, 2, 3]; // Vec::newを使用 let mut v: Vec<i32> = Vec::new(); v.push(1); v.push(2); v.push(3); }
ベクタの操作
ベクタには多くの便利なメソッドがあります。以下にいくつかの例を示します:
- 要素の追加:
push
- 要素の削除:
pop
- 要素の挿入:
insert
- 要素の削除:
remove
- 長さの取得:
len
- 空チェック:
is_empty
- 全要素の削除:
clear
fn main() { let mut v = vec![1, 2, 3]; v.push(4); // 末尾に要素を追加 v.pop(); // 末尾の要素を削除 v.insert(1, 10); // インデックス1に要素を挿入 v.remove(1); // インデックス1の要素を削除 println!("ベクタの長さ: {}", v.len()); // 長さを取得 println!("ベクタは空か?: {}", v.is_empty()); // 空チェック v.clear(); // 全要素を削除 }
タプル型は要素数、配列型は長さと型が固定されているため、柔軟なデータ管理が難しいです。 一方、ベクタ型は動的にサイズを変更できるため、メモリの再割り当てが発生することがあり、これがパフォーマンスに影響を与えることがあります。また、ベクタの初期化には配列よりもコストがかかる場合があります。
特徴
タプル型 | 配列型 | ベクタ型 | |
---|---|---|---|
要素 | 異なる型の要素を持つことができる | 同じ型の要素のみ | 同じ型の要素のみ |
要素数 | 固定 | 固定(長さ) | 可変 |
要素へのアクセス | インデックスまたはパターンマッチング | インデックス | インデックス |
用途 | 関数から複数の値を返す、一時的なデータのグループ化 | 固定長のデータ、ループ処理やインデックスを使ったアクセス | 動的にサイズを変更できるデータ、可変長のデータ、頻繁な追加や削除が必要な場合 |
文字列型
String型
String型は、ヒープに格納された可変の文字列データを表します。文字列の内容を変更したり、追加したりすることができます 。
- 可変の文字列: String型は、ヒープに格納された可変長の文字列データを表します。文字列の内容を変更したり、追加したりすることができます。
- 所有権: Stringは所有権を持ち、メモリ管理を行います。
- 用途: 動的にサイズを変更する必要がある文字列データを扱う場合に適しています。
#![allow(unused)] fn main() { let mut s = String::from("Hello"); s.push_str(", world!"); // ", world!"を追加 println!("{}", s) }
String型を用いたサンプルコードでは、s.push_str(", world")
で値を追加しています。そうです。ベクタ型と同じです。
String型は内部的にはVec<u8>
(バイトのベクタ)として実装されているようです。
文字列スライス型(&str
)
文字列スライスは、既存の文字列データを参照する不変のビューです。文字列スライス自体は変更できません。 文字列スライスは、文字列データの一部を効率的に参照するために使用されます。
- 不変の文字列: 文字列スライスは、既存の文字列データを参照する不変のビューです。文字列スライス自体は変更できません。
- 軽量: 文字列スライスは、文字列データの一部を効率的に参照するために使用されます。
- 用途: 既存の文字列データを参照する場合や、文字列リテラルを扱う場合に適しています。
#![allow(unused)] fn main() { let s = "Hello, world!"; let slice = &s[0..5]; // "Hello" }
カスタム型
Rustでは、カスタム型を定義することで、プログラムの構造をより明確にし、再利用性を高めることができます。カスタム型には主に**構造体(struct)と列挙型(enum)**の2種類があります。
列挙体
Rustの列挙体(enum)は、複数の関連する値を一つの型としてまとめるために使われます。以下に、Rustの列挙体の基本的な使い方を紹介します。
基本的な定義
Rustでは、enumキーワードを使って列挙体を定義します。例えば、色を表す列挙体を定義する場合は次のようになります。
#![allow(unused)] fn main() { enum Color { Red, Green, Blue, } }
値を持つ列挙体
列挙体の各バリアントに値を持たせることもできます。例えば、RGBカラーを表す場合は次のように定義します。
#![allow(unused)] fn main() { enum Color { RGB(u8, u8, u8), RGBA(u8, u8, u8, u8), } }
列挙体の使用
列挙体の値を使うには、match文を使って各バリアントに対して処理を行います。
enum Color { RGB(u8, u8, u8), RGBA(u8, u8, u8, u8), } fn main() { let color = Color::RGB(0, 255, 0); match color { Color::RGB(r, g, b) => println!("RGB({}, {}, {})", r, g, b), Color::RGBA(r, g, b, a) => println!("RGBA({}, {}, {}, {})", r, g, b, a), } }
列挙体の利点
- 型安全性: 列挙体を使うことで、特定の値の集合を型として扱うことができ、型安全性が向上します。
- 可読性: コードの可読性が向上し、意図が明確になります。
- パターンマッチング: match文を使って、各バリアントに対する処理を簡潔に記述できます。
構造体
Rustの構造体(struct)は、関連するデータを一つの単位としてまとめるためのカスタムデータ型です。 構造体を使うことで、異なるデータ型を持つ複数のフィールドを一つにまとめて管理できます。
構造体の定義
構造体は struct
キーワードを使って定義します。
例えば、Person
という名前の構造体を定義する場合は次のようになります:
#![allow(unused)] fn main() { struct Person { name: String, age: u32, } }
インスタンスの生成
構造体のインスタンスを生成するには、次のように記述します:
#![allow(unused)] fn main() { let person = Person { name: String::from("Alice"), age: 30, }; }
フィールドへのアクセス
struct Person { // 構造体を定義 name: String, age: u32, } fn main() { let person = Person { //Personという名の構造体のインスタンスを生成 name: String::from("Alice"), age: 30, }; // フィールドへアクセス println!("Name: {}, Age: {}", person.name, person.age); }
Cスタイル構造体
-
フィールドに名前を付けて定義する、最も一般的な構造体です。
#![allow(unused)] fn main() { struct Color { red: u8, green: u8, blue: u8, } let black = Color { red: 0, green: 0, blue: 0 }; }
タプルスタイル構造体
-
タプル構造体は、通常のタプルに名前を付けたものです。 フィールドに名前を付けずに、位置でアクセスします。
#![allow(unused)] fn main() { struct Point(i32, i32, i32); let origin = Point(0, 0, 0); }
ユニットスタイル構造体
-
フィールドを持たない構造体です。主に型としての意味を持たせるために使用されます。
#![allow(unused)] fn main() { struct Unit; let unit = Unit; }
リテラル
リテラルは、ソースコードに直接記述された固定値を指します。 Rustでは、数値、文字、文字列、ブール値、および複合データ型(タプルや配列など)に対応するリテラルが用意されています。
数値リテラル
- 整数リテラル: デフォルトでi32型。型サフィックスを使用して異なる数値型を指定できます(例: 57u8, 3.14f32)。
- 浮動小数点数リテラル: デフォルトでf64型。型サフィックスを使用して指定できます(例: 3.14f32)。
- 進数リテラル: 二進数(0b)、八進数(0o)、十六進数(0x)の表記が可能です。
fn main() { let dec = 98_222; // 十進数 let hex = 0xff; // 十六進数 let oct = 0o77; // 八進数 let bin = 0b1111_0000; // 二進数 let byte = b'A'; // バイトリテラル println!("dec: {}, hex: {}, oct: {}, bin: {}, byte: {}", dec, hex, oct, bin, byte); }
数値リテラルにアンダースコア(_)を使って桁区切りを行うことができます。これにより、長い数値をより読みやすくすることができます。アンダースコアは、数値のどこにでも挿入でき、コンパイラはこれを無視して数値を処理します。
文字列と文字リテラル
- 文字列リテラル: ダブルクォートで囲みます(例: "hello")。&str型のスライスを表し、UTF-8でエンコードされたテキストを保持します。
- 文字リテラル: シングルクォートで囲みます(例: 'a')。char型を表します。
fn main() { let s = "Hello, world!"; let c = 'z'; let heart_eyed_cat = '😻'; println!("s: {}, c: {}, heart_eyed_cat: {}", s, c, heart_eyed_cat); }
ブールリテラル
- ブールリテラル: trueまたはfalseの値を持ちます。
fn main() { let t = true; let f: bool = false; // 明示的に型を指定 println!("t: {}, f: {}", t, f); }
オペレータ
Rustのオペレータには、算術、比較、論理、ビット演算、型キャストなどがあります。
算術オペレータ
- 加算:
+
- 減算:
-
- 乗算:
*
- 除算:
/
- 剰余:
%
fn main() { let sum = 5 + 10; let difference = 95.5 - 4.3; let product = 4 * 30; let quotient = 56.7 / 32.2; let remainder = 43 % 5; println!("sum: {}, difference: {}, product: {}, quotient: {}, remainder: {}", sum, difference, product, quotient, remainder); }
比較オペレータ
- 等しい:
==
- 等しくない:
!=
- 小さい:
<
- 大きい:
>
- 以下:
<=
- 以上:
>=
fn main() { let a = 5; let b = 10; println!("a == b: {}", a == b); println!("a != b: {}", a != b); println!("a < b: {}", a < b); println!("a > b: {}", a > b); println!("a <= b: {}", a <= b); println!("a >= b: {}", a >= b); }
論理オペレータ
- 論理AND:
&&
- 論理OR:
||
- 論理否定:
!
fn main() { let t = true; let f = false; println!("t && f: {}", t && f); println!("t || f: {}", t || f); println!("!t: {}", !t); }
ビット演算オペレータ
- ビットAND:
&
- ビットOR:
|
- ビットXOR:
^
- ビットNOT:
~
- 左シフト:
<<
- 右シフト:
>>
fn main() { let a = 0b1111_0000; let b = 0b1010_1010; println!("a & b: {:08b}", a & b); println!("a | b: {:08b}", a | b); println!("a ^ b: {:08b}", a ^ b); println!("!a: {:08b}", !a); println!("a << 2: {:08b}", a << 2); println!("a >> 2: {:08b}", a >> 2); }
型キャスト
- 型キャスト:
as
キーワードを使用して型を変換します。
fn main() { let x: i32 = 42; let y: f64 = x as f64 + 0.5; let z: u8 = x as u8; println!("x: {}, y: {}, z: {}", x, y, z); }
usb serial
本章では、usbシリアル通信を用いてrp2040とPCで通信できるようにします。 必要なツールは以下です。
用途 | ツール名 |
---|---|
文字入出力ツール | Tera Term VT |
デバッグツール | Baker link. Dev |
ターゲット | Raspbery Pi pico |
動作の内容は、Tera Termで入力した小文字アルファベットが大文字アルファベットとして返信されます。 以下は、Tera Termでの動作動画です。
本チャプターでは、Baker link. Devを外部マイコンへの書き込み機能で用います。
外部マイコンへの書き込み手順(Rasberry Pi Pico)
- Baker link. Envを起動し、プロジェクト名、createクリック、プロジェクト保存先を選択すると、数秒後にVisual Studio Codeが開きます。
- Visual Studio Codeの左下に表示される「コンテナ―で再度開く」をクリックしてください。すると、Dockerイメージのダウンロード&ビルド処理が開始されます。この処理が数分程度かかりますので、しばらくお待ちください。
- src/main.rsを開き、以下のプログラムに書き換えてください。 コードにはcopilotでコメントを添えています。
#![no_std] // 標準ライブラリを使用しないことを指定 #![no_main] // 標準のエントリーポイント(main関数)を使用しないことを指定 // RTT(リアルタイムトレース)を使用するための設定 use defmt_rtt as _; // パニック時のプローブ設定 use panic_probe as _; // RP2040のハードウェア抽象化レイヤーをインポート use rp2040_hal as hal; // スタートアップ関数のマクロ use hal::entry; // Peripheral Access Crateの短縮エイリアス、低レベルのレジスタアクセスを提供 use hal::pac; // USBデバイスサポート use usb_device::{class_prelude::*, prelude::*}; // USB通信クラスデバイスサポート use usbd_serial::SerialPort; // フォーマットされた文字列を書き込むために使用 use core::fmt::Write; use heapless::String; /// ベアメタルアプリケーションのエントリーポイント。 /// /// `#[entry]`マクロは、Cortex-Mのスタートアップコードがすべてのグローバル変数を初期化した後にこの関数を呼び出すことを保証します。 /// /// この関数はRP2040の周辺機器を設定し、USBシリアル経由で受信した文字をエコーします。 /// #[link_section = ".boot2"] // ブートローダーセクションを指定 #[used] // コンパイラにこの静的変数が使用されることを明示 pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; // ブートローダーのデータ const XTAL_FREQ_HZ: u32 = 12_000_000u32; // 外部クリスタルの周波数を定義 #[entry] fn main() -> ! { // シングルトンオブジェクトを取得 let mut pac = pac::Peripherals::take().unwrap(); // 周辺機器のハンドルを取得 // ウォッチドッグドライバを設定 - クロック設定コードに必要 let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); // ウォッチドッグタイマーを初期化 // クロックを設定 // // デフォルトでは125 MHzのシステムクロックを生成 let clocks = hal::clocks::init_clocks_and_plls( XTAL_FREQ_HZ, // 外部クリスタルの周波数 pac.XOSC, // 外部オシレータ pac.CLOCKS, // クロック制御 pac.PLL_SYS, // システムPLL pac.PLL_USB, // USB PLL &mut pac.RESETS, // リセット制御 &mut watchdog, // ウォッチドッグタイマー ) .ok() .unwrap(); // クロック設定が成功したか確認 let timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); // タイマーを初期化 #[cfg(feature = "rp2040-e5")] // コンパイル時の条件付きコンパイル { let sio = hal::Sio::new(pac.SIO); // シリアル入出力を初期化 let _pins = hal::Pins::new( pac.IO_BANK0, // IOバンク0 pac.PADS_BANK0, // パッドバンク0 sio.gpio_bank0, // GPIOバンク0 &mut pac.RESETS, // リセット制御 ); } // USBドライバを設定 let usb_bus = UsbBusAllocator::new(hal::usb::UsbBus::new( pac.USBCTRL_REGS, // USBコントローラのレジスタ pac.USBCTRL_DPRAM, // USBコントローラのDPRAM clocks.usb_clock, // USBクロック true, // VBUS検出を有効にする &mut pac.RESETS, // リセット制御 )); // USB通信クラスデバイスドライバを設定 let mut serial = SerialPort::new(&usb_bus); // シリアルポートを初期化 // 偽のVIDとPIDでUSBデバイスを作成 let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd)) // USBデバイスを構築 .strings(&[StringDescriptors::default() .manufacturer("Fake company") // メーカー名 .product("Serial port") // 製品名 .serial_number("TEST")]) // シリアル番号 .unwrap() .device_class(2) // デバイスクラスコード(2は通信デバイス) .build(); let mut said_hello = false; // ウェルカムメッセージを表示したかどうかのフラグ loop { // 最初にウェルカムメッセージを表示 if !said_hello && timer.get_counter().ticks() >= 2_000_000 { // タイマーが2秒以上経過したか確認 said_hello = true; // フラグを更新 let _ = serial.write(b"Hello, World!\r\n"); // ウェルカムメッセージを送信 let time = timer.get_counter().ticks(); // 現在のタイマー値を取得 let mut text: String<64> = String::new(); // フォーマットされた文字列を格納するバッファ writeln!(&mut text, "Current timer ticks: {}", time).unwrap(); // タイマー値を文字列に書き込む // これは、シリアルポートに書き込まれるバイト数がUSBペリフェラルに利用可能なバッファよりも小さいため、信頼性があります。 // 一般的には、転送されていないバイトが失われないように、戻り値を処理する必要があります。 let _ = serial.write(text.as_bytes()); // タイマー値をシリアルポートに送信 } // 新しいデータをチェック if usb_dev.poll(&mut [&mut serial]) { // USBデバイスのポーリング let mut buf = [0u8; 64]; // データを格納するバッファ match serial.read(&mut buf) { // シリアルポートからデータを読み込む Err(_e) => { // 何もしない } Ok(0) => { // 何もしない } Ok(count) => { // 大文字に変換 buf.iter_mut().take(count).for_each(|b| { b.make_ascii_uppercase(); // 文字を大文字に変換 }); // ホストに送り返す let mut wr_ptr = &buf[..count]; // 書き込みポインタを設定 while !wr_ptr.is_empty() { // バッファが空になるまでループ match serial.write(wr_ptr) { // シリアルポートに書き込む Ok(len) => wr_ptr = &wr_ptr[len..], // 書き込んだ分だけポインタを進める // エラーが発生した場合、未書き込みデータを破棄します。 // 一つの可能なエラーはErr(WouldBlock)で、これはUSB書き込みバッファがいっぱいであることを意味します。 Err(_) => break, // エラーが発生したらループを抜ける }; } } } } } } // ファイルの終わり
- Baker link. EnvのRunをクリックしてください。するとバックグラウンドで、probe-rsのDAP Serverが起動します。
- Baker link. DevとRasberry Pi PicoをJST-SH型3ピンコネクタケーブルで接続してください。
- Raspberry Pi Picoに電源を供給するために、Raspberry Pi PicoとPCをUSBケーブルで接続します。
- 次のBaker link. Devを外部マイコン書き込みモードで接続するために、真ん中のボタンを押しながら、Baker link. DevとPCをUSBケーブルで接続してください。
外部マイコン書き込みモードで接続されると緑のLEDが点灯します。
- Visual Studio CodeでF5キーを押してください。すると、以下のようなアイコンが表示されます。
- もう一度、F5キーを押すと、プログラムが動作します。Tera Termの設定は、新しい接続でシリアル、ポートは書き込んだマイコンボードのポート番号を選択してください。また、設定->端末->ローカルエコーにチェックを入れると文字を記入できます。
rp-rsを参考にしました。
adc oneshot
RP2040は内部に複数のADC(Analog Digital Converter)を有しています。 RP2040が実装されたRaspberry Pi PicoもBaker link. Devも、GP26,27,28にADC0,ADC1,ADC2が実装されています。
本章では、このADCを使って、電圧値を計測してみます。
必要なもの
Q&A
Q. Baker link. Devのファームウェアを元に戻したい
Baker link. Devのデバッカー側のマイコン(RP2040)に公式ファームウェア以外のプログラムを書き込んだ後に、再度元の公式ファームウェアに戻したい時にどうすればよいか?
A. デバッカーのファームウェア更新 に記載している手順にしたがって、公式ファームウェアを書き込みし直してください。
Q. Baker link. Devが正常に動作しない
Baker link. DevのUSBが途切れたり電源がOFFになったりする。
A. Baker link. Devの基板表面の「Baker link. Dev © 2024」(初版のRev表記なし版)の場合は、37 Pin(EN)のイネーブルピンが未接続(フロート状態)のままだと、ディスイネーブル状態(ENがHigh)の状態になる場合があります。 37 Pin(EN)をLowつまり、37 Pin(EN)と38 Pin(GND)を接続(ショート)しください。
Dev Containers
Docker上で構築した仮想マシンに開発環境構築をし、LocalのVSCodeからアクセスできるVSCodeの機能です。 もし環境構築に失敗してもOSを初期化することをせずに、コンテナー(仮想マシン)を再構築するだけで良くなります。
またDockerfileを丁寧に調整すれば、同じ開発環境を他人に配布することさえできます。 今まで再現性のない環境開発構築に悩まされてきた人たちにとっては、革命的なツールです。
この環境を手軽に利用できるのが、Baker link. Envです。
Baker link. Envは、Dev Containersをセットアップしたテンプレートからプロジェクトを作成し、Localにインストールしたprobe-rsのDAP Serverを起動してくれます。
use
use
の通常パターン
#![allow(unused)] fn main() { use embedded_hal::delay::DelayNs; }
単純に省略しているパターンです。
こうすることでembedded_hal::delay::DelayNs
はDelayNs
で呼び出し可能になります。
use
の*
パターン
*
を最後につけるとそれ以下のmod(モジュール)や関数を呼び出せるようになります。
#![allow(unused)] fn main() { use defmt::*; }
イメージで説明すると、crate_a
の下にモジュール(m1)
があり、m1
内に関数func3
があった場合、それらが省略できるという意味です。
crate_a
├── m1
└── func2
use crate_a::*; fn main(){ m1::func3(); // crate_a::M1::func3としなくてよい func2(); // crate_a::func2()としなくて良い }
as
#![allow(unused)] fn main() { use rp2040_hal as hal; }
as
は、as
の前のクレート名を変更する機能を持っています。この文だとrp2040_hal
からhal
にクレート名を変更しています。
use
のas _
パターン
#![allow(unused)] fn main() { use defmt_rtt as _; use panic_probe as _; }
これは、クレートをmain.rsに取り込んだ後に、as _
でクレート名を_
に変換することで、main.rsで直接呼び出しをできないようにしています。(Underscore Imports)
defmt_rtt
の例で説明すると、defmt_rtt
はdefmt
をrttとして利用する設定にし、その後にas _
をすることでmain.rsで直接参照できなくなるのでmain.rsに悪影響を与えなくなります。
光を灯せ(Lチカ) コンパイル設定
Cargo.toml
cargo generate
で生成したCargo.toml
は次の通りになっているかと思います。
[package]
edition = "2021"
name = "blink"
version = "0.1.0"
license = "MIT OR Apache-2.0"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = { version = "0.2.5", features = ["unproven"] }
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }
# We're using a Pico by default on this template
rp-pico = "0.8"
# but you can use any BSP. Uncomment this to use the pro_micro_rp2040 BSP instead
# sparkfun-pro-micro-rp2040 = "0.6"
# If you're not going to use a Board Support Package you'll need these:
# rp2040-hal = { version="0.8", features=["rt", "critical-section-impl"] }
# rp2040-boot2 = "0.2"
# ~以下省略~
[package]
[package]
には、この作成しているプログラム(blink
)の情報が記載されています。
edition
は、利用するRustのコンパイラーのエディション(バージョンに近いがい少し違う)です。ここは、基本変更の必要はないはずです。(もし、古いエディションのRustが利用したいときは、変更する必要があると思います。)
それ以外として、name
はこのプログラムの名前、version
はこのプログラムのVersion、license
はこのプログラムのライセンスです。これらの情報は直接プログラムに影響を与えるものではないので、任意の値で結構です!
[dependencies]
[dependencies]
には、利用したいcrates.ioに存在するライブラリ(クレート)や自作ライブラリ(クレート)の利用情報を記載します。ここに書かれたライブラリ(クレート)は、src/
の下に描かれるプログラムから参照できるようになり、さらにcrates.ioのライブラリ(クレート)は、インターネットから自動でダウンロードしコンパイルができるようになります。
お気づきかもしれないですが、Rustではライブラリに近いものをクレートと呼んでいます。 これは、他の言語の経験がある方にとってには少し戸惑いがあると思いますが、慣れるとむしろクレートと聞いて少し嬉しい気持ちになったりします! また他にもトレイトと言われるものがあります。これは他の言語でいうインターフェイスに近いものになります。
それぞれ記載しているクレートについて説明します。
cortex-m
Cortex-Mに低レベルにアクセスする処理をするためのクレートです。
Cortexとは、マイクロプロセッサの設計開発をしているARM社のCPUファミリーの名称で、Cortex-A、Cortex-R、Cortex-Mの3つのシリーズがあります。その中でも、Cortex-Mは、ローエンドの組み込み用プロセッサとして開発されてます。後ろについているMは、マイクロコントローラーのMを指しているようです。
cortex-m-rt
Cortex-M マイクロコントローラーのセットアップ用のコードと最小限のランタイムです。
cortex-m
と基本セットで利用する必要があります。
embedded-hal
Rustの組み込み用のトレイトの集まりです。
組み込みで利用するCPUチップは、数多く存在するしますが、細かい違いはあるものも共通的な機能が多いです。このembedded-hal
は、それらの共通化に大きく役に立っています。
ただembeded-hal
には、直接的な処理の機能がないためこれを継承して作成されたクレートを呼び出して利用する必要があり、今回はrp2040-hal
を内包しているrp-pico
がそれに当たります。
詳細は、The Embedded Rust Bookに記載されているのでそちらを見ていただきたいですが、プログラミングに慣れてない方が見ても難しい内容なので、初めは見なくても支障はありません。
halとは、Hardware Abstraction Layerの略で、ハードウェア抽象化層と言われるものです。このhalがハードウェアごとの仕様の違いを吸収し、利用するCPUが異なっていてもユーザーには、別のCPUと同じ使用間でプログラムができるになります。
rp-pico
rp2040をHALであるrp2040-hal
をRaspberry Pi Picoにカスタマイズしたクレートです。
rp2040のGPIOやI2Cなどの制御処理を関数を呼び出すだけで利用できます。
defmt
組み込みプログラミングで効率的にロギングを利用するためのフレームワークです。
defmt
という名前は、deferred formattingから来ているそうです。
defmt-rrt
RTT(Real-Time Transfer)プロトコルで、defmt
のログメッセージを送信するクレートです。
デバッカーのファームウェア更新手順
- Baker link. Devの29 Pin(BOOTSEL 1)と28 Pin(GND)を接続(ショート)し、デバッカー側のマイコン(RP2040)を書き込みモードにしてください。
- releaseの*.uf2ファイルをダウンロードし、ドラック&ドロップで"RPI-RP2"(RP2040の書き込み用フォルダ)に*.uf2ファイルをコピーしてください。(これで書き込まれます)
Baker link. Devのファームウェア(UF2)は、こちらからダウンロード可能です
Baker link. Devによる外部マイコン書き込み
Baker link. Devは、外部マイコン書き込みをサポートしており、SWDデバックに対応している他のARM系CPUにRustのプログラムを書き込み可能です。
本節では、Baker link.Devを利用してRaspberry Pi PicoにRustプログラムを書き込む例に、外部マイコンへの書き込み方法を説明します。
開発環境
Baker link. Devで利用してきたBaker link. Envをそのまま利用することが可能です。 Baker link. Envを含めた環境構築については、環境構築を参照ください。
外部マイコンへの書き込み手順(Rasberry Pi Pico)
- Baker link. Envを起動し、プロジェクト名、createクリック、プロジェクト保存先を選択すると、数秒後にVisual Studio Codeが開きます。
- Visual Studio Codeの左下に表示される「コンテナ―で再度開く」をクリックしてください。すると、Dockerイメージのダウンロード&ビルド処理が開始されます。この処理が数分程度かかりますので、しばらくお待ちください。
- src/main.rsを開き、以下のプログラムに書き換えてください。
このプログラムは、Raspberry Pi PicoのUSB付近のLED(GPIO25)をON/OFFするプログラムです。
#![no_std] #![no_main] use defmt::*; use defmt_rtt as _; use panic_probe as _; use rp2040_hal as hal; use hal::pac; use embedded_hal::delay::DelayNs; use embedded_hal::digital::OutputPin; #[link_section = ".boot2"] #[used] pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; const XTAL_FREQ_HZ: u32 = 12_000_000u32; #[rp2040_hal::entry] fn main() -> ! { info!("Program start!"); let mut pac = pac::Peripherals::take().unwrap(); let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); let clocks = hal::clocks::init_clocks_and_plls( XTAL_FREQ_HZ, pac.XOSC, pac.CLOCKS, pac.PLL_SYS, pac.PLL_USB, &mut pac.RESETS, &mut watchdog, ) .ok() .unwrap(); let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); let sio = hal::Sio::new(pac.SIO); let pins = hal::gpio::Pins::new( pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS, ); let mut led = pins.gpio25.into_push_pull_output(); loop { info!("LED on"); led.set_high().unwrap(); timer.delay_ms(2000); info!("LED off"); led.set_low().unwrap(); timer.delay_ms(2000); } }
- Baker link. EnvのRunをクリックしてください。するとバックグラウンドで、probe-rsのDAP Serverが起動します。
- Baker link. DevとRasberry Pi PicoをJST-SH型3ピンコネクタケーブルで接続してください。
販売しているBaker link. Dev Rev.1のSWDのピン配置は、販売時期によって以下のようにSWDのCLKとDIOの配置が逆転しています。これはストレートケーブルからクロスケーブル対応にBaker link. Devのファームウェアが変更されたためです。正常に通信がされない場合は、CLKとDIOの接続を逆にして接続することをお勧めいたします。
また配線で解決できない場合は、CLKとDIOが逆のバージョンのファームウェアをインストールすることも可能です。
- Raspberry Pi Picoに電源を供給するために、Raspberry Pi PicoとPCをUSBケーブルで接続します。
- 次のBaker link. Devを外部マイコン書き込みモードで接続するために、真ん中のボタンを押しながら、Baker link. DevとPCをUSBケーブルで接続してください。
外部マイコン書き込みモードで接続されると緑のLEDが点灯します。
- Visual Studio CodeでF5キーを押してください。すると、以下のようなアイコンが表示されます。
- もう一度、F5キーを押すと、プログラムが動作します。
info!(...)のログが下の画面に表示されます。
LEDも点灯されます。
この方法は、SWDデバックに対応している他のARM系CPUでも対応可能なので是非別のCPUでも試してみてください!
Baker link. Dev以外のハードウェア
Baker link. Dev以外のハードウェアでも、本チュートリアルを試すことができます。 例えば、Rasberry Pi Pico とRasberry Pi Debug Probeを組み合わせのパターンがあります。