手元にあったラズパイ4で、Yocto組込みLinuxのリアルタイム性能を測ってみた

つぶつぶコラム

はじめに

最近、組込みLinux、特にYoctoを使った案件のお問い合わせが増えてきました。「そういえば、実際どれくらいリアルタイム性能が出るんだろう?」とふと気になったので、手元に転がっていたRaspberry Pi 4を使って、ちょっと試してみることにしました。

比較対象として、STM32 Nucleo(F446RE)ボード+ Arduino + FreeRTOSという、いわゆるベアメタルフレームワーク + RTOS環境でも同じ測定を行い、数字を並べてみています。

今回はあくまで「手軽に試してみた」レベルの検証です。厳密な学術的検証というよりは、実務の肌感覚として参考にしていただければと思います。

測定方法

やりたいことはシンプルで、「一定周期でGPIOをトグルさせて、その周期がどれだけブレるか(ジッタ)」を見る、というものです。

手軽にやりたかったので、Linuxの timerfd という仕組みで1ms周期のタイマを作り、満了するたびにGPIOの出力(0/1)を反転させています。理論上は2ms周期の矩形波になるはずですが、実際には多少のタイミングのズレが発生します。このズレを「ジッタ」として記録しました。

ポイントは以下の通りです。

・タイマ: timerfd_create() を使った1ms周期のタイマ

・GPIO制御: Linuxの標準的なsysfs経由(/sys/class/gpio/…/value への書き込み)

・スケジューリング: SCHED_FIFO優先度80(組込みLinuxアプリでは一般的なリアルタイム優先度設定)

・ジッタの記録: clock_gettime(CLOCK_MONOTONIC) で前回のトグルからの経過時間を測り、期待値(1ms)とのズレを記録

コード自体は200行程度のシンプルなCプログラムです。sysfs経由のwrite()にかかる時間もあえて測定に含めているので、実際のアプリが体感するオーバーヘッド込みの数字になっています。

環境構築

今回使った環境は以下の通りです。

・Raspberry Pi 4

・Yocto(Poky)ベースのminimal構成イメージ

・カーネル: 6.6.63(特別なリアルタイムパッチなどは無し、標準的なプリエンプティブルカーネル)

Yoctoでの環境構築も、実はそんなに手間はかかりません。参考までに手順を載せておきます(WSL上での作業を想定しています)。

Yocto(Scarthgap)の取得とビルド環境の作成

# 必要パッケージ(Ubuntu/Debian系の例)
$ sudo apt update
$ sudo apt install -y gawk wget git diffstat unzip texinfo gcc build-essential chrpath socat cpio python3 python3-pip python3-pexpect xz-utils debianutils iputils-ping python3-git python3-jinja2 libegl1-mesa libsdl1.2-dev python3-subunit mesa-common-dev zstd liblz4-tool file locales libacl1
 
# 各レイヤーをclone(Scarthgapブランチ)
$ git clone -b scarthgap git://git.yoctoproject.org/poky.git
$ git clone -b scarthgap git://git.openembedded.org/meta-openembedded
$ git clone -b scarthgap https://github.com/agherzan/meta-raspberrypi.git
 
# ビルド環境を作成(build-raspi/ が作られ、カレントディレクトリもそこに移動する)
$ source ./poky/oe-init-build-env build-raspi/
 
# 使うレイヤーを追加
$ bitbake-layers add-layer ../meta-openembedded/meta-oe
$ bitbake-layers add-layer ../meta-openembedded/meta-python
$ bitbake-layers add-layer ../meta-openembedded/meta-networking
$ bitbake-layers add-layer ../meta-raspberrypi
$ bitbake-layers add-layer ../meta-raspi400-performance-check
 
# conf/local.conf を編集(後述の内容を追記)
 
# ビルド実行
$ bitbake core-image-minimal

あとはビルドが終わったSDカードイメージを書き込んで起動すれば、`rt-gpio-square` コマンドがそのまま使える状態になっています。ちなみに、現時点(2026年8月)のScarthgapの素の設定だと、カーネルバージョンは6.6系になります。

$ uname -a
Linux raspberrypi4-64 6.6.63-v8 #1 SMP PREEMPT Fri Dec  6 10:10:05 UTC 2024 aarch64 GNU/Linux

レイヤー構成(bblayers.conf、上記コマンドを実行すると自動生成される内容)

