📋 專案概述

上周五團練,主唱在台上調音調了四分鐘。他腳邊那台 599 元的踏板調音器是「麥克風式」的——底下鼓手試大鼓、前面吉他手隨手撥兩下,螢幕上的指針就跟著全團搖,永遠定不下來。最後他掏出手機開 App,把麥克風對著琴頭調——團長當場臉黑:演出前的手忙腳亂,一半是從調音開始的。

問題的本質很簡單:用耳朵聽的調音器,在噪音環境裡就是亂指。真正的夾式調音器夾在琴頭上讀「振動」,外界多吵都與它無關。但市售品查下來:

  1. 兩三百元的「夾式」多半是偽裝:規格表寫「夾式/麥克風雙模式」,實際是同一顆麥克風加了個觸發器,現場照樣失效。
  2. 真的壓電感測的入門款 700+ 元起跳,螢幕還是一格 1 cent 都看不出來的粗指針;同一顆陶瓷感測片,原料成本不到 20 元。
  3. 手機 App 是最後的底牌不是常態:要解鎖、要開 App、佔記憶體、來一通電話前功盡棄——團練一週三次,你不該依賴一個要解鎖的工具。

這個專案用 ESP32-C3 SuperMini(比金士戒還小)+ 一颗 15 元壓電陶瓷片,做一台夾上去就動的調音器:

成本帳:入門壓電調音器 700 元起,本專案 411 元;重點是韌體在你手上——指針阻尼、睡前去抖、甚至加個半音下落調弦模式,都是改幾行 code 的事。

為什麼選 ESP32-C3?

需求C3 的優勢
運算量RISC-V 单核 160MHz 帶硬件乘法,256 點定點 FFT 綽綽有餘;C2 的運算餘裕就緊張了
ADC 品質12-bit ADC + 可調衰減,壓電訊號直接吃,不用外接放大濾波電路
深睡電流~5µA:調音器 99% 的時間在夾著發呆,省電 = 電池半年
體積SuperMini 22 × 18 mm,塞進琴夾殼裡剛剛好,重心不壓琴頭

🧱 硬體清單

項目型號數量預估單價備註
主控板ESP32-C3 SuperMini1~40 元各廠 C3 SuperMini 皆可;腳位以絲印為準
振動感測27MHz 壓電陶瓷圓片(直徑 35mm)1~15 元報廢的壓電蜂鳴器拆開拿陶瓷片也一樣,比 MEMS 加速度計便宜一個量級
顯示SSD1306 0.91" OLED(窄長條)1~85 元128×32,比方屏更适合指針 + 音名排版
電源開關SS-12D00 滑動開關1~10 元接 GPIO2 當「軟體電源」:放琴盒不會誤喚醒吃光電池
電池座AAA×2 電池盒(帶引線)1~20 元3V 直供板子 3V3 腳,鹼性可用半年
電阻1MΩ、100kΩ(0805 或直插)各 1~2 元壓電偏壓分壓:1M 上拉 3V3 + 100k 下拉(見接線)
電容100nF 陶瓷電容1~1 元高通:擋掉指板按壓的準直流分量
夾面墊TPU/矽膠夾墊 52×30mm1~15 元夾琴頭不留夾痕是底線
雜項M2 自攻螺絲、0.5mm 漆包線、尼龍束帶1~18 元壓電片接線用漆包線(軟,不傳機械震動)
合計~411 元

🔌 接線圖

ESP32-C3 SuperMini · USB 朝檢修孔 USB-C(燒錄用) GPIO0 (ADC) GPIO2 (SW) GPIO8/9 I2C 壓電陶瓷片 (膠著琴頭背面) 1MΩ 1MΩ 上拉 3V3(給陶瓷片偏壓回路) 100kΩ ↓GND 100nF 分壓中點 0.26V + 100nF 高通:只留振動、擋掉按壓準直流 滑動開關 GPIO2 ↔ GND(内部上拉,滑下=GND) E -12c 6E 0.91" OLED 128×32 SDA SCL 3V3 GND 藍線 ×2 GPIO8/9 → SDA/SCL AAA×2 → 3V3 / GND 軟體電源(GPIO2 深睡)
▲ 壓電片本體是高阻抗電壓源:1M/100k 分壓把它拉到 ADC 安全區,100nF 高通濾掉按壓的準直流,全電路被動零零件加價。

ASCII 對照表

