FreeRTOS POSIX porting + Catch2 でマルチタスクなユニットテスト環境を整備する part1

この記事は Nature Remo Advent Calendar 2022 の2日目として書きました。

adventar.org

ファームウェアエンジニアの中林です。 みなさん、ファームウェアユニットテスト、楽しんでいますでしょうか? 以前も紹介しているとおり、Nature Remo の開発では Catch2 を使って、ホストPC / ターゲットデバイス両方のテストを回しています。

engineering.nature.global

本エントリシリーズではなんと!複数のタスクで動く機能をホストPC上でテストする方法をご紹介しちゃいます。 自分で言うのもなんですが、初めてテスト駆動開発を知ってから早7年、その集大成のように思える組込み向けのテスト環境が構築できた、と感じています。 今、Nature Remo のファームウェア開発で実装している機能は、この環境を使ってゴリゴリとユニットテストを回しています。 そのおかげで、新しいコードを試すまでの時間短縮や、サニタイザーによるバグの早期発見により、目覚ましい成果を上げています。多分!

Nature では主に Nature Remo のネットワーク周りやシリアルインタフェースを使用するドライバ実装にこの環境を使用しています。 これらの機能開発は、lwip の socket API や、ESP-IDF の vfs API があることで、ビルドターゲットごとに実装を切り替えるだけで比較的簡単にホストPC上でも動かすことができます。 逆にシビアなリアルタイム性をテストする目的や割り込みハンドラを含むような機能開発で使うのは難しい部分もあるかと思います。

ぜひ日頃の開発にお役立てください。 3部構成になっており、残りの2部は順次頑張ります。

part1 では FreeRTOS POSIX porting について紹介します。 part2 では FreeRTOS POSIX porting + Catch2 で簡単なテストを書く方法を説明します。 part3 では FreeRTOS POSIX porting + Catch2 で少し実用的なテストを書いてみます。

FreeRTOS POSIX porting

概要

Catch2 は前回紹介したので、飛ばすとして、もう1つの鍵となる要素、FreeRTOS POSIX porting について少し紹介します。 と言っても、紹介するも何も、POSIX 上で動く FreeRTOS です、以上!以外の何者でもないのですが。

何をきっかけに見つけたか覚えていないのですが、ある日、FreeRTOS Kernel に POSIX 上で動く実装あるやん、ということがわかりました。Cool!!! 手元に残っていたメモによると、どうやらちょうど2年前の12月に見つけて遊んでいたようです。

www.freertos.org

コードはこのあたりです。

github.com

コードをリバースエンジニアリングしたり、Port-Layer Design Description を読むとわかるのですが、pthread や condvar を使って FreeRTOS のタスクを模擬しています。 pthread を使っている、と言っても並列実行のためではなく、タスクコンテキストを各 pthread に持たせるため、こういう仕組みになっています。 pthread で生成されたスレッドはすぐに自分自身を suspend し、FreeRTOS スケジューラから resume シグナルを受けるのを待ちます。 ということで、pthread を裏側で使っているが、基本的には FreeRTOS スケジューラが指定した1つの thread (FreeRTOS タスク) が実行されている状態となります。

Kernel demo

公式ページにもデモの動かし方が乗っていますが、ミニマムに動かす方法を紹介します。 ネットワークのデモを動かさないのであれば、makegcc があれば OK です。

$ git clone https://github.com/FreeRTOS/FreeRTOS.git --recursive -b V10.4.1
$ cd FreeRTOS/Demo/Posix_GCC/
$ make -j`nproc`

実行すると次のようになります。

$ ./build/posix_demo 
 
Trace started.
The trace will be dumped to disk if a call to configASSERT() fails.

The trace will be dumped to disk if Enter is hit.
Starting full demo
OK: No errors - tick count 10000 
OK: No errors - tick count 20000 
OK: No errors - tick count 30000 
OK: No errors - tick count 40000 
OK: No errors - tick count 50000 
OK: No errors - tick count 60000 
OK: No errors - tick count 70000

POSIX porting の使い方

先程動かしたデモをトリアージして最小構成で動かせる状態にしましょう。 やることとしては次の4つです。

  1. FreeRTOSConfig.h を用意する
  2. いくつかフック関数を定義する
  3. Makefile を用意する
  4. タスクスケジューラを起動する

1. FreeRTOSConfig.h を用意する