BBLAYERS ?= " \
  ${TOPDIR}/../poky/meta \
  ${TOPDIR}/../poky/meta-poky \
  ${TOPDIR}/../poky/meta-yocto-bsp \
  ${TOPDIR}/../meta-openembedded/meta-oe \
  ${TOPDIR}/../meta-openembedded/meta-python \
  ${TOPDIR}/../meta-openembedded/meta-networking \
  ${TOPDIR}/../meta-raspberrypi \
  ${TOPDIR}/../meta-raspi400-performance-check \
  "

local.conf への追加設定

MACHINE = "raspberrypi4-64"
 
# シリアルコンソールを有効化
ENABLE_UART = "1"
SERIAL_CONSOLES = "115200;ttyS0"
 
# 自作のテストアプリをイメージに含める
IMAGE_INSTALL:append = " rt-gpio-square"

テスト用レシピ(rt-gpio-square.bb)

SUMMARY = "Simple SCHED_FIFO 1ms timer GPIO square wave test"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=b1527676ccfbafba56875d2b6a7c4881"
 
SRC_URI = "file://rt_gpio_square.c"
S = "${WORKDIR}"
 
do_compile() {
    ${CC} ${CFLAGS} ${LDFLAGS} rt_gpio_square.c -o rt-gpio-square -lm
}
 
do_install() {
    install -d ${D}${bindir}
    install -m 0755 rt-gpio-square ${D}${bindir}/rt-gpio-square
}
 
FILES:${PN} = "${bindir}/rt-gpio-square"

これだけの設定で bitbake でイメージをビルドすれば、テスト用のバイナリが最初から入った状態のSDカードイメージが出来上がります。既存のYocto環境に、実質4ファイルを足すだけで済みました。

結果

起動後に rt-gpio-square を実行し、約73分間動作させた結果です。

 約73分run
サンプル数4,391,079
取りこぼし(1周期超の遅延)0回
ジッタ平均ほぼ0us
ジッタ最小/最大-13.35 / +12.81 us
標準偏差2.44 us
±10us以内の割合99.996%
GPIO write()時間(最小/平均/最大)2.87 / 4.02 / 6.98 us

特別なリアルタイムパッチを入れていない、ごく普通のYocto minimal構成で、1時間以上動かして1周期分の取りこぼしが一度も無く、ズレも最悪で±13us強に収まっている、というのは素直に「思ったよりやるな」という印象でした。

実際にターミナル上で実行したときの表示は以下の通りです(Ctrl-Cで計測を打ち切った時点までの統計が表示されます)。

root@raspberrypi4-64:~# rt-gpio-square
Running 1ms timer, toggling GPIO 530 (2ms period square wave)
^C
--- timerfd stats (total=4391081, errored=1, max_expirations=1) ---
 
--- jitter stats (samples=4391079) ---
mean                    :    -0.00 us
min                     :   -13.35 us
max                     :    12.81 us
stddev                  :     2.44 us
jitter <  -100us        : 0
-100us <= jitter < -10us: 1
 -10us <= jitter <  10us: 4390825
  10us <= jitter <  20us: 253
  20us <= jitter <  30us: 0
  30us <= jitter <  50us: 0
  50us <= jitter < 100us: 0
 100us <= jitter < 200us: 0
 200us <= jitter < 500us: 0
 500us <= jitter <   1ms: 0
   1ms <= jitter <  10ms: 0
  10ms <= jitter < 100ms: 0
 100ms <= jitter < 200ms: 0
 200ms <= jitter < 500ms: 0
 500ms <= jitter <1000ms: 0
 jitter >= 1000ms       : 0
 
--- gpio write() time (samples=4391080) ---
min                     :     2.87 us
mean                    :     4.02 us
max                     :     6.98 us

表示の見方を簡単に補足します。timerfd stats の max_expirations は「1周期(1ms)を超える遅延が最大何回分あったか」を示す値で、1であれば遅延なしを意味します。jitter stats は、期待した1ms周期からどれだけズレたか(us単位)の平均・最小・最大・標準偏差と、ズレの大きさ別のサンプル数の内訳です。gpio write() time は、GPIOへの書き込み(sysfs write())自体にかかった時間の統計です。

STM32 Nucleo(FreeRTOS)との比較

参考として、STM32 Nucleo(F446RE)上でFreeRTOSタスクを使い、同じ考え方(1ms周期でGPIOをトグルし、ジッタを計測)のプログラムを動かして比較しました。

開発環境は VSCode + PlatformIO拡張機能 + Arduinoフレームワーク(FreeRTOSライブラリ組み込み)という構成です。PlatformIOがツールチェーンの導入からビルド・書き込みまで面倒を見てくれるので、環境構築からLチカ(というかLトグル)まで、ものの30分ほどで到達できました。詳しい手順はまた気が向いたら別記事にするかもしれません。

 Raspberry Pi 4(Yocto, SCHED_FIFO)STM32 Nucleo F446RE(FreeRTOS)