ESP32-C3 SuperMini GPIO0 ○── 黃線 ── 壓電前端(陶瓷片 →1MΩ→ 節點 ←100kΩ← GND,節點經 100nF → GPIO0) GPIO2 ○── 綠線 ── 滑動開關 → GND(內部上拉:開=HIGH / 關=LOW) GPIO8 ○── 藍線 ──── OLED SDA GPIO9 ○── 藍線 ──── OLED SCL 3V3 ○── 紅線 ──┬── OLED VCC、1MΩ 上拉端 GND ○── 黑線 ──┬── OLED GND、100kΩ 下端、電池負極 ※ 開關走「軟體電源」設計:滑到 OFF → GPIO2 讀 LOW → 韌體深睡(5µA);滑回 ON 即喚醒 ※ 意外福利:C3 的 GPIO2 是 strapping 腳,夾著開關(LOW)按 RESET 直接進燒錄模式,不用拆殼 ※ 全機無 DC-DC,AAA 直接吃 3V3 腳(C3 允許 3.0~3.6V);長期不用請拔電池

詳細接線

ESP32-C3 腳位連接說明
GPIO0(ADC1_CH0)100nF 高通後接分壓中點靜態工作點 ≈ 3.3V × 100k/(1M+100k) ≈ 0.3V;撥弦訊號±0.15V 內線性擺動
GPIO2滑動開關 → GND內部上拉;「關」不是斷電而是深睡——韌體能顯示剩餘電量、開關壞了也不鎖死
GPIO8 / GPIO9OLED SDA / SCLC3 默認 I2C 腳,免指定引腳號
3V3 / GND電池直接供電(開關走 GPIO2 軟體電源)AAA 鹼性 3.0→1.8V 衰降,C3 3V3 腳允許範圍 3.0~3.6V:低於 2.7V 韌體強制深睡護電池
⚠️ 壓電片兩面引線務必用軟漆包線,且走線留 2cm 應力迴圈後點膠固定。硬銅線等於把琴頭的振動直接「焊」進陶瓷片——調音器會愛上你自己手指的動静,稍碰琴頭就觸發。

🖨️ 3D 列印零件

零件清單

零件數量材質建議顏色列印時間
琴夾主體(含彈簧夾臂)1PETG(韌性,夾臂反復掰不斷)黑色~1.5 hr
夾臂墊塊2TPU 95A灰~15 min
壓電感測杯1PLA黑~25 min
上蓋(OLED 窗)1PETG黑~40 min
開關帽1TPU橘~8 min

列印設定

設計要點

  1. 感測杯「三點球接」:壓電片膠在杯底中心,杯殼則由三條細筋懸吊固定在本體——機械震動走「杯底→陶瓷片」主路徑,殼體的雜訊被細筋隔開。這是本機信噪比比「陶瓷片直接貼殼」方案高一倍的關鍵
  2. 夾力 300g 設計:夾臂厚度 3.2mm + 根部 1.5mm 圓角,夾持力剛好壓實矽膠墊又不變形;夾口開度 38~56mm 通吃民謠/古典琴頭
  3. OLED 上傾 30°:夾上琴頭後正對演奏者眼睛,不用低頭看琴頭底
  4. 電池艙側抽:不拆殼換電池;艙門卡扣 + 一片防誤開內鎖

💻 程式碼

📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
📚 相依:板子選「ESP32C3 Dev Module」,PSRAM 關、CDC On Boot 開;Lib Manager 裝 Adafruit SSD1306 + Adafruit GFX。FFT 手刻定點,不吃浮點庫記憶體、不吃 FPU(C3 無硬件 FPU,浮點 FFT 會慢四倍)。
/* ============================================================
 *  ESP32-C3 夾式吉他調音器  |  Clip-On Guitar Tuner
 *  ------------------------------------------------------------
 *  功能:壓電片測振 → 256 點 Q15 FFT → 拋物線內插,
 *        E2(82Hz)~E4(330Hz) 六弦 ±1 cent 解析、2 分鐘自動深睡
 *  硬體:ESP32-C3 SuperMini + 壓電陶瓷片(GPIO0/ADC)
 *        + SSD1306 128x32(GPIO8/9) + 滑動開關(GPIO2)
 *  供電:AAA x2 → 3V3 腳(無 DC-DC)
 *  相依:Arduino core for ESP32 v3.x / Adafruit_SSD1306 / GFX / Wire
 *  原理速記:調音要的不是「聽清音高」而是「追蹤基頻」——
 *        256 點 @8kHz 解析度 31Hz 不夠,但音高是窄帶訊號,
 *        頻譜峰值用相鄰三 bin 拋物線內插可把誤差壓到 <1% bin,
 *        82Hz 處 <0.3 cent。這比上 4096 點浮點 FFT 便宜 30 倍。
 *  ============================================================ */

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

