Arduinoプロトスレッディング入門:シングルコアで疑似マルチスレッドを実現するステップバイステップガイド
必要な部品と材料
| Arduino UNO | ×1 |
| Sunfounder Blue 16x2 LCDディスプレイ | ×1 |
| ブレッドボード(汎用)とジャンパーワイヤー | ×1 |
| 回転ポテンショメータ(汎用、1kΩ推奨) | ×1 |
このプロジェクトについて
シングルコアのArduinoで「同時に3つのことをやる」方法を知りたいと思ったことはありませんか?このチュートリアルでは、以下の3つのタスクを中断なしで並行して実行させます。
- LCDのバックライトを一定周期でパルス点滅(フェードイン/アウト)
- カウンタを1秒ごとにインクリメントしてLCDに表示
- 複数のメッセージを数秒ごとに切り替えてLCDに表示
プロトスレッディングとは?
プロトスレッディング(Protothreading)は、本来マルチタスク(複数の処理を同時に、または異なる間隔で実行)が必要な場面で、Arduinoのようなシングルコア・手続き型の環境でも「あたかもマルチスレッドのように」振る舞わせる手法です。ただし、Arduino(ATmega328P)は16MHzのシングルコアであり、真のマルチスレッドはハードウェア的に不可能です。では、プロトスレッディングはどう違うのでしょうか。
「本物の」マルチスレッド vs プロトスレッディング
理解のために、CPUの「ハイパースレッディング」を思い出してください。IntelがPentium時代に導入した技術で、1つの物理コアを論理的に2コアのように見せかけます。鍵はクロックサイクルです。Arduino UnoのATmega328Pは16MHz、つまり毎秒1,600万サイクルで命令を実行します。しかしこれは「コードの行数」ではなく、レジスタ間のデータ移動など極めて低レベルな命令単位です。
マルチコアCPUでは、複数のスレッドが別々のコアで並列実行されます。メインスレッドがワーカースレッドを管理する典型的な例はChromeブラウザ:各タブが独立したスレッドで動き、1つクラッシュしても全体は生き残ります。Arduinoにはコアが1つしかないため、このアプローチは使えません。そこで1つのコア上で複数の作業を時間分割して「同時に見せる」のがプロトスレッディングです。
プロトスレッディングの仕組み
プロトスレッディングはハイパースレッディングに似ていますが、CPUサイクルではなくコードの「ループ」や「行」単位で作業を分割します。各ループで「他にやるべき仕事があるか」をチェックし、タイミングが来ていれば分岐して実行します。重要なのはブロッキング(長時間割り込めない)処理、例えばSDカード読み書きなどは、他のプロトスレッドの実行タイミングをズラしてしまう点です。しかし、変数更新や出力変更程度の軽快な処理であれば非常にうまく機能します。より高度なマルチタスクが必要なら、RTOS(リアルタイムOS)対応のMCUを検討してください。
実装のポイント:タスクの優先順位
まず実行したいタスクを洗い出し、「ブロッキング度合い」で並べます。
- 最もブロッキングでない(最速・最頻繁)タスク → メインループ(プライマリスレッド)として採用
- それ以外 → メインループ内で
.check()を呼び出して実行タイミングを判定
今回の例では:
- バックライトのパルス点滅(最高頻度・非ブロッキング) → メインループ
- カウンタインクリメント(700ms間隔) →
numberThread - メッセージローテーション(3000ms間隔) →
textThread
メインループ(およびその中のforループ内)でnumberThread.check()とtextThread.check()を呼ぶことで、各タスクが「自分の番なら実行」されます。TimedActionライブラリを使うとこの管理が容易になります(※新しいArduino IDEではWProgram.hをArduino.hに書き換える必要あり)。
コード全文(multithread.ino v1.1)
/* Arduino Protothreading Example v1.1 by Drew Alden (@ReanimationXP)
Updated 8/18/17 for Arduino 1.6.6+ */
#include <TimedAction.h> // 別途インストール要。TimedAction.h内のWProgram.h→Arduino.h修正必要
#include <LiquidCrystal.h>
// ピン定義・グローバル変数
int backlightPin = 9;
int timerCounter = 0;
int stringNo = 0;
char* stringArray[] = {"Check it out... ", "I have 3 threads", "going at once...", "Cool, huh?! :D "};
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
// タスク関数
void incrementNumber() {
lcd.setCursor(0, 1);
timerCounter++;
lcd.print(timerCounter);
}
void changeText() {
lcd.setCursor(0, 0);
lcd.print(stringArray[stringNo]);
if (stringNo >= sizeof(stringArray)/sizeof(char*)) {
stringNo = 0;
changeText();
} else {
stringNo++;
}
}
// TimedActionインスタンス(関数定義の後で初期化)
TimedAction numberThread = TimedAction(700, incrementNumber);
TimedAction textThread = TimedAction(3000, changeText);
void setup() {
lcd.begin(16, 2);
changeText(); // 初期表示
}
void loop() {
// 他スレッドのチェック
numberThread.check();
textThread.check();
// タスク1: バックライト フェードイン
digitalWrite(13, HIGH);
for (int fadeValue = 0; fadeValue <= 255; fadeValue += 10) {
numberThread.check(); textThread.check(); // ループ内でもチェック必須
analogWrite(backlightPin, fadeValue);
delay(20); // 短い遅延に留める
}
// タスク1: バックライト フェードアウト
digitalWrite(13, LOW);
for (int fadeValue = 255; fadeValue >= 0; fadeValue -= 10) {
numberThread.check(); textThread.check();
analogWrite(backlightPin, fadeValue);
delay(20);
}
}
まとめと次のステップ
この手法の核心は「最も頻繁に実行されるタスクをメインループにし、他をタイマー管理する」というシンプルな発想です。delay()を多用すると他タスクのタイミングが狂うため、待ち時間が必要な場合はmillis()ベースの非ブロッキング設計や、より洗練されたライブラリ(TaskScheduler、FreeRTOSなど)の検討をお勧めします。この例がArduinoで「同時に複数のことをやる」ヒントになれば幸いです。
製造プロセス
- OpenSensorsで空気質を測定: ArduinoとPythonによるガイド
- Arduinoで作るセキュアなRFIDロック – ステップバイステップガイド
- ArduinoでユニバーサルIRリモコンを自作:ステップバイステップガイド
- デジタル身長計の自作:Arduino Nanoとレーザー距離センサーで成長記録
- サーミスタを使った温度測定: Arduinoステップバイステップガイド
- Arduinoで楽器演奏:音階生成のステップバイステップガイド
- ArduinoをNMEA-0183に接続するステップバイステップガイド
- Arduino Nanoと指紋センサーで作るバイオメトリクス認証システム – 完全チュートリアル
- ArduinoでModbusをマスターする:ステップバイステップガイド
- Arduino UNOで作るミニピアノ - ステップバイステップチュートリアル
- Arduino初心者チュートリアル01: 最初のプロジェクトを始めよう