RXマイコンで、FreeRTOS を使う場合の要点


はじめに

FreeRTOS は、組み込みマイコンでは標準的なリアルタイムOSとなっています。
非常に沢山のマイコンに対応しており、情報も多く、Amazon が主管となって MIT ライセンスに変更になり、より扱いやすくなったと思います。

今回は「FreeRTOSv10.2.1」を使いました。

RX マイコン用の実装は、GCC版、ルネサス版がありますが、どちらも、ルネサスのリソースありきになっていて中途半端な感じがします。

今回、独自でコンパイルした gcc-6.4.0 を使って、Makefile を使ったビルドを行い、
最低限の修正で FreeRTOS を RX マイコンで動かす手順をまとめ、github にソースをプッシュしました。

RX24T、RX64M、RX71M、RX65N、RX66T での動作を確認しました。

メイン部は C++ を使っています。


FreeRTOS の動作要件

FreeRTOS は、必要最低限のハードウェアーで動作するよう設計されているようで、以下の二つがあれば動作するものと思います。

・タイマー割り込み
・ソフトウェアー割り込み

RXマイコンでは、それぞれ、CMT0(コンペアマッチタイマ0)、SWINT(ソフトウェアー割り込み)を使う事で実現できるようです。
また、配列の割り当てに、記憶割り当てが必要ですが、いくつかのバージョンが用意されていて、メモリが少ない場合に対応できるようになっています。
※通常、「portable/MemMang/heap_3.c」(malloc/free)版を使えば良いようです。
※スタティックなメモリ管理を行う場合「portable/MemMang/heap_1.c」を使う事になると思いますが、推奨されていないようです。

FreeRTOS の構成

全体は、以下のような構成になっていて、機種依存は「portable」以下に置いてあるようです。

機種依存のタスク切り替えコードは、アセンブラによる実装になっていて、gcc で用意されている asm("") 関数を多用しています。

※ GCC 以下は、オリジナルアーカイブでは、ARM、AVR32 など色々用意されていますが、
RX マイコン用のみ入れてあります。

/FreeRTOS/Source/
    + include/
    | +- croutine.h
    | +- list.h
    | +- portable.h
    | +- stack_macros.h
    | +- task.h
    | +- deprecated_definitions.h
    | +- message_buffer.h
    | +- projdefs.h
    | +- StackMacros.h
    | +- timers.h
    | +- event_groups.h
    | +- mpu_prototypes.h
    | +- queue.h
    | +- stdint.readme
    | +- FreeRTOS.h
    | +- mpu_wrappers.h
    | +- semphr.h
    | +- stream_buffer.h
    +- croutine.c
    +- event_groups.c
    +- list.c
    +- queue.c
    +- stream_buffer.c
    +- tasks.c
    +- timers.c
    +- portable/
       +- Common/
       |  |
       |  +- mpu_wrappers.c
       +- MemMang/
       |  +- heap_1.c
       |  +- heap_2.c
       |  +- heap_3.c
       |  +- heap_4.c
       |  +- heap_5.c
       +- GCC/
          +- RX600/
          |  +- port.c
          |  +- portmacro.h
          +- RX600v2/
             +- port.c
             +- portmacro.h 

※「MemMang」はいくつかのメモリモデルが用意されており、自分のシステムに合った物をどれか一つだけリンクする。
※「RX600v2」は、RXv2 コア対応です。

Makefile の構成(リンク情報):

    FreeRTOS/Source/portable/GCC/RX600v2/port.c \
    FreeRTOS/Source/portable/MemMang/heap_3.c \
    FreeRTOS/Source/croutine.c \
    FreeRTOS/Source/event_groups.c \
    FreeRTOS/Source/list.c \
    FreeRTOS/Source/queue.c \
    FreeRTOS/Source/stream_buffer.c \
    FreeRTOS/Source/tasks.c \
    FreeRTOS/Source/timers.c

