基礎 · 読了まで21分

Arduinoプログラミングの基本:変数・ループ・関数

Arduinoプログラミングをゼロから学びます。setupとloop、int・long・byte、オーバーフロー、const、#define、if-else、switch-case、ループ、配列、関数をLEDの例で解説します。

Arduinoプログラミングとは、Arduinoのボードに何をさせるかをC/C++の言語で伝えることです。どのLEDをいつ点灯させるか、ボタンが押されたら何をするか、センサーから届いた数値をどう解釈するかをコードに書くと、ボードはその命令を1秒間に何千回も繰り返します。うれしいことに、LEDの点滅からロボットカーまで、目にするスケッチのほとんどは同じ10個ほどの部品の組み合わせでできています。それがsetup()とloop()、変数、定数、演算子、if-else、switch-case、forループとwhileループ、配列、関数です。

このガイドでは、これらの部品を一つずつ、それぞれ実際のLEDやボタンの例とともに学びます。すべての例は同じシンプルな回路で動き、Arduino Uno向けにコンパイルして動作を確認しています。初心者が特につまずきやすいポイント、つまり変数のオーバーフロー、=と==の取り違え、breakの書き忘れ、毎回リセットされてしまうカウンターについては別に説明します。最後のセクションでは、同じ考え方がESP32でどう動くか、Raspberry Pi PicoのMicroPythonではどう書くかも紹介します。

Arduino Unoのボードとデジタルピン
Arduino Uno:この記事の例はすべて、D2ピンにつないだボタン1個と、D8〜D13ピンにつないだLED 6個を使います。D13には内蔵LEDがあるので、最初のいくつかの例では配線すら必要ありません。

Arduinoプログラミングとは?使う言語は?

ArduinoのコードはC++をベースにしていて、独立した「Arduino言語」というものはありません。ArduinoのコアがC++にpinMode()、digitalWrite()、delay()といった便利な関数を追加しています。書いたファイルはスケッチと呼ばれ、拡張子は.inoです。「検証」をクリックすると、Arduino IDEがファイルの先頭にArduino.hヘッダーを追加し、関数の宣言を自動で作成します。そしてコンパイラがコードをプロセッサが理解できる機械語に変換します。「書き込み」ボタンを押すと、その結果がUSB経由でボードのブートローダーに送られます。まだIDEをインストールしていない場合は、Arduino IDEのインストールのガイドから始めてください。

コードを書くときに守るべき基本ルールがいくつかあります。最初に覚えておけば、始めたばかりのころに出るコンパイルエラーの半分はなくなります。

ルール正しい書き方守らなかった場合
文の終わりには必ずセミコロンを付けるdigitalWrite(13, HIGH);エラー:expected ';' before '}' token
大文字と小文字は区別されるpinMode(13, OUTPUT);エラー:'pinmode' was not declared in this scope
コードのブロックは波かっこで囲むvoid loop() { ... }{ には必ず対応する } が必要
1行コメント// この行は実行されないコメントはコンパイルされず、説明のためのもの
複数行コメント/* ... */閉じ忘れると、その下がすべてコメントになる
文字列はダブルクォート、1文字はシングルクォート"Hello"と'A'クォートの種類を間違えると、値がおかしくなるかエラーになる

例で使う回路

すべての例は1つの回路で動きます。D8〜D13ピンにLEDを6個、それぞれ220 Ωの抵抗を通してつなぎ、各LEDの短い足(カソード)をGNDにつなぎます。手持ちのLEDに合う抵抗値はLED抵抗計算ツールで確認できます。ボタンは片方の足をD2に、対角にある足をGNDにつなぎます。コードでINPUT_PULLUPを使うので外付けの抵抗は不要で、ボタンを離しているときはピンがHIGH、押すとLOWになります。詳しくはArduinoに押しボタンをつなぐ方法とLEDとは?を参照してください。

部品Arduino UnoESP32Raspberry Pi Pico
LED 6個(それぞれ220 Ω、カソードはGNDへ)D8, D9, D10, D11, D12, D13GPIO 18, 19, 21, 22, 23, 25GP14, GP13, GP12, GP11, GP10, GP9
ボタン(もう片方の足はGNDへ)D2GPIO 4GP2
ボード上のLEDD13(Lと書かれたLED)GPIO 2(多くのDevKit)GP25(Pico Wでは"LED")
シリアル通信速度9600 baud115200 baudThonnyのShell
LEDを抵抗なしでつながないでください。LEDもArduinoのピンも壊れるおそれがあります。ブレッドボードにボタンを挿すとき、同じ側にある2本の足を使うと、その足は内部ですでにつながっていることがあり、ボタンを押しても反応しないように見えます。対角の足を使うのがいちばん確実です。ボードのピン番号はピン配置ツールで確認できます。

setup()とloop():すべてのスケッチの骨組み

Arduinoのスケッチには必ず2つの関数があります。setup()はボードの電源が入ったときかリセットボタンが押されたときに一度だけ実行され、ピンの設定やSerial.begin()などの準備をここに書きます。loop()はsetup()が終わったあと、ずっと繰り返し実行されます。裏側では、Arduinoのコアに見えないmain()関数があり、まずハードウェアを準備してからsetup()を一度呼び出し、終わりのないループの中でloop()を何度も呼び出しています。この2つの関数の外側、ファイルの先頭部分はグローバル領域で、どこからでも使える必要がある定数や変数はここで定義します。

C++ (Arduino IDE)
1// 1) Global alan: sabitler ve değişkenler / Global area: constants and variables
2const int LED_PIN = 13;        // dahili LED / built-in LED
3unsigned long passCount = 0;   // loop() kaç kez döndü / how many times loop() has run
4
5// 2) setup(): açılışta bir kez çalışır / runs once at power-up or reset
6void setup() {
7  pinMode(LED_PIN, OUTPUT);
8  Serial.begin(9600);
9  Serial.println("setup() bitti / setup() done");
10}
11
12// 3) loop(): sonsuza kadar tekrar eder / repeats forever
13void loop() {
14  passCount++;                       // her turda 1 artır / add 1 on every pass
15  Serial.print("Tur / Pass: ");
16  Serial.println(passCount);
17
18  digitalWrite(LED_PIN, HIGH);       // LED'i yak / LED on
19  delay(500);                        // 500 ms bekle / wait 500 ms
20  digitalWrite(LED_PIN, LOW);        // LED'i söndür / LED off
21  delay(500);
22}

