Seeeduino XIAOでFreeRTOSを実装する

組み込みエンジニア

こんにちは、ENGかぴです。

FreeRTOSライブラリを使用するとマルチタスクの動作ができます。BME280から温湿度・気圧の情報を取得しLCDに表示するタスクと動作中であることを表示するLEDを点灯/消灯させるタスクを登録して動作確認を行いました。

BME280使用 温湿度・気圧センサモジュールキットAE-BME280(秋月電子)を使用してデータを取得します。LCDはAQM1602XA-RN-GBW(秋月電子)を使用しています。

FreeRTOSを実装する

Seeeduino XIAOは通常シングルタスクで動作しますが、FreeRTOSライブラリを使用するとマルチタスクの動作が実現できます。厳密さはありませんがイメージすると以下の通りになります。

タスクのイメージ
タスクのイメージ

シングルタスクの場合はloop()内に記述した処理が順番に処理されます。処理Aの次に処理Bは処理されることを繰り返します。この場合、処理Aがイベントなどで処理が重たくなった場合処理Bに遷移するタイミングが遅くなります。

FreeRTOSを使用した場合は処理Aと処理Bがそれぞれ別のスレッド上でタスク動作するためお互いの処理が影響しなくなります。カーネルと呼ばれるスケジュール管理で優先度の高いタスクに切り替えながら処理を行います。

FreeRTOSライブラリを使用する

#include <Seeed_Arduino_FreeRTOS.h>

TaskHandle_t Handle_Bme280; //タスクを管理する変数
TaskHandle_t Handle_LED; //タスクを管理する変数
TickType_t tickTimled; //RTOS上の遅延時間(タスクの一時停止)用のタイマ
TickType_t tickTimbme280; //RTOS上の遅延時間(タスクの一時停止)用のタイマ

FreeRTOSを使用するためにSeded_Aruino_FreeRTOS.hをインクルードします。タスクを管理する変数をTaskHandle_tの型で宣言します。TickType_tはタスクを一時停止する場合に使用するタイマを管理する変数です。

void setup(){

    xTaskCreate(threadBme280, "Task_BME280", 256, NULL, 2, &Handle_Bme280 );
    xTaskCreate(threadLed, "Task_LED", 256, NULL, 1, &Handle_LED );
    vTaskStartScheduler(); //スケジュラースタート
}

xTaskCreate()でタスクを登録します。第1引数にタスク処理を行う関数を指定します。第2引数にタスクの名前を指定します。第3引数にスタック領域を使用する最大数を指定します。第4引数は使用しないのでNULLにします。第5引数はタスクを管理する変数を指定します。

static void threadLed(void* pvParameters){

    while(1){
        digitalWrite(PIN_DO1,HIGH);
        myDelayMsUntil(&tickTimled,200);
        digitalWrite(PIN_DO1,LOW);
        myDelayMsUntil(&tickTimled,200);
    }
}

LEDを点灯/消灯するタスクの例を示しています。digitalWrite()でDO出力を操作しmyDelayMsUntil()でタスクを一時停止します。第1引数にタスクを一時停止する期間を保管する変数のアドレスを指定し、第2引数にタスクを一時停止したい期間をセットします。

タスクを一時停止した場合は、一時停止しているタスクよりも低い優先度のタスクを実行することができます。delay()でウェイトを置いた場合は優先度が高いタスクしか処理することができなくなるため注意が必要です。

必要に応じてdelay()とmyDelayMsUntil()を使い分ける必要があります。

スタックオーバーに注意

スタックオーバーに注意
スタックオーバーに注意

RAM領域の変数割り当てはユーザー使用領域はアドレスの上位から順番に変数が割り当てられますが、スタック領域はRAM領域の下位から上位に積み重なるよう(後入れ先出し)に割り振られます。

タスク登録を増やすとスタック領域が大きくなるためユーザー使用領域と衝突しやすくなります。ユーザー使用領域とスタック領域が衝突することをスタックオーバーといいますが、これが発生すると動作が不安定になるため注意が必要です。

