ラズピコでRust(14) UARTのリングバッファを embedded-hal Ver1.0.0 に更新する

ラズピコでRust(14) UARTのリングバッファを embedded-hal Ver1.0.0 に更新する

皆さん こんにちは。
ポンコツRustacean の moon です。

以前つくったUARTのリングバッファの処理を embedded-hal Ver1.0.0 に対応させてみます。
GPIO出力の場合、コード自体そのまま使えましたが、UARTの場合にはコードを少し変更する必要がありました。

リングバッファ(元の記事)に関しては ラズピコでRust(6) UARTでリングバッファを使ってみる を参照してください。

この記事は開発環境を構築することを前提にしています。
環境構築について知りたい方は こちらの記事 をご覧になってください。

上記の環境を構築することで、PCとUARTで通信することができるようになります。
その環境で今回紹介するコードを動かしてみました。

このサイトは 書籍 基礎から学ぶ組込みRust を参考にしています。

パッケージの複製を作成する

まず私のGitHubリポジトリにあるリングバッファのパッケージをベースにして別名でパッケージをつくります。
元パッケージはデバッグ環境用のものですから、その複製もデバッグできるようになります。

新しいパッケージ名を rp2040-ehv1-uart-receiver として git clone します。
xxxxは皆さんのユーザー名です。
カレントディレクトリを rp2040-ehv1-uart-receiver に移し、VSCodeを起動します。

ehv1 は embedded-hal ver1 の略です。

C:\Users\xxxx>cd pprp
C:\Users\xxxx\pprp>git clone https://github.com/moons3925/rp2040-uart-receiver.git rp2040-ehv1-uart-receiver
C:\Users\xxxx\pprp>cd rp2040-ehv1-uart-receiver
C:\Users\xxxx\pprp\rp2040-ehv1-uart-receiver>code .

Cargo.tomlを編集する

[dependencies]を以下の通りに編集します。

[dependencies]
rp2040-hal = "0.10.2"
rp-pico = "0.9.0"
embedded-hal = "1.0.0"
embedded-io = "0.6.1"
panic-halt = "0.2.0"
cortex-m-rt = "0.7.3"
cortex-m = "0.7.7"
critical-section = "1.1.2"

それから [package] の name を以下の通りにします。

name = "rp2040-ehv1-uart-receiver"

launch.jsonを編集する

.vscode にある launch.json を編集します。
executable: の名称を rp2040-uart-receiver から rp2040-ehv1-uart-receiver に変更します。

"executable": "./target/thumbv6m-none-eabi/debug/rp2040-ehv1-uart-receiver",

main.rsを編集する

main.rsを以下の通りに編集します。

#![no_std]
#![no_main]

use embedded_hal::digital::OutputPin;
use embedded_io::{Read, Write};

use hal::gpio::bank0::{Gpio0, Gpio1};
use hal::pac;
use hal::pac::interrupt;
use hal::uart::{DataBits, StopBits, UartConfig};
use panic_halt as _;
use rp2040_hal as hal;
use rp2040_hal::fugit::RateExtU32;
use rp2040_hal::Clock;
use rp_pico::entry;
type UartPins = (
    hal::gpio::Pin,
    hal::gpio::Pin,
);

const BUFF_SIZE: usize = 2048;
struct RingBuffer {
    buffer: [u8; BUFF_SIZE],
    read_ptr: usize,
    write_ptr: usize,
}

impl RingBuffer {
    fn read(&mut self) -> u8 {
        let mut byte = 0;
        critical_section::with(|_| {
            byte = self.buffer[self.read_ptr];
            self.read_ptr = (self.read_ptr + 1) & (BUFF_SIZE - 1);
        });
        byte
    }
    fn write(&mut self, c: u8) {
        self.buffer[self.write_ptr] = c;
        self.write_ptr = (self.write_ptr + 1) & (BUFF_SIZE - 1);
    }
    fn readable(&mut self) -> bool {
        let mut b = false;
        critical_section::with(|_| {
            if self.read_ptr == self.write_ptr {
                b = false;
            } else {
                b = true;
            }
        });
        b
    }
    fn writable(&mut self) -> bool {
        if (self.write_ptr + 1) & (BUFF_SIZE - 1) == self.read_ptr {
            return false;
        } else {
            return true;
        }
    }
}

static mut RING: RingBuffer = RingBuffer {
    buffer: [0; BUFF_SIZE],
    read_ptr: 0,
    write_ptr: 0,
};

static mut UART_RECEIVER: Option<hal::uart::Reader<pac::UART0, UartPins>> = None;

fn on_idle(writer: &mut hal::uart::Writer) {
    unsafe {
        while RING.readable() {
            let mut c: [u8; 1] = [0; 1];
            c[0] = RING.read();
            if b'a' <= c[0] && c[0] <= b'z' {
                let _ = writer.write(&c[..]);
            } else if b'A' <= c[0] && c[0] <= b'Z' {
                let _ = writer.write(&c[..]);
            } else if c[0] == 0x0a || c[0] == 0x0d {
                let _ = writer.write(&c[..]);
            }
        }
    }
}