コードを書き込んで、シリアルモニタを9600 baudで開いてください。「setup() done」が一度表示され、続いて1秒ごとに増えていく回数が表示されます。同じリズムで内蔵LEDも点滅します。loop()の中にある2つのdelay(500)が1回の周回をおよそ1秒に延ばしています。待ち時間もシリアル出力もないloop()なら、Unoでも1秒間に数十万回まわることがあります。シリアルモニタを使いこなすには、Arduinoのシリアルモニタのガイドも見てみてください。

変数とデータ型:int、long、float、bool、byte

変数とは、値を保存しておくメモリ上の名前付きの箱です。定義するときは、まず型を書き、次に名前、必要なら初期値を書きます。たとえばint counter = 0;のようにします。型によって、箱が何バイトを使うか、どの範囲の数値を入れられるかが決まります。Arduino UnoのRAMはわずか2 KBなので、適切な型を選ぶことが大切です。変数名は英字かアンダースコアで始め、スペースや、é、ü、çのようなアクセント付きの文字(もちろん日本語も)は使えません。

型Unoでのサイズ範囲主な用途
bool1バイトtrueまたはfalseLEDが点いているか、ボタンが押されたか
byte1バイト0 – 255ピン番号、PWMの明るさ
char1バイト-128 – 1271文字:'A'
int2バイト-32,768 – 32,767カウンター、analogRead()の結果
unsigned int2バイト0 – 65,535正の数だけ
long4バイト-2,147,483,648 – 2,147,483,647大きな整数
unsigned long4バイト0 – 4,294,967,295millis()による時間の計測
float4バイト約±3.4 × 10^38、精度は6〜7桁温度、電圧、平均値
double4バイト(Unoではfloatと同じ)floatと同じESP32では8バイト

いくつかの実践的なルールを知っておくと、多くのトラブルを避けられます。analogRead()は0〜1023の数値を返すので、intで十分です。一方、millis()はunsigned longを返します。時間をint変数に入れておくと、32.7秒で値がオーバーフローしてタイミングが狂います。Unoではfloatの計算はソフトウェアで処理されるので整数の計算より遅く、6〜7桁を超える部分は信頼できません。そのため、2つのfloatを==で比較してはいけません。どのボードでもサイズが変わらない型を使いたいときは、uint8_t、int16_t、uint32_tのように名前にサイズが入った型を使います。

オーバーフロー:変数が上限を超えるとどうなる?

型に入りきらない大きな数を入れようとしても、Arduinoはエラーを出しません。数値は車の走行距離計のように一周して戻ります。Unoのintは最大32,767で、1を足すと-32,768になります。byteは255から0に戻ります。もっと気づきにくい落とし穴は定数どうしの計算です。60 * 1000のような式は、結果をlong変数に入れる場合でも、まずintとして計算されてオーバーフローします。次のスケッチはこれらのケースをすべてシリアルモニタに表示し、loop()ではbyteのオーバーフローをLEDで見せます。

C++ (Arduino IDE)
1const int LED_PIN = 9;        // PWM destekli pin (~9) / PWM-capable pin (~9)
2byte brightness = 0;          // byte: 0-255
3
4void setup() {
5  pinMode(LED_PIN, OUTPUT);
6  Serial.begin(9600);
7
8  int a = 32767;              // Uno'da int'in en büyük değeri / largest int on an Uno
9  a = a + 1;                  // taşma: -32768 olur / overflow: becomes -32768
10  Serial.println(a);
11
12  long wrong = 60 * 1000;     // int ile hesaplanır, taşar: -5536 / computed as int, overflows: -5536
13  long right = 60 * 1000L;    // L eki hesabı long yapar: 60000 / the L suffix makes it long math: 60000
14  Serial.println(wrong);
15  Serial.println(right);
16
17  float half = 5 / 2;         // tam sayı bölmesi: 2.00 / integer division: 2.00
18  float half2 = 5 / 2.0;      // ondalıklı bölme: 2.50 / floating-point division: 2.50
19  Serial.println(half);
20  Serial.println(half2);
21}
22
23void loop() {
24  analogWrite(LED_PIN, brightness);   // parlaklığı uygula / apply the brightness
25  brightness++;                       // 255'ten sonra 0'a döner / wraps to 0 after 255
26  delay(10);
27}
コードの行Unoでの出力ESP32とPicoでの出力理由
a = 32767 + 1-3276832768intはUnoでは2バイト、ほかのボードでは4バイト
60 * 1000-553660000定数はまずintとして計算される
60 * 1000L6000060000接尾辞Lを付けるとlongで計算される
5 / 22.002.00整数どうしの割り算の結果は整数になる
5 / 2.02.502.50片方に小数点があれば、結果も小数になる

D9のLEDを見てください。明るさが0から255へゆっくり上がったあと、brightness++の行で値が256ではなく0になり、LEDが急に消えます。オーバーフローを自分の目で確かめるいちばん簡単な方法です。D9はUnoの~印が付いたPWMピンの一つなので、analogWrite()が使えます。PWMについてはPWMとは?で詳しく説明しています。millis()がおよそ49.7日で一周するのもオーバーフローですが、正しく書かれたコードなら問題は起きません。書き方はmillis()とdelay()の違いのガイドで紹介しています。

Arduino IDEで「ファイル」>「基本設定」を開き、コンパイラの警告を「もっと」か「全て」にしてください。こうしておくと、60 * 1000のようにオーバーフローする式を書いたときに、コンパイラがinteger overflow in expression(式の中で整数オーバーフロー)という警告を出し、コードがボードに届く前にバグに気づけます。

constと#define:変わらない値