/* ------------------------------------------------------------
 * 1. 腳位與取樣參數
 * ------------------------------------------------------------ */
const uint8_t PIN_PIEZO = 0;    // ADC1_CH0
const uint8_t PIN_SW    = 2;    // 滑動開關→GND(内部上拉)
const uint8_t I2C_SDA   = 8;
const uint8_t I2C_SCL   = 9;

const uint32_t SAMPLE_RATE_HZ = 8000;
const uint8_t  FFT_N          = 256;               // Q15 FFT 以 256 點為速度甜點
const float    BIN_HZ         = (float)SAMPLE_RATE_HZ / FFT_N;  // 31.25 Hz
// 吉他開弦範圍 E2(82.4)~E4(329.6);搜 bin 4~10(含低次泛音容錯上限)
const uint8_t  SEARCH_LO_BIN  = 4;                 // ~125Hz 以下環境隆隆聲不搜
const uint8_t  SEARCH_HI_BIN  = 45;                // ~1400Hz 涵蓋 E4 三次泛音
const uint16_t TRIG_MAG_MIN   = 900;               // 峰值幅度門槛:低於視為環境噪音
const uint8_t  SLEEP_TIMEOUT_S = 120;              // 無有效音 2 分鐘深睡

/* ------------------------------------------------------------
 * 2. Q15 定點 FFT(radix-2 原位,rotator 查表)
 *    C3 無 FPU:浮點 cosf 每點 400+ cycles,查表乘加只要 8 個
 * ------------------------------------------------------------ */
static int16_t fftRe[FFT_N], fftIm[FFT_N];
static int16_t sinTab[FFT_N / 4];                   // 64 點 sin 表(Q15,setup 時 sinf 生成一次)

static inline int16_t q15mul(int16_t a, int16_t b) { // 定點乘:(a*b)>>14 四捨五入
  return (int16_t)((((int32_t)a * b) + (1 << 13)) >> 14);
}
// 以 sin 表算 cos/sin(角度取負=正向旋因子 e^{-j2πk/N})
static inline void rotator(int k, int16_t& c, int16_t& s) {
  k &= (FFT_N - 1);
  if (k < FFT_N / 4)            { s = sinTab[k];           c = sinTab[FFT_N / 4 - k]; }
  else if (k < FFT_N / 2)       { s = sinTab[FFT_N / 2 - k]; c = (int16_t)-sinTab[k - FFT_N / 4]; }
  else if (k < 3 * FFT_N / 4)   { s = (int16_t)-sinTab[k - FFT_N / 2]; c = (int16_t)-sinTab[3 * FFT_N / 4 - k]; }
  else                          { s = (int16_t)-sinTab[FFT_N - k];     c = sinTab[k - 3 * FFT_N / 4]; }
}
static void fftQ15(void) {
  /* --- bit-reverse 置換 --- */
  for (uint16_t i = 1, j = 0; i < FFT_N; i++) {
    uint16_t bit = FFT_N >> 1;
    for (; j & bit; bit >>= 1) j &= ~bit;
    j |= bit;
    if (i < j) {
      int16_t t = fftRe[i]; fftRe[i] = fftRe[j]; fftRe[j] = t;
      t = fftIm[i]; fftIm[i] = fftIm[j]; fftIm[j] = t;
    }
  }
  /* --- 蝶形運算:長度 2→256,每級 k 用 rotator 查表 --- */
  for (uint16_t len = 2; len <= FFT_N; len <<= 1) {
    uint16_t half = len >> 1, step = FFT_N / len;
    for (uint16_t i = 0; i < FFT_N; i += len) {
      for (uint16_t k = 0; k < half; k++) {
        int16_t c, s;
        rotator((int)k * step, c, s);
        uint16_t a = i + k, b = a + half;
        int16_t tr = (int16_t)((q15mul(fftRe[b], c) - q15mul(fftIm[b], s)) >> 1); // >>1 防各級溢位
        int16_t ti = (int16_t)((q15mul(fftRe[b], s) + q15mul(fftIm[b], c)) >> 1);
        fftRe[b] = (int16_t)((fftRe[a] - tr) >> 1);
        fftIm[b] = (int16_t)((fftIm[a] - ti) >> 1);
        fftRe[a] = (int16_t)((fftRe[a] + tr) >> 1);
        fftIm[a] = (int16_t)((fftIm[a] + ti) >> 1);
      }
    }
  }
}