上記のように関係ソースをコンパイル、リンクすれば良く、簡潔に使えます。

※現在、gcc ソースコードの RX マイコン依存の実装は、古くて、「-mcpu」のタイプで「rxv2」がサポートされていません。
RX600v2/port.c をコンパイルすると、rxv2 のアセンブラニーモニックを翻訳出来ずにエラーとなります。
これは、port.c 内に、asm マクロで rxv2 の命令が使われている為で、これを回避する為、C コンパイラから、
アセンブラへオプションを渡しています。

    -Wa,-mcpu=rxv2

アセンブラ (2.28) は、rxv2 をサポートしています。

 % ./rx-elf-as -v --help
GNU assembler version 2.28 (rx-elf) using BFD version (GNU Binutils) 2.28
Usage: ./rx-elf-as [option...] [asmfile...]
Options:
  -a[sub-option...]       turn on listings

...
...
...

  --mcpu=<rx100|rx200|rx600|rx610|rxv2>
  --mno-allow-string-insns
Report bugs to <http://www.sourceware.org/bugzilla/>

RXv1 と RXv2 の主な違い

大きく異なるのは、以下の部分と思います。

  • アキュムレータ:64ビット×1本 ---> アキュムレータ:72ビット× 2本
  • DSP機能命令:9種類 ---> DSP機能命令:23種類

上記のように、DSP 関係の命令で使う「アキュムレータ」が拡張された為、タスク切り替えの際に、RXv2 専用命令で、拡張されたアキュムレータを退避する必要があります。

系列 コア アキュムレータ
RX621, RX62N RXv1 64 ビット x 1
RX631, RX63N, RX63T RXv1 64 ビット x 1
RX24T RXv2 72 ビット x 2
RX64M, RX71M RXv2 72 ビット x 2
RX651, RX65N RXv2 72 ビット x 2
RX66T, RX72T RXv3 72 ビット x 2
RX72M RXv3 72 ビット x 2

※この為、rxv2 対応アセンブラが必須となっています。
※ C コンパイラは、RX マイコンの DSP 命令を使わないと思いますが、ルネサス社がリリースしている DSP ライブラリを使ったプログラムを使う場合、上記の対応が必要と思われます。

FreeRTOS のデバイス依存部分

RX マイコン用(GCC)には、以下の3つが用意されており、今回「RX600」を流用しました。
・RX100
・RX600
・RX600v2
※本来は、RXv2 コアの RX64M では、RX600v2 を使うべきではあるのですが、ルネサス社が、gcc に最新のソースを提供していないようで、gcc-6.4.0 では
対応していません。
ルネサス社が提供する gcc 4.8 系の魔改造版は、RXv2 をサポートしているようですが、いまさら 4.8系を使うのはキビシイので、
今回はとりあえず RX600 で行います。
※ rx-elf-as は RXv2 に対応しているようなので、Cのソースに埋め込まれたアセンブラ命令郡をアセンブラソースとして外に出せば対応できるものと思います。

現在カレントで利用している rx-elf-gcc は 6.4.0 で、アセンブラ rx-elf-as は 2.28 です。
このアセンブラは、rxv2 のアセンブルに対応しています。

C コンパイラは、rxv2 に対応していないものの、FreeRTOS のコード上は直接アセンブラコードが埋め込まれています。
C コンパイラがアセンブラにソースを渡す時にオプションを渡す事が出来る事に気がつき「-mcpu=rxv2」のオプションを渡すよう Makefile に以下のように追加したところ、エラー無くコンパイルが出来る事を確認しました。

-Wa,-mcpu=rxv2

これで、スマートに現在のコンパイラでも、rxv2 対応に出来ました。

起動部分(リセット起動)

