概要
この記事は、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
に変更
開発の基本
必要なもの
当然ですが、Raspberry Pi Picoは絶対に必要です。
ピンヘッダをハンダ付けして、ブレットボードに刺しておくと良いでしょう。
あとはLEDや抵抗があると最低限の動作確認ができます。必要なら、各種センサやモーター、ジャンパワイヤなどを用意するとGood。
環境構築
Rustで組み込みを行うためには様々なクレートが必要になりますが、Raspberry Pi Picoを用いるなら、rp-hal
のリポジトリをフォークし、boards/pico/examples
にファイルを追加する形でコードを書いていくのが一番ラクです。
この記事でも、その方式で進めていきます。
そしてrp-hal/README.md
の内容を参考に、必要なものをrustup
, cargo install
等で追加してください。
(2022/04/22追記)
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番のピンを指定しました。
なお、LEDは100Ωの抵抗を介して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
を指定しましたが、このPWMの番号はピン番号に対応して変化します。(適宜エラーを確認しながら修正してください)
肝心の明るさは、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()
により取得することができ、0で消灯し、最大値で最も明るくなります(普通のOutputPin
から3.3Vを給電したときと同等)。
なお、get_max_duty()
により得られる最大値は、後述するPWMパラメータのtop
と等しくなります。
配線図
コード例
サーボを制御する(PWMの周波数を設定する)
解説
PWMを利用することで、LEDの明るさ調整以外にも例えばサーボモーターを制御することができます。
ここではSG90-HV
の連続回転サーボを制御してみます。
その際、サーボの説明書を読むと、PWMの周波数を固定した上での説明が書かれている場合が多いのですが、この周波数は使用するマイコンによって変化します。例えば、Raspberry Pi Picoの場合はクロック数と同じ125MHzです。
今回は、この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();
while timer.get_counter() - start < 500_000 {}
led_pin.set_low().unwrap();
let start = timer.get_counter();
while timer.get_counter() - start < 500_000 {}
}
Raspberry Pi Picoのペリフェラルタイマーは、1μ秒でカウンタを1つ繰り上げます。
そのため、timer.get_counter()
で現在のカウンタの値を調べつつ、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行を次のように書き換えておきましょう。
# 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();
ここの設定は(正直良くわかっていないので)おまじないとして記述しても良いと思います。(適当で良い設定にはフェイクの値を入れています)
ここでは例として、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
変換→表示
という手順を踏む必要があります。
コード例
応用
超音波測距(シリアル通信表示)

コメント