
為什麼要做一個水下通訊設備
以前跑船的時候,我常常在當值時盯著Echo Sounder看他的反射波。想著或許能從中找到大魚的蹤跡,滿足作為釣魚愛好者的心。大學的課上有學到Echo Sounder的原理是超聲波回波測深。我就在想,超聲波除了這個功能以外,能不能做些其他的事情。後來上網查到超聲波還可以看胎兒發育之類的,更有甚者,還可以通訊。
於是,一個念頭在腦中萌芽:不如來做一個超聲波通訊模組吧!
水下無線通訊第一個反直覺的地方是:電磁波在水裡幾乎不能用。海水對 RF 的衰減極大,低頻雖然能傳得遠一點,但代價是天線尺寸與發射功率都要放大到不合理的程度。所以把手機丟進水裡,它不是「訊號變弱」,而是直接失去作用。
能在水裡傳遞的唯一實用載體是聲波。這也是為什麼聲納存在了一百年,而水下數據機至今仍是活躍的研究題目。
決定做這個題目之後,很快就發現一件事:水下聲通道不是「比較吵的無線通道」,而是一條性質完全不同的通道。下面這張表列出的四道關卡,決定了後面所有的設計決定。

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

而 40 kHz 剛好是車用倒車雷達與測距模組大量使用的規格(例如常見的HC-SR04模組),所以換能器又便宜又到處都有;而 40 kHz 的方波用一顆 NE555 就能穩定產生。
(我參考 HC-SR04 上面是這麼做的,所以不用PWM而是用NE555無穩態產生40kHz方波驅動超聲波換能器)
2. 為什麼用 8051
選 STC8H8K64U SOP16 有幾個理由。
第一,我桌上剛好有一條不知道啥時候買的還沒用過的芯片。第二,他便宜又好焊,腳位很寬,保護眼睛。第三,他的性能在八位元的單片機裡算很不錯了。
3. 目標規格
短距離的文字訊息交換。
也就是「A 裝置打一行字,B 裝置收得到」這種最基本的通訊設備。
系統架構總覽
整個系統可以拆成三塊:發射鏈路、接收鏈路,以及夾在中間的自適應閾值控制。

我把所有類比的事情都交給硬體,讓 MCU 只讀一個腳,減輕負擔。(效能都用在讓蜂鳴器播放音樂了)接收端從放大、整流、包絡檢波到比較,全部是類比電路;MCU 拿到的只是一個OOK。而發射端只負責切換開關,40 kHz 由 NE555 產生。
另一個限制是半雙工:發射時自己的訊號會讓接收鏈路飽和,所以發射期間必須暫停接收。
發射鏈路
發射端的電路很簡單,完全按照HC-SR04上面去設計:MCU 一支腳接到 NE555 的 RST,NE555 產生 40 kHz 方波去推換能器。

對,OOK 調變就是這麼回事 —— 用「有沒有載波」來表示 1 與 0。要送 bit 1 就把 NE555 放開讓它振盪,要送 bit 0 就把它停住。調變這件事被簡化成了「開關開關」:
- 高頻由硬體產生,MCU 不需要用軟體去 toggle 出 40 kHz
- 不佔 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 看得懂的東西。

說明:
- 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 完成的(不用硬體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」完全取決於軟體怎麼判斷位元邊界。所以,下一篇會講我的軟體框架是怎麼設計的,然後我面臨了哪些問題。
雖說中途有查到水下通訊根本就不該用OOK,而是應該用ASK、FSK之類的調頻。但基於成本考量最後還是一條路走到黑,算了。網路上很難買40kHz以外的換能器。我算好不容易才找到這顆還算有防水的。
文章評論