Vol.05 VendorHAL-NXP

AIDL Audio HAL:厂商实现,关键接口与核心机制

基于 Android 16 / NXP i.MX 平台音频栈源码(hardware/interfaces/audio/aidl + vendor/nxp-opensource/imx/audio/default

从 Android 13 起,Audio HAL 从 HIDL 迁移到 AIDL。这不只是 IDL 语法的替换,而是 数据面与控制面彻底分离 的一次重构:控制走 Binder,PCM 数据走 FMQ(Fast Message Queue)。本文从厂商实现的视角,讲清三件事------AIDL 的关键接口、PCM 数据流转的核心机制,以及一个真实厂商(NXP)是怎么把这套接口落地的。


一、整体架构:接口树与进程模型

AIDL Audio HAL 的接口定义在 android/hardware/audio/core/ 下,核心是一棵以 IModule 为根的树:

scss 复制代码
IConfig                     // 全局配置 / 效果反射(独立 service)
IModule                     // 一个音频模块(= 一块声卡 / 一类设备)
 ├── IStreamOut  ── IStreamCommon   // 播放流
 ├── IStreamIn   ── IStreamCommon   // 录制流
 ├── ITelephony                     // 通话
 ├── IBluetooth / IBluetoothA2dp / IBluetoothLe
 └── (AudioPort / AudioRoute / AudioPatch 作为数据结构)

一个 IModule 实例对应 audio policy 里的一个 module,按设备类型拆分。NXP 实际注册的实例:

bash 复制代码
android.hardware.audio.core.IModule/default     -> 板载 codec(ModulePrimary)
android.hardware.audio.core.IModule/r_submix    -> Remote Submix
android.hardware.audio.core.IModule/usb         -> USB Audio
android.hardware.audio.core.IModule/bluetooth   -> BT
android.hardware.audio.core.IConfig/default     -> 全局配置

整个 HAL 跑在一个进程 vendor.audio-hal-aidl 里(打包进 audio APEX),实时优先级运行(rlimit rtprio 10 10ioprio rt 4)。


二、厂商落地的范式:fork + 继承,而非从零实现

这是 AIDL HAL 和 HIDL 时代最大的工程差异。AOSP 在 hardware/interfaces/audio/aidl/default 提供了一份功能完整的参考实现ModuleStream、状态机、FMQ 框架全都写好了)。AOSP 推荐的厂商模式是:把这份参考实现 fork 进 vendor 库,然后用继承去覆写硬件相关的钩子方法 ,而不是直接实现 BnModule

NXP 正是这么做的。它把源码 fork 成 libaudioserviceimximpl,继承链如下:

scss 复制代码
BnModule            (由 IModule.aidl 自动生成)
  └── Module        (AOSP 参考实现,含全部通用逻辑)
        └── ModulePrimary / ModuleUsb / ModuleBluetooth / ModuleRemoteSubmix / ModuleStub

基类 Module.h 里把这个扩展模型写得很明白:

"The vendor extension done via inheritance can override interface methods and augment a call to the base implementation."

也就是说,厂商绝大多数接口(getAudioPortssetAudioPatchopenOutputStream 的骨架......)直接复用基类,只覆写真正和硬件强相关的几个 protected 虚函数。对 NXP 来说,硬件特性几乎全部集中在:

  • createOutputStream / createInputStream------根据 flag/format 选择具体 Stream 子类
  • populateConnectedDevicePort------HDMI 热插拔、动态 profile 协商
  • getMmapPolicyInfos / getNominalLatencyMs / calculateBufferSizeFrames------平台参数

加上一个 AudioCardManager 做 ALSA 声卡发现的胶水代码。这就是「厂商如何实现 AIDL」的答案核心:选择性覆写,而非全盘重写。

service 注册:数据驱动 + 普通 binder service

main.cpp 没有用 lazy service,而是建 16 线程的 binder 池、addService 后 join 阻塞:

cpp 复制代码
ABinderProcess_setThreadPoolMaxThreadCount(16);
ABinderProcess_startThreadPool();
// ... 解析 audio_policy_configuration.xml,按 (name, config) 创建 Module ...
ABinderProcess_joinThreadPool();

关键点是 module 实例不是硬编码的,而是由 XML 驱动main() 解析 audio_policy_configuration.xml,对每个 module 名字调用工厂:

cpp 复制代码
// name("default"/"usb"/...) -> Type -> 具体子类
std::shared_ptr<Module> Module::createInstance(Type type, ...) {
    switch (type) {
        case Type::DEFAULT:   return SharedRefBase::make<ModulePrimary>(...);
        case Type::USB:       return SharedRefBase::make<ModuleUsb>(...);
        case Type::BLUETOOTH: return SharedRefBase::make<ModuleBluetooth>(...);
        // ...
    }
}
// 实例名 = descriptor + "/" + name
const std::string moduleFqn = std::string(Module::descriptor).append("/").append(name);
AServiceManager_addService(result.getBinder(), moduleFqn.c_str());

VINTF manifest 声明 android.hardware.audio.core version 3,列出所有可能的实例(是个超集,实际注册哪些取决于 XML 配置)。


三、关键接口逐个看

IModule ------ 模块的中枢

IModule 管理一块声卡的拓扑、路由和流的生命周期。最重要的几组方法:

拓扑与路由(静态)

aidl 复制代码
AudioPort[]  getAudioPorts();                       // 所有端口(device port + mix port)
AudioRoute[] getAudioRoutes();                       // 端口间可达关系
AudioPort    connectExternalDevice(in AudioPort templateIdAndAdditionalData);
void         disconnectExternalDevice(int portId);

connectExternalDevice 是动态设备(HDMI、USB headset)接入的入口:它以一个 template device port 为模板,填入客户端给的设备地址,分配新的 portId,再调用可覆写的 populateConnectedDevicePort 去探测动态 profile(采样率/格式/声道)。NXP 在这里打开 ALSA mixer 读 HDMI EDID。

配置与连接(动态)

aidl 复制代码
boolean   setAudioPortConfig(in AudioPortConfig requested, out AudioPortConfig suggested);
AudioPatch setAudioPatch(in AudioPatch requested);   // 建立 source->sink 的数据通路
void      resetAudioPatch(int patchId);

AudioPatch 是 AIDL 路由模型的核心:它把 source port config 和 sink port config 连起来,等价于「把这条流接到这个设备上」。

开流(控制面与数据面的交汇点)

aidl 复制代码
OpenOutputStreamReturn openOutputStream(in OpenOutputStreamArguments args);
OpenInputStreamReturn  openInputStream(in OpenInputStreamArguments args);

返回值里既有 IStreamOut/IStreamIn binder 句柄,也有 StreamDescriptor------后者携带了三个 FMQ 的描述符。开流这一刻,控制走 binder,数据通道(FMQ)一次性建好,后续 PCM 传输就不再经过 binder 了。

全局控制setMasterVolume/MutesetMicMuteupdateAudioMode(通话/铃声/普通)、updateScreenRotationgenerateHwAvSyncId(隧道模式音视频同步)、setVendorParameters(厂商私有 KV 参数)。

IStreamOut / IStreamIn / IStreamCommon

开流后真正用于流控的接口。IStreamCommon 是共享部分:

aidl 复制代码
interface IStreamCommon {
    void close();                                  // 释放 FMQ 与共享内存
    void prepareToClose();
    void updateHwAvSyncId(int hwAvSyncId);
    VendorParameter[] getVendorParameters(in String[] ids);
    void setVendorParameters(in VendorParameter[] params, boolean async);
    void addEffect(in IEffect effect);             // offload 效果
    void removeEffect(in IEffect effect);
}

注意一个约定:IStreamOut.getStreamCommon() 必须每次返回同一个实例

IStreamOut 额外有 updateMetadata(用途元数据)、updateOffloadMetadata(压缩 offload)、get/setHwVolume(按声道,[0.0, 1.0],不支持就返回 EX_UNSUPPORTED_OPERATION)、dual-mono、playback rate、selectPresentation(NGA 多节目流)。