ピン番号やしきい値のように、プログラムの実行中に変わらない数値に名前を付ける方法は2つあります。constで定義した定数には型があり、スコープのルールにも従います。うっかり変更しようとすると、コンパイラがassignment of read-only variable 'LED_PIN'(読み取り専用変数への代入)で止めてくれます。一方、#defineはプリプロセッサの命令で、コンパイルが始まる前にコード中のBUTTON_PINをすべて2に置き換えます。型はなく、行末にセミコロンも等号も付けません。単純な数値なら、どちらもRAMを使いません。コンパイラが値を直接コードに埋め込むからです。現在はconstが好まれ、定数名を大文字(UPPER_CASE)で書くのが一般的な習慣です。次の例では両方を使っています。ボタンを押している間、LEDが点灯します。

C++ (Arduino IDE)
1#define BUTTON_PIN 2          // önişlemci: derlemeden önce metin değişimi / preprocessor: text swap before compiling
2const int LED_PIN = 13;       // tipi olan, salt okunur sabit / typed, read-only constant
3
4void setup() {
5  pinMode(LED_PIN, OUTPUT);
6  pinMode(BUTTON_PIN, INPUT_PULLUP);   // dahili pull-up: basılınca LOW / internal pull-up: LOW when pressed
7}
8
9void loop() {
10  if (digitalRead(BUTTON_PIN) == LOW) {   // buton basılı mı? / is the button pressed?
11    digitalWrite(LED_PIN, HIGH);          // evet: LED yanar / yes: LED on
12  } else {
13    digitalWrite(LED_PIN, LOW);           // hayır: LED söner / no: LED off
14  }
15}
#define LED_PIN 13;と書くと、セミコロンが値の一部になり、pinMode(LED_PIN, OUTPUT)の行はpinMode(13;, OUTPUT)になってしまいます。コンパイラはexpected ')' before ';' token(';'の前に')'が必要)を報告し、その下のin expansion of macro 'LED_PIN'(マクロ'LED_PIN'の展開中)というメモは、問題が定数を使った場所で起きていることを示します。しかし直すべき行は#defineのほうです。#define LED_PIN = 13と書いた場合も、expected primary-expression before '=' token('='の前に式が必要)というエラーになります。

演算子:算術、比較、論理

演算子を使うと、変数で計算したり、比較したり、条件を組み合わせたりできます。普段のプロジェクトで使う演算子は、ほぼすべてこの表にあります。

種類演算子意味例
算術+ - * /四則演算x = a * 2;
剰余%割り算の余り(整数のみ)7 % 3は1
代入= += -= *= /=代入、加算して代入、減算して代入…total += 5;
インクリメント、デクリメント++ --1を足す、または1を引くi++;
比較== != < > <= >=結果はtrueかfalseif (x >= 10)
論理&& || !AND(かつ)、OR(または)、NOT(否定)if (a && !b)
条件? :短いif-elsex > 0 ? HIGH : LOW
ビット演算& | ^ ~ << >>数値のビットに対して働く1 << 3は8

特に取り違えやすいのが=と==、&と&&の2組です。=が1つなら値を代入し、2つなら比較します。&が1つなら数値のビットに対して働き、&&は両方の条件が真かどうかを調べます。AND、OR、NOTの真理値表は論理ゲートツールで試せます。ビット演算子については数の表し方とビット演算のガイドで詳しく説明しています。次の例は、ボタンが押された回数を数え、押した回数が奇数のときにLEDを点灯させます。

C++ (Arduino IDE)
1const int LED_PIN = 13;
2const int BUTTON_PIN = 2;
3
4int presses = 0;               // toplam basış sayısı / total number of presses
5bool lastPressed = false;      // önceki turdaki durum / state on the previous pass
6
7void setup() {
8  pinMode(LED_PIN, OUTPUT);
9  pinMode(BUTTON_PIN, INPUT_PULLUP);
10  Serial.begin(9600);
11}
12
13void loop() {
14  bool pressed = (digitalRead(BUTTON_PIN) == LOW);   // karşılaştırma: true ya da false / comparison: true or false
15
16  if (pressed && !lastPressed) {     // şimdi basılı VE önceden değildi: yeni basış / pressed now AND not before: new press
17    presses++;                       // 1 artır / increment by 1
18    Serial.print("Basış / Presses: ");
19    Serial.println(presses);
20    delay(50);                       // basit debounce / simple debounce
21  }
22  lastPressed = pressed;
23
24  // Kalan (%) ile tek mi çift mi? / odd or even using the remainder (%)
25  digitalWrite(LED_PIN, (presses % 2 == 1) ? HIGH : LOW);
26}

ポイントはpressed && !lastPressedという条件です。これは「今押されている、かつ、前回の周回では押されていなかった」という意味です。こうすると、ボタンを押し続けても数が増え続けることはなく、1回押すごとに1回だけ数えられます。presses % 2はその数を2で割った余りで、奇数なら1、偶数なら0になります。最後の行にある「? :」演算子が、その結果に応じてHIGHかLOWを選びます。

if、else if、else:条件で処理を分ける

ifは、かっこの中の条件が真のとき、波かっこの中のコードを実行します。条件が偽のときは、elseブロックがあればそれが実行されます。選択肢がもっと必要なときは、間にelse ifを追加します。条件は上から順に調べられ、最初に真になったブロックが実行されると、残りは飛ばされます。C++では0以外の値はすべて真として扱われるので、if (counter)は「カウンターが0でなければ」という意味になります。シンプルなif-elseの例はconstのセクションですでに見ました。今度は短押しと長押しを区別する例を書いてみましょう。

