第 17 章 專題製作
本章目標
- 學完你會照著「問題 → 規劃 → 接線 → 程式 → 測試 → 展示」六個步驟,把一個想法變成會動的作品。
- 學完你會獨立完成五個專題中的至少兩個:一個用 UNO、一個用 ESP32。
- 學完你會用示波器或邏輯分析儀「證明」你的作品真的照你想的方式在運作,而不是只看它會不會動。
- 學完你會寫一份有量測數據的專題報告,並在三分鐘內把作品講清楚。
開場:從「會動」到「我知道它為什麼會動」
社團成果發表會前兩週,小安把一個夜燈接好了:天黑 LED 就亮,天亮就暗。她很開心,直到隔壁的學長問了一句:「你的燈是漸漸變亮,還是一下子跳亮?」她愣住了。肉眼看起來好像是漸漸的……但她沒辦法確定,也說不出 LED 收到的訊號到底長什麼樣。
學長把示波器探棒鉤上去,螢幕上出現一串方波,寬窄正在慢慢變化。「這叫占空比,你第 14 章學過。你的燈的確是漸亮的,而且從全暗到全亮花了大概 2.5 秒。把這張波形放進報告,評審一看就知道你不只是接對線,你還懂它。」
這一章就是要帶你走到那一步。前面十六章你學了元件、儀器和程式;現在要把它們組合成完整的作品,而且每一個作品都要能用儀器拿出證據。五個專題難度由淺到深,你可以挑一個做,也可以全部做完。最後我們會談怎麼寫報告、怎麼上台。
核心概念
專題的六個步驟
比喻:做專題像蓋一間樹屋。你不會一開始就釘木板,而是先想「要給誰用、要多大」(問題),畫個草圖、列材料(規劃),再搭骨架(接線)、裝門窗(程式),爬上去跳一跳看會不會垮(測試),最後邀朋友來參觀(展示)。
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ 問題 │──►│ 規劃 │──►│ 接線 │──►│ 程式 │──►│ 測試 │──►│ 展示 │
└──────┘ └──────┘ └──────┘ └──────┘ └──┬───┘ └──────┘
▲ │
└────── 測試不過就回頭改 ─────────┘
1. 問題:用一句話說清楚「誰、在什麼情況下、需要什麼」。例如:「晚上起床上廁所的人,需要一盞不刺眼、會自己亮的小燈。」寫不出這句話,後面都會很模糊。
2. 規劃:把問題拆成「輸入 → 處理 → 輸出」三格,再把每一格填上你會的元件。
| 輸入(感測) | 處理(微控制器做判斷) | 輸出(動作) |
|---|---|---|
| 光敏電阻量亮度 | 太暗就慢慢調高 PWM | LED 漸亮 |
填好以後,列材料清單,並且翻回前面的章節確認每個元件你都接過。這一步最常被跳過,也最常導致後面卡住。
3. 接線:先接電源和 GND,再接輸入,最後接輸出。每接完一部分就用三用電表量一下該有電壓的地方有沒有電壓。一次只接一個部分。
4. 程式:先寫「最小可動版本」,例如只讀感測器印到序列埠;確認數字合理,再加判斷;最後加輸出。每加一段就上傳一次。千萬不要一口氣寫完 100 行才第一次上傳。
5. 測試:分三層。第一層「看起來會動」;第二層「序列埠印出的數字合理」;第三層「用示波器或邏輯分析儀量到的波形跟程式設定的一樣」。本章每個專題都會做到第三層。
6. 展示:準備一張海報或三張投影片、一段兩分鐘的現場操作、一份附量測數據的報告。章末會細講。
專題程式的共同骨架
五個專題的程式看起來很長,但骨架都一樣:
// 1. 腳位與常數:所有數字集中放在最上面,要調整時不用到處找
// 2. 全域變數:記住「現在的狀態」,例如 LED 亮度、風扇段速
// 3. 小函式:每個函式只做一件事(讀感測器、更新螢幕、處理網頁)
// 4. setup():設定腳位、啟動序列埠、連 Wi-Fi、初始化螢幕
// 5. loop():用 millis() 排程,不要用長 delay()
第 5 點在 ESP32 專題裡特別重要:網頁伺服器需要 loop() 一直跑 server.handleClient(),一個 delay(2000) 就會讓手機按鈕兩秒沒反應。寫法是記住「上次做這件事的時間」,每圈 loop() 檢查「現在減上次」有沒有超過間隔:
if (millis() - lastReadTime >= 2000) { // 距離上次讀已經 2 秒了嗎?
lastReadTime = millis(); // 記住這次的時間
readSensor(); // 做該做的事
}
除錯的三個習慣
- 先量電,再看程式:七成的「不會動」是接線問題。三用電表量 VCC 和 GND 之間有沒有正確電壓,是第一件事。
- 每個關鍵數值都
Serial.print():感測器讀值、算出來的 PWM 值、收到的網頁路徑。看得到數字才知道哪一段出錯。 - 一次只改一個地方:改了接線就不要同時改程式。不然你不知道是哪個改動讓它好起來或壞掉。
動手做
每個專題的結構都一樣:目標 → 材料 → 接線 → 程式 → 測試方法 → 延伸挑戰。「測試方法」裡至少有一項要用示波器或邏輯分析儀,請把量到的數字填進表格,這些就是你報告裡的證據。
實驗 17-1:專題一 智慧夜燈
目標
做一盞用光敏電阻感應環境亮度的夜燈。環境越暗,LED 越亮;而且亮度是漸漸變化的,不會一下子跳亮或跳暗。整個過程用示波器證明 PWM 占空比真的在連續變化。
材料
- Arduino UNO ×1
- 光敏電阻(LDR)×1
- 10kΩ 電阻 ×1
- 白色 LED ×1(紅色也可以,但夜燈用白色或暖色比較像真的)
- 220Ω 電阻 ×1
- 麵包板、杜邦線
接線
UNO 5V ────[ LDR ]────┬────[ 10kΩ ]──── GND
│
UNO A0
UNO D9 ──[ 220Ω ]──►|── GND
白色 LED
文字說明:
- UNO 5V → 光敏電阻一腳;光敏電阻另一腳 → A0,同一點 → 10kΩ → GND。
- UNO D9 → 220Ω → LED 長腳;LED 短腳 → GND。
光敏電阻在亮的時候電阻小(幾百歐姆),暗的時候電阻大(幾十 kΩ 以上)。這個分壓器讓 A0 在亮的時候電壓高、暗的時候電壓低,第 13 章做過。
程式
// 專題一:智慧夜燈(UNO)
// 天暗 → LED 漸亮;天亮 → LED 漸暗
const int ldrPin = A0; // 光敏電阻分壓點
const int ledPin = 9; // PWM 腳位,接 LED
// 這兩個數值要依你的教室調整:先看序列埠,記下「蓋住」和「正常光」的讀值
const int darkValue = 200; // 低於這個算全暗 → LED 全亮
const int brightValue = 700; // 高於這個算全亮 → LED 全暗
int currentBrightness = 0; // 目前 LED 的 PWM 值(0–255)
const int fadeStepDelay = 10; // 每 10 毫秒調整 1,全程 255 步 ≈ 2.5 秒
void setup() {
pinMode(ledPin, OUTPUT);
Serial.begin(9600);
Serial.println("smart night light start");
}
void loop() {
int light = analogRead(ldrPin); // 0–1023,越亮越大
// 把亮度讀值對應成「目標 LED 亮度」:暗 → 255,亮 → 0
int target = map(light, darkValue, brightValue, 255, 0);
target = constrain(target, 0, 255); // 超出範圍就夾在 0–255
// 每圈只往目標走一步,所以變化是漸漸的
if (currentBrightness < target) {
currentBrightness++;
} else if (currentBrightness > target) {
currentBrightness--;
}
analogWrite(ledPin, currentBrightness); // 輸出 PWM
// 印成「名稱:數值」格式,序列繪圖器會畫成兩條線
Serial.print("light:");
Serial.print(light);
Serial.print(",led:");
Serial.println(currentBrightness);
delay(fadeStepDelay);
}
步驟
- 接線完成後,先用三用電表量 A0 對 GND:正常光線下應該有 2–4V,用手蓋住光敏電阻會掉到 1V 以下。沒有變化就是分壓器接錯了。
- 上傳程式,打開序列繪圖器(工具 → 序列繪圖器,鮑率 9600)。
- 看
light那條線,在正常光線和蓋住的情況各記一個數值,填進程式的brightValue和darkValue,重新上傳。 - 用手慢慢蓋住光敏電阻,看 LED 和
led那條線。
你應該看到
蓋住光敏電阻後,light 線立刻往下掉,但 led 線是一條斜斜往上爬的線,花大約 2.5 秒才到 255;LED 也跟著慢慢變亮。放開手則反過來。
測試方法(示波器)
探棒鉤 D9,鱷魚夾接 GND。設定 2 V/div、500 µs/div、觸發 CH1 上升緣 2.5V。
電壓(LED 全暗,占空比 0%)
5V ┤
0V ┼──────────────────────────── 時間
電壓(半亮,占空比約 50%)
5V ┤ ┌────┐ ┌────┐ ┌────┐
0V ┼──┘ └────┘ └────┘ └─
電壓(LED 全亮,占空比 100%)
5V ┤──────────────────────────────
0V ┼
◄────────►
約 2.04 ms(490 Hz)
用手慢慢蓋住光敏電阻,你會看到方波的「高的部分」慢慢變寬,直到整條線貼在 5V。用示波器的自動量測功能讀「頻率」和「正占空比(+Duty)」,填表:
| 情況 | 序列埠的 led 值 | 示波器占空比 | 算出來的 led 值(占空比 × 255) | 頻率 |
|---|---|---|---|---|
| 正常光線 | ||||
| 半蓋住 | ||||
| 全蓋住 |
舉例:如果示波器讀到占空比 40%,0.40 × 255 = 102,應該跟序列埠印的 led 值差不多。頻率應該接近 490 Hz,這是 UNO D9 的 PWM 頻率。
延伸挑戰
- 加一顆按鈕(D2,INPUT_PULLUP):按一下切換「自動/強制關燈」,半夜不想被亮到的時候可以關掉。
- 把 LED 換成 2N2222 電晶體驅動三顆白色 LED 並聯(第 9 章接法),亮度夠照亮床邊。
- 加一個「亮了 30 分鐘就自動變暗」的功能,用
millis()計時。
實驗 17-2:專題二 溫濕度氣象站
目標
做一台放在教室的氣象站:OLED 顯示目前溫濕度和今天的最高、最低溫;手機打開網頁可以看到目前數值和最近 24 筆溫度的長條圖。用邏輯分析儀證明 ESP32 確實用 I2C 在跟 OLED 講話。
材料
- ESP32 DevKit V1 ×1
- DHT11 溫濕度感測器 ×1
- 0.96 吋 OLED(SSD1306,I2C)×1
- 10kΩ 電阻 ×1(裸元件四腳版 DHT11 才需要)
- 麵包板、杜邦線
- USB 邏輯分析儀(測試用)
接線
┌─────────────────┐
3V3 ────────┤ VCC DHT11 │
GPIO 4 ─────┤ DATA │
GND ────────┤ GND │
└─────────────────┘
┌─────────────────┐
3V3 ────────┤ VCC OLED │
GND ────────┤ GND SSD1306 │
GPIO 21 ────┤ SDA │
GPIO 22 ────┤ SCL │
└─────────────────┘
文字說明:跟第 16 章實驗 16-4 完全一樣。DHT11 DATA → GPIO 4;OLED SDA → GPIO 21、SCL → GPIO 22;兩個元件都用 3V3 和 GND。
程式
// 專題二:溫濕度氣象站(ESP32)
// OLED 顯示目前值與最高最低溫,網頁顯示數值與最近 24 筆長條圖
#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const char* ssid = "你的Wi-Fi名稱";
const char* password = "你的Wi-Fi密碼";
#define DHTPIN 4
DHT dht(DHTPIN, DHT11);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
WebServer server(80);
float temperature = 0, humidity = 0; // 目前值
float tempMax = -100, tempMin = 100; // 開機以來的最高、最低溫
const int HISTORY_SIZE = 24; // 記最近 24 筆
float tempHistory[HISTORY_SIZE];
int historyCount = 0; // 目前記了幾筆
// 展示時可以把 60000 改成 5000(每 5 秒一筆),長條圖很快就會長出來
const unsigned long HISTORY_INTERVAL = 60000UL;
unsigned long lastHistoryTime = 0;
unsigned long lastReadTime = 0;
// 讀感測器並更新最高最低
void readSensor() {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (isnan(t) || isnan(h)) { // 讀失敗就跳過這次
Serial.println("DHT11 read failed");
return;
}
temperature = t;
humidity = h;
if (t > tempMax) tempMax = t; // 更新紀錄
if (t < tempMin) tempMin = t;
}
// 把目前溫度加進歷史紀錄,滿了就把最舊的擠掉
void addHistory() {
if (historyCount < HISTORY_SIZE) {
tempHistory[historyCount] = temperature;
historyCount++;
} else {
for (int i = 0; i < HISTORY_SIZE - 1; i++) {
tempHistory[i] = tempHistory[i + 1]; // 全部往前移一格
}
tempHistory[HISTORY_SIZE - 1] = temperature;
}
}
// OLED 畫面
void showOnOled() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.print("http://");
display.println(WiFi.localIP()); // 第一行印網址
display.setTextSize(2);
display.setCursor(0, 16);
display.print(temperature, 1);
display.print("C ");
display.print(humidity, 0);
display.println("%");
display.setTextSize(1); // 第三行印最高最低
display.setCursor(0, 44);
display.print("Max ");
display.print(tempMax, 1);
display.print(" Min ");
display.println(tempMin, 1);
display.setCursor(0, 56);
display.print("Up ");
display.print(millis() / 60000); // 開機幾分鐘
display.println(" min");
display.display();
}
// 網頁:目前值、最高最低、長條圖
void handleRoot() {
String html = "<!DOCTYPE html><html><head><meta charset='utf-8'>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<meta http-equiv='refresh' content='10'>";
html += "<title>教室氣象站</title></head>";
html += "<body style='font-family:sans-serif;margin:20px'>";
html += "<h1>教室氣象站</h1>";
html += "<p style='font-size:2.5em;margin:0'>" + String(temperature, 1) + " °C</p>";
html += "<p style='font-size:2.5em;margin:0'>" + String(humidity, 0) + " %</p>";
html += "<p>最高 " + String(tempMax, 1) + " °C 最低 " + String(tempMin, 1) + " °C";
html += " 已開機 " + String(millis() / 60000) + " 分鐘</p>";
html += "<h2>最近 " + String(historyCount) + " 筆溫度</h2>";
// 每一筆畫一條長條:溫度 10°C = 0 像素,40°C = 300 像素
for (int i = 0; i < historyCount; i++) {
int width = (int)((tempHistory[i] - 10.0) * 10.0);
width = constrain(width, 2, 300);
html += "<div style='display:flex;align-items:center;margin:2px'>";
html += "<div style='background:#4a90e2;height:14px;width:" + String(width) + "px'></div>";
html += "<span style='margin-left:6px'>" + String(tempHistory[i], 1) + "</span></div>";
}
html += "</body></html>";
server.send(200, "text/html; charset=utf-8", html);
}
void setup() {
Serial.begin(115200);
dht.begin();
Wire.begin(21, 22);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("OLED not found");
while (true) delay(10);
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Connecting WiFi...");
display.display();
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("http://");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.begin();
delay(1500); // DHT11 開機要一點時間
readSensor();
addHistory(); // 第一筆
showOnOled();
}
void loop() {
server.handleClient();
if (millis() - lastReadTime >= 2000) { // 每 2 秒讀一次、更新 OLED
lastReadTime = millis();
readSensor();
showOnOled();
}
if (millis() - lastHistoryTime >= HISTORY_INTERVAL) { // 定時記一筆
lastHistoryTime = millis();
addHistory();
Serial.print("history ");
Serial.print(historyCount);
Serial.print(" : ");
Serial.println(temperature);
}
}
步驟
- 填入 Wi-Fi 名稱密碼,上傳。
- OLED 第一行出現網址,手機輸入。
- 展示時把
HISTORY_INTERVAL改成 5000,讓長條圖在兩分鐘內長出 24 條;正式使用再改回 60000。 - 用手捏住 DHT11 一分鐘,看最高溫紀錄會不會更新。
你應該看到
OLED 顯示三行:網址、大字溫濕度、最高最低與開機時間。網頁每 10 秒更新,長條圖一條一條增加,捏住感測器後長條變長。
測試方法(邏輯分析儀)
邏輯分析儀 CH0 → GPIO 21(SDA),CH1 → GPIO 22(SCL),GND → ESP32 GND。PulseView 取樣率 2 MHz、取樣 2 秒,加入 I2C 解碼器(SDA=CH0、SCL=CH1)。
SCL ─┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌─
└┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘
SDA ─┐ ┌──┐ ┌─────┐ ┌── ...
└───┘ └──┘ └─────┘
S 0 1 1 1 1 0 0 W A ← 解碼器顯示:位址 0x3C,寫入,ACK
每 2 秒應該出現一大串 I2C 封包(那是 display.display() 把整個畫面 1024 個位元組送過去)。填表:
| 項目 | 你量到的 |
|---|---|
| 解碼出的 OLED 位址 | (應該是 0x3C) |
| 兩串封包之間的間隔 | (應該約 2 秒) |
| 一串封包總共多長(毫秒) | |
| SCL 一個週期的時間 → 算出 I2C 時脈頻率 |
舉例:如果 SCL 一個週期是 10 µs,頻率就是 1 ÷ 0.00001 = 100 kHz,這是 I2C 的標準速度。
延伸挑戰
- 加入濕度的長條圖,用不同顏色。
- 用第 16 章的深度睡眠,改成電池供電版本:醒來 10 秒把數值印在 OLED 上,然後睡 5 分鐘。
- 濕度超過 80% 時讓 GPIO 2 的 LED 閃爍提醒。
實驗 17-3:專題三 手機 Wi-Fi 遙控風扇
目標
做一台可以用手機網頁控制「關/低/中/高」四段速的小風扇。ESP32 輸出 PWM,經 IRLZ44N MOSFET 驅動直流馬達。用示波器量 MOSFET 閘極與汲極的波形,證明三段速真的是三種占空比。
材料
- ESP32 DevKit V1 ×1
- N 通道 MOSFET IRLZ44N ×1
- 小直流馬達(3–6V)×1,可以在軸上黏一片紙板當扇葉
- 整流二極體 1N4007 ×1(飛輪二極體)
- 220Ω 電阻 ×1(閘極電阻)
- 10kΩ 電阻 ×1(閘極下拉)
- 3 號電池盒(3 顆,4.5V)×1
- 麵包板、杜邦線
接線
IRLZ44N(正面看,腳位由左到右 G D S)
ESP32 GPIO 18 ──[ 220Ω ]──┬── G
│
[ 10kΩ ]
│
GND
電池盒 +4.5V ──┬───( 馬達 )───┬── D
│ │
└───|◄─────────┘ 1N4007:有條紋的那端朝電池正極
1N4007
S ── GND ──┬── 電池盒 −
└── ESP32 GND(一定要跟電池盒負極接在一起)
文字說明:
- ESP32 GPIO 18 → 220Ω → IRLZ44N 閘極(G);G 另外 → 10kΩ → GND(確保 ESP32 還沒開機時 MOSFET 是關的)。
- 電池盒正極 → 馬達一腳;馬達另一腳 → IRLZ44N 汲極(D)。
- 1N4007 跨接在馬達兩端:有條紋的一端接電池正極那側,另一端接汲極那側。沒有它,關掉馬達時的尖波會打壞 MOSFET(第 7 章看過)。
- IRLZ44N 源極(S)→ GND。電池盒負極、ESP32 GND、MOSFET 的 S 三個點要接在一起。
- 不要用 ESP32 的 3V3 腳供電給馬達,馬達啟動電流會讓 ESP32 重開。
程式
// 專題三:手機 Wi-Fi 遙控風扇(ESP32 + IRLZ44N)
#include <WiFi.h>
#include <WebServer.h>
const char* ssid = "你的Wi-Fi名稱";
const char* password = "你的Wi-Fi密碼";
const int fanPin = 18; // PWM 輸出 → MOSFET 閘極
const int PWM_FREQ = 1000; // 1 kHz,馬達不會發出刺耳的聲音
const int PWM_RES = 8; // 8 位元 → 0–255
int fanLevel = 0; // 0 關、1 低、2 中、3 高
const int levelDuty[4] = {0, 110, 180, 255}; // 各段的 PWM 值;低速太小馬達會轉不動
const char* levelName[4] = {"關", "低", "中", "高"};
WebServer server(80);
// 設定風扇段速
void setFan(int level) {
fanLevel = constrain(level, 0, 3);
ledcWrite(fanPin, levelDuty[fanLevel]); // 輸出對應的占空比
Serial.print("fan level ");
Serial.print(fanLevel);
Serial.print(" duty ");
Serial.println(levelDuty[fanLevel]);
}
// 網頁:四顆大按鈕
String makePage() {
String html = "<!DOCTYPE html><html><head><meta charset='utf-8'>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<title>Wi-Fi 風扇</title></head>";
html += "<body style='font-family:sans-serif;text-align:center;margin-top:30px'>";
html += "<h1>Wi-Fi 風扇</h1>";
html += "<p style='font-size:1.5em'>目前:<b>" + String(levelName[fanLevel]) + "</b>";
html += "(PWM " + String(levelDuty[fanLevel]) + "/255)</p>";
const char* paths[4] = {"/off", "/low", "/mid", "/high"};
for (int i = 0; i < 4; i++) {
html += "<p><a href='" + String(paths[i]) + "'><button style='font-size:1.8em;width:60%;padding:15px";
if (i == fanLevel) html += ";background:orange"; // 目前段速的按鈕上色
html += "'>" + String(levelName[i]) + "</button></a></p>";
}
html += "</body></html>";
return html;
}
void sendPage() {
server.send(200, "text/html; charset=utf-8", makePage());
}
void handleRoot() { sendPage(); }
void handleOff() { setFan(0); sendPage(); }
void handleLow() { setFan(1); sendPage(); }
void handleMid() { setFan(2); sendPage(); }
void handleHigh() { setFan(3); sendPage(); }
void setup() {
Serial.begin(115200);
// 設定 PWM(esp32 核心 3.x 的寫法)
ledcAttach(fanPin, PWM_FREQ, PWM_RES);
// 如果你的 esp32 核心是 2.x,編譯會說找不到 ledcAttach,請改用下面三行:
// ledcSetup(0, PWM_FREQ, PWM_RES);
// ledcAttachPin(fanPin, 0);
// 並把 setFan 裡的 ledcWrite(fanPin, ...) 改成 ledcWrite(0, ...)
setFan(0); // 一開機先確定風扇是關的
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("connecting");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("http://");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/off", handleOff);
server.on("/low", handleLow);
server.on("/mid", handleMid);
server.on("/high", handleHigh);
server.begin();
}
void loop() {
server.handleClient(); // 不要加 delay
}
步驟
- 先不要裝電池。接好所有線後,用三用電表確認 ESP32 GND 和電池盒負極之間是 0Ω(通路)。
- 上傳程式,看序列埠拿到 IP。
- 裝上電池,手機打開網頁,依序按「低、中、高、關」。
- 用手摸 IRLZ44N 的外殼,跑一分鐘高速後應該只是微溫。如果燙手,檢查是不是 G、D、S 接反了。
你應該看到
按「低」馬達慢慢轉(有時候要用手撥一下才會啟動,這是正常的),「中」明顯變快,「高」最快,「關」停下來。網頁上目前段速的按鈕會變橘色。
測試方法(示波器,雙通道)
CH1 探棒鉤 IRLZ44N 的 G(閘極),CH2 鉤 D(汲極),兩個鱷魚夾都接 GND。設定 CH1 2 V/div、CH2 2 V/div、時間 200 µs/div,觸發 CH1 上升緣 1.5V。
CH1 閘極(中速,占空比約 70%)
3.3V ┤ ┌───────┐ ┌───────┐ ┌───────┐
0V ┼─┘ └───┘ └───┘ └──
CH2 汲極(跟閘極相反:閘極 HIGH 時 MOSFET 導通,汲極被拉到 0V)
4.5V ┤─┐ ┌───┐ ┌───┐ ┌──
0V ┼ └───────┘ └───────┘ └───────┘
◄─────────►
1 ms(1 kHz)
填表:
| 段速 | 程式的 PWM 值 | 示波器 CH1 占空比 | PWM 值 ÷ 255 × 100% | 頻率 | CH2 的 HIGH 電壓 |
|---|---|---|---|---|---|
| 低 | 110 | 43% | |||
| 中 | 180 | 71% | |||
| 高 | 255 | 100% |
舉例:中速 180 ÷ 255 = 0.706,示波器應該讀到大約 70%。CH2 的 HIGH 電壓會接近電池電壓 4.5V,LOW 時接近 0V,而且 CH2 和 CH1 剛好相反:這就是第 9 章學的「開關反相」。如果你在 CH2 上看到馬達關掉瞬間有小小的尖波,那是二極體還沒完全壓下去的部分;沒有二極體的話尖波會高到十幾伏特。
延伸挑戰
- 把四顆按鈕換成一條滑桿(HTML 的
<input type='range'>),讓風速可以無段調整。提示:路徑可以用/set?duty=123這種帶參數的形式,用server.arg("duty")讀取。 - 加上 DHT11,溫度超過 28°C 自動開到中速,形成「智慧風扇」。
- 加一個定時:按「高」之後 10 分鐘自動關。
實驗 17-4:專題四 ESP32 迷你示波器
目標
用 ESP32 的類比輸入快速取樣,把波形畫在 Arduino IDE 的序列繪圖器上,並自動算出頻率和電壓範圍。訊號源是 UNO 的 tone() 方波,經過分壓器降到 2.5V 以下再送進 ESP32。最後用真的示波器量同一個訊號,比較兩邊的結果,看看你的迷你示波器準不準。
材料
- ESP32 DevKit V1 ×1
- Arduino UNO ×1(當訊號產生器)
- 10kΩ 電阻 ×2
- 麵包板、杜邦線
- 示波器(比對用)
接線
UNO D8 ──[ 10kΩ ]──┬──[ 10kΩ ]── GND
(tone 方波 0–5V) │
├────────────── ESP32 GPIO 34(0–2.5V)
│
└────────────── 示波器 CH1 探棒(比對用)
UNO GND ──────────── ESP32 GND ──── 示波器鱷魚夾
文字說明:
- UNO D8 → 10kΩ → 分壓點;分壓點 → 10kΩ → GND。
- 分壓點 → ESP32 GPIO 34。
- UNO GND → ESP32 GND,用一條杜邦線直接接。沒接共地,讀值會亂飄。
- 分壓器把 5V 降成 2.5V,這樣 ESP32 的輸入腳才安全(第 16 章講過)。
程式(UNO:訊號產生器)
// 專題四(UNO 端):產生 440 Hz 方波當訊號源
const int tonePin = 8;
void setup() {
tone(tonePin, 440); // 440 Hz,就是鋼琴的 A 音
}
void loop() {
// 什麼都不用做,tone() 會自己持續輸出
}
程式(ESP32:迷你示波器)
// 專題四(ESP32 端):迷你示波器
// plotMode = true → 給序列繪圖器畫波形
// plotMode = false → 給序列監控視窗印頻率、電壓等數據
const bool plotMode = true;
const int probePin = 34; // 只能輸入的腳,適合當探棒
const int SAMPLES = 400; // 一次抓 400 個點
uint16_t buffer[SAMPLES]; // 存取樣值(0–4095)
void setup() {
Serial.begin(115200);
analogReadResolution(12); // 12 位元:0–4095
analogSetAttenuation(ADC_11db); // 量測範圍最大約 3.3V
pinMode(probePin, INPUT);
}
void loop() {
// 1. 盡可能快地連續取樣,並記錄花了多久
unsigned long t0 = micros();
for (int i = 0; i < SAMPLES; i++) {
buffer[i] = analogRead(probePin);
}
unsigned long t1 = micros();
float dtMicros = (float)(t1 - t0) / SAMPLES; // 每兩個點之間的時間(µs)
// 2. 找最大值、最小值
int vMaxRaw = 0, vMinRaw = 4095;
for (int i = 0; i < SAMPLES; i++) {
if (buffer[i] > vMaxRaw) vMaxRaw = buffer[i];
if (buffer[i] < vMinRaw) vMinRaw = buffer[i];
}
float vMax = vMaxRaw * 3.3 / 4095.0; // 換算成伏特
float vMin = vMinRaw * 3.3 / 4095.0;
// 3. 算頻率:數波形從下面穿過中線往上的次數
int mid = (vMaxRaw + vMinRaw) / 2;
int margin = 100; // 留一點空間避免雜訊重複計數
int crossings = 0;
int firstCross = -1, lastCross = -1;
bool wasLow = buffer[0] < mid;
for (int i = 1; i < SAMPLES; i++) {
if (wasLow && buffer[i] > mid + margin) { // 由低變高
crossings++;
if (firstCross < 0) firstCross = i;
lastCross = i;
wasLow = false;
} else if (!wasLow && buffer[i] < mid - margin) {
wasLow = true;
}
}
float freq = 0;
if (crossings >= 2) {
// 第一次到最後一次上升之間有 (crossings-1) 個完整週期
float spanSeconds = (lastCross - firstCross) * dtMicros / 1000000.0;
freq = (crossings - 1) / spanSeconds;
}
// 4. 輸出
if (plotMode) {
for (int i = 0; i < SAMPLES; i++) {
Serial.println(buffer[i] * 3.3 / 4095.0, 2); // 每行一個電壓值
}
} else {
Serial.print("sample interval: ");
Serial.print(dtMicros, 1);
Serial.print(" us sample rate: ");
Serial.print(1000000.0 / dtMicros / 1000.0, 1);
Serial.println(" kHz");
Serial.print("Vmax: ");
Serial.print(vMax, 2);
Serial.print(" V Vmin: ");
Serial.print(vMin, 2);
Serial.print(" V Vpp: ");
Serial.print(vMax - vMin, 2);
Serial.println(" V");
Serial.print("frequency: ");
Serial.print(freq, 1);
Serial.println(" Hz");
Serial.println("------");
delay(1000);
}
}
步驟
- UNO 上傳訊號產生器程式。ESP32 上傳迷你示波器程式(
plotMode = true)。 - 打開 ESP32 的序列繪圖器(115200)。
- 看到方波後,把
plotMode改成false,重新上傳,改開序列監控視窗,抄下取樣間隔、Vmax、Vmin、頻率。 - 把示波器探棒鉤在分壓點,設定 1 V/div、1 ms/div、觸發上升緣 1.2V,用自動量測讀頻率和 Vpp。
你應該看到
序列繪圖器出現一串整齊的方波,高的地方約 2.5V、低的地方約 0V。序列監控視窗印出取樣間隔大約 20–60 µs(不同版本的 esp32 核心差很多)、Vpp 約 2.4–2.5V、頻率在 430–450 Hz 之間。
序列繪圖器畫面(示意)
2.5V ┤ ┌───┐ ┌───┐ ┌───┐ ┌───┐
│ │ │ │ │ │ │ │ │
0V ┼──┘ └───┘ └───┘ └───┘ └──
◄───────►
大約 50 個點 = 一個週期
測試方法(示波器比對)
這個專題的測試就是「跟真的示波器打對台」。填表:
| 項目 | 迷你示波器 | 真示波器 | 差多少 |
|---|---|---|---|
| 頻率(Hz) | |||
| 最高電壓(V) | |||
| 最低電壓(V) | |||
| Vpp(V) | |||
| 取樣間隔(µs) | (示波器顯示的取樣率倒數) |
算一個例子:如果迷你示波器的取樣間隔是 50 µs,取樣率就是 1 ÷ 0.00005 = 20 kHz,也就是每秒 20,000 個點。440 Hz 的方波一個週期是 2.27 ms = 2270 µs,2270 ÷ 50 ≈ 45 個點,畫出來很清楚。真示波器的取樣率通常是幾百 MHz,是你的幾萬倍,這就是為什麼它能看高頻訊號而你的不行。
一個有趣的實驗:把 UNO 的 tone(tonePin, 440) 改成 tone(tonePin, 5000)。5000 Hz 一個週期只有 200 µs,用 50 µs 取樣只能抓到 4 個點,畫出來會歪七扭八;改成 tone(tonePin, 19000) 的話,甚至會畫出一個完全錯誤的慢波。這叫混疊(Aliasing):取樣太慢的時候看到的是假象。所以經驗法則是,一個週期至少要取 10 個點,你的迷你示波器大概只能相信 1000 Hz 以下的訊號。
延伸挑戰
- 讓 UNO 每 3 秒換一個頻率(220、440、880 Hz),看迷你示波器算的頻率跟不跟得上。
- 把分壓點加上第 10 章的 RC 低通濾波器(10kΩ + 100nF),用迷你示波器看方波變成圓角的樣子。
- 加上「觸發」:在印出 buffer 之前,先找到第一個上升沿的位置,從那裡開始印。這樣每次畫出來的波形起點都一樣,不會左右亂跳,就像真示波器的觸發功能。
實驗 17-5:專題五 反應遊戲與電子琴
目標
用三顆按鈕、三顆 LED 和一顆蜂鳴器做一台雙用途的機器。開機時按住按鈕 1 進入「電子琴模式」:三顆按鈕各發一個音。直接開機則進入「反應遊戲模式」:隨機等幾秒後隨機亮一顆燈並嗶一聲,玩家要盡快按對應的按鈕,機器算出反應時間,五回合後公布最佳成績。用邏輯分析儀獨立量出「燈亮到按下」的時間,驗證程式算的反應時間是對的。
材料
- Arduino UNO ×1
- 按鈕開關 ×3
- LED(紅、綠、藍)各 1
- 220Ω 電阻 ×3
- 無源蜂鳴器 ×1
- 麵包板、杜邦線
- USB 邏輯分析儀(測試用)
接線
UNO D2 ───┬── 按鈕 1 ── GND (內建上拉:沒按 = HIGH,按下 = LOW)
UNO D3 ───┬── 按鈕 2 ── GND
UNO D4 ───┬── 按鈕 3 ── GND
UNO D9 ──[ 220Ω ]──►|── GND 紅 LED(對應按鈕 1)
UNO D10 ──[ 220Ω ]──►|── GND 綠 LED(對應按鈕 2)
UNO D11 ──[ 220Ω ]──►|── GND 藍 LED(對應按鈕 3)
UNO D8 ──( 蜂鳴器 + )
( 蜂鳴器 − )── GND
文字說明:
- 三顆按鈕各一腳接 D2、D3、D4,另一腳接 GND。程式用
INPUT_PULLUP,不用外接電阻。 - D9 → 220Ω → 紅 LED 長腳,短腳 → GND;D10 綠、D11 藍同樣接法。
- 蜂鳴器正極(長腳或標 + 的那端)→ D8,負極 → GND。
- 把三顆按鈕和三顆 LED 由左到右排整齊,紅配按鈕 1、綠配按鈕 2、藍配按鈕 3,玩家才不會搞混。
程式
// 專題五:反應遊戲 + 電子琴(UNO)
// 開機時按住按鈕 1 → 電子琴模式;否則 → 反應遊戲模式
const int buttonPins[3] = {2, 3, 4}; // 三顆按鈕(INPUT_PULLUP,按下 = LOW)
const int ledPins[3] = {9, 10, 11}; // 三顆 LED
const int buzzerPin = 8;
const int notes[3] = {262, 330, 392}; // Do、Mi、Sol 的頻率(Hz)
const int ROUNDS = 5; // 一局幾回合
bool pianoMode = false;
// 讀第 i 顆按鈕,按下回傳 true
bool isPressed(int i) {
return digitalRead(buttonPins[i]) == LOW;
}
// 等所有按鈕都放開
void waitAllReleased() {
while (isPressed(0) || isPressed(1) || isPressed(2)) {
delay(5);
}
delay(30); // 等彈跳結束
}
// 三顆 LED 全關
void allLedsOff() {
for (int i = 0; i < 3; i++) digitalWrite(ledPins[i], LOW);
}
// 嗶一聲:頻率、長度
void beep(int freq, int ms) {
tone(buzzerPin, freq, ms);
delay(ms);
}
void setup() {
Serial.begin(9600);
for (int i = 0; i < 3; i++) {
pinMode(buttonPins[i], INPUT_PULLUP);
pinMode(ledPins[i], OUTPUT);
}
pinMode(buzzerPin, OUTPUT);
randomSeed(analogRead(A0)); // 用沒接東西的 A0 的雜訊當亂數種子
delay(50);
pianoMode = isPressed(0); // 開機時按住按鈕 1 → 電子琴
if (pianoMode) {
Serial.println("piano mode");
for (int i = 0; i < 3; i++) { // 三顆燈依序閃一下表示進入電子琴
digitalWrite(ledPins[i], HIGH);
beep(notes[i], 150);
digitalWrite(ledPins[i], LOW);
}
waitAllReleased();
} else {
Serial.println("reaction game: press the button that lights up");
beep(880, 200);
}
}
// ===== 電子琴模式 =====
void pianoLoop() {
bool anyPressed = false;
for (int i = 0; i < 3; i++) {
if (isPressed(i)) {
tone(buzzerPin, notes[i]); // 按著就一直響
digitalWrite(ledPins[i], HIGH);
anyPressed = true;
} else {
digitalWrite(ledPins[i], LOW);
}
}
if (!anyPressed) noTone(buzzerPin); // 全放開就安靜
delay(10);
}
// ===== 反應遊戲模式 =====
void playOneRound(int roundNumber, unsigned long &bestTime) {
allLedsOff();
waitAllReleased();
// 隨機等 1–4 秒,期間偷按就算犯規
unsigned long waitMs = random(1000, 4000);
unsigned long waitStart = millis();
while (millis() - waitStart < waitMs) {
if (isPressed(0) || isPressed(1) || isPressed(2)) {
Serial.println("too early!");
beep(150, 400); // 低沉的犯規聲
waitAllReleased();
waitStart = millis(); // 重新等
}
}
// 隨機亮一顆燈並嗶一聲,同時開始計時
int target = random(0, 3);
digitalWrite(ledPins[target], HIGH);
tone(buzzerPin, 880, 80);
unsigned long startTime = millis();
// 等玩家按(最多等 3 秒)
int pressed = -1;
while (millis() - startTime < 3000) {
for (int i = 0; i < 3; i++) {
if (isPressed(i)) { pressed = i; break; }
}
if (pressed >= 0) break;
}
unsigned long reaction = millis() - startTime;
digitalWrite(ledPins[target], LOW);
Serial.print("round ");
Serial.print(roundNumber);
Serial.print(": ");
if (pressed < 0) {
Serial.println("timeout");
beep(150, 400);
} else if (pressed != target) {
Serial.println("wrong button");
beep(150, 400);
} else {
Serial.print(reaction);
Serial.println(" ms");
beep(1200, 100);
if (reaction < bestTime) bestTime = reaction; // 更新最佳成績
}
waitAllReleased();
delay(500);
}
void gameLoop() {
unsigned long bestTime = 99999;
for (int r = 1; r <= ROUNDS; r++) {
playOneRound(r, bestTime);
}
Serial.print("best: ");
Serial.print(bestTime);
Serial.println(" ms");
// 結束動畫:三顆燈一起閃三下
for (int k = 0; k < 3; k++) {
for (int i = 0; i < 3; i++) digitalWrite(ledPins[i], HIGH);
beep(notes[k], 150);
allLedsOff();
delay(150);
}
delay(2000); // 兩秒後自動開始下一局
}
void loop() {
if (pianoMode) {
pianoLoop();
} else {
gameLoop();
}
}
步驟
- 接好線,先上傳程式,直接開機(不按按鈕)。
- 打開序列埠監控視窗(9600),等燈亮就按對應按鈕,看印出來的毫秒數。玩完五回合會印「best」。
- 按一下 UNO 的 RESET 鍵,同時按住按鈕 1 不放,三顆燈依序閃並發出 Do Mi Sol,就進入電子琴模式。
你應該看到
反應遊戲:等 1–4 秒後隨機一顆燈亮、嗶一聲,按對會印出像「round 1: 243 ms」的反應時間,按錯或超過 3 秒會低鳴一聲。人類的反應時間大約 200–300 毫秒;偷按會被抓到「too early!」。電子琴:按哪顆按鈕就亮哪顆燈、響哪個音,可以彈出簡單的旋律。
測試方法(邏輯分析儀 + 示波器)
邏輯分析儀:CH0 → D9(紅 LED)、CH1 → D10、CH2 → D11、CH3 → D2(按鈕 1)、CH4 → D3、CH5 → D4、GND → UNO GND。PulseView 取樣率 100 kHz、取樣 10 秒,按「執行」後開始玩一回合。
CH0 紅 LED ───────┐ ┌─────────────────────────────────┐
└───────┘ └────
CH3 按鈕 1 ───────────────────────────────┐ ┌┐┌───────┐
└─┘└┘ └─────────
◄───────────────────────►
燈亮到按下 = 反應時間(用游標量)
在 PulseView 用兩個游標夾住「LED 由低變高」和「按鈕由高變低的第一個下降緣」,讀出時間差,跟序列埠印的反應時間比對。順便放大看按鈕那一段,你會看到第 11 章講的彈跳:按下的瞬間有好幾個很短的上下跳動。
示波器:探棒鉤 D8,鱷魚夾接 GND,1 V/div、1 ms/div,進入電子琴模式按住按鈕 1,量頻率。
| 項目 | 程式設定/印出 | 儀器量到 | 差多少 |
|---|---|---|---|
| 反應時間(毫秒) | 序列埠印的 | 邏輯分析儀游標 | |
| 按鈕彈跳持續時間 | (無) | ||
| 按鈕 1 的音高(Hz) | 262 | 示波器頻率 | |
| 按鈕 3 的音高(Hz) | 392 |
程式算的反應時間和邏輯分析儀量的應該只差幾毫秒。差的來源是 loop() 裡檢查按鈕的那個 for 迴圈需要一點點時間,以及彈跳的第一下和程式認定的那一下可能不是同一下。
延伸挑戰
- 加入 OLED(第 15 章的接法,UNO 的 SDA=A4、SCL=A5)顯示每回合的反應時間和最佳成績。
- 做成「記憶遊戲」:機器依序亮 3 顆燈,玩家要照順序按;每成功一次就多加一顆。
- 雙人對戰:按鈕 1 和按鈕 3 各屬於一個玩家,中間的綠燈亮時比誰先按。
- 電子琴加入「和弦」:同時按兩顆按鈕時,用更高頻率的
tone()表示(蜂鳴器一次只能發一個音,想想怎麼取捨)。
收尾:專題報告與上台展示
作品會動只是一半,另一半是讓別人在三分鐘內看懂你做了什麼、為什麼成功。
專題報告格式(兩到四頁)
1. 題目與一句話說明 例:「智慧夜燈:天暗自動漸亮的 LED 燈」
2. 問題 誰、在什麼情況、需要什麼
3. 規劃 輸入 → 處理 → 輸出 表格 + 材料清單
4. 接線圖 ASCII 或手繪,標清楚每個腳位
5. 程式說明 不要貼全部程式,只貼最關鍵的 5–10 行並解釋
6. 測試與量測數據 ★至少一張示波器或邏輯分析儀的畫面(拍照)
★至少一張填好的量測表格
★程式設定值 vs 儀器量測值的比較
7. 遇到的問題與解法 列 2–3 個真的卡住過的地方
8. 延伸與反思 如果有更多時間會改什麼
第 6 點是整份報告的核心,也是本章每個專題都附「測試方法」的原因。「我的夜燈 PWM 頻率是 490 Hz,示波器量到 489.6 Hz」這一句,比「夜燈運作正常」有說服力一百倍。
第 7 點很多人會跳過,覺得寫出問題很丟臉。其實評審最想看的就是這段,因為它證明你真的動手做過,而不是抄來的。
上台展示要點(三分鐘)
- 第一句話講問題,不要講技術:「晚上起床找燈的開關很麻煩,所以我做了……」比「我用 Arduino UNO 的 PWM……」好得多。
- 現場操作一定要成功:上台前一小時再完整跑一次;帶備用電池、備用 USB 線;手機先連好 Wi-Fi 並把網頁開在畫面上。ESP32 的專題要事先確認場地的 Wi-Fi 是 2.4 GHz,不確定就帶自己的手機熱點。
- 用一張波形圖證明:把示波器畫面拍下來放在投影片或海報上,指著它說「這裡可以看到占空比從 0 變到 100%」。
- 講一個卡住的故事:「一開始馬達關掉時 MOSFET 會發燙,後來加了二極體就好了。」這種故事比任何技術細節都讓人記得住。
- 準備被問的三個問題:「為什麼選這個元件?」「如果要用電池可以撐多久?」「下一步想加什麼?」照本章的內容你都答得出來。
- 時間控制:問題 30 秒、作品示範 90 秒、量測證據 30 秒、問題與延伸 30 秒。練習時用手機計時。
用儀器看看
本章的儀器操作已經分散在五個專題的「測試方法」裡。這裡整理成一張表,方便你規劃測試,也方便你在報告裡寫清楚設定:
| 專題 | 儀器 | 接哪裡 | 設定 | 要量的東西 | 預期值 |
|---|---|---|---|---|---|
| 一 智慧夜燈 | 示波器 | CH1→UNO D9 | 2 V/div、500 µs/div、上升緣 2.5V | 占空比、頻率 | 占空比隨光線 0–100%,約 490 Hz |
| 二 氣象站 | 邏輯分析儀 | CH0→GPIO 21、CH1→GPIO 22 | 2 MHz、I2C 解碼器 | 位址、封包間隔、SCL 頻率 | 0x3C、2 秒、約 100 kHz |
| 三 風扇 | 示波器 | CH1→閘極、CH2→汲極 | 2 V/div、200 µs/div、上升緣 1.5V | 三段占空比、反相、頻率 | 43%/71%/100%,1 kHz |
| 四 迷你示波器 | 示波器 | CH1→分壓點 | 1 V/div、1 ms/div、上升緣 1.2V | 頻率、Vpp,與 ESP32 比對 | 440 Hz、約 2.5V |
| 五 反應遊戲 | 邏輯分析儀+示波器 | LA:LED 與按鈕腳;示波器→D8 | 100 kHz、10 秒;1 V/div、1 ms/div | 反應時間、彈跳、音高 | 與序列埠相符;262/330/392 Hz |
把示波器畫面變成報告裡的證據
示波器通常有「儲存畫面」的功能(存到 USB 隨身碟),沒有的話用手機拍也可以。拍的時候請確認畫面上看得到:V/div、s/div、觸發設定、以及自動量測的數字。報告裡每一張波形圖旁邊都要寫一句「這張圖證明了什麼」,例如:
圖 3:風扇中速時閘極波形。占空比 70.2%、頻率 1.00 kHz,與程式設定的 180/255 = 70.6% 一致。
雙通道的共地提醒
專題三和專題四都用到兩塊板子或兩個通道。兩支探棒的鱷魚夾都要夾 GND,而且兩塊板子的 GND 要用杜邦線接在一起。鱷魚夾夾錯地方(例如夾到電池正極)會造成短路,這是本書從第 6 章一直強調的規矩。
常見錯誤與除錯
- 夜燈 LED 一直全亮或一直全暗,不會變 →
darkValue、brightValue沒有依教室光線調整 → 看序列繪圖器的light值,重新填這兩個數字。 - 夜燈 LED 突然跳亮,不是漸亮 →
fadeStepDelay太小,或程式直接analogWrite(target)沒有走一步一步 → 確認currentBrightness每圈只加減 1。 - 氣象站長條圖一直只有一條 →
HISTORY_INTERVAL是 60 秒,要等很久 → 展示時改成 5000。 - 氣象站最低溫顯示 100 → 開機後 DHT11 第一次讀取失敗,
tempMin沒更新 → 等 2 秒後自然會更新;或在setup()裡多等 1.5 秒再讀。 - 風扇按「低」不轉,按「中」才轉 → 小馬達啟動需要較高電壓 → 把
levelDuty[1]從 110 調到 130 或 150。 - 風扇一開就 ESP32 重開機 → 馬達電流從 ESP32 供電 → 馬達一定要用獨立電池盒,只有 GND 跟 ESP32 共接。
- 編譯錯誤
ledcAttach was not declared→ esp32 核心版本是 2.x → 改用程式註解裡的ledcSetup、ledcAttachPin三行寫法。 - MOSFET 發燙 → G、D、S 接反,或閘極沒接 10kΩ 下拉讓它半開半關 → 對照腳位圖重接,確認 10kΩ 在閘極和 GND 之間。
- 迷你示波器波形亂跳、全部貼在最高或最低 → 沒共地,或分壓器接錯讓 GPIO 34 吃到 5V → 先拔掉訊號,用三用電表確認分壓點是 2.5V 左右,確認 UNO GND 接到 ESP32 GND。
- 迷你示波器頻率算出 0 → 訊號太小或沒接,
crossings不到 2 → 檢查 UNO 有沒有在tone();把margin從 100 改小一點試試。 - 序列繪圖器波形只看到一小段 → 繪圖器一次顯示的點數有限 → 把
SAMPLES改成 100,或在繪圖器設定裡調大顯示點數。 - 反應遊戲一開機就說 too early → 按鈕接錯成「沒按是 LOW」,或
pinMode忘了INPUT_PULLUP→ 用三用電表量 D2 沒按時應該是 5V。 - 反應遊戲每次亮的燈都一樣 →
randomSeed(analogRead(A0))的 A0 接了東西,雜訊不夠 → 確認 A0 空接;或改用randomSeed(micros())在第一次按鈕後呼叫。 - 電子琴按兩顆按鈕只響一個音 → 蜂鳴器用
tone()一次只能一個頻率 → 這是正常的,看延伸挑戰。 - 邏輯分析儀量的反應時間和序列埠差很多(超過 20 ms) → 游標夾的是彈跳的最後一下,不是第一下 → 放大看按鈕訊號,把游標移到第一個下降緣。
想一想
- 專題一的夜燈從全暗到全亮花多少時間?如果想改成 1 秒,要改哪個數字、改成多少?
參考答案
255 步 × 10 毫秒 = 2550 毫秒 ≈ 2.5 秒。想改成 1 秒,把 fadeStepDelay 改成 4(255 × 4 = 1020 毫秒)。
- 專題三的風扇,如果把
PWM_FREQ從 1000 改成 100,示波器上會看到什麼不同?馬達會有什麼變化?
參考答案
示波器上方波的週期從 1 ms 變成 10 ms(要把時間調到 2 ms/div 才看得到幾個週期)。占空比不變,所以平均轉速差不多,但 100 Hz 在人耳聽得到的範圍,馬達會發出明顯的嗡嗡聲,而且低速時可能一頓一頓的。
- 專題四的迷你示波器,取樣間隔是 40 µs。它最高大概能可靠地看到幾 Hz 的方波?請用「一個週期至少 10 個點」的法則算。
參考答案
10 個點 × 40 µs = 400 µs 是最短的可靠週期,頻率 = 1 ÷ 0.0004 = 2500 Hz。超過這個頻率,波形會變形,再高就會出現混疊的假波形。
- 專題五用邏輯分析儀量到的反應時間比序列埠印的少了 3 毫秒。可能的原因是什麼?這算是程式有錯嗎?
參考答案
程式用 millis() 計時,而 millis() 只有 1 毫秒的解析度;再加上 loop() 裡用 for 迴圈輪流檢查三顆按鈕,從按下到程式「發現」會慢一點點;彈跳也可能讓儀器和程式認定的「按下瞬間」不同。差 3 毫秒對反應遊戲來說完全可以接受,不算錯,但在報告裡要誠實寫出來並解釋原因。
- 你想把專題二的氣象站改成放在陽台用電池跑一個月。列出至少三件需要改動的事。
參考答案
(任三個)用第 16 章的深度睡眠,每 5–10 分鐘醒來一次而不是一直開著 Wi-Fi;OLED 只在醒來那幾秒顯示,或拿掉 OLED;資料不能只存在 ESP32 的記憶體裡(睡覺會不見),要用 RTC_DATA_ATTR 或送到網路上(例如 MQTT);網頁伺服器在睡覺時無法回應,要改成 ESP32 主動把資料送出去;做防水外殼;用 3 顆 3 號電池接 VIN(4.5V 對 DevKit V1 的穩壓器來說有點低,可能要 4 顆)。
- 開放挑戰:從五個專題裡挑兩個,把它們合併成一個新作品,寫下那句「誰、在什麼情況、需要什麼」的問題描述,和「輸入 → 處理 → 輸出」表格。
參考答案
範例:合併專題二和專題三:「夏天午睡的人,需要一台房間太熱時會自己開、涼了會自己關的風扇。」輸入:DHT11 溫度;處理:高於 28°C 開中速、高於 30°C 開高速、低於 26°C 關閉,並在網頁上顯示目前溫度與段速、允許手動覆蓋;輸出:IRLZ44N 驅動馬達、OLED 顯示狀態。另一個例子是合併一和五:天黑後用按鈕反應遊戲當「晚安儀式」,玩完燈才慢慢暗下來。答案不唯一,重點是問題描述要具體,表格的每一格都要是你接過的元件。
本章小結
- 專題六步驟:問題 → 規劃 → 接線 → 程式 → 測試 → 展示;測試不過就回頭改,這是正常流程不是失敗。
- 規劃時把題目拆成「輸入 → 處理 → 輸出」三格,每一格都填上你在前面章節接過的元件。
- 接線先接電源再接輸入最後接輸出,每接一段就用三用電表量一次;程式先寫最小可動版本,每加一段就上傳一次。
- 專題程式的骨架:常數放最上面、小函式各做一件事、
loop()用millis()排程不用長delay()。 - 五個專題各自用儀器驗證了一件事:PWM 占空比隨光線變化(示波器)、I2C 位址 0x3C 與封包間隔(邏輯分析儀)、三段占空比與汲極反相(雙通道示波器)、迷你示波器 vs 真示波器的頻率與 Vpp(示波器比對)、反應時間與按鈕彈跳(邏輯分析儀)。
- 取樣率決定你看得到多快的訊號:一個週期至少 10 個點,太慢會混疊出假波形。
- 馬達一定要獨立供電、加飛輪二極體、閘極加下拉電阻;ESP32 的 GPIO 34 吃 5V 訊號前一定要分壓。
- 報告的核心是「程式設定值 vs 儀器量測值」的比較;上台第一句講問題不講技術,現場示範上台前一小時再跑一次。
名詞表
| 中文 | 英文 | 一句話解釋 |
|---|---|---|
| 專題 | Project | 把多個元件和程式組合起來解決一個具體問題的作品。 |
| 最小可動版本 | Minimum working version | 功能最少但已經能跑的第一版程式,之後再一段一段加。 |
| 輸入/處理/輸出 | Input / Process / Output | 規劃專題時把題目拆成感測、判斷、動作三格的方法。 |
| 占空比 | Duty cycle | PWM 方波裡 HIGH 所占的時間比例,決定 LED 亮度或馬達轉速。 |
| 閘極下拉電阻 | Gate pull-down resistor | 接在 MOSFET 閘極與 GND 之間的電阻,確保沒訊號時 MOSFET 關閉。 |
| 飛輪二極體 | Flyback diode | 跨接在馬達兩端的二極體,吸收關掉時的反電動勢尖波。 |
| 共地 | Common ground | 兩塊板子或儀器的 GND 接在一起,讓它們有相同的 0V 基準。 |
| 取樣率 | Sampling rate | 每秒取幾個點,決定能看到多快的訊號。 |
| 取樣間隔 | Sampling interval | 兩個取樣點之間的時間,是取樣率的倒數。 |
| 混疊 | Aliasing | 取樣太慢時看到的假波形。 |
| 觸發 | Trigger | 示波器或程式從波形固定的位置(例如上升緣)開始畫,讓畫面不亂跳。 |
| 歷史紀錄 | History buffer | 用陣列存最近 N 筆資料,滿了就擠掉最舊的。 |
| 亂數種子 | Random seed | 讓 random() 每次開機產生不同序列的起始值。 |
| 反應時間 | Reaction time | 從刺激出現到做出動作的時間,人類大約 200–300 毫秒。 |
| 量測證據 | Measurement evidence | 報告裡用儀器數據證明作品照設計運作的部分。 |