リアルタイム制御

ポーリング

状態を繰り返し確認する方式。割り込みの対義。

概要

ポーリング(Polling)は、CPUがあるペリフェラルや変数の状態を定期的または連続的に確認(問い合わせ)することでイベントや変化を検知する方式です。割り込み(インタラプト)と対をなす概念で、「ビジーウェイト(Busy-Wait)」とも呼ばれます。

割り込みでは外部イベントがCPUに通知を送る「プッシュ型」であるのに対し、ポーリングはCPUが自ら状態を確認しに行く「プル型」です。実装は単純で直感的ですが、イベントが発生していない間もCPUリソースを消費するという欠点があります。

組み込みシステムでは、用途に応じてポーリングと割り込みを使い分けます。ポーリングが適しているのは、高速・頻繁にイベントが発生する場合や、実装のシンプルさが重要な場合です。

歴史・背景

ポーリングはコンピュータ黎明期から存在する最も基本的なI/O制御方式です。初期のメインフレームでは、CPUがI/Oデバイスの完了ビットを確認するビジーウェイトが標準でした。割り込み機構が登場する以前は、ポーリングが唯一の選択肢でした。

8ビットマイコン時代(1970〜1980年代)の多くのシステムはポーリングループ(メインループ内でフラグを逐次確認)で制御されていました。構造が単純で割り込みの複雑さがなく、リソースが少ないマイコンに適した手法でした。

2000年代以降、省電力が重要なIoT機器では、アクティブなポーリングは電池消耗の観点から敬遠されるようになり、イベント駆動(割り込み + スリープ)設計が主流となっています。一方で高速なSPIデバイス(Ethernetコントローラ、Wi-Fiチップなど)の大量データ転送ではポーリングがスループット上有利なケースもあり、「割り込みとポーリングのハイブリッド」として使われることも多いです。

技術仕様

ポーリングの種類

種別説明特徴
ビジーウェイトフラグが変わるまでループで待つCPU占有率100%、最小レイテンシ
タイムアウト付きポーリング一定時間経過で諦めるハング防止
周期ポーリング(RTOSタスク)一定周期でスリープ後確認CPU効率向上
選択的ポーリング(select/epoll)複数ソースをOS支援で監視Linux/POSIXで使用

ポーリング間隔と応答時間の関係

ポーリングによる応答時間はポーリング間隔に依存します。

最悪応答時間 = ポーリング間隔 × 1回分
平均応答時間 = ポーリング間隔 / 2

例:1ms周期ポーリングの場合
最悪応答時間 = 1ms(イベント発生直後にポーリングが終わり、次の確認まで待つ)
平均応答時間 = 0.5ms

CPU使用率の比較

方式イベント発生頻度CPU使用率(イベント処理以外)
ビジーウェイト低〜高100%(常時ループ)
1ms周期ポーリング(RTOSスリープ)ポーリング確認コスト(μs/ms)
割り込みほぼ0
割り込み非常に高(Mbps以上)割り込みオーバーヘッドが支配的

高速データ転送(例:Gigabit Ethernet)では、割り込みの発生頻度が高すぎてオーバーヘッドが問題になることがあります。LinuxカーネルのNAPIはこの問題を解決するため、受信割り込みを一時無効化してポーリングに切り替える仕組みです。

動作原理

基本的なポーリングパターン

1. ビジーウェイト(HALのPolling関数)

/* STM32 HALのポーリング方式UART受信 */
uint8_t buf[10];
/* タイムアウトまで待機(CPUはここでブロック) */
HAL_StatusTypeDef ret = HAL_UART_Receive(&huart1, buf, 10, 1000 /* 1秒タイムアウト */);
if (ret == HAL_OK) {
    /* 受信完了 */
} else if (ret == HAL_TIMEOUT) {
    /* タイムアウト */
}

2. フラグポーリング(ノンブロッキング確認)

/* レジスタのステータスビットをポーリング */
while (!(USART1->SR & USART_SR_RXNE)) {
    /* 受信データレジスタが空の間は待つ */
    /* 注意:タイムアウト処理なしのビジーウェイト */
}
uint8_t data = USART1->DR;

3. RTOSタスクでの周期ポーリング

/* 10ms周期でGPIO状態を確認するタスク */
void gpioPollingTask(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    GPIO_PinState lastState = GPIO_PIN_RESET;

    for (;;) {
        GPIO_PinState currentState = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
        if (currentState != lastState) {
            /* 状態変化を検知 */
            onButtonStateChange(currentState);
            lastState = currentState;
        }
        /* 10ms待機(この間は他のタスクが実行される) */
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(10));
    }
}

4. スーパーループ(Bare-metal)

/* シンプルなベアメタル・スーパーループ */
int main(void) {
    SystemInit();
    PeripheralInit();

    while (1) {
        /* 各ペリフェラルの状態を順番に確認 */
        if (uart_rx_available()) {
            handle_uart_rx();
        }
        if (button_pressed()) {
            handle_button();
        }
        if (timer_elapsed()) {
            handle_control_loop();
        }
        /* ... */
    }
}

