📋 專案概述

0403 那個星期五,我在台北睡得比想像中沉。把我拉起來的不是搖晃——是自己在黑暗裡被書架推倒的椅子撞醒的。事後我躺在還微微發暈的床上想了一件很可怕的事:我的身體比警報系統先知道地震,但我的意識晚了它整整幾秒,而「幾秒」在強震裡是以「來不來得及把女兒從床邊抱開」計時的。手機的強震即時警報後來也響了,屏幕在床頭櫃上亮著,而我花了三秒才理解那串 nationally 統一的字在叫什麼。真正的窗口在 P 波:縱波先到、橫波隨後,本市距震中 100 公里以內時,兩者之間大約 5~20 秒。這 5~20 秒足夠關火、開門、鑽桌下、把孩子拉離窗邊——前提是有個東西在那幾秒內替你清醒。

市售方案我全部摸過一遍:

  1. 日本地震警報燈(地震予知ウォッチ那一類):2,000 元有找,只吃 100V、日文語音、感測門檻封死不能調。放在台北的鋼筋混凝土樓房裡,它聽的是牆、不是地板。
  2. 手機會員推送:它假設你睡著時把手機壓在枕頭下且音量全開——我睡覺飛彈來了都不翻身,別測試我的睡眠品質。
  3. 「強震即時警報本來就免費,你还要什麼」:要的是本地、即時、不經手機的觸發。中央氣象署的警報是「已經確定要來了才發」;自己床頭的地震儀是「地板一抖我就知道」,兩者差的那一段,正是本專案。
⚠️ 誠實定位:本機不是地震預報儀,沒有任何東西能預報地震。它做的是「P 波本地偵測+提前幾秒喚醒與照明」——以及最重要的一件事:斷電時它還亮著。它也不會幫你關瓦斯、開電鎖,那些是另外的專案。

這台 ESP32-C3 床頭警報塔做的事:

帳目:日本警報燈 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 SuperMini1~22 元C3-MINI-1 模组;買兩顆備用,這東西值得床頭+客廳各一
主感測器27mm 壓電陶瓷片+銅片1~15 元直徑越大低頻越靈;附 20g 銅質量塊(大螺母自製亦可)
輔助感測ADXL335 三軸模組1~65 元類比輸出、2g 量程對「搖晃程度」剛好;買 ADXL345 數位版請改 I2C 讀法
警報105dB 有源蜂鳴器(5V)1~18 元「有源」自振、GPIO 一給電就叫——喚醒耳朵不喚醒邏輯
燈環WS2812B 12 燈環1~55 元倒數、夜燈、警報三種燈語;兼床頭小夜燈,老婆第一票就過
RTCDS3231 模組+CR12201~35 元斷電三天時間不丟——事件報告沒有正確時間就只是噪音
備援電源18650 帶保護板 2600mAh1~85 元正規品牌;警報器不配無保護板的拆機芯
充電/升壓TP4056(帶保護)+MT3608各 1~55 元充飽自動停;升壓輸出串 1A 保險絲——鋁箔紙的教訓都懂
訊號保護1MΩ、100kΩ×2、BAV99 clamp1 組~8 元壓電盤絕不能被靜電脈衝親過,clamp 到 3V3 是標配
電源5V 1A USB-C 火牛+短線1~45 元全年不關的火牛,買品牌貨,這是不二法門
列印件塔身、壓電夾具、底座配重、RTC 抽屜1 組~130 元詳見 3D 列印節
合計~522 元全機無強電,火牛用現成品

🔌 接線圖

