ラズピコでRust(4) 番犬にトレイト

  • 2023.09.29
  • WDT
ラズピコでRust(4) 番犬にトレイト

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

今回はウォッチドッグタイマーのコードを実装したところ、うまく動いてくれなかったのでその対策について書きます。

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

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

ウォッチドッグタイマーとは

番犬タイマーと呼べば良いでしょうか、WDTとかWDGなどと省略して呼ばれています。
カウンタを監視していて、オーバーフローするとマイコンにリセットをかける等の機能を持ちます。
例えばプログラムが暴走して動かなくなった場合にリセットをかけて正常に動作させます。
通常はリセットがかからないようにカウンタをリフレッシュします。

以前は電源電圧の低下を監視する機能なども含んだ専用のICで実現することが主流でしたが、今ではマイコンに内蔵しているものが多くなっています。
コスト的にも、こちらを使わない手はありません。

launch.jsonを編集する

executable: の名称を rp2040 から rp2040-watchdog に変更します。

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

Cargo.tomlを編集する

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

[dependencies]
cortex-m = "0.7.4"
cortex-m-rt = "0.7.1"
embedded-time = "0.12.1"
futures = { version = "0.3.21", default-features = false }
embedded-hal = { version = "0.2.7", features = ["unproven"] }
panic-halt = "0.2.0"
rp2040_lib = { path = "rp2040_lib" }
rp-pico = "0.7.0"
fugit = "0.3.7" # millis()

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

name = "rp2040-watchdog"

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

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

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

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

ライブラリクレートの作成

書籍の105ページ以降に書かれている方法でライブラリクレートを作成し、トレイトを実装した構造体をwatchdogモジュールとしてパッケージに収めてみます。

まず以下のコマンドでライブラリクレートを作成し、VSCodeを起動します。

C:\Users\xxxx\pprp\rp2040-watchdog>cargo new --lib rp2040_lib
C:\Users\xxxx\pprp\rp2040-watchdog>code .

上位のCargo.tomlを編集する

rp2040-watchdogフォルダにある Cargo.toml のdependencies に次の2行を追加します。
fugitは組み込み用のタイムライブラリです。
たまたま今回使うので、そのクレートを取り込んでおきます。

rp2040_lib = { path = "rp2040_lib" }
fugit = "0.3.7" # millis()

下位のCargo.tomlを編集する

rp2040-libフォルダにある Cargo.toml を以下のように編集します。
(dependencies にクレートを2つ追加する)

[package]
name = "rp2040_lib"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rp_pico = "0.7.0"
rp2040_hal = "0.8.2"

lib.rsを編集する

rp2040-lib\src フォルダにある lib.rs を以下のように編集します。
(元コード(テストコード)は削除します)

#![no_std]
pub mod watchdog; // watchdog.rs が見えるようにする

watchdog.rsを作成・編集する

rp2040-lib\srcフォルダ(lib.rsがあるフォルダ)に watchdog.rs を追加し、以下のように編集します。

use rp_pico::hal::watchdog::Watchdog as Wdt;
use rp_pico::hal::pac::WATCHDOG;
pub trait WatchdogPauseOff {    // (1)
    fn pause_off(&self);
}

impl WatchdogPauseOff for Wdt {
    fn pause_off(&self) {   // (2)
        unsafe {(*WATCHDOG::ptr()).ctrl.modify(|_, w| w.pause_dbg0().bit(false).pause_dbg1().bit(false).pause_jtag().bit(false))} // (3)
    }
}

(1)トレイトをつくります。
(2)Watchdog構造体にトレイトを実装します。
(3)PACによる modify() の実装です。

main.rsを編集する

#![no_std]
#![no_main]

use bsp::entry;
use embedded_hal::digital::v2::OutputPin;
use panic_halt as _;
use rp_pico as bsp;
use bsp::hal::{
    clocks::{init_clocks_and_plls, Clock},
    pac,
    sio::Sio,
};

use embedded_hal::watchdog::{Watchdog, WatchdogEnable}; // (1) feed() , start()

use fugit::ExtU32;  // (2) millis()

//use rp2040_lib::watchdog::WatchdogPauseOff; // (3) 追加するトレイトを使えるようにする

#[entry]
fn main() -> ! {
    let mut pac = pac::Peripherals::take().unwrap();
    let core = pac::CorePeripherals::take().unwrap();
    let mut watchdog = bsp::hal::watchdog::Watchdog::new(pac.WATCHDOG);
    let sio = Sio::new(pac.SIO);

    // External high-speed crystal on the pico board is 12Mhz
    let external_xtal_freq_hz = 12_000_000u32;
    let clocks = init_clocks_and_plls(
        external_xtal_freq_hz,
        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 pins = bsp::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );
    let mut led_pin = pins.led.into_push_pull_output();

    led_pin.set_high().unwrap();
    delay.delay_ms(3000);

    watchdog.start(3000.millis()); // (4)

//    watchdog.pause_off(); // (5)
    
    for _ in 0..5 {
        led_pin.set_low().unwrap();
        delay.delay_ms(500);
        led_pin.set_high().unwrap();
        delay.delay_ms(500);
        watchdog.feed(); // (6)
    }

    loop {
        led_pin.set_high().unwrap();
        delay.delay_ms(100);
        led_pin.set_low().unwrap();
        delay.delay_ms(100);
    }

}

(1)Watchdog構造体、WatchdogEnableトレイトを使えるようにします。
(2)millis()を使えるようにします。このコードでは(4)でstart()の引数として3000msecを渡すために使います。
(3)今回つくったライブラリクレートのトレイトを使えるようにします。
(4)ウォッチドッグタイマーをスタートさせます。
(5)ウォッチドッグタイマーのpauseをオフにします。(ウォッチドッグタイマーを有効にします)
(6)ウォッチドッグタイマーにカウンタをロードします。

