[PolarFire SoC] FreeRTOS を動かしてみる

投稿者: | 2020/11/06

Running FreeRTOS scheduler on a PolarFire SoC RISC-V hart (core).

前回までに、PolarFire SoC の RISC-V コアで、ベアメタルライブラリを使った割込プログラミングの基本を確認しました。これにより、PolarFire SoC による製品設計の進め方が見えてきましたが、実際の設計では、やはりリアルタイム OS(RTOS)が動かないと不安が残ります。

FreeRTOS の使用例を探す

GitHub: polarfire-soc/polarfire-soc-bare-metal-library のディレクトリ src/platform には、FreeRTOS は含まれていませんが、examples を探すと、examples/mss-ethernet-mac/mpfs-uart-mac-freertos_lwip というプロジェクトだけが、FreeRTOS を動かしていることが分かります。(コミット 5ed00cd 現在)

このサンプルプロジェクトの src/middleware というディレクトリの中に、FreeRTOS というディレクトリが含まれ、また、src/middleware/config の中にコンフィグレーション用のヘッダファイル FreeRTOSConfig.h があります。FreeRTOS のソースコードの冒頭コメントを見たところでは、バージョンは 9.0.0(Amazon が買収する前の最終バージョン)を使用している模様です。

なるべくシンプルに FreeRTOS を試す

残念ながら(?)上記のサンプルプロジェクトはかなり複雑なコードで、またコードがクリーンアップされておらず、お世辞にも分かりやすいものとは言えません。そこで、前回までに独自に修正した mpfs-blinky をベースとして、FreeRTOS を動かしてみたいと思います。以下に、修正点を説明していきたいと思います。

ベアメタルライブラリを最新に

10月20日の記事で、ベアメタルライブラリのコミット 920c14b を使用していますが、上記 GutHub リポジトリを見ていると、比較的高頻度で更新がされていることが分かります。今回は、最新版の正式リリース 2020.10 にアップデートしました。

FreeRTOS のコードをコピーする

続いて、polarfire-soc-bare-metal-library 下のディレクトリ examples/mss-ethernet-mac/mpfs-uart-mac-freertos_lwip/src/middleware/FreeRTOS を、次のような構成になるようにコピーします。(ま、ディレクトリ構造はそれほど重要でありませんが。)

mpfs-blinky/src/middleware/FreeRTOS/
├── include
└── portable
    ├── GCC
    │   └── RISCV
    └── MemMang

オリジナルのプロジェクトでは、Eclipse CDT の .cproject の機能を使い、コンパイルしないコードを選択的に除いていますが、それは面倒ですので、不要なファイルは削除します。今回は、以下のディレクトリおよびファイルは不要です。

src/middleware/FreeRTOS/portable/
├── CCS
├── GCC(RISCV を除く全て)
├── IAR
├── MemMang(heap_3.c を残す)
│   ├── heap_1.c
│   ├── heap_2.c
│   └── heap_4.c
└── RVDS

なお、FreeRTOSConfig.h は、src/boards/icicle-kit-es/FreeRTOSConfig.h に置くようにしました。

include パスの追加

次の 2つのディレクトリを include パスに追加します。いままで src/boards/icicle-kit-es を追加していない場合は、それも追加します。

  • ../src/middleware/FreeRTOS/include
  • ../src/middleware/FreeRTOS/portable/GCC/RISCV

u54_2.c の修正

ここがメインとなる修正です。まずは、パッチを示します。

diff --git a/mpfs-blinky/src/application/hart2/u54_2.c b/mpfs-blinky/src/application/hart2/u54_2.c
index dbc0c29..903f669 100644
--- a/mpfs-blinky/src/application/hart2/u54_2.c
+++ b/mpfs-blinky/src/application/hart2/u54_2.c
@@ -7,15 +7,23 @@
  *
  */
 
+#include <stdio.h>
+
 #include "mpfs_hal/mss_hal.h"
 
 #include "drivers/mss_gpio/mss_gpio.h"
 #include "drivers/mss_mmuart/mss_uart.h"
 
