ガイド · 読了まで14分

Arduinoで信号機:回路、コード、歩行者用ボタン、交差点

Arduinoで信号機を作ります。LED 3個と220 Ωの配線、delayとmillisのコード、歩行者用ボタン、2方向の交差点、Tinkercad、ESP32とPicoの版を紹介します。

Arduinoの信号機は電子工作の定番の最初のプロジェクトです。赤、黄、緑の3つのLEDが、本物の信号機と同じように順番に点いたり消えたりします。必要なのはArduino Uno、LED 3個、220 Ωの抵抗3本、ジャンパーワイヤー数本だけで、最初の動く版は30分で完成します。それでも、この簡単なプロジェクトから学べることはたくさんあります。デジタル出力、LEDと抵抗の計算、タイミング、ボタンの読み取り、そしてコードを最もすっきり整理する方法であるステートマシンが、この数個のLEDに詰まっています。

このガイドでは、まず本物の信号機がどんな順番で、どのくらいの時間で切り替わるのかを確認し、回路を組んでdelay()を使った最も簡単なコードを書きます。次に歩行者用の押しボタンを追加し、同じロジックをmillis()とステートマシンで止まらない形に書き直し、2方向の交差点を制御します。最後の章では、何もインストールせずにTinkercadでプロジェクトを試す方法、ESP32とRaspberry Pi Pico(MicroPython)の版、プロジェクトについて学校のレポートを書くコツを紹介します。

Arduinoの信号機に使う赤、黄、緑の5 mm LED
5 mmのLED:信号機には赤、黄、緑が1個ずつあれば十分です。歩行者用信号と交差点用に、もう何個か用意しておきましょう。

本物の信号機のしくみ

信号機は、それぞれの交通の流れに順番を割り当てるタイマーです。イギリス、ドイツ、トルコでは、サイクルは赤 → 赤+黄 → 青 → 黄 → 赤です。赤と黄が同時に点くのは「もうすぐ青になるので準備して、でもまだ進まないで」という意味で、黄だけのときは「赤に変わるので止まる準備をして」という意味です。アメリカ、フランス、イタリア、スペインには赤+黄の段階がなく、赤からすぐ青に変わります(日本も同じで、赤 → 青 → 黄 → 赤です)。黄の時間もでたらめではありません。ドイツの規則では、制限速度50 km/hの道路で黄を3秒、60 km/hで4秒、70 km/hで5秒にしています。黄になったときに止まるには近すぎるドライバーが、安全に交差点を抜けられる必要があるからです。

段階点灯するランプ意味このガイドでの時間
1赤止まれ5秒
2赤+黄もうすぐ青、準備して1秒
3青進め5秒
4黄もうすぐ赤、止まれ3秒
交差点では全方向が赤交差点を空けるための安全な間隔1秒

部品リストと220 Ωの抵抗の計算

部品数量メモ
Arduino Uno(またはNano)1ESP32やRaspberry Pi Picoでも可
5 mmのLED:赤、黄、緑各1歩行者用信号に赤と緑を+1ずつ、交差点用に+1セット
220 Ωの抵抗3LED 1個に1本。全部の版で8本
押しボタン(6×6 mm)1歩行者用の版のみ
ブレッドボード1ハーフサイズ(400穴)で十分
ジャンパーワイヤー10〜20オス-オス
USBケーブル1コードの書き込みとボードの給電用

LEDは自分で電流を制限しません。5 Vに直接つないだLEDは、LEDとピンが耐えられる以上の電流を流します。そのため、LEDには必ず直列に抵抗を入れます。値はオームの法則で求められます:R = (電源電圧 − LEDの順方向電圧) / 電流。赤のLEDなら、(5 V − 2.0 V) / 0.015 A ≈ 200 Ωです。最も近い標準値の220 Ωにすると、電流は約13.6 mAになります。LEDは十分明るく光り、Arduinoのピンの推奨値20 mAも下回ります。220 Ωの抵抗は、4本帯の部品では赤-赤-茶-金、5本帯の部品では赤-赤-黒-黒-茶です。確実に確かめるには抵抗のカラーコードツールを、ほかの色や電圧にはLEDの抵抗の計算ツールを使いましょう。

LEDの色順方向電圧(目安)5 Vでの電流(220 Ω)3.3 Vでの電流(220 Ω)
赤1.8〜2.2 V≈ 13.6 mA≈ 5.9 mA
黄1.9〜2.2 V≈ 13.2 mA≈ 5.5 mA
緑(黄緑)2.0〜2.4 V≈ 13.2 mA≈ 5.5 mA
緑(明るい純緑)2.8〜3.4 V≈ 9.1 mA≈ 1.4 mA(とても暗い)

回路の配線とピンの表

各LEDの長い足(アノード、+)は抵抗を通してArduinoのデジタルピンへ、短い足(カソード、−)はGNDへつなぎます。直列になっていれば、抵抗はアノード側でもカソード側でもかまいません。ブレッドボードでは、ArduinoのGNDピンを青(−)のレールにつなぎ、3つのLEDの短い足をすべてそのレールに集めるのが一番すっきりします。下の表は、このガイドのすべてのスケッチで使うピンです。最初のスケッチでは最初の3行だけを使います。ボードのピン配置はピン配置ツールで確認できます。

