资讯动态

RK3588 Android12多路音频输出实战:双HDMI与喇叭并发改造

发布时间:2026/9/28 8:49:26 来源:尧图企业网站定制
1. 从一次声音只从HDMI出来的调试说起RK3588这块板子在Android12上做多路音频输出最容易踩的坑就是插上HDMI之后声音全跑到HDMI去了板载喇叭一声不吭或者反过来喇叭响了HDMI那边没动静。更麻烦的是双HDMI场景——两个HDMI口加一个喇叭三路要同时出声默认的tinyalsa_hal配置根本扛不住。这个问题的本质不在硬件RK3588的I2S、SPDIF、HDMI音频控制器都是独立的硬件层面完全支持多路并发。问题出在Android的音频HAL层tinyalsa_hal默认按音频设备的优先级做路由同一时间只激活一个output谁优先级高谁出声。你要让三路同时工作就得改HAL的设备选择逻辑和混音策略。这篇内容适合正在做RK3588 Android12音频定制的驱动工程师、系统集成人员以及遇到HDMI插上喇叭就没声这类问题的开发者。我会把tinyalsa_hal的修改过程完整拆开包括设备枚举、路由决策、混音配置、实测验证几个环节每一步都说明为什么这么改以及改完之后可能遇到的新问题怎么处理。代码基于Android12的AOSP音频HAL结构RK3588的BSP里tinyalsa_hal通常放在hardware/rockchip/audio/tinyalsa_hal/目录下。先说结论核心改动集中在三个文件——audio_hw.c的设备路由逻辑、audio_hw.h的设备定义、以及混音器配置里的mixer_paths.xml。改完之后双HDMI和喇叭可以同时输出延迟和同步性在可接受范围内。下面逐步展开。2. 先搞清楚RK3588的音频通路到底怎么走的2.1 三路输出的硬件拓扑RK3588的音频子系统比一般SoC复杂因为它要同时支持HDMI RX/TX、多路I2S、SPDIF、PDM等。做双HDMI加喇叭同步发声你得先理清这三路各自走的是哪条物理通路。板载喇叭一般接在I2S上通过外置功放芯片比如ES8388、RK809内置codec、或者独立的功放IC驱动。RK3588有多个I2S控制器I2S0通常给codec用I2S1/I2S2可能分配给HDMI或者扩展接口。HDMI音频走的是独立的HDMI TX控制器数据从I2S或者SPDIF进入HDMI TX的音频FIFO再嵌进TMDS链路里发出去。关键点在于HDMI音频和I2S喇叭音频在SoC内部是两条完全独立的DMA通路。这意味着硬件上它们可以同时工作不存在资源抢占。你在/proc/asound/cards里能看到类似这样的设备列表0 [rockchiphdmi0 ]: rockchip-hdmi0 - rockchip-hdmi0 1 [rockchiphdmi1 ]: rockchip-hdmi1 - rockchip-hdmi1 2 [rockchipi2s0 ]: rockchip-i2s0 - rockchip-i2s0每个card对应一个PCM设备各自有独立的playback substream。硬件没问题问题全在HAL怎么调度这些card。2.2 Android音频HAL的设备抽象Android的audio HAL把物理设备抽象成audio_devices_t比如AUDIO_DEVICE_OUT_SPEAKER、AUDIO_DEVICE_OUT_AUX_DIGITALHDMI用的就是这个、AUDIO_DEVICE_OUT_HDMI。tinyalsa_hal在初始化时会根据mixer_paths.xml和板级配置把可用的output设备注册进去。默认逻辑是这样的HAL维护一个out_device变量记录当前激活的输出设备。当上层AudioFlinger要求切换输出比如插入HDMIHAL会调用select_output_device()重新决策通常按优先级选一个然后关掉其他的。这就是插HDMI喇叭没声的根源——不是硬件断了是HAL主动把喇叭的PCM关了。要让多路同时出声思路有两个方向一是改路由逻辑让HAL在特定条件下同时激活多个output二是用ALSA的softvol/dmix插件做混音把多路PCM合并。tinyalsa_hal本身不直接支持多output并发所以实际做法是在HAL层做设备组合把多个物理card绑定成一个逻辑output或者修改路由决策让多个PCM保持打开。2.3 为什么不能简单改优先级了事有人会想那把喇叭和HDMI设成同一优先级不就行了实测不行。因为AudioFlinger的策略是一个stream对应一个output它不会主动往多个output写数据。你就算在HAL里同时打开了三个PCMAudioFlinger只往它认为的那个output写另外两个PCM拿不到数据照样没声。所以真正的解法是在HAL的out_write路径里做数据分发把AudioFlinger写进来的PCM数据同时写到多个物理PCM设备。这需要在audio_hw.c里改out_write()函数让它遍历所有激活的PCM逐个pcm_write()。这是整个修改的核心也是最容易出问题的地方——多路写入的时序、缓冲区管理、underrun处理都得重新考虑。3. tinyalsa_hal的改造从单路输出到三路并发3.1 设备定义与配置文件的对应关系先看audio_hw.h里的设备定义。RK3588的BSP通常已经定义了HDMI0、HDMI1、SPEAKER这几个设备但默认只启用一个。你需要确认mixer_paths.xml里有对应的路径配置。一个典型的配置片段长这样path namespeaker ctl nameI2S0 Playback Volume value100 / ctl nameSpeaker Switch valueOn / /path path namehdmi0 ctl nameHDMI0 Audio Switch valueOn / /path path namehdmi1 ctl nameHDMI1 Audio Switch valueOn / /path这些path在HAL初始化时通过mixer_ctl_select()应用。如果你要三路并发得确保这三个path都能被独立选中而不是互斥的。检查mixer_paths.xml里有没有path namespeaker_hdmi这种组合path有的话可以直接用没有就自己加。3.2 修改select_output_device的路由逻辑select_output_device()是路由决策的核心。默认实现大概是这样的static void select_output_device(struct tinyalsa_audio_device *adev) { if (adev-out_device AUDIO_DEVICE_OUT_AUX_DIGITAL) { adev-out_device AUDIO_DEVICE_OUT_AUX_DIGITAL; } else { adev-out_device AUDIO_DEVICE_OUT_SPEAKER; } }这段逻辑的问题在于它用覆盖而不是|累加。改成多路并发的第一步就是让它在检测到HDMI时保留SPEAKERstatic void select_output_device(struct tinyalsa_audio_device *adev) { uint32_t devices adev-out_device; uint32_t active 0; if (devices AUDIO_DEVICE_OUT_SPEAKER) active | AUDIO_DEVICE_OUT_SPEAKER; if (devices AUDIO_DEVICE_OUT_AUX_DIGITAL) active | AUDIO_DEVICE_OUT_AUX_DIGITAL; if (devices AUDIO_DEVICE_OUT_HDMI) active | AUDIO_DEVICE_OUT_HDMI; adev-out_device active; }注意这里要区分AUX_DIGITAL和HDMI两个宏不同Android版本定义不一样。Android12里AUDIO_DEVICE_OUT_HDMI是AUX_DIGITAL的别名实际用的时候看BSP怎么定义的。双HDMI场景下两个HDMI口可能都报AUX_DIGITAL你需要用AUDIO_DEVICE_OUT_HDMI配合AUDIO_DEVICE_OUT_AUX_DIGITAL的位掩码来区分或者干脆在板级配置里给两个HDMI分配不同的device bit。3.3 out_write的多路分发实现这是最关键的改动。原来的out_write()只往一个PCM写static ssize_t out_write(struct audio_stream_out *stream, const void *buffer, size_t bytes) { struct stream_out *out (struct stream_out *)stream; struct tinyalsa_audio_device *adev out-dev; int ret pcm_write(out-pcm, buffer, bytes); return ret 0 ? bytes : ret; }改成多路分发后要遍历所有激活的PCMstatic ssize_t out_write(struct audio_stream_out *stream, const void *buffer, size_t bytes) { struct stream_out *out (struct stream_out *)stream; struct tinyalsa_audio_device *adev out-dev; int ret 0; pthread_mutex_lock(adev-lock); for (int i 0; i adev-active_pcm_count; i) { if (adev-active_pcms[i].pcm) { int r pcm_write(adev-active_pcms[i].pcm, buffer, bytes); if (r 0) { ALOGE(pcm_write failed on pcm %d: %s, i, pcm_get_error(adev-active_pcms[i].pcm)); ret r; } } } pthread_mutex_unlock(adev-lock); return ret 0 ? bytes : ret; }这里有几个坑要提前说。第一pcm_write是阻塞的三路串行写会拉长单次写入时间如果buffer不够大容易underrun。解决办法是给每个PCM配足够的period size和buffer size或者用非阻塞模式加自己的缓冲队列。第二三路PCM的时钟源不同HDMI的时钟来自TMDSI2S的时钟来自PLL长时间播放会有漂移表现为某一路偶尔卡顿。这个在HAL层很难彻底解决只能靠增大buffer缓解。3.4 打开和关闭PCM的时机管理多路PCM的打开和关闭要跟设备插拔事件联动。原来的逻辑是设备切换时关掉旧PCM、打开新PCM。改成并发后要改成按需打开、延迟关闭插入HDMI时打开对应的HDMI PCM但不关闭SPEAKER PCM拔出HDMI时关闭HDMI PCMSPEAKER PCM保持所有设备都拔出时才关闭SPEAKER实现上可以在adev-active_pcms[]数组里维护状态每个元素记录pcm指针和device标志。设备变化时调用update_active_pcms()对比新旧设备集合只打开新增的、关闭移除的。这样避免频繁开关PCM导致的爆音和延迟。注意HDMI的PCM在没插线的时候打开会失败所以打开前要先检查HDMI的HPDHot Plug Detect状态。RK3588的HDMI驱动会在/sys/class/switch/hdmi/state里暴露HPD状态HAL可以读这个节点判断。3.5 混音器控制的同步三路PCM打开后还要确保对应的mixer control都设对了。比如SPEAKER path要开功放HDMI path要开音频输出开关。这些在select_output_device()之后统一应用static void apply_mixer_paths(struct tinyalsa_audio_device *adev) { if (adev-out_device AUDIO_DEVICE_OUT_SPEAKER) mixer_ctl_select(adev-mixer, speaker); if (adev-out_device AUDIO_DEVICE_OUT_AUX_DIGITAL) mixer_ctl_select(adev-mixer, hdmi0); if (adev-out_device AUDIO_DEVICE_OUT_HDMI) mixer_ctl_select(adev-mixer, hdmi1); }如果mixer_paths.xml里没有独立的hdmi0/hdmi1 path就得自己加。加的时候注意control名字要和驱动里注册的kcontrol名字完全一致否则mixer_ctl_select会静默失败。用tinymix命令可以列出所有可用的control对照着写。4. 编译、烧录与实测中暴露的问题4.1 编译tinyalsa_hal的注意事项RK3588的Android12 BSP里tinyalsa_hal通常作为独立模块编译。在hardware/rockchip/audio/tinyalsa_hal/目录下执行mm或者用mmm指定路径。编译前确认Android.bp或Android.mk里链接了libtinyalsa否则pcm_write这些符号找不到。编译产物是audio.primary.rk3588.so推到板子的/vendor/lib/hw/或者/vendor/lib64/hw/下看是32位还是64位。推之前先adb root和adb remount推完adb reboot或者重启audioserveradb root adb remount adb push audio.primary.rk3588.so /vendor/lib64/hw/ adb shell killall audioserverkillall audioserver之后系统会自动重启audioserver不用重启整机。但有时候HAL的so被占用kill不掉那就得reboot。4.2 用tinymix和tinyplay做底层验证改HAL之前先用底层工具确认硬件通路是通的。tinymix看control状态tinyplay直接往PCM写数据# 列出所有card tinymix -D 0 # 播放测试音频到HDMI0 tinyplay /data/test.wav -D 0 -d 0 # 同时播放到I2S0 tinyplay /data/test.wav -D 2 -d 0如果两个tinyplay能同时出声说明硬件和驱动没问题问题纯在HAL。如果tinyplay单独播放都不出声那得先查驱动和dts配置别急着改HAL。4.3 实测中的典型问题与处理问题一三路同时出声但有一路断断续续。这是buffer不足的典型表现。检查每个PCM的period_size和period_count建议period_size设1024或2048period_count设4以上。在pcm_open时通过config结构体指定struct pcm_config config { .channels 2, .rate 48000, .period_size 2048, .period_count 4, .format PCM_FORMAT_S16_LE, .start_threshold 0, .stop_threshold 0, .silence_threshold 0, };问题二HDMI和喇叭声音不同步差几十毫秒。这是时钟漂移加buffer深度不同导致的。HDMI的buffer通常比I2S深因为HDMI链路有额外的FIFO。缓解办法是把两边的period_size设成一样并且尽量用同一个时钟源。RK3588的HDMI音频可以配置成从I2S取时钟通过/sys/kernel/debug/clk查但配置起来比较麻烦实际项目中如果同步要求不高接受几十毫秒的偏差是常见的。问题三插拔HDMI时喇叭有爆音。这是因为HDMI PCM开关时影响了共享的时钟或电源域。处理办法是在开关PCM前后加静音控制先mute再操作操作完再unmute。在mixer_paths.xml里加ctl nameMaster Playback Switch valueOff /操作完再设回On。问题四AudioFlinger报underrun。三路写入拉长了out_write的执行时间如果超过AudioFlinger的容忍阈值就会报underrun。看logcat里的AudioFlinger和audio_hw_primary标签。解决办法是减小单次写入的bytes或者把out_write改成异步——起一个写线程out_write只往队列里塞数据写线程负责分发到各PCM。这个改动比较大但效果最好。4.4 验证三路并发的完整测试流程改完之后按这个顺序验证开机adb shell dumpsys media.audio_flinger看output列表确认SPEAKER和两个HDMI都在不插HDMI播放音乐确认喇叭出声插HDMI0播放音乐确认喇叭和HDMI0同时出声再插HDMI1确认三路同时出声拔HDMI0确认HDMI1和喇叭继续出声无爆音拔HDMI1确认喇叭继续出声重复插拔10次确认无异常每一步都用tinymix确认对应的control状态正确。如果某一步失败先回退到上一步确认基础功能正常再逐步排查。5. 几个容易被忽略的细节和长期维护建议5.1 关于AUDIO_DEVICE_OUT_HDMI的位掩码陷阱Android的audio_devices_t是32位掩码AUDIO_DEVICE_OUT_AUX_DIGITAL和AUDIO_DEVICE_OUT_HDMI在某些版本里是同一个值。RK3588的BSP里如果两个HDMI口都报同一个device bitHAL层就没法区分。这时候需要在板级配置里给两个HDMI分配不同的bit比如用AUDIO_DEVICE_OUT_AUX_DIGITAL给HDMI0用AUDIO_DEVICE_OUT_HDMI给HDMI1。但这两个宏在Android12的audio.h里可能定义相同那就得用AUDIO_DEVICE_OUT_HDMI_ARC或者自定义bit。改之前先grep一下system/media/audio/include/system/audio.h确认定义。5.2 功耗与热设计的影响三路PCM同时工作RK3588的音频子系统功耗会上升尤其是HDMI TX的功耗不低。实测中如果发现板子发热明显或者HDMI输出一段时间后花屏可能是电源域供电不足。检查dts里HDMI和I2S的regulator配置确保供电能力够。另外三路并发时DDR带宽占用也会增加如果同时跑视频解码可能出现音频卡顿这时候要调DDR QoS优先级。5.3 升级Android版本时的移植成本这套改动在Android12上验证过升级到Android13或14时audio HAL的接口可能有变化。主要关注audio_stream_out结构体有没有新增回调以及select_output_device的调用时机有没有变。tinyalsa_hal本身是Rockchip维护的升级BSP时先看官方有没有更新版本有的话优先用官方的把自己的改动以patch形式合进去别直接覆盖。5.4 一个实用的调试技巧改HAL的时候在out_write里加日志打印每次写入的bytes和耗时用ALOGD输出。跑一段时间后logcat抓下来分析能看出哪一路慢、哪一路underrun。日志别打太频繁否则本身会影响性能建议每100次写入打一次。另外/proc/asound/cardX/pcm0p/sub0/status能看到PCM的实时状态包括hw_ptr和appl_ptr排查underrun时很有用。我个人在实际项目中的体会是RK3588的多路音频并发硬件从来不是瓶颈瓶颈永远在HAL的调度逻辑和buffer管理上。把out_write的多路分发做稳把buffer配足把插拔事件处理干净三路同步出声是可以做到的。同步精度要求特别高的场景比如专业音频建议还是走外部同步方案HAL层能做到的是都能出声、不爆音、不卡顿这个目标在RK3588 Android12上是完全可达的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