+#include "FreeRTOS.h"
+#include "task.h"
+#include "semphr.h"
+
 #include "inc/common.h"
 
 uint64_t uart_lock;
 
+volatile uint64_t* mtime;
+volatile uint64_t* timecmp;
 
 uint8_t fabric_f2h_0_plic_IRQHandler(void)
 {
@@ -141,35 +149,76 @@ void u54_2_setup(void)
 }
 
 
-void u54_2_application(void)
+void print_stack_watermark(const char *s)
 {
-    safe_MSS_UART0_polled_tx_string("Hello World from u54_2 (hart 0).\r\n");
+    UBaseType_t water_mark;
+    char sbuf[10];
+
+    water_mark = uxTaskGetStackHighWaterMark(NULL);
+    snprintf(sbuf, sizeof(sbuf), "%d", (int) water_mark);
+    safe_MSS_UART0_polled_tx_string(s);
+    safe_MSS_UART0_polled_tx_string(": uxTaskGetStackHighWaterMark = ");
+    safe_MSS_UART0_polled_tx_string(sbuf);
+    safe_MSS_UART0_polled_tx_string("\r\n");
+}
+
+
+SemaphoreHandle_t sem1;
+SemaphoreHandle_t sem2;
+
+
+void u54_2_application_on(void * pvParameters)
+{
+    safe_MSS_UART0_polled_tx_string(
+            "Hello World from u54_2 (hart 2) app_on.\r\n");
 
     while (1)
     {
         // Stay in the infinite loop, never return from main
 
-        const uint64_t delay_loop_max = 1000000;
-        volatile uint64_t delay_loop_sum = 0;
+        xSemaphoreTake(sem1, portMAX_DELAY);
+        safe_MSS_UART0_polled_tx_string("Took sem1\r\n");
 
-        for (uint64_t i = 0; i< delay_loop_max; i++) {
-            delay_loop_sum = delay_loop_sum + i;
-        }
+        vTaskDelay(2000 / portTICK_RATE_MS);
 
-        safe_MSS_UART0_polled_tx_string("Setting outputs 0, 1 and 2 to high\r\n");
+        safe_MSS_UART0_polled_tx_string(
+                "Setting outputs 0, 1 and 2 to high\r\n");
 
         MSS_GPIO_set_output(GPIO2_LO, MSS_GPIO_16, 1);
         MSS_GPIO_set_output(GPIO2_LO, MSS_GPIO_17, 1);
         MSS_GPIO_set_output(GPIO2_LO, MSS_GPIO_18, 1);
 
-        for (uint64_t i = 0; i< delay_loop_max; i++) {
-            delay_loop_sum = delay_loop_sum + i;
-        }
+        xSemaphoreGive(sem2);
+
+        print_stack_watermark("app_on");
+    }
+}
+
+
+void u54_2_application_off(void * pvParameters)
+{
+    safe_MSS_UART0_polled_tx_string(
+            "Hello World from u54_2 (hart 2) app off.\r\n");
+
+    while (1)
+    {
+        // Stay in the infinite loop, never return from main
+
+        xSemaphoreTake(sem2, portMAX_DELAY);
+        safe_MSS_UART0_polled_tx_string("Took sem2\r\n");
+
+        vTaskDelay(2000 / portTICK_RATE_MS);
+
+        safe_MSS_UART0_polled_tx_string(
+                "Setting outputs 0, 1 and 2 to low\r\n");
 
-        safe_MSS_UART0_polled_tx_string("Setting outputs 0, 1 and 2 to low\r\n");
         MSS_GPIO_set_output(GPIO2_LO, MSS_GPIO_16, 0);
         MSS_GPIO_set_output(GPIO2_LO, MSS_GPIO_17, 0);
         MSS_GPIO_set_output(GPIO2_LO, MSS_GPIO_18, 0);
+
+        xSemaphoreGive(sem1);
+
+        print_stack_watermark("app_off");
     }
 }
 