役割Arduino UnoESP32Raspberry Pi Pico
車両用の赤(方向A)D10GPIO 5GP13
車両用の黄(方向A)D9GPIO 18GP12
車両用の青(方向A)D8GPIO 19GP11
歩行者用の赤D7GPIO 21GP15
歩行者用の青D6GPIO 22GP14
歩行者用ボタン(もう一方の足はGNDへ)D2GPIO 23GP4
方向Bの赤 / 黄 / 青D5 / D4 / D3GPIO 25 / 26 / 27GP8 / GP7 / GP6
LEDのカソードとボタンGNDGNDGND
LEDを逆につないでも点かないだけで壊れませんが、抵抗を忘れるとLEDとピンに負担がかかり、LEDがすぐに暗くなってしまうことがあります。Arduino Unoでは1ピンあたり最大20 mA(絶対最大定格は40 mA)を目安にし、全ピンの合計を200 mA未満に保ちましょう。LEDがまったく点かないときは、まずGNDを確認します。ブレッドボードの青のレールがArduinoのGNDピンにつながっていないと、回路が閉じません。

ステップ1:delay()を使った最も簡単な信号機のコード

C++ (Arduino IDE)
1// Arduino trafik lambası: delay() ile en basit sürüm / simplest version with delay()
2const int KIRMIZI = 10;  // kırmızı LED / red LED
3const int SARI = 9;      // sarı LED / yellow LED
4const int YESIL = 8;     // yeşil LED / green LED
5
6// Üç LED'i tek satırda ayarla / set all three LEDs in one line
7void lambalar(int k, int s, int y) {
8  digitalWrite(KIRMIZI, k);
9  digitalWrite(SARI, s);
10  digitalWrite(YESIL, y);
11}
12
13void setup() {
14  pinMode(KIRMIZI, OUTPUT);
15  pinMode(SARI, OUTPUT);
16  pinMode(YESIL, OUTPUT);
17}
18
19void loop() {
20  lambalar(HIGH, LOW, LOW);   // DUR: kırmızı / STOP: red
21  delay(5000);
22  lambalar(HIGH, HIGH, LOW);  // HAZIRLAN: kırmızı + sarı / GET READY: red + yellow
23  delay(1000);
24  lambalar(LOW, LOW, HIGH);   // GEÇ: yeşil / GO: green
25  delay(5000);
26  lambalar(LOW, HIGH, LOW);   // YAVAŞLA: sarı / SLOW DOWN: yellow
27  delay(3000);
28}

このスケッチは、setup()で3つのピンを出力に設定し、loop()の中で4つの段階を順番に実行します。digitalWriteの3行を何度も書く代わりに、lambalar()(「ランプ」)という小さな関数を使うので、各段階が1行で読め、間違いも見つけやすくなります。書き込むには、「ツール」メニューでボードにArduino Unoと正しいポートを選び、「書き込み」をクリックします。書き込みに失敗したら、Arduinoにコードが書き込めないときのチェックリストを確認しましょう。タイミングを変えるには、delay()の中のミリ秒の値を書き換えるだけです。5000は5秒です。

ステップ2:歩行者用押しボタン付きの信号機

街で見かける押しボタン式の横断歩道は、車を青のままにしておき、誰かがボタンを押したときだけ歩行者に順番を回します。この版では、車両用の信号に、2つのLED(赤と青)の歩行者用信号とD2のボタンを追加します。ボタンの片方の足をD2に、対角にある足をGNDにつなぎます。コードでINPUT_PULLUPを使うので外付けの抵抗は不要で、ボタンを押している間ピンはLOWになります。4本足のボタンの内部の接続とチャタリング対策については、Arduinoに押しボタンをつなぐ方法で詳しく読めます。

6×6 mmの4本足の押しボタン
6×6 mmの押しボタン:4本の足は内部で2本ずつつながっています。対角の2本の足を使えば、配線は必ず正しくなります。
C++ (Arduino IDE)
1// Yaya butonlu trafik lambası (delay ile) / pedestrian push-button light (with delay)
2const int KIRMIZI = 10, SARI = 9, YESIL = 8;  // araç lambası / vehicle light
3const int YAYA_KIRMIZI = 7, YAYA_YESIL = 6;   // yaya lambası / pedestrian light
4const int BUTON = 2;                          // diğer ucu GND / other leg to GND
5
6void setup() {
7  pinMode(KIRMIZI, OUTPUT);
8  pinMode(SARI, OUTPUT);
9  pinMode(YESIL, OUTPUT);
10  pinMode(YAYA_KIRMIZI, OUTPUT);
11  pinMode(YAYA_YESIL, OUTPUT);
12  pinMode(BUTON, INPUT_PULLUP);               // basılınca LOW okunur / reads LOW when pressed
13  digitalWrite(YESIL, HIGH);                  // araçlar geçer / cars go
14  digitalWrite(YAYA_KIRMIZI, HIGH);           // yayalar bekler / pedestrians wait
15}
16
17void loop() {
18  if (digitalRead(BUTON) == LOW) {            // yaya butona bastı / pedestrian pressed
19    delay(2000);                              // araçlara kısa süre tanı / short grace time for cars
20    digitalWrite(YESIL, LOW);
21    digitalWrite(SARI, HIGH);                 // sarı / yellow
22    delay(3000);
23    digitalWrite(SARI, LOW);
24    digitalWrite(KIRMIZI, HIGH);              // araçlar durur / cars stop
25    delay(1000);                              // tüm kırmızı güvenlik payı / all-red safety gap
26    digitalWrite(YAYA_KIRMIZI, LOW);
27    digitalWrite(YAYA_YESIL, HIGH);           // yayalar geçer / pedestrians cross
28    delay(6000);
29    for (int i = 0; i < 6; i++) {             // yaya yeşili 3 kez yanıp söner / green man flashes 3 times
30      digitalWrite(YAYA_YESIL, i % 2 == 0 ? LOW : HIGH);
31      delay(500);
32    }
33    digitalWrite(YAYA_YESIL, LOW);
34    digitalWrite(YAYA_KIRMIZI, HIGH);
35    delay(1000);                              // tüm kırmızı / all red
36    digitalWrite(SARI, HIGH);                 // kırmızı + sarı / red + yellow
37    delay(1000);
38    digitalWrite(KIRMIZI, LOW);
39    digitalWrite(SARI, LOW);
40    digitalWrite(YESIL, HIGH);                // araçlar yeniden geçer / cars go again
41    delay(8000);                              // en kısa yeşil süresi / minimum green time
42  }
43}

