/*
* 115-04-11-clock2
* 功能:以 8 顆七段顯示器實現時鐘功能
* - 模式 0:正常走時顯示 HH:MM:SS
* - 模式 1:設定目前時間(時/分)
* - 模式 2:設定鬧鐘時間(時/分)
* 硬體:
* - 兩組 74HC595 串行移位暫存器
* ‧ 第一組:控制七段顯示器的段碼輸出 (abcdefg + dot)
* ‧ 第二組:控制 8 顆七段顯示器的掃描線 (digit select)
* - 4 顆獨立按鍵 (A0~A3 / D14~D17,INPUT_PULLUP)
* - 2 顆 LED 指示燈 (D8、D9)
* - 1 顆蜂鳴器 (A4 / D18):鬧鐘觸發時發出 1kHz 蜂鳴聲
*/
#include <Arduino.h>
// 按鍵掃描間隔 (ms),目前未使用此宏,預留供未來確認節拍用
#define delayT 200
// ======================================================
// 第一組 74HC595:控制七段顯示器的「段碼」資料輸出
// abcdefg + dot (共 8 bit)
// ======================================================
#define DATA_SER1 2 // SER — 段碼序列資料輸入接腳
#define ST_CP1 \
3 // RCLK — 段碼暫存器鎖存時脈(上升緣將移位暫存器資料送到輸出腳)
#define SH_CP1 4 // SRCLK— 段碼移位時脈(每個上升緣將 DATA_SER1 移入一位)
// ======================================================
// 第二組 74HC595:控制七段顯示器的「掃描線」(digit select)
// 決定目前點亮第幾顆七段顯示器
// ======================================================
#define DATA_SER2 5 // SER — 掃描線序列資料輸入接腳
#define ST_CP2 6 // RCLK — 掃描線暫存器鎖存時脈
#define SH_CP2 7 // SRCLK— 掃描線移位時脈
// ======================================================
// 蜂鳴器接腳定義
// ======================================================
#define BUZZER_PIN 18 // A4 — 蜂鳴器輸出接腳(1kHz tone())
// ======================================================
// setup():初始化 I/O 腳位與序列埠
// ======================================================
void setup() {
// D2~D9 設定為輸出,用來驅動兩組 74HC595 及 LED
for (int i = 2; i <= 9; i++)
pinMode(i, OUTPUT);
// A0~A3 (D14~D17) 設定為內建提升電阻輸入,接按鍵(按下 = LOW)
for (int i = 14; i <= 17; i++)
pinMode(i, INPUT_PULLUP); // 使用內建提升電阻,免去外接上拉電阻
// A4 (D18) 設定為輸出,驅動蜂鳴器
pinMode(BUZZER_PIN, OUTPUT);
// 啟動序列埠,鮑率 115200,可用於除錯輸出
Serial.begin(115200);
}
// ======================================================
// 七段顯示器段碼對照表(共陰極,0 = 不亮,Binary → Hex)
// 索引 0~9 →數字 0~9
// 索引 10~15→16進位字元 A~F
// 索引 16 →全暗(消隱)0x00
// 索引 17 →只亮中間一橫(減號)0x40
// ======================================================
byte seg[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x27, 0x7F,
0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x00, 0x40};
// Buff[0]~Buff[7]:8 顆七段顯示器各自要顯示的數值(seg[]的索引)
byte Buff[] = {0, 0, 0, 0, 0, 0, 0, 0};
// scan[]:第二組 74HC595 的掃描線遮罩
// scan[n] 令第 n 顆七段顯示器的公共腳為 HIGH(共陰極點亮選中那顆)
byte scan[] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
// data[] / data1[]:在 disp() 中處理「冒號」位置(位置 2 與 5)
// 顯示動態流水燈效果作為冒號閃爍動畫的幀序列
byte data[] = {0x20, 0x10, 0x08, 0x04, 0x02, 0x01}; // 正向流水
byte data1[] = {0x02, 0x04, 0x08, 0x10, 0x20, 0x01}; // 反向流水
// ──────────────────────────────────────────────────────
// 目前動畫幀索引(0~5 循環),控制冒號流水燈動畫
byte parttern = 0;
// 計數器模組使用的 LED 狀態旗標(保留,目前未使用)
int leddis = 0;
// ======================================================
// disp():驅動「單顆」七段顯示器
// data7s — 要顯示的數值(seg[] 的索引)
// number7s — 選擇第幾顆七段顯示器(0~7)
// ======================================================
void disp(byte data7s, byte number7s) {
// 先將掃描線全部清零(關閉所有顯示器),避免殘影
digitalWrite(ST_CP2, LOW);
shiftOut(DATA_SER2, SH_CP2, MSBFIRST, 0x00);
digitalWrite(ST_CP2, HIGH);
// number7s == 2 或 5 為「冒號」位置,顯示流水燈動畫
if (number7s == 2 || number7s == 5) {
digitalWrite(ST_CP1, LOW);
switch (number7s) {
case 2: // 第 2 顆:使用正向流水幀
shiftOut(DATA_SER1, SH_CP1, MSBFIRST, ~data[parttern]);
break;
case 5: // 第 5 顆:使用反向流水幀
shiftOut(DATA_SER1, SH_CP1, MSBFIRST, ~data1[parttern]);
break;
}
digitalWrite(ST_CP1, HIGH);
} else {
// 一般數字位置:查表 seg[] 取得段碼,取反(共陰極需反相)後送出
digitalWrite(ST_CP1, LOW);
shiftOut(DATA_SER1, SH_CP1, MSBFIRST, ~seg[data7s]);
digitalWrite(ST_CP1, HIGH);
}
// 送出掃描線,選中 number7s 對應的那顆七段顯示器
digitalWrite(ST_CP2, LOW);
shiftOut(DATA_SER2, SH_CP2, MSBFIRST, scan[number7s]);
digitalWrite(ST_CP2, HIGH);
}
// 目前輪到顯示第幾顆七段顯示器(0~7 循環,動態掃描用)
byte no = 0;
// ======================================================
// show():動態掃描函式,每次呼叫點亮下一顆七段顯示器
// 需在 loop() 中高頻呼叫,使視覺上看起來全部同時亮
// ======================================================
void show(void) {
disp(Buff[no], no); // 以 Buff[no] 的值點亮第 no 顆七段顯示器
no = (no + 1) % 8; // 移到下一顆(0→1→…→7→0)
}
// ======================================================
// pow_value():計算 base^number(次方)
// 使用 unsigned long long 避免 16 進位 16^8 超出 32 位元上限
// ======================================================
unsigned long long pow_value(int base, int number) {
unsigned long long value = 1;
for (int i = 1; i <= number; i++)
value = base * value; // 連乘 number 次
return value;
}
// ──────────────────────────────────────────────────────
// 計數器模組全域變數
unsigned long counterM = 0; // 上次更新計數器的時間戳記 (ms)
unsigned long long count = 0; // 目前計數值(支援 8 位元最大進制)
unsigned long count1 = 0, count2 = 0; // 保留,供擴充使用
int startflag = 0; // 計數器啟動旗標:0=停止,1=計數中
int updown = 0; // 計數方向:0=遞增,1=遞減
int baseflag = 0; // 進制選擇:0=十進制,1=十六進制,2=八進制
int Base = 0; // 目前使用的進制值(由 baseflag 決定)
// ======================================================
// counter():計數器功能(目前未掛入 loop,保留供擴充)
// - 依 updown 控制 D8/D9 LED 方向指示
// - 依 baseflag 設定進制
// - 每 300ms 更新一次計數值並分解到 Buff[]
// ======================================================
void counter() {
// 依計數方向點亮對應 LED(D8=遞增,D9=遞減)
if (updown == 0) {
digitalWrite(8, 1); // 遞增模式,D8 亮
digitalWrite(9, 0);
} else {
digitalWrite(8, 0);
digitalWrite(9, 1); // 遞減模式,D9 亮
}
// 根據 baseflag 設定進制
switch (baseflag) {
case 0:
Base = 10;
break; // 十進制
case 1:
Base = 16;
break; // 十六進制
case 2:
Base = 8;
break; // 八進制
default:
break;
}
// 每 300ms 更新一次
if (millis() - counterM >= 300) {
counterM = millis();
// 將 count 分解成 8 位(最高位在 Buff[0],最低位在 Buff[7])
Buff[0] = count / pow_value(Base, 7) % Base;
Buff[1] = count / pow_value(Base, 6) % Base;
Buff[2] = count / pow_value(Base, 5) % Base;
Buff[3] = count / pow_value(Base, 4) % Base;
Buff[4] = count / pow_value(Base, 3) % Base;
Buff[5] = count / pow_value(Base, 2) % Base;
Buff[6] = count / pow_value(Base, 1) % Base;
Buff[7] = count % Base;
// 已啟動計數時才更新計數值
if (startflag == 1) {
if (updown == 0) {
// 遞增,溢位後歸零(模 Base^8)
count = (count + 1) % pow_value(Base, 8);
} else {
// 遞減(利用補數方式讓負數繞回最大值)
count = (count + (pow_value(Base, 8) - 1)) % pow_value(Base, 8);
}
}
}
}
// ──────────────────────────────────────────────────────
// 時鐘模組全域變數
unsigned long clockM = 0; // 上次秒計時更新的時間戳記 (ms)
unsigned long patternM = 0; // 上次冒號動畫更新的時間戳記 (ms)
byte hour = 23, min = 59, sec = 50; // 目前時刻(初始值方便觀察進位)
byte hour1 = 0, min1 = 0, sec1 = 0; // 鬧鐘設定時刻
byte mode = 0; // 顯示模式:0=正常走時,1=設定目前時間,2=設定鬧鐘
byte alarmflag = 0; // 鬧鐘開關:0=關閉,1=開啟
byte flash = 0; // 模式 1 閃爍計數器(0~15 循環)
// ======================================================
// clock_count():時鐘走時與 Buff[] 更新(模式 0 使用)
// - 每 100ms 更新一次冒號動畫幀 (parttern)
// - 每 1000ms 進行秒/分/時進位
// - 將 HH MM SS 分解寫入 Buff[]
// ======================================================
void clock_count() {
// 每 100ms 切換一次冒號流水燈動畫幀
if (millis() - patternM >= 100) {
patternM = millis();
parttern = (parttern + 1) % 6; // 幀索引 0~5 循環
}
// 每 1000ms 秒數遞增,並進行分/時進位
if (millis() - clockM >= 1000) {
clockM = millis();
sec = (sec + 1) % 60; // 秒:0~59
if (sec == 0) {
min = (min + 1) % 60; // 分:0~59
if (min == 0) {
hour = (hour + 1) % 24; // 時:0~23
}
}
}
// 將時/分/秒各位數寫入顯示緩衝區
Buff[0] = hour / 10; // 時十位
Buff[1] = hour % 10; // 時個位
// Buff[2] 為冒號位置,由 disp() 中的流水燈動畫處理
Buff[3] = min / 10; // 分十位
Buff[4] = min % 10; // 分個位
// Buff[5] 為冒號位置,由 disp() 中的流水燈動畫處理
Buff[6] = sec / 10; // 秒十位
Buff[7] = sec % 10; // 秒個位
}
// ──────────────────────────────────────────────────────
// 設定時間模組全域變數
unsigned long setnowtimeM = 0; // 上次更新設定時間顯示的時間戳記
unsigned long flashM = 0; // 右側閃爍計時用時間戳記
// ======================================================
// setnowtime():設定目前時間顯示(模式 1 使用)
// - 每 100ms 更新 Buff[0~4](顯示 HH:MM)
// - 右側 Buff[6~7] 以 40ms 為閃爍節拍
// 前半顯示目前模式碼,後半清空(消隱)
// ======================================================
void setnowtime() {
// 每 100ms 更新一次時/分顯示(HH:MM)
if (millis() - setnowtimeM >= 100) {
setnowtimeM = millis();
Buff[0] = hour / 10; // 時十位
Buff[1] = hour % 10; // 時個位
Buff[3] = min / 10; // 分十位
Buff[4] = min % 10; // 分個位
}
// 每 40ms 切換一次右側閃爍(共 16 步為一個週期)
if (millis() - flashM >= 40) {
flashM = millis();
flash = (flash + 1) % 16; // 0~15 循環
if (flash < 8) {
// 前 8 步:顯示模式碼(mode = 1,顯示 "01")
Buff[6] = mode / 10;
Buff[7] = mode % 10;
} else {
// 後 8 步:消隱(使用 seg[16] = 0x00 全暗)
Buff[6] = 16;
Buff[7] = 16;
}
}
}
// ──────────────────────────────────────────────────────
// 鬧鐘設定模組全域變數
unsigned long setalarmtimeM = 0; // 上次更新鬧鐘顯示的時間戳記
unsigned long flashM1 = 0; // 右側閃爍計時用時間戳記
int flash1 = 0; // 閃爍計數器(0~15 循環)
// ======================================================
// setalarmtime():設定鬧鐘時間顯示(模式 2 使用)
// 邏輯與 setnowtime() 相同,但操作的是 hour1/min1
// ======================================================
void setalarmtime() {
// 每 100ms 更新一次鬧鐘時/分顯示(HH:MM)
if (millis() - setalarmtimeM >= 100) {
setalarmtimeM = millis();
Buff[0] = hour1 / 10; // 鬧鐘時十位
Buff[1] = hour1 % 10; // 鬧鐘時個位
Buff[3] = min1 / 10; // 鬧鐘分十位
Buff[4] = min1 % 10; // 鬧鐘分個位
}
// 每 40ms 切換一次右側閃爍
if (millis() - flashM1 >= 40) {
flashM1 = millis();
flash1 = (flash1 + 1) % 16; // 0~15 循環
if (flash1 < 8) {
// 前 8 步:顯示模式碼(mode = 2,顯示 "02")
Buff[6] = mode / 10;
Buff[7] = mode % 10;
} else {
// 後 8 步:消隱
Buff[6] = 16;
Buff[7] = 16;
}
}
}
// ──────────────────────────────────────────────────────
// 鬧鐘響鈴模組全域變數
unsigned long alarmshowM = 0; // 上次 LED 狀態切換的時間戳記 (ms)
byte alarmLedState = 0; // LED 目前狀態:0=滅,1=亮
byte ringing = 0; // 鬧鐘響鈴旗標:0=未響,1=響鈴中(持續閃爍直到按鍵取消)
// ======================================================
// alarmshow():鬧鐘響鈴時的 LED 閃爍 + 蘑鳴器控制
// 以非阻塞式 millis() 實現:
// 亮 200ms + 蘑鳴 → 滅 300ms + 静音 → 亮 200ms + 蘑鳴 → …
// D8 與 D9 同步閃爍,蘑鳴器同步發出 1kHz 袁鳴聲
// ======================================================
void alarmshow() {
if (alarmLedState == 1) {
// 目前 LED 亮,等待 200ms 後關閉
if (millis() - alarmshowM >= 200) {
alarmshowM = millis(); // 記錄狀態切換時刻
alarmLedState = 0; // 切換至「滅」狀態
digitalWrite(8, 0); // D8 滅
digitalWrite(9, 0); // D9 滅
noTone(BUZZER_PIN); // 蘑鳴器停止發音
}
} else {
// 目前 LED 滅,等待 300ms 後點亮
if (millis() - alarmshowM >= 300) {
alarmshowM = millis(); // 記錄狀態切換時刻
alarmLedState = 1; // 切換至「亮」狀態
digitalWrite(8, 1); // D8 亮
digitalWrite(9, 1); // D9 亮
tone(BUZZER_PIN, 1000); // 蘑鳴器發出 1kHz 袁鳴聲
}
}
}
// ======================================================
// isontime():鬧鐘觸發判斷與持續閃爍
// - alarmflag==1 且 HH:MM:SS 吻合時,設 ringing=1(一次性觸發)
// - 只要 ringing==1 就持續呼叫 alarmshow() 保持閃爍
// - 按 S0 關閉鬧鐘時由 switchkey() 將 ringing 清為 0
// ======================================================
void isontime() {
// 時間吻合瞬間:設定響鈴旗標(之後秒數跑過去仍會繼續響)
if (alarmflag == 1 && hour == hour1 && min == min1 && sec == sec1) {
ringing = 1; // 觸發響鈴
}
// 持續閃爍直到 ringing 被清除
if (ringing == 1) {
alarmshow();
}
}
// ======================================================
// ReadKey():讀取 4 顆按鍵狀態,回傳 4 位元旗標
// - bit0 (1):S3 (D14) 按下
// - bit1 (2):S2 (D15) 按下
// - bit2 (4):S1 (D16) 按下
// - bit3 (8):S0 (D17) 按下
// ======================================================
char ReadKey(void) {
char key = 0;
if (digitalRead(14) == 0)
key |= 1; // S3 按下(低電位)
if (digitalRead(15) == 0)
key |= 2; // S2 按下(低電位)
if (digitalRead(16) == 0)
key |= 4; // S1 按下(低電位)
if (digitalRead(17) == 0)
key |= 8; // S0 按下(低電位)
return key; // 回傳按鍵狀態位元組
}
// ──────────────────────────────────────────────────────
// 按鍵掃描模組全域變數
unsigned long switchkeyM = 0; // 上次按鍵掃描時間戳記
int newkey, oldkey; // 目前與上次的按鍵值(用於邊緣偵測)
// ======================================================
// switchkey():按鍵邏輯處理(每 50ms 掃描一次)
// 偵測「按下瞬間」(newkey != oldkey 且 newkey != 0)
//
// 按鍵功能對應:
// newkey == 1 (S3):切換模式 0→1→2→0
// mode 0:D8/D9 全滅(正常走時)
// mode 1:D8 亮/D9 滅(設定時間提示)
// mode 2:D8 滅/D9 亮(設定鬧鐘提示)
//
// newkey == 2 (S2):小時 +1
// mode 1 → 調整目前時間 hour
// mode 2 → 調整鬧鐘 hour1
//
// newkey == 4 (S1):分鐘 +1
// mode 1 → 調整目前時間 min
// mode 2 → 調整鬧鐘 min1
//
// newkey == 8 (S0):切換鬧鐘開關
// alarmflag 0↔1
// 開啟時 D8/D9 同時亮,關閉時全滅
// ======================================================
void switchkey() {
// 每 50ms 才執行一次(軟體去彈跳)
if (millis() - switchkeyM >= 50) {
switchkeyM = millis();
newkey = ReadKey(); // 讀取目前按鍵狀態
// 僅在按鍵狀態改變時(邊緣偵測)執行對應動作
if (newkey != oldkey) {
// ── S3:模式切換 ──────────────────────────────
if (newkey == 1) {
mode = (mode + 1) % 3; // 0 → 1 → 2 → 0 循環
switch (mode) {
case 0: // 正常走時:LED 全滅
digitalWrite(8, 0);
digitalWrite(9, 0);
break;
case 1: // 設定時間:D8 亮
digitalWrite(8, 1);
digitalWrite(9, 0);
break;
case 2: // 設定鬧鐘:D9 亮
digitalWrite(8, 0);
digitalWrite(9, 1);
break;
}
}
// ── S2:小時 +1 ──────────────────────────────
if (newkey == 2) {
if (mode == 1) {
hour = (hour + 1) % 24; // 調整目前時間時
} else if (mode == 2) {
hour1 = (hour1 + 1) % 24; // 調整鬧鐘時
}
// mode 0 時按下無效
}
// ── S1:分鐘 +1 ──────────────────────────────
if (newkey == 4) {
if (mode == 1) {
min = (min + 1) % 60; // 調整目前時間分
} else if (mode == 2) {
min1 = (min1 + 1) % 60; // 調整鬧鐘分
}
// mode 0 時按下無效
}
// ── S0:鬧鐘開關切換 / 響鈴中按下則停止閃爍 ────
if (newkey == 8) {
if (ringing == 1) {
// 響鈴中按 S0 → 停止閃爍,但保持 alarmflag 不變
ringing = 0; // 清除響鈴旗標
noTone(BUZZER_PIN); // 蘑鳴器停止
digitalWrite(8, 0); // D8 滅
digitalWrite(9, 0); // D9 滅
} else {
alarmflag = (1 + alarmflag) % 2; // 0↔1 切換
if (alarmflag == 1) {
// 鬧鐘開啟:D8、D9 同時亮
digitalWrite(8, 1);
digitalWrite(9, 1);
} else {
// 鬧鐘關閉:D8、D9 同時滅
digitalWrite(8, 0);
digitalWrite(9, 0);
}
}
}
}
oldkey = newkey; // 記錄本次按鍵狀態,供下次比較
}
}
// ======================================================
// loop():主迴圈
// 1. switchkey() — 每 50ms 處理一次按鍵輸入
// 2. show() — 每次迴圈推進動態掃描一顆七段顯示器
// 3. 依 mode 呼叫對應顯示更新函式:
// mode 0 → clock_count() (正常走時)
// mode 1 → setnowtime() (設定時間)
// mode 2 → setalarmtime() (設定鬧鐘)
// ======================================================
void loop() {
switchkey(); // 按鍵掃描與功能處理
show(); // 七段顯示器動態掃描(每次點亮一顆)
// 依目前模式更新顯示緩衝區 Buff[]
if (mode == 0) {
clock_count(); // 模式 0:走時並更新 HH:MM:SS
} else if (mode == 1) {
setnowtime(); // 模式 1:設定目前時間顯示
} else if (mode == 2) {
setalarmtime(); // 模式 2:設定鬧鐘時間顯示
}
isontime(); // 鬧鐘時間到 且 alarmflag==1 時才閃爍
}