スパイス  組み込み制御装置の受注製作

ライブラリの導入
平成26年10月13日

 MPLAB Harmonyの使い方(6) FreeRTOSの導入(1)
 前回でタイマ動作の設定方法や割り込みを含めた周辺機能の使い方が分かりました。
類似の機能であるUARTやSPIについても動作検証すべきとは思いますが、ここは先にFreeRTOSの導入を行います。

 従来のCPUに対しては自作のコルーチンおよびタスク切り替え機能を使用して、実質的にマルチタスクを運用してきました。
この程度の機能でも十分な用途は結構あり、今までもあまり不自由を感じたことはないのですが、CPUの性能が一桁上のレベルに上がります。これを機会にファイルシステムや高速通信などを前提にしたより大規模なシステムにまで拡張可能なプラットフォームを作成します。
 MPLAB Harmony(使用しているVer.は1.00)にはRTOSを使ったサンプルプロジェクトbasic_freertos.Xが添付されています。
このサンプルをベースにして基本機能を動かすまでに必要なファイルや設定を確認します。
    (注意事項)
 このVer.のサンプルでは、プロジェクト自体がMPLAB Harmony Configratorで作成されていないようです。
このため自前で最初からプロジェクトを作成した場合は、このサンプルプロジェクトと同じファイル構成にはならない可能性もありあります。
 当面の目標はRTOSを含めたプロジェクトのパラメータ設定の場所と内容を掌握することになります。
MPLAB Harmony Configratorでの設定内容はsystem_config.hおよびsystem_init.cに保存されています。
が、FreeeRTOSに対する主要な設定はFreeRTOSConfig.hにあります。それ以外にもハードウェアの変更によって変更が必要な設定項目があるかもしれません。


 RTOSを導入するにあたって、組み込み用のRTOSが持つべき基本的な機能に関する知識が必要です。
ある程度の説明は書いていくつもりですが十分な説明をすることは困難です。
ネット上で参考になりそうなHPを書いておきます。
  後閑氏のHP    云わずと知れた超有名HPです。

 もう少し基本に立った情報が必要なら下記のような参考書があります。
参考文献
  リアルタイム組み込みOS    ISBN4-7981-1004-3
原著者は外国人です。外国人特有の非常に丁寧な説明がされています。が、その分難しい内容にまで踏み込んでいます。
最初は戸惑うこともあると思いますが、後々まで参考になる内容です。(個人的にはこちらがお勧め)
  リアルタイムOSから出発して組み込みソフトエンジニアを極める    ISBN978-4-434-15937-4
こちらは日本人の著作です。組み込みOSだけではなく、OSを使う上での機能分割の考え方や手法にまで議論されています。
OS基本部分の説明を望むなら参考になりそうです。が、機能分割に関する内容は消化不良な気がします。


    basic_freertos.Xは何をするプロジェクトか
 このプロジェクトはRTOSを使った非常に単純なプログラムです。
内容を説明するために、元のプログラムから内容が変わらない範囲で書き直したリストを示しながら説明します。
内部では4個のタスクに分かれています。タスクとは、基本的に別個に動作する個別のプログラムです。
このうちプログラム1と2および3はキューと呼ばれるデータの通信路で接続されています。
このため、個々が完全に独立したプログラムではなく、データの受け渡しによる部分的な同期が発生します。
プログラム4は他とは完全に独立したプログラムです。

  プログラム1:キューにデータを送信するプログラム
 200msおきにキューにデータ(100および1000)を出力します。
    vTaskDelayUntil(&xNextWakeTime, time)関数
xNextWakeTime(前回動作開始時の時間を記憶している)からtime(今回は200)だけタイマの値が経過するまで待機します。
ここで引数timeは経過時間ではなく、タイマのカウントアップ値です。つまり、1カウントが1msなら10カウントで10ms、1カウントが2msなら10カウントは20msになります。これから出てくるOS付属の関数では時間の指定は全てがこのルールに従います。
ここではカウント値がmsに等しいとして説明します。
xNextWakeTimeは待機終了時に内部で自動的に更新されるので、初期値を設定する以外は更新不要です。
    xQueueSend(to, *value, delaytime)関数
キューにデータを出力します。toは複数ありえるキューから特定のキューを指定します。delaytimeはキューへの出力時間を指定時間だけ遅らせます。
 この例から分かるようにキューで送受信できるのは固定長のデータのみです。
//プログラム1
static void QueueSendTask( void *pvParameters )  //キューにデータを出力する
{
portTickType xNextWakeTime;
const unsigned long ulValueToSend1 = 100UL;
const unsigned long ulValueToSend2 = 1000UL;

        /* Check the task parameter is as expected. */  //エラー検出(今は無視)
        configASSERT( ( ( unsigned long ) pvParameters ) == QUEUE_SEND_PARAMETER );

        /* Initialise xNextWakeTime - this only needs to be done once. */
        xNextWakeTime = xTaskGetTickCount();  //処理の開始時間を得る

        for( ;; )
        {
                vTaskDelayUntil( &xNextWakeTime, 200 );  //開始時間から200ms経過するまで待つ

                xQueueSend( xQueue, &ulValueToSend1, 0U );  //データ100を出力
                xQueueSend( xQueue, &ulValueToSend2, 0U );  //データ1000を出力
        }
}
 次に受信側のプログラムを示します。受信側は二つの関数が受信を行います。
    xQueueReceive(name, *data, tme)関数