標準偏差2.4us前後0.16us
ジッタ範囲±13〜19us±0.49us
トグル操作自体の時間2.9〜9.7us(sysfs write)1.34〜1.36us(digitalWrite)

当然ながら、専用マイコン+RTOSの方が一桁近くタイトです。ハードウェア割り込みからOSのスケジューリングまで、何もかもがこの1タスクのために作られているような環境なので、これは順当な結果だと思います。

ただ、絶対値で見ると、Raspberry Pi 4側も最悪で±20us弱という数字です。一般的な産業用制御ループの多くは、ミリ秒オーダーの周期・精度で十分なケースが多く、その観点では汎用Linux(それも特別なチューニングをしていない標準的なYocto環境)でも、十分実用に耐える精度が出ている、と言えそうです。

なお、今回のSTM32側の数値は「チャンピオンデータ」である点には注意が必要です。今回計測した構成は、タイマ割り込み1つと、トグル用・ボタン監視用の2つのFreeRTOSタスクだけが動く、ごく最小構成です。実際の組込みアプリケーションでは、UART・I2C・CAN・USB・Ethernetなど複数のデバイス制御や、もっと多くのリアルタイムタスクが並行して動くのが普通です。そうした現実的な構成では、割り込み処理やタスク切り替えの競合が増える分、今回計測したジッタよりも悪化する可能性が高いと考えています。

考察

今回の結果を、実際の案件の目線で捉え直すと、以下のようなことが言えると思います。

・「Linuxはリアルタイム性が無いから使えない」という判断を、検証せずに下してしまっているケースは意外と多いのではないか

・SCHED_FIFOでの優先度設定など、Linuxの標準機能の範囲内でも、多くの制御用途には十分な精度が出せる

・専用RTOS/MCUに比べればもちろん見劣りするが、その代わりLinuxは既製のネットワークスタック・ファイルシステム・豊富なデバッグツール・ソフトウェア資産をそのまま使える、というトレードオフがある

つまり「精度が全てを支配するようなシビアな制御」以外の、多くの組込みLinux案件においては、専用RTOSに移行するコストをかけずとも、Yoctoベースの組込みLinuxで十分要件を満たせる可能性が高い、ということです。

まとめ

今回、手元にあったRaspberry Pi 4とYoctoの組み合わせで、簡易的にリアルタイム性能を測ってみました。特別なチューニングをしていない状態でも、1時間以上の連続動作で取りこぼしゼロ、ジッタも±20us弱に収まるという結果が得られました。

STM32+FreeRTOSのような専用環境には数字上及ばないものの、絶対値としては十分小さく、多くの組込み用途では実用上問題にならないレベルだと考えています。

今回はあくまで「手っ取り早く」標準のPREEMPTカーネルで試した結果です。カーネルをPREEMPT_RT対応にした上でスレッドの優先度をきちんと調整したり、CPUアフィニティを調整して他の処理から隔離したりすれば、さらにジッタを改善できる余地もあると見ています。この辺りは今後の検証テーマとして温めています。

当社は、リアルタイムOS周りを得意とする会社ですが、組込みLinux・Yoctoを使った開発もしています。Linuxでのお困りごとがあれば、ぜひお気軽にお問い合わせください。

付録: 計測プログラムのソースコード(rt_gpio_square.c)

今回使用したCプログラムの全文です。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <sys/timerfd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sched.h>
#include <errno.h>
#include <stdint.h>
#include <signal.h>
#include <inttypes.h>
#include <math.h>
 
#define GPIO_NUM "530"   // GPIO18 (ピン12) 例 [512(BASE) + 18 = 530]
#define PERIOD_MS 1     // 1ms周期タイマ
 
static volatile sig_atomic_t g_stop = 0;
 
static void handle_stop_signal(int sig)
{
    (void)sig;
    g_stop = 1;
}
 
