この記事は、C言語基礎ドリルの9本目・最終回です。1〜8では「変数」「if」「ループ」「配列」「関数」「ポインタ」「構造体」「ビット演算」を1テーマずつ、それぞれ7問の練習問題で固めてきました。最後の総仕上げとして、この記事では1〜8を1本のプログラムの中で同時に使う「総合実力テスト」に挑戦します。

今回は問題を1問ずつ解く形式ではありません。発注者(筆者)が用意した要件定義書と設計書を渡すので、読者が実装担当としてプログラムを書き上げる、という実務に近い形式で進めます。題材は組込みのサンプルとして定番の「エアコン(クーラー)のリモコン」を模擬するプログラムです。マイコンもリモコンの実物も使いません。ボタン番号をキーボードで入力し、画面に状態を表示するだけのシンプルなCプログラムですが、中身は状態遷移・データ設計・ビット操作まで、組込みプログラムの骨格をひと通り含んでいます。

<自己紹介>
筆者は現役の組込みエンジニアです。未経験向けのC言語練習問題を通じて最低限の言語知識を身につけ、最終的にはArduinoでマイコンプログラムを自作できるようになるのをゴールとして、このシリーズを作成しています。この9本目まで来た方は、もう文法の知識に不足なし。あとは組み合わせて使う練習をするだけです。

このテーマの要点

総合実力テストでやることはこれだけ

  • 要件定義書・設計書はすでに完成している。読者がやるのは、それを読んで実装することだけ
  • 状態遷移(if/switch)・ボタン入力の繰り返し(ループ)・履歴の保存(配列)・処理のまとまり(関数)・状態を書き換える受け渡し(ポインタ)・関連データの一本化(構造体)・状態の可視化(ビット演算)を1つのプログラムの中で同時に使う
  • 解けなくても大丈夫。設計書を読むだけでも、1〜8がどう組み合わさって実務のプログラムになるかが見えてくる

1〜8のドリルは、いわば工具箱に工具を1本ずつ入れていく作業でした。この記事は、その工具箱を持って実際に1つの製品(エアコンリモコンの模擬プログラム)を組み立てる回です。実務でも、要件定義と設計はすでに決まっていて、そこから実装するという流れは非常によくあります。その最小疑似体験だと思って取り組んでください。

この記事の進め方

次の順番で進めてください。

  1. 要件定義書を読む(何を作るかが分かる)
  2. 設計書を読む(どう作るかが分かる。データの持ち方・状態遷移・関数の一覧・各関数のフローチャートまで指定済み。フローチャートを見ずに自力で組みたい人は、折りたたみを開かなければOK)
  3. 実装課題のとおりに、自分の手で main.c を書いてみる(30〜90分は見ておくとよいでしょう)
  4. 書けたら(あるいは詰まったら)模範解答を開いて、1〜8のどこが自分のコードと違うかを見比べる
動かす環境
これまでのドリルと同じく、マイコンは不要です。VS Code+gccなど、Cのコンパイラが動く環境であれば十分です(環境がなければ親ハブの「練習環境」節を参照してください)。gcc -Wall -Wextra -std=c11 main.c -o main でコンパイルし、./main(Windowsなら main.exe)で実行します。

要件定義書:発注者からの依頼内容

ここから先は「発注者から渡された依頼内容」だと思って読んでください。実装方法(Cのどの機能を使うか)にはまだ触れません。何を作るかだけを決めた文書です。

製作物

エアコン(クーラー)のリモコンを模擬するプログラム。マイコンやリモコンの実物は使わず、キーボードからのボタン番号入力と、画面表示だけで動作を再現する。

① 起動・停止

  • プログラム起動直後は「停止状態」。0番ボタンを押すと起動し、「起動状態」になる
  • 起動状態・冷房・暖房・風量モードのどの画面でも、0番ボタンを押すと「停止状態」に戻る
  • 停止状態からもう一度0番ボタンを押すと再び起動できる。9番ボタンを押すといつでもプログラムを終了する
  • 停止状態に戻っても、設定していた温度・風量の値は保持する(消えるのは「今どのモードか」の表示だけ)

② 温度調整

  • 起動時の温度は25℃
  • 温度調整ができるのは冷房モードか暖房モードを選んだ後だけ。モードを選ぶ前は温度調整ボタンを機能させない
  • 冷房モード中に温度調整ボタンを押すと-1℃、暖房モード中に押すと+1℃
  • 温度の範囲は0℃〜40℃。それ以上・それ以下には変化させない
  • 現在の設定温度は、どの画面でも常に表示する

③ 風量モード

  • 風量モードの画面では、操作ボタンを押すたびに風量が切り替わる:0回目=自動、1回押す=強風、2回押す=弱風、さらに押すと自動に戻る(3種類を循環)
  • 現在の風量モードは、どの画面でも常に表示する

④ モードごとの操作ボタンの意味

起動状態からは、冷房モード・暖房モード・風量モードのいずれかを選べる。どのモードに入っているかによって、同じ操作ボタンの意味が変わる(冷房中は温度を下げるボタン、風量モード中は風量を切り替えるボタン、というように)。

⑤ 変更履歴

  • 温度を変更するたびに、「そのとき何モードだったか」と「変更後の温度」をセットで履歴に残す
  • 履歴は直近3件だけ保持できればよい。4件目を記録するときは、一番古い履歴を1件捨てる

⑥ 状態の可視化

  • デバッグ・保守のために、今の状態(運転中か・どのモードか・風量・温度の上下限に達しているか)を1バイトにまとめたステータスレジスタとして、どの画面でも表示する

⑦ 入力の頑健性

  • 数字以外が入力された場合や、その画面で意味を持たないボタン番号が入力された場合は、プログラムを異常終了させず、再入力を促す
要件定義書に「実装方法」は書かれていない
気づいた方もいるかもしれませんが、この要件定義書のどこにも「配列を使え」「構造体にしろ」とは書いていません。実務の要件定義書も同じで、「何を実現したいか」だけを書き、「どう実装するか」は設計者・実装者に委ねられます。次の「設計書」で、この要件をどう実現するかが決まります。

設計書:発注者が用意した設計

ここからは「どう作るか」です。設計はすでに完成しています。読者は、この設計のとおりに実装するだけで、要件定義書の内容をすべて満たせます。

⓪ ファイル構成

今回はファイルを分けません
実務のCプログラムは、ヘッダファイル(.h)に構造体や関数の宣言をまとめ、複数の .c ファイルに分けて書くのが普通です。ただしヘッダファイルへの分割は、この基礎ドリルではまだ扱っていない技術。今回はその技術を新しく持ち込まず、main.c 1ファイルだけで完結させます。#define・構造体の定義・関数プロトタイプ・main()・各関数の中身、すべてを1つのmain.cに上から順に書いていきます。

① 状態遷移

この製作物は、6つの状態を行き来する状態遷移で成り立っています。マイコンの組込みプログラムは、ほぼ必ずこの「状態遷移」という考え方で設計します。