FreeRTOS の kernel コンフィギュレーションするためのヘッダーファイルを用意します。 希望のコンフィギュレーションに合わせて必要であれば編集しましょう。

#define configUSE_PREEMPTION                    1
#define configUSE_PORT_OPTIMISED_TASK_SELECTION    0
#define configUSE_IDLE_HOOK                        1
#define configUSE_TICK_HOOK                        1
// 以下略

2. いくつかフック関数を定義する

デモコードでは main.c に書いてある、下のような関数です。 どれが必要かわからない場合は、main.c の main 関数以外全部フック関数だと思っておけば良いです。 デモのコードをコピペして使っていますが、それで困ったことはないです。

void vApplicationMallocFailedHook( void )
{
    /* vApplicationMallocFailedHook() will only be called if
   configUSE_MALLOC_FAILED_HOOK is set to 1 in FreeRTOSConfig.h.  It is a hook
   function that will get called if a call to pvPortMalloc() fails.
   pvPortMalloc() is called internally by the kernel whenever a task, queue,
   timer or semaphore is created.  It is also called by various parts of the
   demo application.  If heap_1.c, heap_2.c or heap_4.c is being used, then the
   size of the heap available to pvPortMalloc() is defined by
   configTOTAL_HEAP_SIZE in FreeRTOSConfig.h, and the xPortGetFreeHeapSize()
   API function can be used to query the size of free heap space that remains
   (although it does not provide information on how the remaining heap might be
   fragmented).  See http://www.freertos.org/a00111.html for more
   information. */
    vAssertCalled( __FILE__, __LINE__ );
}

3. Makefile を用意する

Makefile も素直な作りなので、デモに関するところ以外をコピペして、自分のアプリケーションもしくはテストコードをコンパイルしてリンクするようにすれば OK です。 必要なのはこのあたりです。

FREERTOS_DIR_REL := ../../../FreeRTOS
FREERTOS_DIR := $(abspath $(FREERTOS_DIR_REL))

INCLUDE_DIRS := -I.
INCLUDE_DIRS += -I${FREERTOS_DIR}/Source/include
INCLUDE_DIRS += -I${FREERTOS_DIR}/Source/portable/ThirdParty/GCC/Posix
INCLUDE_DIRS += -I${FREERTOS_DIR}/Source/portable/ThirdParty/GCC/Posix/utils