C++ (Arduino IDE)
1const int LED_PIN = 13;
2const int BUTTON_PIN = 2;
3
4void setup() {
5  pinMode(LED_PIN, OUTPUT);
6  pinMode(BUTTON_PIN, INPUT_PULLUP);
7  Serial.begin(9600);
8}
9
10void loop() {
11  if (digitalRead(BUTTON_PIN) == LOW) {             // basıldı / pressed
12    unsigned long start = millis();
13    while (digitalRead(BUTTON_PIN) == LOW) {         // bırakılana kadar bekle / wait until released
14    }
15    unsigned long held = millis() - start;          // basılı kalma süresi (ms) / hold time (ms)
16
17    if (held < 50) {
18      return;                                       // 50 ms'den kısa: parazit, yok say / under 50 ms: bounce, ignore
19    } else if (held < 1000) {
20      digitalWrite(LED_PIN, HIGH);                  // kısa basış: LED yak / short press: LED on
21      Serial.println("Kısa basış / Short press");
22    } else {
23      digitalWrite(LED_PIN, LOW);                   // uzun basış: LED söndür / long press: LED off
24      Serial.println("Uzun basış / Long press");
25    }
26  }
27}

ボタンが押された瞬間に、コードはmillis()で時刻を記録し、ボタンが離されるまでwhileループで待ってから、その間の時間を計算します。50 msより短い押下は、ボタンの機械的なチャタリングによるノイズとみなし、returnでloop()のその周回を早めに終わらせます。1秒より短い押下ならLEDを点灯し、それより長ければ消灯します。条件の順番が重要です。もしheld < 1000のチェックを先に書いていたら、30 msのノイズも「短押し」として数えられてしまいます。

よくある間違いの代表がif (mode = 2)です。この行は何も比較しておらず、modeに2を代入しています。結果が0ではないので、条件は常に真になります。もう一つ気づきにくいバグは、if (x == 5);のようにかっこの直後にセミコロンを付けてしまうことです。ifは空の文で終わり、その下のブロックは条件に関係なく毎回実行されます。

switch-case:多くの場合から選ぶ

変数の値に応じてさまざまな処理をしたいときは、長いelse ifの連続よりswitch-caseのほうが読みやすくなります。switchはかっこの中の値を各caseラベルと比べ、一致したところから実行を始めます。各caseはbreakで終わらせる必要があります。そうしないと、プログラムは次のcaseのコードへそのまま進んでしまいます。どのcaseにも一致しなければ、defaultブロックが実行されます。switchはint、byte、char、enumのような整数の型でしか使えません。String、float、または「10から20の間」のような範囲にはifが必要です。次の例では、ボタンを押すたびにランプが次のモードに切り替わります。消灯、点灯、ゆっくり点滅、速い点滅の順です。

C++ (Arduino IDE)
1const int LED_PIN = 13;
2const int BUTTON_PIN = 2;
3
4byte mode = 0;                 // 0 kapalı, 1 açık, 2 yavaş, 3 hızlı / 0 off, 1 on, 2 slow, 3 fast
5bool lastPressed = false;
6
7void setup() {
8  pinMode(LED_PIN, OUTPUT);
9  pinMode(BUTTON_PIN, INPUT_PULLUP);
10}
11
12void loop() {
13  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
14  if (pressed && !lastPressed) {
15    mode = (mode + 1) % 4;      // 0 → 1 → 2 → 3 → 0
16    delay(50);                  // basit debounce / simple debounce
17  }
18  lastPressed = pressed;
19
20  switch (mode) {
21    case 0:                                        // kapalı / off
22      digitalWrite(LED_PIN, LOW);
23      break;
24    case 1:                                        // sürekli açık / always on
25      digitalWrite(LED_PIN, HIGH);
26      break;
27    case 2:                                        // 500 ms'de bir değiş / toggle every 500 ms
28      digitalWrite(LED_PIN, (millis() / 500) % 2 ? HIGH : LOW);
29      break;
30    case 3:                                        // 100 ms'de bir değiş / toggle every 100 ms
31      digitalWrite(LED_PIN, (millis() / 100) % 2 ? HIGH : LOW);
32      break;
33    default:                                       // beklenmeyen değer: başa dön / unexpected value: reset
34      mode = 0;
35      break;
36  }
37}

mode = (mode + 1) % 4の行は、モードを0、1、2、3と進め、4になると%演算子のおかげで0に戻します。点滅にdelay()を使っていない点に注目してください。(millis() / 500) % 2という式は500 msごとに0と1を切り替えるので、LEDが点滅している間もボタンにすぐ反応します。この「モード」の考え方を発展させたものはステートマシン(状態遷移)と呼ばれます。信号機やメニューのようなプロジェクトでの使い方は、Arduinoのステートマシンのガイドで紹介しています。

caseの中で変数を定義するときは、そのcaseのコードを波かっこで囲んでください。case 1: { byte step = 3; ... break; }のように書きます。波かっこがなくてもUnoでは警告だけでコンパイルできますが、ESP32ではjump to case label(caseラベルへのジャンプ)というエラーになり、コードはまったくコンパイルできません。

for、while、do-whileループ

ループは、コードのブロックを何度も繰り返し実行します。forループのかっこには3つの部分があります。開始(int i = 0)、続ける条件(i < 6)、各周回のあとの処理(i++)です。forは繰り返す回数がわかっているときに最適です。whileは条件が真である限り続き、各周回の前に条件を調べるので、どれくらい続くか事前にわからない待ち時間に向いています。do-whileは最後に条件を調べるので、必ず1回は実行されます。ループの中では、breakでただちにループを抜け、continueで現在の周回の残りを飛ばして次の周回に進みます。loop()はもともと終わりのないループなので、その中にwhile (true)を書く必要は普通ありません。

ループ条件を調べるタイミング使いどころ例
for各周回の前繰り返す回数がわかっているときfor (int i = 0; i < 6; i++)
while各周回の前何かが起きるのを待つときwhile (digitalRead(2) == HIGH)
do-while各周回のあと少なくとも1回は実行したいときdo { ... } while (x < 10);

次の例は、6個のLEDでナイトライダー効果を作ります。setup()のforループで6本のピンを1行で出力に設定し、loop()のwhileループでボタンが押されるまで待ちます。ボタンを押している間、光がD8からD13へ走り、また戻ってきます。

