フェーズ2

E2-05 状態で整理(IDLE / RUN / ERROR):周期処理と安全停止を分けて書く

シリーズ:実験シリーズ(フェーズ2)
対応ロードマップ:フェーズ2 / E2-05
この記事で扱う範囲IDLE / RUN / ERROR の3状態に分けて、待機中・動作中・異常時の処理を整理して書けるようにする。

1. 目的

前回までで、

  • delay で待っていると他の処理が止まること
  • 1msごとの時刻を作ること
  • 周期処理を複数動かせること

を確認してきました。

ただ、処理が増えてくると、今は何をしている場面なのか が分かりにくくなります。
たとえば、待っている途中なのか、動いている途中なのか、異常で止まっているのかが整理しにくくなります。

そこで今回は、処理を次の3つの状態に分けます。

  • IDLE:待機中
  • RUN:通常動作中
  • ERROR:異常で停止中

今回のゴールは、
状態ごとに“やること”を分けて書けるようになること です。

この形にしておくと、今後のロボット制御でも、

  • 通常動作は RUN
  • 異常時は ERROR
  • 復帰待ちは IDLE

このように分けておくと、あとで機能を増やすときに、何をどこに書けばよいか迷いにくくなります。

2. 前提・環境

2-1. 前提

この記事は、次の実験が終わっている前提で進めます。

  • E1-05:ボタン入力でLED(ポーリング)
  • E1-08:ボタン入力を安定させる(デバウンス)
  • E2-02:1msごとの時刻を作る
  • E2-03:タイマで周期処理
  • E2-04:複数周期タスク

2-2. 使用するもの

  • 評価ボード:RTK7EKA8M2S00001BE
  • e² studio + FSP
  • 使用部品
    • LED × 1
    • 抵抗:1kΩ × 1
    • タクトスイッチ × 1
    • ブレッドボード
    • ジャンパ線
  • 使用するGPIO
    • 外付けLED用のGPIO:P409(J25-9)
    • ボタン入力用のGPIO:P704(J25-10)
    • GND:J25-11

2-3. 今回の動作イメージ

今回は次のように動かします。

  • IDLE
    • LEDは消灯
    • ボタンが押されるまで待つ
  • RUN
    • 500msごとにLEDを点滅する
    • 一定時間たったらテスト用に ERROR へ入る
  • ERROR
    • LEDを消灯して安全停止する
    • ボタンが押されるまで復帰待ちする
    • ボタンが押されたら IDLE に戻る

ここで大事なのは、1つの while の中で全部をぐちゃぐちゃに書かず、状態ごとに分ける ことです。

3. 今回の変更点

3-1. 配線変更

  • E1-05 ボタン入力でLEDを点ける(ポーリング):押したら点灯、離したら消灯(リンク)の配線をそのまま使います

3-2. 設定変更

  • E1-05 ボタン入力でLEDを点ける(ポーリング):押したら点灯、離したら消灯(リンク)の設定に、
    E2-02 ミリ秒のタイマを作る(1msカウンタ)(リンク)の1msタイマの設定を追加したもの

3-3. コード変更

  • state という状態変数を追加
  • switchIDLE / RUN / ERROR を分ける
  • RUN では周期処理を実行
  • ERROR では安全停止と復帰待ちを行う

4. 手順

4-1. 状態を決める

まずは、使う状態を決めます。

typedef enum
{
    APP_STATE_IDLE = 0,
    APP_STATE_RUN,
    APP_STATE_ERROR
} app_state_t;

今回は3つだけです。
最初は増やしすぎず、この3つで十分です。

4-2. 状態ごとの役割を決める

状態に名前を付けただけでは足りません。
その状態で何をするのか を先に決めます。

  • IDLE
    • LEDを消灯する
    • ボタンが押されるのを待つ
  • RUN
    • 500msごとにLEDを反転する
    • 異常条件になったら ERROR へ移る
  • ERROR
    • LEDを消灯する
    • RUN中の処理を止める
    • ボタンが押されたら IDLE へ戻る

この「状態ごとの役割」を先に言葉で決めておくと、コードがかなり書きやすくなります。

4-3. switch で状態ごとに分ける

状態分けは if でも書けますが、今回は見やすさのため switch を使います。