// トグル間隔の実測値(clock_gettime差分)を期待周期(1ms)と比べたずれを
// 1本の連続した尺度として扱い、us単位/ms単位のバケットに分けて集計する。
// (1ms以上の遅れがus側とms側の両方で二重にカウントされないよう、
//  バケットは重複なく1つの数直線として並べている)
typedef struct {
    uint64_t total;
    uint64_t errored;
    uint64_t max_expirations;
 
    uint64_t count;
    double sum_ns;
    double sum_sq_ns;
    int64_t min_ns;
    int64_t max_ns;
 
    uint64_t lt_neg100us;   // jitter < -100us
    uint64_t lt_neg10us;    // -100us  <= jitter < -10us
    uint64_t lt_10us;       //  -10us  <= jitter <  10us
    uint64_t lt_20us;       //   10us  <= jitter <  20us
    uint64_t lt_30us;       //   20us  <= jitter <  30us
    uint64_t lt_50us;       //   30us  <= jitter <  50us
    uint64_t lt_100us;      //   50us  <= jitter < 100us
    uint64_t lt_200us;      //  100us  <= jitter < 200us
    uint64_t lt_500us;      //  200us  <= jitter < 500us
    uint64_t lt_1ms;        //  500us  <= jitter <   1ms
    uint64_t lt_10ms;       //    1ms  <= jitter <  10ms
    uint64_t lt_100ms;      //   10ms  <= jitter < 100ms
    uint64_t lt_200ms;      //  100ms  <= jitter < 200ms
    uint64_t lt_500ms;      //  200ms  <= jitter < 500ms
    uint64_t lt_1000ms;     //  500ms  <= jitter < 1000ms
    uint64_t ge_1000ms;     // jitter >= 1000ms
} JitterStat;
 
// write()自体の所要時間(最短/平均/最長)をざっくり見るための集計
typedef struct {
    uint64_t count;
    double sum_ns;
    int64_t min_ns;
    int64_t max_ns;
} WriteTimeStat;
 
static void write_time_stat_add(WriteTimeStat *ws, int64_t elapsed_ns)
{
    ws->count++;
    ws->sum_ns += (double)elapsed_ns;
    if (elapsed_ns < ws->min_ns) ws->min_ns = elapsed_ns;
    if (elapsed_ns > ws->max_ns) ws->max_ns = elapsed_ns;
}
 