SOURCE_FILES := $(wildcard *.c)
SOURCE_FILES += $(wildcard ${FREERTOS_DIR}/Source/*.c)
# Memory manager (use malloc() / free() )
SOURCE_FILES += ${FREERTOS_DIR}/Source/portable/MemMang/heap_3.c
# posix port
SOURCE_FILES += ${FREERTOS_DIR}/Source/portable/ThirdParty/GCC/Posix/utils/wait_for_event.c
SOURCE_FILES += ${FREERTOS_DIR}/Source/portable/ThirdParty/GCC/Posix/port.c

CFLAGS := -ggdb3 -O0 -DprojCOVERAGE_TEST=0 -D_WINDOWS_
LDFLAGS := -ggdb3 -O0 -pthread -lpcap

OBJ_FILES = $(SOURCE_FILES:%.c=$(BUILD_DIR)/%.o)

DEP_FILE = $(OBJ_FILES:%.o=%.d)

${BIN} : $(BUILD_DIR)/$(BIN)

${BUILD_DIR}/${BIN} : ${OBJ_FILES}
   -mkdir -p ${@D}
    $(CC) $^ $(CFLAGS) $(INCLUDE_DIRS) ${LDFLAGS} -o $@


-include ${DEP_FILE}

${BUILD_DIR}/%.o : %.c
   -mkdir -p $(@D)
    $(CC) $(CFLAGS) ${INCLUDE_DIRS} -MMD -c $< -o $@

.PHONY: clean

clean:
   -rm -rf $(BUILD_DIR)

コンパイルオプションで projCOVERAGE_TEST を指定しているのですが、このマクロが定義されていないと、FreeRTOSConfig.h でエラーになるようになっています。 ほしいのかしら? 特に支障がないので、一旦そのまま使っています。

// FreeRTOSConfig.h
/* projCOVERAGE_TEST should be defined on the command line so this file can be
used with multiple project configurations.  If it is
 */
#ifndef projCOVERAGE_TEST
    #error projCOVERAGE_TEST should be defined to 1 or 0 on the command line.
#endif

4. タスクスケジューラを起動する

main 関数でタスクを起動したら、スケジューラを起動します。 自分自身がスケジューラになるので、見た目上はプログラムがここでブロックされる形になります。

 /* Start the scheduler itself. */
    vTaskStartScheduler();

このままですとコンソールで Ctrl+C しないとプログラムの実行が止まりません。 vTaskEndScheduler(); を呼ぶと、スケジューラが停止して、vTaskStartScheduler(); の続きから再開されます。 これは後々テストを終了するために使うので覚えておきましょう!

ちなみに、FreeRTOS ソースコードで一番好きなところは、ヘッダーに 1 tab == 4 spaces! って書いてあるところです。

/*
 * FreeRTOS Kernel V10.4.1
 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
中略...
 * http://www.FreeRTOS.org
 * http://aws.amazon.com/freertos
 *
 * 1 tab == 4 spaces!
 */

自作プログラムを動かしてみよう

それでは、ミニマムな自作 FreeRTOS プログラムを Linux 上で動かしてみましょう。 私の環境は Ubuntu 20.04 です。 用意するファイルは

の4つです。

コードはこちらで公開しています。

https://github.com/tomoyuki-nakabayashi/freertos-catch

詳細はGitHubを見ていただくとして、いくつかポイントだけ解説します。

main.c

FreeRTOS タスクを1つ立ち上げて、1秒ごとに "hello from task" を10回出力して、終了します。

#include <FreeRTOS.h>
#include <task.h>

#include <stdio.h>

static void test_task(void *arg)
{
    (void)arg;
    for (int i = 0; i < 10; i++) {
        printf("hello from task\n");
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
    vTaskEndScheduler();
}

int main(void)
{    
    printf("start\n");
    xTaskCreate(&test_task, "test", 1024, NULL, 2, NULL);
    vTaskStartScheduler();
    printf("finish!\n");

    return 0;
}

Makefile

今回は、FreeRTOS のコードは make prepare でダウンロードしてくるようにしています。

prepare: $(FREERTOS_DIR)

$(FREERTOS_DIR):
  curl -L https://github.com/FreeRTOS/FreeRTOS-Kernel/releases/download/V10.5.0/FreeRTOS-KernelV10.5.0.zip -O
  unzip FreeRTOS-KernelV10.5.0.zip
   -@$(RM) -r FreeRTOS-KernelV10.5.0.zip
  mv -T FreeRTOS-KernelV10.5.0 $@

軽い気持ちで -std=c17 つけてみたら実装で GNU 拡張けっこう使っていることがわかりました。 -std=gnu17 としても良いです。

# GNU 拡張使っているので `_GNU_SOURCE` マクロを定義する
CFLAGS := -Wall -Wextra -std=c17 -D_GNU_SOURCE -DprojCOVERAGE_TEST=0

せっかくホストPCで実行しているので、サニタイザーをモリモリ盛っておきましょう! これで何回バグを早期発見できたか…。 恩恵が大きすぎます。

ifndef DISABLE_SANITIZER
CFLAGS += -g -fsanitize=address -fsanitize=undefined -fsanitize=leak -fsanitize=alignment 
endif

実行する

実行するとこうなります。

$ git clone https://github.com/tomoyuki-nakabayashi/freertos-catch.git
$ cd freertos-catch/first_app
$ make prepare
$ make run
./build/hello
start
hello from task
hello from task
hello from task
hello from task
hello from task
hello from task
hello from task
hello from task
hello from task
hello from task
finish!

まとめ

さあ!これであなたも今日から Linux で FreeRTOS のコードを書きまくりましょう!

テストで使う他にも、FreeRTOS の API だけで機能を作る時にも便利です。 実際、Nature Remo では複数の定期ジョブやワンタイムジョブを実行するための、ワーカータスクが動いており、その実装は FreeRTOS POSIX porting で全て済ませたものをファームウェアに移植して動かしています。 試行錯誤のリードタイムが圧倒的に短くなるため、非常に良い開発体験でした。

エンジニア積極採用中です

Natureでは全力でユニットテスト環境を構築してくれる仲間を募集しています。

herp.careers

カジュアル面談も歓迎なので、ぜひお申し込みください。

herp.careers

Natureのミッション、サービス、組織や文化、福利厚生についてご興味のある方は、ぜひCulture Deckをご覧ください。

speakerdeck.com