RX マイコンの場合、FreeRTOS の起動時、「スーパーバイザモード」にしておく必要があり、自前の「start.s」に手を加えて、ユーザーモードに移行する部分をバイパスする仕組みを追加しています。
※「start.s」をアセンブルする際、「--defsym NOT_USER=1」を付加しています。
アセンブラコードでは以下のようにバイパスします。

# PMレジスタを設定し、ユーザモードに移行する、ユーザースタックに切り替わる
.ifndef NOT_USER
        mvfc    psw,r1
        or      #0x00100000, r1
        push.l  r1

# UレジスタをセットするためにRTE命令を実行する
        mvfc    pc,r1
        add     #10,r1
        push.l  r1
        rte
        nop
        nop
.endif

タイマー割り込み関係

FreeRTOS では、標準的に、1000Hz のタイマー割り込みを使い、タスク並行動作をさせています。
これを実現する為、通常「コンペアマッチタイマー0」を利用します。
このハードウェアー設定は、外部の関数(ユーザーアプリケーション)を呼ぶ構成になっています。
「void vApplicationSetupTimerInterrupt( void )」
また、タイマー割り込み時の関数(port.c 内)は
「vTickISR」
を使う構成になっています。

※cmt_io クラスでは、割り込み時の関数は、内部で閉じており、ファンクタを使って登録するしくみなので、少し改造して、
割り込み関数を引数として与えられるように修正しました。

namespace {
    typedef device::cmt_io<device::CMT0> CMT;
    CMT         cmt_;
}

extern "C" {

    extern void vTickISR(void);
    extern void vSoftwareInterruptISR(void);

    void vApplicationSetupTimerInterrupt(void)
    {
        // CMT0 割り込みを設定(周期は「configTICK_RATE_HZ」(1000Hz)
        uint8_t intr = configKERNEL_INTERRUPT_PRIORITY;
        cmt_.start(configTICK_RATE_HZ, intr, vTickISR);

        // SWINT ベクターの登録
        device::icu_mgr::set_task(device::ICU::VECTOR::SWINT, vSoftwareInterruptISR);
        // SWINT 割り込みレベルを設定して、SWINT 割り込みを有効にする。
        device::icu_mgr::set_level(device::ICU::VECTOR::SWINT, configKERNEL_INTERRUPT_PRIORITY);
    }
};

ソフトウェアータイマー割り込み関係

FreeRTOS では、タスクの切り替えとして、ソフトウェアー割り込みも使います。
この設定(割り込みレベルと、許可ビットの操作)は、port.c 内にあるのですが、それでは、ハードウェアー依存のヘッダー等をインクルードする必要があるので、コメントアウトして、外部に出しています。
※元々、ルネサス社が提供している「iodefine.h」をインクルードしていましたが、構成を少しだけ修正して、依存部分を減らしています。
※依存する部分は、main.cpp 内だけで行い、「port.c」にはRXマイコンソフトウェアー依存部分のみになるようにしています。
※実際には、portmacro.h に、SWINT 起動レジスター関連のコードがハードコードされています。
※上記のタイマー割り込みの設定時に同時に行っています。

また、オリジナルコードでは、「vSoftwareInterruptISR」は、割り込関数内から呼ぶような構成になっていたので、直接呼べるように修正
しましたが、良く見ると、この関数はアセンブラで書かれていて、最後関数を抜ける命令は「RTE」になっている為、どちらでも良い事にな
ります、なので、最終的には、オリジナルコードを優先して、結局元に戻してあります。

/// void vSoftwareInterruptISR( void ) __attribute__((naked));
void vSoftwareInterruptISR( void ) __attribute__((interrupt));
void vSoftwareInterruptISR( void ) __attribute__((naked));

ソフトウェアー割り込みの起動は、RX64M では、ハードウェアー操作で起動できるのですが、普通にソフトウェアー割り込み命令を使えば良さそう
なので、修正しました。
※SWINT は27番ベクターを使います。

