📋 專案概述
上周五團練,主唱在台上調音調了四分鐘。他腳邊那台 599 元的踏板調音器是「麥克風式」的——底下鼓手試大鼓、前面吉他手隨手撥兩下,螢幕上的指針就跟著全團搖,永遠定不下來。最後他掏出手機開 App,把麥克風對著琴頭調——團長當場臉黑:演出前的手忙腳亂,一半是從調音開始的。
問題的本質很簡單:用耳朵聽的調音器,在噪音環境裡就是亂指。真正的夾式調音器夾在琴頭上讀「振動」,外界多吵都與它無關。但市售品查下來:
- 兩三百元的「夾式」多半是偽裝:規格表寫「夾式/麥克風雙模式」,實際是同一顆麥克風加了個觸發器,現場照樣失效。
- 真的壓電感測的入門款 700+ 元起跳,螢幕還是一格 1 cent 都看不出來的粗指針;同一顆陶瓷感測片,原料成本不到 20 元。
- 手機 App 是最後的底牌不是常態:要解鎖、要開 App、佔記憶體、來一通電話前功盡棄——團練一週三次,你不該依賴一個要解鎖的工具。
這個專案用 ESP32-C3 SuperMini(比金士戒還小)+ 一颗 15 元壓電陶瓷片,做一台夾上去就動的調音器:
- 夾上即調:沒有開機步驟——夾緊、撥弦、0.4 秒出音名與偏差,靜置 2 分鐘自動睡
- 測振不測音:壓電片直接讀琴頭振動,團練現場鼓聲再大都不受影響
- ±1 cent 級解析:8kHz 取樣 + 256 點定點 FFT + 拋物線內插,E2 低音弦也能穩穩咬住
- 弦號直覺顯示:6E/5A/4D/3G/2B/1e + 音名 + 指針 + cents 數字,一眼完成
- 兩顆 AAA 電池供電,34 克,比任何市售款都輕
成本帳:入門壓電調音器 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 SuperMini | 1 | ~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×30mm | 1 | ~15 元 | 夾琴頭不留夾痕是底線 |
| 雜項 | M2 自攻螺絲、0.5mm 漆包線、尼龍束帶 | 1 | ~18 元 | 壓電片接線用漆包線(軟,不傳機械震動) |
| 合計 | ~411 元 |
🔌 接線圖
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 / GPIO9 | OLED SDA / SCL | C3 默認 I2C 腳,免指定引腳號 |
| 3V3 / GND | 電池直接供電(開關走 GPIO2 軟體電源) | AAA 鹼性 3.0→1.8V 衰降,C3 3V3 腳允許範圍 3.0~3.6V:低於 2.7V 韌體強制深睡護電池 |
⚠️ 壓電片兩面引線務必用軟漆包線,且走線留 2cm 應力迴圈後點膠固定。硬銅線等於把琴頭的振動直接「焊」進陶瓷片——調音器會愛上你自己手指的動静,稍碰琴頭就觸發。
🖨️ 3D 列印零件
零件清單
| 零件 | 數量 | 材質建議 | 顏色 | 列印時間 |
|---|---|---|---|---|
| 琴夾主體(含彈簧夾臂) | 1 | PETG(韌性,夾臂反復掰不斷) | 黑色 | ~1.5 hr |
| 夾臂墊塊 | 2 | TPU 95A | 灰 | ~15 min |
| 壓電感測杯 | 1 | PLA | 黑 | ~25 min |
| 上蓋(OLED 窗) | 1 | PETG | 黑 | ~40 min |
| 開關帽 | 1 | TPU | 橘 | ~8 min |
列印設定
- 材質:夾臂是運動件——PETG 層間剪切強度遠勝 PLA,PLA 夾臂半年後從印層起裂是標配劇情
- 夾臂填滿 45%、其餘 15%;夾臂與本體一体列印(45° 傾斜支撑),不後裝鉸鏈
- 層厚:本體 0.2、TPU 墊 0.24(軟料放慢 25mm/s)
- 夾面曲率:夾墊內弧 R28mm 貼合吉他琴頭側板曲率,接觸面積大 = 震動傳遞好 + 不留痕
設計要點
- 感測杯「三點球接」:壓電片膠在杯底中心,杯殼則由三條細筋懸吊固定在本體——機械震動走「杯底→陶瓷片」主路徑,殼體的雜訊被細筋隔開。這是本機信噪比比「陶瓷片直接貼殼」方案高一倍的關鍵
- 夾力 300g 設計:夾臂厚度 3.2mm + 根部 1.5mm 圓角,夾持力剛好壓實矽膠墊又不變形;夾口開度 38~56mm 通吃民謠/古典琴頭
- OLED 上傾 30°:夾上琴頭後正對演奏者眼睛,不用低頭看琴頭底
- 電池艙側抽:不拆殼換電池;艙門卡扣 + 一片防誤開內鎖
💻 程式碼
📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
📚 相依:板子選「ESP32C3 Dev Module」,PSRAM 關、CDC On Boot 開;Lib Manager 裝
📚 相依:板子選「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 組裝說明書
- 列印與試夾:夾臂一体列印完成後徒手掰合 10 次去應力;先空夾一本書(厚度 45mm)試夾力,手感應是「需用力但能單手開合」
- 做感測杯:壓電片以瞬間膠點中心點在杯底,周邊走漆包線;三條懸吊筋最後才剪到 1.5mm——筋越細隔振越好但別剪斷
- 前端電路:1M/100k/100nF 直接搭焊在板子背面(三顆元件不需要洞洞板),萬用表確認 GPIO0 靜態 0.25~0.3V
- 鎖固與理線:板子兩顆 M2 銅柱鎖上蓋內側;所有線留 20% 長度餘量後熱膠點固定——夾式機殼內任何沒點膠的東西都會變成「沙鈴」
- 校驗:燒錄後夾到吉他上撥 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 幀樣本暖機再進主迴路 |
🎯 進階延伸
- 降半音/全音模式:設定區加
TRANSPOSE變數,弦號比對表整體平移——金屬團常玩 Drop D / Eb 標準調 - 指針平滑換曲線:cents 映射改指数濾波 + 死區 2c,指針接近中心時明顯「吸住」,手感立刻像千元級貨
- Ukulele / 贝斯模式:STRING_MIDI 表換一組(u 琴 G4 C4 E4 A4)再加個切換按鈕,一台變三台
- BLE 调音日志:C3 原生 BLE,把手機 App 換成自己寫的 nRF Connect 面板——順便把每首歌的音準歷史存下来看進步曲線
📦 檔案清單
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 # 說明文件