組込みRust実装例集―Raspberry Pi Picoで電子工作

Rust
Sponsored

概要

この記事は、Raspberry Pi PicoにRust言語を使って組込みを行う際のTips集です。

各電子パーツを使って実現したいことから、対応するコードの記法を探せるようになっています。

適宜追加するので、ちょくちょく見に来てもらえると良いことがあるかもしれません。

(この記事は「Rust Advent Calendar 2021|25日目に登録されています)

Rustのカレンダー | Advent Calendar 2021 - Qiita
Rustのカレンダーページです。

(この記事は「Raspberry Pi Advent Calendar 2021|25日目に登録されています)

Raspberry Piのカレンダー | Advent Calendar 2021 - Qiita
Raspberry Piのカレンダーページです。

編集記録

2022/04/22

  • 開発環境をrp_pico_examplesへ移行
  • PWMのDuty比の説明を修正
  • 回路図を写真からfrintzingに変更

2023/08/21

  • コードを最新版のrp2040-halクレートに対応するため、コードをアップデート
  • 一部表現を修正

開発の基本

必要なもの

当然ですが、Raspberry Pi Picoは絶対に必要です。

ピンヘッダをハンダ付けして、ブレットボードに刺しておくと良いでしょう。

あとはLEDや抵抗があると最低限の動作確認ができます。必要なら、各種センサやモーター、ジャンパワイヤなどを用意するとGood。

環境構築

環境構築の方法は

組込みRust爆速入門―Raspberry Pi Picoで電子工作
Raspberry Pi Picoを用いた、組込みRustの入門編です。開発を爆速で始められるよう、便利なテンプレートを用いて「Lチカ」を実行する方法について解説します。作業は約10分で完了し、その後は参考文献を見ながら思い通りの電子工作をすることができます。

を参照してください。

艮電算術研究所では、RustでRaspberry Pi Picoの開発を行うためのテンプレートをGithubに公開しています。

GitHub - doraneko94/rp_pico_template: Template for developing Raspberry Pi Pico in Rust.
Template for developing Raspberry Pi Pico in Rust. - GitHub - doraneko94/rp_pico_template: Template for developing Raspberry Pi Pico in Rust.

この記事のコードは、テンプレートを使用して作成したrp_pico_examplesbinファイルを追加する形で開発を進めています。

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を参照してください。

https://github.com/doraneko94/rp_pico_examples/blob/main/src/led.rs

useの辺りや、main内の

let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();

は、どんなコードでも必要になってくる部分ですし、watchdog, clocksdelay(待機)を使用するために不可欠です(そして、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);
}

が、上から

  1. LED点灯
  2. 500ミリ秒待機
  3. LED消灯
  4. 500ミリ秒待機

を実行し、給電が無くなるまでこの動作を繰り返します。

慣れないうちは、

  1. とりあえず基本となる部分をおまじないとして記述
  2. 使うピンを定義
  3. loopの中に好きな処理を書いていく

という流れで開発を進めると良いでしょう。

外部のLEDを点灯・消灯(出力ピン)

解説

let mut led_pin = pins.gpio3.into_push_pull_output();

led_pinに指定するピンの番号を変更すると、そのピンを操作する事ができます。

今回はgpio3、つまり5番のピンを指定しました(ピン番号とGPIOは通常異なるので、下の図を参照してください)。

配線図

外部LEDは、100Ωの抵抗を介して5番ピンと3番ピン(GND)を繋ぐように接続します。

コード例

https://github.com/doraneko94/rp_pico_examples/blob/main/src/led_ex.rs

スイッチを使う(入力ピン)

解説

スイッチなどの入力を受け付けるためには、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が点灯するような回路を作ることができます。

配線図

コード例

https://github.com/doraneko94/rp_pico_examples/blob/main/src/led_ex_switch.rs

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の使い方は以下のとおりです。

  1. PWM sliceの作成
  2. 対応する番号のPWMの有効化(今回はPWM2
  3. チャンネルの定義と出力ピンの設定(今回はchannel_bgpio5に出力)

PWMのチャンネルにはAとBがあり、2種類の信号を使い分けることができます。

また、今回はgpio5を使用するのでPWM2channel_bを指定しましたが、このPWM番号やチャンネルはピン番号に対応して変化します。

PWM番号・チャンネルとGPIO番号の関係は以下のようになっています。

GPIO0123456789
PWM0A0B1A1B2A2B3A3B4A4B
GPIO10111213141516171819
PWM5A5B6A6B7A7B0A0B1A1B
GPIO20212223242526272829
PWM2A2B3A3B4A4B5A5B6A6B

肝心の明るさは、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と等しくなります。

配線図

コード例

https://github.com/doraneko94/rp_pico_examples/blob/main/src/led_ex_pwm.rs

サーボを制御する(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);

周波数の引き下げにはtopdivの変数が使えます。

このうち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);
}

配線図

コード例

https://github.com/doraneko94/rp_pico_examples/blob/main/src/servo_pwm_hz.rs

時間を計測する

解説

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を定義しておくと良いでしょう。

配線図

コード例

https://github.com/doraneko94/rp_pico_examples/blob/main/src/led_ex_timer.rs

シリアル通信をする

解説

シリアル通信を使うと、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!を出力するという少々回りくどいことをしています。

コード例

https://github.com/doraneko94/rp_pico_examples/blob/main/src/serial_hello.rs

数字を出力する

解説

シリアル通信等で数値を出力したい場合、これを文字列型に変換する必要がありますが、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を変換したい場合、例えば

  1. fを整数型と見て(f_int = f as i32、整数部の切り出し)、numtoa変換→表示
  2. "."を表示
  3. 表示したい小数点以下の桁数に合わせてf - f_int as f64を10のべき乗倍し、それを整数型と見て(小数部の切り出し)、numtoa変換→表示

という手順を踏む必要があります(ただし手順3.において、桁数に応じたゼロ埋めを行う必要あり)。

この手間を簡略化するため、艮電算術研究所ではserial_writeというクレートを開発しました。

GitHub - doraneko94/serial_write: Simplifying serial output in a no_std environment, both string and numeric.
Simplifying serial output in a no_std environment, both string and numeric. - GitHub - doraneko94/serial_write: Simplifying serial output in a no_std environmen...

このクレートを用いることで、

  • 文字列
  • 整数
  • 小数
  • 整数の配列
  • 小数の配列

の表示を1行で行うことができます。

また、小数は指数表示にも対応しています。

コード例

https://github.com/doraneko94/rp_pico_examples/blob/main/src/serial_number.rs

応用

超音波測距(シリアル通信表示)

タイマー・PWM・シリアル通信を組み合わせて、超音波センサによる距離測定デバイスを作成します。

Rust x Raspberry Pi Pico で超音波測距
概要 この記事では、 Raspberry Pi Pico に超音波測距モジュール HC-SR04 を接続し、それらを Rust 言語によって制御して、物体との距離を測る方法を説明する。 仕様 (※写真と回路図は、 超音波測距モジュールの向き...

IMU(慣性計測センサ)で姿勢推定

技術書典14で発行した上の電子書籍では、コードの基本部分やシリアル通信について、より詳しく解説しています。

また、IMU慣性計測センサ)の調整・読み込みを行い、現在のデバイスの姿勢(傾き・回転)を取得し、PC画面にグラフ表示することができるようになります。

Raspberry Pi Picoのフラッシュ領域に情報を保存する方法についても解説しているので、本格的な組込みRustを行いたい方におすすめです。

Comments