C++ (Arduino IDE)
1const int FIRST_LED = 8;       // ilk LED pini / first LED pin
2const int LAST_LED = 13;       // son LED pini / last LED pin
3const int BUTTON_PIN = 2;
4
5void setup() {
6  for (int pin = FIRST_LED; pin <= LAST_LED; pin++) {
7    pinMode(pin, OUTPUT);      // 6 pini tek döngüyle çıkış yap / make 6 pins outputs with one loop
8  }
9  pinMode(BUTTON_PIN, INPUT_PULLUP);
10}
11
12void loop() {
13  while (digitalRead(BUTTON_PIN) == HIGH) {
14    // butona basılana kadar burada bekle / wait here until the button is pressed
15  }
16
17  for (int pin = FIRST_LED; pin <= LAST_LED; pin++) {      // ileri: 8 → 13 / forward: 8 → 13
18    digitalWrite(pin, HIGH);
19    delay(80);
20    digitalWrite(pin, LOW);
21  }
22  for (int pin = LAST_LED - 1; pin > FIRST_LED; pin--) {   // geri: 12 → 9 / back: 12 → 9
23    digitalWrite(pin, HIGH);
24    delay(80);
25    digitalWrite(pin, LOW);
26  }
27}

戻りのループをLAST_LED - 1から始め、ピンがFIRST_LEDより大きい間だけ回しているのは意図的です。両端のLEDが2回続けて点灯しないので、動きがなめらかに見えます。速度の調整、光が満ちていく効果やほかのパターンについては、Arduinoのナイトライダーと流れるLEDのガイドを参照してください。

for (byte i = 0; i < 256; i++)というループは終わりません。byteは最大255までしか入らないので、iは256に届く前に0に戻り、条件はずっと真のままです。同じように、配線が外れたセンサーのように決して起こらない条件を待つwhileループも、プログラムを完全に止めてしまいます。

配列:多くの値を1つの名前でまとめる

配列は、同じ型の値を複数、順番に1つの名前でまとめて持ちます。const byte LEDS[] = {8, 9, 10, 11, 12, 13};の行は、要素が6個の配列を作ります。要素には角かっこでアクセスし、番号は0から始まります。LEDS[0]は8、LEDS[5]は13です。配列の長さは定義したあとで変えられません。要素の数を手で書く代わりにsizeof(LEDS) / sizeof(LEDS[0])で計算させれば、配列に新しいピンを追加したときにループが自動で対応します。LEDS[6]のように配列の範囲外の番号を使ってもエラーにはならず、メモリ上の別の変数を読んだり壊したりしてしまいます。そのため、番号が0から「要素数より1小さい数」までに収まるように注意する必要があります。次の例は、6個のLEDを電子サイコロにします。

C++ (Arduino IDE)
1const byte LEDS[] = {8, 9, 10, 11, 12, 13};               // LED pinleri / LED pins
2const byte LED_COUNT = sizeof(LEDS) / sizeof(LEDS[0]);    // eleman sayısı: 6 / number of elements: 6
3const byte BUTTON_PIN = 2;
4
5void setup() {
6  for (byte i = 0; i < LED_COUNT; i++) {     // indeks 0'dan başlar / index starts at 0
7    pinMode(LEDS[i], OUTPUT);
8  }
9  pinMode(BUTTON_PIN, INPUT_PULLUP);
10  randomSeed(analogRead(A0));                // boştaki A0'ın gürültüsüyle tohumla / seed from noise on the unconnected A0
11}
12
13void loop() {
14  if (digitalRead(BUTTON_PIN) == LOW) {
15    byte dice = random(1, 7);                // 1 ile 6 arası / between 1 and 6
16    for (byte i = 0; i < LED_COUNT; i++) {
17      digitalWrite(LEDS[i], (i < dice) ? HIGH : LOW);   // ilk 'dice' kadar LED yanar / the first 'dice' LEDs light up
18    }
19    delay(300);                              // basılı tutarsan zar dönmeye devam eder / keeps rolling while held
20  }
21}

random(1, 7)は1から6までの数を作ります。上限の値は結果に含まれないからです。その数だけLEDが点灯します。サイコロの目が4なら、LEDS[0]からLEDS[3]までの4個のLEDが点きます。randomSeed(analogRead(A0))の行は、何もつながっていないA0ピンの電気的なノイズを初期値として使います。これがないと、ボードは電源を入れるたびに同じ数の並びを作ってしまいます。配列のもう一つの良いところは、LEDをほかのピンに移しても、最初の1行を変えるだけで済むことです。

関数を書く:コードを部品に分ける

プログラムの何か所かで同じ処理をしているなら、その処理を関数にしましょう。関数は、戻り値の型、名前、かっこの中の引数、波かっこの中の本体からできています。値を返さない関数の型はvoidで、値を返す関数はreturnで結果を返します。引数はコピーとして渡されるので、関数の中で引数を変えても、呼び出した側の変数は変わりません。Arduino IDEは.inoファイル内の関数の宣言を自動で作るので、関数をloop()の下に書くこともできます。ただし、.cppファイルやPlatformIOでは、関数を使う前に宣言しなければなりません。関数にはblinkLedやisPressedのように、何をするのかがわかる名前を付けましょう。

C++ (Arduino IDE)
1const byte LED_PIN = 13;
2const byte BUTTON_PIN = 2;
3
4// Değer döndürmeyen (void) fonksiyon / function that returns nothing (void)
5void blinkLed(byte pin, byte times, int ms) {
6  for (byte i = 0; i < times; i++) {
7    digitalWrite(pin, HIGH);
8    delay(ms);
9    digitalWrite(pin, LOW);
10    delay(ms);
11  }
12}
13
14// bool döndüren fonksiyon / function that returns a bool
15bool isPressed(byte pin) {
16  return digitalRead(pin) == LOW;      // INPUT_PULLUP: basılı = LOW / pressed = LOW
17}
18
19void setup() {
20  pinMode(LED_PIN, OUTPUT);
21  pinMode(BUTTON_PIN, INPUT_PULLUP);
22  blinkLed(LED_PIN, 3, 100);           // açılışta 3 hızlı yanıp sönme / 3 quick blinks at start-up
23}
24
25void loop() {
26  if (isPressed(BUTTON_PIN)) {
27    blinkLed(LED_PIN, 2, 250);         // basınca 2 yavaş yanıp sönme / 2 slow blinks per press
28  }
29}

