Arduinoのmillis()でdelayを使わない書き方|LED点滅とボタン・センサーを同時に扱う実践ガイド

Arduino 機器制作






Arduinoのmillis()でdelayを使わない書き方|LED点滅とボタン・センサーを同時に扱う実践ガイド










Arduino millis() Without Delay — 2026

delay()が止めるのは、
LEDだけじゃない。

Lチカ(LEDを点滅させる基本の工作)は、delay()を使えば数行で書けます。ですがdelay()には「実行している間、プログラム全体が完全に止まる」という性質があり、ボタンやセンサーを同時に扱いたくなった瞬間につまずきの原因になります。この記事では、delay()が実際に「困る」場面を実機レベルのコードで見せたうえで、millis()を使ってプログラムを止めずに時間を計る書き方へ、1つずつ書き換えていきます。if文の基本を理解した人が、次に必ず通る橋渡しの技術です。

対象読者: if文の基本を理解したArduino初級〜中級者 対象ボード: Arduino Uno / Nano互換機 収録コード: 4本
delay版とmillis版を比較公式Blink Without Delay準拠全コードCOPY対応配線メモ付き
01

delay()が「困る」瞬間を実機で確認する

delay(1000)と書けば、Arduinoは1000ミリ秒(1秒)の間、次の行に進むのを待ちます。LEDを1秒おきに点滅させるだけなら、これでまったく問題ありません。問題が起きるのは、「LEDを点滅させながら、同時にボタンの状態も見張りたい」というように、2つ以上のことを同時にやりたくなった時です。

delay()で待っている間、Arduinoはボタンが押されたことにすら気づけません。たとえば「LEDが点灯している1秒間にボタンを押した」としても、その入力はArduinoが待ち終わってから確認するまで、なかったことにされてしまいます。ゲームの反応判定や、非常停止ボタンのような「いつ押されるか分からない入力」を扱う工作では、これが致命的な問題になります。

delay()方式

1秒待っている間はArduinoが完全に停止。ボタンを押しても、センサーの値が変わっても、待ち時間が終わるまで一切気づけない。

millis()方式

時間を計りながらも、loop()は止まらず何度も回り続ける。LEDの点滅とボタンの読み取りを、同じタイミングで両立できる。

この現象は、Arduino公式のビルトイン例でも「Blink Without Delay」↗として紹介されており、ボタンやセンサーを同時に扱う際の基本技術として位置づけられています。3章以降で、実際にdelay()版とmillis()版を並べて動きの違いを確認していきましょう。

この記事の前提

この記事は、if文の基本(if・else・比較演算子)とpinMode・digitalRead・analogReadの使い方をすでに理解している人向けの、一歩進んだ内容です。if文自体の基礎を先に確認したい場合は、下記の関連記事もあわせてご覧ください。

02

millis()の基本と、書き換えの型

millis()は、Arduinoのプログラムが始まってから経過した時間をミリ秒単位で返してくれる関数です。delay()のように待つのではなく、「今、何ミリ秒経過したか」を一瞬で教えてくれるだけなので、loop()の実行は止まりません。この性質を使うと、「delay()で止める」代わりに「millis()で時間を確認し続けて、条件が成り立ったらその時だけ処理をする」というif文中心の書き方に変えられます。

用語メモ:unsigned longmillis()が返す値はunsigned longという種類の変数で受け取ります。intよりも大きな数字を扱える型で、マイナスの値を扱わない代わりに、非常に大きな数字(約42億まで)を正確に記録できます。時間のように増え続ける値を扱う時の定番の型です。

delay()をmillis()に書き換える時は、次の3ステップがほぼ共通の型になります。

ステップ やること
① 記録用の変数を用意 「前回いつ動作したか」を覚えておくunsigned long型の変数を1つ用意する
② 経過時間を毎回チェック loop()の中でmillis() - 前回の時刻を計算し、これが目的の間隔(例:1000ミリ秒)以上になったかをif文で確認する
③ 動作と記録の更新 条件が成り立ったら処理を実行し、同時に「前回の時刻」を今のmillis()の値で上書きする

