ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

高通Qcom音频架构:从FE/BE到ADSP的完整链路与排障指南

高通Qcom音频架构:从FE/BE到ADSP的完整链路与排障指南 在Android BSP这个圈子里Qcom音频架构是绕不开的话题。做系统定制的、做驱动移植的、做VoIP优化的几乎天天跟“FE/BE”打交道面试问起“从FE/BE到ADSP的完整链路”也基本算是送分题里最容易翻车的。高通这套音频其实不复杂麻烦在链条太长上层App、AudioFlinger、AudioPolicy、Audio HAL、tinyalsa、ALSA core、machine驱动、q6audio驱动、ADSP固件、Codec任何一个环节掉链子最终都表现为“没声音”或者“音频卡顿”。这篇就把整条链路完整串一遍重点说清楚FE和BE在高通平台到底指什么、数据怎样通过AFE端口进入ADSP、又怎样被路由到Codec同时把我调试时最常用的命令和排障思路一并整理出来。适合正在做Audio HAL或驱动移植的人也适合想真正弄懂音频路由的新手。我尽量用实际项目里观察到的现象来讲不干巴巴贴源码。1. 先从FE/BE说起理解Qcom音频的骨架1.1 为什么是FE和BEFE的全称是Front EndBE是Back End这两个词最早是从ASoCALSA System on Chip框架里来的。放在高通平台上FE一般指ALSA暴露给上层的pcm设备比如pcmC0D0p、pcmC0D0c这种节点BE则是指向物理音频外设的链路比如某个I2S、SLIMbus、TDM或SoundWire端口。播放时数据从App进入FE经过ADSP处理后从BE出来到Codec录音时反向麦克风信号进Codec经过BE到ADSP再从FE返回上层。为什么要这样分最直接的原因是不能让每个App直接去操作Codec。Codec和物理总线是稀缺资源多个App同时要放声音总不能大家都去抢同一个I2S。高通的做法是让FE变成一个“虚拟会话入口”每个App的音频流先进入ADSP里的某个会话在DSP内部做混音、重采样、EQ、降噪等处理最后再由DSP统一从BE推送出去。这也解释了为什么Qcom平台经常有多个pcm设备pcm0p、pcm1p、pcm5p可能都是FE但它们对应不同的usecase比如低延迟播放、deep buffer播放、语音通话。打个比方FE就像是微信里的聊天窗口每个App都可以开一个窗口说话ADSP是电话局的交换机BE则是连接到具体电话线或者基站的那条物理线路。你对着窗口说的话不会直接飞到对方耳朵里而是先经过交换机路由、混音、处理再从对应的线路出去。理解了这一步后面看音频路由就不会晕。1.2 Qcom音频分层的整体脉络Qcom的音频分层从上到下大概是这样的App和Framework层App调用AudioTrack/AudioRecord数据交给audioserver进程里的AudioFlinger和AudioPolicyServiceAudioPolicy负责路由决策AudioFlinger负责调度和混音。Audio HAL层高通的开源HAL在hardware/qcom/audio目录会创建一个或多个usecase通过tinyalsa打开对应的ALSA pcm设备同时负责加载mixer_paths.xml配置路由。Kernel ALSA层使用ASoC框架machine驱动负责把各个dai link组装起来platform驱动就是高通这一套q6audio相关驱动codec驱动负责具体Codec芯片的寄存器控制。ADSP固件层AP侧通过APRAsynchronous Packet Router和DSP通信。ADSP内部有ASM、ADM、AFE三大核心模块ASM负责音频流会话ADM负责路由和拓扑AFE负责物理端口收发。一颗移动SoC里有CPU、GPU、NPU、VPU、DPU还有Audio市场上常把它们叫“几大引擎”。但Audio和GPU/DPU有个明显区别GPU主要靠AP侧算力而音频从某个版本开始越来越依赖独立ADSP。数据不是简单地从内存DMA到Codec而是先进入ADSP处理再由ADSP控制端口送出去。这带来一个好处就是AP在音频低负载时可以休眠同时DSP可以做到比AP核更稳定的实时调度。代价就是链路复杂度上来了排查问题时要同时懂AP侧和DSP侧。从App到喇叭一条完整播放路径是这样的App创建的AudioTrack把数据写到共享内存AudioFlinger混合后交给HALHAL根据usecase调用pcm_write数据进入ALSA FE设备内核驱动通过q6asm创建一个ASM会话让DSP接管数据DSP的ADM根据路由关系把流送到指定AFE端口AFE端口驱动对应的物理总线比如SLIMbus或MI2S最后Codec接收数字信号转成模拟信号推动Speaker。这条链路里的每个箭头都对应一个独立模块也对应不同的日志和调试工具。下面几节逐个拆开看。2. 从用户空间到内核关键节点逐一拆解2.1 audioserver与AudioPolicyAndroid 8之后音频核心服务集中在audioserver进程里包含AudioFlinger和AudioPolicyService。AudioPolicyService的核心职责是“路由决策”它决定当前的播放/录音请求应该走哪个设备。比如音乐播放时如果耳机插入策略就从Speaker切到WiredHeadset如果连了蓝牙耳机可能又切到A2DP。这个决策结果最终会通过HAL的start_output/start_input应用下去。决策依据是什么一是App请求的audio attributes比如stream type、usage二是当前系统里的audio devices状态。高通平台的策略实现通常在AudioPolicyManager基础上扩展比如hardware/qcom/audio的policy_hal会解析audio_policy_configuration.xml、audio_policy_engine_*.xml等配置。这些xml里大量出现mixPort、devicePort、route也就是前面说的audio端口定义。简单理解mixPort是软件侧的虚拟端口devicePort是物理设备端口route是把它们连起来的一条条路径。实际改路由配置时大多数情况不该直接改C代码而是改xml。比如新增一个USB声卡、调整某个usecase的默认输出设备多半在audio_policy_configuration.xml里加端口和route就行。有个常见坑修改了devicePort但忘了给对应的strategy添加路由结果dumpsys显示available devices是对的但AudioPolicy就是找不到匹配route最终回退到None或默认设备。检查路由配置时要把“设备已连接”和“策略能找到路径”两件事分开验证。2.2 Audio HAL与tinyalsa高通HAL最核心的一个概念是usecase。一个usecase代表一类完整的音频场景比如USECASE_AUDIO_PLAYBACK、USECASE_AUDIO_PLAYBACK_LOW_LATENCY、USECASE_AUDIO_RECORD、USECASE_VOICE_CALL等等。HAL内部维护一张usecase列表每个usecase会绑定对应的pcm设备id、设备类型、路由信息。比如播放低延迟音频时HAL会创建low latency usecase然后pcm_open一个特定的pcm节点播放普通音乐时可能走deep buffer usecasepcm节点就换成了另一个id。pcm_open这些接口来自tinyalsa库。tinyalsa是用户在ALSA基础上封装的轻量客户端它负责打开类似/dev/snd/pcmC0D0p的设备完成frame读写。高通HAL内部不用复杂的直接ioctl就是靠tinyalsa解决绝大部分数据收发。这也是为什么我们经常在HAL日志里看到pcm_name、pcm_id这些关键词。pcm设备编号在平台间差异很大不是固定不变的。同一颗芯片不同产品定制时可能会裁剪掉某些声卡导致pcm id整体偏移。HAL侧有一张pcm设备表内核侧也有自己的注册顺序两者必须对齐。改机器配置后如果出现HAL报“Failed to open pcm device”或者音频路由切换后打开错误节点优先去核对audio_platform_info.xml和内核pcm table。分享一个很实用的调试手法在HAL开启PCM数据dump。很多最新平台支持通过属性把输入输出pcm数据存成文件adb shell setprop debug.audio.pcm 1 adb shell stop adb shell start之后在/data/misc/audiorecord目录下能找到dump出来的pcm文件。播放无声时只要把dump到的文件拉出来用Audacity看波形就能判断是HAL之前的问题还是HAL之后的问题dump里没数据说明上层就没给到HALdump里有数据但喇叭不响问题就跑不了要往下看内核和Codec。这个手段比反复加log高效得多。2.3 ADSP侧的AFE端口与audio topology到了ADSP侧视角要切换成“DSP世界”的语言。AP侧通过APR协议给DSP发消息DSP内部有三大块ASM管理audio stream sessionADM管理audio routing和topologyAFE管理物理端口。AFE端口是DSP对外部数字总线的抽象比如AFE_PORT_ID_SLIMBUS_0_RX、AFE_PORT_ID_QUATERNARY_MI2S_RX。当内核侧的BE dai_link启动时q6afe驱动会向ADSP发送AFE端口配置和启动命令把采样率、位深、声道数、总线类型这些参数告诉DSP。一旦AFE port start成功DSP和Codec之间的物理链路才真正建立起来。很多“无声”问题最后定位到AFE port start失败原因是adsp子系统crash后没有重启成功。ADM干的是路由和拓扑活儿。DSP内部可以把来自ASM的多个流经过mixer合到一起再通过某个AFE端口输出也可以把一路流同时送到两个端口。拓扑里的“copp”其实就是每个端口对应的输出处理通道EQ、DRC、SRC这些效果器都挂在拓扑里。所以如果某个音效开关改了但实际听感没变化大概率是ADM里的topology实例没选对或者ACDB里对应拓扑的参数没刷进去。ACDBAudio Calibration Database也要提一句。Qcom很多音频参数比如不同采样率下的滤波器系数、设备校准值、topology选择都存在ACDB里。系统起来时adsp_loader负责把ACDB加载进ADSP。ACDB版本和HAL配置不匹配是很经典的坑表现是某些usecase能出声音、某些usecase无声或声音怪。遇到这种问题先别急着改DSP固件重新校准ACDB并确认加载配置反而更快。3. 实操链路怎么看清楚一条音频通路3.1 用dumpsys快速定位上层问题接到一个“没声音”的bug我习惯先在上层确认整个链路有没有正常建立。最常用的就是两个dumpsysadb shell dumpsys media.audio_flinger adb shell dumpsys media.audio_policyaudio_flinger的输出里要先看有没有对应的Output ThreadThread里有没有TrackTrack类型和状态是什么。比如音乐播放没声音查Track还在不在如果Track已经停止那是App侧主动停了如果Track状态正常但HAL没有数据可能是HAL start失败或者AudioFlinger的mixer线程卡死。输出的末尾一般还有underrun计数如果underrun持续上涨说明数据供给速度跟不上消费速度大概率是buffer配置或性能问题。audio_policy的输出主要看设备和路由状态。比如用蓝牙播放没声音检查ActiveOutput、Devices、DeviceId这些字段能确认policy是不是真的把设备切到了Bluetooth A2DP。如果policy还停在Speaker那是上层路由问题跟蓝牙硬件链路无关如果policy已经切到A2DP但没声音那问题就转移到HAL和蓝牙协议栈。还有一个容易被忽略的技巧dumpsys audio_flinger里能看到每个Thread的采样率、通道、format以及mixer配置。当上层协商出来的采样率和HAL/ADSP期望的不一致时会出现“有声音但变调/卡顿”的现象。先通过dumpsys确认两端参数一致再往下查能省很多时间。3.2 tinymix/tinyplay直连内核链路如果怀疑问题在HAL或更低层可以用tinyplay和tinymix绕开Android音频栈直接测试内核ALSA链路。常见的操作adb root adb push sine440.wav /data/local/tmp/ adb shell cd /data/local/tmp tinymix | head tinyplay sine440.wav -D 0 -d 5tinyplay会直接打开指定的pcm设备并写入wav文件数据。如果打开后能听到声音说明内核ALSA到Codec再到喇叭的链路基本正常如果tinymix能看到各个音频寄存器但tinyplay没声音问题很可能在路由配置或者Codec/功放控制上。但这里有个特别容易踩的坑高通平台不像普通Linux声卡打开pcm设备后数据不会自动送到某个物理端口。FE只是入口后面还要经过DSP路由才到BE。直接用tinyplay播放一个FE设备如果HAL没有设置过对应路由数据可能在DSP里没被接到任何AFE端口结果就是“播放看起来成功了但喇叭没声音”。这不代表链路坏而是你还没做路由。所以用tinyplay做测试时要先用tinymix确认把对应BE的MUX选到正确的数据源或者直接调用HAL已经拉起来的路由。tinypcminfo也有用它能列出pcm设备支持的格式、采样率范围、通道数tinypcminfo -D 0 -d 5比如想确认某个pcm设备是否支持24bit/192kHz一跑就知道。很多“格式不支持”的问题根源上就是HAL和pcm设备能力不匹配没必要去内核里翻半天。3.3 抓log与debugfs让DSP开口说话上层log和内核log的tag要先记熟。HAL层主要看audio_hw_primary、audio_hw_utils、audio_route还有acdb_loaderframework层看APM_AudioPolicyManager、AudioFlinger内核层看dmesg里和q6、apr、slim、i2s、msm_dai相关的关键词。抓现场时建议一条命令把日志带时间戳一起拉adb logcat -v threadtime -b all logcat_all.txt adb shell dmesg dmesg.txtDSP内部状态在量产机上通常拿不到工程机或debug版可以看一些debugfs节点比如/sys/kernel/debug/q6下面有apr、afe、asm相关的状态。更高阶的做法是抓subsystem ramdump当ADSP crash之后把ramdump拿回到高通工具里解析能看到DSP侧卡在哪条消息上。不过这套东西对一般调试来说太重平时先把APR消息是否正常确认了大部分问题都能定位出一半。举一个真实例子有次遇到“播放偶尔中断而且没有任何App层报错”logcat和dmesg刷了很久最后在dmesg里看到类似AFE port enable failed、APR timeout的字段。这就说明AP侧到ADSP的通信出了问题后来发现adsp子系统温度过高触发了重启动重启动过程中AFE端口全部被释放等AP侧重新拉起AFE端口时已经丢了一个session。这种问题靠HAL层怎么优化都解决不了得回到DSP负载和散热策略上处理。4. 常见问题与排障记录4.1 多应用同时录音Android 9.0上可以这么改Android系统默认情况下同一时刻只有一个普通应用能占用麦克风录音。后请求录音的应用会拿到“麦克风被占用”的拒绝结果或者干脆进入wait状态。这个是策略层在拦不是DSP能力不够。高通平台的ADSP其实支持同时打开多个录音流只是Android默认策略不允许共用输入设备。如果想在Android 9.0上支持多个应用同时录音大致思路是在AudioPolicyManager里放宽输入流的独占约束。默认逻辑是在getInputForAttr里找到一个合法的input descriptor如果这个input已经在active通常就直接拒绝新请求。要做并发录音就要改成允许新的App挂载到同一个input流上让AudioFlinger的RecordThread同时为多个RecordTrack提供数据。这种方案要求各个App的采样率、通道、格式尽量一致不一致还得加一路重采样复杂度会明显上升。高风险点是路由切换。如果其中某个App要求切换麦克风设备整个输入流的路由都会跟着变其他App的录音数据也会被带过去。所以并发录音的定制项目里不建议开放太灵活的设备切换能力。并且要记得重新过CTS相关用例尤其是testConcurrentCapture这类测试不然很容易在系统升级后暴露兼容性问题。这个改动属于深度定制不是通用补丁做之前要评估好产品场景确实需要并发录音。4.2 无声、杂音、单侧声道的常规排查顺序无声问题要先确认路径方向是播放、录音还是通话是所有场景都无声还是只有特定usecase无声这两问能砍掉一半排查面。播放无声我一般按这个顺序来先dumpsys确认Track和路由再检查HAL有没有打开正确的pcm设备然后看tinymix里相关MUX和Codec寄存器是否配到了预期值最后查功放使能GPIO和硬件连接。每一步都找一个能证明链路通断的日志或值而不是反复试。杂音问题要先区分是偶发还是持续。持续杂音多半是采样率不匹配、DSP某段SRC配置不对、或地线/供电问题偶发杂音优先怀疑underrun也就是buffer饥饿。看underrun可以直接在dumpsys audio_flinger里搜索underrun字段如果数值在播放过程中持续增长说明数据供不上需要调大buffer或优化CPU/DSP调度。单侧声道问题倒是比较有意思。遇到“左边没声音/右边没声音”先用单声道正弦波测试文件播放分别确认左右声道在原始wav里是否都有数据。如果源文件本身没问题再看DSP拓扑里channel mapping是否配置正确比如某些平台用AIF1对应Left、AIF2对应Right配置反了会导致声道互换或单侧无声。最后再查Codec到功放的硬件链路。多数情况下这种问题不是硬件坏了而是链路某一层把左右声道搞错位了。4.3 低延迟与系统性能buffer不是越小越好低延迟是音频项目里最常被提起的性能指标。Qcom平台处理低延迟的思路是给不同场景分配不同pcm设备低延迟场景走专门的low latency pcm普通音乐走deep buffer。low latency pcm的buffer申请得比较小数据路径也尽量短代价是对系统调度更敏感deep buffer pcm数据路径长、延迟大但对实时性要求低可以更好地省电。延迟可以由buffer大小简单估算。比如采样率48000Hzperiod设为192帧那么每个period对应的时间就是192/480004ms。如果整个链路有2个period的buffer至少就有8ms的基础延迟还要再加上ADSP内部处理、Codec内部FIFO、总线传输等额外延迟。所以不要盲目追求极小的buffer size太小的话一次CPU调度不及时就会underrun产生爆音或卡顿。检查低延迟情况下的性能问题可以用adb shell dumpsys media.audio_flinger | grep -i underrun adb shell cat /proc/asound/card0/pcm0p/sub0/hw_paramshw_params里能看到当前实际使用的period_size、buffer_size、rate。如果发现allowed和actual不一致就要回头查HAL是否真的按预期配置到了设备端。我在项目里的体会是低延迟设计最优先考虑“稳定”而不是“数字最小”。有一次做K歌场景把period压到2ms本地测试延迟确实漂亮但一跑到高负载压力测试underrun次数肉眼可见地涨。后来把period调到4ms感官延迟只多了不到2ms稳定性却完全不一样。做性能优化必须先有衡量指标不然为了纸面延迟牺牲稳定性最终伤害的还是用户体验。最后说一个我自己的调试习惯处理音频问题多了之后我现在接bug第一时间不急着看代码而是先问自己这条链路的哪个环节还没有证据证明它是正常的通过dumpsys确认策略层通过HAL log确认usecase和pcm设备通过tinymix确认路由和Codec寄存器通过AFE日志确认ADSP端口状态一层层找那个“没有证据”的地方。绝大多数问题都出在还没被验证到的环节里而不是大家一开始怀疑的那个环节。这套方法在Qcom上管用换到其他平台也同样适用因为音频链路本质上都逃不开“虚拟端点—DSP处理—物理端口—Codec”这条主线。
返回列表