實戰(zhàn):BLE鏈路、GATT協(xié)議與波形繪制全解析)
簡介一份面向電子愛好者、嵌入式開發(fā)者與移動測量應(yīng)用學(xué)習(xí)者的安卓藍牙示波器資料包將手機轉(zhuǎn)化為便攜式信號檢測與分析工具無需傳統(tǒng)臺式示波器即可在電路調(diào)試、傳感器信號觀察、課堂演示等場景中查看實時波形。壓縮包約115KB共8個文件包含1個可直接安裝的APK、5個Java源碼文件、1張硬件原理圖JPG及1個dsPIC固件RAR包覆蓋信號采集、藍牙傳輸?shù)桨沧慷瞬ㄐ物@示的完整鏈路。電路圖清晰展示信號采集與ADC模數(shù)轉(zhuǎn)換部分Java源碼便于學(xué)習(xí)Android藍牙API調(diào)用、藍牙Socket通信和波形繪制邏輯dsPIC固件則為下位機采集程序提供參考利用這套資料可自行修改硬件設(shè)計提升ADC分辨率或增加存儲、回放、參數(shù)設(shè)置等高級功能。已有2383人學(xué)習(xí)下載適用于需要低成本移動測量方案或入門藍牙示波器開發(fā)的用戶也可作為電子類課程設(shè)計與畢業(yè)設(shè)計的參考資料。1. 打開 App 卻不出波形藍牙示波器的帶寬悖論把手機放到口袋、點開 App屏幕上應(yīng)該出現(xiàn)一條規(guī)整的正弦波。可很多人的第一次藍牙示波器體驗是波形成了一片馬賽克或者卡在某個位置一動不動。問題不在 App 寫得爛而在于你讓一條藍牙鏈路干了它原本干不了的活示波器本質(zhì)是數(shù)據(jù)搬運工它要在幾百毫秒內(nèi)把幾萬個采樣點搬上屏幕而經(jīng)典藍牙串口或 BLE 通知在理想條件下的有效凈荷也只有每秒幾十 KB。一個 20KB/s 左右的信道和動輒幾百 kSa/s 的采集率差了整整一個數(shù)量級。這就是安卓藍牙示波器最底層的悖論采集端多快都能采瓶頸永遠在無線鏈路上。搞懂這套機制不是滿足好奇心。你調(diào)采樣率、改包長度、重畫波形時遇到的所有玄學(xué)問題——為什么低時基下波形稀疏、為什么切到雙通道就丟包、為什么長時間運行后 App 卡死——根源全在這條鏈路上。本文從數(shù)據(jù)怎么從 ADC 進入手機內(nèi)存講起拆到安卓端 GATT 回調(diào)里的字節(jié)流處理最后落到協(xié)議設(shè)計和硬件選型的取舍。理解了這些你和這份資源里的代碼就不再是能跑和看不懂的關(guān)系。2. BLE 鏈路從 ADC 采樣到手機字節(jié)流中間發(fā)生了什么2.1 GATT 模型才是藍牙示波器真正的傳輸層藍牙示波器的硬件端通常是一個 MCU 內(nèi)置 ADC 定時采樣數(shù)據(jù)通過串口發(fā)給藍牙模塊模塊再以無線方式傳給手機。聽起來像一根無線的串口線但工程上和真實的串口天差地別。藍牙模塊在手機側(cè)暴露的是一個 GATTGeneric Attribute Profile服務(wù)它把事情拆成了三層Service服務(wù)、Characteristic特征、Descriptor描述符。你的波形數(shù)據(jù)不是流進來的而是手機主動注冊監(jiān)聽某個 Characteristic 的通知Notification設(shè)備端把數(shù)據(jù)切成一包一包推給手機。這里最關(guān)鍵的一個數(shù)字是 MTU最大傳輸單元。Android 上默認的 MTU 是 23 字節(jié)減去 3 字節(jié)的 Attribute 協(xié)議頭一次通知最多攜帶 20 字節(jié)凈荷。如果你的硬件用了某款經(jīng)典藍牙透傳模塊它內(nèi)部會把 UART 流切成 L2CAP 數(shù)據(jù)包依然受這個限制。20 字節(jié)意味著采樣率 100kSa/s、每個樣本 2 字節(jié)12 位 ADC 對齊到 16 位一秒鐘產(chǎn)生 200KB 數(shù)據(jù)BLE 在 1Mbps 物理速率下實測有效吞吐量樂觀估計在 3050KB/s200KB 顯然是塞不進去的于是硬件端必須降采樣率或者軟件端丟點。你看到的實時波形背后是一個被迫砍掉 90% 數(shù)據(jù)的傳輸策略。2.2 硬件原理圖里的信號路徑典型的硬件方案是信號經(jīng)過運放調(diào)理分壓、跟隨、抬升到 ADC 量程→ MCU 內(nèi)置 ADC 由定時器觸發(fā)采樣 → DMA 把結(jié)果搬到內(nèi)存緩沖區(qū) → 固件按幀打包 → UART TX 發(fā)送到藍牙模塊。原理圖上你會看到三個關(guān)鍵器件運放、MCU、藍牙模塊它們之間只有幾根走線但每根走線都決定了波形質(zhì)量。運放的輸出阻抗和 ADC 采樣電容直接決定了信號建立時間。如果運放到 ADC 引腳之間串了太大的電阻采樣瞬間電壓建立不完全高頻率信號就會幅度衰減俗稱波形糊了。原理圖里如果有 1kΩ 以上的串阻且沒有加采樣保持電容這是第一處要檢查的地方。另一個隱蔽點是 ADC 參考電壓如果用 MCU 內(nèi)置參考且選擇了 3.3V 供電電池電壓波動會直接讓讀數(shù)漂移所以合格的設(shè)計會單獨留一個精密參考源或至少用 LDO 穩(wěn)壓后給 MCU 供電。2.3 為什么說透傳是一個黑匣子很多藍牙透傳模塊對外宣稱串口透傳好像 MCU 寫串口、手機就能收到流。實際工作方式是MCU 的 UART TX 進入模塊模塊內(nèi)部固件將字節(jié)流切包、加 L2CAP 頭、經(jīng)射頻發(fā)出手機端 BLE 協(xié)議棧把這些包重組后交給 App。問題在于重組并不完美模塊廠商會設(shè)置一個打包間隔常見 515ms到點就把緩沖區(qū)的數(shù)據(jù)發(fā)出去沒到點就一直攢著。這意味著你在 UART 側(cè)每次發(fā) 20 字節(jié)到了手機側(cè)可能三包并成一包到達也可能一包被拆成兩包。App 側(cè)根本不能假設(shè)一次通知 一個完整波形幀。這就是后面數(shù)據(jù)幀協(xié)議存在的前提——接收端拿到了是字節(jié)流而不是消息流。從源碼調(diào)試的角度看你可以在模塊的數(shù)據(jù)手冊里找到 PDU 長度和連接間隔Connection Interval參數(shù)。連接間隔 7.5ms30ms它決定了底層多久和主機同步一次間接影響可達吞吐量。同一款模塊廠商讓用戶把連接間隔從 30ms 改成 7.5ms吞吐量能翻倍。資源里的固件工程一般在初始化代碼里有一個連接參數(shù)更新請求修改minInterval/maxInterval就能看到實際速度變化建議你實際動手改一改并抓包對比比讀十遍文檔都管用。3. 數(shù)據(jù)幀協(xié)議怎么在亂序字節(jié)流里切出一整屏波形3.1 為什么不能直接讀一個字節(jié)畫一個點如果 App 每收到 20 字節(jié)就往屏幕上畫兩個點理論延遲最小但工程上完全不可行。原因有三第一字節(jié)流沒有邊界。你不知道哪兩個字節(jié)組成一個樣本如果 MCU 端是 12 位數(shù)據(jù)右對齊成 16 位高 4 位補 0那么每個樣本的低字節(jié)可能一直是 0xF0但高字節(jié)可能是任意值。首字節(jié)錯位會導(dǎo)致整幀數(shù)據(jù)全亂畫出來的波形是疊加了偽隨機噪聲的毛刺。第二無線鏈路會重傳。BLE 在鏈路層有 CRC 校驗和重傳機制但重傳導(dǎo)致的延遲抖動會讓包到達時間完全不可預(yù)測如果按包到達時間推算樣本的時間間隔你得到的時基就是一個不停抖動的擺鐘。第三你不知道 MCU 端此刻處于什么狀態(tài)——是剛開始采樣還是已經(jīng)采了一半還是剛從低功耗模式喚醒。區(qū)分這些信息的唯一手段是在數(shù)據(jù)里顯式聲明邊界和狀態(tài)。3.2 幀協(xié)議的經(jīng)典結(jié)構(gòu)這份資源里的固件和 App 配合使用一套自定義幀格式常見設(shè)計如下包頭(0xAA 0x55) | 幀長度(1B) | 通道數(shù)(1B) | 采樣率編號(1B) | 樣本數(shù)(2B) | 樣本... | 校驗和(1B)對應(yīng)的 C 固件打包偽代碼void pack_and_send_frame(uint16_t* adc_buf, uint32_t sample_count, uint8_t ch_mask) { uint8_t frame[128]; uint8_t idx 0; frame[idx] 0xAA; frame[idx] 0x55; frame[idx] (uint8_t)(2 sample_count * 2 1); // 幀長度 frame[idx] ch_mask; frame[idx] g_sample_rate_code; frame[idx] sample_count 8; frame[idx] sample_count 0xFF; uint8_t sum 0; for (int i 0; i sample_count; i) { frame[idx] (uint8_t)(adc_buf[i] 0xFF); frame[idx] (uint8_t)(adc_buf[i] 8); } for (int i 0; i idx; i) sum frame[i]; frame[idx] sum; uart_write(frame, idx); }注意幾個細節(jié)。包頭用0xAA 0x55不是隨便選的0xAA 是二進制101010100x55 是01010101在這段字節(jié)流里誤碼后重新同步的概率極低。幀長度字段讓你能區(qū)分這只是半包還是完整一幀傳輸被切分后App 可以先把收到的字節(jié)攢著等湊夠了幀長度再解析。采樣率編號不是直接用數(shù)值如 100000因為一個字節(jié)只能表達 0~255所以定義一張枚舉表01kSa/s, 110kSa/s, 250kSa/s...App 端查表還原真實采樣率。校驗和覆蓋從包頭到樣本最后一個字節(jié)能擋掉大部分射頻誤碼——BLE 的鏈路層 CRC 只保證單包傳輸正確不能保證你的數(shù)據(jù)幀跨包后依然正確。3.3 App 端的狀態(tài)機解析安卓端的解析邏輯核心是一個按字節(jié)轉(zhuǎn)移的有限狀態(tài)機。它不依賴 GATT 通知的包邊界而是把所有接收字節(jié)喂進一個環(huán)形緩沖區(qū)然后逐字節(jié)輪詢private void parseByte(byte b) { switch (state) { case IDLE: if ((b 0xFF) 0xAA) { state WAIT_HEADER2; } break; case WAIT_HEADER2: if ((b 0xFF) 0x55) { state WAIT_LENGTH; } else { state IDLE; } // 首字節(jié)對但次字節(jié)錯回退重找 break; case WAIT_LENGTH: expectedLen (b 0xFF) 2; // 長度字段代表剩余部分 state WAIT_CHANNEL; break; case WAIT_CHANNEL: channelMask b; state WAIT_DATA; break; // ... 繼續(xù)收樣本收滿后校驗 } }這個狀態(tài)機的關(guān)鍵在于失敗回退的設(shè)計任何一步出錯不是丟棄一幀就完事而是回到IDLE狀態(tài)重新尋找0xAA不做計數(shù)跳過。原因很實際無線丟包后字節(jié)流里可能出現(xiàn)半個舊幀 半個新幀的拼接只有找到完整包頭才算新幀開始才能避免錯位越積越深。你會在 App 源碼里看到同樣的邏輯只是用 Kotlin/Java 寫成了可讀性更高的狀態(tài)類。解析完成后把樣本數(shù)組交給顯示層同時用幀內(nèi)帶的采樣率編號回推每個樣本的實際時間這樣即使包到達間隔抖動畫出來的波形橫軸依然準(zhǔn)確。從資源的使用角度看如果你想把這套代碼移植到自己的板子上改動只需三處固件里的采樣率編號表、App 端FrameParser里的對應(yīng)關(guān)系、以及校驗算法是否一致。這三處都對不上最常見的結(jié)果是 App 能收到數(shù)據(jù)但一直提示幀校驗失敗波形永遠出不來。4. 安卓端實現(xiàn)GATT 回調(diào)、環(huán)形緩沖與波形繪制管線4.1 BLE 連接與通知注冊的關(guān)鍵步驟安卓端建立 BLE 鏈路有一套固定流程BluetoothLeScanner.startScan掃描設(shè)備找到后connectGatt連接然后在onServicesDiscovered回調(diào)里拿到服務(wù) UUID 和特征 UUID。示波器的設(shè)備端通常暴露一個服務(wù)下面兩個特征一個用于 App 下發(fā)命令Write一個用于設(shè)備上報數(shù)據(jù)Notify。關(guān)鍵代碼如下public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status ! BluetoothGatt.GATT_SUCCESS) return; BluetoothGattService svc gatt.getService(SERVICE_UUID); if (svc null) return; BluetoothGattCharacteristic notifyChar svc.getCharacteristic(NOTIFY_UUID); gatt.setCharacteristicNotification(notifyChar, true); BluetoothGattDescriptor desc notifyChar.getDescriptor( UUID.fromString(00002902-0000-1000-8000-00805f9b34fb)); if (desc ! null) { desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(desc); } BluetoothGattCharacteristic cmdChar svc.getCharacteristic(CMD_UUID); cmdChar.setValue(new byte[]{ (byte)rateCode, (byte)chMask }); gatt.writeCharacteristic(cmdChar); }setCharacteristicNotification只是本地注冊真正讓設(shè)備開始推送的是寫那個2902描述符里面一個字節(jié)的0x01告訴鏈路層客戶端已準(zhǔn)備好收通知。很多人漏掉這一步后果是設(shè)備端已經(jīng)在推數(shù)據(jù)手機卻完全不觸發(fā)回調(diào)。命令特征和通知特征分開是為了避免控制報文和數(shù)據(jù)報文搶占同一個通道。如果你在一個特征上又寫命令又收數(shù)據(jù)寫操作會打斷正在進行的通知流造成周期性丟幀。4.2 回調(diào)里只做拷貝解析放到子線程onCharacteristicChanged是在 Binder 線程上調(diào)用的頻率高的時候每秒可能被調(diào)用幾十次每次攜帶 20 字節(jié)或協(xié)商后更大的數(shù)據(jù)。如果在這個方法里直接做狀態(tài)機解析、數(shù)組拷貝、UI 刷新任何一個環(huán)節(jié)卡頓都會導(dǎo)致 BLE 棧內(nèi)部緩沖積壓。Android 的 BLE 棧對慢消費者沒有流控積壓到一定程度就靜默丟包——不是報錯是直接丟掉。這段資源的實現(xiàn)采用生產(chǎn)者-消費者模型private final ByteArrayRingBuffer ring new ByteArrayRingBuffer(64 * 1024); private final HandlerThread parserThread new HandlerThread(frame-parser); private final Handler parserHandler; public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic ch) { byte[] data ch.getValue(); synchronized (ring) { ring.write(data, 0, data.length); } parserHandler.post(this::parseFromRing); }環(huán)形緩沖區(qū)開 64KB足夠容納幾十幀未解析的數(shù)據(jù)。解析線程每次從 ring 里把所有可讀字節(jié)拉出來跑狀態(tài)機解出完整幀后通過Handler投遞到 UI 線程。注意其中沒有設(shè)置任何每收 20 字節(jié)解析一次的邏輯而是攢到多少解析多少這樣每幀數(shù)據(jù)的處理只經(jīng)過一次狀態(tài)機遍歷CPU 開銷被平攤丟幀率大幅下降。4.3 繪制管線波形不是逐點畫的手機屏幕從刷新到人眼可感知需要穩(wěn)定的幀率但 BLE 數(shù)據(jù)的到達間隔是不穩(wěn)定的。直接每收到一幀就 invalidate的后果是波形快慢不均。這里的做法是在 UI 層緩存最近 N 幀的樣本然后用一個自繪的WaveformView以顯示時鐘比如 30fps主動重繪。繪制核心在onDraw里對樣本做降采樣把 4 萬個樣本映射到屏幕寬度 1080 像素上每個像素列取該區(qū)間內(nèi)的最大值和最小值畫一條豎線而不是取平均值。這樣能保留尖峰信息否則窄脈沖會被平均掉看起來像被磨平了。這個min-max 降采樣策略是數(shù)字示波器顯示引擎的通用做法你在資源里的自定義 View 中能看到對應(yīng)代碼它直接決定你能不能看清一個 10μs 寬的毛刺。基準(zhǔn)線和電壓刻度則是從幀頭里的量程信息換算出來的App 顯示500mV/div時實際上是每格對應(yīng)多少 ADC 碼值。4.4 時基重建不能用包到達時間當(dāng)時間戳這是最容易翻車的地方。很多人畫波形時直接用 System.currentTimeMillis() 記錄每個樣本的到達時間畫出來的波在靜止時能看可一旦信號頻率稍高波形就會橫向抖動看起來像在呼吸。原因很簡單BLE 包的到達間隔在鏈路層重傳和調(diào)度的影響下可能波動數(shù)十毫秒把這個抖動當(dāng)作采樣時間等于給波形加了一個隨機時間噪聲。標(biāo)準(zhǔn)做法是完全信任幀頭里的采樣率編碼。收到一幀時知道起始樣本序號樣本 i 的真實時間為frameStartTick i / sampleRateframeStartTick由前一幀的樣序遞推而來。這樣時基是數(shù)學(xué)重建的與包到達時間無關(guān)。所以如果你的代碼里出現(xiàn)了根據(jù)接包時間計算橫軸的邏輯多半就是波形成像不穩(wěn)定的直接原因。5. 藍牙示波器避坑實錄采樣率虛標(biāo)、丟包和時基漂移5.1 波形毛刺密集像一堆頭發(fā)絲現(xiàn)象輸入一個干凈的正弦波屏幕上卻疊加了大量細碎跳變用手摸一下探頭線波形更亂。原因最常見的是硬件端 ADC 引腳浮空或參考地噪聲。另一個隱蔽原因是固件里沒有對 ADC 結(jié)果做軟件濾波以及運放輸出端到 ADC 引腳之間缺少 RC 濾波網(wǎng)絡(luò)。藍牙示波器為了省功耗MCU 的采樣時鐘可能直接用內(nèi)部 RC 振蕩器頻率誤差在 1%3% 左右觸發(fā)采集的定時器抖動直接轉(zhuǎn)換為波形上的相位噪聲。解決在原理圖上給 ADC 輸入端加一個 100Ω 串阻和 1nF 到地的電容構(gòu)成低通濾波器固件里對相鄰樣本做一階滑動平均如new old * 0.25 current * 0.75如果追求更高精度采集時改用外部晶振或校準(zhǔn)內(nèi)部 RC。這套方案在模擬信號鏈上比對波形的平滑處理要可靠得多。5.2 長時間運行后波形卡死App 無響應(yīng)現(xiàn)象開機正常兩三分鐘后波形開始斷斷續(xù)續(xù)最后完全不動關(guān)閉 App 重進又恢復(fù)正常。原因App 的消息積壓導(dǎo)致onCharacteristicChanged回調(diào)無法及時執(zhí)行。你的解析邏輯可能卡在同步鎖上或循環(huán)里做了大量對象分配導(dǎo)致 GC 頻繁觸發(fā)一旦回調(diào)堆積BLE 棧背壓丟棄通知但 App 端不知道還在等新數(shù)據(jù)。解決保持回調(diào)線程的職責(zé)單一——只拷貝字節(jié)到緩沖區(qū)并 post 一個任務(wù)。解析、檢驗、繪圖全部移出該線程。如果你已經(jīng)用HandlerThread做了解析就在環(huán)內(nèi)禁止一切日志輸出Log.d在頻繁調(diào)用時比解析更耗時。加一條自愈邏輯如果超過 500ms 沒收到任何通知主動重連 GATT。這條邏輯在資源里的連接管理類中有現(xiàn)成實現(xiàn)。5.3 采樣率設(shè)置成 100kSa/s實測只有 30kSa/s現(xiàn)象把采樣率檔位調(diào)到最高波形顯示卻依然粗糙橫軸稀疏感明顯。原因不是 App 騙人是鏈路吞吐量到頂了。BLE 有效載荷按 20 字節(jié)/包、連接間隔 15ms 計算理論 1Mbps 物理速率下每秒只有約 1.3KB/s 可用。100kSa/s × 2 字節(jié) 200KB/s差了快 200 倍。固件可能確實在 100kSa/s 采樣但發(fā)不出去于是只在緩沖區(qū)滿了之后把最新的數(shù)據(jù)發(fā)出來導(dǎo)致大量數(shù)據(jù)被丟棄。解決先用數(shù)據(jù)包計數(shù)驗證真實吞吐量——App 統(tǒng)計每秒收到的有效幀字節(jié)數(shù)顯示為實際采樣率。然后據(jù)此反推能穩(wěn)定跑的最高檔位。如果必須實時就降采樣率如果信號是周期的就讓固件在 MCU 本地緩存一段數(shù)據(jù)一次性發(fā)完App 離線繪制這段波形。這個分段傳輸思路是許多成品藍牙示波器的真正工作模式。5.4 電壓讀數(shù)整體偏小 10%現(xiàn)象輸入 1V 直流App 顯示 0.9V換一個檔位偏差比例不變。原因這是運放增益誤差與 ADC 參考電壓誤差疊加的結(jié)果。原理圖里運放的分壓電阻精度 5% 就能造成 5% 量級誤差MCU 內(nèi)置參考電壓本身有 ±2% 左右的誤差。藍牙示波器為了低成本通常省略精密參考所以誤差全部帶進了讀數(shù)。解決做兩點校準(zhǔn)。用已知精度的 0V 和一個基準(zhǔn)電壓比如 1.000V分別采樣算出增益和偏置把校準(zhǔn)系數(shù)燒進固件或存到 App 偏好里。資源中的 App 有校準(zhǔn)頁面寫算法就是real (raw - offset) * gain。這個系數(shù)位數(shù)必須用 float用整數(shù)除法會導(dǎo)致低量程時階梯狀誤差。5.5 低頻波形出現(xiàn)明顯臺階感現(xiàn)象輸入 50Hz 正弦波波形本來應(yīng)該平滑但屏幕上一段一段地跳像樓梯。原因屏幕刷新率 60HzBLE 推送頻率可能也只有 2030 幀/秒看起來就是一步一個臺階。不要把這個誤認為信號本身是階梯波。如果一個包里有 200 個樣本屏幕卻只在每次收到包時畫一次那么 50Hz 信號一個周期只畫了 5 個點視覺效果就是鋸齒。解決確認 App 是按幀內(nèi)樣本數(shù)補點即每個樣本都畫而不是按包畫。同時檢查WaveformView是否做了偶發(fā)式重繪改為按顯示時鐘固定 3060fps 重繪。如果 MCU 端的采樣率遠高于 BLE 能推送的刷新率那每個包的樣本數(shù)必須足夠多才能保證屏幕上每幀之間有足夠的點覆蓋完整波形周期。6. 設(shè)計取舍帶寬換體驗還是在協(xié)議層做補償藍牙示波器開發(fā)到最后繞不開一個二元選擇是讓硬件降低采樣率保證實時流暢還是讓 MCU 高速采集、分塊傳輸、App 離線繪制前者體驗連貫但看不到高頻瞬態(tài)后者能捕獲細節(jié)但犧牲了實時性。我一般會這么權(quán)衡如果待測信號是持續(xù)輸出且只關(guān)心宏觀趨勢比如電池放電曲線、電源上電時序那 1kSa/s 的實時流就足夠如果要抓一段 5ms 的 I2C 波形毛刺就必須讓 MCU 以 1MSa/s 采集幾百個點存下來一次性發(fā)走。這也是為什么這份資源里的固件同時保留了連續(xù)流模式和單次捕獲模式兩個命令——它們對應(yīng)的協(xié)議幀結(jié)構(gòu)不同App 端根據(jù)幀頭里的模式字段決定是實時追加還是整幀替換。第二個取舍在觸發(fā)機制上。硬件觸發(fā)需要在 MCU 端做比較器邏輯能精準(zhǔn)定位邊沿但固件復(fù)雜度上升App 端軟件觸發(fā)是收到數(shù)據(jù)后在樣本序列里查找跨閾值點實現(xiàn)簡單卻需要傳輸大量無關(guān)數(shù)據(jù)。資源里的固件選擇了 MCU 端做一個簡易上升沿檢測ADC 連續(xù)采樣時在中斷里比較前后值捕獲到邊沿才啟動 DMA 存儲。這樣做的代價是你丟掉了邊沿之前的預(yù)觸發(fā)數(shù)據(jù)要做好取舍就必須在內(nèi)存里開一段循環(huán)緩沖區(qū)讓 DMA 始終寫入最新數(shù)據(jù)觸發(fā)后停止寫保留觸發(fā)前的一段記錄。從那以后我每次調(diào)試藍牙示波器都不先調(diào)采樣率而是先拿一臺信號發(fā)生器輸出一個固定頻率的方波測量 App 端顯示的實際采樣率。算清楚有效帶寬再去決定用哪個檔位、要不要開觸發(fā)、數(shù)據(jù)分塊多大。這套先驗證鏈路后寫業(yè)務(wù)邏輯的習(xí)慣幫我省掉了大量無謂的抓包和改參數(shù)。希望幫到你。本文還有配套的精品資源點擊獲取