訊藍(lán)牙耳機(jī)SDK開發(fā)實(shí)戰(zhàn):消息處理框架與目錄結(jié)構(gòu)全解析)
做藍(lán)牙音頻方案的人應(yīng)該都有體會(huì)這兩年國產(chǎn)芯片在TWS耳機(jī)市場(chǎng)里已經(jīng)占了絕對(duì)主力。今天聊的中科藍(lán)訊BluetrumSDK在電商爆款、白牌走量、品牌中低端機(jī)型里用得非常多。很多朋友第一次拿到這套SDK的時(shí)候都會(huì)對(duì)著解壓出來的幾十個(gè)文件夾發(fā)愁——入口在哪、哪些文件能改、哪些不能碰、按鍵消息到底是怎么一路傳到應(yīng)用層的完全摸不著頭腦。這篇文章就從一個(gè)實(shí)際項(xiàng)目開發(fā)者的角度把中科藍(lán)訊藍(lán)牙耳機(jī)SDK從目錄結(jié)構(gòu)到消息處理框架完整拆一遍帶你看懂這套代碼的設(shè)計(jì)邏輯也順便聊聊我在實(shí)際開發(fā)里踩過的坑。文章更適合這兩類人看一是剛拿到中科藍(lán)訊SDK、準(zhǔn)備在AB56xx之類芯片上做產(chǎn)品的新人二是做過其他藍(lán)牙方案比如杰理、恒玄但被中科藍(lán)訊處理消息的方式繞暈的工程師。內(nèi)容不吹不黑全部基于我實(shí)際寫代碼、調(diào)功能、修Bug的經(jīng)驗(yàn)。1. 中科藍(lán)訊SDK的整體定位與開發(fā)前準(zhǔn)備1.1 這套SDK到底在藍(lán)牙耳機(jī)生態(tài)里處于什么位置中科藍(lán)訊的芯片在國產(chǎn)藍(lán)牙音頻SoC里出貨量一直排在前列主打性價(jià)比和集成度。一顆芯片同時(shí)承擔(dān)藍(lán)牙射頻、音頻編解碼、電源管理、觸摸檢測(cè)這些功能外圍器件很少PCB成本能壓到很低。這也是為什么市面上大量幾十塊錢到一百多塊錢的TWS耳機(jī)、頭戴耳機(jī)用的都是這套方案。但芯片便宜不等于開發(fā)省事。中科藍(lán)訊的SDK跟手機(jī)SoC廠商的SDK風(fēng)格不太一樣——它大量使用回調(diào)函數(shù)、消息隊(duì)列和自定義的輕量級(jí)調(diào)度機(jī)制而不是傳統(tǒng)裸機(jī)的前后臺(tái)大循環(huán)。初次接觸會(huì)覺得繞但只要理解了消息處理框架后續(xù)加功能、改行為就順了。我個(gè)人的看法是這套SDK的定位是“讓廠商在大批量出貨的前提下快速做產(chǎn)品”所以它把藍(lán)牙協(xié)議棧、底層驅(qū)動(dòng)做了封裝同時(shí)保留足夠多的鉤子讓應(yīng)用層去改產(chǎn)品行為。說白了它希望你快又不希望你把底層改壞。這個(gè)設(shè)計(jì)思路直接決定了目錄結(jié)構(gòu)長(zhǎng)什么樣。1.2 開發(fā)環(huán)境搭建拿到SDK以后先干什么中科藍(lán)訊官方提供的開發(fā)環(huán)境通常是Keil工程。不同系列芯片對(duì)應(yīng)的SDK版本不一樣比如AB5636A、AB5686A、AB5688這類拿到手的SDK包名和編譯器版本可能都有差異。我建議第一步不是看代碼而是先把編譯環(huán)境跑通確保官方demo能燒錄進(jìn)芯片。整個(gè)環(huán)境搭建分三步安裝Keil確認(rèn)版本老SDK在Keil 5上編譯很常見個(gè)別老工程需要補(bǔ)ARM Compiler 5編譯器因?yàn)樾掳鍷eil默認(rèn)裝的是AC6AC6對(duì)老代碼的兼容性并不好。確認(rèn)燒錄工具。中科藍(lán)訊有自己的燒錄器和上位機(jī)工具串口燒錄和SWD調(diào)試都可能用到。早期很多開發(fā)板用的是專用下載器新一些的可以用串口ISP。這一步務(wù)必按文檔來燒錄配置不對(duì)會(huì)直接導(dǎo)致連不上芯片。打開SDK里的工程文件先編譯一遍確保0 error 0 warning或者至少?zèng)]error。如果這一步過不去后面看再多代碼都沒用。這里特別提醒一點(diǎn)中科藍(lán)訊SDK不同客戶拿到的版本很可能不一樣文件名、目錄名、API命名都存在差異。我下面講的目錄和函數(shù)名是基于我接觸過的常見版本具體到你的SDK包要以實(shí)際代碼為準(zhǔn)。結(jié)構(gòu)會(huì)大同小異但函數(shù)名千萬別照抄。1.3 定位自己的角色你是“應(yīng)用層開發(fā)”還是“底層移植”拿到SDK后先想清楚一個(gè)問題你這次開發(fā)是要改產(chǎn)品行為還是要改底層協(xié)議中科藍(lán)訊SDK的分工大致是這樣藍(lán)牙協(xié)議棧、射頻校準(zhǔn)、底層音頻鏈路這些都以庫或封裝代碼的形式提供不建議普通工程師去改而產(chǎn)品邏輯、按鍵定義、LED燈效、提示音、EQ調(diào)節(jié)這些全部在應(yīng)用層代碼里。絕大多數(shù)做整機(jī)方案的人工作重心都在應(yīng)用層也就是消息處理框架覆蓋的范圍。所以這篇文章后面講的內(nèi)容也主要集中在應(yīng)用層怎么理解和修改不涉及射頻、基帶那些深水區(qū)。2. 從目錄看SDK的設(shè)計(jì)邏輯一份代碼的骨架解剖2.1 頂層目錄隱藏的“分層思想”中科藍(lán)訊SDK解壓以后頂層目錄非常多乍看很嚇人。我歸納下來其實(shí)就是三個(gè)層次硬件相關(guān)、系統(tǒng)服務(wù)、應(yīng)用業(yè)務(wù)。很多文件夾不用天天碰甚至整個(gè)開發(fā)周期里打開的次數(shù)都不超過五次。我拿一個(gè)常見版本的目錄結(jié)構(gòu)來舉例|- app/ // 應(yīng)用層代碼改產(chǎn)品邏輯主要在這里 |- bsp/ // 板級(jí)支持包按鍵、LED、I2C等外設(shè)初始化 |- chip/ // 芯片底層啟動(dòng)、寄存器定義、硬件初始化 |- cstartup/ // 啟動(dòng)文件復(fù)位向量、棧初始化 |- doc/ // 芯片手冊(cè)、SDK說明文檔 |- libs/ // 庫文件藍(lán)牙協(xié)議棧和音頻算法的閉源實(shí)現(xiàn) |- tool/ // 燒錄工具、音頻轉(zhuǎn)換工具、配置工具 |- third_party/ // 第三方組件比如一些編解碼庫、加密庫 |- main.c // 程序入口看明白沒有它跟很多嵌入式SDK一樣把“你能改的”和“你最好不要碰的”從物理上分開了。應(yīng)用層、BSP、芯片底層各自獨(dú)立改應(yīng)用層的時(shí)候不會(huì)牽連到協(xié)議棧這是好事。2.2 核心目錄逐個(gè)拆解app目錄是這個(gè)SDK里最值得花時(shí)間的地方。它保存的是整個(gè)藍(lán)牙耳機(jī)產(chǎn)品邏輯比如連接狀態(tài)處理、音樂播放暫停、通話狀態(tài)切換、電量提示、按鍵事件分發(fā)等。很多時(shí)候你只改一個(gè)文件就能改掉一個(gè)產(chǎn)品行為比如調(diào)整LED閃爍快慢、修改按鍵組合功能。bsp目錄對(duì)應(yīng)板級(jí)配置。中科藍(lán)訊的SDK對(duì)外設(shè)做了抽象板級(jí)不同主要體現(xiàn)在bsp目錄里的差異比如按鍵接在哪個(gè)GPIO、LED是低電平點(diǎn)亮還是高電平點(diǎn)亮。換板子時(shí)優(yōu)先看這里而不是去翻芯片寄存器手冊(cè)。libs目錄存放閉源庫藍(lán)牙協(xié)議棧、SBC/AAC編解碼這些都在里面。這個(gè)目錄通常以.lib或.so形式提供反正是不能改的。調(diào)試藍(lán)牙連接類問題時(shí)經(jīng)常會(huì)用到庫里導(dǎo)出的調(diào)試接口但不用理解它內(nèi)部怎么寫。doc目錄值得重視。中科藍(lán)訊每年迭代很快SDK版本之間API有差異doc里通常有版本改動(dòng)說明、芯片寄存器手冊(cè)、原理圖參考。遇到代碼看不懂的時(shí)候先翻doc比上網(wǎng)到處搜更有效率。2.3 找到程序的入口main.c的啟動(dòng)線索入口代碼通常很精簡(jiǎn)核心邏輯就是三件事初始化硬件、初始化系統(tǒng)服務(wù)、進(jìn)入消息循環(huán)。以常見的SDK為例入口大概長(zhǎng)這樣int main(void) { sys_init(); // 時(shí)鐘、電源、內(nèi)存等基礎(chǔ)初始化 board_init(); // 板級(jí)外設(shè)初始化按鍵、LED、I2C等 app_init(); // 應(yīng)用層初始化注冊(cè)消息和回調(diào) while(1) { app_msg_deal(); // 主循環(huán)里處理消息隊(duì)列 } }別看這段代碼短整個(gè)系統(tǒng)就是靠這個(gè)東西轉(zhuǎn)起來的。sys_init負(fù)責(zé)把芯片基礎(chǔ)環(huán)境準(zhǔn)備好board_init把耳機(jī)用得上的外設(shè)打開app_init把你自己的業(yè)務(wù)邏輯掛進(jìn)去最后主循環(huán)不停地從消息隊(duì)列里取消息、分發(fā)消息。這里已經(jīng)能看出消息處理框架的影子了系統(tǒng)跑起來之后除了中斷里必須立即處理的事情其他所有事件都會(huì)轉(zhuǎn)化為消息扔進(jìn)一個(gè)隊(duì)列里由主循環(huán)慢慢處理。2.4 哪些目錄可以動(dòng)哪些不要碰很多新手喜歡在庫文件、底層驅(qū)動(dòng)里加打印、改邏輯這是大忌。我給自己定了一條規(guī)矩除了app目錄和bsp目錄其他目錄一律不手改。原因很簡(jiǎn)單中科藍(lán)訊的SDK升級(jí)很頻繁廠商會(huì)不定期出新版本修Bug、加功能。如果你把改動(dòng)埋在了底層目錄下次SDK一升級(jí)你的改動(dòng)全部失效還得一個(gè)個(gè)找回來。而改動(dòng)集中在app、bsp這種應(yīng)用層位置升級(jí)時(shí)差異對(duì)比會(huì)很清晰。另外很多底層的改動(dòng)其實(shí)沒必要。比如你想在藍(lán)牙連接成功時(shí)做點(diǎn)特殊處理正確方式是注冊(cè)連接狀態(tài)回調(diào)在回調(diào)里發(fā)應(yīng)用消息而不是去改藍(lán)牙協(xié)議棧。理解這個(gè)思路后面看消息框架就會(huì)非常順。3. 核心機(jī)制消息處理框架到底在解決什么問題3.1 為什么要設(shè)計(jì)一套消息框架而不是直接調(diào)函數(shù)藍(lán)牙耳機(jī)這個(gè)系統(tǒng)有個(gè)特點(diǎn)事件來源非常雜。用戶按鍵、藍(lán)牙連接斷開、打電話來電、電量變低、充電器插入拔出這些事件什么時(shí)候發(fā)生完全無法預(yù)測(cè)而且很多都來自中斷或者底層回調(diào)。如果每個(gè)事件一來就直接調(diào)用應(yīng)用層函數(shù)會(huì)出現(xiàn)三類問題調(diào)用時(shí)機(jī)不可控。中斷里執(zhí)行的代碼有嚴(yán)格時(shí)間限制你不可能在藍(lán)牙協(xié)議棧回調(diào)里直接去播放一個(gè)提示音那太慢了。上下文沖突。多個(gè)事件可能同時(shí)被觸發(fā)如果CPU正在處理A事件時(shí)B事件插進(jìn)來兩個(gè)函數(shù)同時(shí)修改同一個(gè)全局變量系統(tǒng)就亂了。代碼耦合嚴(yán)重。應(yīng)用層依賴底層函數(shù)底層又不能反向調(diào)用應(yīng)用這種設(shè)計(jì)到最后就會(huì)變成一坨誰都看不懂的亂麻。消息框架的解法是把事件“異步化”底層發(fā)生時(shí)只負(fù)責(zé)“打包消息、扔進(jìn)隊(duì)列”然后立刻返回主循環(huán)在自己的節(jié)奏里把消息取出來處理。這樣中斷里做的事最少應(yīng)用層又有充分的執(zhí)行時(shí)間模塊之間還能解耦??梢阅命c(diǎn)餐做類比你不是在廚房里對(duì)著廚師喊一聲就開始做飯而是先寫一張菜單消息服務(wù)員把菜單交到后廚廚師按順序做。后廚再忙也得按隊(duì)列來不會(huì)因?yàn)橛腥撕暗庙懢筒尻?duì)。3.2 消息長(zhǎng)什么樣結(jié)構(gòu)體里的每個(gè)字段都有講究中科藍(lán)訊SDK里的消息本質(zhì)就是一個(gè)自定義結(jié)構(gòu)體。我有一個(gè)常見版本的樣例如下typedef struct _app_msg { uint16_t msg_id; // 消息類型區(qū)分這是一個(gè)按鍵消息還是藍(lán)牙狀態(tài)消息 uint16_t msg_param; // 消息附帶參數(shù)比如按鍵長(zhǎng)按短按、連接斷開的原因 uint8_t *msg_data; // 指向擴(kuò)展數(shù)據(jù)的指針一般用于傳字符串或者結(jié)構(gòu)體 uint8_t msg_len; // 擴(kuò)展數(shù)據(jù)的長(zhǎng)度 } app_msg_t;msg_id是消息的身份標(biāo)識(shí)決定了這條消息會(huì)被誰處理、處理成什么結(jié)果。系統(tǒng)自帶的消息ID和用戶自定義的消息ID通常分區(qū)間排列避免撞車。msg_param是消息的“補(bǔ)充說明”同一個(gè)消息類型可以帶不同的param產(chǎn)生不同行為。比如按鍵消息里param可能是短按、長(zhǎng)按還是雙擊藍(lán)牙消息里param可能是已連接、正在斷開、還是連接失敗。msg_data和msg_len用于傳遞更多數(shù)據(jù)。比如通知類消息需要攜帶一個(gè)字符串或者一個(gè)結(jié)構(gòu)體就通過這個(gè)指針傳過去。這里要注意數(shù)據(jù)本身的內(nèi)存管理需要格外小心后面在避坑章節(jié)會(huì)展開。3.3 消息的三段旅程投遞、排隊(duì)、分發(fā)一條消息從產(chǎn)生到被處理走的是固定路線第一步是投遞。底層或者中斷通過一個(gè)發(fā)送函數(shù)把消息封裝好放進(jìn)隊(duì)列。投遞動(dòng)作要求特別輕量不允許做耗時(shí)操作否則會(huì)影響實(shí)時(shí)性。第二步是排隊(duì)。消息進(jìn)入一個(gè)先進(jìn)先出的環(huán)形隊(duì)列。隊(duì)列大小在系統(tǒng)初始化時(shí)就定好了所有消息都在這個(gè)隊(duì)列里等待主循環(huán)來取。第三步是分發(fā)。主循環(huán)每輪從隊(duì)列頭部取出一條消息根據(jù)msg_id把消息交給對(duì)應(yīng)的處理函數(shù)。分發(fā)過程通常由一個(gè)大的switch-case或者一張函數(shù)指針表完成。這個(gè)三段式設(shè)計(jì)看著簡(jiǎn)單但它保證了兩個(gè)關(guān)鍵特性一是內(nèi)核執(zhí)行時(shí)間可控二是所有消息按順序處理不互相搶占。這是后面所有業(yè)務(wù)邏輯能穩(wěn)定運(yùn)行的基礎(chǔ)。3.4 系統(tǒng)消息與用戶消息ID分區(qū)規(guī)則中科藍(lán)訊SDK里消息ID不是隨便定的。系統(tǒng)內(nèi)部消息通常占一個(gè)固定的ID區(qū)間用戶自定義消息必須從某個(gè)基礎(chǔ)值之后開始。我在實(shí)際開發(fā)中習(xí)慣用一個(gè)宏來定義自己的消息ID起點(diǎn)#define APP_MSG_USER_BASE (0x2000) #define APP_MSG_EQ_CHANGE (APP_MSG_USER_BASE 1) #define APP_MSG_VOL_MAX (APP_MSG_USER_BASE 2) #define APP_MSG_ANC_SWITCH (APP_MSG_USER_BASE 3)這樣做的目的是避免覆蓋系統(tǒng)預(yù)留的消息。如果你隨意取一個(gè)ID很容易跟系統(tǒng)的某個(gè)內(nèi)部消息撞上導(dǎo)致一個(gè)正常功能被莫名觸發(fā)或者你的消息還沒走到處理函數(shù)就被系統(tǒng)攔截了。這種Bug非常難查所以消息ID規(guī)劃一開始就要規(guī)范。4. 實(shí)操從按鍵到藍(lán)牙動(dòng)作一條完整消息鏈路拆解4.1 按鍵消息的產(chǎn)生底層掃描與去抖藍(lán)牙耳機(jī)上最常見的交互就是按鍵。以單個(gè)多功能按鍵為例底層驅(qū)動(dòng)會(huì)做三件事檢測(cè)GPIO電平變化、軟件去抖、識(shí)別事件類型短按、長(zhǎng)按、雙擊、三擊。按鍵掃描在底層一般有兩種方式一種是在定時(shí)器中斷里輪詢GPIO另一種是GPIO本身支持邊沿觸發(fā)中斷。中科藍(lán)訊SDK通常用前者因?yàn)槠湫酒С钟|摸和實(shí)體按鍵混合方案定時(shí)輪詢更通用。按鍵事件識(shí)別出來之后底層的任務(wù)就完成了。它會(huì)發(fā)送一個(gè)消息msg_id標(biāo)記為按鍵事件類型msg_param填上按鍵值比如短按、長(zhǎng)按。這部分代碼在SDK里已經(jīng)幫你寫好正常情況下不需要改你要理解的是它把什么信息傳了上來。4.2 按鍵事件怎么變成應(yīng)用層消息中科藍(lán)訊SDK在應(yīng)用層注冊(cè)了一個(gè)按鍵事件的接收回調(diào)。底層一旦產(chǎn)生按鍵事件這個(gè)回調(diào)就會(huì)被調(diào)用?;卣{(diào)里通常不建議做業(yè)務(wù)處理而是把按鍵消息重新封裝成應(yīng)用消息再投遞進(jìn)消息隊(duì)列。代碼邏輯大概長(zhǎng)這樣static void user_key_callback(key_event_t key) { app_msg_t msg; msg.msg_id APP_MSG_KEY_EVENT; msg.msg_param (uint16_t)key; msg.msg_data NULL; msg.msg_len 0; app_msg_send(msg); }這里的關(guān)鍵點(diǎn)在于回調(diào)函數(shù)本身是在底層上下文里執(zhí)行的你在這里做太多事情會(huì)拖慢底層。所以只做一件事——發(fā)消息發(fā)完立刻返回。app_msg_send這個(gè)函數(shù)往消息隊(duì)列里塞消息通常內(nèi)部還會(huì)處理隊(duì)列滿的情況比如丟棄并記錄錯(cuò)誤計(jì)數(shù)。這一層封裝把底層和應(yīng)用徹底隔開底層不需要知道應(yīng)用要拿這個(gè)按鍵去做什么。4.3 應(yīng)用層怎么消費(fèi)消息分發(fā)函數(shù)的一切消息進(jìn)入隊(duì)列之后主循環(huán)會(huì)調(diào)用分發(fā)函數(shù)把消息交給對(duì)應(yīng)的處理器。一個(gè)典型的分發(fā)函數(shù)長(zhǎng)這樣void app_msg_handle(app_msg_t *msg) { switch (msg-msg_id) { case APP_MSG_KEY_EVENT: key_event_process(msg-msg_param); break; case APP_MSG_BT_STATUS: bt_status_process(msg-msg_param); break; case APP_MSG_EQ_CHANGE: eq_change_process(msg-msg_param); break; default: break; } }每種消息類型對(duì)應(yīng)一個(gè)處理函數(shù)處理函數(shù)里寫具體的產(chǎn)品行為。這樣代碼結(jié)構(gòu)非常清晰加一個(gè)新功能只需要三步定義一個(gè)消息ID、在發(fā)送方調(diào)用發(fā)送函數(shù)、在分發(fā)函數(shù)里增加一個(gè)case。注意分發(fā)函數(shù)的執(zhí)行上下文是主循環(huán)任務(wù)不是中斷。所以在處理函數(shù)里可以放心做耗時(shí)操作比如讀寫Flash、播放提示音、更新LED狀態(tài)這些都不會(huì)影響中斷響應(yīng)。4.4 一個(gè)完整場(chǎng)景雙擊按鍵切換EQ拿一個(gè)真實(shí)需求來串一遍“雙擊耳機(jī)觸摸按鍵切換EQ模式”。第一步底層識(shí)別到觸摸雙擊動(dòng)作產(chǎn)生KEY_DOUBLE_CLICK事件調(diào)用應(yīng)用層注冊(cè)的按鍵回調(diào)。第二步回調(diào)函數(shù)里發(fā)送APP_MSG_KEY_EVENT消息msg_param KEY_DOUBLE_CLICK消息入隊(duì)。第三步主循環(huán)分發(fā)該消息調(diào)用key_event_process(KEY_DOUBLE_CLICK)。第四步key_event_process里檢測(cè)當(dāng)前EQ模式切換到下一個(gè)模式然后調(diào)用音頻接口刷新EQ參數(shù)static void key_event_process(uint16_t param) { if (param KEY_DOUBLE_CLICK) { uint8_t eq_index eq_get_current_index(); eq_index (eq_index 1) % EQ_MODE_TOTAL; eq_set_index(eq_index); eq_save_to_flash(eq_index); // 記住用戶選擇重啟后恢復(fù) play_tone(TONE_EQ_SWITCH); } }這里我額外做了兩個(gè)動(dòng)作把用戶選擇的EQ索引存進(jìn)Flash切換成功后播放一個(gè)提示音。這兩個(gè)細(xì)節(jié)在產(chǎn)品上很關(guān)鍵但很多新手第一次做完切換EQ卻覺得“沒反應(yīng)”“重啟后變回去了”就是因?yàn)闆]做狀態(tài)保存和用戶反饋。這個(gè)場(chǎng)景完整展示了消息框架的價(jià)值底層不需要知道EQ是什么應(yīng)用層不需要關(guān)心按鍵GPIO怎么掃兩邊通過一條消息就對(duì)接起來了。5. 實(shí)戰(zhàn)經(jīng)驗(yàn)寫消息處理代碼時(shí)容易踩的7個(gè)坑5.1 在消息回調(diào)里做耗時(shí)操作導(dǎo)致藍(lán)牙斷連我最早犯過的錯(cuò)誤就是在回調(diào)函數(shù)里直接做耗時(shí)操作比如讀寫外部Flash、打印超大日志結(jié)果耳機(jī)開始卡頓、斷連、甚至死機(jī)。原因很簡(jiǎn)單雖然消息處理函數(shù)在主循環(huán)里執(zhí)行時(shí)間相對(duì)寬松但主循環(huán)背后還有藍(lán)牙協(xié)議棧的實(shí)時(shí)任務(wù)在跑。如果某個(gè)消息處理函數(shù)執(zhí)行時(shí)間過長(zhǎng)會(huì)阻塞整個(gè)系統(tǒng)的調(diào)度藍(lán)牙協(xié)議的實(shí)時(shí)性就得不到保障。正確做法是把真正耗時(shí)的動(dòng)作再拆出去或者至少保證單個(gè)消息處理函數(shù)執(zhí)行時(shí)間在幾十毫秒以內(nèi)。特別是在處理藍(lán)牙連接狀態(tài)消息時(shí)務(wù)必快速返回。5.2 消息ID撞車導(dǎo)致功能錯(cuò)亂消息ID撞車是我見過最多的問題之一。很多方案商改動(dòng)時(shí)喜歡直接拿系統(tǒng)消息ID段里某個(gè)數(shù)字來用因?yàn)樗翱雌饋頉]被用到”。結(jié)果某個(gè)系統(tǒng)新版本啟用了這個(gè)ID你的功能就莫名其妙被觸發(fā)了。正確做法是嚴(yán)格按照SDK文檔里的ID分區(qū)來定義用戶消息從約定的用戶起點(diǎn)往后編排。并且建議在工程里維護(hù)一個(gè)消息ID清單每新增一個(gè)消息就登記一下防止多人協(xié)作時(shí)互相覆蓋。5.3 消息隊(duì)列溢出造成事件丟失按鍵偶爾失靈、狀態(tài)切換不響應(yīng)很多情況都是消息隊(duì)列滿了新消息被丟棄。SDK底層通常會(huì)提供一個(gè)隊(duì)列長(zhǎng)度的統(tǒng)計(jì)或溢出日志排查時(shí)要先看有沒有“msg queue full”之類的輸出。隊(duì)列大小的選擇本質(zhì)是空間與可靠性的取舍。隊(duì)列太小容易丟消息隊(duì)列太大又浪費(fèi)RAM。我的經(jīng)驗(yàn)是先按當(dāng)前產(chǎn)品事件的峰值估算隊(duì)列深度然后在壓力測(cè)試中觀察是否丟事件再適當(dāng)留余量。5.4 自定義消息里的指針數(shù)據(jù)成了野指針msg_data字段是個(gè)坑。如果你把消息發(fā)進(jìn)隊(duì)列后外部數(shù)據(jù)的內(nèi)存提前被釋放或者被改寫消息處理函數(shù)拿到的就是野指針。常見場(chǎng)景是發(fā)送一個(gè)局部變量地址作為msg_data發(fā)送函數(shù)直接入隊(duì)函數(shù)返回后局部變量銷毀消息處理時(shí)地址已經(jīng)無效了。正確處理方式是為數(shù)據(jù)單獨(dú)分配內(nèi)存整包消息在按鍵回調(diào)后統(tǒng)一釋放或者直接傳遞靜態(tài)數(shù)據(jù)區(qū)。我的建議是消息里盡量少用指針傳遞長(zhǎng)數(shù)據(jù)能用msg_param搞定就用msg_param。如果一定要用指針務(wù)必在接收端處理完成后由同一個(gè)模塊負(fù)責(zé)釋放內(nèi)存。5.5 藍(lán)牙連接狀態(tài)機(jī)與UI狀態(tài)不一致耳機(jī)產(chǎn)品最核心的狀態(tài)是藍(lán)牙連接狀態(tài)已連接、已斷開、回連中、配對(duì)中等。應(yīng)用層通常在消息處理函數(shù)里根據(jù)這些狀態(tài)切換UI行為比如LED快閃、慢閃、熄滅。最容易出Bug的地方是狀態(tài)機(jī)沒有做邊界處理。比如已經(jīng)在已連接狀態(tài)不經(jīng)意間又收到一個(gè)已連接消息按正常邏輯你可能去播放一次“連接成功”提示音結(jié)果用戶會(huì)聽到重復(fù)提示音。處理這類消息時(shí)要判斷當(dāng)前狀態(tài)跟新消息是否一致只有狀態(tài)變化時(shí)才更新UI。5.6 SDK版本升級(jí)后頭文件不匹配中科藍(lán)訊SDK升級(jí)頻率很高廠商發(fā)的版本之間結(jié)構(gòu)體和函數(shù)簽名都可能變。升級(jí)時(shí)如果沒有同步修改代碼編譯報(bào)錯(cuò)是最輕的怕的是編譯能過但行為完全變了比如一個(gè)結(jié)構(gòu)體新增了字段舊代碼初始化的結(jié)構(gòu)體沒有這個(gè)字段導(dǎo)致消息里出現(xiàn)垃圾數(shù)據(jù)。所以每次升級(jí)SDK我強(qiáng)烈建議先看doc目錄下的版本更新說明重點(diǎn)檢查頭文件里的結(jié)構(gòu)體定義、消息ID定義有沒有變化然后全局搜索自己用到的關(guān)鍵API確認(rèn)簽名是否一致。5.7 實(shí)機(jī)調(diào)試時(shí)用日志定位消息走向這套SDK里最常用的調(diào)試手段是串口日志。主控通過UART把調(diào)試信息打印到PC端串口工具。排查消息鏈路問題關(guān)鍵是要在關(guān)鍵節(jié)點(diǎn)打印精簡(jiǎn)日志消息產(chǎn)生時(shí)打一條入隊(duì)時(shí)打一條處理前打一條。通過日志時(shí)間戳可以定位消息是在哪一段被丟棄的。另外很多中科藍(lán)訊SDK支持在編譯期開關(guān)日志分級(jí)建議開發(fā)期把調(diào)試日志全打開量產(chǎn)時(shí)再裁剪成error級(jí)別避免日志本身影響系統(tǒng)性能。為了方便快速排查我整理了一份常見問題速查表現(xiàn)象可能原因快速排查方法按鍵偶爾無反應(yīng)消息隊(duì)列溢出按鍵事件被丟棄查看日志是否有queue full增大隊(duì)列深度聲音斷斷續(xù)續(xù)連電腦時(shí)更明顯系統(tǒng)藍(lán)牙與其他射頻干擾或音頻重傳丟包檢查PCB天線區(qū)域干擾源確認(rèn)A2DP鏈路穩(wěn)定性耳機(jī)被電腦識(shí)別成handsfree設(shè)備耳機(jī)端配置文件未正確聲明A2DP檢查藍(lán)牙配置文件和SDP服務(wù)聲明升級(jí)SDK后編譯告警結(jié)構(gòu)體定義變化字段未同步初始化diff新舊頭文件檢查消息結(jié)構(gòu)體提示音重復(fù)播放狀態(tài)消息重復(fù)觸發(fā)UI刷新消息處理前判斷當(dāng)前狀態(tài)是否相同自定義消息沒被處理消息ID落在系統(tǒng)保留區(qū)間被前置攔截確認(rèn)消息ID在用戶自定義區(qū)間藍(lán)牙回連失敗回連消息隊(duì)列被長(zhǎng)任務(wù)阻塞檢查是否有處理函數(shù)耗時(shí)過長(zhǎng)6. 最后說點(diǎn)實(shí)在的中科藍(lán)訊的SDK上手時(shí)確實(shí)有門檻但它一旦跑通開發(fā)效率是很高的。這套目錄和消息機(jī)制的組合本質(zhì)上就是一套簡(jiǎn)化版的嵌入式事件驅(qū)動(dòng)框架。你不需要把它想得多么高深只需要順著“底層發(fā)消息、隊(duì)列存消息、主循環(huán)分發(fā)消息、處理函數(shù)執(zhí)行動(dòng)作”這條主線去讀代碼很快就能在上面的框架里加出自己的功能。我個(gè)人實(shí)踐中最大的體會(huì)是在這套SDK里克制比聰明更重要。改產(chǎn)品邏輯時(shí)盡量只動(dòng)app層發(fā)消息的地方和處理消息的地方分開絕不跳過消息隊(duì)列直接調(diào)用業(yè)務(wù)函數(shù)。這樣短期看多點(diǎn)幾下鍵盤的效率損失長(zhǎng)期換來的是代碼可維護(hù)性、可升級(jí)性以及少排一堆詭怪的Bug。最后再分享一個(gè)小技巧給每類消息的處理函數(shù)統(tǒng)一加上入口和出口日志用宏控制編譯選項(xiàng)。這招在多人協(xié)作時(shí)特別好用A改的代碼影響到了B的功能誰的消息先到、誰的處理先執(zhí)行日志一拉就清清楚楚。希望你少踩我踩過的坑。