Arduinoプログラミング基礎|変数・関数・制御構文入門
Lチカまでは動いたのに、サンプルコードを開くと急に意味がつながらなくなる——という初心者向けのガイドです。
この記事ではまず setup() と loop() を軸に、変数・関数・if/for/while/switch を LED、ボタン、アナログ入力の動きと結びつけて整理します。
このガイドで目指すのは、単にコードの読み方を教えるだけでなく、「自分で改造できる」状態に到達することです。
所要時間は約30〜60分、難易度は初心者向け(基礎の構文と簡単な配線が分かれば進められます)。
arduinoプログラムの全体像を最初に押さえる">Arduinoプログラムの全体像を最初に押さえる
setup()/loop()の実行モデル
Arduino のスケッチ(.ino)は、まず setup() が起動時に 1 回だけ実行され、その後に loop() が電源が入っている間ずっと繰り返される、という形で読むと全体像がつかめます。
Arduino公式 Getting Started(https://docs.arduino.cc/learn/starting-guide/getting-started-arduino/でも、この 2 つがスケッチの中心として案内されています)。
頭の中では、次のような流れをイメージすると混乱しません。
電源ON / リセット
↓
setup() を1回実行
・ピンの入出力設定
・シリアル通信の開始
・初期値の代入
↓
loop() を繰り返し実行
・センサーを読む
・条件で分岐する
・LEDやモーターを動かす
・また先頭に戻る
↓
電源OFF まで続く
たとえば LED を 1 秒ごとに点滅させるサンプルなら、setup() で pinMode(LED_BUILTIN, OUTPUT); を 1 回だけ設定し、loop() で digitalWrite() と delay(1000) を繰り返します。
この delay(1000) は 1000 ms、つまり 1 秒待つという意味です。
サンプルコードが読みにくく感じる人の多くは、delay(1000) を「なぜ 1000 なのか」で止まるのですが、単位がミリ秒だとわかるだけで動きの見通しが立ちます。
ここで混同しやすいのが、for や while のループと loop() の違いです。for は「3 回だけ点滅する」のように回数が決まっている反復、while は「ボタンが押されるまで待つ」のように条件付きの反復、loop() は Arduino 全体のメイン処理そのものです。
つまり loop() は 1 つの大きな舞台で、その中に if や for や while を配置して動作を組み立てます。
筆者の経験では、初心者ほど loop() に処理を全部書き込みがちです。
LED 制御、ボタン判定、シリアル出力を 1 か所に詰め込むと、“何がいつ起きているか”が追えなくなります。
そのためワークショップでは、readButton()、updateLed()、printStatus() のように小さなプログラミングの関数へ分ける書き方を早い段階から徹底しています。setup() と loop() という骨組みは共通のまま、処理の役割を分けるだけで見通しが一気に良くなります。
NOTE
最初の説明としては、「setup() は準備を 1 回だけ行う場所、loop() は動作をずっと繰り返す場所」と言い切れる状態まで整理できると、サンプル改造で手が止まりにくくなります。
スケッチとIDE〜アップロードの流れ
Arduino の開発では、PC 側でスケッチを書き、Arduino IDEでコンパイルし、その結果をボードへアップロードして実行します。
流れとしてはシンプルで、まず .ino にコードを書き、IDE の検証機能で文法や宣言の誤りを確認し、問題がなければArduino Uno R3へ転送します。
実際のイメージは次の通りです。
PCでスケッチを書く
↓
Arduino IDE でコンパイル
↓
エラーがなければ Uno R3 へアップロード
↓
ボード上の ATmega328P で実行
アップロード時には USB 接続を使い、内部的にはシリアル経由でプログラムが転送されます。
初心者が「書いたコードがどこで動いているのか」を見失う場面がありますが、実行している本体は PC ではなく、アップロード先のマイコンです。
この感覚がつかめると、PC の画面でコードを書いている段階と、ボード上で電気信号を出している段階を切り分けて考えられます。
コンパイル段階で出る代表的なエラーが was not declared in this scope です。
これは変数や関数の宣言位置、あるいはスペルミスが原因になることが多く、Arduino でも C/C++ のスコープの考え方に従います。
たとえば関数の中で宣言した変数は、その関数の外からは見えません。for (int i = 0; ... ) の i も、その for 文のブロック内だけです。
こうしたルールを知らないままエラー文だけ読むと難解ですが、「コンパイラは名前を探したが、その場所では見つけられなかった」と考えると意味が通ります。
Arduino は C/C++ 系の文法を土台にしている一方で、初心者向けに IDE 側が扱ってくれる部分もあります。
とはいえ、関数をどこで定義するか、変数をどこで宣言するかを意識したほうが、後で規模が大きくなったときに崩れません。
筆者は、最初から「setup() と loop() だけで完結させる」のではなく、機能ごとに関数を分けてアップロードと修正を繰り返す教え方を取っています。
そのほうが、エラーが出たときも直す場所を絞り込めます。
本記事の前提ボードと仕様
この記事で前提にするのは Arduino Uno R3 です。
ボードを固定して説明する理由は、検索結果や動画ではUno R3Uno R4などが混在しやすく、初心者が「同じ Uno だから全部同じ」と受け取ると混乱が増えるからです。
ここでは基本文法の学習に向いた定番として、Uno R3 を基準に進めます。
前提仕様は次の通りです。
| 項目 | Arduino Uno R3の仕様 |
|---|---|
| マイコン | ATmega328P |
| クロック | 16 MHz |
| デジタルI/O | 14本 |
| アナログ入力 | 6本 |
| PWM対応ピン | 6本 |
この仕様を押さえておくと、サンプルコードの意味が具体的になります。
たとえば digitalWrite() は 14 本のデジタル I/O のどれかを ON/OFF する操作、analogRead(A0) のような記述は 6 本あるアナログ入力の 1 つを読む操作です。
Uno 系の analogRead() は 0〜1023 の値を返すので、可変抵抗を回したときにシリアルモニタへ表示される数字がこの範囲で動く、と理解できます。
PWM 対応ピンが 6 本あることも、LED の明るさ制御で効いてきます。
単純な ON/OFF だけでなく、analogWrite() を使って明るさを段階的に変える題材へ進むときに、「どのピンでも同じではない」と気づけるからです。
逆に、こうしたボード固有の前提を曖昧にしたまま読むと、配線は合っているのに期待通りに動かない場面で原因を切り分けにくくなります。
この段階で口に出して言えるようにしたい要点は 1 つです。Arduino のプログラムは、setup() で準備を 1 回行い、loop() でメイン処理をずっと繰り返す。
この 1 文が腹落ちすると、サンプルコードの行を上から追うだけの読み方から、「今これは初期化なのか、反復処理なのか」を区別して読めるようになります。
変数の基礎|値を覚える箱をArduinoでどう使うか
代表的なデータ型と使いどころ
変数は、値を覚えておくための箱です。
Arduino では LED の点滅間隔、ボタンの状態、センサーの読み取り値などをいったん変数に入れてから使う場面が多くあります。
数字をそのままコードに何度も書くこともできますが、変数にして名前を付けておくと、「この数字は何のための値か」がすぐ読めます。
ここが見分けどころです。
Arduino Uno R3のような ATmega328P 系ボードでは、型ごとに使える値の範囲や小数が扱えるかどうかが決まっています。
Arduino公式 Programmingでも、Arduino は C/C++ 系の文法をベースにしていると案内されていて、変数の型を意識するとコードの読み方が安定します。
代表的な型を、まずは次の表で押さえておくと十分です。
| 型 | サイズ | 扱える値の目安 | 向いている用途 | 例 |
|---|---|---|---|---|
byte | 1 byte | 0〜255 | 小さな整数、LEDの明るさ段階、フラグ番号 | byte brightness = 128; |
bool | 1 byte相当 | true / false | ON/OFF、押された/押されていない | bool ledState = true; |
int | 2 byte | -32768〜32767 | ピン番号、analogRead(A0) の値、短い時間設定 | int sensorValue = 0; |
long | 4 byte | -2147483648〜2147483647 | 大きな整数、長い時間のカウント | long totalCount = 100000; |
float | 4 byte | 小数を含む数 | 電圧や温度など小数で扱いたい値 | float voltage = 2.75; |
初心者のうちは、迷ったらまず int で考えると進めやすいです。
たとえば analogRead(A0) の戻り値は 0〜1023 なので、int に入れておけば足ります。
LED の点滅間隔を 500 ms や 1000 ms で管理する場面でも int はよく出てきます。
一方で、小数が必要なら float、真偽だけなら bool というように、役割で分けるとコードが読みやすくなります。bool isButtonPressed と書いてあれば、そこには数値ではなく「押されたかどうか」が入ると一目でわかります。
こういう名前と型の組み合わせが、後で見返したときの助けになります。
byte は 0〜255 の範囲なので、たとえば 8 bit の値をそのまま扱いたいときに向いています。
PWM の明るさ制御で 0〜255 を使う場面と相性がいい型です。
反対に、long は大きな整数向けです。
この記事の段階では出番は多くありませんが、長い時間のカウントや大きな計測値を扱うときに選択肢に入ってきます。
TIP
int は便利ですが、Uno R3 では 2 byte です。
大きな数を扱うつもりで int を選ぶと、途中で収まりきらなくなることがあります。
短い間隔やセンサー値なら int、もっと大きな整数なら long と覚えると整理しやすくなります。
宣言・初期化・代入・参照
変数の基本操作は、宣言・初期化・代入・参照の4つです。
言葉だけだと硬く見えますが、やっていることは「箱を作る」「最初の値を入れる」「あとで値を入れ直す」「その値を使う」の順番です。
まず宣言は、型と名前を書いて変数を作ることです。
int interval;
bool ledState;
float voltage;
この時点では「こういう箱を使います」と決めただけです。そこへ最初の値を入れるのが初期化です。
int interval = 1000;
bool ledState = false;
float voltage = 0.0;
Arduino のサンプルを書き換えるとき、最初から値が決まっているなら宣言と初期化を同時に書く形がよく使われます。
コードの読み手にも意図が伝わりやすく、どこからその値が始まるのかも追いやすくなります。
次に代入です。これは、すでにある変数へ新しい値を入れ直す操作です。
interval = 500;
ledState = true;
センサーの値を読む処理では、この代入が何度も発生します。loop() のたびに新しい値で上書きされる、という見方をすると理解が進みます。
参照は、変数に入っている値を使うことです。
delay(interval);
digitalWrite(LED_BUILTIN, ledState);
delay(1000); と直接書く代わりに delay(interval); とすると、待ち時間を 1 か所で管理できます。
筆者はワークショップでこの書き換えをよくやりますが、1000 を interval に変えただけで、受講者が「もっと速くしたい」「少し遅くしたい」と自分から試す回数が増えます。
数字の意味が見えるので、コードを触る心理的な壁が下がるんですよね。
変数名は、意味がわかる英語にしておくのがおすすめです。
たとえば x や a よりも、blinkInterval、sensorValue、buttonState のほうが役割が伝わります。
Arduino のコードでは、先頭を小文字にして単語の区切りを大文字にするキャメルケースがよく使われます。
int blinkInterval = 1000;
int sensorValue = 0;
bool isLedOn = false;
この命名にそろえると、長い名前でも読みやすくなります。
逆に int aaa = 0; のような名前は、数日後に見返したときに意味を思い出せません。
電子工作では配線もコードも同時に追うので、変数名のわかりやすさがそのまま作業の速さにつながります。
変数をどこに書くかも見逃せないポイントです。
関数の外に書けば複数の関数から使え、関数の中に書けばその関数の中だけで使えます。
Arduino公式 Scopeでもこの有効範囲が整理されています。
初心者がよく出会う was not declared in this scope は、「その場所ではその変数が見えていない」という意味のエラーです。
スペルミスだけでなく、宣言した位置が離れすぎていることでも起こります。
具体例: 点滅間隔とanalogRead(A0)
ここでは、変数があると何が変わるかを、LED の点滅とセンサー値の読み取りで見ていきます。
どちらも Arduino 入門で頻出の題材なので、変数の役割がつかみやすい場面です。
まずは、内蔵 LED の点滅間隔を変数にした例です。LED_BUILTIN は Uno R3 では基板上の L LED を指すので、外付け LED なしで試せます。
int interval = 1000;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(interval);
digitalWrite(LED_BUILTIN, LOW);
delay(interval);
}
このコードでは、点滅間隔の数字を interval にまとめています。delay(1000); を2回書くより、変更したい数字が1か所に集まるのが利点です。interval = 300; に変えれば速くなり、interval = 1500; に変えればゆっくり点滅します。
サンプルを改造するときに迷いにくいのは、こうした「数字の管理場所」がはっきりしているからです。
次は、アナログ入力 A0 の値を変数へ入れて、シリアルモニタに表示する例です。analogRead(A0) は Uno R3 では 0〜1023 の整数を返します。
int sensorValue = 0;
void setup() {
Serial.begin(9600);
}
void loop() {
sensorValue = analogRead(A0);
Serial.println(sensorValue);
delay(200);
}
ここでは sensorValue が、その瞬間のセンサー値を覚える箱になっています。
たとえば A0 に可変抵抗をつないでつまみを回すと、シリアルモニタの数字が上下します。
筆者のワークショップでも、この画面を見ながらつまみを回すと、「回路の変化が数字になって返ってくる」という感覚が一気につかめる場面が多いです。
Serial.println(sensorValue); としているので、1 行ごとに現在値が表示されます。Serial.print() は改行なし、Serial.println() は改行ありです。
連続した値を見るときは println() のほうが流れを追いやすくなります。
なお、ログを増やしすぎると処理が詰まりやすくなるので、デバッグの段階では必要な値だけを出すほうが流れを読み取りやすくなります。
この2つの例に共通しているのは、数字を直接ばらまかず、意味のある名前の変数に集めていることです。interval は点滅間隔、sensorValue はセンサー値と、名前だけで役割が見えます。
変数を使うと「数字を1か所で管理する」形になるので、動作確認と改造の往復がぐっと進めやすくなります。
変数のスコープ|グローバル変数とローカル変数の違い
グローバル/ローカル/ブロックスコープ
変数は「どこで宣言したか」で見える範囲が決まります。
ここがポイントです。
Arduino のコードでは、関数の外で宣言した変数はグローバル変数、setup() や loop() など関数の中で宣言した変数はローカル変数として扱われます。
Arduino公式 Scope(https://www.arduino.cc/reference/en/language/variables/variable-scope-qualifiers/scope/でも、この「見える範囲」がスコープとして整理されています)。
たとえば、LED のピン番号を setup() と loop() の両方で使いたいなら、関数の外に置くと意図がはっきりします。
int ledPin = 13;
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
digitalWrite(ledPin, HIGH);
delay(1000);
digitalWrite(ledPin, LOW);
delay(1000);
}
この ledPin はスケッチ全体から見えるので、複数の関数で共有できます。ピン番号や状態フラグのように、処理のあちこちで参照する値に向いています。
一方で、関数の中だけで使う計算用の変数までグローバルにすると、あとからコードを追うときに「この値はどこで変わったのか」が散らばります。
たとえばセンサー値を一時的に読むだけなら、loop() の中に閉じ込めたほうが役割が明確です。
void loop() {
int sensorValue = analogRead(A0);
Serial.println(sensorValue);
delay(200);
}
この sensorValue は loop() の中だけで有効です。
関数の外からは見えません。
こうして必要な場所にだけ変数を置くと、コードの責任範囲が自然に分かれます。
もうひとつ見落とされやすいのが、for 文の中で宣言した変数です。for (int i = 0; i < 3; i++) の i は、その for ブロックの中でしか使えません。
void loop() {
for (int i = 0; i < 3; i++) {
digitalWrite(LED_BUILTIN, HIGH);
delay(200);
digitalWrite(LED_BUILTIN, LOW);
delay(200);
}
// ここで i を使うことはできません
}
この i はブロックスコープです。
波括弧 {} の外へ出た時点で見えなくなります。
初心者の方が「さっきまで使えていたのに、次の行でエラーになる」と感じる場面の多くがここです。
筆者の講座では、この手の修正に入る前に、紙に setup()、loop()、for の箱を書いて、変数名をどの箱に置くかを先に整理します。
コードだけをにらんで直すよりも、見える範囲を図にしたほうが頭の中の混線が減ります。
実際、このやり方に変えてから、同じスコープエラーを繰り返す受講者が目に見えて減りました。
使い分けの感覚としては、ピン番号や複数関数で共有する状態はグローバル、その場限りの計算や一時的な読み取り値はローカル、ループ回数を数えるだけなら for 内変数、と分けると整理しやすくなります。
比較表で理解を定着
文章だけだと混ざりやすいので、宣言場所と有効範囲を表で並べます。Arduino のスケッチでは、この3つを区別できるだけでコードの見通しが大きく変わります。
| 項目 | グローバル変数 | ローカル変数 | for文内変数 |
|---|---|---|---|
| 宣言場所 | 関数の外 | 関数の中 | for の初期化部 |
| 有効範囲 | スケッチ全体 | その関数内 | その for 文ブロック内 |
| 向いている用途 | 複数関数で共有したいピン番号や状態 | 一時的な計算、関数専用の処理 | カウンタ変数 |
| 注意点 | 増やしすぎると追跡が散らばる | 関数外から参照できない | ループ外で使うとエラーになる |
たとえば LED_BUILTIN や buttonPin のような「接続先を表す名前」は、setup() で pinMode() にも使い、loop() で digitalWrite() や digitalRead() にも使うことが多いので、グローバルに置くと筋が通ります。
逆に、analogRead(A0) の結果を一度表示するだけなら、その都度 loop() の中で受けるほうが変数の寿命が短く、読み手にも意図が伝わります。
for 文のカウンタも独立して考えると腑に落ちます。
3回だけ LED を点滅させる処理で i を使うなら、その i は「3回数える役割」しかありません。
ループの外で再利用する前提がないので、for の中だけに閉じておくほうが安全です。
役割が終わった変数を外へ持ち出さないことで、あとから別の i や count と混同しにくくなります。
エラーwas not declared in this scopeの切り分け
'xxx' was not declared in this scope は、Arduino IDE の検証時に出る代表的なコンパイルエラーです。
意味は単純で、「その場所では、その名前が宣言されていない」です。
厄介なのは原因が1つではないことですが、切り分けの順番を決めると詰まりません。
まず疑うのは、宣言した場所の外から読んでいないかです。典型例として、setup() の中で ledPin を宣言し、loop() で使おうとすると失敗します。
void setup() {
int ledPin = 13;
pinMode(ledPin, OUTPUT);
}
void loop() {
digitalWrite(ledPin, HIGH);
delay(1000);
digitalWrite(ledPin, LOW);
delay(1000);
}
このコードでは ledPin は setup() のローカル変数なので、loop() からは見えません。
そのため、loop() 側で ledPin was not declared in this scope が出ます。
修正は、ledPin をグローバルに移すだけです。
int ledPin = 13;
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
digitalWrite(ledPin, HIGH);
delay(1000);
digitalWrite(ledPin, LOW);
delay(1000);
digitalWrite(ledPin, HIGH);
delay(1000);
この形なら `setup()` と `loop()` の両方から参照できます。ハンズオンでは、まず「どこで宣言したか」を探し、その次に「どこで使っているか」を線で結ぶつもりで見ると、原因がほぼ見えてきます。
次に多いのが**スペル違い**です。`sensorValue` と書いたつもりが、どこかで `senserValue` になっていると、コンパイラは別の名前として扱います。Arduino の関数名でも同じで、`digitalWrite` を `digitalwrite` と書けば別物です。英字の大文字小文字も区別されるので、宣言行と使用行を一文字ずつ照らし合わせる視点が効きます。
もうひとつは、**波括弧 `{}` の範囲外へ出てしまっているケース**です。`if` や `for` の中で作った変数を、ブロックの外で使うとエラーになります。
```cpp
void loop() {
if (true) {
int value = analogRead(A0);
Serial.println(value);
}
Serial.println(value);
}
この value は if ブロックの中だけで有効です。
外の Serial.println(value); では見えません。
こういうときは、変数を外へ出してから代入する形に直します。
void loop() {
int value = 0;
if (true) {
value = analogRead(A0);
Serial.println(value);
}
Serial.println(value);
}
切り分けの順番を短くまとめると、次の3点でほとんど説明できます。
- その変数は、使っている場所より前で宣言されているか。
- 宣言した波括弧
{}の内側で使っているか。 - 名前のつづりと大文字小文字が一致しているか
このエラーは一見すると難しそうですが、実際には「見える範囲」と「名前の一致」を確認する作業です。
スコープの感覚が固まると、コンパイルエラーの文章そのものが読めるようになり、修正の手が止まりにくくなります。
関数の基礎|setup()とloop()以外の関数も作れる
関数の構成要素
Arduino のスケッチでは setup() と loop() が最初に出てきますが、処理を増やしていくと、この2つだけで全体を抱えるのはすぐに苦しくなります。
そこで使うのが自分で作る関数です。
長いコードを役割ごとに切り分けると、読む順番がはっきりして、修正箇所も見つけやすくなります。
筆者のワークショップでも、loop() の10行を3つの関数に分けた瞬間、受講者が「何を直せばいいか」を自力で判断できる場面がよくあります。loop() が全部入りの作業場ではなく、各処理に指示を出す司令塔に変わるからです。
関数は、基本的に戻り値、関数名、引数、関数本体で構成されます。たとえば次の形です。
int add(int a, int b) {
int result = a + b;
return result;
}
この int は戻り値で、「この関数は整数を返します」という宣言です。add は関数名、int a, int b は引数で、外から渡される材料です。
波括弧 {} の中が関数本体で、return によって呼び出し元へ値を返します。
呼び出す側はこう書きます。
int total = add(3, 5);
この1行を実行すると、いったん add(3, 5) の処理へ移り、計算が終わると戻ってきて、その結果が total に入ります。
初心者向けには「処理が一時的に別の箱へ飛んで、終わったら元の場所へ戻る」と捉えると十分です。
C++ の基礎ではこの出入りをスタックの動きとして説明しますが、Arduino 入門の段階では「呼び出す」「処理する」「戻る」の流れが見えていれば先へ進めます。
関数を分けるときのコツは、名前を見ただけで役割が想像できるかです。doTask() や process() のような曖昧な名前より、readButton()、blinkNTimes()、calculateAverage() のほうが、コードの意図がその場で読めます。Arduino公式 Function Declaration でも、関数は処理をまとまりとして定義できる要素として扱われています。
ここが見えてくると、1行ずつ追う読み方から、「この関数は入力」「この関数は表示」と役割単位で追う読み方に変わります。
void関数の例: 点灯・消灯・点滅
まず押さえたいのが、値を返さない関数です。
これは戻り値に void を使います。
LED を点ける、消す、数回点滅させる、といった「動作そのもの」が目的の処理では void がよく出てきます。
void turnLedOn() {
digitalWrite(LED_BUILTIN, HIGH);
}
void turnLedOff() {
digitalWrite(LED_BUILTIN, LOW);
}
void blinkNTimes(int n) {
for (int i = 0; i < n; i++) {
turnLedOn();
delay(500);
turnLedOff();
delay(500);
}
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
blinkNTimes(3);
delay(1000);
}
この例では turnLedOn() と turnLedOff() が基本動作、blinkNTimes(int n) がそれらを組み合わせた点滅動作です。loop() は「3回点滅して1秒待つ」としか書いていないので、メインの流れが一目で読めます。delay(1000) は 1,000 ミリ秒、つまり 1 秒です。
ここで見てほしいのは、関数の中から別の関数を呼べることです。blinkNTimes() の中で turnLedOn() と turnLedOff() を呼び出しているので、細かい処理を部品のように積み重ねられます。
もし点灯時間を変えたくなったら blinkNTimes() を見ればよく、LED の ON/OFF の出し方を変えたくなったら turnLedOn() と turnLedOff() を直せば済みます。
修正範囲が役割ごとに分かれるので、コード全体を崩さずに手を入れられます。
TIP
void は「何も返さない」という意味です。LED の点灯のように、結果を変数へ入れる必要がない処理では void を選ぶと意図がはっきりします。
for (int i = 0; i < n; i++) の i が for 文の中だけで使われている点も、前のスコープの話ときれいにつながります。
点滅回数を数える役割しかない変数を外へ出さないことで、関数の外側に余計な情報が漏れません。
関数は処理を分けるだけでなく、変数の見える範囲も整理してくれます。
値を返す関数の例とプロトタイプの話
次は値を返す関数です。
入力を読んで結果を返す、計算して答えを返す、といった処理ではこちらが主役になります。
たとえばボタンの押下状態を 0 と 1 で返す関数は、次のように書けます。
const int buttonPin = 2;
int readButton() {
if (digitalRead(buttonPin) == LOW) {
return 1;
} else {
return 0;
}
void setup() {
pinMode(buttonPin, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
int buttonState = readButton();
if (buttonState == 1) {
digitalWrite(LED_BUILTIN, HIGH);
} else {
digitalWrite(LED_BUILTIN, LOW);
}
ここでは pinMode(buttonPin, INPUT_PULLUP); を使っているので、未押下が HIGH、押下が LOW です。
そのため readButton() の中では LOW を検出したときに 1 を返しています。loop() 側は「ボタン状態を読む」「値で分岐する」という読み方になり、入力処理の中身を毎回見に行かずに済みます。
計算結果を返す関数も同じ考え方です。
float average3(float a, float b, float c) {
return (a + b + c) / 3.0;
}
void setup() {
Serial.begin(9600);
}
void loop() {
float avg = average3(2.0, 4.0, 6.0);
Serial.println(avg);
delay(1000);
}
average3() は 3つの値を受け取り、その平均を float で返します。
こうした関数を作っておくと、計算式があちこちに散らばりません。
同じ式を何度も書かずに済むので、式を直す場所も1か所にまとまります。
関数の定義位置についても触れておきたいところです。
Arduino では、1つのスケッチ内なら関数定義の位置に比較的寛容で、IDE 側が自動的に扱ってくれる場面があります。
そのため、初心者のうちは「loop() の下に関数を書いても動いた」という経験をしがちです。
ただし、C++ の基本としてはプロトタイプ宣言があります。
これは「この名前の関数が後で出てきます」と先に知らせる宣言です。
int readButton();
float average3(float a, float b, float c);
const int buttonPin = 2;
void setup() {
pinMode(buttonPin, INPUT_PULLUP);
}
void loop() {
int state = readButton();
float avg = average3(1.0, 2.0, 3.0);
}
int readButton() {
if (digitalRead(buttonPin) == LOW) {
return 1;
} else {
return 0;
}
float average3(float a, float b, float c) {
return (a + b + c) / 3.0;
}
先頭の int readButton(); や float average3(float a, float b, float c); がプロトタイプです。
Arduino では普段あまり意識しなくても進められますが、複数ファイルに分ける場面や、少し複雑なコードへ進んだときにこの考え方を知っていると詰まりません。
前のセクションで触れた 'xxx' was not declared in this scope にもつながる部分で、関数名が見えていないと同じ系統のエラーになります。
役割ごとに関数へ分けると、loop() は処理を全部書き込む場所ではなく、「何を順番に実行するか」を示す場所になります。
入力、判断、出力をそれぞれ関数へ分離すると、スケッチ全体の流れが見通せるようになり、修正も拡張も迷いにくくなります。
制御構文の基礎|if/else・switch・for・whileの使い分け
使い分け表
関数に役割を分けられるようになると、その次に必要になるのが「どの制御構文を選ぶか」です。
Arduino のコードは、LED を点ける、ボタンの状態で分ける、決まった回数だけ繰り返す、といった小さな判断の積み重ねでできています。
ここが曖昧なままだと、書けてはいても読みにくいコードになりがちです。
Arduino公式 Programming(https://docs.arduino.cc/programming/でも、こうした基本要素を組み合わせてスケッチを構成していく考え方が案内されています)。
まずは、電子工作でよく出る場面に当てはめて整理しておくと頭の中がすっきりします。
| 構文 | 主な役割 | Arduinoでの典型例 | 向いている場面 | 注意したい点 |
|---|---|---|---|---|
if / else | 条件で処理を分ける | ボタンが押されたらLED点灯、離したら消灯 | 条件が少ないとき | 条件が増えると入れ子になりやすい |
switch | 値ごとに多分岐する | モード番号 0, 1, 2 で動作切替 | 1つの値で動作を分けるとき | break; を忘れると次の case まで実行される |
for | 回数が決まった繰り返し | LEDを3回だけ点滅 | 繰り返し回数が最初から決まっているとき | カウンタ変数の範囲をループ外で使えない |
while | 条件が続く間だけ繰り返す | ボタンを押している間だけ点滅 | 終わるタイミングが条件次第のとき | 条件が変わらないと抜けられない |
ここが。回数が決まっているなら for、条件次第なら while、少数条件は if、値ごとの多分岐は switch と口に出せる状態になると、サンプルコードの見え方が変わってきます。
ボタンが押されたら点灯
まずはもっとも基本になる if / else です。ボタンが押されたらLEDを点灯し、離したら消灯する、という動きは条件分岐の最初の練習にぴったりです。
const int buttonPin = 2;
const int ledPin = LED_BUILTIN;
void setup() {
pinMode(buttonPin, INPUT_PULLUP);
pinMode(ledPin, OUTPUT);
}
void loop() {
int buttonState = digitalRead(buttonPin);
if (buttonState == LOW) {
digitalWrite(ledPin, HIGH);
} else {
digitalWrite(ledPin, LOW);
}
この例では INPUT_PULLUP を使っているので、配線はボタンの片側をデジタル入力ピン、もう片側を GND につなぐ形になります。
未押下は HIGH、押したときだけ LOW を読むので、最初は条件が逆に見えて戸惑う人が多いところです。
前のセクションの関数例でも出てきた考え方ですが、Arduino ではこの「押したら LOW」を早めに体に入れておくと、ボタン処理で止まりにくくなります。
筆者がワークショップでよく見るのも、この条件の逆転です。if (buttonState == HIGH) と書いて「押していないのに点灯する」という動きになり、配線ミスだと思って悩み込む場面がよくあります。INPUT_PULLUP は外付け抵抗なしで組めるので便利ですが、その代わり読み取る値の意味が反転する、とセットで覚えておくと混乱が減ります。
3回点滅
LED を決まった回数だけ点滅させたいなら for が向いています。
たとえば「3回だけ点滅して止まる」という処理は、回数が最初から決まっているので while より for のほうが意図がはっきり伝わります。
const int ledPin = LED_BUILTIN;
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
for (int i = 0; i < 3; i++) {
digitalWrite(ledPin, HIGH);
delay(500);
digitalWrite(ledPin, LOW);
delay(500);
}
delay(1000);
}
delay(1000) は 1,000ミリ秒、つまり1秒待つという意味です。このコードでは、LED が 3回点滅したあとに1秒止まり、また 3回点滅します。
for 文は for (初期化; 条件; 増減) の形で書きますが、初心者が見落としやすいのは int i の有効範囲です。
ここで宣言した i は for の中だけで使える変数で、ループの外では参照できません。
前のスコープの話とつながる部分で、for の外で i を使うと 'i' was not declared in this scope のようなエラーになります。
このように、点滅回数が 3回、5回、10回のように先に決まっているなら for が自然です。
「何回やるか」をコードの1行目で読めるので、後から見返したときにも意図を取り違えません。
押している間だけ点滅
今度は「ボタンを押している間だけLEDを点滅し続ける」動きです。
これは終了タイミングが固定回数ではなく、ボタンの状態で決まります。
こういう場面では while が登場します。
const int buttonPin = 2;
const int ledPin = LED_BUILTIN;
void setup() {
pinMode(buttonPin, INPUT_PULLUP);
pinMode(ledPin, OUTPUT);
}
void loop() {
while (digitalRead(buttonPin) == LOW) {
digitalWrite(ledPin, HIGH);
delay(200);
digitalWrite(ledPin, LOW);
delay(200);
}
digitalWrite(ledPin, LOW);
}
このコードは、ボタンが押されている間だけ while の中を回り続けます。ボタンを離して HIGH になった瞬間に while を抜け、LED を消灯します。
while は便利ですが、ここで初心者が一度は引っかかるのが「抜けられないループ」です。
条件が変わらない書き方をすると、その場所に居座ってしまい、loop() 全体の流れが止まったように見えます。
たとえばセンサー値が変化しない条件式を書いたり、while (true) のままにしたりすると、他の処理に戻れません。
電子工作の挙動で考えると、while は「その条件が続く間は、この動作に専念する」という命令です。
ボタン長押し中だけ点滅、一定条件の間だけ待機、といった場面にはよく合いますが、他の処理も同時に回したい段階では設計を見直す余地が出てきます。
TIP
for は回数を数えるループ、while は状態を見続けるループ、と捉えると区別しやすくなります。
LED の点滅でも、「3回だけ」なら for、「押している間だけ」なら while です。
モード分岐
ボタン操作で「消灯」「点滅」「長点灯」のようにモードを切り替える場面では、if / else if でも書けますが、モード番号のような1つの値で分岐するなら switch のほうが見通しがよくなります。
まずは else if の形です。
const int ledPin = LED_BUILTIN;
int mode = 0;
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
if (mode == 0) {
digitalWrite(ledPin, LOW);
} else if (mode == 1) {
digitalWrite(ledPin, HIGH);
delay(300);
digitalWrite(ledPin, LOW);
delay(300);
} else if (mode == 2) {
digitalWrite(ledPin, HIGH);
} else {
digitalWrite(ledPin, LOW);
}
同じ内容を switch で書くと、こうなります。
const int ledPin = LED_BUILTIN;
int mode = 0;
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
switch (mode) {
case 0:
digitalWrite(ledPin, LOW);
break;
case 1:
digitalWrite(ledPin, HIGH);
delay(300);
digitalWrite(ledPin, LOW);
delay(300);
break;
case 2:
digitalWrite(ledPin, HIGH);
break;
default:
digitalWrite(ledPin, LOW);
break;
}
mode が 0 なら消灯、1 なら点滅、2 なら長点灯です。default はどの case にも当てはまらなかったときの処理で、予期しない値が入った場合の逃げ道になります。
筆者の体感では、ボタンでモードを切り替える課題に switch を使うと、if の入れ子で迷子になる問題がきれいに解消します。if を何段も重ねると、「今どの条件の中にいるのか」を追うだけで疲れてしまいますが、switch なら mode の値ごとに処理が横並びになるので、0番、1番、2番の動作を一目で見比べられます。
ワークショップでも、モード切替の課題は switch に書き換えた途端に読めるようになった受講者が多くいました。
ひとつだけ見逃せないのが break; です。
これを忘れると、たとえば case 1: の処理が終わったあと、そのまま case 2: に流れ込みます。
モード分岐で意図しない動作になったときは、まず break; の有無を見ると原因に早くたどり着けます。
サンプルコードでまとめる|変数・関数・制御構文を1つのスケッチに入れる
想定ボードと配線
ここではArduino Uno R3を前提に、基板上の内蔵LED(L)と外付けボタン1個だけで、変数・関数・制御構文をひとまとめにしたスケッチを作ります。
内蔵LEDは LED_BUILTIN を使えばピン番号を直書きせずに扱えますが、今回は変数の役割を見せるために ledPin に 13 を入れて使います。
ボタンは buttonPin に接続し、pinMode(buttonPin, INPUT_PULLUP); で内部プルアップを有効にします。
Arduino公式 Programming(https://docs.arduino.cc/programming/でも案内されている通り、Arduinoのスケッチは setup() と loop() を中心に組み立てますが、長くなってきたら関数へ分けると流れを追いやすくなります)。
配線はシンプルです。
ボタンの片側をデジタル入力ピン 2 番へ、もう片側を GND へつなぎます。INPUT_PULLUP を使うので外付け抵抗は不要です。
この配線では、押していないときが HIGH、押したときが LOW になります。
初心者の段階ではここが直感と逆に見えますが、コード側で「LOW なら押された」と統一して読むと混乱が減ります。
配線はシンプルです。
ボタンの片側をデジタル入力ピン 2 番へ、もう片側を GND へつなぎます。INPUT_PULLUP を使うので外付け抵抗は不要です。
公開時には、サイト内の関連入門記事(例: Arduino入門、はんだごての選び方など)への内部リンクを最低2本追加してください(編集段階で該当記事が増えたらリンクを挿入してください)。
コード全文
// 関数プロトタイプ void blinkNTimes(int count, int waitMs); bool readButton(int pin); // 関数プロトタイプ void blinkNTimes(int count, int waitMs); bool readButton(int pin);
// グローバル変数 const int ledPin = 13; const int buttonPin = 2; int interval = 200; // 点滅間隔(ミリ秒) int mode = 0; // 0: 消灯, 1: 3回点滅, 2: 長押し中点灯
void setup() { pinMode(ledPin, OUTPUT); pinMode(buttonPin, INPUT_PULLUP);
Serial.begin(9600); Serial.println("start"); }
void loop() { // ボタン状態を関数呼び出しで取得 bool pressed = readButton(buttonPin);
// デバッグ表示 Serial.print("button="); Serial.print(pressed ? "PRESSED" : "RELEASED"); Serial.print(" / mode="); Serial.print(mode); Serial.print(" / interval="); Serial.println(interval);
// if文: 押されたらモードを進める if (pressed) { mode++; if (mode > 2) { mode = 0; }
Serial.print("mode changed to ");
Serial.println(mode);
// ボタンを離すまで待つ
while (readButton(buttonPin)) {
Serial.println("waiting release...");
delay(20);
}
delay(50); // 簡単なチャタリング対策
}
// switch文: モードごとに動作を分岐 switch (mode) { case 0: Serial.println("case 0: LED OFF"); digitalWrite(ledPin, LOW); delay(1000); break;
case 1:
Serial.println("case 1: blink 3 times");
blinkNTimes(3, interval); // 関数呼び出し
delay(500);
break;
case 2:
Serial.println("case 2: hold to keep LED ON");
// while文: ボタンが押されるまで待つ
while (!readButton(buttonPin)) {
Serial.println("waiting for press in mode 2...");
digitalWrite(ledPin, LOW);
delay(50);
}
// 押されている間だけ点灯
while (readButton(buttonPin)) {
digitalWrite(ledPin, HIGH);
Serial.println("button held, LED ON");
delay(50);
}
digitalWrite(ledPin, LOW);
break;
default:
Serial.println("default: reset mode");
mode = 0;
break;
} } // loop の閉じ
// void関数の例 // count回だけLEDを点滅させる void blinkNTimes(int count, int waitMs) { Serial.print("blinkNTimes called: count="); Serial.print(count); Serial.print(", waitMs="); Serial.println(waitMs);
// for文: 回数が決まっている繰り返し for (int i = 0; i < count; i++) { Serial.print("blink #"); Serial.println(i + 1);
digitalWrite(ledPin, HIGH);
delay(waitMs);
digitalWrite(ledPin, LOW);
delay(waitMs);
}
// 値を返す関数の例 // trueならボタン押下中、falseなら未押下 bool readButton(int pin) { int raw = digitalRead(pin);
Serial.print("readButton raw="); Serial.println(raw);
if (raw == LOW) { return true; } else { return false; }
コードに入れるなら、たとえば loop() の先頭で次のように書けます。
int sensorValue = analogRead(A0);
interval = map(sensorValue, 0, 1023, 50, 500);
中央付近の 512 なら、おおよそ 275 ミリ秒前後になります。
つまみを回した結果がLEDの速さにそのまま出るので、変数が「ただの箱」ではなく、挙動を決める部品のように見えてきます。
講座でもこの段階に入ると、受講者の手が止まりにくくなります。
配線を1本足しただけで振る舞いが変わるので、コードと回路がつながって見えるからです。
ボタン長押しで for の回数を変える改造も相性がいいです。
たとえば、押していた時間を数えて blinkNTimes() の count に渡せば、短押しでは3回、長押しでは5回という形に広げられます。
ここでも関数に分けてある利点が出ます。
点滅処理が blinkNTimes() に閉じているので、回数を変えたいときは引数の値を調整するだけで済みます。loop() の中に点滅処理がべったり書かれていると、変更箇所を探すだけで疲れてしまいます。
デバッグでは Serial.print() が役立ちます。
今回のコードでは、ボタン状態、現在のモード、どの case に入ったか、関数が呼び出されたかを細かく出しています。
反応しないときにLEDだけ眺めていると、「押せていないのか」「分岐に入っていないのか」「関数は呼ばれているのか」が切り分けられません。
シリアルモニタに文字を出しておくと、止まっている場所が見えます。
TIP
'xxx' was not declared in this scope というエラーが出たときは、スペルミス、変数の宣言場所、波括弧の閉じ忘れを順に見ると原因を絞れます。for (int i = 0; ... ) の i はその for の外では使えない、という点も見落としやすいところです。
初心者がつまずきやすいのは、関数の中と外で使える変数の境界が曖昧になる場面です。ledPin や buttonPin のように複数の関数で共有したいものはグローバル変数に置き、raw や pressed のようにその場だけで使う値はローカル変数に置くと、追跡範囲が狭くなります。
ログを見ながら「どの関数に入り、どの変数が変わったか」を追っていくと、長いスケッチでも迷子になりにくくなります。
よくあるエラーとつまずきポイント
配線・設定の見直しポイント
LEDが点灯しない、ボタンを押しても反応しない、シリアルモニタに何も出ない。
こういうときは、いきなりコード全体を疑うより、まず配線とIDEの設定から順番に見たほうが原因が早く見つかります。
筆者の講座でも、最初の不調の多くはここで止まります。
もっとも多いのがピン番号ミスです。
たとえばLEDをD12に挿したのに、コードでは int ledPin = 13; のままになっているパターンです。
Arduino Uno R3にはデジタルピンが複数あるので、ブレッドボード上で1列ずれただけでも別の信号線になります。
内蔵LEDを使うなら LED_BUILTIN を使う手もありますが、外付けLEDでは「実際に刺した場所」と「コードで指定した番号」が一致しているかを紙に書いて追うほうが確実です。
次に見落としやすいのがGND未接続です。
LED回路でもボタン回路でも、GNDへ戻る経路がないと期待した動作になりません。
ボタン入力ではとくに、片側を入力ピン、もう片側をGNDへつなぐ構成を取り、pinMode(pin, INPUT_PULLUP); を使う形が定番です。
この場合は未押下でHIGH、押下でLOWになります。
Arduino公式 ProgrammingやArduino公式 Getting Startedの流れを見ても、配線とコードの対応を意識することが最初の壁になりやすいとわかります。
ボタンまわりでは極性や配線の向きの間違いもよく出ます。
タクトスイッチは見た目が単純でも、内部で同じ列がつながっている向きがあります。
ブレッドボードの溝をまたいで差したつもりが、同じ側だけで閉じていて、押しても回路が変化していないことがあります。INPUT_PULLUP を使っているのに「押したらHIGHになる」と思い込んで条件式を逆に書くのも典型例です。if (digitalRead(buttonPin) == LOW) が押下判定になる、というところを固定して考えると混乱が減ります。
IDE側ではCOMポートやボード選択ミスも頻出です。
正しいスケッチでも、接続先が違えば書き込みに失敗しますし、シリアルモニタも意図したボードを見ていません。
書き込みできないときは、配線をいじる前に「ボードがArduino Unoになっているか」「接続したCOMポートを選べているか」を先に切り分けると、無駄なやり直しが減ります。
ハード側の症状に見えて、実際はIDE設定だけだったというケースは珍しくありません。
コンパイルエラーの典型例
コンパイルエラーは一見むずかしそうに見えますが、初心者の段階では原因の顔ぶれがだいたい決まっています。
まず疑いたいのはセミコロン忘れです。
たとえば int ledPin = 13 の行末に ; がないだけで、その次の行まで巻き込んで別の場所が悪いように見えることがあります。
エラーメッセージの行番号だけを見て、その行そのものを直そうとして迷子になる場面は多いです。
実際には1行上が原因ということもよくあります。
波かっこの対応ミスも定番です。if や for や関数を入れ子にしているうちに、開いた { を閉じ忘れたり、逆に1つ多く閉じたりすると、関数の外に処理が飛び出したようなエラーになります。
筆者がワークショップで毎回試す小ワザが、IDEの自動整形です。
Ctrl+Tで整形すると字下げが揃い、どこでブロックが閉じたのかが目で追いやすくなります。
受講者にもこの方法は毎回反応がよく、波かっこの位置ずれを自分で見つけられる人が増えます。
変数名や関数名のタイプミスも見逃せません。
Arduinoは大文字小文字を別物として扱うので、ledPin と ledpin は別の名前です。digitalWrite を digitalwrite と書くだけでも別名になります。
エラー文に出ている識別子を、そのままコード全体で検索して、宣言した名前と一字一句一致しているかを見るのが近道です。
条件式では = と == の取り違え も初心者が引っかかる場所です。if (mode = 1) と書くと、比較ではなく代入になってしまいます。
意図は「modeが1なら」なのに、コードは「modeに1を入れる」になっています。
コンパイルが通ることもあるので、動作が変だと感じたときほど見落としやすい。
文法エラーは派手に止まるぶん直しやすく、代入と比較の取り違えは静かに挙動を壊すので、条件式の = は特に丁寧に見たほうが流れがつかめます。
TIP
切り分けの順番を固定すると、手が止まりません。
筆者は「配線、設定、文法、スコープ、ライブラリ」の順で見ます。
LEDが点かないならまず配線とピン番号、その次にボードとCOMポート、続いてセミコロンや波かっこ、そこから変数の見える範囲を追う、という流れです。
スコープエラーの切り分け
'xxx' was not declared in this scope は、初心者が最も戸惑いやすいエラーのひとつです。
意味は単純で、「その場所から、その名前が見えていない」です。
よくあるのは3パターンあります。
1つ目は、変数を関数の中で宣言したのに、別の関数から使おうとしたケースです。
void setup() {
int ledPin = 13;
pinMode(ledPin, OUTPUT);
}
void loop() {
digitalWrite(ledPin, HIGH);
}
この ledPin は setup() の中だけで有効です。loop() からは見えないのでエラーになります。
複数の関数で共有したいなら、関数の外で宣言します。
int ledPin = 13;
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
digitalWrite(ledPin, HIGH);
}
2つ目は、波かっこの範囲の外でローカル変数を使っているケースです。
void loop() {
if (digitalRead(2) == LOW) {
int pressed = 1;
}
Serial.println(pressed);
}
pressed は if ブロックの中で生きている変数なので、外へ出た時点で消えています。修正するなら、外で宣言して中で値を入れます。
void loop() {
int pressed = 0;
if (digitalRead(2) == LOW) {
pressed = 1;
}
Serial.println(pressed);
}
3つ目は、for文の中で宣言した変数を外で使っているケースです。
void loop() {
for (int i = 0; i < 3; i++) {
Serial.println(i);
}
Serial.println(i);
}
for (int i = 0; ... ) の i は、その for 文のブロック内だけの変数です。外で使いたいなら、先に外で宣言します。
void loop() {
int i;
for (i = 0; i < 3; i++) {
Serial.println(i);
}
Serial.println(i);
}
このエラーでは、宣言位置だけでなく波かっこの対応ミスが原因でスコープが意図せず閉じていることもあります。
自分では loop() の中に書いたつもりでも、1つ前の } でブロックが閉じていて、次の行が関数外扱いになっていることがあります。
ここでも自動整形が役立ちます。
字下げが急に左へ戻る場所を見ると、「ここで閉じていたのか」と気づける場面が多いです。
変数をどこに置くか迷ったときは、前のセクションで触れた整理に戻ると崩れにくくなります。
ピン番号やモード番号のように複数の関数で共有するものはグローバル変数、その場だけの計算値はローカル変数、ループ回数の数え上げはfor文内変数、という形です。Arduino公式 Scope の説明と照らすと、エラー文が急に読みやすくなります。
Serial.print()でのデバッグ
動かない理由を勘で当てにいくと、時間だけが過ぎます。
初心者の段階では、今どこまで処理が進んだか、変数に何が入っているかを Serial.print() で見える化するのが最短です。Serial.print() や Serial.println() を使う前には Serial.begin(9600); のように初期化が必要で、シリアルモニタ側のボーレートも合わせます。
ここがずれていると文字化けしたり、何も読めなかったりします。
たとえばボタンが反応しないなら、まずは入力値そのものを出します。
void loop() {
int raw = digitalRead(2);
Serial.println(raw);
delay(200);
}
これで未押下時と押下時で値が変わるかがわかります。INPUT_PULLUP を使っているのにずっとHIGHのままなら、GNDまでつながっていないか、ボタンの差し方が違っている可能性が濃くなります。
逆に値は変わるのにLEDが点かなければ、分岐の中や出力側を追えばよいと切り分けられます。
変数の値だけでなく、処理の通過点を文字で出すのも有効です。
Serial.println("button read");
Serial.println("enter case 1");
Serial.println("blink start");
LEDの点滅だけでは「反応がない」の一言で終わってしまう場面でも、どの行まで到達したかが見えると原因の位置が絞れます。switch のどの case に入ったか、if の条件を通ったか、関数が呼ばれたかを短いログで残すだけでも、読む側の負担がぐっと減ります。
ただし、ログを出しすぎると処理が重く感じることがあります。
筆者の感覚では 9600 でも簡単な確認には足りますが、短い周期で大量に出すとシリアル出力待ちでテンポが落ちます。
頻繁に値を出したいときは内容を絞るか、必要な場面だけ出すほうが追跡しやすくなります。
それでも原因が見えないときは、最小再現コードまで削るのが効きます。
LED点滅、ボタン読み取り、モード切替、センサー読取りが全部入っている状態では、どこが悪いか判定しにくくなります。
ボタンだけ読むコード、LEDだけ点けるコードに分けると、配線とコードの責任範囲が分離されます。
1本ずつ戻していけば、壊れる瞬間の変更点が見えてきます。
もうひとつ現場で使うのが、関数の途中に一時的な return を置いて範囲を切る方法です。
たとえば loop() の前半までは動いている気がするなら、途中で return; して後半を止めます。
症状が消えれば、原因は止めた側にあります。
消えなければ前半を見ます。
大きいスケッチを半分ずつ切っていく感覚です。
これは回路とコードが複合した不調でも役に立ちます。
ログを見ながら範囲を狭めると、初心者でも「どこが怪しいか」を自分で説明できるようになります。
次に進むなら何を学ぶべきか
基礎構文がひと通り見えてきたら、次は「1つのことを順番に動かすコード」から「複数のことを同時進行で扱うコード」へ進む段階です。
筆者の講座でも、delay() を millis() に置き換えるところが第二の山になります。
ここを越えると、LEDを点滅させながらボタンを読んだり、センサー値を見ながら別の処理を回したりと、Arduinoらしい制御の感覚が一気につながります。
delayからmillis()へ
delay(1000) は意味が直感的で、最初の学習には向いています。
ただ、この書き方だと待っている間に他の仕事が止まります。
LEDを1秒ごとに点滅させるだけなら困りませんが、点滅中にもボタン入力を受けたい、センサー値の変化を取りこぼしたくない、という段階で壁に当たります。
そこで覚えたいのが millis() です。
Arduinoのリファレンスでは、millis() は起動後の経過時間をミリ秒で返す関数として説明されていて、unsigned long で扱います。
典型形は次のような差分比較です。
unsigned long previousMillis = 0;
unsigned long interval = 500;
bool ledState = false;
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ledState = !ledState;
digitalWrite(LED_BUILTIN, ledState);
}
int buttonState = digitalRead(2);
}
この形にすると、LEDの点滅待ちの最中でも digitalRead(2) を毎周回読めます。
ここが。
光らせる処理と入力処理を順番待ちにしないので、反応の鈍さが減って「同時に動いている」感覚になります。
Arduino公式の millis() リファレンスでも、if (millis() - previousMillis >= interval) という書き方が基本形として紹介されています。
最初は少し回りくどく見えるかもしれませんが、ここで止まらずに慣れておくと、ボタン、ブザー、センサー、表示器を組み合わせるときの見通しが変わります。
点滅プログラムを delay() のまま引き延ばすより、早い段階で interval 変数を持つ形へ改造したほうが、次の学習に自然につながります。
シリアル通信と入力の発展
次に取り組みたいのは、外から値を見たり、逆にPC側から指示を送ったりする流れです。Serial.begin(9600); で初期化し、Serial.print() や Serial.println() で値を出すところまでは前のセクションでも触れましたが、ここから先は「表示」だけで終わらせず、簡単なプロトコルの意識を持つと学びが深まります。
たとえば、シリアルモニタから 1 を送ったら点灯、0 を送ったら消灯、b を送ったら点滅モードに入る、と決めておくと、入力に応じて動作を切り替える練習になります。
ルールを自分で決めて、それに沿って受信文字を解釈するだけでも、通信の基本が見えてきます。
Arduinoのシリアルモニタは送受信の確認にちょうどよく、変数の監視だけでなく「PCからArduinoへ命令を送る」入り口として使えます。
入力系では、まずボタンが定番です。pinMode(buttonPin, INPUT_PULLUP); にして、ボタンをピンとGNDの間に入れる形を試すと、外付け抵抗なしで状態判定まで進めます。
このときは未押下が HIGH、押下が LOW なので、条件が反転する点でつまずく人が多いです。
講座でもここで if (digitalRead(buttonPin) == LOW) の意味が腹落ちすると、回路とコードが一続きで見えるようになります。
ボタン入力を発展させるなら、単純なON/OFFだけでなく、押すたびにモードを切り替える課題がちょうどよいです。
1回押したら点灯、2回目で点滅、3回目で消灯のようにすると、if/else だけでなく switch の使いどころも見えてきます。
ボタンは機械的に細かく揺れるので、同じ押下が複数回に見えることがあります。
ここでデバウンスの考え方に触れると、入力処理が一段実践寄りになります。
アナログ入力も次の一歩として相性がよい題材です。analogRead() は 0〜1023 の値を返すので、可変抵抗器のつまみ位置や光センサーの変化を数値で確認できます。
単純に読むだけでなく、しきい値を決めて「ある値を超えたらLEDを点ける」、数回分を平均して揺れを落ち着かせる、map() で別の範囲に変換する、と進めると一気に応用が広がります。
たとえば analogRead(A0) の値を 0〜1023 のまま扱うのではなく、LEDの点滅間隔や明るさに結びつけると、変数と制御構文が道具として働き始めます。
TIP
ArduinoのBuilt-in Examplesは、この段階の教材としてよくできています。
Blink Without DelayButtonDebounceState Change DetectionAnalogReadSerialの並びで触ると、単体の知識がつながって見えてきます。
関数分割と次のアクション
コードが長くなってきたら、次は関数分割の質を上げる段階です。
最初は setup() と loop() に全部書いても動きますが、入力判定、LED制御、モード更新を1か所に詰め込むと、少し改造しただけで読み返しがつらくなります。
そこで、処理の役割ごとに関数を切り出します。
たとえば readButton()、updateMode()、updateLed() のように分けるだけでも、どこが入力でどこが出力かがはっきりします。
さらに進むと、モードごとに runMode0()、runMode1() と分けたり、複数ファイルに分けたりする整理も見えてきます。
センサー読取りを何度も使うなら、小さなライブラリのようにまとめる発想もここから始まります。
毎回同じ処理を書くのではなく、意味のある部品として再利用するわけです。
この段階でよくあるのが、関数に分けた途端に 'xxx' was not declared in this scope が出るパターンです。
原因の多くは、ピン番号や状態変数をローカルに置いたまま別の関数から使おうとしていることです。buttonPin や ledPin、現在のモード番号のように複数の関数で共有するものは、関数の外に置くと整理しやすくなります。
逆に、その場の計算だけで終わる値はローカルのままにしておくと、変更範囲が広がりません。
次に手を動かす題材としては、Lチカを少しずつ育てるやり方が有効です。
まず interval 変数を追加して点滅間隔を外から変えられる形にします。
次に buttonPin と ledPin をグローバル変数としてまとめ、配線変更に強いスケッチにします。
その後で点灯処理や点滅処理を関数へ切り出し、if/else や for、必要なら switch を使ってモードを増やしていく流れです。
こう進めると、「変数」「関数」「制御構文」が別々の知識ではなく、1つの作品を育てるための部品として結びつきます。
ここまで来たら、サンプルコードを読む目も変わってきます。
前は長く見えたコードでも、「時間管理」「入力」「出力」「状態管理」の塊に分けて追えるようになるからです。
基礎構文を覚えた次の学びは、新しい命令を増やすことではありません。
今ある命令を、止めずに、分けて、つないで使うことです。