ESP32でstdなRust開発入門 その5 Delay

概要

前回はADCでしたが、今回Delayまわりを調べてみました。

ベースのソースコード

use esp_idf_hal::delay::Ets;
use esp_idf_hal::delay::FreeRtos;
use std::thread;
use std::time::Duration;

fn main() -> anyhow::Result<()> {
    esp_idf_sys::link_patches();

    // 未使用エラー回避用
    thread::sleep(Duration::from_millis(0));
    FreeRtos::delay_ms(0);
    Ets::delay_ms(0);

    loop {
    }
}

loopに何もありません。これではFreeRTOSに処理を戻さないのでウォッチドッグタイマーに引っかかるはずです。

E (10416) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (10416) task_wdt:  - IDLE (CPU 0)
E (10416) task_wdt: Tasks currently running:
E (10416) task_wdt: CPU 0: main
E (10416) task_wdt: CPU 1: IDLE
E (10416) task_wdt: Print CPU 0 (current core) backtrace


Backtrace: 0x40105E22:0x3FFB0BD0 0x4008281D:0x3FFB0BF0 0x40086C65:0x3FFB5E20 0x400D4C8D:0x3FFB5E40 0x400D4491:0x3FFB5E60
 0x40113BB7:0x3FFB5E80 0x400D44D4:0x3FFB5EA0 0x400D65BA:0x3FFB5EC0 0x400D44C4:0x3FFB5EF0 0x400D44A7:0x3FFB5F20 0x400D4C9
F:0x3FFB5F40 0x40114BF8:0x3FFB5F60

E (10416) task_wdt: Print CPU 1 backtrace


Backtrace: 0x40084015:0x3FFB11D0 0x4008281D:0x3FFB11F0 0x4000BFED:0x3FFB6D80 0x4008779A:0x3FFB6D90 0x4010608B:0x3FFB6DB0
 0x40106097:0x3FFB6DE0 0x40101F62:0x3FFB6E00 0x40085E6C:0x3FFB6E20

こんな感じの出力がありました。

Delayの動作確認

use esp_idf_hal::delay::Ets;
use esp_idf_hal::delay::FreeRtos;
use std::thread;
use std::time::Duration;

fn main() -> anyhow::Result<()> {
    esp_idf_sys::link_patches();

    // 未使用エラー回避用
    thread::sleep(Duration::from_millis(0));
    FreeRtos::delay_ms(0);
    Ets::delay_ms(0);

    let mut i: i32 = 0;
    loop {
        i += 1;
        if (i % 100) == 0 {
            println!("i = {}", i);
        }
        FreeRtos::delay_ms(10);
    }
}

原始的なコードですが、100回delayを実行したら出力をする処理となります。上記の場合には10msを100回なので1秒間隔で処理されるはずです。ちなみにidf_monitorだとCtrl+T、Ctrl+Iでシリアルの受信時間が表示されます。残念ながら秒単位ですが覚えておくとよいコマンドになります。

FreeRtos::delay_ms(1)

1msを100回なので0.1秒間隔で表示されるように思えますが、1秒間隔のままです。これはデフォルトのFreeRTOSでは処理の単位が10msになっています。この単位をTickといい、秒針などが進むチックタックのチックですね。Tockは音が違う意味だけでTick Tockと重ねて使う場合に使う用語みたいです。

FreeRTOSではTick以下の値を設定してもTick単位で処理をするので10msの遅延となります。

FreeRtos::delay_ms(0)

0は特殊な数値で、遅延しません。なので高速で出力が流れていきますがよく見るとウォッチドッグタイマーが働いています。また、ウォッチドッグタイマーが発生してもリセットが発生するのではなく、割り込みが発生しているようです。

i = 364800
E (10419) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (10419) task_wdt:  - IDLE (CPU 0)
E (10419) task_wdt: Tasks currently running:
E (10419) task_wdt: CPU 0: main
E (10419) task_wdt: CPU 1: IDLE
E (10419) task_wdt: Print CPU 0 (current core) backtrace


Backtrace: 0x40106A7E:0x3FFB0BD0 0x4008281D:0x3FFB0BF0 0x4000BFED:0x3FFB5D80 0x4008779A:0x3FFB5D90 0x40084063:0x3FFB5DB0 0x40084085:0x3FFB5DD0 0x40086C65:0x3FFB5DF0 0x400D4E11:0x3FFB5E10 0x400D45D4:0x3FFB5E30 0x401149E3:0x3FFB5E80 0x400D462C:0x3FFB5EA0 0x400D673E:0x3FFB5EC0 0x400D461C:0x3FFB5EF0 0x400D45FF:0x3FFB5F20 0x400D4E23:0x3FFB5F40 0x401158C8:0x3FFB5F60