@@ -179,8 +228,38 @@ void u54_2_application(void)
  * UART0 PLIC interrupt is enabled on hart0.*/
 void u54_2(void)
 {
+    BaseType_t rtos_result;
+
     u54_2_setup();
-    u54_2_application();
+    mtime = (volatile uint64_t*)0x0200bff8;
+    timecmp = (volatile uint64_t*)0x02004000 + read_csr(mhartid);
+
+    sem1 = xSemaphoreCreateBinary();
+    ASSERT(sem1 != NULL);
+    sem2 = xSemaphoreCreateBinary();
+    ASSERT(sem2 != NULL);
+
+    rtos_result = xTaskCreate(
+            u54_2_application_on,
+            "u54_2_application on",
+            1024,
+            NULL,
+            uartPRIMARY_PRIORITY,
+            NULL);
+    ASSERT(rtos_result == pdPASS);
+
+    rtos_result = xTaskCreate(
+            u54_2_application_off,
+            "u54_2_application off",
+            1024,
+            NULL,
+            uartPRIMARY_PRIORITY,
+            NULL);
+    ASSERT(rtos_result == pdPASS);
+
+    xSemaphoreGive(sem1);
+
+    vTaskStartScheduler();
 
     /* Shouldn't never reach this point */
     while (1)
@@ -191,3 +270,49 @@ void u54_2(void)
        counter = counter + 1;
     }
 }
+
+
+/*-----------------------------------------------------------*/
+
+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 or heap_2.c are 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). */
+    taskDISABLE_INTERRUPTS();
+    for( ;; );
+}
+/*-----------------------------------------------------------*/
+
+void vApplicationIdleHook( void )
+{
+    /* vApplicationIdleHook() will only be called if configUSE_IDLE_HOOK is set
+    to 1 in FreeRTOSConfig.h.  It will be called on each iteration of the idle
+    task.  It is essential that code added to this hook function never attempts
+    to block in any way (for example, call xQueueReceive() with a block time
+    specified, or call vTaskDelay()).  If the application makes use of the
+    vTaskDelete() API function (as this demo application does) then it is also
+    important that vApplicationIdleHook() is permitted to return to its calling
+    function, because it is the responsibility of the idle task to clean up
+    memory allocated by the kernel to any task that has since been deleted. */
+}
+/*-----------------------------------------------------------*/
+
+void vApplicationStackOverflowHook( TaskHandle_t pxTask, char *pcTaskName )
+{
+    ( void ) pcTaskName;
+    ( void ) pxTask;
+
+    /* Run time stack overflow checking is performed if
+    configCHECK_FOR_STACK_OVERFLOW is defined to 1 or 2.  This hook
+    function is called if a stack overflow is detected. */
+    taskDISABLE_INTERRUPTS();
+    for( ;; );
+}

それぞれの修正について説明しましょう。

ヘッダファイルの追加

これは自明ですね。FreeRTOS に必要なヘッダファイルへの include を追加しています。

変数 mtime, timecmp の追加

これらは、src/middleware/FreeRTOS/portable/GCC/RISCV/port.c が参照しています。RISC-V では、CPU から低コストでアクセスできるタイマ mtime と、それを使ってタイマ割込を発生させる timecmp レジスタ(いずれも memory-mapped register)がありますが、それを使って FreeRTOS のスケジューラに割込をかけるようになっています。最新の FreeRTOS では、mtime, timecmp に関してアプリケーションコードで対処しなくても記述できるようになっていますが、FreeRTOS 9.0.0 では RISC-V の対応が不十分だったようで、mtime と timecmp に対する対応があまりきれいになっていないようです。

セマフォの利用

FreeRTOS のセマフォを動作確認すべく、sem1, sem2 を定義、初期化しています。FreeRTOS を多少ご経験の方ならば自明と思いますので、説明は略します。

タスク関数 u54_2_application_{on|off} の追加

こちらも、FreeRTOS を多少ご経験の方ならば自明と思いますので、説明は略します。

スタック使用量のモニタリング

各タスクがどの程度のスタックを使用しているか監視するために、print_stack_watermark() という関数を定義しています。この際、FreeRTOSConfig.h で以下が必要です。

