RK3576 Android14 hfpclient 蓝牙车机通话适配

基于Purple_Pi_OH2开发板

前言

HFP(Hands - Free Profile)是蓝牙技术联盟(Bluetooth SIG)定义的一种蓝牙协议,用于实现免提通信功能。HFP Client(HFP 客户端)则是遵循 HFP 协议的设备或软件组件,它可以与支持 HFP 的蓝牙设备(如车载蓝牙免提系统、蓝牙耳机等)进行通信,以实现免提通话等功能。

蓝牙 HFP 协议规定,通话双方必须有两个角色

  • AG(音频网关,Audio Gateway)就是你的手机。它是音频数据的“源头”和“控制中心”(拨号、挂断、调节音量都在这里)。

  • HF(免提设备,Hands-Free)就是你的蓝牙耳机或车载中控。它是音频数据的“终点”和“执行者”(播放对方声音、拾取你的说话声)。

严格来说,“HFP Client(客户端)”指的就是上面的 HF(免提设备),也就是你的蓝牙耳机/车载机器里的那个软件协议栈

一、硬件连接

硬件需要适配支持蓝牙PCM,蓝牙模组的PCM接口,连接主控I2S。

要实现RK3576与蓝牙模块间的 HFP 通话,硬件上必须连接 RK3576 的 SAI 接口(配置为 PCM 从/主模式)与蓝牙模块的 PCM 音频接口,同时需保证UART 控制通道电源使能以及时钟源(MCLK)的正确供给。

RK3576 SAI 接口 (I2S/PCM 模式)蓝牙模块 PCM/I2S 接口信号说明
SAI_SCLK(串行时钟)PCM_CLK / I2S_SCK位时钟,由主设备提供
SAI_FS(帧同步/字选择)PCM_SYNC / I2S_WS帧同步信号或左右声道选择
SAI_SDI(串行数据输入)PCM_DO / I2S_DO数据输出:RK3576 从蓝牙模块接收数据
SAI_SDO(串行数据输出)PCM_DI / I2S_DI数据输入:RK3576 向蓝牙模块发送数据

PCM(脉冲编码调制),你可以把它理解为蓝牙芯片和主控之间“约定好的对话规则”

  • 它是数字音频的原始裸数据格式(未压缩的)。

  • 蓝牙耳机通话时,麦克风录到的是模拟信号,蓝牙芯片会把它转成 PCM 数字信号,然后通过几根电线发给主控(RK3576)。

  • 关键点:PCM 是 HFP 通话的专用通道,走的是实时语音流,跟听歌的 I2S 不一样(I2S 是给高保真音乐用的)。

SAI(串行音频接口),你可以把它理解为RK3576 芯片内部的一个“硬件耳朵”

  • 它是 Rockchip 主控里专门用来接收/发送数字音频的物理模块

  • SAI可以配置成多种模式,比如 I2S 模式(听歌)、PCM 模式(通话)、TDM 模式(多通道)。

  • 你设备树里写的&sai2,就是 RK3576 上的第 2 个 SAI 控制器

  • PCM:你不用管它是什么芯片,你只需要知道蓝牙模块上有 4 个引脚分别叫 PCM_CLK、PCM_SYNC、PCM_DI、PCM_DO

  • SAI:你只需要在 RK3576 的原理图上找到SAI2_M0 那 4 个引脚(SCLK、LRCK、SDI、SDO)

  • 你的工作:把这 4 对引脚用 PCB 走线或飞线一对一焊起来,这就是硬件连接。

二、dts配置

蓝牙 HFP(免提协议)通话的音频路径 ,用于实现蓝牙电话功能。整个配置分为三个核心部分:

