
為什麼要做一個水下通訊設備
水下無線通訊第一個反直覺的地方是:電磁波在水裡幾乎不能用。海水對 RF 的衰減極大,低頻雖然能傳得遠一點,但代價是天線尺寸與發射功率都要放大到不合理的程度。所以把手機丟進水裡,它不是「訊號變弱」,而是直接失去意義。
能在水裡傳遞的唯一實用載體是聲波。這也是為什麼聲納存在了一百年,而水下數據機至今仍是活躍的研究題目。
決定做這個題目之後,很快就發現一件事:水下聲通道不是「比較吵的無線通道」,而是一條性質完全不同的通道。下面這張表列出的四道關卡,決定了後面所有的設計決定。

其中最容易被低估的是傳播速度。聲音在水裡約 1500 m/s,只比客機慢一點,但這是空氣的 4.5 倍 —— 意味著任何相對運動都會產生明顯的都卜勒偏移,而且多徑回波的到達時間差也跟著放大。這條通道對「低速而穩定」的傳輸方式特別友善,對「快而花俏」的方式特別殘忍。
三個關鍵取捨
1. 為什麼是 40 kHz
頻率選低一點,水中吸收損失就小一點,這是課本上的第一句話。但專案不是照課本做的 —— 真正決定的是「手上能不能買到便宜又穩定的換能器」。

40 kHz 剛好是車用倒車雷達與測距模組大量使用的規格,所以換能器又便宜又到處都有;而 40 kHz 的方波用一顆 NE555 就能穩定產生。這兩件事的權重,在這個專案裡遠高於「理論上最低衰減」。如果目標改成拉長距離,那就該降到 25 kHz 以下 —— 但那是另一個取捨了。
2. 為什麼用 8051
選 STC8H8K64U 有三個理由,其中第三個才是真正的動機。
第一,便宜而且好焊:SOP16 封裝、DIP 版本也有,不需要特殊製程或昂貴的開發板。第二,它是 1T 架構(一個時脈週期執行一條指令),在 11.0592 MHz 下處理本專案所需的即時時序已經綽綽有餘。
第三,也是最關鍵的:這個專案刻意不用任何現成的無線模組,而是從調變層自己做起。如果用一顆現成的藍牙或 LoRa 模組,「水下通訊」就只剩下接線與 API 呼叫;而自己從 OOK 調變、同步、除錯一路走完,才會真的碰到「通訊原理」這四個字的意思。
3. 目標規格
把範圍收斂成一句話:公尺級短距離的文字訊息交換。不是影像、不是高速資料流,而是「A 裝置打一行字,B 裝置收得到」這種最基本的通訊需求。後面的所有設計,都是為了讓這件事在一個會咬人的通道上穩定發生。
系統架構總覽
整個系統可以拆成三塊:發射鏈路、接收鏈路,以及夾在中間的自適應閾值控制。

這裡有個貫穿全案的設計哲學:把所有難的、快的、類比的事情都交給硬體,讓 MCU 只讀一個數位腳。接收端從放大、整流、包絡檢波到比較,全部是類比電路;MCU 拿到的已經是一個乾淨的「有載波/沒載波」。發射端更乾脆 —— 40 kHz 由 NE555 產生,MCU 只負責切換開關。
另一個必須先講的限制是半雙工:發射時自己的訊號會讓接收鏈路飽和,所以發射期間必須暫停接收。這不是偷懶,而是這組換能器與前端架構下的物理現實。
發射鏈路:把 40 kHz 交給硬體
發射端的電路簡單到有點不好意思:MCU 一支腳接到 NE555 的 RST,NE555 產生 40 kHz 方波去推換能器。