blinkLed()はピン、点滅の回数、速さを引数として受け取るので、同じ関数を別のLEDや別の速さにも使い回せます。isPressed()はboolを返し、INPUT_PULLUPの「押された=LOW」という考え方を1か所にまとめています。いつかボタンの配線を逆にしても、この行を変えるだけで済みます。ただし、関数の中のdelay()はやはりプログラムを止めます。複数のことを同時にこなす関数には、millis()を使ったタイミング処理が必要です。

グローバル変数、ローカル変数、static変数

変数がどこから見えるかをスコープと呼びます。関数の外で定義したグローバル変数は、プログラムが動いている間ずっと存在し、どこからでも読んだり変えたりできます。関数や波かっこのブロックの中で定義したローカル変数は、そのブロックが実行されるたびに新しく作られ、終わると消えます。初心者がいちばんよく口にする「カウンターがずっと1のまま」という悩みは、ここから生まれます。ローカル変数の前にstaticを付けると、初期値が入るのは最初の1回だけになり、周回をまたいで値が保たれます。ただし、見えるのはやはりその関数の中だけです。

C++ (Arduino IDE)
1const byte LED_PIN = 13;
2const byte BUTTON_PIN = 2;
3
4int globalCount = 0;            // global: her yerden görünür, değerini korur / global: visible everywhere, keeps its value
5
6void setup() {
7  pinMode(LED_PIN, OUTPUT);
8  pinMode(BUTTON_PIN, INPUT_PULLUP);
9  Serial.begin(9600);
10}
11
12void loop() {
13  int localCount = 0;           // yerel: her turda yeniden 0 olur / local: back to 0 on every pass
14  static int staticCount = 0;   // static yerel: değerini korur, yalnız burada görünür / static local: keeps its value, visible only here
15
16  if (digitalRead(BUTTON_PIN) == LOW) {
17    globalCount++;
18    localCount++;
19    staticCount++;
20    Serial.print(globalCount);
21    Serial.print(' ');
22    Serial.print(localCount);
23    Serial.print(' ');
24    Serial.println(staticCount);                   // 3. basışta: 3 1 3 / on the 3rd press: 3 1 3
25    digitalWrite(LED_PIN, (staticCount % 2) ? HIGH : LOW);
26    delay(300);
27  }
28}

ボタンを3回押すと、シリアルモニタには1 1 1、2 1 2、3 1 3という行が表示されます。グローバルとstaticのカウンターは増え続けますが、ローカルのカウンターはloop()の周回ごとに0からやり直すので、いつも1のままです。グローバル変数と同じ名前のローカル変数を定義すると、ローカル変数がグローバル変数を隠してしまい、その関数の中ではグローバル変数にまったく触れなくなります。割り込み関数(ISR)で変更されるグローバル変数は、さらにvolatileとして宣言する必要があります。詳しくはArduinoの割り込みのガイドで説明しています。

変数はできるだけローカルにしておきましょう。1つの関数だけが使うカウンターにはstaticを、複数の関数で共有する値にはグローバル変数を使います。コンパイル後にIDEが表示するGlobal variables use … bytes of dynamic memory(グローバル変数が使っている動的メモリ)の行を見ると、グローバル変数がRAMをどれだけ使っているかがわかります。Unoでは、これが75パーセントを超えると動作が不安定になり始めることがあります。対策はArduinoのメモリ不足のガイドで紹介しています。

ESP32でのArduinoプログラミング:同じ言語、違うサイズ

Arduino IDEでESP32をプログラミングするときも、同じC++の言語と同じ命令を使いますが、いくつか違いがあります。ESP32は32ビットのプロセッサなのでintは4バイトになり、-2,147,483,648から2,147,483,647までの値を入れられます。そのため、オーバーフローの例の32767 + 1は32768になります。doubleは8バイト、analogRead()は0〜4095の値を返し、ピンは3.3 Vで動作します。Serial.printf()で書式付きの表示もできます。random()は標準でハードウェアの乱数生成器を使うので、randomSeed()の行は必要ありません。次のプログラムは、この記事で学んだことをすべて組み合わせています。配列、関数、switch-case、そしてモードを切り替えるボタンです。ボードの全体的な特徴についてはESP32とは?を参照してください。

C++ (Arduino IDE)
1// ESP32: aynı dil, farklı pinler / ESP32: same language, different pins
2const byte LEDS[] = {18, 19, 21, 22, 23, 25};            // güvenli GPIO'lar / safe GPIOs
3const byte LED_COUNT = sizeof(LEDS) / sizeof(LEDS[0]);
4const byte BUTTON_PIN = 4;                                // diğer bacağı GND'ye / other leg to GND
5
6byte mode = 0;                  // 0 kapalı, 1 yürüyen ışık, 2 hepsi yanıp söner / 0 off, 1 chaser, 2 all blink
7bool lastPressed = false;
8
9void setAll(bool on) {
10  for (byte i = 0; i < LED_COUNT; i++) {
11    digitalWrite(LEDS[i], on ? HIGH : LOW);
12  }
13}
14
15void setup() {
16  Serial.begin(115200);
17  for (byte i = 0; i < LED_COUNT; i++) {
18    pinMode(LEDS[i], OUTPUT);
19  }
20  pinMode(BUTTON_PIN, INPUT_PULLUP);
21  Serial.printf("int = %d bayt / bytes\n", (int)sizeof(int));   // ESP32'de 4 / 4 on the ESP32
22
23  int a = 32767;
24  a = a + 1;
25  Serial.println(a);                                          // ESP32'de 32768, taşma yok / 32768 on the ESP32, no overflow
26}
27
28void loop() {
29  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
30  if (pressed && !lastPressed) {
31    mode = (mode + 1) % 3;      // 0 → 1 → 2 → 0
32    delay(50);
33  }
34  lastPressed = pressed;
35
36  switch (mode) {
37    case 0:
38      setAll(false);
39      break;
40    case 1: {                                                 // case içinde değişken için süslü parantez / braces for a variable inside a case
41      byte step = (millis() / 120) % LED_COUNT;               // sıradaki LED / LED whose turn it is
42      for (byte i = 0; i < LED_COUNT; i++) {
43        digitalWrite(LEDS[i], (i == step) ? HIGH : LOW);
44      }
45      break;
46    }
47    case 2:
48      setAll((millis() / 300) % 2);                           // 300 ms'de bir hepsi / all of them every 300 ms
49      break;
50  }
51}