ビルドして実行する

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

まず提供しているコードでは main.rs の (3)と(5) watchdog.pause_off() メソッドをコメントにしています。

この場合の挙動は
3秒間LEDが点灯する。
その後5秒間LEDがゆっくりと点滅する。
その後永遠にLEDが速く点滅する。
となります。

ゆっくりと点滅しているforループではLチカ処理の間に feed()メソッドをはさんでいるのでウォッチドッグタイマーはリセットをかけません。
デクリメントタイマーになっていてカウンタが0になるとリセットがかかるようになっています。

forループをぬけて loop {} に入るとfeed()メソッドがないので、しばらくするとカウンタが0になりリセットがかかるはずです。
しかし実際には速い点滅を繰り返したままでリセットがかかりません。

では一度プログラムを停止して (3)及び(5)のコメントをはずしてビルドし直して再度プログラムを実行します。
今度はpauseしないようにレジスタを書き換えたので、LEDが速く点滅して少しするとリセットがかかるようになりました。

バグ?

この挙動はバグだと思います。
以下にラズピコのマイコン RP2040 のウォッチドッグタイマーのコントロールレジスターを示します。

CTRLレジスタには ENABLE(Bit30) のビットとPAUSE(Bit24,25,26) のビットが共存しています。

従って例えばENABLEのビットに 0/1 を書く場合、PAUSE(一時停止)のビットがその影響を受けないようにしなければなりません。

このような場合、PACでは modify() を使うようにしなければなりませんが、rp2040_hal の Watchdog 構造体のメソッドの実装では write() が使われています。

rust-embedded の Memory Mapped Registers を見て頂くと確認できるのですが、write()の場合設定しないサブフィールドは全てデフォルト値が書かれるようになっています。

PAUSE(Bit24,25,26)のデフォルト値ではウォッチドッグタイマーは無効(一時停止)ですから、enable()が呼ばれるとウォッチドッグタイマーはリセットがかからなくなります。

この挙動に関連するメソッドは start() , pause_on_debug() , enable() です。

enable() と pause_on_debug() はそれぞれ write() を使っていますので問題があるため、modify() を使うべきです。

start()内で enable() を使っているので start()前に pause_on_debug() でウォッチドッグタイマーを有効にしても、start()することでデフォルト値に書き換わってしまいます。

そこでトレイトを使って modify()で PAUSE(Bit24,25,26) の各ビットを 0 にするメソッドを追加しました。

pause_off()というメソッド名で、start()後にこの処理を入れることでリセットがかかるようになりました。

もちろんfeed()している間は正常に動作しますので、その後のfeed()をはずした処理でloopしてからリセットがかかることになります。

とりあえずこれでウォッチドッグタイマーが動作するようになりました。

メソッドの説明:

start() : ウォッチドッグタイマーの動作をスタートする
feed() : カウンターに値をロードする(リセットがかからないようにリフレッシュする)

HALとトレイト

少し話題をかえて、HAL と トレイトの話をします。

私はHALを使い抽象化してコードを書くことに賛成です。
メンテナンス性や移植の際に有利であることが大きな理由です。

それはPACだけを使ってガリガリとレジスタベースでコードを書きたくないということでもあります。

そこでここではトレイトと抽象化について説明します。

まずウォッチドッグタイマーに関するトレイトは以下の3つです。
(コードは省略して書いています)

pub trait Watchdog { // (1)
    fn feed(&mut self);
}

pub trait WatchdogEnable { // (1)
    fn start(&mut self, period: T);
}

pub trait WatchdogDisable { // (1)
    fn disable(&mut self);
}

これらは embedded_hal の実装です。
各メーカーのマイコンは各ペリフェラルの構造体(今回はウォッチドッグタイマー)にこれらのトレイトを実装します。
disable()は使わないこともありそうですが、feed()とstart()はウォッチドッグタイマーを使う上で必要になります。

このように embedded_hal のトレイトがインターフェースの仕様になるため、これに合わせてコーディングをすればアプリケーション側では(マイコンのレジスタ等の)ハードウェアを意識する必要がありません。

次にトレイトに対する構造体の実装も見ておきましょう。
(コードは省略して書いています)

impl watchdog::Watchdog for Watchdog { // (2)
    fn feed(&mut self) {
    }
}

impl watchdog::WatchdogEnable for Watchdog { // (2)
    fn start>(&mut self, period: T) {
    }
}

impl watchdog::WatchdogDisable for Watchdog { // (2)
    fn disable(&mut self) {
    }
}

おそらくどのマイコンメーカーも構造体には embedded_hal のトレイトに合わせたコードを実装しているはずです。
それによって抽象化が実現でき、プロジェクト数が増えても移植が容易にできるようになります。

ラズピコのファームウェアのレイヤイメージを以下に示しておきます。
上に掲示したコードの(1),(2)が図の(1),(2)に相当します。
(3)のPACは出て来ていませんが(2)の内部でPACを使っています。

(1)がembedded_halのトレイトの定義
(2)が構造体へのトレイトの実装
(3)がPeripreral Access Crate

これとは別に今回追加したトレイトは元構造体のコードを直接編集できないために機能をカスタムする目的としました。
トレイトがこのような使い方をできるのは便利です。

個人的には Watchdog構造体の start() , enable(), pause_on_debug() 等のメソッドを書き直したいところですが、そうもいきません。
GitHubに issue するべきかも知れません。

GitHubにアップ

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

お疲れさまでした。

WDTカテゴリの最新記事