/* ------------------------------------------------------------
 * acquireFrame —— 定時基擷取 256 樣本 → 去直流 → Hanning → Q15
 *   為什麼用 micros() 排程而非 delay:ADC 讀取耗時隨 Wi-Fi 中斷抖動,
 *   取樣間距不規律會讓頻譜「 smear 」,音準判斷直接失準
 * ------------------------------------------------------------ */
static uint16_t mag[FFT_N / 2];
static bool acquireFrame(void) {
  static int32_t prevMean = 2048;
  static uint32_t nextDue = 0;
  int32_t sum = 0;
  uint32_t interval = F_CPU / SAMPLE_RATE_HZ;       // micros 間隔 125
  nextDue = micros() + 1000;                        // 避開剛進函式的抖動區
  for (uint16_t i = 0; i < FFT_N; i++) {
    while ((int32_t)(micros() - nextDue) < 0) {}    // 忙等下一拍(整段擷取僅 32ms)
    int32_t v = analogRead(PIN_PIEZO);              // 12-bit,已設 11dB 衰減
    nextDue += interval;
    sum += v;
    fftRe[i] = (int16_t)(v - 2048);                 // 暫存原始值,均值出来後再去直流
    fftIm[i] = 0;
  }
  int32_t mean = (sum + FFT_N / 2) / FFT_N;
  int32_t drift = mean - prevMean;                  // 低通跟蹤工作點漂移(温漂/壓偏)
  prevMean += drift / 8;
  for (uint16_t i = 0; i < FFT_N; i++) {
    float w = 0.54f - 0.46f * cosf(6.2832f * i / (FFT_N - 1)); // Hanning:抑制頻譜泄漏,
    int32_t v = (fftRe[i] - prevMean) * w;          // 讓相鄰泛音不被主瓣裙邊吞掉
    fftRe[i] = (int16_t)constrain(v, -32768, 32767);
  }
  return true;
}

/* ------------------------------------------------------------
 * fftMag —— 幅度譜(上三角半頻譜)
 * ------------------------------------------------------------ */
static void fftMag(void) {
  for (uint16_t k = 0; k < FFT_N / 2; k++) {
    int32_t re = fftRe[k], im = fftIm[k];
    mag[k] = (uint16_t)((abs(re) + abs(im)) >> 1);  // |re|+|im| 近似幅度:省開方,
  }                                                // 峰位判斷與開方版結果幾乎同位
}

/* ------------------------------------------------------------
 * parabolicPeak —— 三點拋物線內插
 *   參數:峰值 bin;回傳:修正後小數 bin
 * ------------------------------------------------------------ */
static float parabolicPeak(uint16_t k) {
  float a = mag[k - 1], b = mag[k], c = mag[k + 1];
  float denom = (a - 2 * b + c);
  if (fabs(denom) < 1e-6f) return (float)k;         // 平頂:退回整 bin
  return (float)k + constrain(0.5f * (a - c) / denom, -0.5f, 0.5f);
}

/* ------------------------------------------------------------
 * detectFreq —— 找基頻
 *   回傳:Hz;0 = 無效音(太輕、太吵、或在搜频段外)
 *   為什麼看 bin 的偶數倍能量比:吉他基頻常被 2 次泛音壓過,
 *   若峰 k 的兩倍 bin 能量 > 峰本身×0.7,真基頻在 k/2
 * ------------------------------------------------------------ */
static float detectFreq(void) {
  uint16_t best = 0; uint16_t bestMag = 0;
  for (uint16_t k = SEARCH_LO_BIN; k <= SEARCH_HI_BIN; k++)
    if (mag[k] > bestMag) { bestMag = mag[k]; best = k; }
  if (best < SEARCH_LO_BIN + 1 || bestMag < TRIG_MAG_MIN) return 0;
  uint16_t half = best / 2;
  if (half >= SEARCH_LO_BIN && (uint32_t)mag[half] * 10 > (uint32_t)mag[best] * 7)
    best = half;                                    // 基頻被泛音吃掉的補救
  return parabolicPeak(best) * BIN_HZ;
}