BME280ライブラリを使用する

BME280のライブラリが公開されているためArduinoライブラリに追加して使用します。

Adafruit BME280 Libraryの追加
Adafruit BME280 Libraryの追加

ライブラリマネージャの検索欄にbme280を入力するとBME280ライブラリの候補が表示されます。候補の中からAdafruit BME280 Libraryをインストールします。インストールするライブラリは任意でも問題ありませんが使用方法などはスケッチ例を参考にして実装してください。

Adafruitの他のライブラリを追加する
Adafruitの他のライブラリを追加する

Adafruit BME280 Libraryをインストールする際に他の追加ライブラリをインストールするか選択するメッセージが表示されることがあります。

Adafruit BME280 Libraryは他のライブラリと関連性があるため単体では使用できないためインストールを勧められています。Install allを選択してその他のライブラリも追加します。

#include <Adafruit_BME280.h>
#include <Wire.h>
//変数宣言
Adafruit_BME280 bme280; //測定情報などを格納する変数を宣言
//初期化
bme280.begin(0x76); //スレーブアドレスに0x76を指定して初期化
//使用例
bme280data[0] = bme280.readTemperature(); //温度情報を取得
bme280data[1] = bme280.readPressure() / 100.0F; //気圧情報を取得
bme280data[2] = bme280.readHumidity(); //湿度情報を取得

Adafruit BME280 Libraryをインクルードします。setup()関数内でbme28.begin()でライブラリの初期化を行います。BME280をI2C通信で使用するためWire.hも同時にインクルードします。

BME280が測定した情報などを格納する変数を宣言します。例ではbme280をAdafruit_BME280の型で宣言しています。

温度情報の取得はreadTemperature()、気圧情報の取得はreadPressure()、湿度情報の取得はreadHumidity()を使用します。

BME280のメーカーであるBOSCH社のAPIを組み込んで使用して動作確認した結果を下記記事にまとめています。

Seeeduino XIAOのWireを使ってBME280のデータを取得

Seeeduino XIAOのSPIを使ってBME280のデータを取得

LCDに表示する

引用:LCD資料(参考資料)ー秋月電子HP(AQM1602XA-RN-GBW)

LCDで文字を表示するために初期化を行います。秋月電子のHPに公開されているAQM1602XA-RN-GBWのLCD資料(参考資料)の例を一部抜粋して説明します。LCDにコマンドを送るためLcdComand()関数を実装します。

/* LCDへのコマンド処理 */
void LcdComand(uint8_t cmd){

    Wire.beginTransmission(SLAVE_ADRS);
    Wire.write(0x00);
    Wire.write(cmd);
    Wire.endTransmission();
    delay(1);
}

スレーブアドレスを指定して0x00を書き込んだ後にコマンドを書き込みます。初期化の手順に従ってコマンドを引数に指定することで設定を行います。

/* LCD初期化 */
void LcdInit(void){
 
    LcdComand(FUNC1_SET); //8ビットバス・2LINE表示
    //省略
    LcdComand(FUNC1_SET); //拡張コマンドをオフ
    DspClear();
    LcdComand(DISP_ONOFF_SET);
    delay(2000);
}

初期化の例に従って8ビットバス・2LINE表示、内部周波数調整、コントラストの設定などを行います。

/* LCDに文字を表示 */
void LcdWriteData(uint8_t *data, uint8_t sz){

    Wire.beginTransmission(SLAVE_ADRS);
    Wire.write(0x40);
    Wire.write(data, sz);
    Wire.endTransmission();
}
//使用例
    LcdWriteData( &initmoji[0][0], sizeof(initmoji[0])); //1段目の表示

LCDに文字を表示するためLcdWriteData()を実装します。第1引数に表示したい文字を示すアドレス、第2引数にサイズを指定します。

引用:ST7032のデータシート(2-line Interface protocol)
引用:ST7032のデータシート(2-line Interface protocol)