#if 0
#define portYIELD()                         \
    __asm volatile                          \
    (                                       \
        "PUSH.L R10                 \n"     \
        "MOV.L  #0x872E0, R10       \n"     \
        "MOV.B  #0x1, [R10]         \n"     \
        "MOV.L  [R10], R10          \n"     \
        "POP    R10                 \n"     \
    )
#else
#define portYIELD() __asm volatile( "INT #27" )
#endif

※ 0x872E0 は、ICUの「ソフトウェア割り込み起動レジスタ(SWINTR)」となっています。
※「MOV.L [R10], R10」は意味不明です。

追記:
「INT #27」と、「0x872E0 = 0x1」は動作が異なるとの知見を得たので、以下のように修正した。
※「MOV.L [R10], R10」は、「予約領域」をアクセスしているので、「CMP [R10].UB, R10」に変更している。

#if 1
#define portYIELD()                         \
    __asm volatile                          \
    (                                       \
        "PUSH.L R10                 \n"     \
        "MOV.L  #0x872E0, R10       \n"     \
        "MOV.B  #0x1, [R10]         \n"     \
        "CMP    [R10].UB, R10       \n"     \
        "POP    R10                 \n"     \
    )
#else
#define portYIELD() __asm volatile( "INT #27" )
#endif

メモリモデルの選択

FreeRTOS が用意しているメモリモデルはいくつかあるので、自分のシステムに合った物を選択すれば良いと思われる。
大雑多に言って:

  • ヒープ領域が狭く(無く)、malloc/free が使えない場合「heap_1.c」。
  • malloc/free が使える場合「heap_3.c」(FreeRTOS がスレッドセーフにして呼んでいる)。

※newlib などが標準的に使っている、malloc/free の実装は、dlmalloc の亜種と思われるが、現在のところ、この記憶割り当てシステムより総合的に高性能な実装は今のところ無いので、これがベターだと思える。

基本設定

FreeRTOS では、RTOS の動作設定を「FreeRTOSConfig.h」で行い、アプリケーション毎に管理するようになっています。

このファイルで、
・割り込み間隔(1000Hz)
・割り込みプライオリティー

など基本的な設定を行います。


超簡単なサンプル

動作確認の為、以下のようなサンプルを作って確認しました。

  • タスク1とタスク2で、LED を0.5秒、0.1秒間隔で点滅します。
    ※ハードウェアーがあれば、別々の LED にした方が良いと思います。
  • シリアルインターフェースを使い文字をシリアル出力します。
  • シリアル出力では、簡単な排他制御を使い、RTOS 対応としています。

・ハードウェアーリソースの定義

    typedef device::system_io<12000000> SYSTEM_IO;
#ifdef GR_KAEDE
    typedef device::PORT<device::PORTC, device::bitpos::B1> LED;
    typedef device::PORT<device::PORTC, device::bitpos::B0> LED2;
    typedef device::SCI7 SCI_CH;
    static const char* system_str_ = { "GR-KAEDE" };
#else
    typedef device::PORT<device::PORT0, device::bitpos::B7> LED;
    typedef device::SCI1 SCI_CH;
    static const char* system_str_ = { "RX64M" };
#endif


    typedef device::cmt_io<device::CMT0> CMT;
    CMT         cmt_;

    typedef utils::fixed_fifo<char, 512> RXB;  // RX (RECV) バッファの定義
    typedef utils::fixed_fifo<char, 256> TXB;  // TX (SEND) バッファの定義

    typedef device::sci_io<SCI_CH, RXB, TXB> SCI;
    SCI         sci_;

・シリアルインターフェース排他制御

    // syscalls.c から呼ばれる、標準出力(stdout, stderr)
    void sci_putch(char ch)
    {
        static volatile bool lock_ = false;
        while(lock_) ;
        lock_ = true;
        sci_.putch(ch);
        lock_ = false;
    }


    void sci_puts(const char* str)
    {
        static volatile bool lock_ = false;
        while(lock_) ;
        lock_ = true;
        sci_.puts(str);
        lock_ = false;
    }