/* ------------------------------------------------------------
 * noteFor —— 頻率 → 最近半音 + cents 偏差
 *   輸出:midi 音序號(69=A4)、cents(-50~+50)
 * ------------------------------------------------------------ */
static void noteFor(float f, uint8_t& midi, float& cents) {
  float n = 69.0f + 12.0f * log2f(f / 440.0f);
  midi  = (uint8_t)roundf(n);
  cents = (n - (float)midi) * 100.0f;
}

const char* NOTE_NAMES[12] = {"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"};
// 標準定音六弦的 midi:E2 A2 D3 G3 B3 E4
const uint8_t STRING_MIDI[6] = {40, 45, 50, 55, 59, 64};
const char* STRING_LABEL[6]  = {"6E","5A","4D","3G","2B","1e"};

/* ------------------------------------------------------------
 * uiDraw —— 音名大字 + 指針 + cents + 弦號
 *   指針刻度 ±50 cents 映射到 128px 寬
 * ------------------------------------------------------------ */
Adafruit_SSD1306 disp(128, 32, &Wire, -1);
void uiDraw(const char* note, uint8_t stringIdx, float cents, bool active) {
  disp.clearDisplay();
  disp.setTextColor(SSD1306_WHITE);
  if (!active) {
    disp.setTextSize(1);
    disp.setCursor(34, 12);
    disp.print("...ready");
    disp.display();
    return;
  }
  disp.setTextSize(2);
  disp.setCursor(0, 0);
  disp.print(note);                                  // 音名大字(最多 2 字)
  if (stringIdx < 6) {
    disp.setTextSize(1);
    disp.setCursor(100, 0);
    disp.print(STRING_LABEL[stringIdx]);             // 右上角弦號:該擰哪根弦
  }
  // 指針:中心線 + 偏差刻度
  for (int x = 0; x < 128; x += 8) disp.drawFastVLine(x, 22, 3, SSD1306_WHITE);
  int16_t px = 64 + (int16_t)(cents * 1.24f);        // ±50c → ±62px
  px = constrain(px, 1, 126);
  disp.fillFastVLine(px - 1, 16, 14, SSD1306_WHITE);
  disp.setCursor(64 + 8, 22);                        // cents 數字縮在中間空位
  disp.printf("%+d", (int)cents);
  disp.display();
}

void setup() {
  pinMode(PIN_SW, INPUT_PULLUP);
  analogSetPinAttenuation(PIN_PIEZO, ADC_11db);      // 0~3.3V 量程
  analogSetWidth(12);

  for (int i = 0; i < FFT_N / 4; i++)                // sin 表只生成一次,上電即定點
    sinTab[i] = (int16_t)(sinf(6.2832f * i / FFT_N) * 32767.0f + (sinf(6.2832f * i / FFT_N) >= 0 ? 0.5f : -0.5f));

  Wire.begin(I2C_SDA, I2C_SCL);
  disp.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  disp.clearDisplay(); disp.display();
  disp.setTextSize(1); disp.setCursor(20, 12);
  disp.print("CLIP & STRUM");
  disp.display();
  delay(600);
}