#define INCLUDE_uxTaskGetStackHighWaterMark 1

タスクの生成とスケジューラの起動

xTaskCreate() 関数を使い、上記 2つのタスクを生成しています。スタックサイズは 1024 ワードとしています。

フック関数の定義

以下の 3つのフック関数を定義しています。これは、mpfs-uart-mac-freertos_lwip のものをそのまま利用しています。

  • vApplicationMallocFailedHook()
  • vApplicationIdleHook()
  • vApplicationStackOverflowHook()

その他の変更

まず最初に重要な点として、src/platform/mpfs_hal/entry.S の修正が必要です。ベアメタルライブラリに含まれる entry.S では、FreeRTOS のスケジューラを呼び出すための割込処理に対応していないので、mpfs-uart-mac-freertos_lwip のものをコピーし、.cproject の設定で USING_FREERTOS を define するようにします。(アセンブラのほうの設定変更が必要なことに注意)

余談: ちなみに、src/platform/mpfs_hal/mtrap.c の中でタイマ割込に対するハンドラ呼び出し(handle_m_timer_interrupt())が定義されているのに、どうして entry.S を修正しているのか不明です。時間があったら、ちょっと追いかけてみたいところです。

次に、リンカスクリプトファイルでヒープサイズを変更します。HEAP_SIZE = 64k くらいにしておけば十分でしょう。ランタイムエラーが出た場合は、もっと増やしてください。

最後に、mpfs-blinky/src/application/hart1/u54_1.c の u54_1_application() における num_loops を大きくすることで、UART出力の頻度を下げています。(U54_2 の UART 出力が見やすくなるように)

おまとして、コンパイル時の警告が気になる方は、src/middleware/FreeRTOS/portable/GCC/RISCV/portmacro.h の中で、以下を追加しましょう。

extern void vPortEnterCritical(void);
extern void vPortExitCritical(void);

今後の課題

いかがでしょうか。ちゃんと動きましたでしょうか。UART_0 に、

Took sem1
app_off: uxTaskGetStackHighWaterMark = 921
Hart 1, mcycle_delta=89129562 SW_IRQs=0 mcycle=213118477387
Hart 1, mcycle_delta=88931630 SW_IRQs=0 mcycle=213207585291
Hart 1, mcycle_delta=89108744 SW_IRQs=0 mcycle=213300015549
(略)
Setting outputs 0, 1 and 2 to high
Took sem2
app_on: uxTaskGetStackHighWaterMark = 921
Hart 1, mcycle_delta=92348228 SW_IRQs=0 mcycle=214384531503
Hart 1, mcycle_delta=91514878 SW_IRQs=0 mcycle=214473608327
Hart 1, mcycle_delta=89077414 SW_IRQs=0 mcycle=214565955609
(略)
Setting outputs 0, 1 and 2 to low
Took sem1

のような表示が出れば、正常に動作しています。(LED も 2秒 on、2秒 off で点滅しているはずです。)

課題としては、GDB や Eclipse でデバッグするときに、FreeRTOS の thread-aware なデバッグができないことです。PolarFire SoC 用の OpenOCD 設定では、各 hart(コア)を hwthread として割り当てているので、各コアがスレッドの単位となってしまいます。RTOS アプリケーションのデバッグでは、RTOS thread を意識したデバッグができると嬉しいですよね。

ちょっと OpenOCD の設定ファイルを覗いてみたのですが、なかなか難しそうです。これは、今後の課題にしたいと思います。(その前に、Microchip 社がなんとかしてくれるかも。)

あとは、FreeRTOS に代えて、近年注目を集めている Zephyr OS に対応できたら嬉しいな、とも思います。

今日はここまで。

お問い合わせはお気軽に!

御返答は 24時間以内(営業時間中)とさせて頂いております。もし返答が届かない場合、何らかの事情でメールが不達となっている可能性がございます。大変お手数ですが、別のメールアドレス等で督促頂けますと幸いです。なお、Facebook ページ(https://www.facebook.com/flogics/)でも御連絡頂けます。