static uint64_t ts_to_ns(const struct timespec *ts)
{
    return (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec;
}
 
static void jitter_stat_add(JitterStat *js, int64_t jitter_ns)
{
    js->count++;
    js->sum_ns += (double)jitter_ns;
    js->sum_sq_ns += (double)jitter_ns * (double)jitter_ns;
    if (jitter_ns < js->min_ns) js->min_ns = jitter_ns;
    if (jitter_ns > js->max_ns) js->max_ns = jitter_ns;
 
    if      (jitter_ns < -100000)    js->lt_neg100us++;
    else if (jitter_ns < -10000)     js->lt_neg10us++;
    else if (jitter_ns < 10000)      js->lt_10us++;
    else if (jitter_ns < 20000)      js->lt_20us++;
    else if (jitter_ns < 30000)      js->lt_30us++;
    else if (jitter_ns < 50000)      js->lt_50us++;
    else if (jitter_ns < 100000)     js->lt_100us++;
    else if (jitter_ns < 200000)     js->lt_200us++;
    else if (jitter_ns < 500000)     js->lt_500us++;
    else if (jitter_ns < 1000000)    js->lt_1ms++;
    else if (jitter_ns < 10000000)   js->lt_10ms++;
    else if (jitter_ns < 100000000)  js->lt_100ms++;
    else if (jitter_ns < 200000000)  js->lt_200ms++;
    else if (jitter_ns < 500000000)  js->lt_500ms++;
    else if (jitter_ns < 1000000000) js->lt_1000ms++;
    else                              js->ge_1000ms++;
}
 
static void print_jitter_stats(const JitterStat *js)
{
    printf("\\n--- timerfd stats (total=%" PRIu64 ", errored=%" PRIu64 ", max_expirations=%" PRIu64 ") ---\\n",
           js->total, js->errored, js->max_expirations);
 
    if (js->count == 0) return;
    double mean = js->sum_ns / (double)js->count;
    double variance = js->sum_sq_ns / (double)js->count - mean * mean;
    if (variance < 0) variance = 0; // 丸め誤差対策
    double stddev = sqrt(variance);
 
    printf("\\n--- jitter stats (samples=%" PRIu64 ") ---\\n", js->count);
    printf("mean                    : %8.2f us\\n", mean / 1000.0);
    printf("min                     : %8.2f us\\n", (double)js->min_ns / 1000.0);
    printf("max                     : %8.2f us\\n", (double)js->max_ns / 1000.0);
    printf("stddev                  : %8.2f us\\n", stddev / 1000.0);
    printf("jitter <  -100us        : %" PRIu64 "\\n", js->lt_neg100us);
    printf("-100us <= jitter < -10us: %" PRIu64 "\\n", js->lt_neg10us);
    printf(" -10us <= jitter <  10us: %" PRIu64 "\\n", js->lt_10us);
    printf("  10us <= jitter <  20us: %" PRIu64 "\\n", js->lt_20us);
    printf("  20us <= jitter <  30us: %" PRIu64 "\\n", js->lt_30us);
    printf("  30us <= jitter <  50us: %" PRIu64 "\\n", js->lt_50us);
    printf("  50us <= jitter < 100us: %" PRIu64 "\\n", js->lt_100us);
    printf(" 100us <= jitter < 200us: %" PRIu64 "\\n", js->lt_200us);
    printf(" 200us <= jitter < 500us: %" PRIu64 "\\n", js->lt_500us);
    printf(" 500us <= jitter <   1ms: %" PRIu64 "\\n", js->lt_1ms);
    printf("   1ms <= jitter <  10ms: %" PRIu64 "\\n", js->lt_10ms);
    printf("  10ms <= jitter < 100ms: %" PRIu64 "\\n", js->lt_100ms);
    printf(" 100ms <= jitter < 200ms: %" PRIu64 "\\n", js->lt_200ms);
    printf(" 200ms <= jitter < 500ms: %" PRIu64 "\\n", js->lt_500ms);
    printf(" 500ms <= jitter <1000ms: %" PRIu64 "\\n", js->lt_1000ms);
    printf(" jitter >= 1000ms       : %" PRIu64 "\\n", js->ge_1000ms);
}
 
static void print_write_time_stats(const WriteTimeStat *ws)
{
    printf("\\n--- gpio write() time (samples=%" PRIu64 ") ---\\n", ws->count);
    if (ws->count == 0) return;
    double wmean = ws->sum_ns / (double)ws->count;
    printf("min                     : %8.2f us\\n", (double)ws->min_ns / 1000.0);
    printf("mean                    : %8.2f us\\n", wmean / 1000.0);
    printf("max                     : %8.2f us\\n", (double)ws->max_ns / 1000.0);
}
 
static int gpio_export(const char *num)
{
    int fd = open("/sys/class/gpio/export", O_WRONLY);
    if (fd < 0) return -1;
    if (write(fd, num, strlen(num)) < 0) { close(fd); return -1; }
    close(fd);
    return 0;
}
 
static int gpio_set_dir(const char *num, const char *dir)
{
    char path[64];
    snprintf(path, sizeof(path), "/sys/class/gpio/gpio%s/direction", num);
    int fd = open(path, O_WRONLY);
    if (fd < 0) return -1;
    if (write(fd, dir, strlen(dir)) < 0) { close(fd); return -1; }
    close(fd);
    return 0;
}
 
static int gpio_open_value(const char *num)
{
    char path[64];
    snprintf(path, sizeof(path), "/sys/class/gpio/gpio%s/value", num);
    return open(path, O_WRONLY);
}
 
int main(void)
{
    struct sched_param sp;
    int tfd, gfd;
    struct itimerspec its;
    uint64_t expirations;
    int level = 0;
    JitterStat js;
    WriteTimeStat ws;
    uint64_t prev_ns = 0;
    int have_prev = 0;
 
    memset(&js, 0, sizeof(js));
    js.min_ns = INT64_MAX;
    js.max_ns = INT64_MIN;
    memset(&ws, 0, sizeof(ws));
    ws.min_ns = INT64_MAX;
    ws.max_ns = INT64_MIN;
 
    // Ctrl-C/SIGTERMで統計を表示してから終了できるようにする
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_stop_signal;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
 
    // SCHED_FIFO に変更
    memset(&sp, 0, sizeof(sp));
    sp.sched_priority = 80; // root前提
    if (sched_setscheduler(0, SCHED_FIFO, &sp) < 0) {
        perror("sched_setscheduler");
        return 1;
    }
 
    // GPIO準備
    gpio_export(GPIO_NUM);
    usleep(100000); // ちょっと待つ
    if (gpio_set_dir(GPIO_NUM, "out") < 0) {
        perror("gpio_set_dir");
        return 1;
    }
    gfd = gpio_open_value(GPIO_NUM);
    if (gfd < 0) {
        perror("gpio_open_value");
        return 1;
    }
 
    // timerfd 1ms周期
    tfd = timerfd_create(CLOCK_MONOTONIC, 0);
    if (tfd < 0) {
        perror("timerfd_create");
        return 1;
    }
 
    its.it_interval.tv_sec = 0;
    its.it_interval.tv_nsec = PERIOD_MS * 1000000;
    its.it_value.tv_sec = 0;
    its.it_value.tv_nsec = PERIOD_MS * 1000000;
 
    if (timerfd_settime(tfd, 0, &its, NULL) < 0) {	// 相対タイマーの場合
        perror("timerfd_settime");
        return 1;
    }
 
    printf("Running 1ms timer, toggling GPIO %s (2ms period square wave)\\n", GPIO_NUM);
 
    while (!g_stop) {
        ssize_t r = read(tfd, &expirations, sizeof(expirations));
        if (r != sizeof(expirations)) {
            js.total++;
            js.errored++;
            if (errno == EINTR) break;
            perror("read timerfd");
            break;
        }
        js.total++;
        if (expirations > js.max_expirations) js.max_expirations = expirations;
 
        // 1msごとにトグル → 2ms周期矩形波
        level = !level;
        const char *val = level ? "1" : "0";
 
        struct timespec wt0, wt1;
        clock_gettime(CLOCK_MONOTONIC, &wt0);
        ssize_t wr = write(gfd, val, 1);
        clock_gettime(CLOCK_MONOTONIC, &wt1);
        if (wr < 0) {
            perror("write gpio");
            break;
        }
        write_time_stat_add(&ws, (int64_t)(ts_to_ns(&wt1) - ts_to_ns(&wt0)));
 
        // write()完了直後の時刻を「実際にGPIOが変化したとみなせる時刻」として
        // 前回との差分(期待周期1msからのずれ)をusec単位のジッタとして記録する
        uint64_t now_ns = ts_to_ns(&wt1);
        if (have_prev) {
            int64_t delta_ns = (int64_t)(now_ns - prev_ns);
            int64_t jitter_ns = delta_ns - (int64_t)PERIOD_MS * 1000000;
            jitter_stat_add(&js, jitter_ns);
        } else {
            have_prev = 1;
        }
        prev_ns = now_ns;
    }
 
    close(gfd);
    close(tfd);
 
    print_jitter_stats(&js);
    print_write_time_stats(&ws);
 
    return 0;
}

付録: STM32側の計測プログラムのソースコード(src/main.cpp)

STM32 Nucleo(F446RE)側、PlatformIO + Arduino(FreeRTOS)環境で使用したソースコードの全文です。
#include <Arduino.h>
#include <STM32FreeRTOS.h>
#include <math.h>
 
// ---------------------------------------------------------------------------
// DWT->CYCCNT による高精度サイクルカウンタ
// (RPi4側の clock_gettime(CLOCK_MONOTONIC) に相当する時刻ソース)
// ---------------------------------------------------------------------------
static double g_nsPerCycle = 0.0;
 
static void dwtInit()
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    g_nsPerCycle = 1.0e9 / (double)SystemCoreClock;
}
 