スレーブアドレスを送信した後にcontrol byteを指定しますが2-line interface protocolに従って書き込みアドレスと文字データを指定します。control byte以降に制御コードを書き込まないのでCoに0をセットしRSがHになるので0x40になります。R/WはW固定なのでLになります。

control byteの後に送信する文字をセットしてサイズ分書き込み、最後にSTOP状態の通知のためendTransmissionを送信します。

void ShowData(void){
    char temphumi[16];
    char press[16];

    DspClear();
    sprintf(temphumi, "T:%-5.1f H:%-5.1f",bme280data[0],bme280data[2]);
    sprintf(press, "P:%-6.1f       ",bme280data[1]);
    LcdWriteData( (uint8_t*)&temphumi, sizeof(temphumi)); //1段目の表示
    DspLine2Top(); //2段目にカーソル移動
    LcdWriteData( (uint8_t*)&press, sizeof(press)); //2段目の表示
}

ShowData()関数をBME280から取得したデータをsprintf()で文字変換しています。1段目に温度情報と湿度情報を文字列に変換した結果を表示し2段目に気圧情報を文字変換した結果を表示します。

動作確認

BME280からSPIを使用してデータを取得してI2Cを使用してLCDに温湿度・気圧の情報を表示します。LCDの1段目に温度と湿度を表示し2段目に気圧を表示します。

動作確認用の回路図

動作確認用の回路図
動作確認用の回路図

I2C通信の配線をBME280モジュールとLCDを共通にしていますがスレーブアドレスが異なるため問題ありません。AE-BME280にプルアップ抵抗が実装されているため追加の抵抗は必要ありません。

LED1は200ms毎に点灯/消灯させます。2秒ごとにBME280からデータを取得してLCDに表示します。

動作確認

動作結果
動作結果

電源を投入すると1段目に「Seeeduino test」2段目に「Ver0.01」を表示されていることを確認しました。また温度、湿度、気圧が測定できていることが確認できました。

温度は室温計とほぼ同じでした。気圧はスマホで確認すると1007mBar[hPa]であったためそれなりに測定できていると思います。湿度についてはスマホの情報では62%とずれがありましたが屋外と屋内の差が出ているだろうという感じです。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

#include <Wire.h>
#include <Seeed_Arduino_FreeRTOS.h>
#include <Adafruit_BME280.h>

#define PIN_DO1 2

#define SLAVE_ADRS 0x3E
#define LINE1_ADRS 0x40
#define LINE2_TOP (0x40 +0x80)
#define FUNC1_SET 0x38
#define FUNC2_SET 0x39
#define INT_OSC 0x14
#define CONST_SET 0x70
#define PWR_ICON_SET 0x56
#define FOLLOWER_SET 0x6B
#define CLR_DISP 0x01
#define DISP_ONOFF_SET 0x0C

/* 変数の宣言 */
TaskHandle_t Handle_Bme280; //タスクを管理する変数
TaskHandle_t Handle_LED; //タスクを管理する変数
TickType_t tickTimled; //RTOS上の遅延時間(タスクの一時停止)用のタイマ
TickType_t tickTimbme280; //RTOS上の遅延時間(タスクの一時停止)用のタイマ
Adafruit_BME280 bme280;
float bme280data[3];
uint8_t initmoji[2][16] ={"Seeeduino  Test","        Ver0.01"};

/* プロトタイプ宣言 */
static void threadBme280(void* pvParameters);
static void threadLed(void* pvParameters);
void LcdInit(void);
void LcdComand(uint8_t cmd);
void LcdWriteData(uint8_t *data, uint8_t sz);
void DspLine2Top(void);
void DspClear(void);
void ShowData(void);

void setup(){

    pinMode(PIN_DO1,OUTPUT);

    Serial.begin(115200);
    bme280.begin(0x76); //BME280を0x76でスタートする
    LcdInit();
    xTaskCreate(threadBme280, "Task_BME280", 256, NULL, 2, &Handle_Bme280 );
    xTaskCreate(threadLed, "Task_LED", 256, NULL, 1, &Handle_LED );
    vTaskStartScheduler(); //スケジュラースタート
}