このプログラムをUnoで動かすには、LEDS配列を{8, 9, 10, 11, 12, 13}に変え、BUTTON_PINを2にして、Unoが対応していないSerial.printf()の行をSerial.print()に置き換えるだけです。ピン番号を先頭で定数や配列として定義しておく利点は、まさにここにあります。また、case 1のブロックが波かっこで囲まれていることにも注目してください。中でstep変数を定義しているので、ESP32ではこの波かっこが必須です。

Raspberry Pi Pico:同じ考え方をMicroPythonで

Raspberry Pi Picoは、Arduino IDEにRP2040のボードパッケージを追加すればC++でプログラミングできます。その場合、この記事のコードはピン番号を変えるだけで動き、intはやはり4バイトです。ただし、Picoのいちばん一般的な使い方はMicroPythonです。Pythonでは変数の型を書かず、ブロックは波かっこではなくインデントで表し、行末にセミコロンは付けません。また、整数はオーバーフローせず、必要なだけ大きくなります。次のコードは、電子サイコロの例のMicroPython版です。セットアップについてはRaspberry Pi Picoとは?を、言語の詳細についてはMicroPythonの基本を参照してください。

MicroPython
1from machine import Pin
2import time, random
3
4leds = [Pin(n, Pin.OUT) for n in (14, 13, 12, 11, 10, 9)]   # liste = dizi / list = array
5button = Pin(2, Pin.IN, Pin.PULL_UP)                        # basılınca 0 / reads 0 when pressed
6
7def show(count):                                            # fonksiyon / function
8    for i, led in enumerate(leds):                          # for döngüsü / for loop
9        led.value(1 if i < count else 0)
10
11show(0)                                                     # setup() karşılığı: bir kez çalışır / like setup(): runs once
12while True:                                                 # loop() karşılığı / like loop()
13    if button.value() == 0:                                 # buton basılı mı? / is the button pressed?
14        show(random.randint(1, 6))                          # 1-6 arası zar at / roll a number from 1 to 6
15        time.sleep_ms(300)
概念Arduino(C++)MicroPython
一度だけ実行される部分setup()while True:の行より前のコード
ずっと繰り返される部分loop()while True:
変数int x = 5;x = 5
定数const int PIN = 2;PIN = const(2)(from micropython import constが必要)
条件分岐if / else if / elseif / elif / else
多分岐switch-caseif / elifの連続
回数を数えるループfor (int i = 0; i < 6; i++)for i in range(6):
配列byte a[] = {1, 2, 3};a = [1, 2, 3]
関数void blink(int pin) { ... }(関数の定義)def blink(pin):(本体はインデントで書く)
論理&& || !and or not
コメント// コメント# コメント

この基本で作れるプロジェクトのアイデア

  • 信号機:const値、delay()、ifを使って、歩行者用ボタン付きの横断歩道を作ります。手順はArduinoで信号機で紹介しています。
  • ナイトライダーと流れるLED:forループと配列で、何十種類もの光のパターンを作ります。
  • 電子サイコロ:配列、random()、関数を使って、LED 6個のサイコロを7セグメントディスプレイに移します。詳しくは7セグメントディスプレイのガイドを参照してください。
  • 反応速度テスト:ランダムなタイミングでLEDを点け、ボタンを押すまでの速さをunsigned longとmillis()で計ります。
  • マルチモードランプ:switch-caseを使い、1つのボタンで消灯、点灯、呼吸(ゆっくり明滅)、フラッシュのモードを切り替えます。カラー版を作るならRGB LEDの使い方を見てみてください。
  • 暗証番号ロック:押されたキーを配列に集め、保存してある暗証番号と比べます。詳しくはキーパッドのガイドで説明しています。

よくある間違いと直し方

間違い症状直し方
比較に=を使うifのブロックが常に実行される比較には==を使う
#defineの行に;や=を付けるエラー:expected ')' before ';' token#define LED_PIN 13と書く
命令の大文字・小文字を間違えるエラー:'pinmode' was not declared in this scopepinModeのように大文字と小文字に注意する
変数名にアクセント付きの文字や日本語を使うエラー:stray '\303' in program名前には半角英字、数字、_だけを使う
60 * 1000をintで計算する結果が-5536になる60 * 1000Lか60000ULと書く
millis()をintに入れる32秒後にタイミングが狂うunsigned longを使う
整数の割り算5 / 2が2になる5 / 2.0と書くか、floatに変換する
caseの最後のbreakを忘れる次のcaseまで実行されるcaseはすべてbreakで終える
配列の範囲外に書き込むおかしな値が出る、フリーズする番号を0から「要素数 - 1」までに収める
カウンターをローカル変数にするカウンターがずっと1のままグローバル変数かstatic変数にする
コンパイルエラーが出たら、最初のエラー行だけを読んでそれを直してください。後に続く行のエラーの多くは、最初のエラーの副作用です。メッセージの意味はArduinoのコンパイルエラーのガイドで確認できます。手元にボードがなければ、Tinkercadのシミュレーションでコードを試すこともできます。

これらの部品を覚えたら、次はよく使う命令を知るステップです。Arduinoの命令一覧では、pinMode()からattachInterrupt()まで例付きで紹介しています。BlueGraysアプリには、LEDの点滅、ボタンでLEDを操作、ナイトライダー、信号機、サイコロといったプロジェクトが、回路図とすぐに使えるコード付きでステップごとに用意されているので、ここで学んだ構文をすべて実際の回路で試せます。