真正的 PCM 读写不在这些方法里------它在 FMQ 上。这就引出核心机制。


四、核心机制:StreamDescriptor 与三条 FMQ

为什么要 FMQ

HIDL 时代每次 write()/read() 都是一次 binder 调用,传一个 buffer。对音频这种高频、低延迟、周期性的负载,binder 的开销和不确定性是硬伤。AIDL 的解法:开流时建立一组 Fast Message Queue(基于共享内存的无锁环形队列),之后 PCM 数据和流控命令都走 FMQ,binder 线程只负责开关流这种低频操作。

StreamDescriptor 这个 parcelable 就是数据通道的「钥匙」,在 openOutputStream 时随 binder 句柄一起交给客户端:

aidl 复制代码
parcelable StreamDescriptor {
    MQDescriptor<Command, SynchronizedReadWrite> command;   // client 写, HAL 读
    MQDescriptor<Reply,   SynchronizedReadWrite> reply;     // HAL 写,   client 读
    // AudioBuffer audio:
    MQDescriptor<byte, SynchronizedReadWrite> fmq;          // PCM 数据队列
    MmapBufferDescriptor mmap;                               // MMAP NOIRQ 模式的共享 buffer
    int  frameSizeBytes;
    long bufferSizeFrames;
}

三条队列:

  • command:客户端 → HAL 的命令队列(容量 1,严格 lockstep)
  • reply:HAL → 客户端的回复队列(容量 1)
  • audio.fmq:真正搬运 PCM 的数据队列

三条都是 SynchronizedReadWrite(单读单写、阻塞)。command/reply 各只存 1 个元素,强制请求/响应一一对应。

状态机:流在 HAL I/O 线程里跑

StreamDescriptor.State@Backing(type="int"))定义了流的全部状态:

状态 含义
1 STANDBY 开流初始态,硬件可掉电
2 IDLE 硬件已激活。输出:缓冲空,硬件输出静音;输入:缓冲填充,绕回则覆盖。不计 xrun
3 ACTIVE 正常周期性 I/O,客户端迟到计为 xrun
4 PAUSED 消费方停止;输入缓冲满则停(不覆盖),输出发静音
5 DRAINING 生产方停,消费方排空缓冲;瞬时态,排空后自动跳转
6 DRAIN_PAUSED 仅输出,暂停排空
7 TRANSFERRING 仅异步模式,回复 burst 后提前解阻塞客户端;瞬时态
8 TRANSFER_PAUSED 仅输出异步,TRANSFERRING 中 pause
100 ERROR 不可恢复错误,只能 close

关键原则(写在 StreamDescriptor.aidl 头部):HAL 起一个专用高优先级 I/O 线程,独占状态机和所有队列访问,把整个模型抽象成「生产者 ↔ 环形缓冲 ↔ 消费者」(输出:client 生产 / 硬件消费;输入:反过来)。

Command 与 Reply 的字段

aidl 复制代码
@FixedSize union Command {
    int  halReservedExit;   // HAL 内部专用,解阻塞 I/O 线程退出,client 不能发
    Void getStatus;         // 任意状态可用,只读位置/状态
    Void start;
    int  burst;             // 参数=请求字节数;输出=要从 fmq 消费的字节,输入=要读入的字节
    DrainMode drain;        // DRAIN_ALL / DRAIN_EARLY_NOTIFY(gapless)
    Void standby;
    Void pause;
    Void flush;
}

@FixedSize parcelable Reply {
    int      status;        // OK / BAD_VALUE / INVALID_OPERATION / NOT_ENOUGH_DATA
    int      fmqByteCount;  // 实际消费/提供的字节,不超过命令请求值
    Position observable;    // 呈现/采集位置,HAL 永不重置(连续流抽象)
    Position hardware;      // 仅 MMAP:硬件读写位置
    int      latencyMs;     // 当前硬件延迟,-1 = 未知
    int      xrunFrames;    // 上一次传输丢失的帧
    State    state;         // 写回复时流所处的状态
}