void loop() {
  static uint32_t lastValidMs = 0;

  /* --- 1. 開關:關 = 深睡,撥回=GPIO 喚醒 --- */
  if (digitalRead(PIN_SW) == LOW) {
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_2, 1);     // HIGH 喚醒(撥回開)
    disp.clearDisplay(); disp.display();             // 先滅屏再睡,不留殘影
    esp_deep_sleep_start();
  }

  /* --- 2. 擷取 + 頻域 --- */
  acquireFrame();
  fftQ15();
  fftMag();
  float f = detectFreq();

  /* --- 3. 無效音:靜態畫面,逾時進深睡 --- */
  if (f <= 0) {
    uiDraw("", 9, 0, false);
    if (millis() - lastValidMs > SLEEP_TIMEOUT_S * 1000UL) {
      esp_deep_sleep_start();                        // 開關已在 GPIO2 掛好喚醒源
    }
    delay(60);
    return;
  }
  lastValidMs = millis();

  /* --- 4. 音名/弦號/cents,顯示側再做一次中位平滑 --- */
  static float cHist[5]; static uint8_t cIdx = 0;
  uint8_t midi; float cents;
  noteFor(f, midi, cents);
  cHist[cIdx = (cIdx + 1) % 5] = cents;
  float sortBuf[5]; memcpy(sortBuf, cHist, sizeof(cHist));
  for (int i = 0; i < 4; i++)                        // 氣泡排序取中位:指針不抖
    for (int j = 0; j < 4 - i; j++)
      if (sortBuf[j] > sortBuf[j + 1]) { float t = sortBuf[j]; sortBuf[j] = sortBuf[j + 1]; sortBuf[j + 1] = t; }
  cents = sortBuf[2];

  uint8_t strIdx = 9;
  for (uint8_t i = 0; i < 6; i++)
    if (STRING_MIDI[i] == midi) { strIdx = i; break; }

  char noteStr[8];
  snprintf(noteStr, sizeof(noteStr), "%s%d", NOTE_NAMES[midi % 12], midi / 12 - 1);
  uiDraw(noteStr, strIdx, cents, true);
  Serial.printf("%s %.2fHz %+5.1fc %s\n", noteStr, f, cents, strIdx < 6 ? STRING_LABEL[strIdx] : "--");
  delay(80);                                          // ~10 更新/秒:調音器要「反應快」不要 60fps
}

🏗️ 組裝步驟

📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
  1. 列印與試夾:夾臂一体列印完成後徒手掰合 10 次去應力;先空夾一本書(厚度 45mm)試夾力,手感應是「需用力但能單手開合」
  2. 做感測杯:壓電片以瞬間膠點中心點在杯底,周邊走漆包線;三條懸吊筋最後才剪到 1.5mm——筋越細隔振越好但別剪斷
  3. 前端電路:1M/100k/100nF 直接搭焊在板子背面(三顆元件不需要洞洞板),萬用表確認 GPIO0 靜態 0.25~0.3V
  4. 鎖固與理線:板子兩顆 M2 銅柱鎖上蓋內側;所有線留 20% 長度餘量後熱膠點固定——夾式機殼內任何沒點膠的東西都會變成「沙鈴」
  5. 校驗:燒錄後夾到吉他上撥 6 弦,序列埠應出 E2 ~82Hz;對照手機調音 App 兩邊偏差 < ±5 cents 即合格

🔧 問題排除

問題原因解決方法
完全沒反應(指針不動)靜態工作點飄出 ADC 線性區 / 引線焊反到高通電容後未接穩萬用表量 GPIO0 靜態應 0.25~0.35V;不在範圍先查 1M/100k 有沒搭錯
輕輕碰琴就觸發門檻太低 / 漆包線太硬把殼體震動全傳給陶瓷片TRIG_MAG_MIN 調高到 1500;引線改細軟漆包線加應力圈
6 弦常讀成高八度基頻 bin 能量被 2 次泛音壓過(面板共鸣補救失敗)本韌體已有泛音補救仍翻車 → 琴頭夾太鬆,壓電耦合不足時高頻泛音佔優;夾緊重測
指針左右抖 ±10c取樣時基被中斷打亂 / 顯示未平滑確認用的是 micros 排程版擷取;cHist 中位數窗口 5→7
電池兩週就沒電深睡未生效(開關接錯腳,MCU 一直跑)序列埠印 sleep 訊息確認進睡;靜態電流應 < 20µA,超過就是 GPIO2 接法不對
低溫後音準整体偏移ADC 基準温漂(冬天户外團練)影響 < 1 cent 可忽略;若堅持,開機先丟 10 幀樣本暖機再進主迴路

🎯 進階延伸

📦 檔案清單

clip-tuner/
├── src/
│   └── clip-tuner.ino         # ESP32-C3 程式碼(本頁完整程式)
├── stl/
│   ├── clip-body.stl          # 琴夾主體含夾臂(PETG)
│   ├── jaw-pad.stl            # 夾臂墊塊(TPU)
│   ├── sense-cup.stl          # 壓電感測杯(PLA)
│   ├── top-cover.stl          # 上蓋含 OLED 窗(PETG)
│   └── switch-cap.stl         # 開關帽(TPU)
├── f3d/
│   └── clip-tuner.f3d         # Fusion 360 原始檔
├── manual/
│   └── assembly.html          # 圖文組裝說明書
└── README.md                  # 說明文件
← 上一篇:ESP32-C2 桌面智能加濕器 📚 系列索引 下一篇:ESP32-S3 浴室潮氣哨兵 🔜