よくある質問

Arduinoで使うプログラミング言語は?

ArduinoのコードはC++をベースにしています。ArduinoのコアがpinMode()、digitalWrite()、delay()といった便利な関数を言語に追加しており、コードは拡張子.inoのファイルに書きます。ESP32やRaspberry Pi Picoのようなボードは、好みに応じてMicroPythonでもプログラミングできます。

Arduinoプログラミングは何から学べばいい?

まずArduino IDEをインストールし、内蔵LEDを点滅させるBlinkの例を書き込みます。次にsetup()とloop()、変数、if-else、ループ、配列、関数の順に、それぞれLEDやボタンの回路で試してみてください。新しいテーマを小さなプロジェクトで使うと、読むだけよりずっとしっかり身につきます。

Arduinoのintとlongの違いは?

Arduino Unoでは、intは2バイトで-32,768から32,767までの数を入れられ、longは4バイトで約±21億まで扱えます。この上限を超えた数はエラーなしで一周してしまうので、millis()のような大きな値にはlongかunsigned longを使います。ESP32とPicoでは、intも4バイトです。

Arduinoのconstと#defineの違いは?

constで定義した定数には型があり、スコープのルールに従い、変更しようとするとコンパイラがエラーを報告します。#defineはコンパイル前に行われる文字の置き換えで、型がなく、末尾にセミコロンを付けると思わぬエラーの原因になります。単純な数値ならどちらもRAMを使いませんが、より安全なので普通はconstが好まれます。

Arduinoのforループとwhileループの違いは?

forループは開始、条件、処理の更新を1行にまとめたもので、6個のLEDを順番に点灯させるときのように、繰り返す回数がわかっている場合に使います。whileループは条件だけを調べて、それが真である限り動き続けるので、ボタンが押されるのを待つような長さのわからない場面に向いています。

ArduinoはPythonでプログラミングできる?

Arduino Unoのような8ビットのボードではPythonを動かせないので、C++でプログラミングします。一方、ESP32やRaspberry Pi PicoはMicroPythonでプログラミングできます。また、パソコン上で動くPythonのプログラムが、シリアルポート経由でUnoとやり取りして間接的に制御することもできます。

全プロジェクトを見る
LEDの点滅
#1初級

LEDの点滅

電子工作の第一歩は、1個のLEDを点けたり消したりすることです。Arduinoで短い間隔で点灯と消灯を繰り返すと、LEDが素早く点滅します。ボードが光を制御する仕組みを最もシンプルな形で確かめ、これから取り組むプロジェクトの基礎を身につけます。

4.6(188)
1127
ボタンでLEDを点灯(押している間のみ)
#9初級

ボタンでLEDを点灯(押している間のみ)

呼び鈴のように、押している間は点灯し、離すと消灯します。Arduinoはボタンの状態を常に読み取り、押されている間はLEDを点灯させ、指を離した瞬間に消灯させます。このシンプルな回路で、ボードが外部からの操作を検知する仕組みを学べます。

4.8(113)
229
ボタンでLEDの点灯・消灯を切り替え
#10初級

ボタンでLEDの点灯・消灯を切り替え

部屋の照明スイッチのように、ボタンを1回押すと点灯し、もう1回押すと消灯します。Arduinoはボタンが押されるたびにLEDの状態を反転させます。ボタン内部の小さな接点の振動で誤動作しないよう、短い待ち時間も設けます。1つのボタンでオン・オフを切り替える、シンプルな仕組みです。

4.8(100)
167
Knight Rider風LED
#6中級

Knight Rider風LED

Knight Riderでおなじみの、光が左右に往復する動きです。6個のLEDの上を光の点が端から端へ進み、折り返します。Arduinoが各LEDを10分の1秒ずつ点灯させ、端に着くたびに向きを変えて往復し続けます。装飾用の照明や警告灯作りの第一歩にぴったりです。

4.8(106)
202
信号機
#3初級

信号機

毎日道路で見かける信号機の小さな模型です。赤が2秒点灯し、黄が一瞬点灯した後、緑が2秒点灯します。その後、再び黄を経て赤に戻り、この動作を繰り返します。3個のLEDを順番に点けたり消したりしながら、タイミングの組み立て方を学びます。

4.8(131)
397
ESP32でボタンによるLED制御(押している間だけ点灯)
#104初級

ESP32でボタンによるLED制御(押している間だけ点灯)

ボタンを押している間はLEDが点灯し、離すと消灯します。ESP32のデジタル入力ピンと内蔵プルアップ抵抗の使い方を学びます。INPUT_PULLUPモードを使うため、外付け抵抗は不要です。

4.6(36)
55
Raspberry Pi PicoでボタンによるLEDの点灯・消灯切り替え
#156初級

Raspberry Pi PicoでボタンによるLEDの点灯・消灯切り替え

ボタンとLEDを使って、さらに一歩進みます!基本のボタンプロジェクトと違い、ボタンを押し続ける必要はありません。1回押して離すとLEDが点灯し、もう1回押して離すと消灯します(トグル動作)。回路は同じ(LEDはGP14、ボタンはGP2)ですが、コードは大きく変わります。エッジ検出(1→0に変わる瞬間の検出)、状態の記録、チャタリング対策(機械的な接点の振動を除去)を学びます。MicroPythonのled.toggle()なら1行で状態を切り替えられます。モード切り替え、IRQ、ダブルクリックなど、ボタン操作を使うあらゆるプロジェクトの基礎になります。

0.0(0)
9
サイコロのプロジェクト
#79中級
プレミアム

サイコロのプロジェクト

ボードゲームでサイコロをなくす心配がなくなります。ボタンを押すたびに、7セグメントディスプレイに1から6までのランダムな数字が表示されます。1回押すと結果が1つだけ出て、コンピューターの画面にも表示されます。友達とのゲームで使える、手作りのデジタルサイコロです。

5.0(48)
67