switch (g_state)
{
    case APP_STATE_IDLE:
        /* IDLEの処理 */
        break;

    case APP_STATE_RUN:
        /* RUNの処理 */
        break;

    case APP_STATE_ERROR:
        /* ERRORの処理 */
        break;

    default:
        g_state = APP_STATE_ERROR;
        break;
}

こうしておくと、今どの状態の処理を書いているのか が見やすくなります。

4-4. RUNの中に周期処理を書く

前回までの実験で作った1msタイマを使って、RUN のときだけ周期処理を動かします。
たとえば、

  • 500msごとにLED反転
  • 5000ms経過したらテスト用に ERRORに遷移

のようにします。

これにより、周期処理そのものと、その周期処理を“いつ動かすか” を分けて考えられるようになります。

4-5. ERRORでは安全停止に集中する

ERROR に入ったら、まず 動作を止める ことを優先します。
今回はLED実験なので、

  • LEDを消灯
  • RUN で行っていたLED点滅処理を止める
  • ボタンを押すまで復帰しない

という構成にします。

5. コード

※下記は考え方を分かりやすくするためのシンプルな例です。
※LEDピン名、ボタンピン名、1msカウンタ変数名は、自分のプロジェクトに合わせて置き換えてください。

#include "hal_data.h"
#include <stdbool.h>

typedef enum
{
    APP_STATE_IDLE = 0,
    APP_STATE_RUN,
    APP_STATE_ERROR
} app_state_t;

/* 1msごとに増えるカウンタ(E2-02で作成したものを使う) */
volatile uint32_t g_ms_count = 0;

/* 状態 */
static app_state_t g_state = APP_STATE_IDLE;

/* RUN開始時刻 */
static uint32_t g_run_start_ms = 0;

/* LED点滅管理 */
static uint32_t g_led_toggle_ms = 0;
static bool g_led_on = false;

/* ここは自分の環境に合わせて実装する */
static bool button_is_pressed(void);
static void led_on(void);
static void led_off(void);
static void led_toggle(void);

/* 1msタイマコールバック */
void timer0_callback(timer_callback_args_t * p_args)
{
    FSP_PARAMETER_NOT_USED(p_args);
    g_ms_count++;
}

void hal_entry(void)
{
    while (1)
    {
        switch (g_state)
        {
            case APP_STATE_IDLE:
            {
                /* 待機中はLED消灯 */
                led_off();
                g_led_on = false;

                /* ボタンが押されたらRUNへ */
                if (button_is_pressed())
                {
                    g_run_start_ms  = g_ms_count;
                    g_led_toggle_ms = g_ms_count;
                    g_state = APP_STATE_RUN;
                }
                break;
            }

            case APP_STATE_RUN:
            {
                /* 500msごとにLED反転 */
                if ((g_ms_count - g_led_toggle_ms) >= 500U)
                {
                    g_led_toggle_ms = g_ms_count;
                    led_toggle();
                    g_led_on = !g_led_on;
                }

                /* テスト用:5秒経過したらERRORへ */
                if ((g_ms_count - g_run_start_ms) >= 5000U)
                {
                    g_state = APP_STATE_ERROR;
                }
                break;
            }

            case APP_STATE_ERROR:
            {
                /* 安全停止:LED消灯 */
                led_off();
                g_led_on = false;

                /* 復帰待ち:ボタンが押されたらIDLEへ戻る */
                if (button_is_pressed())
                {
                    g_state = APP_STATE_IDLE;
                }
                break;
            }

            default:
            {
                g_state = APP_STATE_ERROR;
                break;
            }
        }
    }
}

/* --- 下は例。自分のGPIO設定に合わせて修正する --- */

static bool button_is_pressed(void)
{
    bsp_io_level_t level = BSP_IO_LEVEL_HIGH;

    /* 例:内部プルアップ入力で、押したらLOWになる想定 */
    R_IOPORT_PinRead(&g_ioport_ctrl, BUTTON_PIN, &level);

    if (level == BSP_IO_LEVEL_LOW)
    {
        return true;
    }
    else
    {
        return false;
    }
}

static void led_on(void)
{
    /* Active-Low の場合は LOW/HIGH を逆にする */
    R_IOPORT_PinWrite(&g_ioport_ctrl, LED_PIN, BSP_IO_LEVEL_HIGH);
}

static void led_off(void)
{
    /* Active-Low の場合は LOW/HIGH を逆にする */
    R_IOPORT_PinWrite(&g_ioport_ctrl, LED_PIN, BSP_IO_LEVEL_LOW);
}

