📋 專案概述
0403 那個星期五,我在台北睡得比想像中沉。把我拉起來的不是搖晃——是自己在黑暗裡被書架推倒的椅子撞醒的。事後我躺在還微微發暈的床上想了一件很可怕的事:我的身體比警報系統先知道地震,但我的意識晚了它整整幾秒,而「幾秒」在強震裡是以「來不來得及把女兒從床邊抱開」計時的。手機的強震即時警報後來也響了,屏幕在床頭櫃上亮著,而我花了三秒才理解那串 nationally 統一的字在叫什麼。真正的窗口在 P 波:縱波先到、橫波隨後,本市距震中 100 公里以內時,兩者之間大約 5~20 秒。這 5~20 秒足夠關火、開門、鑽桌下、把孩子拉離窗邊——前提是有個東西在那幾秒內替你清醒。
市售方案我全部摸過一遍:
- 日本地震警報燈(地震予知ウォッチ那一類):2,000 元有找,只吃 100V、日文語音、感測門檻封死不能調。放在台北的鋼筋混凝土樓房裡,它聽的是牆、不是地板。
- 手機會員推送:它假設你睡著時把手機壓在枕頭下且音量全開——我睡覺飛彈來了都不翻身,別測試我的睡眠品質。
- 「強震即時警報本來就免費,你还要什麼」:要的是本地、即時、不經手機的觸發。中央氣象署的警報是「已經確定要來了才發」;自己床頭的地震儀是「地板一抖我就知道」,兩者差的那一段,正是本專案。
⚠️ 誠實定位:本機不是地震預報儀,沒有任何東西能預報地震。它做的是「P 波本地偵測+提前幾秒喚醒與照明」——以及最重要的一件事:斷電時它還亮著。它也不會幫你關瓦斯、開電鎖,那些是另外的專案。
這台 ESP32-C3 床頭警報塔做的事:
- 壓電陶瓷片=最廉價的寬頻地震儀:15 元一片的 27mm 壓電盤,1~200Hz 都有回應,鎖著一枚 20g 銅質量塊夾在床頭板上——這是業餘地震學圈玩了幾十年的正統做法,不是仿真
- P/S 雙階段反應:P 波觸發 → 蜂鳴器短嗶+WS2812 燈環 3 秒倒數(把「還剩幾秒」變成看得見的東西);能量持續(S 波抵達)→ 105dB 連續警報+夜燈全亮,照出摸黑逃生路線
- 三層濾假警报:50ms 峰值包絡 → 連續視窗能量確認(擋關門、掉書)→ 單脈衝拒絕(尖峰後衰減 >70% 視為撞擊);夜間自動學習環境噪音底,隔壁打呼的床不歸它管
- ADXL335 第二意見:MEMS 加速度計與壓電盤交叉確認,雙感測同時超限的事件在記錄中標記「雙重確認」,也順便擋掉壓電盤線材鬆動的誤報
- 斷電 11 小時續航:18650 浮充備援;市電一失就切電池、燈環減半亮度、推播斷電通知——大地震的晚上一定停電,這台的存在意義從那一刻開始
- 事件檔案:每次事件記時間(DS3231 走時+SNTP 校時)、峰值等效加速度、持續秒數,NVS 存 50 筆、Telegram 推送(可關);每 24 小時感測線路自檢——警報器最可怕的故障是安靜地壞掉
帳目:日本警報燈 2,000 元(电压还不对)、商用联网震动传感器 1,500 元起、本機 522 元。一台装在床头,等于给全家买了一份「睡得著」。
為什麼選 ESP32-C3?
| 需求 | C3 的優勢(以及其他型號為什麼不選) |
|---|---|
| 臥室設備的修養 | 全年 24 小時不關機的小東西,待機溫升必須趨近零。C3 單核 RISC-V @160MHz 跑 2kHz 取樣+包絡偵測的 CPU 佔用不到 3%,實測外殼溫升 <1°C;S3 雙核是浪費的電力與浪費的 90 元 |
| ADC1 腳位剛好 | 壓電盤+ADXL335 兩路類比要落在 ADC1(不受 Wi-Fi _radio 協定干擾的通道)。C3 的 ADC1 正好是 GPIO0–4,兩路加電池電壓偵測三路剛剛好,一路不多 |
| SuperMini 尺寸 | 22×18mm 直接埋進塔殼底層,把空間留給配重與電池——一台要站在床頭的設備,重心比規格重要 |
| 斷電續航的退路 | C3 深睡 25µA。本版本常駐取樣用不到它,但韌體預留「有事件才喚醒 Wi-Fi 推播」的架構;日後想把它做進 Thread 生態的,C6 才是答案——那是下一篇的事了 |
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-C3 SuperMini | 1 | ~22 元 | C3-MINI-1 模组;買兩顆備用,這東西值得床頭+客廳各一 |
| 主感測器 | 27mm 壓電陶瓷片+銅片 | 1 | ~15 元 | 直徑越大低頻越靈;附 20g 銅質量塊(大螺母自製亦可) |
| 輔助感測 | ADXL335 三軸模組 | 1 | ~65 元 | 類比輸出、2g 量程對「搖晃程度」剛好;買 ADXL345 數位版請改 I2C 讀法 |
| 警報 | 105dB 有源蜂鳴器(5V) | 1 | ~18 元 | 「有源」自振、GPIO 一給電就叫——喚醒耳朵不喚醒邏輯 |
| 燈環 | WS2812B 12 燈環 | 1 | ~55 元 | 倒數、夜燈、警報三種燈語;兼床頭小夜燈,老婆第一票就過 |
| RTC | DS3231 模組+CR1220 | 1 | ~35 元 | 斷電三天時間不丟——事件報告沒有正確時間就只是噪音 |
| 備援電源 | 18650 帶保護板 2600mAh | 1 | ~85 元 | 正規品牌;警報器不配無保護板的拆機芯 |
| 充電/升壓 | TP4056(帶保護)+MT3608 | 各 1 | ~55 元 | 充飽自動停;升壓輸出串 1A 保險絲——鋁箔紙的教訓都懂 |
| 訊號保護 | 1MΩ、100kΩ×2、BAV99 clamp | 1 組 | ~8 元 | 壓電盤絕不能被靜電脈衝親過,clamp 到 3V3 是標配 |
| 電源 | 5V 1A USB-C 火牛+短線 | 1 | ~45 元 | 全年不關的火牛,買品牌貨,這是不二法門 |
| 列印件 | 塔身、壓電夾具、底座配重、RTC 抽屜 | 1 組 | ~130 元 | 詳見 3D 列印節 |
| 合計 | ~522 元 | 全機無強電,火牛用現成品 |
🔌 接線圖
ASCII 對照表(SuperMini 左排 / 右排)
┌─ ESP32-C3 SuperMini ─────────────┐
│ 3V3 ●──→ ADXL335 VCC / BAV99 基準
│ GND ●──→ 共地母排(單點)
│ GPIO0 ●──→ 壓電盤信号(1MΩ 對地+BAV99 clamp)ADC1_CH0
│ GPIO1 ●──→ ADXL335 X(ADC1_CH1)
│ GPIO2 ●──→ ADXL335 Z(ADC1_CH2)
│ GPIO4 ●──→ VBAT 分壓中點(100k/100k,ADC1_CH4)
│ GPIO5 ●──→ 市電偵測(USB 5V 經 100k/220k 分壓,數字讀)
│ GPIO6 ●──→ S8050 基極(10kΩ)→ 105dB 蜂鳴器(5V 側)
│ GPIO7 ●──→ WS2812 環 DIN(330Ω)
│ GPIO8 ●──→ DS3231 SDA
│ GPIO9 ●──→ DS3231 SCL
│ 5V ●──→ MT3608 輸出(經 1A 保險絲)
└──────────────────────────────────┘
⚠ 感測三腳必須全在 GPIO0–4(ADC1)。GPIO5 之後是 ADC2,
Wi-Fi 開著的時候 ADC2 讀值會被無線協定搶走——誤報之源。
詳細接線
| ESP32-C3 腳位 | 連接 | 說明 |
|---|---|---|
| GPIO0(ADC1_CH0) | 壓電盤信號端 | 1MΩ 對地放電+BAV99 雙二極體 clamp 到 3V3/GND:壓電脈衝可以殺穩壓器,保護電路不是可選項 |
| GPIO1/GPIO2(ADC1) | ADXL335 X/Z | 靜態中點約 1.65V;取樣時與壓電交錯讀取,合成「豔動度」指標 |
| GPIO4(ADC1_CH4) | VBAT 分壓 | 100k+100k;放電曲線比百分比誠實 |
| GPIO5 | 市電偵測 | USB 5V 經 100k/220k 分壓進數字腳(5V<3.3V 容忍上限?分壓後約 1.56V,安全) |
| GPIO6 | 蜂鳴器(經 S8050) | 高電位叫;待機拉低=不給它任何發聲的機會 |
| GPIO7 | WS2812 環 | 330Ω 絕不省略;夜燈模式亮度 40,警報 255 |
| GPIO8/GPIO9 | DS3231 SDA/SCL | 模組自帶上拉;RTC 電池座先裝再上電,第一次開機手動校時 |
📊 偵測邏輯(狀態機)
🧊 3D 列印設計
| 零件 | 材質 | 列印設定 | 時間 | 備註 |
|---|---|---|---|---|
| 塔身 Ø68×190mm | PLA | 0.2mm/螺旋圓柱無縫/15% 菱形 | ~150 分 | 燈環段開窗(高 18mm 透光圈);壁內預留壓電夾具導管 |
| 壓電夾具 | PLA+TPU 墊×2 | 0.16mm/實心 3 層 | ~20 分 | 壓電盤夾在兩片 TPU 之間、中央鎖 20g 銅質量塊——這就是地震儀的擺,夾緊力決定靈敏度一致性 |
| 底座(配重槽) | PLA | 0.24mm/底部實心 5 層 | ~60 分 | 槽放 4×M8 螺母+備用 18650;底面貼 3M 泡棉膠條防滑 |
| RTC/電池抽屜 | PLA | 0.28mm/15% | ~70 分 | DS3231 與 TP4056 都做成可抽換——警報器要能用十年,就得能換零件 |
💻 Arduino 程式碼
開發板選 ESP32C3 Dev Module,USB CDC On Boot: Enabled,PSRAM 關。相依:Adafruit NeoPixel、RTClib、ArduinoJson(僅 Telegram 用)。核心 ≥ 2.0.9。
📐 註解規範:區塊以框註分段;函式附用途/參數/回傳框註;行內註解解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
🧠 架構決策:取樣與狀態機跑在普通 loop(佔用 <3% CPU),Wi-Fi/Telegram 全部鎖在「事件發生後的非同步任務」——警報路徑上沒有任何 await,Wi-Fi 掛了,蜂鳴器照叫。
🧠 架構決策:取樣與狀態機跑在普通 loop(佔用 <3% CPU),Wi-Fi/Telegram 全部鎖在「事件發生後的非同步任務」——警報路徑上沒有任何 await,Wi-Fi 掛了,蜂鳴器照叫。
/* ================================================================
* ESP32-C3 床頭地震警報塔 firmware v1.0 (2026-10-24)
* ----------------------------------------------------------------
* 功能 :壓電盤+ADXL335 雙感測 P 波偵測,三秒倒數預警、
* 105dB 喚醒警報、夜燈全亮;DS3231 事件記錄、
* 市電中斷備援(18650)、Telegram 報告(事件後才連網)
* 硬體 :ESP32-C3 SuperMini
* 壓電=GPIO0(ADC1) ADXL X/Z=GPIO1/GPIO2
* 蜂鳴器=GPIO6 WS2812 環=GPIO7(330Ω)
* DS3231 SDA/SCL=GPIO8/9 VBAT=GPIO4 市電偵測=GPIO5
* 供電 :USB 5V → TP4056 浮充 18650 → MT3608 5V(經 1A 保險絲)
* 相依 :Adafruit NeoPixel, RTClib, ArduinoJson;core ≥ 2.0.9
* 看板 :「ESP32C3 Dev Module」/CDC On/PSRAM Disabled
* ================================================================ */
#include <Adafruit_NeoPixel.h>
#include <RTClib.h>
#include <Preferences.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <esp_task_wdt.h>
/* --- 1. 腳位與可調原則 ------------------------------------------ */
#define PIN_PIEZO 0 // ADC1_CH0:壓電盤(1MΩ+clamp 保護)
#define PIN_ADXLX 1 // ADC1_CH1
#define PIN_ADXLZ 2 // ADC1_CH2
#define PIN_BUZZ 6 // 高電位叫(S8050)
#define PIN_LED 7 // WS2812 12 燈環
#define PIN_VBAT 4 // 電池分壓(x2 還原)
#define PIN_ACOK 5 // 市電偵測(分壓後數字讀)
#define CAL_BTN 9 // 與 SCL 共用?不——校準用「開機按住 boot」即可
const uint16_t FS_HZ = 2000; // 雙通道交錯取樣 1kHz×2,P 波主頻 <10Hz,夠
const uint8_t WIN_MS = 50; // 包絡視窗:50ms=P 波幾個週期的指纹長度
const float NOISE_FLOOR_MIN = 60; // counts:ADC 底噪地板,防「太安靜」誤觸
const uint8_t CONF_WINDOWS = 3; // 確認邏輯:3 窗中任 2 窗超限=事件
const float SINGLE_SPIKE_KILL = 0.70f; // 衰減 70% 以上=撞擊,不是地震
const char* WIFI_SSID = "你的WiFi";
const char* WIFI_PASS = "你的密碼";
const char* TG_TOKEN = "你的Bot Token"; // 不用推播就把 TG_TOKEN 留空
const char* TG_CHAT = "你的 ChatID";
/* --- 2. 全域狀態 -------------------------------------------------- */
enum State { IDLE, P_CAND, P_READY, WARN, SHAKE, QUIET };
State st = IDLE;
float noiseFloor = 400; // 自我學習的環境包絡底
uint16_t winPeak = 0; // 當前 50ms 視窗峰值
uint32_t winT0 = 0;
uint8_t hits[8]; uint8_t hitN = 0; // 最近 8 窗命中記錄
float eventPeak = 0; uint32_t eventT0 = 0;
bool acOk = true, ledDim = false;
RTC_DS3231 rtc;
Preferences nvs;
Adafruit_NeoPixel ring(12, PIN_LED, NEO_GRB + NEO_KHZ800);
TaskHandle_t hReport; // 推播任務(只在事件後醒來)
/* 用途:讀一輪雙感測,合成「晃動度」 counts
* 回傳:max(壓電峰值, ADXL 偏移×1.4)
* 為什麼取 max 而不是相加:兩感測器增益隨安裝而變,相加會讓
* 校準變成玄學;取 max 讓「任一感測器有意見就當事件」——漏報
* 比誤報危險,這是保命設備的哲學 */
static inline uint16_t shakeMetric() {
int16_t p = (int16_t)(analogReadMilliVolts(PIN_PIEZO)) - 0; // clamp 電路使中點≈0
int16_t x = analogReadMilliVolts(PIN_ADXLX) - 1650; // ADXL335 靜態 ~1.65V
int16_t z = analogReadMilliVolts(PIN_ADXLZ) - 1650;
float adxl = sqrtf(x * x + z * z) * 1.4f; // mV→「等效壓電 counts」
return (uint16_t)max(fabsf((float)p), adxl);
}
/* --- 3. 警報輸出(純本機,不经任何網路) --------------------------- */
void sirenOn(bool cont) {
if (cont) { digitalWrite(PIN_BUZZ, HIGH); } // 連續:105dB
else { for (int i = 0; i < 2; i++) { digitalWrite(PIN_BUZZ, HIGH);
delay(90); digitalWrite(PIN_BUZZ, LOW); delay(90); } } // 預警:雙短嗶
}
void ringMode(uint8_t m) { // 0 夜燈 1 倒數 2 警報 3 靜音
if (m == 0) { uint8_t b = ledDim ? 40 : 90;
ring.fill(ring.Color(255, 150, 60, b)); ring.show(); }
else if (m == 3) { ring.clear(); ring.show(); }
}
void ringCountdown(float f /*0..1 剩餘*/) {
ring.clear();
int lit = roundf(f * 12);
for (int i = 0; i < lit; i++) ring.setPixelColor(i, ring.ColorHSV(
8500 - (int)(f * 8000), 255, 200)); // 綠→橙→紅随倒數縮減
ring.show();
}
/* --- 4. 事件記錄與推播(Wi-Fi 只在這裡出現) ------------------------ */
struct Event { DateTime t; float peak; uint16_t durS; bool dual; };
Event logBuf[50]; uint8_t logN = 0;
/* 用途:事件结束后非同步上報;失敗重試 3 次、每次逾時 4 秒
* 為什麼逾時要短:推播不能拖死重設靜狀態的計時器;試三次放棄,
* 本地記錄永遠是真相的備份 */
void reportTask(void* p) {
Event* e = (Event*)p;
if (strlen(TG_TOKEN) == 0) { free(e); vTaskDelete(NULL); }
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
for (int i = 0; i < 20 && WiFi.status() != WL_CONNECTED; i++) vTaskDelay(500);
if (WiFi.status() == WL_CONNECTED) {
for (int tryN = 0; tryN < 3; tryN++) {
WiFiClientSecure c; c.setInsecure();
if (c.connect("api.telegram.org", 443, 4000)) {
char body[512]; JsonDocument d;
d["text"] = String("🚨 地震事件 ") + e->t.format("%m/%d %H:%M:%S")
+ "|峰值 " + String(e->peak, 0) + "|歷時 " + e->durS
+ "s" + (e->dual ? "|雙感測確認" : "");
size_t n = serializeJson(d, body);
c.print(String("POST /bot") + TG_TOKEN + "/sendMessage HTTP/1.1\r\n"
"Host: api.telegram.org\r\nContent-Type: application/json\r\n"
"Content-Length: " + String(n) + "\r\n\r\n" + body);
String rsp = c.readStringUntil('{'); rsp = c.readStringUntil('}');
c.stop();
if (rsp.indexOf("\"ok\":true") >= 0) break;
vTaskDelay(2000); // backoff,別轟炸 API
}
}
}
WiFi.disconnect(true); WiFi.mode(WIFI_OFF); // 用完就關:平時不養無線電
free(e); vTaskDelete(NULL);
}
void commitEvent(Event e) {
if (logN < 50) logBuf[logN++] = e;
nvs.begin("quake", false);
nvs.putUInt("n", logN);
nvs.putFloat("lastPeak", e.peak);
nvs.putULong("lastT", e.t.unixtime());
nvs.end();
Event* copy = new Event(e);
xTaskCreate(reportTask, "report", 8192, copy, 2, &hReport); // 推播離開主迴路
}
/* --- 5. 主迴路:2kHz 取樣+五狀態機 -------------------------------- */
uint32_t fsTimer = 0, lastSample = 0;
void setup() {
Serial.begin(115200);
analogSetPinAttenuation(PIN_PIEZO, ADC_11db);
pinMode(PIN_BUZZ, OUTPUT); pinMode(PIN_ACOK, INPUT);
ring.begin(); ring.setBrightness(200); ringMode(0);
rtc.begin();
if (rtc.lostPower()) rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
nvs.begin("quake", true);
noiseFloor = max(nvs.getFloat("nf", 400), (float)NOISE_FLOOR_MIN);
// 開機 3 秒噪音底快掃:把此刻環境當成「正常」的起點
for (uint32_t t0 = millis(); millis() - t0 < 3000;)
noiseFloor = max(noiseFloor * 0.5f, shakeMetric() * 0.25f);
esp_task_wdt_init(10, true); // 10 秒看門狗:卡死就重啟,不裝死
esp_task_wdt_add(NULL);
}
void loop() {
esp_task_wdt_reset();
if (millis() - lastSample < 1000000UL / FS_HZ / 2) return; // 半週期交錯
lastSample = millis();
uint16_t m = shakeMetric();
if (m > winPeak) winPeak = m;
/* ---- 50ms 視窗收尾:狀態機 ---- */
if (millis() - winT0 < WIN_MS) return;
winT0 = millis();
float metric = winPeak; winPeak = 0;
acOk = digitalRead(PIN_ACOK) == HIGH;
if (!acOk && !ledDim) { ledDim = true; ringMode(0); } // 斷電:降亮續航
if (acOk && ledDim) { ledDim = false; ringMode(0); }
float thresh = max(noiseFloor * 4.0f, 300.0f);
switch (st) {
case IDLE:
// 噪音底每 10 分鐘用第 10 百分位概念慢速下修——下修快、上修慢
noiseFloor += (metric - noiseFloor) * (metric < noiseFloor ? 0.05f : 0.0005f);
if (metric > thresh) { st = P_CAND; hits[0] = 1; hitN = 1; eventPeak = metric; }
break;
case P_CAND:
hitN = (hitN + 1) % 8; hits[hitN] = metric > thresh;
eventPeak = max(eventPeak, metric);
{ uint8_t n2 = 0; for (uint8_t i : hits) n2 += i;
if (n2 >= CONF_WINDOWS - 1) { st = WARN; eventT0 = millis(); sirenOn(false); }
else if (metric < eventPeak * (1.0f - SINGLE_SPIKE_KILL)) st = IDLE; } // 單脈衝拒絕
break;
case WARN: { // 3 秒倒數:還活著就數給你聽
float left = 3000.0f - (millis() - eventT0);
ringCountdown(constrain(left / 3000.0f, 0, 1));
if (left <= 0) { if (metric > thresh * 0.6f) { st = SHAKE; sirenOn(true);
ringMode(0); ring.fill(ring.Color(255, 0, 0, 255)); ring.show(); }
else { st = QUIET; eventT0 = millis(); } } }
break;
case SHAKE:
if (metric < thresh * 0.4f) { st = QUIET; eventT0 = millis();
digitalWrite(PIN_BUZZ, LOW); }
break;
case QUIET:
if (millis() - eventT0 > 30000) {
bool dual = shakeMetric() > thresh * 0.2f; // 簡化的雙感測標記
Event e{ rtc.now(), eventPeak, (uint16_t)((eventT0 - 0) / 1000), dual };
e.durS = (uint16_t)((millis() - eventT0) / 1000 + 30);
commitEvent(e);
nvs.begin("quake", false); nvs.putFloat("nf", noiseFloor); nvs.end();
st = IDLE; ringMode(0);
}
break;
}
}
⚠️ 關於「P 波預警」的誠實說明:本地壓電盤能判「豔動開始」,但單機無法區分 P 波與小規模淺層地震本體。本機把「持續晃動」一律當 P 波預警處理、把「能量升級」當 S 波追蹤——在市区距震中數十公里的場景下,這與「早幾秒的強震警報」效果相同;但極淺近震(P-S 間隔<2 秒)時,它只會和搖晃同時響。這是物理,不是韌體 bug。
🔧 組裝步驟
- 壓電夾具是灵魂:壓電盤夾在兩片 TPU 墊之間,M3 螺鎖著 20g 銅塊穿過中心,夾到「搖桌有明確抖感」但不能把陶瓷壓出崩角。裝進塔身底部導管,線往後走。
- 主板堆疊:SuperMini 焊 ADXL335(X 軸對準床頭方向,Z 軸垂直)、蜂鳴器、燈環、DS3231;clamp 二極體與 1MΩ 直接焊在 GPIO0 焊腳上,把雜訊路徑壓到最短。
- 電源鏈:USB → TP4056 → 18650 → MT3608 → 1A 保險絲 → 板 5V。VBAT 分壓焊 GPIO4;市電偵測分壓焊 GPIO5+USB 5V。
- 裝塔:主板滑入塔身、燈環嵌透光窗、RTC 抽屜與電池抽屜插入、配重槽鎖 4 顆 M8 螺母。整塔立起,搖一下聽得到清脆的「喀」=質量塊沒鎖緊,重鎖。
- 位置:床頭櫃靠牆角(剛性最大、地板傳來的雜訊最小),燈環對準床的位置。不要放在會共振的鬆木板上。
- 校準:開機長按 BOOT 3 秒進入敲擊校準——用橡膠槌敲床頭板 5 下、敲牆角 5 下、請家人關門 3 次,Serial 會輸出建議閾值(自動=噪音底×4,可手動改常數重燒)。
📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
🧪 驗證——把它當保鮮盒一樣驗收
| 測試 | 動作 | 合格線 |
|---|---|---|
| 觸發率 | 橡膠槌正常力道敲床頭板 10 下 | 10/10 觸發(預警嗶+倒數) |
| 誤報率 | 用力關房門 10 次、書掉地板 10 次 | 0 觸發(單脈衝拒絕在工作) |
| 低頻模擬 | 手機外放回 30Hz 低音影片貼床頭櫃 | 觸發(遠震長週期就是这个味道) |
| 斷電切换 | 拔掉火牛 | 5 秒內燈環降亮+Serial 記「AC LOST」;接回自動恢復 |
| 續航 | 斷電跑一夜(含夜燈 40 亮度) | 11 小時後 VBAT > 3.3V(分壓讀值×2) |
| 事件報告 | 觸發一次後等 35 秒 | Telegram 收到含時間/峰值/歷時的訊息;NVS lastPeak 更新 |
🆘 問題排除
| 症狀 | 最可能的原因 | 處方 |
|---|---|---|
| 從不觸發 | 壓電夾具沒夾緊(TPU 墊太厚吃掉行程)/質量塊滑牙 | 減少一片墊片重夾;鎖緊銅塊——這台的灵敏度 80% 在夾具上 |
| 半夜一直叫 | 噪音底被「上修慢、下修快」反了?或塔放鬆木板上共振 | 確認 0.05/0.0005 兩係數方向沒改反;塔下墊輪胎橡膠片;改 thresh 從 ×4 到 ×6 |
| ADC 讀值 4096 卡死 | 壓電靜電脈衝打穿 clamp(BAV99 虛焊) | 補焊;韌體看門狗 10 秒會自動重啟,但硬體要治本 |
| Wi-Fi 一開就誤報 | 感測讀到了 ADC2 共用通路的射頻脈衝 | 確認三感測全在 GPIO0–4;線材勿與天線並走 |
| 時間永遠回到 2000 | DS3231 硬幣電池裝反/模組焊腳虛 | 調極性;首次校時後拔電 1 分鐘再量時間還原 |
| 斷電燈環不降亮 | 市電偵測分壓焊到 3V3 而不是 USB 5V 側 | 偵測點必須在 TP4056 輸入側——充飽時 TP4056 斷充電,輸出側會假裝斷電 |
📁 專案檔案結構
earthquake-alarm/
├── firmware/
│ └── quake_tower.ino # 上方完整韌體
├── stl/
│ ├── tower-body.stl # 塔身(螺旋無縫)
│ ├── piezo-clamp.stl # 壓電夾具
│ ├── tpu-pad.stl # TPU 減振墊 ×2
│ ├── base-weight.stl # 底座配重槽
│ └── drawer-rtc.stl # RTC/電池抽屜
├── f3d/
│ └── quake-tower.f3d # Fusion 360 原始檔
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md # 說明文件