nameで指定されたキューにデータが到着するのを待つ。timeは最大待ち時間でデータがこの指定時間以内に到着しないときも、処理を再開する。設定値portMAX_DELAYは無限に待ち続ける。
 従って、これらのプログラムはキューにデータが到着するのをずっと待っています。データが到着後は
//プログラム2
static void QueueReceiveTask1( void *pvParameters )
{
unsigned long ulReceivedValue;
        /* Check the task parameter is as expected. */  //エラー検出(今は無視)
        configASSERT( ( ( unsigned long ) pvParameters ) == QUEUE_RECEIVE_PARAMETER );
        for( ;; )
        {
                xQueueReceive( xQueue, &ulReceivedValue, portMAX_DELAY );  //キューの受信までずっと待つ
               if( ulReceivedValue == 1000UL )
                {
                        BSP_LEDToggle( TASK1_LED );
                        ulReceivedValue = 0U;
                }
        }
}

//プログラム3
static void QueueReceiveTask2( void *pvParameters )
{
unsigned long ulReceivedValue;
        /* Check the task parameter is as expected. */  //エラー検出(今は無視)
        configASSERT( ( ( unsigned long ) pvParameters ) == QUEUE_RECEIVE_PARAMETER );
        for( ;; )
        {
                xQueueReceive( xQueue, &ulReceivedValue, portMAX_DELAY );  //キューの受信までずっと待つ
                if( ulReceivedValue == 100UL )
                {
                        BSP_LEDToggle( TASK2_LED );
                        vTaskDelay((portTickType)ulReceivedValue);  //100ms処理を中断する
                        ulReceivedValue = 0U;
                }
        }
}

ここで、幾つかの疑問が出てきます。  実は、タスクには実行に際して優先順位が設定されています。この優先順位はタスクを生成するときに設定します。
今回のケースでは各タスクの優先順位はタスク1−2−3に対して1−2−3となっています。数字の大きな方が高い優先度を持ちます。
従って、タスク1がキューにデータ100を送信すると、この段階でタスク1の処理は中断し、キューの受信待ちをしていたタスク3が起動します。タスク2はこの時点では優先度がタスク3よりも低いので動作できません。
タスク3はLED表示を反転後、100msの間処理を中断します。この中断によって、再びタスク1の処理が再開しキューにデータ1000を出力します。ここでタスク2が起動し(タスク3はまだ停止中のため動作できません)、LED表示を反転します。
 このようにタスク1で出力されるデータ100は必ずタスク3で受信され、次のデータ1000は必ずタスク2で受信されることになります。  OSの内部では基本的に(使わない設定が許されているかもしれないですが)タイマを使った経過時間の管理を行っています。
従ってOSの実装にはタイマを一つ使用します。FreeRTOSの標準仕様ではタイマ1を使用します。
 タイマを使用するということは、PBCLKやタイマのプリスケーラといったハードウェアに関連した初期値の設定が必要になります。


  プログラム4: タイマとセマフォによる周期的動作
 こちらはタスク1〜3とは全く関連無く動作しています。
上記で説明したOS管理下のタイマとは別にタイマ5を使用して一定周期ごとにLEDを点滅しています。
    セマフォとは
 簡単にいうと占有権を示すフラグのようなもの。例えばシリアルポートのように一つのハードウェアを一つのプログラムが占有する使い方をする場合、セマフォをtakeすることで他のプログラムはこのシリアルポートにアクセスできない。占有が終わるとGiveによって他のプログラムに対してハードウェアを開放する。セマフォ作成時の状態はtakeされた状態のようです。
     vSemaphoreCreateBinary(name)
 セマフォをnameという名前で作成する。
     xSemaphoreTake(name, time)
nameというセマフォに占有要求を出す。占有できるまで処理は中断する。timeは最大待ち時間。
    xSemaphoreGiveFromISR(name, *xHigherPriorityTaskWoken)
 割り込み処理関数内部からnameセマフォの占有解除する。xHigherPriorityTaskWokenは、このセマフォ解除によって割り込み処理開始前のタスクより上位優先度のタスクが動作する場合にpdTRUE、そうでない場合にpdFALSEが設定される。この関数を呼び出す時にはpdFalseを設定しておく。この変数は、後にportEND_SWITCHING_ISR()関数を呼び出すために必要。
(この説明だけでは良く分からないと思うので、今の段階では指示されたとおりにしておくことにする)
    portEND_SWITCHING_ISR(xHigherPriorityTaskWoken)
 xSemaphoreGiveFromISR()関数を使用した後には無条件にこの関数が必要。(ここも指示に従う)