このスケッチは動きますが、大きな弱点があります。ボタンが押されると、Arduinoは約25秒間delay()の行で止まったままになります。その間に2回目に押されても気づかず、シリアルポートから何も読めず、ディスプレイも更新できません。最後の8秒の待ち時間に押されたボタンは完全に失われます。歩行者が押して離しても何も起きず、もう一度押さなければなりません。音やカウントダウンを加えると、この構造はさらに扱いにくくなります。解決策は、待つのをやめて時間を測ることです。

ステップ3:millis()とステートマシンで止まらないコード

ステートマシンとは、システムが常にちょうど1つの状態にあり、決まった条件を満たすと次の状態に移る設計のことです。信号機はその完璧な例で、「車が青」「車が黄」「歩行者が横断中」がそれぞれ1つの状態です。コードはもうdelay()で待ちません。loop()は毎回、今どの状態にいるか、その状態になってどのくらい経ったか、という2つのことだけを確認します。経過時間はmillis() - durumBaslangic(状態が始まった時刻)で測り、時間が来たら次に進みます。millis()が初めてなら、先にArduinoのmillis()とdelay()を読んでください。コードを書く前に状態を表にしておくと、作業がずっと楽になります。

状態車両用信号歩行者用信号時間次の状態
ARAC_YESIL青赤最低8秒、要求が来るまでARAC_SARI
ARAC_SARI黄赤3秒YAYA_BEKLE
YAYA_BEKLE赤赤1秒YAYA_GECER
YAYA_GECER赤青6秒YAYA_FLAS
YAYA_FLAS赤青の点滅3秒YAYA_BITTI
YAYA_BITTI赤赤1秒KIRMIZI_SARI
KIRMIZI_SARI赤+黄赤1秒ARAC_YESIL
C++ (Arduino IDE)
1// Yaya butonlu trafik lambası: millis() + durum makinesi / pedestrian light: millis() + state machine
2const int KIRMIZI = 10, SARI = 9, YESIL = 8;  // araç lambası / vehicle light
3const int YAYA_KIRMIZI = 7, YAYA_YESIL = 6;   // yaya lambası / pedestrian light
4const int BUTON = 2;                          // diğer ucu GND / other leg to GND
5
6enum Durum { ARAC_YESIL, ARAC_SARI, YAYA_BEKLE, YAYA_GECER, YAYA_FLAS, YAYA_BITTI, KIRMIZI_SARI };
7const char* const ISIM[] = { "ARAC_YESIL", "ARAC_SARI", "YAYA_BEKLE", "YAYA_GECER",
8                             "YAYA_FLAS", "YAYA_BITTI", "KIRMIZI_SARI" };
9
10// Süreler (ms) / durations (ms)
11const unsigned long MIN_YESIL = 8000;   // araçlara en az yeşil / minimum green for cars
12const unsigned long SARI_SURE = 3000;   // sarı / yellow
13const unsigned long GUVENLIK = 1000;    // tüm kırmızı / all red
14const unsigned long YAYA_SURE = 6000;   // yaya yeşili / pedestrian green
15const unsigned long FLAS_SURE = 3000;   // yanıp sönen yaya yeşili / flashing green man
16const unsigned long KS_SURE = 1000;     // kırmızı + sarı / red + yellow
17
18Durum durum = ARAC_YESIL;
19unsigned long durumBaslangic = 0;       // durumun başladığı an / when the state started
20bool yayaTalebi = false;                // butona basıldı mı? / was the button pressed?
21
22void aracLamba(bool k, bool s, bool y) {
23  digitalWrite(KIRMIZI, k);
24  digitalWrite(SARI, s);
25  digitalWrite(YESIL, y);
26}
27
28void yayaLamba(bool k, bool y) {
29  digitalWrite(YAYA_KIRMIZI, k);
30  digitalWrite(YAYA_YESIL, y);
31}
32
33// Yeni duruma geç: lambaları ayarla, saati başlat / enter a state: set the lamps, restart the clock
34void gec(Durum yeni) {
35  durum = yeni;
36  durumBaslangic = millis();
37  switch (yeni) {
38    case ARAC_YESIL:   aracLamba(0, 0, 1); yayaLamba(1, 0); break;
39    case ARAC_SARI:    aracLamba(0, 1, 0); yayaLamba(1, 0); break;
40    case YAYA_BEKLE:   aracLamba(1, 0, 0); yayaLamba(1, 0); break;
41    case YAYA_GECER:   aracLamba(1, 0, 0); yayaLamba(0, 1); yayaTalebi = false; break;
42    case YAYA_FLAS:    aracLamba(1, 0, 0); yayaLamba(0, 1); break;
43    case YAYA_BITTI:   aracLamba(1, 0, 0); yayaLamba(1, 0); break;
44    case KIRMIZI_SARI: aracLamba(1, 1, 0); yayaLamba(1, 0); break;
45  }
46  Serial.println(ISIM[yeni]);           // durum günlüğü / state log
47}
48
49void setup() {
50  Serial.begin(9600);
51  pinMode(KIRMIZI, OUTPUT);
52  pinMode(SARI, OUTPUT);
53  pinMode(YESIL, OUTPUT);
54  pinMode(YAYA_KIRMIZI, OUTPUT);
55  pinMode(YAYA_YESIL, OUTPUT);
56  pinMode(BUTON, INPUT_PULLUP);
57  gec(ARAC_YESIL);
58}
59
60void loop() {
61  unsigned long gecen = millis() - durumBaslangic;  // bu durumda geçen süre / time spent in this state
62
63  // yaya yeşili yanarken basış yok sayılır / presses during the green man are ignored
64  if (digitalRead(BUTON) == LOW && durum != YAYA_GECER) {
65    yayaTalebi = true;                  // talebi hatırla / remember the request
66  }
67
68  switch (durum) {
69    case ARAC_YESIL:
70      if (yayaTalebi && gecen >= MIN_YESIL) gec(ARAC_SARI);
71      break;
72    case ARAC_SARI:
73      if (gecen >= SARI_SURE) gec(YAYA_BEKLE);
74      break;
75    case YAYA_BEKLE:
76      if (gecen >= GUVENLIK) gec(YAYA_GECER);
77      break;
78    case YAYA_GECER:
79      if (gecen >= YAYA_SURE) gec(YAYA_FLAS);
80      break;
81    case YAYA_FLAS:
82      digitalWrite(YAYA_YESIL, (gecen / 250) % 2 == 0);  // 250 ms'de bir yanıp söner / toggles every 250 ms
83      if (gecen >= FLAS_SURE) gec(YAYA_BITTI);
84      break;
85    case YAYA_BITTI:
86      if (gecen >= GUVENLIK) gec(KIRMIZI_SARI);
87      break;
88    case KIRMIZI_SARI:
89      if (gecen >= KS_SURE) gec(ARAC_YESIL);
90      break;
91  }
92}