・FreeRTOS とのインターフェース部分(一部)

    extern void vTickISR(void);
    extern void vSoftwareInterruptISR(void);

    void vApplicationSetupTimerInterrupt(void)
    {
        uint8_t intr = configKERNEL_INTERRUPT_PRIORITY;
        cmt_.start(configTICK_RATE_HZ, intr, vTickISR);

        device::icu_mgr::set_task(device::ICU::VECTOR::SWINT, vSoftwareInterruptISR);
        device::icu_mgr::set_level(device::ICU::VECTOR::SWINT, configKERNEL_INTERRUPT_PRIORITY);
    }

・各タスク

    void vTask1(void *pvParameters)
    {
        uint32_t loop = 0;
        uint32_t cnt = 0;
        while(1) {
            LED::P = !LED::P();
            vTaskDelay(500 / portTICK_PERIOD_MS);
            ++loop;
            if(loop >= 10) {
                loop = 0;
                utils::format("Task1: %u\n") % cnt;
                ++cnt;
            }
        }
    }


    void vTask2(void *pvParameters)
    {
        uint32_t loop = 0;
        uint32_t cnt = 0;
        while(1) {
#ifdef GR_KAEDE
            LED2::P = !LED2::P();
#else
            LED::P = !LED::P();
#endif
            vTaskDelay(100 / portTICK_PERIOD_MS);
            ++loop;
            if(loop >= 12) {
                loop = 0;
                utils::format("Task2: %u\n") % cnt;
                ++cnt;
            }
        }
    }

    void vTask3(void *pvParameters)
    {
        uint32_t cnt = 0;
        while(1) {
            utils::format("Task3: %u\n") % cnt;
            ++cnt;
            vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
    }

・メイン部分

int main(int argc, char** argv)
{
    SYSTEM_IO::setup_system_clock();

    LED::OUTPUT();  // LED ポートを出力に設定
    LED::P = 1;     // Off
#ifdef GR_KAEDE
    LED2::OUTPUT();
    LED2::P = 1;
#endif

    {  // SCI の開始
        uint8_t intr = 2;        // 割り込みレベル
        uint32_t baud = 115200;  // ボーレート
        sci_.start(baud, intr);
    }

    auto clk = F_ICLK / 1000000;
    utils::format("Start FreeRTOS sample for '%s' %d[MHz]\n") % system_str_ % clk;

    {
        uint32_t stack_size = 512;
        void* param = nullptr;
        uint32_t prio = 1;
        xTaskCreate(vTask1, "Task1", stack_size, param, prio, nullptr);
        xTaskCreate(vTask2, "Task2", stack_size, param, prio, nullptr);
    }

    vTaskStartScheduler();

    // タスクスケジューラーが正常なら実行されない
    while(1) {
        utils::delay::milli_second(250);
        LED::P = !LED::P();
        utils::delay::milli_second(250);
        LED::P = !LED::P();
    }
}

Github FreeRTOS/main.cpp


最後に

ソフトウェアー割り込みの部分で、少し「ハマリ」ましたが、無事動作を確認出来ました。

ただ、現状では、各種ドライバクラスも、マルチタスクを考慮して作られていないので、まだまだ実用的に使うには、時間がかかりそうですが、
応用範囲が広がり、色々な展開が見込めます。

ネットワーク関係のプロトコルを扱う場合、オープンソースのスタック(lwIP など)では、マルチタスクが基本なので、これもありがたいです。
※FreeRTOS-Plus には、TCP/UDP のプロトコルスタックもあるので、時間が出来たら実験してみたいと思います。

全ソースコードは以下に置いてありますので参照して下さい。

ルネサス社の FreeRTOS 資料:
https://www.renesas.com/jp/ja/doc/products/mpumcu/apn/rx/002/r01an4307js0100-rx.pdf