スポンサーリンク

Rust x Raspberry Pi Picoで電子工作―実装例集

プログラミング
5
(1)

概要

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

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

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

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

Calendar for Rust | Advent Calendar 2021 - Qiita
Calendar page for Rust.

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

Calendar for Raspberry Pi | Advent Calendar 2021 - Qiita
Calendar page for Raspberry Pi.

開発の基本

必要なもの

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

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

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

環境構築

Rustで組み込みを行うためには様々なクレートが必要になりますが、Raspberry Pi Picoを用いるなら、rp-halのリポジトリをフォークし、boards/pico/examplesにファイルを追加する形でコードを書いていくのが一番ラクです。

GitHub - rp-rs/rp-hal: A Rust Embedded-HAL for the rp series microcontrollers
A Rust Embedded-HAL for the rp series microcontrollers - GitHub - rp-rs/rp-hal: A Rust Embedded-HAL for the rp series microcontrollers

この記事でも、その方式で進めていきます。

そしてrp-hal/README.mdの内容を参考に、必要なものをrustup, cargo install等で追加してください。

Raspberry Pi Picoへの書き込み

Raspberry Pi Picoに新規ファイルを書き込むときは、Raspberry Pi Picoのボード上にある白色のボタン(BOOTSEL)を押しながらPCにUSB接続します。

こうしないと、PCがRaspberry Pi Picoを認識してくれません。

その後、シェルでboards/picoに移動して

.../rp_hal/boards/pico> cargo run --release --example ファイル名

とすることで、ファイルの内容が実行されます。

(この時、ファイルはRaspberry Pi Picoへと書き込まれます。そのため、給電のみを行った際(白ボタンを押さずにPCに刺した場合や、バッテリーに繋いだ場合など)には、その内容が実行されるようになります)

例文集

すべての基本/ボード上でLチカ

すべての基本となる記述は、boards/pico/examples/pico_blinky.rsを参照してください。

Build software better, together
GitHub is where people build software. More than 73 million people use GitHub to discover, fork, and contribute to over 200 million projects.

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番のピンを指定しました。

なお、LEDは100Ωの抵抗を介して3番ピン(GND)に繋いであります。

配線図

コード例

rp-hal/led_ex.rs at main · doraneko94/rp-hal
A Rust Embedded-HAL for the rp series microcontrollers - rp-hal/led_ex.rs at main · doraneko94/rp-hal

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

解説

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

配線図

コード例

rp-hal/led_ex_switch.rs at main · doraneko94/rp-hal
A Rust Embedded-HAL for the rp series microcontrollers - rp-hal/led_ex_switch.rs at main · doraneko94/rp-hal

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を使用するのでPWM2を指定しましたが、このPWMの番号はピン番号に対応して変化します。(適宜エラーを確認しながら修正してください)

肝心の明るさは、Duty比の調整channel.set_duty()によって制御できます。

// Infinite loop, fading LED up and down
loop {
    delay.delay_ms(1000);
    channel.set_duty(65535);
    delay.delay_ms(1000);
    channel.set_duty(25000);
    delay.delay_ms(1000);
    channel.set_duty(0);
}

引数の値は0(消灯)~65535(最大)です。

配線図

コード例

rp-hal/led_ex_pwm.rs at main · doraneko94/rp-hal
A Rust Embedded-HAL for the rp series microcontrollers - rp-hal/led_ex_pwm.rs at main · doraneko94/rp-hal

サーボを制御する(PWMの周波数を設定する)

解説

PWMを利用することで、LEDの明るさ調整以外にも例えばサーボモーターを制御することができます。

ここではSG90-HVの連続回転サーボを制御してみます。

その際、サーボの説明書を読むと、PWMの周波数を固定した上での説明が書かれている場合が多いのですが、この周波数は使用するマイコンによって変化します。例えば、Raspberry Pi Picoの場合はクロック数と同じ125MHzです。

今回は、この125MHz50Hzまで落とします。

// 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
loop {
    channel.set_duty(300);
    delay.delay_ms(5000);
    channel.set_duty(920);
    delay.delay_ms(5000);
    channel.set_duty(1540);
    delay.delay_ms(5000);
}

配線図

コード例

rp-hal/motor_pwm_hz.rs at main · doraneko94/rp-hal
A Rust Embedded-HAL for the rp series microcontrollers - rp-hal/motor_pwm_hz.rs at main · doraneko94/rp-hal

時間を計測する

解説

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)を実装することができます。

配線図

コード例

rp-hal/led_ex_timer.rs at main · doraneko94/rp-hal
A Rust Embedded-HAL for the rp series microcontrollers - rp-hal/led_ex_timer.rs at main · doraneko94/rp-hal

シリアル通信をする

解説

シリアル通信を使うと、Raspberry Pi Pico内の様々な情報をPCに出力することができます。

Raspberry Pi Picoへのファイルの書き込みと同時に、ターミナルをシリアル通信に使えるようにするため、rp-hal直下の.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!を出力するという少々回りくどいことをしています。

コード例

rp-hal/serial_hello.rs at main · doraneko94/rp-hal
A Rust Embedded-HAL for the rp series microcontrollers - rp-hal/serial_hello.rs at main · doraneko94/rp-hal

数字を出力する

解説

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

という手順を踏む必要があります。

コード例

rp-hal/serial_number.rs at main · doraneko94/rp-hal
A Rust Embedded-HAL for the rp series microcontrollers - rp-hal/serial_number.rs at main · doraneko94/rp-hal

応用

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

解説&配線図

近日公開します。

コード例

rp-hal/ultrasonic_ranging.rs at main · doraneko94/rp-hal
A Rust Embedded-HAL for the rp series microcontrollers - rp-hal/ultrasonic_ranging.rs at main · doraneko94/rp-hal

この記事は役に立ちましたか?

星をクリックして、評価してください!

現在の平均評価 5 / 5. 評価した人数: 1

お役に立てたようで嬉しいです!

著者SNSをフォローしていただけると、更新情報が手に入ります。

記事がご期待に沿えなかったようで、申し訳ありません…。

是非、改善点を教えてください!

この記事において改善すべき点や、追加で知りたかったことは何ですか?

コメント