スケッチの中心はgec()(「移る」)関数です。新しい状態を保存し、タイマーをリセットし、その状態のランプを設定し、状態の名前をシリアルモニタに表示します。loop()の中のswitchは、時間が来たかどうかを確認するだけです。ボタンを押してもすぐには何も起きません。yayaTalebi(歩行者の要求)フラグを立てるだけで、車が最低8秒の青を得たあとにサイクルが始まります。これでボタンの押下が失われることはなく、ボタンはフラグを変えるだけなので、チャタリングも問題になりません。歩行者用の青がすでに点いている間の押下は無視します。そうしないと、歩行者が渡り終えた直後に、不要な新しいサイクルが始まってしまうからです。シリアルモニタを9600ボーで開くと、切り替わるたびに名前が表示されます。この出力はデバッグにも学校のレポートにも役立ちます。

機能を追加したいときは、まず表に行を足し、それからコードにcaseを書きます。たとえば、黄が点滅する夜間の状態を追加し、LDR光センサーが暗さを検知したらそこに切り替えることもできます。ステートマシンをもっと深く学ぶには、Arduinoのステートマシンをご覧ください。

ステップ4:2方向の交差点

本物の交差点では、2つの道路の信号は連動しています。道路Aが青の間、道路Bは赤でなければなりません。この版では6つのLEDを使います。方向AにD10、D9、D8、方向BにD5、D4、D3です。大切なのは、短い全赤のクリアランス時間です。片方の方向が赤になっても、もう片方はすぐ青にならず、交差点の中に残っている車が出られるよう、制御装置は1秒待ちます。今回は状態をswitchではなく表(配列)に入れます。各ステップについて、点灯するランプを1バイトのビットとして、時間をミリ秒で保存します。

C++ (Arduino IDE)
1// İki yönlü kavşak: tablo tabanlı durum makinesi / two-way junction: table-driven state machine
2const byte A_KIRMIZI = 10, A_SARI = 9, A_YESIL = 8;  // A yönü (ana yol) / direction A (main road)
3const byte B_KIRMIZI = 5,  B_SARI = 4, B_YESIL = 3;  // B yönü (ara yol) / direction B (side road)
4const byte PINLER[6] = { A_KIRMIZI, A_SARI, A_YESIL, B_KIRMIZI, B_SARI, B_YESIL };
5
6struct Adim {
7  byte maske;            // bit sırası: A_K A_S A_Y B_K B_S B_Y / bit order: A_R A_Y A_G B_R B_Y B_G
8  unsigned long sure;    // ms
9};
10
11const Adim PLAN[] = {
12  { 0b001100, 8000 },   // A yeşil, B kırmızı / A green, B red
13  { 0b010100, 3000 },   // A sarı, B kırmızı / A yellow, B red
14  { 0b100100, 1000 },   // tüm yönler kırmızı / all red
15  { 0b100110, 1000 },   // A kırmızı, B kırmızı + sarı / A red, B red + yellow
16  { 0b100001, 5000 },   // A kırmızı, B yeşil / A red, B green
17  { 0b100010, 3000 },   // A kırmızı, B sarı / A red, B yellow
18  { 0b100100, 1000 },   // tüm yönler kırmızı / all red
19  { 0b110100, 1000 },   // A kırmızı + sarı, B kırmızı / A red + yellow, B red
20};
21const byte ADIM_SAYISI = sizeof(PLAN) / sizeof(PLAN[0]);
22
23byte adim = 0;
24unsigned long adimBaslangic = 0;
25
26void uygula(byte maske) {
27  if ((maske & 0b001000) && (maske & 0b000001)) {  // iki yeşil aynı anda yanamaz / never two greens
28    maske = 0b100100;                              // güvenli durum: tüm kırmızı / fail-safe: all red
29  }
30  for (byte i = 0; i < 6; i++) {
31    digitalWrite(PINLER[i], (maske >> (5 - i)) & 1);  // soldaki bit A_KIRMIZI / leftmost bit is A_RED
32  }
33}
34
35void setup() {
36  for (byte i = 0; i < 6; i++) pinMode(PINLER[i], OUTPUT);
37  uygula(PLAN[0].maske);
38  adimBaslangic = millis();
39}
40
41void loop() {
42  if (millis() - adimBaslangic >= PLAN[adim].sure) {
43    adimBaslangic += PLAN[adim].sure;   // kaymasız zamanlama / drift-free timing
44    adim = (adim + 1) % ADIM_SAYISI;
45    uygula(PLAN[adim].maske);
46  }
47  // Burada buton, sensör ya da ekran işleri çalışabilir / buttons, sensors or a display can run here
48}