デバウンスとポーリング

機械的スイッチのチャタリング(高速なON/OFFの繰り返し)除去にはポーリングが適しています。

/* ソフトウェアデバウンス(10ms周期ポーリング) */
#define DEBOUNCE_COUNT 5 /* 5回連続同じ値で確定 */

uint8_t debounce_counter = 0;
GPIO_PinState stable_state = GPIO_PIN_RESET;

void debounce_poll(void) {
    GPIO_PinState raw = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);
    if (raw == stable_state) {
        debounce_counter = 0;
    } else {
        if (++debounce_counter >= DEBOUNCE_COUNT) {
            stable_state = raw;
            debounce_counter = 0;
            on_button_change(stable_state);
        }
    }
}

用途・ユースケース

ポーリングが適している場面

  1. 高速・連続的なデータ処理 SPI接続のTFT液晶への大量ピクセル書き込みなど、データ転送が連続的に発生する場合はポーリングの方がスループットが高いことがあります。割り込みオーバーヘッドがデータ転送時間を支配するためです。

  2. シンプルなベアメタル実装 教育用・プロトタイプ用途では、RTOSを使わずスーパーループ+ポーリングで迅速に実装できます。

  3. 超低レイテンシが必要な場面 割り込みのコンテキストスイッチオーバーヘッド(数μs)が許容できない場合、ビジーウェイトで1クロック周期単位の応答が可能です。

  4. センサの単発読み取り 起動時の1回限りの初期化・設定確認など、応答時間が問題にならない場合。

ポーリングが不適切な場面

  • 長時間待機が必要(CPUを無駄に消費)
  • 複数の非同期イベントを同時に処理(スーパーループのリアルタイム性が問題になる)
  • 消費電力が重要な電池駆動機器(ポーリング中はディープスリープ不可)

組み込みLinuxでのポーリング

Linuxではポーリングをpoll()/select()/epoll()システムコールで行います。複数のファイルディスクリプタ(デバイスファイル含む)を同時に監視できます。

#include <poll.h>
#include <fcntl.h>

int fd = open("/dev/gpio0", O_RDONLY);
struct pollfd fds[1];
fds[0].fd = fd;
fds[0].events = POLLIN;

/* 最大1秒待機してGPIO変化を検知 */
int ret = poll(fds, 1, 1000);
if (ret > 0 && (fds[0].revents & POLLIN)) {
    /* イベント発生 */
    char buf[4];
    read(fd, buf, sizeof(buf));
}

実装・開発のポイント

タイムアウトの必須化

ポーリングループには必ずタイムアウトを設けます。無限ビジーウェイトはウォッチドッグリセットを引き起こすか、システムをハング状態にします。

/* タイムアウト付きポーリングの定石 */
uint32_t timeout_ms = 100;
uint32_t start = HAL_GetTick();

while (!(USART1->SR & USART_SR_RXNE)) {
    if ((HAL_GetTick() - start) > timeout_ms) {
        return ERROR_TIMEOUT; /* タイムアウトで脱出 */
    }
}

ポーリングと割り込みのハイブリッド

初期同期(ポーリングでロック完了を確認)+データ転送(DMA + 完了割り込み)という組み合わせが実用的です。SPIセンサの読み取りなどで多用されます。

スーパーループの限界と対策

スーパーループのリアルタイム性は、ループ1周の最大実行時間で決まります。ループ内の各処理をブロッキングなしで実装し、時間のかかる処理は状態機械(ステートマシン)で分割します。

/* 状態機械によるノンブロッキング処理 */
typedef enum { STATE_IDLE, STATE_SENDING, STATE_WAITING } State;
State state = STATE_IDLE;
uint32_t state_timer = 0;

void updateStateMachine(void) {
    switch (state) {
        case STATE_IDLE:
            if (send_request) {
                startSend();
                state = STATE_SENDING;
            }
            break;
        case STATE_SENDING:
            if (isSendComplete()) {
                state_timer = HAL_GetTick();
                state = STATE_WAITING;
            }
            break;
        case STATE_WAITING:
            if (HAL_GetTick() - state_timer > 100) {
                state = STATE_IDLE;
            }
            break;
    }
}

他技術との比較

観点ポーリング(ビジーウェイト)周期ポーリング(RTOSタスク)割り込み
最悪応答時間最短(クロック単位)ポーリング間隔割り込みレイテンシ(μs)
CPU使用率100%(待機中も)低(スリープ中は0)低(イベント時のみ)
実装の簡易さ最も簡単中(RTOS必要)中〜複雑
消費電力最大最小
タイミングの確実性確実ポーリング間隔に依存ハードウェア依存
向いている用途高速転送・シンプルな処理低頻度監視・デバウンス非同期イベント・省電力

ポーリングはその単純さゆえに組み込み開発の基礎を学ぶ上で重要です。実際のシステム設計では、ポーリングと割り込みDMAを適切に組み合わせることで、CPU効率・応答性・消費電力を最適化します。

関連用語

参考リンク