この型さえ覚えてしまえば、LEDの点滅に限らず、センサーを一定間隔で読み取る、一定時間ごとにログを送るなど、さまざまな「時間を使う処理」に応用できます。公式の言語リファレンスにあるmillis()のページ ↗delay()のページ ↗も、あわせて確認しておくと理解が深まります。

03

コード1:delayを使ったLチカ

まずは基準となる、delay()を使った普通のLチカです。1秒おきにLEDが点いたり消えたりします。シンプルで読みやすい反面、この後の章で説明する「困る場面」の元になる書き方でもあります。

blink_with_delay.ino
// delay()を使った基本のLチカ
const int LED_PIN = 8;
void setup() {
  pinMode(LED_PIN, OUTPUT);
}
void loop() {
  digitalWrite(LED_PIN, HIGH);  // LED点灯
  delay(1000);                  // 1000ミリ秒(1秒)待つ
  digitalWrite(LED_PIN, LOW);   // LED消灯
  delay(1000);                  // 1000ミリ秒(1秒)待つ
}
delay(1000);
この1行の間、Arduinoは他の何もできません。ここに「ボタンを確認する処理」を追加しても、delay()で止まっている間はチェックされないため、反応が最大2秒遅れる(点灯中と消灯中、両方のdelay分)ことになります。
試してみよう

このコードにボタンを1つ追加し、「ボタンを押したらすぐにLEDを消す」処理をloop()の中に足してみてください。delay()の実行中はボタンを押しても反応が遅れることを、実機で体感できます。

04

コード2:millisに書き換えたLチカ

同じ「1秒おきの点滅」を、今度はdelay()を一切使わずにmillis()だけで実現します。動きの見た目はコード1とまったく同じですが、内部の仕組みはまるで違います。

blink_without_delay.ino
// millis()を使ったノンブロッキングなLチカ(delay不使用)
const int LED_PIN = 8;
const unsigned long INTERVAL = 1000;  // 点滅の間隔(ミリ秒)
unsigned long previousMillis = 0;     // 前回LEDを切り替えた時刻
int ledState = LOW;                   // 現在のLEDの状態
void setup() {
  pinMode(LED_PIN, OUTPUT);
}
void loop() {
  unsigned long currentMillis = millis();
  // 前回の切り替えから、目的の間隔が経過したかを毎回チェック
  if (currentMillis - previousMillis >= INTERVAL) {
    previousMillis = currentMillis;  // 時刻を記録し直す
    // LEDの状態を反転させる(点灯⇔消灯)
    ledState = (ledState == LOW) ? HIGH : LOW;
    digitalWrite(LED_PIN, ledState);
  }
  // ここには、他の処理を自由に追加できる
  // (delay()で止まっていないので、いつでも実行されるチャンスがある)
}
if (currentMillis – previousMillis >= INTERVAL)
これがmillis()方式の心臓部です。loop()が回るたびに「今の時刻」から「前回LEDを切り替えた時刻」を引き算し、その差がINTERVAL(1000ミリ秒)以上になっていたら「そろそろ切り替える時間だ」と判断します。delay()のように待つのではなく、条件を毎回確認しているだけなので、loop()自体は止まりません。
ledState = (ledState == LOW) ? HIGH : LOW;
「もしledStateがLOWなら、次はHIGHに。そうでなければLOWに」という意味を1行で書ける三項演算子という書き方です。digitalRead(LED_PIN)で現在の状態を読み直しても同じ結果になりますが、変数で状態を持っておく方がコードの意図が読みやすくなります。
POINT

コード1とコード2は、LEDの見た目の動きは同じです。違いはloop()が「止まっているか、止まっていないか」だけ。この違いが、次の5章・6章で大きな意味を持ってきます。

05

コード3:速度の違うLEDを2個同時点滅

delay()方式の限界がはっきり分かるのが、「速さの違う点滅を同時に行いたい」という場面です。delay()は1つの時間しか待てないため、2つの異なる間隔を同時に扱うのは工夫が必要になります。millis()方式なら、記録用の変数を2セット用意するだけで、驚くほど自然に対応できます。