就這樣?對,OOK 調變就是這麼回事 —— 用「有沒有載波」來表示 1 與 0。要送 bit 1 就把 NE555 放開讓它振盪,要送 bit 0 就把它停住。調變這件事被簡化成了「切開關」,而這正是它聰明的地方:
- 高頻完全由硬體產生,MCU 不需要用軟體去 toggle 出 40 kHz(在 11 MHz 的 8051 上這根本不可能)
- 不佔 CPU:切完腳就可以去做別的事
- 沒有軟體抖動:方波頻率只取決於 RC 與晶片,不受中斷延遲影響
對應的程式碼只有幾行,先看接腳定義:
/* ==================== 接腳定義 ==================== */
#define PIN_US_RX P10 /* 超聲波接收: 比較器輸出 (輸入) */
#define PIN_US_TX P11 /* 超聲波發射: NE555 RST (輸出) */
#define PIN_RS485_DE P32 /* MAX3485 方向控制 (輸出) */
#define PIN_MCP_CS P33 /* MCP41010 CS# (輸出, 低有效) */
#define PIN_MCP_SI P34 /* MCP41010 SI (輸出) */
#define PIN_MCP_SCK P35 /* MCP41010 SCK (輸出) */
換句話說,MCU 在發射端的角色是「節拍器」而不是「訊號產生器」。這個分工讓後面所有的事情都變簡單 —— 包括接收端。
接收鏈路:把類比留給類比
接收端是整個硬體設計裡最費心的地方。訊號從換能器進來之後,要經過四道處理才會變成 MCU 看得懂的東西。

逐段說明:
- LM386 放大:換能器收到的訊號只有毫伏等級,用 LM386 的 GAIN 腳接 10 µF 電容把它拉到約 200 倍
- 整流與包絡檢波:1N5819WS 整流之後,用 10 nF/22 kΩ 做包絡檢波,時間常數約 0.22 ms。這一步把 40 kHz 的載波「抹平」成它的振幅包絡
- LM393 比較器:把包絡與參考電壓比較,輸出乾淨的數位訊號
- P1.0:MCU 讀這個腳,1 代表有載波、0 代表沒有。開集極輸出需要外部 4.7 kΩ 上拉到 5 V
包絡檢波是這裡的關鍵。它把「一個 40 kHz 的類比訊號」轉換成「一個振幅大小的問題」,而振幅只要跟門檻比一次就變成 0 與 1。時間常數 0.22 ms 遠小於 16 ms 的位元寬,所以位元邊界不會被糊掉。
整個接收鏈路的設定,在程式的檔頭註解裡寫得很清楚:
* 接收鏈路 (訊號流):
* 超聲波換能器 -> LM386 放大 -> 1N5819WS 整流 -> 10nF/22K 包絡檢波
* -> LM393 比較器 (1IN+ = 包絡, 1IN- = MCP41010 PW0 閾值)
* -> 1OUT -> P1.0 (1 = 有超過閾值的載波, 0 = 無)
*
* MCP41010 的角色: 提供比較器參考電壓 (偵測門限 threshold)。
* wiper 越大 -> PW0 電壓越高 -> 閾值越高 -> 越不容易觸發 (抗雜訊)。
* 自動調整 = 自適應閾值控制 (adaptive threshold control), 效果等同 AGC。
自適應閾值:用數位電位器對抗不穩定的振幅
前面的比較器需要一個參考電壓 —— 也就是「多大聲才算是有載波」。這個門檻是所有設計裡最麻煩的一個數字,因為它同時要滿足兩個相反的需求:
- 門檻太高:雜訊不會誤觸發,但微弱的訊號會整段漏掉
- 門檻太低:訊號都收得到,但環境雜訊會讓接收端瘋狂誤判
更麻煩的是,這個「最佳門檻」會變。空氣與水的聲阻抗匹配天差地遠 —— 在水中匹配良好、在空氣中嚴重失配,同一套硬體換了介質,收到的訊號強度可能差一個數量級。加上水溫、距離、氣泡都會影響振幅,一個固定的門檻根本不可能通用。
解法是用一顆數位電位器 MCP41010 來當可程式化的參考電壓源。它的 PW0 輸出電壓是 VCC × wiper/256,接到 LM393 的負輸入端。wiper 越大、門檻越高、越不容易觸發。也就是說,只要改變一個數值,就能改變整條接收鏈路的靈敏度。
開機時系統會自己找出這個數值:

校準的邏輯是「掃描 → 找到雜訊臨界 → 往下退一段餘量 → 寫進 EEPROM」。這段程式只有幾行,但其中的一個符號決定了專案能不能動:
/* ==================== MCP41010 (自適應閾值) ==================== */
#define MCP_WIPER_INIT 128 /* 開機初始值 (0~255) */
#define MCP_WIPER_MIN 8 /* 校準掃描下限 */
#define MCP_WIPER_MAX 248 /* 校準掃描上限 */
/* 校準鎖定 = 臨界 - MARGIN (往下退 = 更靈敏, 訊號餘量)。
* 為什麼是減不是加: 超聲波信封檢波對「短 burst」(14ms 1-bit) 的峰值
* 比長 preamble 低, 閾值太接近峰值時短位元會整個偵測不到 (LEN=0x00)。
* 往下退 16 格讓短 burst 也有充足餘量; 雜訊誤觸發由 preamble
* 44 連續載波視窗門檻擋掉 (隨機雜訊湊不滿 44 個連續視窗)。 */
#define MCP_WIPER_MARGIN 16 /* 校準後往下退的訊號餘量 */
而設定 wiper 的動作是靠軟體模擬的 SPI 完成的(這顆晶片用 bit-bang 比硬體 SPI 更單純):
static void mcp_spi_byte(uint8_t v)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
PIN_MCP_SCK = 0;
PIN_MCP_SI = (v & 0x80) ? 1 : 0; /* SI 先就緒 */
v <<= 1;
PIN_MCP_SCK = 1; /* 上升緣: MCP41010 取樣 */
}
PIN_MCP_SCK = 0;
}
void mcp41010_set_wiper(uint8_t v)
{
PIN_MCP_CS = 0; /* 片選 (低有效) */
mcp_spi_byte(0x11); /* 指令: 寫入 wiper (volatile) */
mcp_spi_byte(v); /* 資料: wiper 值 */
PIN_MCP_CS = 1; /* 完成一次寫入 */
g_wiper = v;
}
/* 寫入 wiper 並存入內部 EEPROM (指令 0x30) — 斷電後自動恢復此值。
* 校準/手動設定用, 下次開機不需重新校準。 */
這裡有個實測打臉理論的教訓值得單獨講。最早的版本是「臨界 + 16」—— 找到雜訊不會觸發的門檻後,往上再加一點餘量當作安全邊際,聽起來很合理。結果是:短訊息可以收,但長訊息完全收不到,LEN 永遠被解成 0x00。
追下去才發現,包絡檢波對「短 burst」(單一 1-bit 的載波,約 16 ms)所產生的峰值,低於長 preamble 的峰值。門檻一旦鎖得太高,短位元就會整個被吃掉 —— 而長訊息裡充滿了各種長短不一的位元組合。改成「臨界 - 16」(往下退,保留訊號餘量)之後,所有訊息都通了;至於雜訊誤觸發,改由後面的同步機制去擋。
這是典型「理論推導正確、實測打臉」的案例,也是這個專案裡第一個真正學到東西的地方。
接腳分配與資源盤點

晶片內部的資源分配也值得記一筆,因為它後來變成了一個架構警訊:
- Timer 0:250 µs 週期中斷,同時跑接收取樣狀態機與發送狀態機
- Timer 1:拿來做 delay_ms() 的軟體延時
- Timer 2:UART1 的鮑率產生器(115200 @ 11.0592 MHz,重載值剛好整除)
- IRAM:幾乎全滿,最大連續空閒只剩 3 bytes;XRAM 只用了 319/8192 bytes
- Flash:約 11.3 KB/64 KB
IRAM 只剩 3 bytes 這件事,在下一篇會變成真正的問題 —— 因為後面加的每一個功能,都得先想辦法把某個變數搬到 XRAM 去。這不是「最佳化」,而是「還能不能繼續加功能」的差別。
小結:硬體做完了,然後呢
到這裡,硬體的部分已經成形:發射端用一顆 NE555 產生載波,接收端用類比鏈路把聲音變成 0 與 1,中間再用一顆數位電位器讓門檻可以自我調整。MCU 只負責切腳與讀腳。
但真正的難題才要開始。這條鏈路每 16 ms 只能傳一個 bit,而「我收到的是 0x00 還是 0xE6」完全取決於軟體怎麼判斷位元邊界。下一篇會講軟體:框架怎麼設計、為什麼最後放棄了滑動視窗、以及三個「直覺做法反而是錯的」的教訓。
(下集:USLink 水下通訊模組(中)— OOK 軟體設計與除錯歷程)
文章評論