void loop(){
    //タスクで管理するため処理なし
}
/* BME280テスクの処理 */
static void threadBme280(void* pvParameters){

    while (1){
        Serial.print("Temperature = ");
        bme280data[0] = bme280.readTemperature();
        Serial.print(bme280data[0]);
        Serial.println(" °C");

        Serial.print("Pressure = ");
        bme280data[1] = bme280.readPressure() / 100.0F;
        Serial.print(bme280data[1]);
        Serial.println(" hPa");

        Serial.print("Humidity = ");
        bme280data[2] = bme280.readHumidity();
        Serial.print(bme280data[2]);
        Serial.println(" %");

        Serial.println();
        ShowData();
        myDelayMsUntil(&tickTimbme280,2000); //2s間タスクを一時停止
    }
}
/* LEDテスクの処理 */
static void threadLed(void* pvParameters){

    while(1){
        digitalWrite(PIN_DO1,HIGH);
        myDelayMsUntil(&tickTimled,200); //200ms間タスクを一時停止
        digitalWrite(PIN_DO1,LOW);
        myDelayMsUntil(&tickTimled,200); //200ms間タスクを一時停止
    }
}
/* タスクを一時停止(ms単位)     */
void myDelayMsUntil(TickType_t* previousWakeTime, int ms) {
    vTaskDelayUntil(previousWakeTime, (ms * 1000) / portTICK_PERIOD_US);
}
/* LCD初期化処理 */
void LcdInit(void){
 
    LcdComand(FUNC1_SET); //8ビットバス・2LINE表示
    LcdComand(FUNC2_SET); //拡張コマンド
    LcdComand(INT_OSC); //内部周波数調整
    LcdComand(CONST_SET); //コントラスト1
    LcdComand(PWR_ICON_SET); //コントラスト2
    LcdComand(FOLLOWER_SET); //フォロワー制御
    LcdComand(FUNC1_SET); //拡張コマンドをオフ
    DspClear();
    LcdComand(DISP_ONOFF_SET);
    LcdWriteData( &initmoji[0][0], sizeof(initmoji[0])); //1段目の表示
    DspLine2Top(); //カーソル移動
    LcdWriteData( &initmoji[1][0], sizeof(initmoji[1])); //2段目の表示
    delay(2000);
}
/* LCDへのコマンド処理 */
void LcdComand(uint8_t cmd){

    Wire.beginTransmission(SLAVE_ADRS);
    Wire.write(0x00);
    Wire.write(cmd);
    Wire.endTransmission();
    delay(1);
}
/* LCDに文字を表示 */
void LcdWriteData(uint8_t *data, uint8_t sz){

    Wire.beginTransmission(SLAVE_ADRS);
    Wire.write(0x40);
    Wire.write(data, sz);
    Wire.endTransmission();
}
/* 2段目にカーソル移動 */
void DspLine2Top(void){
    
    LcdComand(LINE2_TOP);
    delayMicroseconds(40);
}
/* ディスプレイクリア */
void DspClear(void){
  
    LcdComand(CLR_DISP);
    delayMicroseconds(40);
}
/* ディスプレイ表示 */
void ShowData(void){
    char temphumi[16];
    char press[16];

    DspClear();
    sprintf(temphumi, "T:%-5.1f H:%-5.1f",bme280data[0],bme280data[2]);
    //Serial.println(temphumi);

    sprintf(press, "P:%-6.1f       ",bme280data[1]);
    //Serial.println(press);

    LcdWriteData( (uint8_t*)&temphumi, sizeof(temphumi)); //1段目の表示
    DspLine2Top(); //2段目にカーソル移動
    LcdWriteData( (uint8_t*)&press, sizeof(press)); //2段目の表示
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

Code Camp完全オンラインのプログラミング個人レッスン【無料体験】

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

タイトルとURLをコピーしました