two_leds_different_speed.ino
// 速度の違うLEDを2個、それぞれ独立したペースで点滅させる
const int LED_A_PIN = 8;
const int LED_B_PIN = 9;
const unsigned long INTERVAL_A = 300;   // LED Aは0.3秒ごと(速い)
const unsigned long INTERVAL_B = 900;   // LED Bは0.9秒ごと(遅い)
unsigned long previousMillisA = 0;
unsigned long previousMillisB = 0;
int ledStateA = LOW;
int ledStateB = LOW;
void setup() {
  pinMode(LED_A_PIN, OUTPUT);
  pinMode(LED_B_PIN, OUTPUT);
}
void loop() {
  unsigned long currentMillis = millis();
  // LED Aの担当:0.3秒おきに切り替え
  if (currentMillis - previousMillisA >= INTERVAL_A) {
    previousMillisA = currentMillis;
    ledStateA = (ledStateA == LOW) ? HIGH : LOW;
    digitalWrite(LED_A_PIN, ledStateA);
  }
  // LED Bの担当:0.9秒おきに切り替え(Aとは完全に独立)
  if (currentMillis - previousMillisB >= INTERVAL_B) {
    previousMillisB = currentMillis;
    ledStateB = (ledStateB == LOW) ? HIGH : LOW;
    digitalWrite(LED_B_PIN, ledStateB);
  }
}
previousMillisA / previousMillisB
4章のコードでは記録用の変数が1つでしたが、ここでは「LED A用」「LED B用」の2セットに増やしています。それぞれのif文が、それぞれの間隔と、それぞれの前回時刻だけを見て動くため、お互いに干渉しません。同じ考え方をコピーして増やしていけば、3個・4個と点滅対象を増やすこともできます。
delay()で無理にやろうとすると

もしdelay()だけでこれを再現しようとすると、delay(300)を8回くり返してから初めてLED Bを切り替える、といった最小公倍数を計算する力技が必要になり、間隔を変えるたびに全体を計算し直すことになります。millis()方式は、こうした組み合わせの複雑さから解放してくれます。

06

コード4:LED点滅中もボタンとセンサーを読み取る

最後は、1章で提示した「困る場面」そのものへの回答です。LEDを一定間隔で点滅させながら、同時にボタンの押下とセンサーの値も毎回チェックする、という複数の役割を1つのloop()で両立させます。

blink_button_sensor.ino
// LEDを点滅させながら、ボタンとセンサーも同時に読み取る
const int LED_PIN = 8;
const int BUTTON_PIN = 2;
const int SENSOR_PIN = A0;
const int ALERT_PIN = 9;
const unsigned long BLINK_INTERVAL = 500;  // LEDの点滅間隔
unsigned long previousBlinkMillis = 0;
int ledState = LOW;
void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(ALERT_PIN, OUTPUT);
  Serial.begin(9600);
}
void loop() {
  unsigned long currentMillis = millis();
  // 役割1:LEDを一定間隔で点滅させ続ける
  if (currentMillis - previousBlinkMillis >= BLINK_INTERVAL) {
    previousBlinkMillis = currentMillis;
    ledState = (ledState == LOW) ? HIGH : LOW;
    digitalWrite(LED_PIN, ledState);
  }
  // 役割2:ボタンはdelayを挟まずに毎回チェックできる
  bool buttonPressed = digitalRead(BUTTON_PIN) == LOW;
  if (buttonPressed) {
    Serial.println("Button pressed!");
  }
  // 役割3:センサーの値も、LEDの点滅を止めずに毎回確認できる
  int sensorValue = analogRead(SENSOR_PIN);
  if (sensorValue >= 700) {
    digitalWrite(ALERT_PIN, HIGH);
  } else {
    digitalWrite(ALERT_PIN, LOW);
  }
}
// 役割1 / 役割2 / 役割3
loop()が1回まわるたびに、LEDの点滅チェック・ボタンの確認・センサーの確認が、順番に、しかしごく短い時間ですべて実行されます。delay()が1つもないため、どの役割も他の役割の待ち時間に邪魔されません。これが「ノンブロッキング(処理を止めない)」な書き方です。
POINT

このコードのボタン部分は、押した瞬間だけを検知したい場合、「前回の状態」も変数で記録しておき、状態が変化した瞬間だけ処理する書き方に発展させられます。センサー部分はArduino用センサー完全ガイド16選 ↗で紹介している各種センサーにそのまま応用できます。

07

書き換えでつまずきやすいポイント

CHECK 1:型をintにしてしまう