#[entry]
fn main() -> ! {
    let mut pac = pac::Peripherals::take().unwrap();
    let core = pac::CorePeripherals::take().unwrap();
    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
    let clocks = hal::clocks::init_clocks_and_plls(
        rp_pico::XOSC_CRYSTAL_FREQ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());

    let sio = hal::Sio::new(pac.SIO);

    let pins = rp_pico::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    let uart_pins = (
        // UART TX (characters sent from RP2040) on pin 1 (GPIO0)
        pins.gpio0.reconfigure(),
        // UART RX (characters received by RP2040) on pin 2 (GPIO1)
        pins.gpio1.reconfigure(),
    );
    let uart = hal::uart::UartPeripheral::new(pac.UART0, uart_pins, &mut pac.RESETS)
        .enable(
            UartConfig::new(9600.Hz(), DataBits::Eight, None, StopBits::One),
            clocks.peripheral_clock.freq(),
        )
        .unwrap();

    let (mut uart_rx, mut uart_tx) = uart.split();

    unsafe {
        pac::NVIC::unmask(hal::pac::Interrupt::UART0_IRQ);
    }

    uart_rx.enable_rx_interrupt();

    critical_section::with(|_| unsafe {
        UART_RECEIVER = Some(uart_rx);
    });

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

    loop {
        on_idle(&mut uart_tx);
        led_pin.set_high().unwrap();
        delay.delay_ms(100);
        led_pin.set_low().unwrap();
    }
}

#[interrupt]
fn UART0_IRQ() {
    unsafe {
        if let Some(ref mut uart_recv) = UART_RECEIVER.as_mut() {
            let mut c: [u8; 1] = [0; 1];
            uart_recv.read(&mut c[..]).unwrap();
            if RING.writable() {
                RING.write(c[0]);
            }
        }
    }
}

PC側の準備

通信の確認用として Tera Term を使います。

インストール後、 Tera Term を起動し、シリアルポートでUSBシリアル変換モジュールのCOMポートを選択します。

もしCOMポートが複数ある場合には、USBシリアル変換モジュールのケーブルをはずしてデバイスマネージャーでCOMポート番号を確認しておきます。

起動後、設定メニューのシリアルポートから以下の設定を行います。

スピート : 9600
データ : 8 bit
パリティ : なし
ストップビット : 1
フロー制御 : none

これらの値は送受信する相手と合わせておく必要があります。

改行コードを設定する

設定 – 端末 で改行コードを LF に設定します。

ビルドして実行する

Run – Start Debugging (F5) からプログラムを実行します。

Tera Termで文字を打ち込みenterキーで改行され、アルファベッドだけが表示されれば成功です。

nbクレートは使わなくて良い

私はこれまでのところブロッキングI/Oで処理するコードを書いています。
ブロッキングとは処理が終わるまで、その関数(メソッド)から戻ってこない処理のことです。
それに対して関数(メソッド)から処理が完了する前に戻ってくるものをノンブロッキングと言います。

それに対するものとして、ノンブロッキングI/Oと非同期I/O処理があります。

ノンブロッキングI/Oは処理が終わっていない場合にエラーを返します。
非同期I/Oは処理の完了をシグナルかコールバックでお知らせするものです。

embedded-hal Ver 0.2.x のリングバッファのコードではUARTの送信処理に nb::block! マクロを使っていました。
これはまだデータを送り終えていない場合にエラーを返して処理を待たせるというマクロです。

今回のrp2040-halのUARTのWriter構造体のコードは以下のようになっているため、nb::block!マクロを使う必要はありません。
メソッド内部で同様の処理がなされているためです。

下の match 内の Err(WouldBlock) => continue, の部分です。

impl<D: UartDevice, P: ValidUartPinout<D>> embedded_io::Write for Writer<D, P> {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
        self.write_full_blocking(buf);
        Ok(buf.len())
    }
}
/// This function blocks until the full buffer has been sent.
pub(crate) fn write_full_blocking(rb: &RegisterBlock, data: &[u8]) {
    let mut temp = data;

    while !temp.is_empty() {
        temp = match write_raw(rb, temp) {
            Ok(remaining) => remaining,
            Err(WouldBlock) => continue,
            Err(_) => unreachable!(),
        }
    }
}

コードの比較

短いコードですから新旧のmain.rsをWinMergeなどのツールを使って比較すると、その違いを確認することができます。

GitHubにアップ

GitHubの rp2040-ehv1-uart-receiver にプロジェクトをアップしましたので参考になさってください。

お疲れさまでした。

UARTカテゴリの最新記事