1. 蓝牙更新13节——这套Framework到HAL的内容到底在讲什么
先说个背景。我们这个系列一直盯着Android系统底层,从Framework到HAL再到具体平台适配。标题里写"蓝牙更新13节"的时间是2019年6月5日,那个时间点恰好是Android 9已经大面积铺开、Android 10还在预览的阶段,车载、TWS耳机、蓝牙键盘手柄这类外设需求爆发式增长,系统级蓝牙的问题比以往任何时候都多。做Framework开发的会发现,应用层一个BluetoothAdapter能调起来的连接,背后牵扯到权限校验、Binder通信、JNI、协议栈、HCI命令、HAL层最终落到一块蓝牙芯片的固件行为——任何一层出问题,表现出来都只是"连不上"或"声音断断续续"。
这13节的内容就是沿着这条链路往下打。从BR/EDR到BLE,从A2DP音视频传输到HID人机接口设备,从HCI日志分析到HAL接口适配。它不是给应用开发者教你怎么调BluetoothGattAPI,而是面向需要在系统层面改蓝牙、排查疑难问题、做设备适配的人。我花了将近一个月把这13节全部跟完,并且在自己手头的RK3568平台和高通平台上看代码验证,下面把整个学习过程、重要知识点和踩过的坑一次性整理出来。
如果你是下面这几类人,这篇笔记应该对你有直接帮助:
- Android系统工程师,需要排查蓝牙配对、通话音频、BLE连接等底层问题
- BSP/HAL开发工程师,需要对接或修改蓝牙HAL实现
- 嵌入式玩家,手里有HC05、ESP32这类蓝牙模块,想搞清楚手机端到底是怎么跟它们交互的
- 车载和盒子开发者,被A2DP切SCO、HID键值这类问题反复折磨过
2. 13节的全景拆解:从蓝牙协议栈到HAL的完整贯穿
先说一下这13节是怎么组织的,因为这套编排顺序本身就很值得参考——它就是按照"从用户可见行为向底层推进"的思路设计的,学完等于把Android蓝牙代码从外到内翻了一遍。
2.1 前4节:历史、架构和Framework入门
前几节把背景铺得很扎实:蓝牙从1.0到5.0每个版本的关键变化,Android从早期用BlueZ到后来全面切换Bluedroid协议栈的来龙去脉,经典蓝牙BR/EDR和低功耗蓝牙BLE两大类技术分支的区别。尤其建议认真看Android蓝牙整体架构这一节,它把整个栈分层讲清楚了:
- 应用层:
BluetoothAdapter、BluetoothManager以及各个profile API - Framework Java层:
BluetoothManagerService、BluetoothAdapterService、BluetoothDevice、BluetoothA2dp等系统服务 - JNI层:
com_android_bluetooth_*,Java和C++相互调用的桥 - 协议栈层:Bluedroid,对应
btif/btc/bt_stack这一坨 - HCI层:Host Controller Interface,协议栈和蓝牙控制器之间的命令/事件通道
- HAL层:
libbt-vendor和系统调用控制芯片固件
第一节就直说Android蓝牙是"双栈架构"——HCI上面是Bluedroid,下面连接的是各家vendor的蓝牙控制器(CSR、Realtek、AW、QCOM、Bestechnic等)。也就是说,你从Framework层改得再多,底层芯片不支持也没用;反过来,芯片的HCI行为异常,上层日志里只能看到超时和断链。这套双栈概念贯穿了整个13节,后面的内容都是在这个分层里展开的。
2.2 第5-7节:经典蓝牙核心——SPP、L2CAP、A2DP、HFP
这几节是BR/EDR的核心。SDP服务发现、L2CAP通道复用、RFCOMM串口仿真、A2DP音乐传输、AVRCP控制指令、HFP免提通话,全部覆盖。
重点留意A2DP和HFP这两节。音乐播放走的是A2DP,它本质是双向的:Source和Sink之间协商codec(SBC、AAC、aptX之类),然后通过L2CAP建立传输通道;而打电话时用的是HFP协议,音频从A2DP转到HFP建立的SCO/eSCO链路。这两条路的切换是整个蓝牙系统中特别容易翻车的一个环节,13节里专门用了两节去讲它的路由逻辑,后面我也会重点展开。
2.3 第8-10节:BLE协议栈——GAP、GATT、MTU、SMP
BLE这几节含金量很足。GAP负责广播、扫描、连接建立,GATT负责连接建立之后的服务/特征读写,MTU协商决定了一次能传多少数据,SMP则负责配对、密钥分发、安全加密。
值得一提是MTU协商这一节有个细节很重要:Android侧的默认MTU是23字节,即20字节有效载荷,很多开发者在BLE通信时发现一次读不到完整数据,就是卡在MTU没变大。AndroidBluetoothGatt.requestMtu()在API 21之后就能用了,但代码里必须等onMtuChanged回调成功后再发大包,否则数据会被底层截断。这节里还对比了不同Android版本对MTU协商的差异,Android 8以后协商成功率高很多,旧版本有些设备上就只能靠L2CAP层固定缓冲。
2.4 第11-12节:HID设备和HAL接口
HID(人机接口设备)这一节主要讲蓝牙键盘、鼠标、手柄、游戏手柄的接入流程。Android把蓝牙HID归到BluetoothHidDevice和BluetoothHidHost两类:前者是让Android充当HID设备(比如手机模拟鼠标控制电脑),后者是让Android作为主机(连接蓝牙键盘)。很多电视盒子厂家头疼的遥控器适配问题,就在这里。
第12节开始进HAL。蓝牙的HAL接口在Android里经历了好几代:早期是BluetoothHAL(libbt-vendor),后来演进到HIDL化的android.hardware.bluetooth@1.1,再下一篇是AIDL化的AGHAL。HAL层做的事情包括HCI命令下发、固件加载、地址管理、功耗控制,以及一些芯片厂商自定义的私有命令透传。第12节用了一个真实的vendor实现当例子,把hal_bluetooth.h里的接口一个个对过去,看完对"怎么从Framework调到底层芯片"会有一个非常具体的认识。
2.5 第13节:实战日志分析与疑难问题排查
最后一节是综合实战:抓取一份完整蓝牙日志,从logcat、dumpsys、btsnoop逐个看,定位一个"手机连车载蓝牙放音乐卡顿"的真实问题。头一次把前面的理论全部串起来——最后发现卡顿的原因是A2DP的buffer大小和底层sco调度冲突,要在HAL层调整interleaving参数。
这13节的编排逻辑很像我之前带新人时给的路线:先让人知道整个地图长什么样,再从地图上的每一层深入,最后带他走一遍完整的排查路线。对照这套目录去读AOSP源码,你会发现自己能快速定位到具体文件和函数,而不是一头扎进几百MB源码里瞎翻。
3. 最容易绕晕的三个点:A2DP切SCO、MTU协商、HID连接
这套13节里头,我印象最深、也是出现在很多工程现场的问题,就是这三个。我分别展开,尽量把本质讲透。
3.1 A2DP切SCO:为什么通话时声音突然"退回去"了
先说现象:手机连着蓝牙音箱放歌,一切正常,突然来了个电话,音乐声停了,通话声音从音箱里传出来,但音质明显变差,像对讲机。这是正常现象,因为通话音频走的是HFP建立的SCO链路,SCO链路是窄带或宽带语音(CVSD或mSBC编码),带宽固定,跟A2DP那种高码率、可变码率的媒体传输不是一回事。
Android侧的架构里,A2DP和HFP分别是两个独立的profile服务,但它们在音频HAL层共用同一个音频通路。Framework里负责决策的是AudioPolicyManager,当电话状态变化时,它会通知BluetoothManagerService,进而触发A2DP暂停、HFP创建SCO。从logcat看,你会看到这类日志:
audio_policy: getOutputForAttr: device BT_A2DP_OUT -> BT_SCO bluetooth_audio: session switched from A2DP to SCO btif_hf: profile state changed: CONNECTED, state=7在实际排查中,最头疼的是切换不回来。常见原因有这些:
- 通话结束后
AudioPolicy没有恢复A2DP输出,表现为音乐一直在"暂停"状态 - HFP AT指令协商失败,SCO迟迟无法建立,导致手机显示"通话音频已连接"但没有声音
- 某些国产平台对SCO的采样率支持不全,mSBC 16kHz没走通,只能回落到8kHz CVSD,音质明显发闷
我之前遇到一个RK3568平台的通话蓝牙噪声问题,就是典型的本能在HAL层。平台接的是AP6275S蓝牙芯片,打电话时SCO链路通了,但声音里夹杂着周期性的爆音。从btsnoop看HCI事件没有任何异常,HFP指令也正常,最后查到是音频HAL里SCO的I2S引脚和WiFi共用了一条MCLK线,蓝牙和WiFi并发工作时的时钟抖动直接灌到SCO音频上。这个问题你只盯Bluedroid永远找不到答案,必须把视野放到HAL和平台音频路由层。
3.2 MTU协商:一次最多传20字节是谁定的
MTU在BLE里有明确的机制:链路上默认MTU是23字节(20字节有效载荷+3字节L2CAP头),Android的requestMtu()本质上是通过L2CAP的Connection Parameter Update协商,把MTU变成更大的值,最大值受控制器能力限制,通常BLE芯片能支持247。
实际开发中经常踩的坑是这些:
- 在
onConnectionStateChange回调里马上requestMtu(),但连接刚建立时底层通道还没就绪,很多设备上这个请求会失败。 - 部分外设不支持MTU协商,
onMtuChanged永远不来,你需要给个超时兜底。 - 就算MTU协商到了247,也不代表你可以在一个write请求里塞247字节——Android的
BluetoothGatt.writeCharacteristic在协商完成后可以传最多mtu-3字节,但如果特征本身设置了Write Without Response,某些外设会限制单包大小,没响应的写法容易丢包,充分沟通后还是建议做分包和应答确认。
我用过一个很直观的类比讲给团队新人听:MTU就像水管粗细,23字节是默认细细的水管,想传输大文件就要换粗水管,但换粗水管之前你不能先猛灌水,否则水会倒灌回来(数据被截断)。
3.3 HID:键盘手柄连接不上多数是配置问题
蓝牙键盘、手柄这类HID设备,Android的设备端日志里常见问题:
- Framework扫描到设备但配对弹窗不出现
- 配对成功但键值映射不对,某些按键没反应
- 连接一会儿就断开,报
BtGatt.GattService: connectionTimeout
第一类问题通常是PROFILE_CONNECTOR优先级没设置。代码里要确保HID的profile优先级高于默认,否则手柄被连接后会被判定为低优先级设备,系统在资源紧张时直接把它踢掉。
第二类问题的根源在HCI层,键值映射和led事件,调试时要打开hci snoop log,看HID Report描述符是否正确下发。有一定概率是外设的Report Descriptor本身不规范,Android解析到一半就放弃了。
第三类问题我遇到过最典型的场景是:某些游戏手柄协议栈在idle时会休眠,但Android的BluetoothHidHost不会自动重连,表现为"一放下十分钟再拿起来就没反应了"。排查时先去Settings里看蓝牙设备详情有没有"重新连接"开关,再不行就要在应用层额外做连接监控。
4. HAL层接口与Vendor适配:看懂蓝牙芯片的控制边界
HAL层是Framework和芯片固件之间的分界线,也是整套代码里最"玄"的地方。很多人问,为什么改Framework有时不解决问题,最后还得让芯片厂商出patch,答案就在HAL这层。
4.1 什么是蓝牙HAL,它和嵌入式HAL库不是一回事
先说一个常见误区。很多人搜"HAL"的时候会看到STM32的HAL库(比如hal库驱动dht11、hal库驱动oled这类),以为跟Android的HAL是一回事。英文缩写确实一样,但语境完全不同:STM32的HAL库是芯片厂商提供的硬件外设驱动函数库,Android的HAL则是系统框架与硬件驱动的中间接口层。理解了这一点,你去看hardware/libhardware/include/hardware/bluetooth.h时就不会被绕晕。
蓝牙HAL在Android代码里主要负责这些事:
- 初始化:加载固件、设置蓝牙地址、打开UART/USB/SDIO/PCIe与蓝牙控制器的物理通道
- 指令下发:把HCI命令包从Bluedroid下发到物理传输层
- 事件上报:从物理层读取Event和ACL数据上报给协议栈
- 私有命令:芯片厂商扩展的HCI vendor command,用来做校准、功耗管理、产测等
4.2 HIDL到AIDL的演进
在Android 8-10时代,蓝牙HAL走的是HIDL接口,包名是android.hardware.bluetooth@1.0和android.hardware.bluetooth@1.1,核心方法就是initialize、start、stop这几个,真正的数据通路在BluetoothHci接口里。
到了Android 11以后,谷歌开始推行AIDL化的HAL,即AGHAL(AIDL Generic HAL),把传统的hal_bluetooth.h那套C接口全面改成AIDL接口。这个改动对做BSP的人影响很大:之前直接改libbt-vendor就能实现的功能,现在要同时适配HIDL到AIDL两层。很多平台厂商的升级patch里,一半的工作量都花在这套接口迁移上。
学习这一节,建议直接打开AOSP对应目录对照看:
hardware/interfaces/bluetooth/ hardware/interfaces/bluetooth/1.0/default/ hardware/interfaces/bluetooth/1.1/default/看它的默认实现怎么把HCI包从socket搬到物理传输层,就理解了"HAL是一层壳"这句话的真义——它本身不实现蓝牙协议,只负责搬运HCI数据和加载固件。
4.3 Vendor层:芯片私有HCI和产测指令
HAL层再往下,就不是AOSP源码管得到的了,是各家芯片厂商的闭源库或者平台代码。高通那边对应bt-hci和wcnss,瑞芯微平台对应AP6275S等模组的libbt-vendor,Realtek对应rtkbt的HCI扩展,博通/Cypress对应btlpm——这些模块都藏在HAL的Vendor实现里。
实际上,当你遇到下面的问题,基本就要进入Vendor层排查:
- 蓝牙开关打不开(可能是固件加载失败)
- 功耗异常(蓝牙一直在扫描或未进入sniff模式)
- 兼容性问题(某朵耳机连不上,但其它耳机正常)
- 产测指令未实现(工厂测试的HCI vendor command返回失败)
这块没有标准文档,只能判读Vendor的日志。好在HCI层有统一的日志格式,通过hci snoop抓到的log在Wireshark里可以直接看指令和事件的对应关系。建议每个做平台移植的团队都建立一套自己的HCI vendor命令速查表,不然每次都要翻厂商的协议文档。
5. 蓝牙问题的实战排查链路:日志、btsnoop和代码定位
排查蓝牙问题,最忌讳的是一上来就翻源码。正确的顺序应该是:先确定故障现象对应的协议/功能,然后通过日志把链路逐层“点亮”,最后才去改代码。13节里最后一节讲的就是这套方法论,我把实际操作步骤拆开来说。
5.1 抓日志四件套:logcat、dumpsys、btsnoop、HCI trace
第一件是logcat。重点过滤Bluetooth*、bt_btif、btif_*、btif_hf、BTA_*、A2dp*、audio_hw_primary这些tag。
adb logcat -v threadtime | grep -E "Bluetooth|btif|BTA_|A2dp|HFP|GattService"第二件是dumpsys bluetooth_manager。这个命令能输出当前蓝牙状态、绑定设备列表、profile连接状态、以及各profile的优先级。排查"为什么自动重连不生效"的时候,这个命令很有用。
第三件是btsnoop。这是蓝牙层的“网络抓包”,最关键。Android开发者选项里默认关闭,需要手动打开:
adb shell settings put global bluetooth_btsnoop_log_quality_mode 1 adb shell settings put global bluetooth_hci_log 1打开后日志会生成在/data/misc/bluetooth/logs/btsnoop_hci.log,导出后直接用Wireshark打开,能看完整的HCI Command/Event/ACL数据。很多“连不上”“断链”“丢包”的硬件问题,在这里一眼就能看出来。
第四件是bugreport。它会把系统所有服务的状态快照打包,包含蓝牙相关的各profile状态、audio_policy的状态、射频相关状态,适合排查跨模块的诡异问题。
实际排查时,我的固定套路是:
- 先看
dumpsys bluetooth_manager,确认Profile连接状态和优先级 - 再用
logcat看Framework层的报错和流程 - 如果涉及音频,抓一份
btsnoop,看A2DP/HFP/HCI的事件是否正常 - 定位到HAL层再抓
vendor log,通常是开UART或者htcisnoop的厂商接口
5.2 结合真实案例:通话蓝牙噪声的完整定位顺序
之前遇到过一个"RK3568 + AP6275S 通话蓝牙噪声"的问题,当时的定位过程完美体现了这套链路的方法。现象是连蓝牙耳机打电话,对方听到每秒钟一次轻微爆破声。排查顺序:
第一步确认不是耳机的锅,换三副耳机都一样。第二步抓btsnoop,看SCO链路有没有重传、丢包、乱序——结果HCI层干干净净,没有retransmit。第三步去dumpsys audio和logcat看蓝牙音频HAL的路由,发现蓝牙SCO走的是平台I2S0口,而WiFi芯片也挂在I2S0附近。第四步做交叉验证,关闭WiFi后再打电话,噪声立刻消失,锁定是I2S时钟串扰。
最后在HAL配置里调整了SCO使用的时钟源和驱动强度,问题解决。这个过程如果用一句话概括:永远不要在HCI层没问题的时候就急着改Framework源码,先在日志里分清是协议层、传输层还是物理层的问题。一个可靠的排查者,最擅长的是判断"这个问题该在哪一层问"。
5.3 常见的日志误判和陷阱
有几种日志现象容易被误判:
btif_hf: state changed: CONNECTED出现不代表SCO音频已经建立成功,这只是HFP服务层的连接,后面器材需要继续看SCO/ESCO connected事件。GattService: onConnectionStateChange回调了STATE_CONNECTED,不代表MTU协商完成,要等onMtuChanged回调用完才能确定链路完全可用。A2dpSinkStreaming状态正常,但音频卡顿——不一定是蓝牙链路的问题,可能是音频HAL的resampler在作怪。logcat里看到大量的Unexpected event(比如avdtp收到不期望的包),不要急着判协议栈bug,很多国产芯片的HCI实现就是会在特定状态下发额外事件,看厂商的已知问题列表往往更快。
这些判断,都是在一次一次踩坑中积累的。13节里最宝贵的并不是知识本身,而是它把"什么现象对应什么层级"这件事讲透了。
6. 学习这套内容时的亲测经验和避坑建议
最后写几个我自己的实操体会,都是代码之外的东西。
第一,跟着目录去敲代码之前,先建立一份自己的"蓝牙接口速查表"。把BluetoothAdapter、BluetoothManagerService、btif接口、hal接口的关键函数名和文件路径列在一张表里。学习过程中每遇到一个接口就往表里填一行。到后来排查问题时,这张表比任何源码文档都管用。
第二,反复练习"从现象倒推代码路径"的思维。比如看到Google搜索词里的"蓝牙A2DP切SCO模式"、"蓝牙模块MTU"、"蓝牙HID(人机接口设备)",这些都是别人真实遇到的坑。你完全可以当面试题来训练:如果用户告诉我"手机连蓝牙音箱打电话声音断断续续",我应该先看哪份日志?先查哪个类?我习惯的做法是每人给一个场景,然后限时10分钟,要求写出从logcat到btsnoop再到HAL层的排查计划。这个方法用在带新人身上,效果明显。
第三,不要忽视硬件和嵌入式玩家发来的问题。我看到热搜词里大量出现"HC05蓝牙模块连接不上"、"hal库驱动dht11"、"hal库驱动oled"这类内容,表面上和Android Framework无关,但实际上,很多做Android系统的工程师是从嵌入式那一边转过来的,反过来,玩STM32的人对HCI、GATT、SPP的理解往往很扎实。理解模块端的行为,帮助你反过来理解Android主机端的处理方式。比如HC05模块连不上的问题,常见原因是掩码回复的模块固件默认波特率不对——这跟Android侧有什么关系?有,很多蓝牙故障的根源恰恰是物理串口/UART的波特率激励参数,在HAL层初始化时没有和模块端匹配上。
第四,掌握几个直接可复用的快速检查命令。这个值得存在笔记里:
# 查看蓝牙开关状态和profile连接 adb shell dumpsys bluetooth_manager # 重置蓝牙状态(很多时候比开关一次飞机模式更好用) adb shell svc bluetooth disable adb shell svc bluetooth enable # 抓取hci日志开关 adb shell settings put global bluetooth_btsnoop_log_quality_mode 1 # 查看当前蓝牙连接设备的详细信息 adb shell dumpsys bluetooth_manager | grep -A 30 "mConnectedDevices"第五,学习过程中一定会遇到一个微妙但致命的问题:Android版本差异。2019年那会儿的主流是Android 9/10,HIDL还是主流,但转到Android 12以后,AGHAL的AIDL接口已经全面铺开了。我现在看代码的习惯是同时打开两个分支的源码对比着看:一个选项目实际平台版本,一个选最新的AOSP master,这样既能解决眼前项目的问题,也能提前知道下一代接口要怎么迁移。
第六,这一行的核心能力其实是"建立链路感"。Framework层改一个权限、HAL层改一个参数、物理层换一根天线,最终体现在用户体验上的结果可能是同一个现象。没有链路感的人遇到问题只能猜,有链路感的人看一眼现象就知道该去哪个目录找代码。这13节,本质上就是在帮你把这条链路焊扎实。
如果你的项目正好在做车载、盒子、TWS耳机、蓝牙键盘手柄的适配,我建议按这个目录,在自己的设备上逐节去验证。源码在AOSP里都是开放的,日志工具也都在开发者选项里,剩下的就是把每一个知识点真实地动手过一遍。