static void led_toggle(void)
{
    if (g_led_on == false)
    {
        led_on();
    }
    else
    {
        led_off();
    }
}

6. 実行結果

6-1. 確認したい動き

今回の実験では、次の流れになればOKです。

  1. 電源投入直後の状態は IDLE
    • LEDは消えている
  2. ボタンを押す
    • 状態がRUN になる
    • LEDが500msごとに点滅する
  3. 5秒たつ
    • 状態がERROR になる
    • LEDが消灯して止まる
  4. もう一度ボタンを押す
    • 状態がIDLE に戻る
    • LED消灯の待機状態に戻る

6-2. 今回確認できればよいこと

今回の段階では、次が確認できれば十分です。

  • IDLE / RUN / ERROR で処理が分けられている
  • RUN のときだけ周期処理が動く
  • ERROR ではLEDを消灯したままにできる
  • ERROR に入ったあと、ボタンが押されるまで待ち、押されたら IDLE に戻せる

7. ハマりポイント/原因と対策

7-1. ボタンを押してもRUNに入らない

原因

  • ボタンを押したことを、プログラムが正しく読めていない
  • 「押したときON」と思っていたが、実際は「押したときOFF」の設定になっている
  • ボタン入力が安定していない

対策

  • まず、ボタンを押したときに button_is_pressed()true になるか確認する
  • 押したときに false のままなら、押した/離したの判定を逆にする
  • E1-05、E1-08 と同じ配線・同じ入力設定にそろえる
  • ボタンがふらつく場合は、デバウンスした入力処理を使う

7-2. RUNに入るがLEDが点滅しない

原因

  • 1msカウンタが動いていない
  • 500msごとの判定がうまくできていない
  • LEDのON/OFFの設定が合っていない

対策

  • g_ms_count の値が増え続けているか確認する
  • 増えていなければ、1msタイマやコールバック処理を見直す
  • g_led_toggle_ms = g_ms_count; を入れて、次の500msを正しく測れるようにする
  • LEDが逆の動きをする場合は、led_on()led_off() の中身を見直す

7-3. ERRORに入ってもLEDが消えない

原因

  • ERROR に入っても、RUNのときの点滅処理を続けてしまっている
  • ERROR の中で LED を消灯していない

対策

  • ERROR の処理に入ったら、最初に led_off(); を書く
  • RUN のときだけ点滅処理を行う形になっているか見直す
  • ERROR では LED を消灯したままにする、と先に決めてからコードを書く

7-4. ERRORから復帰できない

原因

  • ボタンが押されたら IDLE に戻る処理が入っていない
  • 押したときの判定が逆になっている
  • ボタン入力が安定していない

対策

  • if (button_is_pressed()) { g_state = APP_STATE_IDLE; } があるか確認する
  • ボタンを押しても戻らない場合は、押したときの判定を見直す
  • 配線を見直し、必要ならデバウンス済みの入力処理にする

8. 今回わかったこと

今回の実験で大事なのは、難しい状態を作ることではありません。
大事なのは次の3つです。

  • 今の状態に名前を付ける
  • 状態ごとにやることを分ける
  • 異常時の処理と通常処理を分ける

これができると、
「動く処理」
「止める処理」
「復帰を待つ処理」
を整理して書けるようになります。

フェーズ2では、これで十分です。
まずは、周期処理を状態で分けて書けること が大事です。

9. 次回やること

E3-00 I2C配線の“詰まりどころ”を先に潰す(電圧/プルアップ/GND)
タイマと状態で処理を整理できるようになったので、次はセンサ実験に入る前に、I2C配線でつまずきやすいポイントを先に確認します。

10. 関連リンク

  • 実験シリーズ:E2-01 delayの限界を体験(リンク)
  • 実験シリーズ:E2-02 タイマで1ms毎にカウントアップするストップウォッチを作る(リンク)
  • 実験シリーズ:E2-03 タイマで周期処理(正確な点滅)(リンク)
  • 実験シリーズ:E2-04 複数周期タスク(リンク)
  • 基礎シリーズ:組み込み開発とは(リンク)
  • 基礎シリーズ:C言語のはじめ(main/ループ/関数/変数)(リンク)
  • 基礎シリーズ:はじめての状態遷移(IDLE/RUN/ERROR)(リンク)

-フェーズ2