static inline uint32_t dwtNow()
{
    return DWT->CYCCNT;
}
 
// CYCCNTは32bitでオーバーフローするが、差分を符号なし32bit演算のまま取れば
// ラップアラウンドしても正しい経過サイクル数になる。
static inline double cyclesToNs(uint32_t cycles)
{
    return (double)cycles * g_nsPerCycle;
}
 
// ---------------------------------------------------------------------------
// ジッタ統計(RPi4側 rt-gpio-square と同じ、1本の数直線上のバケット定義)
// ---------------------------------------------------------------------------
struct JitterStat
{
    uint32_t count = 0;
    double sum_ns = 0;
    double sum_sq_ns = 0;
    double min_ns = 1e18;
    double max_ns = -1e18;
 
    uint32_t lt_neg100us = 0; // jitter < -100us
    uint32_t lt_neg10us = 0;  // -100us <= jitter < -10us
    uint32_t lt_10us = 0;     //  -10us <= jitter <  10us
    uint32_t lt_20us = 0;     //   10us <= jitter <  20us
    uint32_t lt_30us = 0;     //   20us <= jitter <  30us
    uint32_t lt_50us = 0;     //   30us <= jitter <  50us
    uint32_t lt_100us = 0;    //   50us <= jitter < 100us
    uint32_t lt_200us = 0;    //  100us <= jitter < 200us
    uint32_t lt_500us = 0;    //  200us <= jitter < 500us
    uint32_t lt_1ms = 0;      //  500us <= jitter <   1ms
    uint32_t lt_10ms = 0;     //    1ms <= jitter <  10ms
    uint32_t lt_100ms = 0;    //   10ms <= jitter < 100ms
    uint32_t lt_200ms = 0;    //  100ms <= jitter < 200ms
    uint32_t lt_500ms = 0;    //  200ms <= jitter < 500ms
    uint32_t lt_1000ms = 0;   //  500ms <= jitter < 1000ms
    uint32_t ge_1000ms = 0;   // jitter >= 1000ms
};
 