PLAN配列の各行が1つのステップです。0b001100のような2進数では、左から順にビットがAの赤、Aの黄、Aの青、Bの赤、Bの黄、Bの青を表し、1が点灯、0が消灯です。uygula()(「適用する」)関数が、この6ビットを順番にピンに書き込みます。関数の最初のチェックは、打ち間違いで両方向が同時に青になるのを防ぎ、代わりに交差点を全赤にします。本物の交差点の制御装置にも、矛盾する青を検出する別の安全回路があります。タイミングを変えたりステップを足したりするには表だけを書き換え、残りのコードには触れません。LEDが増えてピンが足りなくなったら、74HC595シフトレジスタで3本のピンから8つの出力が得られます。

Tinkercadで試す:部品なしでシミュレーション

  1. tinkercad.comで無料のアカウントを作り、Circuits(回路)のセクションで新しい回路を始めます。
  2. 右の部品リストからArduino Uno R3、ブレッドボード、LED 3個、抵抗3本をドラッグします。LEDをクリックして色を選び、抵抗をクリックして値を220 Ωに設定します。
  3. 線を引きます。LEDのアノードを抵抗を通してD10、D9、D8へ、カソードをGNDへつなぎます。線の色を設定すると(たとえばGNDは黒)、回路図が読みやすくなります。
  4. Code(コード)をクリックし、ドロップダウンメニューからText(文字)を選び、サンプルコードを消して、このガイドのスケッチを貼り付けます。
  5. Start Simulation(シミュレーションを開始)をクリックすると、LEDが順番に切り替わり始めます。シリアル出力は、コードパネルの下のシリアルモニタに表示されます。

Tinkercadは、本物の部品で組む前に配線とコードを試す一番簡単な方法です。抵抗を忘れると、シミュレーターはLEDに電流が大きすぎるという警告を表示します。歩行者用の版を試すには、シミュレーション中にマウスでボタンをクリックするだけです。遅いパソコンではシミュレーションが実際の時間より遅く動くことがあり、タイミングが少し長く見えるかもしれません。学校の課題には、Tinkercadの共有リンクや回路のスクリーンショットを提出できます。詳しい手順はTinkercadでArduinoのシミュレーションを、ESP32とPicoにも対応した代わりのツールはWokwiをご覧ください。

夜間モード付きのESP32の信号機

このガイドのArduinoのスケッチはESP32でも動きます。Arduino IDEでボードにESP32 Dev Moduleを選び、表のとおりにピンの定数を変えてください。下の版には夜間モードという機能を1つ追加しています。交通量の少ない時間帯には、多くの交差点が黄の点滅に切り替わります。ESP32 DevKitのボードにはGPIO 0につながったBOOTボタンがあるので、追加のボタンも不要です。ボードが起動したあとは、通常の入力として読めます。押すたびに通常モードと夜間モードが切り替わります。

