概要
この記事は、Raspberry Pi PicoにRust言語を使って組込みを行う際のTips集です。
各電子パーツを使って実現したいことから、対応するコードの記法を探せるようになっています。
適宜追加するので、ちょくちょく見に来てもらえると良いことがあるかもしれません。
(この記事は「Rust Advent Calendar 2021|25日目に登録されています)
(この記事は「Raspberry Pi Advent Calendar 2021|25日目に登録されています)
編集記録
2022/04/22
- 開発環境を
rp_pico_examples
へ移行 - PWMのDuty比の説明を修正
- 回路図を写真から
frintzing
に変更
2023/08/21
- コードを最新版の
rp2040-hal
クレートに対応するため、コードをアップデート - 一部表現を修正
開発の基本
必要なもの
当然ですが、Raspberry Pi Picoは絶対に必要です。
ピンヘッダをハンダ付けして、ブレットボードに刺しておくと良いでしょう。
あとはLEDや抵抗があると最低限の動作確認ができます。必要なら、各種センサやモーター、ジャンパワイヤなどを用意するとGood。
環境構築
環境構築の方法は
を参照してください。
艮電算術研究所では、RustでRaspberry Pi Picoの開発を行うためのテンプレートをGithubに公開しています。
この記事のコードは、テンプレートを使用して作成したrp_pico_examples
にbin
ファイルを追加する形で開発を進めています。
Raspberry Pi Picoへの書き込み
Raspberry Pi Picoに新規ファイルを書き込むときは、Raspberry Pi Picoのボード上にある白色のボタン(BOOTSEL)を押しながらPCにUSB接続します。
こうしないと、PCがRaspberry Pi Picoを認識してくれません。
その後、シェルでプロジェクトディレクトリに移動して
...> cargo run --release --bin ファイル名
とすることで、ファイルの内容が実行されます。
(この時、ファイルはRaspberry Pi Picoへと書き込まれます。そのため、給電のみを行った際<白ボタンを押さずにPCに刺した場合や、バッテリーに繋いだ場合など>には、既に書き込まれているファイルの内容が実行されます)
例文集
すべての基本/ボード上でLチカ
すべての基本となる記述は、src/led.rs
を参照してください。
use
の辺りや、main
内の
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();
は、どんなコードでも必要になってくる部分ですし、watchdog
, clocks
はdelay
(待機)を使用するために不可欠です(そして、delay
を使わないことは滅多にありません)。
このサンプルを実行すると、Raspberry Pi Picoのボード上にあるLEDが点灯・消灯を繰り返します。
let mut led_pin = pins.led.into_push_pull_output();
が、led_pin
にボード上LEDを指定し、
loop {
led_pin.set_high().unwrap();
delay.delay_ms(500);
led_pin.set_low().unwrap();
delay.delay_ms(500);
}
が、上から
- LED点灯
- 500ミリ秒待機
- LED消灯
- 500ミリ秒待機
を実行し、給電が無くなるまでこの動作を繰り返します。
慣れないうちは、
- とりあえず基本となる部分をおまじないとして記述
- 使うピンを定義
- loopの中に好きな処理を書いていく
という流れで開発を進めると良いでしょう。
外部のLEDを点灯・消灯(出力ピン)
解説
let mut led_pin = pins.gpio3.into_push_pull_output();
led_pin
に指定するピンの番号を変更すると、そのピンを操作する事ができます。
今回はgpio3
、つまり5番のピンを指定しました(ピン番号とGPIOは通常異なるので、下の図を参照してください)。
配線図
外部LEDは、100Ωの抵抗を介して5番ピンと3番ピン(GND)を繋ぐように接続します。
コード例
スイッチを使う(入力ピン)
解説
スイッチなどの入力を受け付けるためには、InputPin
を使用します。
LED用のOutputPin
に加え、これもインポートするようにしましょう。
// GPIO traits
use embedded_hal::digital::v2::{OutputPin, InputPin};
今回はgpio5
をスイッチ入力用のピンに指定します。
// Set an input from a switch to gpio5
let switch = pins.gpio5.into_pull_up_input();
ここではPull Upという入力形式を採用しました。これは、ボタンを押していないときにhigh
、押したときにlow
となる方式です。そのため
// Blink the LED by a switch
loop {
delay.delay_ms(5);
if switch.is_high().ok().unwrap() {
led_pin.set_low().ok().unwrap();
} else {
led_pin.set_high().ok().unwrap();
}
}
と記述することで、スイッチを押すとLEDが点灯するような回路を作ることができます。
配線図
コード例
LEDの明るさを調節する(PWMの基本)
解説
これまではRaspberry Pi Picoからの信号の出力にembedded_hal::digital::v2::OutputPin
を使っていましたが、ここではPWMの機能を利用します。
PWMは、出力をHighとする時間の割合を変化させることにより、0/1の出力だけで連続値を表現する手法です。
このHighの時間割合をDuty比と言います。
use embedded_hal::PwmPin;
(前々節と同じgpio3
でも良いですが)今回はgpio5
をPWMのピンとして使ってみましょう。
// Init PWMs
let mut pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
// Configure PWM2
let pwm = &mut pwm_slices.pwm2;
pwm.set_ph_correct();
pwm.enable();
// Output channel B on PWM2 to the LED pin
let channel = &mut pwm.channel_b;
channel.output_to(pins.gpio5);
PWMの使い方は以下のとおりです。
- PWM sliceの作成
- 対応する番号のPWMの有効化(今回は
PWM2
) - チャンネルの定義と出力ピンの設定(今回は
channel_b
をgpio5
に出力)
PWMのチャンネルにはAとBがあり、2種類の信号を使い分けることができます。
また、今回はgpio5
を使用するのでPWM2
、channel_b
を指定しましたが、このPWM番号やチャンネルはピン番号に対応して変化します。
PWM番号・チャンネルとGPIO番号の関係は以下のようになっています。
GPIO | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
PWM | 0A | 0B | 1A | 1B | 2A | 2B | 3A | 3B | 4A | 4B |
GPIO | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
---|---|---|---|---|---|---|---|---|---|---|
PWM | 5A | 5B | 6A | 6B | 7A | 7B | 0A | 0B | 1A | 1B |
GPIO | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
---|---|---|---|---|---|---|---|---|---|---|
PWM | 2A | 2B | 3A | 3B | 4A | 4B | 5A | 5B | 6A | 6B |
肝心の明るさは、Duty比の調整channel.set_duty()
によって制御できます。
let duty_max = channel.get_max_duty();
// Infinite loop, fading LED up and down
loop {
delay.delay_ms(1000);
channel.set_duty(duty_max);
delay.delay_ms(1000);
channel.set_duty(duty_max / 2);
delay.delay_ms(1000);
channel.set_duty(0);
}
set_duty()
の引数の最大値はget_max_duty()
により取得することができます。
Duty比が0のとき消灯し、最大値で最も明るくなります(普通のOutputPin
から3.3Vを給電したときと同等)。
なお、get_max_duty()
により得られる最大値は、後述するPWMパラメータのtop
と等しくなります。
配線図
コード例
サーボを制御する(PWMの周波数を設定する)
解説
PWMを利用することで、LEDの明るさ調整以外にも例えばサーボモーターを制御することができます。
ここではSG90-HV
の連続回転サーボを制御してみます。
PWMの基本周波数は、Raspberry Pi Picoのクロック数と同じ125MHzですが、今回はこれを50Hzまで落として制御します。
// Set the PWM frequency to 50Hz
pwm.set_top(24999);
pwm.set_div_int(100);
pwm.set_div_frac(0);
周波数の引き下げにはtop
とdiv
の変数が使えます。
このうちdiv
は小数を指定することができ、プログラム的にはset_div_int()
とset_div_frac()
で、それぞれ整数部と小数部を分けて与えます。
マイコンのクロック数を \(\mathrm{clk}\) とした場合、PWMの周波数 \(f\) は次のように計算されます。
$$f=\frac{\mathrm{clk}}{(\mathrm{top}+1)\cdot\mathrm{div}}$$
ただし、
$$\mathrm{div}=\mathrm{div_{int}}+\frac{\mathrm{div_{frac}}}{16}$$
です。今回の場合、
$$f=\frac{125\times 10^{6}}{(24999+1)\times 100}=50$$
となります。
サーボの回転速度はDuty比によって制御できます。
// Infinite loop, rotating the servo left or right
// SG90-HV datasheet
// Right Max: 25000 * 5.0% = 1250
// Stop : 25000 * 7.5% = 1875
// Left Max: 25000 * 10.0% = 2500
loop {
channel.set_duty(1250);
delay.delay_ms(5000);
channel.set_duty(1875);
delay.delay_ms(5000);
channel.set_duty(2500);
delay.delay_ms(5000);
}
配線図
コード例
時間を計測する
解説
Raspberry Pi Picoに搭載されているペリフェラルタイマーを使って、時間を計測することができます。
// Create a timer
let timer = hal::timer::Timer::new(pac.TIMER, &mut pac.RESETS);
ここではdelay
を使わずに、1HzのLチカを実装してみましょう。
// Blink the LED at 1 Hz
loop {
led_pin.set_high().unwrap();
let start = timer.get_counter().ticks();
while timer.get_counter().ticks() - start < 500_000 {}
led_pin.set_low().unwrap();
let start = timer.get_counter().ticks();
while timer.get_counter().ticks() - start < 500_000 {}
}
Raspberry Pi Picoのペリフェラルタイマーは、1μ秒でカウンタを1つ繰り上げます。
そのため、timer.get_counter().ticks()
で現在のカウンタの値を調べつつ、0.5秒 = 500,000μ秒経過するまでwhile
文を回し続けることでdelay.delay_ms(500)
と同等の処理を実装することができます。
(2022/04/22追記)
なお、delay
を用いないプログラムを書く場合、clocks
を定義せずともコンパイルが通る場合がありますが、その結果、timer
の精度が大きく狂うことがあります。
そのため、未使用の場合でもclocks
を定義しておくと良いでしょう。
配線図
コード例
シリアル通信をする
解説
シリアル通信を使うと、Raspberry Pi Pico内の様々な情報をPCに出力することができます。
Raspberry Pi Picoへのファイルの書き込みと同時に、ターミナルをシリアル通信に使えるようにするため、.cargo/config
の1行を次のように書き換えておきましょう。
(rp_pico_template
では、既に以下の処理を行っているため、変更は不要です)
# This runner will make a UF2 file and then copy it to a mounted RP2040 in USB
# Bootloader mode:
runner = "elf2uf2-rs -d -s" # 変更後
# runner = "elf2uf2-rs -d" # 変更前
examples
内のファイルには、必要なものをインポートしておきます。
// USB Device support
use usb_device::{class_prelude::*, prelude::*};
// USB Communications Class Device support
use usbd_serial::SerialPort;
main
関数の中では、USBバス、シリアルポート、USBデバイスを定義します。
// Set the USB bus
let usb_bus = UsbBusAllocator::new(hal::usb::UsbBus::new(
pac.USBCTRL_REGS,
pac.USBCTRL_DPRAM,
clocks.usb_clock,
true,
&mut pac.RESETS,
));
// Set the serial port
let mut serial = SerialPort::new(&usb_bus);
// Set a USB device
let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd))
.manufacturer("Fake company")
.product("Serial port")
.serial_number("TEST")
.device_class(2)
.build();
ここの設定の詳細は、電子書籍「Rust x Raspberry Pi Picoで実装する IMUからの姿勢情報の取得と応用」のシリアル通信の項を参照してください。
ここでは例として、hello!
を1秒毎にターミナル上に表示するコードを書いてみましょう。
let mut count = 0;
// Infinite loop, saying `hello!`
loop {
delay.delay_ms(5);
let _ = usb_dev.poll(&mut [&mut serial]);
count += 1;
if count == 200 {
let _ = serial.write(b"hello!\r\n");
count = 0;
}
}
表示はserial.write()
によって行われます。表示する文字列はバイナリ列を指定することに注意してください。
シリアル通信を行う際のもう1つの注意点として、usb_dev.poll(&mut [&mut serial])
があります。
これはシリアル通信によって新規に書き込む・読み込むべきデータが存在するか「お伺いを立てる」関数です。
これが長時間実行されないとシリアル通信は終了してしまうので、5msに1回はお伺いを立てるようにしましょう。
そのため、5msが200回溜まったらhello!
を出力するという少々回りくどいことをしています。
コード例
数字を出力する
解説
シリアル通信等で数値を出力したい場合、これを文字列型に変換する必要がありますが、no_std
環境ではなかなか難しい作業になります。
numtoa
はこの変換を行うためのクレートです。
// Convert a number to a string
use numtoa::NumToA;
1秒毎にhello!
を出力し、同時にその累積回数count_time
も表示するコードは次のように書けます。
let mut count = 0;
let mut count_time = 0;
// Buffer for NumToA
let mut buf = [0u8; 20];
// Infinite loop, saying `hello!` and counting the time
loop {
delay.delay_ms(5);
let _ = usb_dev.poll(&mut [&mut serial]);
count += 1;
if count == 200 {
count_time += 1;
let _ = serial.write(b"hello! x");
let s = count_time.numtoa(10, &mut buf);
let _ = serial.write(s);
let _ = serial.write(b"\r\n");
count = 0;
}
}
NumtoA
で変換を行う際にはバッファbuf = [0u8; 20]
が必要になるので事前に定義しておきます。
その上で、整数型に対して.numtoa(10, &mut buf)
を行うことで、10進数の文字列型へと変換できます。
この変換は小数型には対応していないので、とある小数f: f64
を変換したい場合、例えば
f
を整数型と見て(f_int = f as i32
、整数部の切り出し)、numtoa
変換→表示"."
を表示- 表示したい小数点以下の桁数に合わせて
f - f_int as f64
を10のべき乗倍し、それを整数型と見て(小数部の切り出し)、numtoa
変換→表示
という手順を踏む必要があります(ただし手順3.において、桁数に応じたゼロ埋めを行う必要あり)。
この手間を簡略化するため、艮電算術研究所ではserial_write
というクレートを開発しました。
このクレートを用いることで、
- 文字列
- 整数
- 小数
- 整数の配列
- 小数の配列
の表示を1行で行うことができます。
また、小数は指数表示にも対応しています。
コード例
応用
超音波測距(シリアル通信表示)
タイマー・PWM・シリアル通信を組み合わせて、超音波センサによる距離測定デバイスを作成します。
IMU(慣性計測センサ)で姿勢推定
技術書典14で発行した上の電子書籍では、コードの基本部分やシリアル通信について、より詳しく解説しています。
また、IMU(慣性計測センサ)の調整・読み込みを行い、現在のデバイスの姿勢(傾き・回転)を取得し、PC画面にグラフ表示することができるようになります。
Raspberry Pi Picoのフラッシュ領域に情報を保存する方法についても解説しているので、本格的な組込みRustを行いたい方におすすめです。
Comments