// GPIO操作(digitalWrite)自体の所要時間統計(RPi4側の write() 計測に相当)
struct OpTimeStat
{
    uint32_t count = 0;
    double sum_ns = 0;
    double min_ns = 1e18;
    double max_ns = -1e18;
};
 
static void jitterStatAdd(JitterStat &js, double jitter_ns)
{
    js.count++;
    js.sum_ns += jitter_ns;
    js.sum_sq_ns += jitter_ns * jitter_ns;
    if (jitter_ns < js.min_ns) js.min_ns = jitter_ns;
    if (jitter_ns > js.max_ns) js.max_ns = jitter_ns;
 
    if      (jitter_ns < -100000.0)    js.lt_neg100us++;
    else if (jitter_ns < -10000.0)     js.lt_neg10us++;
    else if (jitter_ns < 10000.0)      js.lt_10us++;
    else if (jitter_ns < 20000.0)      js.lt_20us++;
    else if (jitter_ns < 30000.0)      js.lt_30us++;
    else if (jitter_ns < 50000.0)      js.lt_50us++;
    else if (jitter_ns < 100000.0)     js.lt_100us++;
    else if (jitter_ns < 200000.0)     js.lt_200us++;
    else if (jitter_ns < 500000.0)     js.lt_500us++;
    else if (jitter_ns < 1000000.0)    js.lt_1ms++;
    else if (jitter_ns < 10000000.0)   js.lt_10ms++;
    else if (jitter_ns < 100000000.0)  js.lt_100ms++;
    else if (jitter_ns < 200000000.0)  js.lt_200ms++;
    else if (jitter_ns < 500000000.0)  js.lt_500ms++;
    else if (jitter_ns < 1000000000.0) js.lt_1000ms++;
    else                                js.ge_1000ms++;
}
 
static void opTimeStatAdd(OpTimeStat &ws, double elapsed_ns)
{
    ws.count++;
    ws.sum_ns += elapsed_ns;
    if (elapsed_ns < ws.min_ns) ws.min_ns = elapsed_ns;
    if (elapsed_ns > ws.max_ns) ws.max_ns = elapsed_ns;
}
 
static void printJitterStats(const JitterStat &js)
{
    Serial.println();
    Serial.print(F("--- jitter stats (samples="));
    Serial.print(js.count);
    Serial.println(F(") ---"));
    if (js.count == 0) return;
 
    double mean = js.sum_ns / (double)js.count;
    double variance = js.sum_sq_ns / (double)js.count - mean * mean;
    if (variance < 0) variance = 0; // 丸め誤差対策
    double stddev = sqrt(variance);
 
    Serial.print(F("mean                    : ")); Serial.print(mean / 1000.0, 2); Serial.println(F(" us"));
    Serial.print(F("min                     : ")); Serial.print(js.min_ns / 1000.0, 2); Serial.println(F(" us"));
    Serial.print(F("max                     : ")); Serial.print(js.max_ns / 1000.0, 2); Serial.println(F(" us"));
    Serial.print(F("stddev                  : ")); Serial.print(stddev / 1000.0, 2); Serial.println(F(" us"));
    Serial.print(F("jitter <  -100us        : ")); Serial.println(js.lt_neg100us);
    Serial.print(F("-100us <= jitter < -10us: ")); Serial.println(js.lt_neg10us);
    Serial.print(F(" -10us <= jitter <  10us: ")); Serial.println(js.lt_10us);
    Serial.print(F("  10us <= jitter <  20us: ")); Serial.println(js.lt_20us);
    Serial.print(F("  20us <= jitter <  30us: ")); Serial.println(js.lt_30us);
    Serial.print(F("  30us <= jitter <  50us: ")); Serial.println(js.lt_50us);
    Serial.print(F("  50us <= jitter < 100us: ")); Serial.println(js.lt_100us);
    Serial.print(F(" 100us <= jitter < 200us: ")); Serial.println(js.lt_200us);
    Serial.print(F(" 200us <= jitter < 500us: ")); Serial.println(js.lt_500us);
    Serial.print(F(" 500us <= jitter <   1ms: ")); Serial.println(js.lt_1ms);
    Serial.print(F("   1ms <= jitter <  10ms: ")); Serial.println(js.lt_10ms);
    Serial.print(F("  10ms <= jitter < 100ms: ")); Serial.println(js.lt_100ms);
    Serial.print(F(" 100ms <= jitter < 200ms: ")); Serial.println(js.lt_200ms);
    Serial.print(F(" 200ms <= jitter < 500ms: ")); Serial.println(js.lt_500ms);
    Serial.print(F(" 500ms <= jitter <1000ms: ")); Serial.println(js.lt_1000ms);
    Serial.print(F(" jitter >= 1000ms       : ")); Serial.println(js.ge_1000ms);
}
 