burst 握手:整个机制的心跳

阻塞模式输出为例,一次数据传输是 command → audio.fmq → reply 的乒乓:

  1. 客户端把 PCM 写进 audio.fmq
  2. 客户端往 command 写一个 burst,然后阻塞在 reply
  3. HAL I/O 线程被唤醒,从 audio.fmq 读数据交给硬件,把位置/延迟/状态写进 reply,然后阻塞回 command
  4. 客户端被 reply 唤醒,读到本次进度

输入 对称:客户端发 burst 阻塞在 reply,HAL 填 audio.fmq 并把字节数写进 reply.fmqByteCount,客户端醒来后必须把 FMQ 里的数据全部取走。

异步(非阻塞)模式 打破这个 lockstep:HAL 回复 burst 时进入 TRANSFERRING 而非 ACTIVE,提前放客户端走;客户端要等 IStreamCallback.onTransferReady 这个 oneway 回调才能发下一个 burst。

MMAP NOIRQ 模式 则完全不走数据队列:PCM 通过 mmap 共享 buffer 直接和 DSP 交换,burst 参数恒为 0,只用来取位置和延迟。


五、厂商侧的数据流转:从 FMQ 到 tinyalsa

AOSP 参考实现把上面这套 FMQ + 状态机做成了通用框架,厂商只需要填「真正碰硬件」的那一层。NXP 的实现分两层线程:

arduino 复制代码
AudioFlinger (client) --command/data/reply FMQ--> StreamWorker 线程 (cycle())
                                                       │
                                            DriverInterface (init/start/transfer/
                                            drain/pause/flush/standby/shutdown)
                                                       │
                            StreamPrimary (设备仲裁 + 重采样 + 声道转换)
                                                       │
                            StreamAlsa::transfer  -->  alsa::DeviceProxy
                                                       │
                                          tinyalsa pcm_writei / pcm_readi

第一层:每条流一个 worker 线程

每条流有一个专用 worker 线程(输出名 "writer",输入名 "reader"),循环执行 cycle():阻塞读一条命令 → 按 tag 分发 → 阻塞写一条回复。所有状态转换只在这个线程上发生,binder 线程只是把命令塞进队列。

cpp 复制代码
StreamDescriptor::Command command{};
mContext->getCommandMQ()->readBlocking(&command, 1);
switch (command.getTag()) {
    case Tag::start:   mDriver->start();   break;  // STANDBY->IDLE
    case Tag::burst:   isMmap() ? readMmap(&reply) : read(fmqByteCount, &reply); break;
    case Tag::drain:   mDriver->drain(mode); break;
    case Tag::standby: mDriver->standby(); break;  // IDLE->STANDBY
    case Tag::pause:   mDriver->pause();  break;
    case Tag::flush:   mDriver->flush();  break;
}
reply.state = mState;
mContext->getReplyMQ()->writeBlocking(&reply, 1);

burst 是 PCM 真正流动的地方。输出方向:从 data FMQ 读进 mDataBuffer → 交给 mDriver->transfer() → 推进帧计数:

cpp 复制代码
const size_t readByteCount = dataMQ->availableToRead();
dataMQ->read(&mDataBuffer[0], readByteCount);
size_t byteCount = std::min({clientSize, readByteCount, mDataBufferSize});
mDriver->transfer(mDataBuffer.get(), byteCount / frameSize, &actualFrameCount, &latency);
reply->fmqByteCount += actualFrameCount * frameSize;
mContext->advanceFrameCount(actualFrameCount);

设备断开时,两个方向都用 usleep 模拟时序(输入额外清零),让状态机和帧计数器照常推进------这保证了拔设备不会卡死流。

第二层:DriverInterface 是硬件抽象的缝

worker 线程不直接碰硬件,而是调用 DriverInterface 这个纯虚接口:

This interface provides operations of the stream which are executed on the worker thread.