C3 SuperMini 22 × 18 mm GPIO0 GPIO1 GPIO2 GPIO4 GPIO6 GPIO7 GPIO8 GPIO9 GPIO5 壓電盤+20g 質量塊 1MΩ 對地 BAV99 ADC1_CH0(避開 ADC2) ADXL335 X→GPIO1 Z→GPIO2 +3V3/GND 105dB 蜂鳴器(S8050 驅動) WS2812 12 燈環(倒數/夜燈/警報) DS3231 SDA→GPIO8 SCL→GPIO9 USB 5V 分壓→GPIO5 市電偵測 18650+TP4056 MT3608 5V VBAT 100k/100k 分壓→GPIO4(ADC1_CH4) 全機 5V 由升壓供電;USB 直插僅充電與市電偵測參考
圖一:ESP32-C3 床頭地震警報塔接線圖。紫=類比感測、紅=數字/警報、藍=I2C、橘=電源、深藍=GND
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)高電位叫;待機拉低=不給它任何發聲的機會
GPIO7WS2812 環330Ω 絕不省略;夜燈模式亮度 40,警報 255
GPIO8/GPIO9DS3231 SDA/SCL模組自帶上拉;RTC 電池座先裝再上電,第一次開機手動校時

📊 偵測邏輯(狀態機)

IDLE 監視 噪音底自學習 候選 P? 單視窗超限 確認 P 波 3 窗內 2 窗持續 倒數預警 3s 短嗶+燈環倒數 搖晃追蹤 105dB+夜燈全亮 歸靜 30s 寫事件+推播 單脈衝拒絕:衰減 >70%=撞擊非地震 包絡 > 噪音底×4 能量持續(S 波) 能量 < 閾值×0.4 持續 30s
圖二:五狀態偵測機。IDLE 的噪音底每 10 分鐘用「第 10 百分位包絡」更新一次——它學習你家的關門聲,但不學習搖晃

🧊 3D 列印設計

零件材質列印設定時間備註
塔身 Ø68×190mmPLA0.2mm/螺旋圓柱無縫/15% 菱形~150 分燈環段開窗(高 18mm 透光圈);壁內預留壓電夾具導管
壓電夾具PLA+TPU 墊×20.16mm/實心 3 層~20 分壓電盤夾在兩片 TPU 之間、中央鎖 20g 銅質量塊——這就是地震儀的擺,夾緊力決定靈敏度一致性
底座(配重槽)PLA0.24mm/底部實心 5 層~60 分槽放 4×M8 螺母+備用 18650;底面貼 3M 泡棉膠條防滑
RTC/電池抽屜PLA0.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 掛了,蜂鳴器照叫。
/* ================================================================
 *  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。

🔧 組裝步驟

  1. 壓電夾具是灵魂:壓電盤夾在兩片 TPU 墊之間,M3 螺鎖著 20g 銅塊穿過中心,夾到「搖桌有明確抖感」但不能把陶瓷壓出崩角。裝進塔身底部導管,線往後走。
  2. 主板堆疊:SuperMini 焊 ADXL335(X 軸對準床頭方向,Z 軸垂直)、蜂鳴器、燈環、DS3231;clamp 二極體與 1MΩ 直接焊在 GPIO0 焊腳上,把雜訊路徑壓到最短。
  3. 電源鏈:USB → TP4056 → 18650 → MT3608 → 1A 保險絲 → 板 5V。VBAT 分壓焊 GPIO4;市電偵測分壓焊 GPIO5+USB 5V。
  4. 裝塔:主板滑入塔身、燈環嵌透光窗、RTC 抽屜與電池抽屜插入、配重槽鎖 4 顆 M8 螺母。整塔立起,搖一下聽得到清脆的「喀」=質量塊沒鎖緊,重鎖。
  5. 位置:床頭櫃靠牆角(剛性最大、地板傳來的雜訊最小),燈環對準床的位置。不要放在會共振的鬆木板上。
  6. 校準:開機長按 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;線材勿與天線並走
時間永遠回到 2000DS3231 硬幣電池裝反/模組焊腳虛調極性;首次校時後拔電 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                     # 說明文件
← 上一篇:ESP32-S2 免接觸特雷門 📚 系列索引 下一篇:敬請期待(系列索引)→