時間を記録する変数をintにすると、約32秒(32,767ミリ秒)で桁あふれを起こし、計算がおかしくなります。時間を扱う変数は必ずunsigned longにしてください。

CHECK 2:引き算の順番を間違える

previousMillis - currentMillisのように順番を逆にすると、正しく動きません。「今の時刻 − 前回の時刻」の順番を必ず守ってください。

CHECK 3:記録の更新を忘れる

if文の中でLEDを切り替えても、previousMillis = currentMillis;で時刻を記録し直すのを忘れると、次のloop()からずっと条件が成り立ち続けてしまい、点滅が異常に速くなります。

オーバーフローは気にしなくてよい

millis()の値は約50日で0に戻ります(オーバーフロー)。しかしcurrentMillis - previousMillisという「差」で計算している限り、unsigned long型の性質によりこの計算は自動的に正しい結果になるよう作られています。差分計算の型さえ守っていれば、オーバーフローを特別に心配する必要はありません。

08

動かないときの確認項目

1

previousMillisなどの時刻変数がunsigned long型になっているか。intのままだと短時間で挙動がおかしくなります。

2

if文の引き算が「現在時刻 − 前回時刻」の順になっているか。逆順にすると正しく判定できません。

3

条件が成り立った時に、記録用の変数を更新し忘れていないか。更新を忘れると、次のloop()以降ずっと条件が成立し続けます。

4

loop()のどこかにdelay()が紛れ込んでいないか。1箇所でもdelay()が残っていると、その間は他の処理がすべて止まってしまい、millis()方式にした意味が薄れます。

5

複数のタイマーを扱う時、記録用の変数を共有していないか。5章のようにLEDごと・センサーごとに専用のprevious変数を分けて持つ必要があります。

6

INTERVAL(間隔)の単位を秒とミリ秒で混同していないか。1秒は1000ミリ秒です。300のつもりが3000になっていないか確認しましょう。

確認のコツ

Serial.println(currentMillis - previousMillis);を一時的にif文の直前に入れると、差分がどう増えているかをシリアルモニタで直接確認できます。数字が想定通りに増えていない場合は、上の1〜3番を優先して見直してください。

09

よくある質問とまとめ

delay()を使うと何が困るのですか?

delay()は指定した時間、プログラム全体を完全に止めてしまいます。LEDを点滅させている間はボタンを押しても反応せず、センサーの値も読み取れません。複数の動きを同時に扱いたくなった瞬間に、delay()だけでは対応できなくなります。

millis()はどうやって時間を計っているのですか?

millis()はArduinoの電源が入ってから(正確にはプログラムが始まってから)経過したミリ秒数を返す関数です。プログラムを止めることなく、現在の経過時間を教えてくれるので、この値を使って「前回の動作から何ミリ秒経ったか」を毎回チェックする書き方ができます。

delayをmillisに書き換える基本の型はありますか?

はい。前回動作した時刻を記録する変数(unsigned long型)を用意し、loop()の中で「現在のmillis() − 前回の時刻 が、目的の間隔以上になったか」をif文で毎回チェックします。条件が成り立ったら処理を実行し、そのタイミングの時刻を記録し直す、という型が基本になります。

millisの値はいつかリセットされますか?

millis()が返す値はunsigned long型で管理されており、約50日が経過すると0に戻ります(オーバーフローと呼ばれる現象です)。now – previousMillisのように「差」で計算する書き方をしていれば、このオーバーフローが起きても計算結果は正しく保たれます。

delay()は書きやすい反面、「1つのことしかできない」という制約を持っています。millis()への書き換えは最初は少し面倒に感じますが、「記録用の変数を用意する」「差分をif文でチェックする」「記録を更新する」という3ステップの型さえ身につければ、LEDの点滅に限らずセンサーの定期読み取りやアニメーションなど、あらゆる「時間を使う処理」に応用できます。まずは4章のコードを実際に書き換えてみて、5章・6章で複数の役割を同時に持たせる感覚をつかんでみてください。

ARDUINO MILLIS WITHOUT DELAY GUIDE 2026 — 「止めない書き方」が、次の作品の幅を広げます。



関連記事

特集記事

コメント

この記事へのコメントはありません。

TOP
CLOSE