【Arduinoの裏側②】void loop() の外側、隠された main 関数

※本ページはプロモーションが含まれています

前回の「アップロードの仕組み」に続き、今回はプログラムの中身のお話です。

前回はこちら cratech.hatenablog.jp

C言語C++の教科書を読むと、最初の方に必ずこう書いてあります。 「プログラムは main 関数から始まり、main 関数で終わる」

しかし、Arduinoのスケッチ(プログラム)を見てみましょう。あるのは setup()loop() だけ。 C言語のルールに従えば、これはコンパイルエラーになるはずです。

なぜArduinoはエラーにならずに動くのでしょうか? 実は、私たちの目に見えない裏側に、本当のmain関数が隠されているからです。

今回は、Arduino Core(システムの核)のソースコードを覗き見て、その正体を暴いていきましょう。


全体像:setuploop は「呼ばれている」だけ

結論から言います。 私たちが一生懸命書いている setup()loop() は、プログラムの支配者ではありません。 彼らは、裏にいる main() 関数から「呼び出されている」だけの部品に過ぎないのです。

イメージとしては、このような包含関係になっています。

main という大きな枠組みの中に、私たちのコードがすっぽりと収まっているのです。


証拠:実際のソースコードを見てみよう

論より証拠。Arduinoのインストールフォルダの奥底に眠る、実際のソースコードmain.cpp)をお見せします。 これが、Arduinoが動いている時の「真の姿」です。

実際の main.cpp の中身(要約)

#include <Arduino.h>

// ここが本当のスタート地点!
int main(void)
{
    init();  // ① 初期化(タイマー設定など)

    setup(); // ② 私たちが書いたsetupを実行

    for (;;) { // ③ 無限ループ
        loop(); // ④ 私たちが書いたloopを実行
        if (serialEventRun) serialEventRun(); // ⑤ シリアル通信のチェック
    }
        
    return 0;
}

※わかりやすく一部省略していますが、構造は忠実に書いています。


コードの解説:裏で何が起きている?

このコードを読み解くと、Arduinoの挙動が見えてきます。

① init()

まず最初に init() という関数が呼ばれます。 ここでは、「時間の計測(millis)」「PWM(アナログ出力)」などが使えるように、マイコン内部のタイマー設定を自動で行っています。 私たちが面倒なレジスタ設定なしで delay(1000) と書けるのは、ここで準備してくれているおかげです。

② setup()

次に、私たちが書いた setup()1回だけ呼ばれます。

③ for (;;) { ... }

for (;;)C言語で無限ループです。

Arduinoは、この無限ループの中で、 1. loop() を呼ぶ 2. loop() が終わったら、また戻ってきて loop() を呼ぶ

をひたすら繰り返しているだけなのです。

つまり、void loop() という関数自体がループしているわけではなく、「外側の main 関数によって、何度も何度も呼び出されている」というのが真実です。

なので、loop関数内でreturnしても、loopの最初に返ってきます。これはfor(;;)で囲まれているからなのです。


なぜこんな構造なのか?

なぜ普通に main() を書かせず、わざわざ setuploop に分けたのでしょうか? それは、「組み込みプログラミング特有の作法(無限ループ)」を初心者に意識させないためです。

本来、組み込みプログラムは以下のように書きます。

int main() {
    初期化();
    while(1) {  // 自分で無限ループを作らないといけない
        処理();
    }
}

もし初心者が while(1) を書き忘れると、プログラムは一瞬で終了して止まってしまいます(Lチカなら、一瞬光って消えて終わりです)。 Arduino開発チームは、この「枠組み」をあらかじめ用意してあげることで、 「とにかく loop の中に書けば動き続けるよ!」 という手軽さを実現したのです。素晴らしいですね。


エンジニアの視点:ここから分かること

この構造を知っていると、開発で役立つ知識が2つあります。

1. loop() の終わりは一瞬の隙間

loop() 関数の最後の行まで行くと、一度 main に戻り、またすぐに loop が呼ばれます。 この「戻って入り直す」処理には、ほんのわずかですが時間がかかります(オーバーヘッド)。

もし、マイクロ秒単位で極限まで速い処理(高速な波形生成など)をしたい場合は、loop() の中身をあえて while(1) { ... } で閉じ込めてしまい、main に戻さないというテクニックもあります。

2. serialEvent の正体

main.cpp の中に if (serialEventRun)... という記述がありましたね。 これは「シリアル通信でデータが来ているか?」をチェックする機能です。 loop() が一周するたびに毎回これがチェックされるため、loop() の中で重い処理(delay(3000)など)をしてしまうと、このチェックが遅れ、通信の反応が悪くなる原因になります。


まとめ

  • Arduinoにもちゃんと main() 関数はある。
  • setuploop は、裏方から呼ばれているだけのパーツ。
  • この親切設計のおかげで、初心者は「無限ループ」を気にせずコードが書ける。

さて、裏側の構造が見えてきました。 次回は、いよいよハードウェアに近い部分に踏み込みます。

便利でよく使う digitalWrite 関数。実はこれ、プロの現場では「遅すぎるから使うな」と言われることがあります。 なぜ遅いのか? どうすれば速くなるのか? 次回はレジスタ操作」について書く予定です。