ESP32を使って、時間になると音で知らせてくれるキッチンタイマーを作ってみましょう。
今回は、ボタンで時間を設定し、OLEDディスプレイに残り時間を表示するカウントダウンタイマーを製作します。
設定した時間になると、ブザーから電子音が鳴って終了を知らせます。
さらに、タイマーの一時停止や再開、リセットにも対応させます。
ESP32-C3を搭載したKUMITATE-C3なら、スイッチ・OLED・ブザーがすべて基板に搭載されているため、追加の配線なしで製作できます。
この記事では、Arduino IDEを使ったプログラムの作成方法と、時間計測・状態遷移・ブザー制御の仕組みを初心者向けに解説します。
今回作るキッチンタイマー
完成するキッチンタイマーには、次の機能を搭載します。
- 1〜60分の時間設定
- OLEDへの残り時間表示
- カウントダウンの開始
- 一時停止・再開
- リセット
- 時間終了時のブザー通知
KUMITATE-C3の4つのスイッチを次のように使用します。
| スイッチ | 機能 |
|---|---|
| SW1 | 設定時間を1分増やす |
| SW2 | 設定時間を1分減らす |
| SW3 | 開始・一時停止・再開 |
| SW4 | リセット |
例えば、5分に設定してSW3を押すと、OLEDの表示が次のように変化します。
05:00
04:59
04:58
:
00:02
00:01
00:00
残り時間が0になると、ブザーが3回鳴ります。
必要なもの
| 部品 | 数量 | 用途 |
|---|---|---|
| ESP32開発ボード | 1 | 時間計測・制御 |
| OLEDディスプレイ | 1 | 残り時間表示 |
| タクトスイッチ | 4 | 時間設定・操作 |
| ブザー | 1 | 終了通知 |
| USBケーブル | 1 | 電源供給・書き込み |
| パソコン | 1 | プログラム作成 |
KUMITATE-C3を使用すれば、OLED、スイッチ、ブザーが最初から搭載されているため、USBケーブルで接続するだけで始められます。
ESP32とOLED・スイッチ・ブザーの接続
今回はKUMITATE-C3のピン配置に合わせてプログラムを作成します。
| 機能 | GPIO |
|---|---|
| OLED SDA | GPIO1 |
| OLED SCL | GPIO2 |
| SW1 | GPIO21 |
| SW2 | GPIO3 |
| SW3 | GPIO9 |
| SW4 | GPIO8 |
| ブザー | GPIO20 |
OLEDはI²C通信で制御します。I²Cアドレスは0x3Cです。
スイッチは内部プルアップを使用し、押すとLOWになる構成です。
一般的なESP32開発ボードを使用する場合は、配線に合わせてGPIO番号を変更してください。また、ブザーは種類によって必要な駆動回路が異なるため、GPIOから直接接続できるとは限りません。
Arduino IDEにライブラリをインストールする
今回使用するライブラリは次の2つです。
- Adafruit GFX Library
- Adafruit SSD1306
Arduino IDEの「スケッチ」→「ライブラリをインクルード」→「ライブラリを管理」からインストールしてください。
ESP32用のArduinoボードパッケージも必要です。
ESP32キッチンタイマーのプログラム
それでは実際にプログラムを作成します。
今回はdelay()で1秒ずつ待つ方法ではなく、millis()を使って残り時間を計算します。
この方法なら、カウントダウン中でもスイッチ操作を受け付けることができます。
以下のプログラムをArduino IDEに貼り付けてください。
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// OLED設定
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDRESS 0x3C
#define SDA_PIN 1
#define SCL_PIN 2
// KUMITATE-C3のスイッチ
const uint8_t SW_PINS[4] = {
21, 3, 9, 8
};
#define BUZZER_PIN 20
// OLED
Adafruit_SSD1306 display(
SCREEN_WIDTH,
SCREEN_HEIGHT,
&Wire,
-1
);
// タイマーの状態
enum TimerState {
SETTING,
RUNNING,
PAUSED,
ALARM
};
TimerState state = SETTING;
// 初期設定:3分
uint8_t selectedMinutes = 3;
// 残り時間(ミリ秒)
uint32_t remainingMs = 180000;
// 終了予定時刻
uint32_t deadlineMs = 0;
// スイッチのチャタリング対策
bool lastRaw[4];
bool stableSw[4];
uint32_t lastChange[4] = {0};
const uint32_t DEBOUNCE_MS = 30;
// OLED更新
uint32_t lastDraw = 0;
bool redraw = true;
// ブザー管理
uint8_t beepCount = 0;
bool beepActive = false;
uint32_t beepStart = 0;
uint32_t lastBeepStart = 0;
// スイッチの押下イベントを検出
bool buttonPressed(int index, uint32_t now) {
bool raw = digitalRead(SW_PINS[index]);
if (raw != lastRaw[index]) {
lastRaw[index] = raw;
lastChange[index] = now;
}
if (now - lastChange[index] >= DEBOUNCE_MS) {
if (raw != stableSw[index]) {
stableSw[index] = raw;
return raw == LOW;
}
}
return false;
}
// タイマーをリセット
void resetTimer() {
noTone(BUZZER_PIN);
beepActive = false;
beepCount = 0;
state = SETTING;
remainingMs =
(uint32_t)selectedMinutes * 60000UL;
redraw = true;
}
// カウントダウン開始・再開
void startTimer(uint32_t now) {
deadlineMs = now + remainingMs;
state = RUNNING;
redraw = true;
}
// 一時停止
void pauseTimer(uint32_t now) {
int32_t diff =
(int32_t)(deadlineMs - now);
remainingMs =
diff > 0 ? (uint32_t)diff : 0;
state = PAUSED;
redraw = true;
}
// タイマー終了
void finishTimer() {
remainingMs = 0;
state = ALARM;
beepCount = 0;
beepActive = false;
redraw = true;
}
// 終了音を3回鳴らす
void updateBuzzer(uint32_t now) {
if (state != ALARM) {
return;
}
// 140ms鳴らしたら停止
if (beepActive &&
now - beepStart >= 140) {
noTone(BUZZER_PIN);
beepActive = false;
}
// 380ms間隔で3回鳴らす
if (!beepActive && beepCount < 3) {
if (beepCount == 0 ||
now - lastBeepStart >= 380) {
tone(BUZZER_PIN, 2000);
beepStart = now;
lastBeepStart = now;
beepActive = true;
beepCount++;
}
}
}
// OLEDの画面表示
void drawScreen(uint32_t now) {
uint32_t ms = remainingMs;
if (state == RUNNING) {
int32_t diff =
(int32_t)(deadlineMs - now);
ms = diff > 0 ? (uint32_t)diff : 0;
}
// 残り秒数は切り上げ表示
uint32_t totalSeconds =
(ms + 999) / 1000;
uint32_t minutes =
totalSeconds / 60;
uint32_t seconds =
totalSeconds % 60;
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
// タイトル
display.setTextSize(1);
display.setCursor(0, 0);
display.print("KITCHEN TIMER");
// 残り時間
display.setTextSize(3);
display.setCursor(19, 19);
if (minutes < 10) {
display.print("0");
}
display.print(minutes);
display.print(":");
if (seconds < 10) {
display.print("0");
}
display.print(seconds);
display.setTextSize(1);
// 状態と操作ガイド
display.setCursor(0, 48);
switch (state) {
case SETTING:
display.print("SETTING");
display.setCursor(0, 56);
display.print("1:+ 2:- 3:GO 4:CLR");
break;
case RUNNING:
display.print("RUNNING");
display.setCursor(0, 56);
display.print("3:PAUSE 4:RESET");
break;
case PAUSED:
display.print("PAUSED");
display.setCursor(0, 56);
display.print("3:GO 4:RESET");
break;
case ALARM:
display.print("TIME UP!");
display.setCursor(0, 56);
display.print("SW4: RESET");
break;
}
display.display();
}
void setup() {
Serial.begin(115200);
// スイッチ初期化
for (int i = 0; i < 4; i++) {
pinMode(SW_PINS[i], INPUT_PULLUP);
lastRaw[i] = digitalRead(SW_PINS[i]);
stableSw[i] = lastRaw[i];
}
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// OLED初期化
Wire.begin(SDA_PIN, SCL_PIN);
if (!display.begin(
SSD1306_SWITCHCAPVCC,
OLED_ADDRESS)) {
Serial.println("OLED ERROR");
while (true) {
delay(100);
}
}
drawScreen(millis());
}
void loop() {
uint32_t now = millis();
bool pressed[4];
// 4つのスイッチを確認
for (int i = 0; i < 4; i++) {
pressed[i] = buttonPressed(i, now);
}
// カウントダウンの更新
if (state == RUNNING) {
if ((int32_t)(now - deadlineMs) >= 0) {
finishTimer();
}
}
// SW4:リセット
if (pressed[3]) {
resetTimer();
}
else {
switch (state) {
case SETTING:
// SW1:1分増やす
if (pressed[0] &&
selectedMinutes < 60) {
selectedMinutes++;
remainingMs =
(uint32_t)selectedMinutes * 60000UL;
redraw = true;
}
// SW2:1分減らす
if (pressed[1] &&
selectedMinutes > 1) {
selectedMinutes--;
remainingMs =
(uint32_t)selectedMinutes * 60000UL;
redraw = true;
}
// SW3:開始
if (pressed[2]) {
startTimer(now);
}
break;
case RUNNING:
// SW3:一時停止
if (pressed[2]) {
pauseTimer(now);
}
break;
case PAUSED:
// SW3:再開
if (pressed[2]) {
startTimer(now);
}
break;
case ALARM:
// SW4でリセットするまで待機
break;
}
}
// ブザーの処理
updateBuzzer(now);
// OLED更新
if (redraw ||
(state == RUNNING &&
now - lastDraw >= 150)) {
drawScreen(now);
lastDraw = now;
redraw = false;
}
}
キッチンタイマーを動かしてみよう
プログラムを書き込んだら、実際に操作してみましょう。
① 時間を設定する
電源を入れると、OLEDには初期設定の「03:00」が表示されます。
SW1を押すと1分増加し、SW2を押すと1分減少します。
今回は1〜60分の範囲で設定できます。
② カウントダウンを開始する
SW3を押すと、カウントダウンが始まります。
例えば3分に設定した場合、OLEDの表示は次のように変化します。
03:00
02:59
02:58
02:57
...
00:01
00:00
③ 一時停止・再開する
カウントダウン中にSW3を押すと、一時停止します。
もう一度SW3を押すと、停止した時点の残り時間から再開します。
④ 時間終了時に音を鳴らす
残り時間が0になると、OLEDに「TIME UP!」と表示されます。
同時に、ブザーが3回鳴ります。
SW4を押すとタイマーがリセットされ、再び時間設定ができるようになります。
プログラムの仕組みを解説
① millis()で時間を計測する
今回のキッチンタイマーでは、millis()を使用して時間を管理しています。
millis()は、プログラム起動後の経過時間をミリ秒単位で取得する関数です。
例えば、現在の経過時間が10000msで、3分間のタイマーを開始する場合、終了予定時刻は次のようになります。
deadlineMs = now + remainingMs;
3分は180000ミリ秒なので、
10000 + 180000 = 190000ms
となります。
あとは、現在時刻と終了予定時刻を比較することで、残り時間を求められます。
今回のコードでは、millis()がオーバーフローした場合も考慮して、32ビットの時刻差を利用しています。
② delay()を使わずに時間を管理する理由
例えば、1秒ごとにカウントダウンする場合、次のように書くこともできます。
delay(1000);
remainingSeconds--;
しかし、この方法ではdelay()の実行中、同じ処理の流れでほかの操作を受け付けられません。
そのため、スイッチによる一時停止やリセットなどの操作が遅れることがあります。
また、delay(1000)以外にかかる処理時間が積み重なると、時間のずれが生じる可能性があります。
今回のプログラムでは、終了予定時刻と現在時刻の差を利用しているため、表示更新などの処理時間が単純に積み重なる構造にはなっていません。
③ 状態遷移でタイマーを管理する
今回のプログラムでは、次の4つの状態を使用しています。
| 状態 | 動作 |
|---|---|
| SETTING | 時間を設定する |
| RUNNING | カウントダウン中 |
| PAUSED | 一時停止中 |
| ALARM | 終了通知中 |
プログラムでは、これらをenumで定義しています。
enum TimerState {
SETTING,
RUNNING,
PAUSED,
ALARM
};
例えば、RUNNING状態のときにSW3が押されると、PAUSED状態へ切り替わります。
同じSW3でも、SETTING状態なら開始、PAUSED状態なら再開というように、現在の状態に応じて機能を変えています。
これが「状態遷移」という考え方です。
状態遷移は、家電製品や産業機器など、さまざまな組み込みシステムで使用されています。
④ ブザーで終了を知らせる
今回のプログラムでは、tone()を使ってブザーから音を鳴らしています。
tone(BUZZER_PIN, 2000);
これは、指定したピンから2000Hzの音を出力する命令です。
音を止める場合はnoTone()を使用します。
noTone(BUZZER_PIN);
今回のプログラムでは、140ミリ秒間音を鳴らし、380ミリ秒間隔で合計3回の通知音を鳴らしています。
音を鳴らす処理にもmillis()を使用しているため、ブザー動作中でもSW4によるリセットを受け付けられます。
さらに改造してみよう
基本的なキッチンタイマーが完成したら、次のような機能を追加してみましょう。
① 秒単位で時間を設定できるようにする
今回は1分単位で時間を設定しましたが、例えば10秒単位で設定できるように変更すると、より細かい時間管理ができます。
短い時間で動作確認をしたい場合にも便利です。
② 残り時間に応じてLEDの色を変える
KUMITATE-C3にはRGB LEDも搭載されています。
例えば、次のような表示にしてみましょう。
- 残り1分以上:緑
- 残り1分未満:黄
- 時間終了:赤
OLEDだけでなく光でも残り時間を知らせられます。
③ ブザーのメロディーを変更する
終了時に単純な電子音ではなく、複数の音程を使ったメロディーを鳴らすように変更してみましょう。
周波数と時間を組み合わせることで、さまざまな音を表現できます。
④ 複数のタイマーを同時に動作させる
さらに応用するなら、2つのタイマーを同時に動作させることもできます。
例えば、パスタを茹でる時間と、別の料理の加熱時間を同時に計測するといった使い方です。
複数のタイマーを管理することで、配列や構造体を使ったプログラミングの学習にもつながります。
動作しない場合の確認ポイント
OLEDが表示されない
I²CのSDA・SCLのGPIO番号とI²Cアドレスを確認してください。
KUMITATE-C3では、今回のプログラムでSDAにGPIO1、SCLにGPIO2、アドレスに0x3Cを使用しています。
ボタンを押しても反応しない
使用するGPIO番号と、INPUT_PULLUPの設定を確認してください。
また、SW1〜SW4は押したときにLOWとなる前提のプログラムです。
ブザーから音が出ない
ブザーのGPIO番号と、使用しているブザーの種類を確認してください。
KUMITATE-C3ではGPIO20を使用しています。
なお、ESP32のArduino環境ではコアのバージョンによってPWM関連のAPIが異なる場合があります。今回のプログラムはtone()を使用する構成です。
KUMITATE-C3なら配線なしで製作できる
今回のキッチンタイマーでは、スイッチ入力、OLED表示、時間管理、ブザー制御を組み合わせました。
KUMITATE-C3はESP32-C3を搭載した組み込みプログラミング学習用ボードです。
LED・スイッチ・OLED・ブザーなどが基板に搭載されているため、今回のような電子工作を追加配線なしで学習できます。
また、クミタテではGPIO、I²C、PWM、Wi-Fiなどを段階的に学べるオンライン教材も提供しています。
まとめ
今回はESP32を使って、OLEDとブザーを備えたキッチンタイマーを製作しました。
今回学習した主な内容は次のとおりです。
millis()によるカウントダウン- GPIOによるスイッチ入力
- OLEDによる残り時間の表示
tone()によるブザー制御- 状態遷移による開始・停止・再開
- チャタリング対策
キッチンタイマーは身近な電子工作ですが、入力・表示・時間計測・音の制御など、組み込みプログラミングで重要な要素が多く含まれています。
最初は今回のプログラムをそのまま動かし、その後、秒単位の時間設定やLED通知など、自分なりの機能を追加してみてください。