C++ (Arduino IDE)
1// ESP32 trafik lambası + gece modu (BOOT düğmesi) / ESP32 traffic light + night mode (BOOT button)
2const int KIRMIZI = 5;      // GPIO 5
3const int SARI = 18;        // GPIO 18
4const int YESIL = 19;       // GPIO 19
5const int BOOT_BUTON = 0;   // kart üzerindeki BOOT düğmesi / on-board BOOT button
6
7struct Adim { bool k, s, y; unsigned long sure; };
8const Adim PLAN[] = {
9  { true,  false, false, 5000 },   // kırmızı / red
10  { true,  true,  false, 1000 },   // kırmızı + sarı / red + yellow
11  { false, false, true,  5000 },   // yeşil / green
12  { false, true,  false, 3000 },   // sarı / yellow
13};
14const int ADIM_SAYISI = sizeof(PLAN) / sizeof(PLAN[0]);
15
16int adim = 0;
17unsigned long adimBaslangic = 0;
18bool geceModu = false;
19int sonOkuma = HIGH, kararli = HIGH;     // debounce için / for debouncing
20unsigned long degisimZamani = 0;
21
22void lambalar(bool k, bool s, bool y) {
23  digitalWrite(KIRMIZI, k);
24  digitalWrite(SARI, s);
25  digitalWrite(YESIL, y);
26}
27
28void setup() {
29  Serial.begin(115200);
30  pinMode(KIRMIZI, OUTPUT);
31  pinMode(SARI, OUTPUT);
32  pinMode(YESIL, OUTPUT);
33  pinMode(BOOT_BUTON, INPUT_PULLUP);
34  lambalar(PLAN[0].k, PLAN[0].s, PLAN[0].y);
35  adimBaslangic = millis();
36}
37
38void loop() {
39  unsigned long simdi = millis();
40
41  // 50 ms kararlı kalan basışı kabul et / accept a press that stays stable for 50 ms
42  int okuma = digitalRead(BOOT_BUTON);
43  if (okuma != sonOkuma) {
44    sonOkuma = okuma;
45    degisimZamani = simdi;
46  }
47  if (simdi - degisimZamani >= 50 && okuma != kararli) {
48    kararli = okuma;
49    if (kararli == LOW) {                  // yeni basış / new press
50      geceModu = !geceModu;
51      Serial.println(geceModu ? "Gece modu / Night mode" : "Normal mod / Normal mode");
52      adim = 0;                            // normal mod kırmızıdan başlar / normal mode restarts on red
53      adimBaslangic = simdi;
54      lambalar(PLAN[0].k, PLAN[0].s, PLAN[0].y);
55    }
56  }
57
58  if (geceModu) {
59    lambalar(false, (simdi / 500) % 2 == 0, false);  // yanıp sönen sarı / flashing yellow
60    return;
61  }
62
63  if (simdi - adimBaslangic >= PLAN[adim].sure) {
64    adimBaslangic = simdi;
65    adim = (adim + 1) % ADIM_SAYISI;
66    lambalar(PLAN[adim].k, PLAN[adim].s, PLAN[adim].y);
67  }
68}

ボタンの読み取りには簡単なチャタリング対策が入っていて、値が50 ms変わらないときだけ押下として数えます。ランプの状態を保存するPLANの表は、交差点のスケッチで使った考え方を読みやすくしたものです。ESP32のピンは3.3 Vを出力するので、LEDに流れる電流はArduinoより少なく、少し暗く見えます。赤と黄のLEDは220 Ωでも十分明るいですが、順方向電圧が約3 Vの明るい緑のLEDはとても暗いままになることがあります。その場合は緑の抵抗を100 Ωに下げましょう。ESP32が初めてなら、インストールと安全なピンについてはESP32とは?から始めるのがおすすめです。

Raspberry Pi Picoで歩行者用信号機(MicroPython)

Raspberry Pi Picoでは、同じステートマシンをMicroPythonで書きます。今回は状態をリストに入れ、各行に状態の名前、5つのランプの値、時間を持たせます。ボタンには割り込みを使います。GP4がLOWに下がるとbuton_kesmesi()(「ボタンの割り込み」)関数が実行され、要求フラグを立てるだけです。Thonnyでコードをmain.pyとしてPicoに保存すると、ボードの電源を入れるたびに自動で始まります。