/ { /* ... 你原有的其他节点 ... */ bt_sco: bt-sco { status = "okay"; compatible = "delta,dfbmcs320"; #sound-dai-cells = <1>; }; bt_sound: bt-sound { status = "okay"; compatible = "simple-audio-card"; simple-audio-card,format = "dsp_a"; simple-audio-card,bitclock-inversion; simple-audio-card,mclk-fs = <256>; simple-audio-card,name = "rockchip,bt"; simple-audio-card,cpu { sound-dai = <&sai2>; }; simple-audio-card,codec { sound-dai = <&bt_sco 1>; }; }; }; &sai2 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&sai2m0_lrck &sai2m0_sclk &sai2m0_sdi &sai2m0_sdo>; };

2.1、架构总览

┌─────────────────────────────────────────────────────────────┐ │ Android 系统 │ │ 蓝牙协议栈 (BT Stack) ←→ TinyALSA ←→ Audio HAL │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Linux 内核 │ │ bt_sound (simple-audio-card) │ │ ┌──────────┴──────────┐ │ │ ▼ ▼ │ │ CPU端 (SAI2) 编解码器端 (bt_sco) │ │ (I2S 接口) (delta,dfbmcs320) │ └─────────────────────────────────────────────────────────────┘ │ ▼ 蓝牙芯片 (SCO 链路)

2.2、第一部分: bt_sco 节点(编解码器端)

bt_sco: bt-sco { status = "okay"; compatible = "delta,dfbmcs320"; #sound-dai-cells = <1>; };

2.3、第二部分: bt_sound 节点(音频卡)

bt_sound: bt-sound { status = "okay"; compatible = "simple-audio-card"; simple-audio-card,format = "dsp_a"; simple-audio-card,bitclock-inversion; simple-audio-card,mclk-fs = <256>; simple-audio-card,name = "rockchip,bt"; simple-audio-card,cpu { sound-dai = <&sai2>; }; simple-audio-card,codec { sound-dai = <&bt_sco 1>; }; };

CPU 端和编解码器端

simple-audio-card,cpu { sound-dai = <&sai2>; // 连接到 CPU 的 SAI2 接口 }; simple-audio-card,codec { sound-dai = <&bt_sco 1>; // 连接到 bt_sco 的第 1 个 DAI };
  • - CPU 端 :使用 &sai2 ,即 Rockchip 芯片的 SAI2(Serial Audio Interface),作为 I2S 主设备
  • - 编解码器端 :使用 &bt_sco 1 ,即蓝牙 SCO 编解码器的第 1 个 DAI 接口( #sound-dai-cells = <1> 定义了需要索引)

2.4、第三部分: &sai2 节点(CPU 音频接口)

&sai2 { //启用 CPU 端的 SAI2 接口 status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&sai2m0_lrck &sai2m0_sclk &sai2m0_sdi &sai2m0_sdo>; };

2.5、配置原理总结

  • bt_sco :定义蓝牙 SCO 编解码器,作为音频数据的终点/起点
  • bt_sound :使用 simple-audio-card 框架,建立 CPU(SAI2)和编解码器(bt_sco)之间的音频链路
  • &sai2 :启用 CPU 端的 SAI2 接口,并配置正确的引脚
  • 这样配置后,Android 系统就能通过 TinyALSA 访问名为 rockchip,bt 的音频设备,实现蓝牙通话的音频输入输出。

三、系统配置

3.1 设置主板为A2DP.sink 和avrcp.controller

A2DP(Advanced Audio Distribution Profile)是一种重要的协议,用于在两个蓝牙设备之间传输高质量的立体声音频。A2DP 中有 Sink(接收器)和 Source(源)两种角色。
设置主板作为音频数据接收者,它通过蓝牙接收 A2DP Source 发送过来的编码音频数据,然后对其进行解码,将解码后的音频信号转换为模拟音频信号,最终通过音频输出设备(如扬声器)播放出来。

AVRCP(Audio/Video Remote Control Profile)音频 / 视频远程控制配置文件,它定义了蓝牙设备之间如何进行音频和视频播放的远程控制。一个支持 AVRCP 的设备可以作为控制器(Controller)向目标设备(Target)发送控制命令,如播放、暂停、切换曲目等。
主板端配置为AVRCP.controller控制端,用于控制连接的手机播放音乐。

--- a/device/rockchip/common/modules/bluetooth.mk +++ b/device/rockchip/common/modules/bluetooth.mk @@ -30,11 +30,14 @@ PRODUCT_PACKAGES += \ # Set supported Bluetooth profiles to enabled PRODUCT_PRODUCT_PROPERTIES += \ bluetooth.profile.asha.central.enabled?=true \ - bluetooth.profile.a2dp.source.enabled?=true \ - bluetooth.profile.avrcp.target.enabled?=true \ + bluetooth.profile.a2dp.source.enabled?=false \ + bluetooth.profile.avrcp.controller.enabled?=true \ + bluetooth.profile.avrcp.target.enabled?=false \ + bluetooth.profile.a2dp.sink.enabled?=true \ bluetooth.profile.bas.client.enabled?=true \

3.2 配置a2dp_sink 自动聚焦

packages/modules/Bluetooth/android/app/res/values/config.xml

<!-- If true, device requests audio focus and start avrcp updates on source start or play --> <bool name="a2dp_sink_automatically_request_audio_focus">true</bool>

当 A2DP Sink 设备(如蓝牙耳机或音箱)连接到 Android 设备并开始接收音频流时,系统会自动为该 A2DP Sink 设备请求音频焦点。这意味着系统会暂停当前正在播放音频的其他应用程序,确保 A2DP Sink 设备能够正常播放接收到的蓝牙音频。例如,当你将蓝牙耳机连接到手机并开始通过蓝牙播放音乐时,如果手机上原本有其他应用(如音乐播放器或视频应用)正在播放音频,这些应用会自动暂停,蓝牙耳机开始播放音乐。

默认修改为BTIF_A2DP_SINK_FOCUS_GRANTED,连接后默认认为获取到焦点,开始尝试播放音乐。

--- a/packages/modules/Bluetooth/system/btif/src/btif_a2dp_sink.cc +++ b/packages/modules/Bluetooth/system/btif/src/btif_a2dp_sink.cc @@ -93,7 +93,7 @@ class BtifA2dpSinkControlBlock { decode_alarm(nullptr), sample_rate(0), channel_count(0), - rx_focus_state(BTIF_A2DP_SINK_FOCUS_NOT_GRANTED), + rx_focus_state(BTIF_A2DP_SINK_FOCUS_GRANTED), audio_track(nullptr), decoder_interface(nullptr) {} @@ -108,7 +108,7 @@ class BtifA2dpSinkControlBlock { alarm_free(decode_alarm); decode_alarm = nullptr; rx_flush = false; - rx_focus_state = BTIF_A2DP_SINK_FOCUS_NOT_GRANTED; + rx_focus_state = BTIF_A2DP_SINK_FOCUS_GRANTED; sample_rate = 0; channel_count = 0; decoder_interface = nullptr; @@ -552,7 +552,7 @@ static void btif_a2dp_sink_avk_handle_timer() { if (btif_a2dp_sink_cb.rx_focus_state == BTIF_A2DP_SINK_FOCUS_NOT_GRANTED) { APPL_TRACE_DEBUG("%s: skipping frames since focus is not present", __func__); - return; + //return; } /* Play only in BTIF_A2DP_SINK_FOCUS_GRANTED case */ if (btif_a2dp_sink_cb.rx_flush) {

3.3 设置蓝牙设备类型

BTA_DM_COD 修改设置为 {0x5A, 0x04, 0x06} // 手机端搜索到蓝牙显示带耳麦的耳机图标

0x5A 服务类

0x04 主设备类,代表音频 / 视频设备

0x06 次设备类,表示特定类型的音频设备,像耳机或者扬声器。

#define BTA_AV_SINK_INCLUDED TRUE //配置为A2DP Sink设备

#define ESCO_DATA_PATH_PCM 6

博通方案中, ESCO_DATA_PATH_PCM 为1代表蓝牙芯片作为pcm master, 6作为pcm slave.

--- a/device/rockchip/rk3562/bluetooth/bdroid_buildcfg.h +++ b/device/rockchip/rk3562/bluetooth/bdroid_buildcfg.h @@ -18,7 +18,10 @@ #define _BDROID_BUILDCFG_H #define BTM_DEF_LOCAL_NAME "rk3562" -#define BTA_DM_COD {0x1A, 0x01, 0x10} +#define BTA_DM_COD {0x5A, 0x04, 0x06} +#define BTA_AV_SINK_INCLUDED TRUE + +#define ESCO_DATA_PATH_PCM 6

3.4 设置蓝牙音频支持8/16KHz采样率

--- a/kernel-5.10/sound/soc/codecs/bt-sco.c +++ b/kernel-5.10/sound/soc/codecs/bt-sco.c @@ -27,14 +27,14 @@ static struct snd_soc_dai_driver bt_sco_dai[] = { .stream_name = "Playback", .channels_min = 1, .channels_max = 2, - .rates = SNDRV_PCM_RATE_8000, + .rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000, .formats = SNDRV_PCM_FMTBIT_S16_LE, }, .capture = { .stream_name = "Capture", .channels_min = 1, .channels_max = 2, - .rates = SNDRV_PCM_RATE_8000, + .rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000, .formats = SNDRV_PCM_FMTBIT_S16_LE, }, },

3.5 配置hfp角色为hf

- bluetooth.profile.hfp.ag.enabled?=true \ + bluetooth.profile.hfp.ag.enabled?=false \ + bluetooth.profile.hfp.hf.enabled?=true \

HFP(Hands - Free Profile)即蓝牙免提配置文件,它定义了蓝牙设备间实现免提通话功能的通信标准。HFP 中有两个关键角色:HFP HF(Hands - Free Unit)和 HFP AG(Hands - Free Audio Gateway)

--- a/overlay/frameworks/base/core/res/res/values/config.xml +++ b/overlay/frameworks/base/core/res/res/values/config.xml @@ -20,7 +20,7 @@ <resources> <!-- This device is not "voice capable"; it's data-only. --> - <bool name="config_voice_capable">false</bool> + <bool name="config_voice_capable">true</bool> <!-- This device does not allow sms service. --> <bool name="config_sms_capable">false</bool>