cpp 复制代码
init(DriverCallbackInterface*) / start() / transfer(buf, frames, *actualFrames, *latencyMs)
/ drain(mode) / pause() / flush() / standby() / shutdown()
/ refinePosition()           // 填 reply 里的 observable 位置

StreamAlsa 是它的 ALSA 实现(class StreamAlsa : public StreamCommonImpl, public DriverInterface),transfer() 最终落到 tinyalsa:

cpp 复制代码
const size_t bytesToTransfer = frameCount * mFrameSizeBytes;
if (mIsInput) {
    proxy_read_with_retries(mAlsaDeviceProxies[0].get(), buffer, bytesToTransfer, ...);
} else {
    alsa::applyGain(buffer, mGain, bytesToTransfer, mConfig->format, mConfig->channels);
    for (auto& proxy : mAlsaDeviceProxies) {     // 一份数据扇出到多个输出设备
        proxy_write_with_retries(proxy.get(), buffer, bytesToTransfer, ...);
    }
}

proxy_write/read_with_retries 包的就是 tinyalsa 的 pcm_writei / pcm_readi,对应内核 ASoC 的 DMA ioctl。

StreamPrimary 夹在中间做平台适配:设备仲裁、mono↔stereo 声道转换、mResampler 软件重采样,再调 StreamAlsa::transfer。NXP 的 ModulePrimary::createOutputStream 在这里做关键路由决策:mmap flag → StreamOutMmap;压缩 offload → StreamOutOffloadStub;MP3 且声卡名含 "sof"(SOF DSP 能解码)→ StreamOutCompress;否则 → StreamOutPrimary


六、总结:理解 AIDL Audio HAL 的三把钥匙

  1. 控制面/数据面分离:binder 只管开关流和全局控制;PCM 走 FMQ 共享内存。这是相对 HIDL 的根本变化,延迟和确定性都大幅改善。

  2. StreamDescriptor 三队列 + 状态机 :command/reply lockstep 跑控制,audio.fmq 搬数据,一个 HAL 专用高优先级线程独占状态机。burst 握手是整个机制的心跳,异步模式用 TRANSFERRING + onTransferReady 解耦。

  3. 厂商靠继承而非重写 :AOSP 给了完整参考实现,厂商 fork 后只覆写 createOutputStreampopulateConnectedDevicePort 等少数硬件钩子,再用一层 DriverInterface 把通用 FMQ 框架桥到 tinyalsa/内核 ASoC。NXP 的 Module → ModulePrimaryDriverInterface → StreamAlsa 就是这个范式的标准样板。

理解了这三点,再看任何一家厂商的 AIDL Audio HAL 源码,都能很快定位「哪些是 AOSP 通用逻辑,哪些是这块板子自己的事」。

相关推荐
声讯电子3 小时前
AU-60 VS A-59F 语音模组完整对比(研发选型专用)
嵌入式·ai降噪·回声消除·语音模组
Highcharts3 小时前
用 Highcharts 构建交互式 ICU 实时生命体征监护仪表盘
嵌入式·数据可视化
字节跳动视频云技术团队18 小时前
不止于 4K,火山引擎画质增强让视频从清晰走向细腻
人工智能·音视频开发
字节跳动视频云技术团队3 天前
火山引擎 × 央视网 打造 2026 世界杯沉浸式观赛盛宴
人工智能·音视频开发
码流怪侠3 天前
StreamingBench:首个流式视频理解基准——多模态大模型离人类实时感知还有多远?
llm·github·音视频开发
左手厨刀右手茼蒿3 天前
PCIe设备驱动开发:从枚举到DMA传输的完整链路
linux·嵌入式·系统内核
济6173 天前
I.MX6U Linux 驱动开发篇---零基础必看!中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)--- Ubuntu20.04
linux·驱动开发·嵌入式·嵌入式linux驱动开发
凉、介3 天前
Linux 设备驱动匹配机制
linux·笔记·单片机·学习·操作系统·嵌入式
凉、介4 天前
Virtio 系列(一):框架概览
笔记·学习·嵌入式·虚拟化·virtio