MicroPython
1# Raspberry Pi Pico (MicroPython): yaya butonlu trafik lambası / pedestrian traffic light
2from machine import Pin
3import time
4
5yesil = Pin(11, Pin.OUT)              # araç yeşil / vehicle green
6sari = Pin(12, Pin.OUT)               # araç sarı / vehicle yellow
7kirmizi = Pin(13, Pin.OUT)            # araç kırmızı / vehicle red
8yaya_yesil = Pin(14, Pin.OUT)         # yaya yeşil / pedestrian green
9yaya_kirmizi = Pin(15, Pin.OUT)       # yaya kırmızı / pedestrian red
10buton = Pin(4, Pin.IN, Pin.PULL_UP)   # GP4, diğer ucu GND / other leg to GND
11
12# (durum, kırmızı, sarı, yeşil, yaya kırmızı, yaya yeşil, süre ms)
13# (state, red, yellow, green, ped red, ped green, duration ms)
14DURUMLAR = [
15    ("ARAC_YESIL",   0, 0, 1, 1, 0, 8000),   # en az yeşil süresi / minimum green
16    ("ARAC_SARI",    0, 1, 0, 1, 0, 3000),
17    ("YAYA_BEKLE",   1, 0, 0, 1, 0, 1000),   # tüm kırmızı / all red
18    ("YAYA_GECER",   1, 0, 0, 0, 1, 6000),
19    ("YAYA_FLAS",    1, 0, 0, 0, 1, 3000),
20    ("YAYA_BITTI",   1, 0, 0, 1, 0, 1000),   # tüm kırmızı / all red
21    ("KIRMIZI_SARI", 1, 1, 0, 1, 0, 1000),
22]
23
24talep = False
25adim = 0                              # şu anki durumun sırası / index of the current state
26
27def buton_kesmesi(pin):               # kesmede yalnız bayrak kaldır / only set a flag in the IRQ
28    global talep
29    if DURUMLAR[adim][0] != "YAYA_GECER":   # yaya yeşilinde basış yok sayılır / ignored during the green man
30        talep = True
31
32buton.irq(trigger=Pin.IRQ_FALLING, handler=buton_kesmesi)
33
34def uygula(i):
35    global talep
36    ad, k, s, y, yk, yy, _ = DURUMLAR[i]
37    kirmizi.value(k)
38    sari.value(s)
39    yesil.value(y)
40    yaya_kirmizi.value(yk)
41    yaya_yesil.value(yy)
42    if ad == "YAYA_GECER":
43        talep = False                 # talep karşılandı / request served
44    print("Durum / state:", ad)
45
46uygula(adim)
47baslangic = time.ticks_ms()
48
49while True:
50    gecen = time.ticks_diff(time.ticks_ms(), baslangic)   # taşmaya karşı güvenli / wrap-safe
51    ad = DURUMLAR[adim][0]
52
53    if ad == "YAYA_FLAS":
54        yaya_yesil.value((gecen // 250) % 2 == 0)          # yanıp sönen yaya yeşili / flashing green man
55
56    if gecen >= DURUMLAR[adim][6] and (ad != "ARAC_YESIL" or talep):
57        adim = (adim + 1) % len(DURUMLAR)                   # yeşil, talep gelene kadar sürer / green lasts until a request
58        uygula(adim)
59        baslangic = time.ticks_ms()
60
61    time.sleep_ms(10)

MicroPythonでは、millis()の代わりにtime.ticks_ms()を、ただの引き算の代わりにtime.ticks_diff()を使います。ticksのカウンターはある値を超えると一周して戻るため、2つの値を直接引き算すると間違った結果になることがあるからです。割り込みハンドラは短くしましょう。printもsleepも長い計算もせず、フラグを変えるだけにします。Picoのピンも3.3 Vで動くので、ESP32の章の緑のLEDについての注意がここでも当てはまります。MicroPythonのインストール方法とピン番号はRaspberry Pi Picoとは?で、言語の基本はMicroPythonの基本で確認できます。

よくある失敗と対処法

症状考えられる原因対処法
LEDが1つも点かないGNDがつながっていない、またはブレッドボードの電源レールが中央で分かれているArduinoのGNDを青のレールにつなぎ、長いブレッドボードでは左右の半分をつなぐ
LEDが1つだけ点かないLEDが逆、または違うピンにつながっている長い足(アノード)を抵抗を通してピンへ、短い足をGNDへ。ピン番号をコードと比べる
違う色が点くコードで赤と緑のピンが入れ替わっているピンの表に合わせて配線か定数を直す
LEDがとても明るく、熱くなる抵抗が入っていないすべてのLEDに220 Ωの直列抵抗を入れる
ボタンが反応しない、またはいつも押されているように見える内部ですでにつながっている2本の足にボタンをつないだ、またはINPUT_PULLUPを忘れた対角の足を使い、pinMode(BUTON, INPUT_PULLUP)と書く
ボタンにすぐ反応しない車の最低青時間がまだ終わっていない不具合ではない。MIN_YESILを小さくする
しばらくするとプログラムがおかしくなる時間の変数がintで宣言されているmillis()で使う変数はすべてunsigned longで宣言する

学校の課題とプロジェクトのレポートのコツ

信号機は、理科、技術、ロボットの授業で最もよく出るArduinoの課題の1つです。先生がふつう見たいのは、動く回路だけでなく、それを理解して説明できることです。次の章立てでレポートを構成すると、整理されて見え、大事なことも漏れません。

  • 目的:プロジェクトで何を示すのかを1〜2文で書きます。たとえば「デジタル出力で信号機のタイミングを制御する」などです。
  • 部品リスト:上の表のように、すべての部品の名前、数量、値(220 Ω)を書きます。
  • 回路図:TinkercadやFritzingのスクリーンショットを入れます。手で描く場合は正しい記号を使いましょう。回路図の読み方が役に立ちます。
  • 抵抗の計算:R = (5 − 2) / 0.015 ≈ 200 Ωを示し、220 Ωを選んだ理由を説明します。
  • 状態の表かフローチャート:状態、時間、遷移の表を入れます。コードを理解していることの一番の証拠になります。
  • コードと説明:コード全体を載せ、大事な行を自分の言葉で説明します。
  • テストと観察:ストップウォッチで各段階の時間を測り、テスターでLED 1個の電流を測って計算と比べます。
  • 結論と改善点:出会った問題とその解決方法、追加できる機能(歩行者用ボタン、音、夜間モード)を書きます。
差をつけたいなら、最初のdelay()の版とmillis()の版を比べましょう。前者ではボタンの押下が見逃され、後者では決して見逃されない理由を説明します。動いている回路の20〜30秒の動画とシリアルモニタの出力のスクリーンショットがあると、レポートはさらに強くなります。

信号機をさらに発展させるアイデア

  • カウントダウン表示:青が終わるまでの残り秒数を7セグメントディスプレイに表示します。
  • 目の不自由な歩行者のための音響信号:歩行者用信号が青の間はブザーを速く、赤の間はゆっくり鳴らします。
  • スマートな交差点:HC-SR04距離センサーで脇道に車が待っているかを検知し、車がいるときだけ方向Bを青にします。
  • 自動の夜間モード:LDRを使い、暗くなったら黄の点滅に切り替えます。
  • 遠隔操作:ESP32でウェブページを作り、スマートフォンからモードとタイミングを変えます。
  • 踏切:同じロジックで、2つの赤のLEDが交互に点滅する踏切の警報灯を作ります。ナイトライダーと流れるLEDのプロジェクトでも似たタイミングの考え方を使っています。

信号機は小さなプロジェクトに見えますが、そこに含まれるステートマシン、止まらないタイミング、安全な遷移という考え方は、これから作るロボット、アラームシステム、スマートホームの機器の土台にもなります。次に作るものを選ぶには、Arduinoのプロジェクトのアイデアのリストを見てみましょう。BlueGraysアプリには、Arduino、ESP32、Raspberry Pi Picoの信号機のプロジェクトに加え、Picoの4モードの信号機などの発展版もあり、それぞれステップごとの回路図とすぐに使えるコード付きで、自分のボードで試せます。

よくある質問

Arduinoで信号機を作るには?

赤、黄、緑のLEDの長い足を220 Ωの抵抗を通してArduinoのデジタルピン(たとえばD10、D9、D8)に、短い足をGNDにつなぎます。コードではそのピンをOUTPUTに設定し、loop()の中でLEDを順番にオン・オフします。最初の版はdelay()で十分ですが、ボタンや追加の機能にはmillis()が必要です。

信号機のLEDにはどの抵抗が必要ですか?

5 VのArduino Unoでは、LED 1個に220 Ωが一般的で安全な選択です。このとき赤のLEDには約13〜14 mAが流れます。330 Ωにすると暗くなりますが、まだはっきり見えます。3.3 VのESP32とPicoでも220 Ωで動きます。明るい緑のLEDが暗いままのときだけ、100 Ωに下げましょう。

信号機で赤と黄が同時に点くのはなぜですか?

イギリス、ドイツ、トルコなどの国では、赤と黄が同時に点くと、もうすぐ青になることを知らせます。ドライバーは準備しますが、青になるまでは動きません。アメリカ、フランス、イタリア、スペイン(そして日本)にはこの段階がなく、赤からすぐ青に変わります。Arduinoのプロジェクトでは、この段階を入れても入れなくてもかまいません。

Arduinoの信号機にはdelayとmillisのどちらを使えばいいですか?

LEDを順番に切り替えるだけなら、delay()が最も簡単で問題なく動きます。ボタン、センサー、ディスプレイ、音を加えたとたん、delay()はプログラムを止めてしまい、それらの入力が見逃されます。millis()とステートマシンで書いたコードは決して待たず、すべてを同時に見張れます。

信号機のプロジェクトはTinkercadで作れますか?

はい。Tinkercad CircuitsにはArduino Uno、LED、抵抗、押しボタンが既製の部品として入っています。回路を描き、Textエディターにコードを貼り付け、シミュレーションを開始します。本物の部品なしで回路とコードを試したり、課題用にスクリーンショットや共有リンクを用意したりするのに最適です。

歩行者用の押しボタン式信号機はどう動きますか?

ふだんは車が青で、ボタンを押すと要求が登録されます。車の最低青時間が終わると、信号は黄、続いて赤になり、短い安全な間隔のあと歩行者用信号が青になります。このガイドのコードでは、歩行者用の青は終わる前に点滅し、その後サイクルは車に戻ります。

全プロジェクトを見る
信号機
#3初級

信号機

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

4.8(131)
397
ESP32の信号機
#107初級

ESP32の信号機

赤・黄・緑の3個のLEDで、実際の信号機を再現します。赤は長く、黄は短く、緑は長く点灯させ、実際の信号機に近いタイミングで動かします。GPIOを順番に制御する基本例です。

4.4(21)
44
Raspberry Pi Picoで信号機(3個のLEDを順に点灯)
#150初級

Raspberry Pi Picoで信号機(3個のLEDを順に点灯)

赤・黄・緑のLEDで本物の信号機を再現する、Picoシリーズ初の3個のLEDを使うプロジェクトです!緑が4秒間「進め」、黄が1.5秒間「準備」、赤が3秒間「止まれ」、赤と黄が1秒間「発進準備」を示します(ヨーロッパの信号機のパターン)。関数の定義(def all_off())、状態機械の仕組み(順序)、複数のGPIOの同期を学びます。回路はとても簡単です。GP11(緑)、GP12(黄)、GP13(赤)に3個のLEDを接続し、それぞれに220Ωの電流制限抵抗を使います。

4.0(1)
12
2個のLEDを点灯
#2初級

2個のLEDを点灯

2個のLEDが素早く交互に点滅します。一方が点くともう一方が消え、まるでボールを投げ合っているようです。ArduinoがそれぞれのLEDを5分の1秒ずつ点灯させ、次のLEDに切り替えます。1個のLEDの次に、複数の光をまとめて制御する方法を学ぶのに最適なステップです。

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

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

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

4.8(113)
229
LEDの警察車両風点滅
#4初級

LEDの警察車両風点滅

警察車両の警光灯のように、赤いLEDが素早く3回点滅し、続いて青いLEDが3回点滅します。このパターンを途切れることなく繰り返します。Arduinoに2個のLEDと2個の抵抗をつなぐだけで、数分で組み立てられる、目を引く入門プロジェクトです。

4.7(103)
258
Raspberry Pi Picoで信号機の4モードをボタンで切り替え(ステートマシン)
#158中級

Raspberry Pi Picoで信号機の4モードをボタンで切り替え(ステートマシン)

1個のボタンで信号機の4つのモードを切り替えます!押すたびに、通常サイクル(赤→黄→緑の順に点灯)、赤のみ(停止)、黄色点滅(注意・夜間モード)、消灯(メンテナンス)へ切り替わります。ステートマシン(mode変数で実行する動作を決定)、剰余演算によるモードの循環((mode + 1) % 4で0→1→2→3→0を繰り返す)、処理を止めない時間管理(time.ticks_ms() + time.ticks_diff()でLEDの各段階の時間を管理)を学びます。ただし、モード切り替え時には待ち時間があります。

5.0(1)
15