E (10419) task_wdt: Print CPU 1 backtrace


Backtrace: 0x40084015:0x3FFB11D0 0x4008281D:0x3FFB11F0 0x4000BFED:0x3FFB6D80 0x4008779A:0x3FFB6D90 0x40106CE7:0x3FFB6DB0 0x40106CF3:0x3FFB6DE0 0x40102BBE:0x3FFB6E00 0x40085E6C:0x3FFB6E20

i = 364900

そのため上記のようにiのカウントが初期化されていませんので、再起動ではないみたいでした。

FreeRtos::delay_ms(11)

11を指定すると10ms単位なので20ms単位となり、それを100回ですので2秒間隔で表示されます。ちなみに19や20を指定しても内部的には20msの遅延になります。

FreeRtos::delay_us(1000)

us指定の関数を使ってみたいと思います。1000usなので1msになります。1ms指定だと10ms遅延となりますので1秒間隔になりました。

FreeRtos::delay_us(999)

超高速表示になりウォッチドッグタイマーにひっかかりました。ソースを見ると1000で割ってdelay_msに渡しているだけでした。つまりus単位では遅延できません。

Ets::delay_us(1000)

FreeRtos以外にもEtsのDelayが準備されていましたのでこちらを使ってみました。すると0.1秒間隔で表示されており、5秒後にウォッチドッグタイマーが働いていました。

Ets::delay_us(999)

こちらもほぼ0.1秒間隔で表示されていました。ソースを確認したところEtsの内部はusで動いていてmsで呼び出すと1000倍してからus関数を呼び出しているようです。

thread::sleep(Duration::from_millis(1))

Rustの標準的なSleepです。1msの遅延が100回ですので0.1秒間隔で出力がありました。そして5秒経過するとウォッチドッグタイマーにひっかかりました。

thread::sleep(Duration::from_millis(10))

10msに増やしてみました。表示は1秒間隔になったのですが、なんとウォッチドッグタイマーにひっかかりません。1Tick以上の遅延は自動的にFreeRTOSに処理になるようです。

thread::sleep(Duration::from_millis(11))

念のため11msにしてみました。11msが100回ですので1.1秒間隔になるかと思ったら2秒間隔になりました。1Tick以上の遅延でのthread::sleep関数の中身はFreeRtos::delay_msですね。

Delay間隔を10msから1msに変更する

# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000

# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
# This allows to use 1 ms granuality for thread sleeps (10 ms by default).
#CONFIG_FREERTOS_HZ=1000

# Workaround for https://github.com/espressif/esp-idf/issues/7631
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n

sdkconfig.defaultsにて設定を変更することが可能です。上から2段落目にデフォルトで設定例が書いてあります。FreeRTOSのデフォルトは100Hzになっています。つまり1秒を100個のTickに分解しているので1Tickは10msになります。これを1000Hzにすることで1Tickが1msになります。

CONFIG_FREERTOS_HZ=1000

上記のようにコメントアウトされている設定を有効化することで変更が可能です。ちなみにこの設定を変更するとesp-idf系のビルドし直しになりますので分単位で時間がかかりますので結構重いです。そしてデフォルトは10msなので、新しいプロジェクトで変更し忘れて、コピペしたコードのdelayが全部おかしくなるとかあるので注意して利用してください。

まとめ

FreeRTOSの1Tickはデフォルトで10ms。変更可能であるがdelayはその単位で動作する。5秒間に1度はFreeRtos::delay_ms()を呼ぶ必要がある。呼び出すときには1以上である必要があり、1Tickが10msの場合には1から10を指定しても繰り上げてすべて10msの遅延となる。

厳密な時間調整が必要な場合にはEts::delay_us()を利用する。ただし、ウォッチドッグタイマーがあるので定期的にFreeRtos::delay_ms(1)を呼ぶ必要がある。もしくは10ms単位はFreeRtos::delay_ms()で、それ以下はEts::delay_us()と呼び分ける必要がある。

thread::sleep()関数の中身はTick以上の遅延はFreeRtos::delay_ms()なのでTick単位での遅延時間となる。Tick未満の場合にはEts::delay_us()相当のビジーループとなる。1msのループなどを作るとウォッチドッグタイマーに引っかかるので個人的にはFreeRtos::delay_ms()と、Ets::delay_us()を意図的に使い分けたほうが安全だと思います。

ウォッチドッグタイマーはデフォルトではメッセージ表示のみで再起動はしない。例外系の処理はまた別途調べてみたいと思います。

コメント