状態 意味 この画面で押せるボタン
WAIT_STATE 停止状態 0:起動状態へ/9:終了
START_UP_STATE 起動状態 0:停止状態へ/1:冷房モードへ/2:暖房モードへ/4:風量モードへ/9:終了
COOLING_STATE 冷房モード 0:停止状態へ/1:冷房のまま/2:暖房モードへ/3:-1℃/4:風量モードへ/9:終了
HOT_STATE 暖房モード 0:停止状態へ/1:冷房モードへ/2:暖房のまま/3:+1℃/4:風量モードへ/9:終了
AIR_FLOW_STATE 風量モード 0:停止状態へ/1:冷房モードへ/2:暖房モードへ/3:風量切替/4:風量モードのまま/9:終了
END_STATE 終了 (プログラムを終了する)
6つの状態とボタンによる状態遷移図。冷房・暖房・風量の各モードからボタン0で停止状態へ戻り、ボタン1・2・4でほかの運転モードへ直接切り替えられる
図:上の表を図にしたもの。温度・風量・履歴は状態をまたいで保持される値なので、あえて「状態」には含めていません

状態を表す値は、次のように定義します。マイコンのレジスタ値を意識して16進数で振っています(#1 変数・型・演算子)。

状態の定義

#define WAIT_STATE      0x01
#define START_UP_STATE  0x02
#define COOLING_STATE   0x03
#define HOT_STATE       0x04
#define AIR_FLOW_STATE  0x05
#define END_STATE       0x06

② データ設計:関連する値を1つの構造体にまとめる

温度・風量・状態・履歴をバラバラのグローバル変数で持つと、増えるたびに管理が大変になり、どの関数がどの変数を書き換えたのか追えなくなります。そこで関連するデータを1つの構造体にまとめます#7 構造体)。

main.cの先頭(定数と構造体の定義)

#define HISTORY_LEN 3

typedef struct {
    int state;                        // 今の状態(WAIT_STATE 等)
    int temperature;                  // 現在の設定温度(0〜40)
    int wind_select;                  // 風量(0=自動 1=強風 2=弱風)
    int history[HISTORY_LEN][2];      // 温度変更履歴 [何番目][0]=モード [1]=変更後の温度
    int history_count;                // 履歴の件数(0〜HISTORY_LEN)
} AirconState;

history2次元配列になっている点に注目してください(#4 配列)。「何回目の変更か」で1次元、「モード番号か温度か」でもう1次元、という2つの軸を持つデータなので、2次元配列がぴったりはまります。

設計方針:グローバル変数を使わない
この設計では、AirconStatemain() の中のローカル変数として1つだけ作ります。グローバル変数にしないのは、どの関数がいつ状態を書き換えたのかを追いやすくするためです(#5 関数のスコープ)。では、ローカル変数を状態を切り替える各関数からどうやって書き換えるのか、そこで次の「ポインタ」が必要になります。

③ 関数設計:状態を書き換える関数はポインタで受け取る

Cの関数は、引数を値としてコピーして受け取ります。AirconState をそのまま渡すと、関数の中でどれだけ書き換えても呼び出し元の変数は変わりません。呼び出し元の AirconState を書き換えたいので、各状態の処理関数は構造体へのポインタを引数に取ります(#6 ポインタ)。

関数 引数 戻り値 役割
handle_wait AirconState * int(次の状態) 停止状態の入力処理
handle_start_up AirconState * int(次の状態) 起動状態の入力処理
handle_cooling AirconState * int(次の状態) 冷房モードの入力処理・温度を-1
handle_hot AirconState * int(次の状態) 暖房モードの入力処理・温度を+1
handle_air_flow AirconState * int(次の状態) 風量モードの入力処理・風量切替の呼び出し
change_wind AirconState * なし 風量を自動→強風→弱風→自動と1つ進める
push_history AirconState *, モード, 温度 なし 履歴に1件追加する(満杯なら古い方を捨てる)
read_button プロンプト文字列 int(入力値) 数字が入力されるまで読み直す

handle_* 関数は「今の状態を処理した結果、次にどの状態へ行くか」を戻り値で返す設計にします(#5 関数の戻り値)。main() 側は、その戻り値を ac.state に代入するだけです。

main側のイメージ

switch (ac.state) {
    case WAIT_STATE:     ac.state = handle_wait(&ac);     break;
    case START_UP_STATE: ac.state = handle_start_up(&ac); break;
    /* ...(略)... */
}

設計書のフローチャートは、下の記号で統一して描きます。読み方は次の4つだけ覚えれば十分です。

  • 上から下へ、矢印の順番にたどる
  • 緑の丸が開始、赤い丸が終了。1つの関数の中で return が複数あるときは、終了の丸も複数出てきます(どの分岐で終わったかが一目で分かるようにするためです)
  • 黄色いひし形は分岐。YES・NOのうち、実際に成り立つ方の矢印へ進む
  • 図の左側を通って上のほうへ戻る矢印は「条件を満たすまで繰り返す」というループ。ループの入り口に小さいひし形が置かれていることがありますが、これは分岐ではなく合流点(最初に入ってくる矢印と、ループから戻ってくる矢印が合流する目印)です
フローチャート記号の凡例。開始・処理・入出力・判断・関数呼び出し・終了の意味
図:この記事で使うフローチャート記号
main() のフローチャートを見る
main関数のフローチャート。開始から初期化・画面表示・状態判定・handle関数呼び出しを繰り返し、END_STATEになったら終了する
図:main()の開始から終了まで。表の関数一覧を、実行される順番で並べ直したもの
ここで初めて出てくる技術:キーボード入力(scanf)

1〜8のドリルは、計算した結果を printf で表示するだけで、キーボードからの入力は扱いませんでした。この総合課題は「ボタンを押す」動きを再現する都合上、scanf でキーボードの数字を読み取る処理が新しく必要になります。

scanf("%d", &value); は、入力された文字を数値として value に読み込みます。このとき戻り値(読み取れた個数)が1かどうかを見ると、数字以外が入力されたかどうかを判定できます。数字以外が入力された場合、その文字は入力の列に残ったままになるため、getchar() で改行が出てくるまで1文字ずつ読み捨ててから、もう一度読み直す流れです。この一連の処理を担当するのが、設計書の read_button 関数。要件定義書の「⑦ 入力の頑健性」を満たすための処理だと考えてください。

④ 名前の表示:配列とポインタ(文字列)でまとめる

状態名や風量名を、状態ごとの ifswitch で1つずつ strcpy していくと、モードが増えるたびにコードが伸びていきます。そこで「状態番号→名前」の対応表を配列として持たせ、添字で引く設計にします。

名前の対応表

static const char *STATE_LABELS[END_STATE + 1] = {
    "-", "停止状態", "起動状態", "冷房モード", "暖房モード", "風量モード", "終了"
};
static const char *WIND_LABELS[3] = { "自動", "強風", "弱風" };

STATE_LABELS[ac.state] と書くだけで、状態に応じた名前を取り出せます。const char * の配列、という形そのものが「配列(複数を並べる)」と「ポインタ(文字列の実体は先頭アドレスで指す)」の組み合わせです(#4#6)。

⑤ ステータスレジスタ:状態をビットで可視化する

実機のマイコンは、電源やモードの状態を1つのレジスタ(数バイトの入れ物)にビット単位で詰めて持つのが普通です。この模擬プログラムでも、デバッグ用に同じ考え方の「ステータスレジスタ」を1バイトで作ります(#8 ビット演算)。

ビット位置 意味
bit0 電源(運転中か) 0=停止/1=運転中
bit1〜bit2(2bit) モード番号 00=なし/01=冷房/10=暖房/11=風量
bit3 温度が上限or下限に到達 0=通常/1=警告
bit4〜bit5(2bit) 風量番号 00=自動/01=強風/10=弱風
bit6〜bit7 (未使用) 0固定
ビットの割り当て

#define STATUS_POWER_BIT   (1 << 0)
#define STATUS_MODE_SHIFT  1
#define STATUS_MODE_MASK   (0x03 << STATUS_MODE_SHIFT)
#define STATUS_WARN_BIT    (1 << 3)
#define STATUS_WIND_SHIFT  4
#define STATUS_WIND_MASK   (0x03 << STATUS_WIND_SHIFT)

ビットを詰めるときはシフト(<<)で位置をずらし、OR(|)で合成し、マスク(&)で余分なビットを落とす、という#8で練習した手順そのままです。

設計対応表:1〜8がどこで使われるか

# 要素 この設計での使いどころ
1 変数・型・演算子 温度の範囲チェック(>=<=)、状態を表す#define定数、風量を3で割った余り
2 if / switch / 三項演算子 状態ごとの分岐処理(switch)、温度上下限ラベルの出し分け(if/else if)
3 ループ・do-while プログラム全体の while ループ、正しい値が入るまで聞き直す do-while
4 配列・2次元配列 温度変更履歴(2次元配列)、状態名・風量名の対応表(配列)
5 関数・引数・戻り値・スコープ 状態ごとの処理を関数に分割、次の状態を戻り値で返す、状態をグローバルにしない設計
6 ポインタ 各関数へAirconState *で状態を渡し、呼び出し元の値を書き換える
7 構造体 温度・風量・状態・履歴を1つのAirconStateにまとめる
8 ビット演算 ステータスレジスタ(電源・モード・風量・警告を1バイトに合成)

⑥ 各関数のフローチャート(自力で実装したい人は閲覧不要)

設計書の仕上げとして、main()以外の12関数ぶんのフローチャートを用意しました。ここまでの設計を関数ごとに図へ落としたものです。この図をもとに実装を進めてもいいですし、図を見ずに自力実装へ挑戦してもかまいません。必要な関数だけ折りたたみを開いてください。どの図も開始(緑の丸)から始まり、終了(赤の丸)で終わります。

read_button(prompt)

フローチャートを見る
read_button関数のフローチャート

push_history(ac, mode, value)

フローチャートを見る
push_history関数のフローチャート

print_history(ac)

フローチャートを見る
print_history関数のフローチャート

build_status(ac)

フローチャートを見る
build_status関数のフローチャート

print_status(status)

フローチャートを見る
print_status関数のフローチャート

show_screen(ac)

フローチャートを見る
show_screen関数のフローチャート

change_wind(ac)

フローチャートを見る
change_wind関数のフローチャート

handle_wait(ac)

フローチャートを見る
handle_wait関数のフローチャート

handle_start_up(ac)

フローチャートを見る
handle_start_up関数のフローチャート

handle_cooling(ac)

フローチャートを見る
handle_cooling関数のフローチャート

handle_hot(ac)

フローチャートを見る
handle_hot関数のフローチャート

handle_air_flow(ac)

フローチャートを見る
handle_air_flow関数のフローチャート

実装課題(あなたの番です)

総合課題

上の要件定義書・設計書のとおりに、main() から始まる1本のCプログラムを書いてください。実行すると次のように、画面表示→ボタン入力→状態遷移、を繰り返して動く想定です。

実行イメージ

========================================
現在状態:冷房モード
現在温度:24℃
現在風量:自動
ステータスレジスタ:0x03 (00000011)
温度変更履歴(直近1件):
  1回目:冷房モードで24℃へ変更
0:停止 1:冷房 2:暖房 3:-1℃ 4:風量モード 9:終了 >

特に次の境界値・異常系は、要件定義書どおりに動くか自分で確認してください。

確認observation 操作 期待する動き
温度の下限 冷房モードで0℃まで下げてさらに-1℃ 0℃のまま。マイナスにならない
温度の上限 暖房モードで40℃まで上げてさらに+1℃ 40℃のまま。41℃にならない
モード未選択 起動状態のまま温度調整ボタン相当を押す そもそも温度調整ボタンの選択肢自体が出ない
風量の循環 風量モードで風量切替を3回押す 自動→強風→弱風→自動、と1周して戻る
履歴の上限 温度を4回以上変更する 履歴は直近3件だけ残り、一番古い1件が消える
停止からの復帰 冷房25℃・強風の状態で停止→再起動 停止画面ではモード名は表示されないが、再度冷房を選ぶと温度25℃・風量は強風のまま
不正な入力 数字以外や、その画面にないボタン番号を入れる 落ちずに再入力を促す

ヒント:まず状態遷移(画面が切り替わるだけ)を動かし、次に温度・風量、最後に履歴とステータスレジスタを足していくと、詰まりにくいです。1つの関数に全部を詰め込まず、設計書の関数一覧のとおりに分割してみてください。書き方に詰まったら、設計書⑥のフローチャートを開いて処理の流れを確認しましょう。

テンプレート:ここから書き始める

白紙から書き始めなくても大丈夫です。次のテンプレートは、設計書の定数・構造体・関数プロトタイプ・main()まで書いた状態のものです。main()は設計書のフローチャートのとおりに完成させてあるので、そのままでは動きませんが、コンパイルは通ります。/* TODO */とある各関数の中身を、要件定義書・設計書のとおりに1つずつ埋めていってください。

main.c(テンプレート)

/*
 * ============================================================
 *  エアコン(クーラー)リモコン 模擬プログラム - テンプレート
 * ------------------------------------------------------------
 *  概要:
 *    要件定義書・設計書はすでに確定している。main() もフローチャート
 *    のとおりに書いてある。TODOの書かれた関数の中身だけを、
 *    要件定義書・設計書のとおりに実装すればプログラムが完成する。
 *  進め方:
 *    まず状態遷移(画面が切り替わるだけ)、次に温度・風量、
 *    最後に履歴とステータスレジスタ、の順に実装すると詰まりにくい。
 * ============================================================
 */
#include <stdio.h>

/* ---- 状態(0x01-0x06はマイコンのレジスタ値を意識した表記) ---- */
#define WAIT_STATE      0x01
#define START_UP_STATE  0x02
#define COOLING_STATE   0x03
#define HOT_STATE       0x04
#define AIR_FLOW_STATE  0x05
#define END_STATE       0x06

/* ---- 風量 ---- */
#define WIND_AUTO   0
#define WIND_STRONG 1
#define WIND_LIGHT  2

/* ---- 温度 ---- */
#define TEMP_MIN     0
#define TEMP_MAX     40
#define TEMP_INITIAL 25

/* ---- 温度変更履歴 ---- */
#define HISTORY_LEN 3

/* ---- 履歴・ステータスレジスタで使うモード番号 ---- */
#define MODE_NONE    0
#define MODE_COOLING 1
#define MODE_HOT     2
#define MODE_AIRFLOW 3

/* ---- ステータスレジスタ(1バイト)のビット配置 ---- */
#define STATUS_POWER_BIT   (1 << 0) /* bit0        : 運転中か */
#define STATUS_MODE_SHIFT  1        /* bit1-2(2bit): モード番号 */
#define STATUS_MODE_MASK   (0x03 << STATUS_MODE_SHIFT)
#define STATUS_WARN_BIT    (1 << 3) /* bit3        : 温度が上下限 */
#define STATUS_WIND_SHIFT  4        /* bit4-5(2bit): 風量番号 */
#define STATUS_WIND_MASK   (0x03 << STATUS_WIND_SHIFT)

/*
 * AirconState - エアコン1台ぶんの状態をまとめた構造体
 * ------------------------------------------------------------
 *  メンバ:
 *    state         今の状態(WAIT_STATE 等)
 *    temperature   現在の設定温度(0〜40℃)
 *    wind_select   現在の風量(0=自動 1=強風 2=弱風)
 *    history       温度変更履歴(直近HISTORY_LEN件の2次元配列)
 *    history_count 履歴の実際の件数(0〜HISTORY_LEN)
 */
typedef struct {
    int state;
    int temperature;
    int wind_select;
    int history[HISTORY_LEN][2]; /* [i][0]=モード番号 [i][1]=変更後の温度 */
    int history_count;
} AirconState;

static const char *STATE_LABELS[END_STATE + 1] = {
    "-", "停止状態", "起動状態", "冷房モード", "暖房モード", "風量モード", "終了"
};
static const char *WIND_LABELS[3] = { "自動", "強風", "弱風" };
static const char *MODE_LABELS[4] = { "-", "冷房", "暖房", "風量" };

int  read_button(const char *prompt);
void push_history(AirconState *ac, int mode, int value);
void print_history(const AirconState *ac);
unsigned char build_status(const AirconState *ac);
void print_status(unsigned char status);
void show_screen(const AirconState *ac);
void change_wind(AirconState *ac);

int handle_wait(AirconState *ac);
int handle_start_up(AirconState *ac);
int handle_cooling(AirconState *ac);
int handle_hot(AirconState *ac);
int handle_air_flow(AirconState *ac);

/*
 * main - プログラム全体のエントリポイント(実装済み・変更不要)
 * ------------------------------------------------------------
 *  概要:
 *    ac を初期化したあと、「画面表示→状態に応じた入力処理→次の状態へ」を
 *    ac.state が END_STATE になるまで繰り返す(設計書のフローチャート参照)。
 *  引数:なし
 *  戻り値:常に0(正常終了)
 */
int main(void) {
    AirconState ac;
    ac.state = WAIT_STATE;
    ac.temperature = TEMP_INITIAL;
    ac.wind_select = WIND_AUTO;
    ac.history_count = 0;

    while (ac.state != END_STATE) {
        show_screen(&ac);
        print_status(build_status(&ac));
        print_history(&ac);

        switch (ac.state) {
            case WAIT_STATE:      ac.state = handle_wait(&ac);      break;
            case START_UP_STATE:  ac.state = handle_start_up(&ac);  break;
            case COOLING_STATE:   ac.state = handle_cooling(&ac);   break;
            case HOT_STATE:       ac.state = handle_hot(&ac);       break;
            case AIR_FLOW_STATE:  ac.state = handle_air_flow(&ac);  break;
            default:               ac.state = END_STATE;             break;
        }
    }

    printf("電源をオフにしました。プログラムを終了します。\n");
    return 0;
}

/*
 * read_button - プロンプトを表示し、数字が入力されるまで読み直す
 * ------------------------------------------------------------
 *  引数:prompt 画面に表示する案内文
 *  戻り値:読み取れた整数(ボタン番号)
 *  TODO:
 *    do-whileで、次を繰り返す。
 *      1) printfでpromptを表示する
 *      2) result = scanf("%d", &value); で読み取る
 *      3) resultが1でなければ、getchar()で改行かEOFまで読み捨ててから
 *         「数字を入力してください」と表示する
 *    resultが1になったらループを抜け、valueを返す。
 */
int read_button(const char *prompt) {
    printf("%s", prompt);
    return 0; /* TODO: 上の説明のとおりに実装する */
}

/*
 * push_history - 温度変更履歴に1件追加する
 * ------------------------------------------------------------
 *  引数:ac=状態へのポインタ、mode=そのときのモード番号、value=変更後の温度
 *  戻り値:なし
 *  TODO:
 *    history_countがHISTORY_LEN未満ならそのまま追加してcount++。
 *    満杯ならforで全要素を1つ前へずらしてから最後に追加する。
 */
void push_history(AirconState *ac, int mode, int value) {
    (void)ac; (void)mode; (void)value; /* TODO */
}

/*
 * print_history - 温度変更履歴を画面に表示する
 * ------------------------------------------------------------
 *  引数:ac 状態へのポインタ(読み取るだけ)
 *  戻り値:なし
 *  TODO:history_countが0なら「なし」、それ以外は件数ぶんforで表示する。
 */
void print_history(const AirconState *ac) {
    (void)ac; /* TODO */
}

/*
 * build_status - 現在の状態を1バイトのステータスレジスタへ詰める
 * ------------------------------------------------------------
 *  引数:ac 状態へのポインタ(読み取るだけ)
 *  戻り値:合成済みのステータスレジスタ(設計書⑤参照)
 *  TODO:電源・モード・風量・警告をシフトとORでstatusへ詰めて返す。
 */
unsigned char build_status(const AirconState *ac) {
    (void)ac; /* TODO */
    return 0;
}

/*
 * print_status - ステータスレジスタを16進と2進の両方で表示する
 * ------------------------------------------------------------
 *  引数:status build_status()が返した1バイト
 *  戻り値:なし
 *  TODO:0x%02Xで16進表示したあと、bitを7から0まで下げながら
 *        (status >> bit) & 1 を1桁ずつ表示する。
 */
void print_status(unsigned char status) {
    (void)status; /* TODO */
}

/*
 * show_screen - 状態名・温度・風量を画面に表示する
 * ------------------------------------------------------------
 *  引数:ac 状態へのポインタ(読み取るだけ)
 *  戻り値:なし
 *  TODO:温度が上限・下限ならラベルを付け、STATE_LABELS[ac->state]・
 *        WIND_LABELS[ac->wind_select]で状態名・風量名を表示する。
 */
void show_screen(const AirconState *ac) {
    (void)ac; /* TODO */
}

/*
 * change_wind - 風量を自動→強風→弱風→自動の順に1つ進める
 * ------------------------------------------------------------
 *  引数:ac 状態へのポインタ(wind_selectを書き換える)
 *  戻り値:なし
 *  TODO:wind_select = (wind_select + 1) % 3;
 */
void change_wind(AirconState *ac) {
    (void)ac; /* TODO */
}

/*
 * handle_wait - 「停止状態」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  引数:ac 状態へのポインタ
 *  戻り値:次の状態(要件定義書「① 起動・停止」・状態遷移表を参照)
 *  TODO:0(起動)か9(終了)が押されるまで読み直し、対応する状態を返す。
 */
int handle_wait(AirconState *ac) {
    (void)ac; /* TODO */
    return WAIT_STATE;
}

/*
 * handle_start_up - 「起動状態」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  引数:ac 状態へのポインタ
 *  戻り値:次の状態(状態遷移表のSTART_UP_STATEの行を参照)
 *  TODO:0/1/2/4/9のいずれかが押されるまで読み直し、switchで次の状態を返す。
 */
int handle_start_up(AirconState *ac) {
    (void)ac; /* TODO */
    return START_UP_STATE;
}

/*
 * handle_cooling - 「冷房モード」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  引数:ac 状態へのポインタ(temperature・historyを書き換える)
 *  戻り値:次の状態(状態遷移表のCOOLING_STATEの行を参照)
 *  TODO:0/1/2/4/9は他と同じ。3のときだけ温度を-1(下限0でクランプ)し、
 *        push_historyで履歴に記録してからCOOLING_STATEのまま返す。
 */
int handle_cooling(AirconState *ac) {
    (void)ac; /* TODO */
    return COOLING_STATE;
}

/*
 * handle_hot - 「暖房モード」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  引数:ac 状態へのポインタ(temperature・historyを書き換える)
 *  戻り値:次の状態(状態遷移表のHOT_STATEの行を参照)
 *  TODO:handle_coolingと対称。3のときだけ温度を+1(上限40でクランプ)し、
 *        push_historyで履歴に記録してからHOT_STATEのまま返す。
 */
int handle_hot(AirconState *ac) {
    (void)ac; /* TODO */
    return HOT_STATE;
}

/*
 * handle_air_flow - 「風量モード」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  引数:ac 状態へのポインタ(change_wind経由でwind_selectを書き換える)
 *  戻り値:次の状態(状態遷移表のAIR_FLOW_STATEの行を参照)
 *  TODO:0/1/2/4/9は他と同じ。3のときだけchange_wind(ac)を呼んで
 *        AIR_FLOW_STATEのまま返す。
 */
int handle_air_flow(AirconState *ac) {
    (void)ac; /* TODO */
    return AIR_FLOW_STATE;
}
テンプレートのままコンパイルすると警告が出ます
STATE_LABELS などの配列は、show_screenprint_history の中身を書くまでは使われないため、gcc -Wall で「使われていません」という警告(-Wunused-variable)が出ます。これはテンプレート段階では正常な状態です。各関数を実装して配列を使うようになると、警告は消えます。
模範解答・解説を見る

模範解答

main.c(模範解答・全文)

/*
 * ============================================================
 *  エアコン(クーラー)リモコン 模擬プログラム - 模範解答
 * ------------------------------------------------------------
 *  概要:
 *    要件定義書・設計書のとおりに実装した参考実装。
 *    状態遷移(1〜8のうち#2 if/switch・#3 ループ)・
 *    温度変更履歴(#4 配列)・状態を渡す構造体とポインタ(#6・#7)・
 *    ステータスレジスタ(#8 ビット演算)を1本のプログラムで使う。
 *  検証:
 *    gcc -Wall -Wextra -std=c11 で警告0件。
 * ============================================================
 */
#include <stdio.h>

/* ---- 状態(0x01-0x06はマイコンのレジスタ値を意識した表記) ---- */
#define WAIT_STATE      0x01
#define START_UP_STATE  0x02
#define COOLING_STATE   0x03
#define HOT_STATE       0x04
#define AIR_FLOW_STATE  0x05
#define END_STATE       0x06

/* ---- 風量 ---- */
#define WIND_AUTO   0
#define WIND_STRONG 1
#define WIND_LIGHT  2

/* ---- 温度 ---- */
#define TEMP_MIN     0
#define TEMP_MAX     40
#define TEMP_INITIAL 25

/* ---- 温度変更履歴 ---- */
#define HISTORY_LEN 3

/* ---- 履歴・ステータスレジスタで使うモード番号 ---- */
#define MODE_NONE    0
#define MODE_COOLING 1
#define MODE_HOT     2
#define MODE_AIRFLOW 3

/* ---- ステータスレジスタ(1バイト)のビット配置 ---- */
#define STATUS_POWER_BIT   (1 << 0) /* bit0        : 運転中か */
#define STATUS_MODE_SHIFT  1        /* bit1-2(2bit): モード番号 */
#define STATUS_MODE_MASK   (0x03 << STATUS_MODE_SHIFT)
#define STATUS_WARN_BIT    (1 << 3) /* bit3        : 温度が上下限 */
#define STATUS_WIND_SHIFT  4        /* bit4-5(2bit): 風量番号 */
#define STATUS_WIND_MASK   (0x03 << STATUS_WIND_SHIFT)

/*
 * AirconState - エアコン1台ぶんの状態をまとめた構造体
 * ------------------------------------------------------------
 *  メンバ:
 *    state         今の状態(WAIT_STATE 等。状態遷移表§設計書①)
 *    temperature   現在の設定温度(0〜40℃)
 *    wind_select   現在の風量(0=自動 1=強風 2=弱風)
 *    history       温度変更履歴(直近HISTORY_LEN件の2次元配列)
 *                  history[i][0]=そのときのモード番号 history[i][1]=変更後の温度
 *    history_count 履歴の実際の件数(0〜HISTORY_LEN)
 *  設計方針:
 *    グローバル変数にはせず、main() 内のローカル変数として1つだけ作る。
 *    状態を書き換える関数は、この構造体へのポインタ(AirconState *)を
 *    引数で受け取る(詳細は各関数のコメントを参照)。
 */
typedef struct {
    int state;
    int temperature;
    int wind_select;
    int history[HISTORY_LEN][2];
    int history_count;
} AirconState;

/* 状態番号→名前、風量番号→名前、モード番号→名前 の対応表(#4 配列 + #6 ポインタ) */
static const char *STATE_LABELS[END_STATE + 1] = {
    "-", "停止状態", "起動状態", "冷房モード", "暖房モード", "風量モード", "終了"
};
static const char *WIND_LABELS[3] = { "自動", "強風", "弱風" };
static const char *MODE_LABELS[4] = { "-", "冷房", "暖房", "風量" };

int  read_button(const char *prompt);
void push_history(AirconState *ac, int mode, int value);
void print_history(const AirconState *ac);
unsigned char build_status(const AirconState *ac);
void print_status(unsigned char status);
void show_screen(const AirconState *ac);
void change_wind(AirconState *ac);

int handle_wait(AirconState *ac);
int handle_start_up(AirconState *ac);
int handle_cooling(AirconState *ac);
int handle_hot(AirconState *ac);
int handle_air_flow(AirconState *ac);

/*
 * main - プログラム全体のエントリポイント
 * ------------------------------------------------------------
 *  概要:
 *    ac を初期化したあと、「画面表示→状態に応じた入力処理→次の状態へ」を
 *    ac.state が END_STATE になるまで繰り返す(設計書のフローチャート参照)。
 *  引数:なし
 *  戻り値:常に0(正常終了)
 *  備考:
 *    ac はここでしか作らないローカル変数。以降のすべての関数へは
 *    &ac(AirconStateへのポインタ)を渡すことで状態を共有する。
 */
int main(void) {
    AirconState ac;
    ac.state = WAIT_STATE;
    ac.temperature = TEMP_INITIAL;
    ac.wind_select = WIND_AUTO;
    ac.history_count = 0;

    while (ac.state != END_STATE) {
        show_screen(&ac);
        print_status(build_status(&ac));
        print_history(&ac);

        switch (ac.state) {
            case WAIT_STATE:      ac.state = handle_wait(&ac);      break;
            case START_UP_STATE:  ac.state = handle_start_up(&ac);  break;
            case COOLING_STATE:   ac.state = handle_cooling(&ac);   break;
            case HOT_STATE:       ac.state = handle_hot(&ac);       break;
            case AIR_FLOW_STATE:  ac.state = handle_air_flow(&ac);  break;
            default:               ac.state = END_STATE;             break;
        }
    }

    printf("電源をオフにしました。プログラムを終了します。\n");
    return 0;
}

/*
 * read_button - プロンプトを表示し、数字が入力されるまで読み直す
 * ------------------------------------------------------------
 *  概要:
 *    scanf の戻り値(読み取れた個数)が1でない=数字以外が入力された場合、
 *    getchar() で改行かEOFが出てくるまで1文字ずつ読み捨ててから、
 *    もう一度 scanf をやり直す(do-while:最低1回は必ず聞く)。
 *  引数:
 *    prompt  画面に表示する案内文(例:"0:停止 1:冷房 ... > ")
 *  戻り値:
 *    読み取れた整数(ボタン番号)
 *  備考:
 *    要件定義書「⑦ 入力の頑健性」に対応する関数。1〜8のドリルでは
 *    扱っていない、キーボード入力ならではの技術。
 */
int read_button(const char *prompt) {
    int value;
    int result;
    do {
        printf("%s", prompt);
        result = scanf("%d", &value);
        if (result != 1) {
            int c;
            while ((c = getchar()) != '\n' && c != EOF) {
                /* 数字以外の入力を1文字ずつ読み捨てる(本体は空でよい) */
            }
            printf("数字を入力してください\n");
        }
    } while (result != 1);
    return value;
}

/*
 * push_history - 温度変更履歴に1件追加する
 * ------------------------------------------------------------
 *  概要:
 *    history_count が HISTORY_LEN 未満ならそのまま末尾に追加する。
 *    満杯のときは、for ループで全要素を1つ前へずらしてから
 *    最後の枠に新しい値を書き込む(=直近HISTORY_LEN件だけが残る)。
 *  引数:
 *    ac     状態へのポインタ(history・history_countを書き換える)
 *    mode   そのとき運転していたモード番号(MODE_COOLING等)
 *    value  変更後の温度
 *  戻り値:なし
 *  備考:
 *    history は2次元配列(#4)。ac をポインタで受け取っているので、
 *    ここでの書き換えは呼び出し元(main側のac)にそのまま反映される(#6)。
 */
void push_history(AirconState *ac, int mode, int value) {
    if (ac->history_count < HISTORY_LEN) {
        ac->history[ac->history_count][0] = mode;
        ac->history[ac->history_count][1] = value;
        ac->history_count++;
    } else {
        for (int i = 0; i < HISTORY_LEN - 1; i++) {
            ac->history[i][0] = ac->history[i + 1][0];
            ac->history[i][1] = ac->history[i + 1][1];
        }
        ac->history[HISTORY_LEN - 1][0] = mode;
        ac->history[HISTORY_LEN - 1][1] = value;
    }
}

/*
 * print_history - 温度変更履歴を画面に表示する
 * ------------------------------------------------------------
 *  概要:
 *    件数が0なら「なし」とだけ表示する。1件以上あれば見出しを出したあと、
 *    for ループで0件目からhistory_count-1件目まで1行ずつ表示する。
 *  引数:
 *    ac  状態へのポインタ(読み取るだけなので const)
 *  戻り値:なし
 */
void print_history(const AirconState *ac) {
    if (ac->history_count == 0) {
        printf("温度変更履歴:なし\n");
        return;
    }
    printf("温度変更履歴(直近%d件):\n", ac->history_count);
    for (int i = 0; i < ac->history_count; i++) {
        printf("  %d回目:%sモードで%d℃へ変更\n",
               i + 1, MODE_LABELS[ac->history[i][0]], ac->history[i][1]);
    }
}

/*
 * build_status - 現在の状態を1バイトのステータスレジスタへ詰める
 * ------------------------------------------------------------
 *  概要:
 *    電源(bit0)・モード番号(bit1-2)・風量番号(bit4-5)・
 *    温度が上下限かどうか(bit3)を、シフト(<<)・OR(|)・
 *    マスク(&)を使って1バイトに合成する(#8 ビット演算)。
 *  引数:
 *    ac  状態へのポインタ(読み取るだけ)
 *  戻り値:
 *    合成済みのステータスレジスタ(unsigned char 1バイト)
 */
unsigned char build_status(const AirconState *ac) {
    unsigned char status = 0;
    int mode_bits = MODE_NONE;

    if (ac->state == COOLING_STATE) {
        mode_bits = MODE_COOLING;
    }
    else if (ac->state == HOT_STATE) {
        mode_bits = MODE_HOT;
    }
    else if (ac->state == AIR_FLOW_STATE) {
        mode_bits = MODE_AIRFLOW;
    }

    if (ac->state != WAIT_STATE && ac->state != END_STATE) {
        status |= STATUS_POWER_BIT;
    }
    status |= (mode_bits << STATUS_MODE_SHIFT) & STATUS_MODE_MASK;
    status |= (ac->wind_select << STATUS_WIND_SHIFT) & STATUS_WIND_MASK;
    if (ac->temperature == TEMP_MIN || ac->temperature == TEMP_MAX) {
        status |= STATUS_WARN_BIT;
    }
    return status;
}

/*
 * print_status - ステータスレジスタを16進と2進の両方で表示する
 * ------------------------------------------------------------
 *  概要:
 *    まず0x%02Xで16進表示。続けてbitを7から0まで1つずつ下げながら、
 *    (status >> bit) & 1 でそのビットだけを取り出して1桁ずつ表示する
 *    (マイコンのレジスタをシリアルモニタで確認するときと同じ考え方)。
 *  引数:
 *    status  build_status() が返した1バイト
 *  戻り値:なし
 */
void print_status(unsigned char status) {
    printf("ステータスレジスタ:0x%02X (", status);
    for (int bit = 7; bit >= 0; bit--) {
        printf("%d", (status >> bit) & 1);
    }
    printf(")\n");
}

/*
 * show_screen - 状態名・温度・風量を画面に表示する
 * ------------------------------------------------------------
 *  概要:
 *    温度が上限・下限のどちらかに達していれば、その旨のラベルを
 *    if/else if で決める(#2)。状態名・風量名は
 *    STATE_LABELS/WIND_LABELS配列を state・wind_select で
 *    そのまま添字参照する(#4配列 + #6ポインタ)。
 *  引数:
 *    ac  状態へのポインタ(読み取るだけ)
 *  戻り値:なし
 */
void show_screen(const AirconState *ac) {
    const char *limit_label = "";
    if (ac->temperature == TEMP_MAX) {
        limit_label = "(設定上限)";
    }
    else if (ac->temperature == TEMP_MIN) {
        limit_label = "(設定下限)";
    }

    printf("========================================\n");
    printf("現在状態:%s\n", STATE_LABELS[ac->state]);
    printf("現在温度:%d℃%s\n", ac->temperature, limit_label);
    printf("現在風量:%s\n", WIND_LABELS[ac->wind_select]);
}

/*
 * change_wind - 風量を自動→強風→弱風→自動の順に1つ進める
 * ------------------------------------------------------------
 *  概要:
 *    (wind_select + 1) % 3 で3種類を循環させる(#1 剰余演算子)。
 *  引数:
 *    ac  状態へのポインタ(wind_selectを書き換える)
 *  戻り値:なし
 */
void change_wind(AirconState *ac) {
    ac->wind_select = (ac->wind_select + 1) % 3;
}

/*
 * handle_wait - 「停止状態」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  概要:
 *    0(起動)か9(終了)が押されるまで読み直す(do-while)。
 *  引数:
 *    ac  状態へのポインタ(この関数では読み書きしない。将来の
 *        拡張=停止中の値表示等に備えて統一的にポインタで受け取る)
 *  戻り値:
 *    次の状態(START_UP_STATE または END_STATE)
 */
int handle_wait(AirconState *ac) {
    (void)ac;
    int button;
    do {
        button = read_button("0:起動  9:終了 > ");
        if (button != 0 && button != 9) {
            printf("ボタン入力ミスです。0か9を入力してください\n");
        }
    } while (button != 0 && button != 9);

    if (button == 0) {
        return START_UP_STATE;
    }
    else {
        return END_STATE;
    }
}

/*
 * handle_start_up - 「起動状態」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  概要:
 *    0/1/2/4/9のいずれかが押されるまで読み直し、switchで次の状態を返す。
 *  引数:
 *    ac  状態へのポインタ(この関数では書き換えない)
 *  戻り値:
 *    次の状態(WAIT_STATE / COOLING_STATE / HOT_STATE /
 *    AIR_FLOW_STATE / END_STATE のいずれか)
 */
int handle_start_up(AirconState *ac) {
    (void)ac;
    int button;
    do {
        button = read_button("0:停止 1:冷房 2:暖房 4:風量モード 9:終了 > ");
        switch (button) {
            case 0: return WAIT_STATE;
            case 1: return COOLING_STATE;
            case 2: return HOT_STATE;
            case 4: return AIR_FLOW_STATE;
            case 9: return END_STATE;
            default: printf("ボタン入力ミスです\n");
        }
    } while (1);
}

/*
 * handle_cooling - 「冷房モード」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  概要:
 *    0/1/2/4/9はhandle_start_upと同じ扱い。3が押されたときだけ、
 *    temperatureが下限(TEMP_MIN)より大きければ1℃下げ、
 *    push_historyで変更履歴(モード=MODE_COOLING)を記録してから、
 *    同じCOOLING_STATEにとどまる。
 *  引数:
 *    ac  状態へのポインタ(temperature・historyを書き換える)
 *  戻り値:
 *    次の状態
 */
int handle_cooling(AirconState *ac) {
    int button;
    do {
        button = read_button("0:停止 1:冷房 2:暖房 3:-1℃ 4:風量モード 9:終了 > ");
        switch (button) {
            case 0: return WAIT_STATE;
            case 1: return COOLING_STATE;
            case 2: return HOT_STATE;
            case 3:
                if (ac->temperature > TEMP_MIN) {
                    ac->temperature--;
                }
                push_history(ac, MODE_COOLING, ac->temperature);
                return COOLING_STATE;
            case 4: return AIR_FLOW_STATE;
            case 9: return END_STATE;
            default: printf("ボタン入力ミスです\n");
        }
    } while (1);
}

/*
 * handle_hot - 「暖房モード」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  概要:
 *    handle_coolingと対称。3が押されたときは temperature が
 *    上限(TEMP_MAX)未満なら1℃上げ、push_historyで記録する
 *    (モード=MODE_HOT)。
 *  引数:
 *    ac  状態へのポインタ(temperature・historyを書き換える)
 *  戻り値:
 *    次の状態
 */
int handle_hot(AirconState *ac) {
    int button;
    do {
        button = read_button("0:停止 1:冷房 2:暖房 3:+1℃ 4:風量モード 9:終了 > ");
        switch (button) {
            case 0: return WAIT_STATE;
            case 1: return COOLING_STATE;
            case 2: return HOT_STATE;
            case 3:
                if (ac->temperature < TEMP_MAX) {
                    ac->temperature++;
                }
                push_history(ac, MODE_HOT, ac->temperature);
                return HOT_STATE;
            case 4: return AIR_FLOW_STATE;
            case 9: return END_STATE;
            default: printf("ボタン入力ミスです\n");
        }
    } while (1);
}

/*
 * handle_air_flow - 「風量モード」でのボタン入力を処理する
 * ------------------------------------------------------------
 *  概要:
 *    0/1/2/4/9は他のhandle_*と同じ扱い。3が押されたときだけ
 *    change_wind(ac) を呼んで風量を1つ進め、同じAIR_FLOW_STATEに
 *    とどまる。
 *  引数:
 *    ac  状態へのポインタ(change_wind経由でwind_selectを書き換える)
 *  戻り値:
 *    次の状態
 */
int handle_air_flow(AirconState *ac) {
    int button;
    do {
        button = read_button("0:停止 1:冷房 2:暖房 3:風量切替 4:風量モード 9:終了 > ");
        switch (button) {
            case 0: return WAIT_STATE;
            case 1: return COOLING_STATE;
            case 2: return HOT_STATE;
            case 3: change_wind(ac); return AIR_FLOW_STATE;
            case 4: return AIR_FLOW_STATE;
            case 9: return END_STATE;
            default: printf("ボタン入力ミスです\n");
        }
    } while (1);
}

検証:このコードは gcc -Wall -Wextra -std=c11 で警告0件です。「起動→冷房で-1℃を4回→風量モード→風量切替→停止→再起動→終了」という操作を流し込んで、状態遷移・温度の上下限・履歴の3件保持と1件破棄・ステータスレジスタのビット表示が設計どおりに動くことを確認しています。

解説:1〜8がコードのどこに現れているか

1. 変数・型・演算子

ac->temperature > TEMP_MINac->temperature < TEMP_MAX の比較演算子で範囲を守っています。(ac->wind_select + 1) % 3 の剰余演算子は、3種類の風量を「自動→強風→弱風→自動」と循環させる定番の書き方です。

2. if / switch

handle_* 関数の中身は switch (button) で、ボタン番号によって処理を分けています。show_screenlimit_label は、上限・下限・通常の3択をif/else ifで決めています。どちらにも当てはまらなければ、最初に入れた空文字のままです。

3. ループ・do-while

main() 全体を包む while (ac.state != END_STATE) が本体のループです。各 handle_* 関数の中は do { ... } while (1) で、正しいボタンが押されるまで同じ画面を出し続けるしくみ。「まず聞いてから、条件を確認する」という順序が自然にdo-whileになる典型例です。read_button 内部の do-while も同じ考え方です。

4. 配列・2次元配列

history[HISTORY_LEN][2] が2次元配列です。push_historyfor ループで、満杯のときに全要素を1つ前へずらしてから最後に新しい値を入れています。STATE_LABELSWIND_LABELSMODE_LABELS は、状態番号を添字にして名前を引く1次元配列です。

5. 関数・引数・戻り値・スコープ

状態ごとの処理を handle_waithandle_air_flow の5つの関数に分割しています。それぞれ「次にどの状態へ行くか」を戻り値で返し、main() はその戻り値を ac.state に代入するだけです。AirconState acmain()ローカル変数で、グローバル変数は1つも使っていません。

6. ポインタ

すべての handle_* 関数・push_historychange_wind は、引数を AirconState *ac(構造体へのポインタ)で受け取っています。もしポインタを使わず AirconState ac で値渡しにすると、関数の中で ac->temperature-- と書いても呼び出し元の main() の温度は変わりません。ローカル変数1つだけで状態を管理しつつ、複数の関数から書き換えるためには、ポインタが不可欠です。

7. 構造体

AirconState が、状態・温度・風量・履歴という関連するデータをすべて1つにまとめた構造体です。もしこれが4つのバラバラな変数だったら、関数に渡す引数は4つになり、ポインタも4つ必要になっていました。構造体1つにまとめてあるから、ポインタ1つで済んでいます。

8. ビット演算

build_status が、電源・モード・風量・警告という4種類の情報を、シフト(<<)・OR(|)・AND(&)を使って1バイトに合成しています。print_status では、その1バイトを1ビットずつ取り出して2進数表示しています((status >> bit) & 1)。実機のレジスタを読むときに使う考え方そのままです。

例:実務で実際に書くコード

この模擬プログラムの設計は、実務の組込みソフトの骨格を、そのまま小さくしたものです。

実務での状態遷移設計の話
私が転職前に自作した温湿度監視システムでも、まさにこの「状態」の設計が一番苦労したところでした。温度・湿度から警戒レベルを判定し、レベルによってエアコンを自動でON/OFFする仕組みだったのですが、「今どの状態か」を1つの変数(または構造体)で正しく管理できていないと、思わぬタイミングで自動制御と手動操作がぶつかるということを痛感しました。特に注意したのは「人間の操作を最優先にする」という考え方です。自動制御が入っていても、人が一度電源を切ったなら、システム側が勝手に電源を入れ直したりはしない。これは今回のような小さな模擬プログラムにも応用できる考え方で、もし発展させて「一定時間で自動的に風量を戻す」といったタイマー機能を足すなら、必ず「人間がボタンを押したら、その操作を優先する」設計にすべきです。実務の状態遷移は、突き詰めると「どちらが優先か」を決める作業でもあります。

また、ステータスレジスタのような「複数の状態を1バイトにまとめて持つ」設計は、実機のマイコンのレジスタそのものの考え方です。GPIOのレジスタも、1つのレジスタの各ビットが別々のピンの状態を表している、というのが基本の形です。#8で学んだビット演算は、こういうレジスタを読み書きするときにそのまま使う知識です。

最後につまずきポイントをおさらい

ポインタ経由は「.」でなく「->」
AirconState *ac のようにポインタで受け取った構造体のメンバにアクセスするときは、ac.temperature ではなくac->temperature(矢印)を使います。main() 側の実体 ac. のまま、関数の引数側の ac(ポインタ)は ->、という使い分けを混同しやすいので注意してください(#7)。

もう一つはビットのシフトと優先順位です。mode_bits << STATUS_MODE_SHIFT & STATUS_MODE_MASK のように括弧を省略すると、演算子の優先順位次第で意図と違う結果になることがあります。この記事のコードのようにシフトとマスクはそれぞれ括弧で囲む癖をつけておくと安全です。

最後に、履歴配列のずらし処理でのオフバイワンです。history[HISTORY_LEN - 1] が最後の要素、for (i = 0; i < HISTORY_LEN - 1; i++) の範囲でずらす、という添字の境界を1つ間違えると、配列の範囲外を読み書きしてしまいます(#4で練習した「最大の添字は要素数-1」がそのまま効いてきます)。

よくある質問(FAQ)

設計書どおりに実装できませんでした。どうすればいいですか?

大丈夫です。まずは状態遷移(画面が切り替わるだけ)を動かすところから始めてください。温度・風量・履歴・ステータスレジスタは、後から1つずつ機能を足していけます。模範解答と自分のコードを1関数ずつ見比べて、違いを確認するのが一番の近道です。

1〜8を全部使わないと解けませんか?

この設計はすべて使う前提で組んでいますが、実装の細部(履歴を配列にせず変数3つにする、構造体を使わず変数を並べる、など)は読者の工夫次第です。ただし構造体・ポインタを外すと、設計書の「グローバル変数を使わない」という方針とは矛盾するので、まずは設計書どおりに書いてみることをおすすめします。

本物のマイコンだと何が変わりますか?

ボタン入力が scanf ではなく実際のスイッチのGPIO読み取りになり、画面表示が printf ではなく液晶やLEDへの出力になります。ただし「状態をどう持つか」「関数をどう分けるか」という設計の考え方そのものは、この模擬プログラムとほとんど変わりません。次のArduinoクラスタで、実際にマイコンへ移していきます。

ステータスレジスタは何のために必要ですか?実用上意味がありますか?

今回のような小さいプログラムなら、無くても動作自体は成立します。ですが実機開発では、シリアルモニタなどでこの1バイトを見るだけで「今どんな状態か」を一目で確認でき、不具合調査の初動が大きく変わります。今のうちに「複数の状態を1バイトにまとめる」考え方に慣れておくと、実機に進んだときに役立ちます。

次に読む

お疲れさまでした。これで1〜9、C言語の基礎からエアコン模擬プログラムまで、組込みプログラムに必要な部品はひと通り揃いました。次はいよいよ、この知識を実際に動くマイコンで試す番です。

次のステップ(準備中)Arduinoとは?できること・種類
前のドリル(#8)C言語 ビット演算の練習問題7問|マスク・シフトを図解で固める
目次へ戻るC言語 基礎ドリル|組込みに必要な8テーマの練習問題まとめ