RP2040(Raspberry Pi Pico) - RustでUART出力

Written on 2024-06-02

Raspberry Pi Pico(RasPico)上で、Rustを使ってUART出力をする方法。

bootloaderのサブ記事だが、独立させておく。

RasPico上でRustを使ってUART出力をする

RasPicoにはRTTという文字出力のほうもうもある。 しかし、UARTは汎用的でなにかと便利だ。使えたほうがありがたいことも多い。

  • HALのuartuseする
  • into_function()を使ってGPIOピンをUART用に割り当てて、uart_pinsというタプルに束縛しておく
  • UartPeripheralを初期化する(new()->enable()->unwrap())
  • uart.write_full_blocking()で文字出力ができる。Rustの通常の文字列はUTF-8だが、この引数はバイト列なのでb"..."を使う
/// uart のHALをuseする。fugitは周波数や時刻の演算用ライブラリ
 use rp2040_hal::{
    clocks::{init_clocks_and_plls, Clock},
    fugit::RateExtU32, // time calculation library
    gpio::Pins,
    pac,
    sio::Sio,
    uart::{DataBits, StopBits, UartConfig, UartPeripheral},   /// HALからUARTをインポートする。この行を追加
    watchdog::Watchdog,
 };
 
/// UARTの初期化
    let uart_pins = (pins.gpio0.into_function(), pins.gpio1.into_function());  // RP2040(BSPではなくHAL)のGP0とGP1をUARTに使う
    let uart = UartPeripheral::new(pac.UART0, uart_pins, &mut pac.RESETS)      // クロックを初期化しつつUARTも初期化する
        .enable(
            UartConfig::new(115200.Hz(), DataBits::Eight, None, StopBits::One),
            clocks.peripheral_clock.freq(),
        )
        .unwrap();

/// メッセージの出力は `write_full_blocking()`で。
/// Rustの通常の文字列はUTF-8だが、ここの引数はUTF-8ではなくバイト列で渡す。
    uart.write_full_blocking(b"bootloader stated...\r\n");

/// ビルド設定を出力するようにしておくと便利    #[cfg(debug_assertions)]
    uart.write_full_blocking(b"bootloader debug build\r\n");

    #[cfg(not(debug_assertions))]
    uart.write_full_blocking(b"bootloader release build\r\n");

UARTの出力は cuなど、好みのターミナルソフトで表示できる。cuの場合、終了は~.

❯ sudo cu -l /dev/tty.usbmodem13202 -s 115200
Connected.
bootloader stated...
bootloader debug build
bootloader on!
bootloader off!
bootloader on!
bootloader off!
bootloader on!
~.

Disconnected.

uartを引数で渡す

関数を呼び出した場合、その呼んだ先でUART出力を行いたい。そのようなときは、上記のように作成したuartオブジェクトも引数として関数に渡さなければならない。初期化時は型が自動で推定されるが、引数として渡すときは関数の型を、自力で、適切に定義する必要がある。

一瞬、グローバル変数にしてしまえ、と思うが、Rustの場合はミュータブルな静的変数はunsafeになってしまう。ここはおとなしく引数で渡すほうがいいだろう。

このような型付け、境界条件付けは初心者には難しい。コンパイラのエラーを丁寧に読み解いていく必要がある。rust-analyzerが自動でやってくれるとよいのだが。

呼ぶ側

    let mut uart = UartPeripheral::new(pac.UART0, uart_pins, &mut pac.RESETS)
        .enable(
            UartConfig::new(115200.Hz(), DataBits::Eight, None, StopBits::One),
            clocks.peripheral_clock.freq(),
        )
        .unwrap();

    ih_print(&ih, &mut uart);

関数定義

  • 引数の型は、&mut UartPeripheralUartPeripheral<S, D, P>の型パラメータを取る(generic)。
  • 引数がジェネリックなので、関数もジェネリックになり、型パラメータと、実際の型を指定する。
  • writeln!()を使うために、UartPeripheralWriteトレイト境界を付ける。そうすると、Writeトレイトがついているオブジェクトを、writeln!()の第一引数で渡すことで、fmt!()で文字列をフォーマットして出力することができる。
fn ih_print<
    S: rp2040_hal::uart::State,
    D: rp2040_hal::uart::UartDevice,
    P: rp2040_hal::uart::ValidUartPinout<D>,
>(
    ih: &ImageHeader,
    uart: &mut UartPeripheral<S, D, P>
)
where UartPeripheral<S, D, P>: Write{
    writeln!(uart, "header_magic: {:04x}\r", ih.header_magic).unwrap();
}

ジェネリック引数の型付けやトレイト境界はヤヤコシイが、それさえ処理すれば、すぐにfmt!()が使えるのは、とても便利。オブジェクトにDebugトレイトがついていれば{:?}などのデバッグ出力も使える。