ラズピコでRust(6) UARTでリングバッファを使ってみる

ラズピコでRust(6) UARTでリングバッファを使ってみる

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

今回はUARTの受信処理にリングバッファを使ってみました。

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

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

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

リングバッファとは

UART等マイコンのペリフェラルで受信したデータを一時的にバッファに保存するメモリー領域のうち、領域の先頭と最後を論理的につないでおき循環的に使えるようにしたものをリングバッファと言います。

書き出すのみの場合だと、いずれ領域を使い切ってしまい過去に書いた領域を上書きすることになり古いデータがなくなってしまいます。
領域が有限なので、書き出しと読み出しのバランスが重要になります。
仕様に合わせてリングバッファを上手に使う必要があります。

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

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

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

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

Cargo.tomlを編集する

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

[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "0.2.7"
rp2040-hal = "0.10.0"
defmt = "0.3"
defmt-rtt = "0.4"
rp-pico = "0.9"
fugit = "0.3.7"
panic-halt = "0.2.0"
critical-section = "1.1.2"
nb = "1.0.0"

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

name = "rp2040-uart-receiver"

launch.jsonを編集する

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

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

main.rsを編集する

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

#![no_std]
#![no_main]

use embedded_hal::digital::v2::OutputPin;
use embedded_hal::serial::{Read, Write};
use fugit::RateExtU32;
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::Clock;
use rp_pico::entry;
type UartPins = (
    hal::gpio::Pin<Gpio0, hal::gpio::FunctionUart, hal::gpio::PullNone>,
    hal::gpio::Pin<Gpio1, hal::gpio::FunctionUart, hal::gpio::PullNone>,
);

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<pac::UART0, UartPins>) {
    unsafe {
        while RING.readable() {
            let c = RING.read();
            if b'a' <= c && c <= b'z' {
                let _ = nb::block!(writer.write(c));
            } else if b'A' <= c && c <= b'Z' {
                let _ = nb::block!(writer.write(c));
            } else if c == 0x0a || c == 0x0d {
                let _ = nb::block!(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() {
            if let Ok(c) = uart_recv.read() {
                if RING.writable() {
                    RING.write(c);
                }
            }
        }
    }
}

PC側の準備

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

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

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

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

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

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

改行コードを設定する

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

ビルドして実行する

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

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

プログラムの概要

まず UART の送受信オブジェクト(構造体)を分離します。
UartPeripheral 構造体に split() という関数があります。
これで構造体を Reader(受信側) と Writer(送信側) に分けられるので扱いやすくなります。

受信側はメインスレッドと割り込み処理の双方で使うので、UART_RECEIVER というグローバル変数をつくりました。
メインスレッドは初期設定だけなので、この変数に Mutex は不要だと思い省きました。
例え unsafe なコードを書こうとも、余計な処理にかかるオーバーヘッドは避けたい考えです。

グローバル変数の UART_RECEIVER は Option<T>のNoneで初期化しておき後から T に Reader をリプレースします。

criticalsection::with()関数

中身が難しくて理解が追い付いていないのですが、非アトミックなアクセスだろうと思われる部分にクリティカルセクションをかぶせてみました。
このプロジェクトでは割り込みとメインスレッドの2つのスレッドだけです。
割り込み処理で使うメソッドがメインスレッドに割り込まれることはないのでクリティカルセクションを省いてみました。

この関数は引数にクロージャーを使っていて、クロージャーの引数にクリティカルセクションを渡しています。
非アトミックな部分をより安全にアクセスするしくみのようです。

概略イメージとしては、まず割り込みを禁止して、非アトミックな部分を処理した後に割り込みを許可します。

リングバッファに負荷をかける

Tera Term のマクロを使うと定期送信できたり、手入力と違いまとめて大量のデータを送信することができます。
そうするとリングバッファらしく取り扱うことができます。

詳細は省きますけれど、ネット上に記事がゴロゴロ転がっていますので、ご興味ある方は調べて使ってみてください。

GitHubにアップ

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

お疲れさまでした。

UARTカテゴリの最新記事