リアルタイム カーネル for Arduino UNO R3 #2

2025年4月2日水曜日

17. 組み込みシステム

t f B! P L

 

はじめに

前回に引き続き、Arduino UNO R3のリアルタイムカーネルの話です。
今回は、複数の割り込み処理からタスクを起動して、優先順位に従った多重処理(マルチタスク)が実現できているかを確認していきます。

多重処理(マルチタスク)の検証なので、各タスクに設定している処理負荷がポイントになります。今回は以下の処理負荷の設定です。

処理周期タスク名処理負荷
10mstask_10ms()1ms
100mstask_100ms()50ms
1sectask_1s()300ms

1秒間のトータルの処理負荷は900ms(90%)です。適切な優先順位の設定と、優先順位に従った多重処理を実現しないと、各処理が規定頻度で実行できない条件での検証になります。

ダウンロード

こちら→ リンク の example2.zip です。解凍してできる example2フォルダ には
 RTK4ArduinoUnoR3.ino
 example2.ino
の2つの .ino ファイルがあります。example2フォルダ を Arduino IDE のフォルダに置けば Arduino IDE で利用可能になります。( GitHubでも公開 )

ソースファイル

RTK4ArduinoUnoR3.inoはリアルタイムカーネルのコードで前回と同じです。example2.ino は複数の割り込み処理からタスクを起動するアプリケーションの例です。今回は、アプリケーション(example2.ino)について説明します。

///////////////////////////////////////////////
// アプリケーション
///////////////////////////////////////////////
#include <avr/interrupt.h>
#include <avr/io.h>

void task_sw(unsigned char no);			  // タスク起動要求
void task_create(void(*task)(void), unsigned char id, unsigned char level); // タスク生成

volatile unsigned long c10ms,c100ms,c1s;
unsigned long n10ms=0,n100ms=0,n1s=0;

// Taskの最大定義数
#define TASK_MAX  4

// Task IDの定義   0~1(TASK_MAX-1)の数値
#define	taskId_10ms	  0
#define	taskId_100ms	1
#define	taskId_1s	    2
#define	taskId_bg   	3

// Application Task
void  task_10ms(void);       // task 優先順位:8
void  task_100ms(void);      // task 優先順位:6
void  task_1s(void);         // task 優先順位:4
void  task_bg(void);         // task 優先順位:2

///////////////////////////////////////////////
//  Arduino IDE Setup + Loop
///////////////////////////////////////////////

void setup() {
  // put your setup code here, to run once:

  ///////////////////////////////////////////////
  //  Taskを生成
  //
  //  Taskの関数名、ID(定義No.)、優先順位を登録しTaskを生成する。
  //  Taskの優先順位は数値が大きい方が優先順位が高い。
  //
  //  task_create( 関数名, Task ID, 優先順位)
  //  優先順位 task_10ms > task_100ms > task_1s > task_bg
  ///////////////////////////////////////////////
  task_create(task_10ms, taskId_10ms, 8);
  task_create(task_100ms, taskId_100ms, 6);
  task_create(task_1s, taskId_1s, 4);
  task_create(task_bg, taskId_bg, 2);

  // シリアル通信の設定
  Serial.begin(9600);
  while (!Serial);

  ////////////////////////////////////////
  // Set Up Timer1(16bit)
  ////////////////////////////////////////

  // タイマーコンペア初期値を設定 
  OCR1A = 625;	    // Compare match 10ms
  OCR1B = 6250;	    // Compare match 100ms
  // フリーランモードに設定
  TCCR1A = 0;       // Free Run mode
  // 分周比256に設定(16us) タイマースタート
  TCCR1B = 0x04;    // CS12 1/256(16us)
  // コンペアマッチA/Bとオーバーフロー割り込みを許可
  TIMSK1 = 0x07;    // OCIE1A, OCIE1B, TOIE1

  Serial.println("ready to go");
  task_sw(taskId_bg);   // BG TASK起動

}

void loop() {
  // put your main code here, to run repeatedly:
}

///////////////////////////////////////////////
//  ISR
///////////////////////////////////////////////

// TIMER1 COMPA 割り込み処理
ISR(TIMER1_COMPA_vect) {
  // コンペア値を更新 
  OCR1A = OCR1A+625;    // 10msごとに割り込み
  // 10ms処理を起動する
  task_sw(taskId_10ms);
}

// TIMER1 COMPB 割り込み処理
ISR(TIMER1_COMPB_vect) {
  // コンペア値を更新 
  OCR1B = OCR1B+6250;	  // 100msごとに割り込み
  // 100ms処理を起動する
  task_sw(taskId_100ms);
}

// TIMER1 OVF 割り込み処理
ISR(TIMER1_OVF_vect) {    // オーバーフロー1048msごとに割り込み
  // 1s処理を起動する
  task_sw(taskId_1s);
}

///////////////////////////////////////////////
//  Task
///////////////////////////////////////////////

// 10ms処理
void task_10ms(void) {
  unsigned long i;
  for (i=0;i<588;i++) c10ms++;      // 1ms負荷
  n10ms++;
}

// 100ms処理
void task_100ms(void) {
  unsigned long i;
  for (i=0;i<29452;i++) c100ms++;   // 50ms負荷
  n100ms++;
}

// 1s処理
void task_1s(void) {
  unsigned long i;
  for (i=0;i<183520;i++) c1s++;     // 300ms負荷
  n1s++;
}