//プログラム4
static void ISRBlockTask( void* pvParameters )
{
        APPTaskParameter_t *pxTaskParameter;
       pxTaskParameter = (APPTaskParameter_t *) pvParameters;

        vSemaphoreCreateBinary( xBlockSemaphore );  //セマフォの作成
    //以下はタイマ5の設定、50msおきに割り込みが発生する。詳細は省略
        /* Set up timer 5 to generate an interrupt every 50 ms */
        PLIB_TMR_Counter16BitClear(TMR_ID_5);
        /* Timer 5 is going to interrupt at 20Hz Hz. (40,000,000 / (64 * 20) */
        PLIB_TMR_PrescaleSelect(TMR_ID_5, T5PRESCALAR);
        PLIB_TMR_Period16BitSet(TMR_ID_5, pxTaskParameter->timerPeriod);
        /* Clear the interrupt as a starting condition. */
        PLIB_INT_SourceFlagClear(INT_ID_0,INT_SOURCE_TIMER_5);
        /* Enable the interrupt. */
        PLIB_INT_SourceEnable(INT_ID_0,INT_SOURCE_TIMER_5);
        /* Start the timer. */
        PLIB_TMR_Start(TMR_ID_5);

        for( ;; )
        {
            xSemaphoreTake( xBlockSemaphore, portMAX_DELAY );  //セマフォによる処理開始待ち
            BSP_LEDToggle( pxTaskParameter->usLEDNumber );

        }
}
/*-----------------------------------------------------------*/
void vT5InterruptHandler( void )
{
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;

        xSemaphoreGiveFromISR( xBlockSemaphore, &xHigherPriorityTaskWoken ); //セマフォによる処理開始指示

        /* Clear the interrupt */
        PLIB_INT_SourceFlagClear(INT_ID_0,INT_SOURCE_TIMER_5);

        portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );  //xSemaphoreGiveFromISRとセットの処理
}
/*-----------------------------------------------------------*/
//割り込み処理関数(アセンブラ記述)
#include <xc.h>
#include <sys/asm.h>
#include "ISR_Support.h"

        .set    nomips16
        .set    noreorder

        .extern vT5InterruptHandler
        .extern xISRStackTop
        .global vT5InterruptWrapper

        .set    noreorder
        .set    noat
        .ent    vT5InterruptWrapper

vT5InterruptWrapper:

        portSAVE_CONTEXT
        jal vT5InterruptHandler
        nop
        portRESTORE_CONTEXT

        .end    vT5InterruptWrapper
 プログラムの動作としては、タイマ5を50msおきに割り込みが入る設定にしておいて、割り込み処理内で出されるセマフォのGiveを待っています。takeが成功するとLEDを反転させます。結局、メイン側のLED処理もタイマ5の割り込み周期に同期することになります。

 このプログラムがタスク1〜3と平行して動作する前提での疑問は下記のとおりです。  これもタスクの生成時に決められていて、優先度は3となっています。つまり、先のタスク1〜3の中では最優先のタスク2と同じ優先度です。感覚的には、本タスクが実行されている合間にタスク1〜3が実行されるような感じになります。
では、タスク2の要求と同時に本タスクが起動されたらどうなるか?
極めて厳密な話にまで突き詰めると、両者の間にも明確な優先度がありますが、そんな微妙な動作を気にする位なら、両者の優先度に差をつけることです。そうすれば問題はなくなります。OSのような規模の大きなソフトウェアを扱うときには出来るだけ微妙な動作を避けることの方が重要です。  割り込み処理内部でOS依存の関数を記述するときには、事前と事後にタスクコンテキストの保存と回復が必要なようです。そのような動作が前提となっていると考えるしかありません。


   PIC32USB実験基板の仕様に合わせて変更する 
 このプログラムはマイクロチップ社のデモンストレーションボードを前提に作られています。
私が使用するPIC32USBで動作するように、ハードウェアの違いを修正します。  system_init.cのConfigBitsのうち、FPLLIDIVの設定を DIV_2からDIV_4に変更します。
(H26/10/16追記)
抜けていました。POSCMODをXTからHSに変更します。
(追記ここまで)
LEDの出力波形はタスク2・3は200msおきに出力が反転するので2.5Hz周期の信号波形になり、タスク4では50msおきに出力反転するので10Hzの出力となります。
変更後のタスク4LED出力波形を示します。手ブレでピントが甘いのですが、時間軸の目盛りは20ms/divです。画面右下の周波数表示では9.9999Hzを示しています。タスク2・3の波形は示さないですが、正しく2.5Hzを示しています。


 今後やってみたいことは、下記のようなことを考えています。
 FreeRTOSの設定ファイルはFreeRTOSConfig.hです。MPLAB Harmony Configrator画面での設定内容はFreeRTOSConfig.hに反映されるのかどうか。されるとすれば何処までされるのかを確認したい。
もし、全く反映されないのであれば、FreeRTOSのソースはライブラリとして別にコンパイルした方がプロジェクトの複雑さを軽減できます。  上記の結果にもよりますが、ハードウェアの変更を人手で修正が必要なら、どのファイルを変更する必要があるのかを把握しておくのは重要です。実際にハード仕様を変更して必要な変更箇所を確認したい。

目次へ  前へ  次へ