static void printOpTimeStats(const OpTimeStat &ws)
{
    Serial.println();
    Serial.print(F("--- gpio toggle time (samples="));
    Serial.print(ws.count);
    Serial.println(F(") ---"));
    if (ws.count == 0) return;
 
    double mean = ws.sum_ns / (double)ws.count;
    Serial.print(F("min                     : ")); Serial.print(ws.min_ns / 1000.0, 2); Serial.println(F(" us"));
    Serial.print(F("mean                    : ")); Serial.print(mean / 1000.0, 2); Serial.println(F(" us"));
    Serial.print(F("max                     : ")); Serial.print(ws.max_ns / 1000.0, 2); Serial.println(F(" us"));
}
 
// ---------------------------------------------------------------------------
// タスク
// ---------------------------------------------------------------------------
static JitterStat g_jitter;
static OpTimeStat g_opTime;
static TaskHandle_t g_toggleTaskHandle = NULL;
 
constexpr double kPeriodNs = 1000000.0; // 1ms
constexpr uint32_t kDebounceMs = 20;
 
void toggleTask(void *pvParameters)
{
    pinMode(LED_BUILTIN, OUTPUT);
 
    bool havePrev = false;
    uint32_t prevCycles = 0;
 
    for (;;)
    {
        vTaskDelay(pdMS_TO_TICKS(1)); // 1msごとにトグル → 2ms周期矩形波
 
        uint32_t t0 = dwtNow();
        digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
        uint32_t t1 = dwtNow();
 
        double opNs = cyclesToNs(t1 - t0);
 
        // digitalWrite完了直後の時刻を「実際にGPIOが変化したとみなせる時刻」として、
        // 前回との差分(期待周期1msからのずれ)をジッタとして記録する(RPi4側と同じ考え方)。
        double jitterNs = 0;
        bool haveJitter = false;
        if (havePrev)
        {
            double deltaNs = cyclesToNs(t1 - prevCycles);
            jitterNs = deltaNs - kPeriodNs;
            haveJitter = true;
        }
 
        taskENTER_CRITICAL();
        opTimeStatAdd(g_opTime, opNs);
        if (haveJitter) jitterStatAdd(g_jitter, jitterNs);
        taskEXIT_CRITICAL();
 
        prevCycles = t1;
        havePrev = true;
    }
}
 
// USER_BTN(Nucleoボードの青いボタン、B1/PC13、押下でLOW)が押されるまで計測を続け、
// 押されたらtoggleTaskを止めてそこまでの統計を表示する(RPi4側のCtrl-Cに相当)。
void buttonTask(void *pvParameters)
{
    pinMode(USER_BTN, INPUT);
 
    // 10ms周期でサンプリングし、3回連続で同じ論理値が読めたらその値を確定値とする
    // (確定値がLOW=押下になったら計測終了)。
    constexpr uint8_t kStableSamples = 3;
    int candidateLevel = digitalRead(USER_BTN);
    uint8_t matchCount = 1;
 
    for (;;)
    {
        vTaskDelay(pdMS_TO_TICKS(kDebounceMs));
        int level = digitalRead(USER_BTN);
 
        if (level == candidateLevel)
        {
            matchCount++;
        }
        else
        {
            candidateLevel = level;
            matchCount = 1;
        }
 
        if (matchCount >= kStableSamples && candidateLevel == LOW) break;
    }
 
    vTaskSuspend(g_toggleTaskHandle); // これ以降サンプルを追加させない
 
    JitterStat jitterCopy;
    OpTimeStat opTimeCopy;
    taskENTER_CRITICAL();
    jitterCopy = g_jitter;
    opTimeCopy = g_opTime;
    taskEXIT_CRITICAL();
 
    Serial.println();
    Serial.println(F("--- button pressed, measurement stopped ---"));
    printJitterStats(jitterCopy);
    printOpTimeStats(opTimeCopy);
 
    vTaskSuspend(NULL); // ボタンタスク自身もここで停止(リセットするまで待ち)
}
 
void setup()
{
    Serial.begin(115200);
    dwtInit();
 
    Serial.println(F("Running 1ms FreeRTOS task, toggling LED (2ms period square wave)"));
    Serial.println(F("Press the user button (B1) to stop measurement and print stats."));
 
    xTaskCreate(toggleTask, "toggle", 256, NULL, 2, &g_toggleTaskHandle);
    xTaskCreate(buttonTask, "button", 256, NULL, 1, NULL);
    vTaskStartScheduler();
}
 
void loop() {}