// BG処理
void task_bg(void) {
  while(1){
    delay(1000);  // 1000ms待機
    Serial.print("Execution Count ");
    Serial.print(n10ms); 
    Serial.print(" ");
    Serial.print(n100ms); 
    Serial.print(" ");
    Serial.println(n1s); 
  }
}

アプリケーションの説明

このアプリケーションでは、複数の割り込み処理からタスクを起動しています。タイマー割り込みから起動する3つのタスクと、バックグラウンドのタスクを1つ動かしています。

タスク定義

タイマー割り込みから起動する3つのタスクは、それぞれ、10ms、100ms、1秒周期のタイマー割り込みから起動される task_10ms()とtask_100ms()、task_1s()です。バックグラウンドのタスク task_bg() は、Arduino の setup() の最後に起動しています。

各タスクの優先順位は以下の通りです。
 task_10ms() > task_100ms() > task_1s() > task_bg()

タイマーの設定

ATmega328Pには3つのタイマーがありますが、このアプリケーションでは、Timer1(16bit)を利用して、10ms、100ms、1秒周期のタイマー割り込みを発生させています。Timer1の設定は以下の通り。

OCR1A, OCR1B

コンペアレジスタに初期値を設定。
 OCR1A = 625     Compare match 16us×625=10ms
 OCR1B = 6250   Compare match 16us×6250=100ms

TCCR1A

 TCCR1A = 0       フリーランモードに設定

TCCR1B 

 TCCR1B = 0x04 分周比256(16us)に設定し、タイマースタート。

TIMSK1

  コンペアマッチA/Bとオーバーフロー割り込みを許可。
 TIMSK1 = 0x07 (OCIE1A, OCIE1B, TOIE1)

タイマー割り込み処理(ISR)

10ms、100ms、1秒の周期処理は、通常は10ms処理でカウントして100ms処理、1秒処理を起動すればよいですが、今回はタスクの多重処理の確認が目的であるため、個別の割り込み処理としています。

TIMER1 COMPA 割り込み処理

コンペアレジスタAを前回設定値+625で更新。(フリーランモードなので)
 OCR1A = OCR1A+625
task_10ms()を起動。

TIMER1 COMPB 割り込み処理

コンペアレジスタBを前回設定値+6250で更新。(フリーランモードなので)
 OCR1B = OCR1B+6250
task_100ms()を起動。

TIMER1 OVF 割り込み処理

16bitタイマーのオーバーフロー割り込みです。1024ms周期で発生するので、1秒タスク task_1s() を起動します。

タスクに設定している処理負荷

タイマー割り込みから起動する3つのタスクでは、カウンタをインクリメントしているだけですが、それぞれ、下記の処理負荷になるようにしています。

処理周期 タスク名 処理負荷
10ms task_10ms() 1ms
100ms task_100ms() 50ms
1sec task_1s() 300ms

1秒間のトータルの処理負荷は900ms(90%)です。
適切な優先順位の設定と、優先順位に従った多重処理(マルチタスク)が実施されなければ、処理が規定頻度で実施されない条件です。

尚、バックグラウンドのタスク task_bg() は、各周期タスクの1秒ごとの実行回数をPCに送っています。Arduino IDEのデフォルトである loop()処理 で実施しても同じですが、今回はタスクの多重処理の確認が目的であるためタスク定義としています。

結果

PCで1秒ごとの各周期タスクの実行回数を確認します。

結果、各タスクは抜けることなく実行されています。リアルタイムカーネルの優先制御により、優先順位に従った多重処理(マルチタスク)が実施されていることが確認できました。

このテストアプリケーションでは、バックグラウンドのタスク task_bg() で周期タスクの1秒ごとの実行回数をPCに送っています。task_bg()は、1秒待つのに delay(1000) を使っています。

よく delay() はCPUの処理を止めるので使わない方が良いとの記述を見ますが、上記の結果からも分かるように、リアルタイムカーネルで多重処理すれば、最下位のタスクで delay() を使う分にはCPUの処理性能を無駄にすることはありません。( task_bg()が delay() で1秒待つ間に、上位タスクが 900msの処理を完了しています。)

優先制御がないと

ところで、上記の各タスクの負荷条件で、10ms,100ms,1secのタスクの優先順位を同一にすると、疑似的なポーリング処理(処理要求のフラグを立てて、バックグラウンドループでそのフラグをチェックして各タスクを実行する)のような動作になります。

やってみると、優先制御ができないので、10msタスクは40%程度、100msタスクは70%程度の実行回数になりました。また、プロセッサの有効稼働率は70%程度になり20%程低下しました。適切な優先順位の設定と、優先制御による多重処理(マルチタスク)が実施できなければ、処理が規定頻度で実施されず、プロセッサの性能を効率的利用できないことがわかります。

終わりに

最後までお読みいただきありがとうございました。

今回のリアルタイムカーネルのROM/RAM消費量を再度示すと、
コードのROM消費量は644Byte。RAM消費量は、SCBの6Byteとタスク毎に管理データ7Byte、またスタックは、タスクの起動時に通常の関数コールに対して6Byteを追加で消費します。

これで、リアルタイム・カーネルのArduino UNO R3編は終了です。何かのお役に立てましたら幸いです。

Arduinoの手軽さは素晴らしいなぁ。

QooQ