牙耳機(jī)固件事件總線與ANC/NVKEY鏈路解析)
簡(jiǎn)介針對(duì)Airoha洛達(dá)AB1568藍(lán)牙耳機(jī)芯片的應(yīng)用開發(fā)資料面向固件工程師與進(jìn)階學(xué)習(xí)者系統(tǒng)梳理耳機(jī)工程的啟動(dòng)與功能實(shí)現(xiàn)路徑。內(nèi)容從設(shè)備上電后的初始化、宏定義與編譯配置文件入手延伸到應(yīng)用活動(dòng)行為管理、主動(dòng)降噪模式切換、呼吸燈樣式修改、充電參數(shù)監(jiān)測(cè)、超時(shí)機(jī)制、事件發(fā)送接收與刪除、自動(dòng)關(guān)機(jī)、配對(duì)時(shí)間、開機(jī)電量提示、清除配對(duì)記錄和鏈路重連等常用模塊并補(bǔ)充接口函數(shù)參考、回調(diào)注冊(cè)、界面框架、側(cè)音、音頻路徑、射頻測(cè)試、硬件復(fù)位、均衡設(shè)定和通話降噪等專題便于建立從底層到應(yīng)用層的完整開發(fā)脈絡(luò)。壓縮包內(nèi)含一個(gè)PDF文件約9.58MB章節(jié)按知識(shí)點(diǎn)編排適合按模塊檢索和對(duì)照代碼查閱可作為開發(fā)手冊(cè)與排錯(cuò)參考。已有374人學(xué)習(xí)能輔助快速定位關(guān)鍵流程與接口用法。1. 一條事件總線撐起整只耳機(jī)為什么主循環(huán)里找不到業(yè)務(wù)代碼很多人第一次翻藍(lán)牙耳機(jī)固件會(huì)下意識(shí)去找while(1)里堆業(yè)務(wù)邏輯的地方結(jié)果發(fā)現(xiàn)主循環(huán)幾乎是空的。AB1568 這類音頻平臺(tái)的真實(shí)骨架是「活動(dòng)堆 事件總線」上電后先跑 bootloader再進(jìn) apps 工程的 main隨后apps_inits把一堆活動(dòng)掛到堆上。按鍵、定時(shí)器、協(xié)議?;卣{(diào)都只做一件事——往總線扔事件由當(dāng)前活動(dòng)決定誰接。搞不清這條鏈調(diào) ANC 切換就會(huì)卡在「三連擊有提示音、降噪?yún)s沒變化」這種玄學(xué)問題上翻遍按鍵配置也找不到根因。這篇就從 bootloader 的第一條指令一路拆到濾波器參數(shù)真正生效的那一刻。2. 從上電到跑起來bootloader、main和mk宏到底誰先誰后2.1 上電后先跑的不是你的應(yīng)用代碼芯片復(fù)位后取指地址先落到 bootloader 的 main而不是 apps 下 earbuds 或 headset 工程的 main。bootloader 這一段只做最小初始化時(shí)鐘、看門狗、調(diào)試串口保證后面能打 log。真正關(guān)鍵的是升級(jí)分支——FOTA 處理函數(shù)被一個(gè)條件編譯宏包著宏沒開這段邏輯在預(yù)處理階段就被剔掉鏈接出來的 bin 里根本沒有它。// bootloader 的 main 骨架示意非逐行還原 int main(void) { bsp_early_init(); // 時(shí)鐘、看門狗、調(diào)試串口先保證能打 log #ifdef BL_FOTA_ENABLE bl_fota_process(); // 只有宏開了這段才會(huì)被編進(jìn)來 #endif app_main_entry(); // 跳板進(jìn) apps 下對(duì)應(yīng)工程的 main for (;;) { } return 0; }bsp_early_init是后續(xù)所有 log 能出來的前提調(diào)不出來先查串口引腳和波特率。BL_FOTA_ENABLE決定bl_fota_process是否成為被編譯的代碼這是「宏不生效」類問題的第一層根因。app_main_entry只是個(gè)跳板真正的業(yè)務(wù)初始化在 apps 工程的 main 里。理解這一點(diǎn)后面看啟動(dòng) log 就不會(huì)把 bootloader 和 apps 的打印混在一起。2.2 宏定義藏在哪個(gè)mk文件./build.sh list的用法代碼里看到的宏不是隨便哪個(gè) mk 文件里寫了就生效它取決于編譯命令實(shí)際調(diào)用了哪個(gè) mk。第一步永遠(yuǎn)是先列出來別憑記憶猜。用./build.sh list能看到每個(gè)工程對(duì)應(yīng)的板級(jí)目錄和 mk 文件當(dāng)前頭戴耳機(jī)工程基于某 1568 評(píng)估板開發(fā)走的是 headset 參考設(shè)計(jì)工程編譯時(shí)調(diào)feature_68_evk.mk。# 列出所有可編譯工程以及它們各自調(diào)用的 mk ./build.sh list # 輸出里會(huì)看到類似映射不同 SDK 版本命名略有差異 # headset_ref_design - 某1568評(píng)估板 - feature_68_evk.mk打開feature_68_evk.mk能看到一串CFLAGS -D...形式的宏開關(guān)ANC 使能、通話降噪、bootloader 的 FOTA 都在這里。新增宏也加在這個(gè)文件里不要在 .c 里隨手#define——換工程或換板子時(shí)隨手寫的宏不會(huì)跟著走這是血淚經(jīng)驗(yàn)里最常見的一類翻車。# feature_68_evk.mk 片段示意 CFLAGS -DANC_ENABLE # ANC 總開關(guān) CFLAGS -DBL_FOTA_ENABLE # bootloader 的 FOTA 處理 CFLAGS -DENC_ENABLE # 通話降噪判斷一個(gè)宏到底有沒有生效不用讀代碼直接看編譯產(chǎn)物里的預(yù)處理結(jié)果或者運(yùn)行時(shí) log 里對(duì)應(yīng)的分支有沒有被打出來。宏改了行為沒變九成是改錯(cuò)了 mk或者 build.sh list 里當(dāng)前工程壓根沒指向你以為的那個(gè)文件。2.3 apps_inits與主進(jìn)程開機(jī)后初始化了什么進(jìn)到 apps 工程 main 后apps_inits負(fù)責(zé)把各個(gè)應(yīng)用模塊拉起來其中就包括所有活動(dòng)行為的初始化。每個(gè)活動(dòng)通過ui_shell_start_activity在活動(dòng)堆里申請(qǐng)一個(gè)空閑 item相當(dāng)于給這個(gè)「界面」占個(gè)位。app_preproc_activity_proc是主進(jìn)程負(fù)責(zé)管理和調(diào)配子進(jìn)程比如app_home_screen_idle_activity_proc這類子 UI 界面。開機(jī)后會(huì)同時(shí)存在很多進(jìn)程每個(gè)進(jìn)程自帶事件組。系統(tǒng)產(chǎn)生一個(gè)事件就往外拋所有進(jìn)程都能監(jiān)聽到只有事件組對(duì)得上的那個(gè)進(jìn)程才處理。這解釋了為什么主循環(huán)是空的業(yè)務(wù)不在循環(huán)里順序跑而是被事件推著走?;顒?dòng)堆里同一時(shí)刻只有一個(gè)當(dāng)前活動(dòng)它決定了這條事件先給誰看。把這一層想通后面 ANC 按鍵為什么進(jìn)_proc_key_event_group、為什么最后落到app_home_screen_process_anc_and_pass_through就都是順理成章的事。3. UI Shell事件總線活動(dòng)堆憑什么決定這條事件歸誰處理3.1 ui_shell_start_activity申請(qǐng)活動(dòng)item活動(dòng)堆不是普通數(shù)組它維護(hù)的是「當(dāng)前活動(dòng)」和「可被喚醒的活動(dòng)」兩類狀態(tài)。ui_shell_start_activity在堆里找一個(gè)空閑 item把活動(dòng) ID、事件處理回調(diào)、活動(dòng)私有數(shù)據(jù)填進(jìn)去然后掛到活動(dòng)表上。每個(gè)活動(dòng)類似手機(jī)里的一個(gè)頁面home screen 空閑態(tài)是一個(gè)活動(dòng)設(shè)置頁是另一個(gè)通話中又是另一個(gè)。這里有個(gè)容易忽略的點(diǎn)活動(dòng)不是越早注冊(cè)越好注冊(cè)順序影響事件分發(fā)時(shí)的遍歷順序。常見實(shí)現(xiàn)思路是按活動(dòng)優(yōu)先級(jí)排序當(dāng)前活動(dòng)排最前其余按狀態(tài)掛起。如果兩個(gè)活動(dòng)都對(duì)同一個(gè)事件 ID 感興趣先被遍歷到的先處理處理完是否繼續(xù)往下傳由返回值決定。調(diào) UI 行為時(shí)如果發(fā)現(xiàn)事件被「截胡」先查活動(dòng)注冊(cè)順序和事件組的匹配關(guān)系而不是懷疑事件沒發(fā)出去。3.2 事件的發(fā)送、接收與刪除三件套怎么配合事件的生命周期就三件事發(fā)、收、刪。發(fā)用ui_shell_send_event收靠ui_shell_init_input_msg注冊(cè)的輸入消息處理刪用ui_shell_remove_event把還在隊(duì)列里沒來得及處理的事件摘掉。三者配合不好最常見的就是事件重復(fù)處理或者內(nèi)存越界。// 發(fā)送一個(gè)自定義事件示意 bool ok ui_shell_send_event( false, // 是否立即發(fā)送false 走隊(duì)列 EVENT_PRIORITY_HIGH, // 優(yōu)先級(jí)高優(yōu)先級(jí)先出隊(duì) EVENT_GROUP_APP, // 事件組決定哪些活動(dòng)能收到 MY_CUSTOM_EVENT_ID, // 事件 ID payload, // 攜帶數(shù)據(jù)指針 sizeof(payload), // 數(shù)據(jù)長(zhǎng)度 0, // 延時(shí)發(fā)送毫秒數(shù) 500 // 超時(shí)毫秒數(shù)超時(shí)后事件被丟棄 );payload是棧上變量時(shí)false走隊(duì)列就有生命周期風(fēng)險(xiǎn)——事件還在隊(duì)列里函數(shù)已經(jīng)返回指針懸空。穩(wěn)妥做法是傳堆上拷貝或者用值傳遞。EVENT_GROUP_APP要和接收方注冊(cè)的事件組一致組不對(duì)事件發(fā)出去也沒人接。500這個(gè)超時(shí)值不是隨便填的它決定這條事件在隊(duì)列里最多等多久超時(shí)后被靜默丟棄log 里往往只有一行不起眼的丟棄記錄。ui_shell_remove_event適合在活動(dòng)切走時(shí)清理未處理事件避免舊頁面的回調(diào)打到新頁面的數(shù)據(jù)上。3.3 超時(shí)、自動(dòng)關(guān)機(jī)與配對(duì)時(shí)間的調(diào)度超時(shí)時(shí)間設(shè)置、自動(dòng)關(guān)機(jī)配置、配對(duì)時(shí)間窗口本質(zhì)都是同一個(gè)機(jī)制的不同參數(shù)化往事件總線注冊(cè)一個(gè)定時(shí)事件到點(diǎn)觸發(fā)觸發(fā)后看當(dāng)前狀態(tài)決定執(zhí)行什么。自動(dòng)關(guān)機(jī)通常是「無連接 無按鍵 超時(shí)」三個(gè)條件同時(shí)滿足才觸發(fā)配對(duì)時(shí)間窗口則是「進(jìn)入配對(duì)態(tài)后啟動(dòng)一個(gè)倒計(jì)時(shí)超時(shí)自動(dòng)退出」。調(diào)這類參數(shù)時(shí)不要只改一個(gè)數(shù)字就完事。配對(duì)時(shí)間改短了用戶可能還沒連上窗口就關(guān)了自動(dòng)關(guān)機(jī)改長(zhǎng)了待機(jī)功耗壓不住。常見的實(shí)現(xiàn)思路是把這些時(shí)間做成 NVKEY 或配置表里的字段運(yùn)行時(shí)讀出來再用而不是硬編碼在 .c 里。這樣產(chǎn)測(cè)和不同客戶版本可以用同一套代碼、不同的配置。改完參數(shù)記得確認(rèn)寫進(jìn)了哪一層是編譯期宏、運(yùn)行時(shí) NVKEY還是 Config Tool 里的 UI 項(xiàng)三層混了就是典型踩坑。4. ANC切換鏈路從按鍵key_action到濾波器參數(shù)生效中間隔了幾層4.1 按鍵事件進(jìn)入_proc_key_event_group物理按鍵按下先產(chǎn)生的是按鍵事件進(jìn)_proc_key_event_group處理。這里做的第一件事是把「哪個(gè)鍵、什么手勢(shì)」翻譯成key_action枚舉。ANC 相關(guān)的四個(gè)典型值KEY_PASS_THROUGH是通透模式開關(guān)KEY_ANC是 ANC 開關(guān)KEY_SWITCH_ANC_AND_PASSTHROUGH是全關(guān)到通透再到 ANC 的循環(huán)KEY_BETWEEN_ANC_PASSTHROUGH是通透和 ANC 之間來回切。物理按鍵到key_action的映射不寫在按鍵處理函數(shù)里而在customerized_key_config.c這張表里。改按鍵行為先改這張表別去動(dòng)_proc_key_event_group的分發(fā)邏輯。// customerized_key_config.c 里的按鍵映射示意 static const key_config_t key_table[] { { KEY_POWER, KEY_TRIPLE_CLICK, KEY_SWITCH_ANC_AND_PASSTHROUGH }, { KEY_POWER, KEY_DOUBLE_CLICK, KEY_PASS_THROUGH }, { KEY_POWER, KEY_LONG_PRESS, KEY_ANC }, };表里第一列是物理鍵第二列是手勢(shì)第三列是行為。三連擊 Power 映射到循環(huán)切換就意味著每按三次觸發(fā)一次 ANC 狀態(tài)機(jī)往前走一步。這張表改錯(cuò)表現(xiàn)就是「按鍵有提示音但模式不切」因?yàn)樘崾疽粼诎存I事件層就響了跟 ANC 服務(wù)是否被調(diào)用是兩回事。4.2 target_anc_type與app_anc_service_enable狀態(tài)機(jī)怎么往下走KEY_SWITCH_ANC_AND_PASSTHROUGH被識(shí)別后處理函數(shù)做兩件事把target_anc_type推到下一個(gè)狀態(tài)把target_filter_id換成對(duì)應(yīng)的濾波器 ID然后一起傳給app_anc_service_enable。所有 ANC 事件行為最終都匯到這個(gè)函數(shù)它是 ANC 服務(wù)的統(tǒng)一入口。// 按鍵行為到 ANC 服務(wù)調(diào)用的鏈路示意 case KEY_SWITCH_ANC_AND_PASSTHROUGH: target_anc_type next_anc_type(target_anc_type); // 全關(guān) - 通透 - ANC 循環(huán) target_filter_id anc_type_to_filter(target_anc_type); app_anc_service_enable(target_anc_type, target_filter_id); break;target_anc_type是模式枚舉target_filter_id是濾波器編號(hào)。ANC 默認(rèn)濾波器AUDIO_ANC_CONTROL_ANC_FILTER_DEFAULT值為 1PassThrough 默認(rèn)濾波器AUDIO_ANC_CONTROL_PASS_THRU_FILTER_DEFAULT值為 9。切換時(shí) log 里能看到anc_result循環(huán)打出三個(gè)模式過濾這個(gè)字段就能確認(rèn)狀態(tài)機(jī)有沒有走對(duì)。如果 log 顯示狀態(tài)在變、聽感沒變問題就不在這層往下查濾波器參數(shù)。4.3 NVKEY濾波器參數(shù)與NVR解析0xE1E5到底怎么讀聲學(xué)調(diào)好的 ANC 參數(shù)不在代碼里在 Config Tool 的 NVR 文件里。四組濾波器 Filter 1 到 Filter 4每組包含 FF前饋、FB反饋、FBC反饋補(bǔ)償左右聲道。把 NVR 導(dǎo)入工程或直接在 Tool 上調(diào)整Write NV 后 Reset參數(shù)才真正寫進(jìn)耳機(jī)。這一步?jīng)]做前面按鍵切得再對(duì)降噪也不會(huì)有變化——這是「三連擊有提示音沒效果」最經(jīng)典的根因。NVR 里的數(shù)值是高字節(jié)在前、低字節(jié)在后解析時(shí)要先反轉(zhuǎn)。比如0xE1E5 10FF 1400 D8DC D8DC10FF反轉(zhuǎn)成FF10十進(jìn)制 65296減 65536 得 -240對(duì)應(yīng) Component Gain 的 -2.4dB1400反轉(zhuǎn)成0014十進(jìn)制 20對(duì)應(yīng) 0.2dB。// NVR 里 Component Gain 的解析示意 int16_t parse_component_gain(uint16_t raw) { uint16_t swapped (uint16_t)((raw 8) | (raw 8)); // 前后位反轉(zhuǎn) if (swapped 0x8000) { // 最高位為 1 表示負(fù)數(shù) return (int16_t)(swapped - 65536); } return (int16_t)swapped; }這個(gè)函數(shù)只做反轉(zhuǎn)和補(bǔ)碼還原不負(fù)責(zé)單位換算。-240是 Tool 里的內(nèi)部單位對(duì)應(yīng) -2.4dB別直接把 -240 當(dāng) dB 用。NVKEY 里 ANC 相關(guān)參數(shù)的 KeyID 段是0xEE00~0xEEFF在nvkey.xml里可能原本沒有這一段需要手動(dòng)補(bǔ)。補(bǔ)的時(shí)候要對(duì)齊 XML 和 bin 兩個(gè)產(chǎn)物只改一個(gè)編譯出來的 download 文件里參數(shù)還是舊的。5. 常見問題與排查ANC沒效果、宏不生效、NVKEY寫不進(jìn)去5.1 現(xiàn)象三連擊有提示音但降噪沒變化現(xiàn)象很直觀按鍵提示音響了log 里anc_result也在循環(huán)打但耳朵聽不出降噪差異。原因通常不在按鍵鏈路而在 NVKEY 里的 ANC 參數(shù)是空的或者只寫了默認(rèn)值。Config Tool 打開工程的 download 文件夾看 ANC 模式里有沒有實(shí)際調(diào)過的參數(shù)沒有就是沒導(dǎo)入 NVR。解決分兩步先用 Config Tool 導(dǎo)入聲學(xué)調(diào)好的 NVR 文件Write NV 后 Reset如果要固化到代碼還要走 Special_Export 把變更導(dǎo)出來更新nvkey.xml和filesystem.bin然后 clean 后重新 build。少任何一步download 文件里的參數(shù)都不會(huì)變。5.2 現(xiàn)象改了宏但編譯行為沒變改了feature_68_evk.mk里的宏重新編譯運(yùn)行時(shí)行為跟沒改一樣。原因有兩個(gè)一是當(dāng)前編譯的工程根本沒調(diào)這個(gè) mk./build.sh list一看就露餡二是改了 mk 但沒 clean增量編譯復(fù)用了舊的目標(biāo)文件。先 list 確認(rèn)工程和 mk 的映射關(guān)系再 clean 后全量 build。判斷宏有沒有生效不用讀代碼看預(yù)處理產(chǎn)物里對(duì)應(yīng)的#ifdef分支有沒有被展開或者運(yùn)行時(shí) log 里那個(gè)分支的打印有沒有出現(xiàn)。這兩步都過了還不對(duì)才去懷疑宏本身寫錯(cuò)。5.3 現(xiàn)象Config Tool導(dǎo)入NVR后參數(shù)還是舊值導(dǎo)入 NVR、Write NV、Reset 都做了Config Tool 里看參數(shù)是新的但耳機(jī)行為還是舊的。原因是參數(shù)只寫進(jìn)了耳機(jī)沒寫進(jìn)工程的 download 文件下次編譯又把舊的 download 燒回去了。要把參數(shù)固化到代碼必須走 Special_Export 導(dǎo)出變更生成的文件里syskey.xml和bin要更新到工程對(duì)應(yīng)的nvkey.xml和filesystem.bin。syskey.xml里有的 KeyID 在nvkey.xml里不一定有沒有的要手動(dòng)添加有的要替換。只改 XML 不改 bin或者只改 bin 不改 XML編譯出來的 download 都是半新半舊。5.4 現(xiàn)象FOTA后NVKEY回退FOTA 升級(jí)完成耳機(jī)重啟之前調(diào)好的 ANC 參數(shù)回到默認(rèn)值。原因是 FOTA 更新固件時(shí)沒有把 NVKEY 的變更一起帶過去新固件里的默認(rèn) NVKEY 覆蓋了舊值。解決思路是把 NVKEY 變更做成升級(jí)包的一部分或者升級(jí)后由NVKEY_Setting.c里的寫入函數(shù)重新寫一遍。FOTA 和 NVDM 的配合要提前驗(yàn)證升級(jí)前記錄關(guān)鍵 KeyID 的值升級(jí)后讀出來比對(duì)不一致就說明 NVKEY 沒跟過去。這個(gè)坑在量產(chǎn)后才暴露出來代價(jià)很大產(chǎn)測(cè)階段就要把 FOTA 前后 NVKEY 一致性加進(jìn)檢查項(xiàng)。6. 設(shè)計(jì)取舍事件驅(qū)動(dòng)與參數(shù)固化為什么選這條難走的路事件驅(qū)動(dòng)不是唯一解。輪詢也能實(shí)現(xiàn)按鍵掃描和狀態(tài)切換代碼寫起來更直白調(diào)試時(shí)打個(gè)斷點(diǎn)就能看到調(diào)用棧。但耳機(jī)這種多狀態(tài)、多回調(diào)、低功耗的場(chǎng)景輪詢的空轉(zhuǎn)功耗和狀態(tài)耦合會(huì)迅速失控。事件總線的代價(jià)是調(diào)用鏈變長(zhǎng)一個(gè)按鍵按下要經(jīng)過按鍵組、活動(dòng)分發(fā)、服務(wù)層好幾跳出問題時(shí) log 是唯一的黑匣子。這套設(shè)計(jì)換來的是活動(dòng)之間解耦home screen 不需要知道 ANC 服務(wù)怎么實(shí)現(xiàn)ANC 服務(wù)也不需要知道當(dāng)前是哪個(gè)界面觸發(fā)的大家只認(rèn)事件 ID。代價(jià)和收益擺在一起事件驅(qū)動(dòng)在耳機(jī)固件這個(gè)約束下是更劃算的選擇。參數(shù)固化走 XML bin Special_Export 這條路看起來比直接改代碼麻煩但它是為量產(chǎn)服務(wù)的。聲學(xué)工程師在 Config Tool 里調(diào)參數(shù)不碰代碼固件工程師維護(hù)nvkey.xml的 KeyID 結(jié)構(gòu)產(chǎn)測(cè)程序拿 XML 和 bin 去燒錄。三方各管一層參數(shù)變更不需要重新理解代碼邏輯。這條鏈的脆弱點(diǎn)在于「同步」Tool 里的參數(shù)、XML、bin、耳機(jī)里的 NVKEY四處任何一處對(duì)不上表現(xiàn)都是「看起來改了但沒生效」。所以每次改完參數(shù)我都會(huì)按固定順序走一遍——Tool 導(dǎo)入 NVR、Write NV、Reset 驗(yàn)證聽感、Special_Export 導(dǎo)出、更新 XML 和 bin、clean 后 build、再燒錄驗(yàn)證。從那以后我每次動(dòng) ANC 參數(shù)都強(qiáng)制走完這套流程不再相信「這次應(yīng)該只改了一個(gè)值」的直覺。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取