1. 概述

瑞芯微平台上的音频项目通常由三层构成:

  1. 物理层 / 控制器层:I2S 控制器 + Codec(已在第二篇拆解)。
  2. 内核驱动层:ASoC CPU DAI、Codec DAI、Machine(已在第二篇拆解)。
  3. 用户空间层:tinyalsa、ALSA Lib、PulseAudio/PipeWire、APP 直接 ioctl(本篇重点)。

调试与调优贯穿三层。常见经验:70 % 的"无声/噪声/卡顿"问题出在用户空间配置、ALSA 参数协商、DAPM widget 上电顺序,而不是驱动本身。所以把用户空间和调试工具链吃透,是从"能跑"到"跑好"的关键一步。


2. 用户空间音频栈

2.1 三种主流使用方式

方式典型场景优点缺点
tinyalsa嵌入式短小应用、recovery、bootanimation、喇叭测试静态库约 30 KB,无 glibc 依赖,启动快API 较底层,路由能力有限
ALSA Lib (libasound)桌面 Linux、Qt/SDL 应用、专业音频工具功能完整、支持 PCM/Mixer/CTL/Route体积大、依赖 pthread、复杂参数
PulseAudio / PipeWire桌面与多应用混音场景自动路由、多客户端、采样率自适应延迟较大、不适合裸机

2.2 设备节点

/dev/snd/
├── controlC0          # Mixer / CTL
├── pcmC0D0p           # PCM playback (hw:0,0)
├── pcmC0D1c           # PCM capture (hw:0,1)
├── pcmC0D2p           # HDMI LPCM
└── timer              # 硬件定时器

C0D0p 中的 C0 = card 0,D0 = device 0,p = playback、c = capture。

2.3 与 I2S 控制器的关系

  ┌──────────────────────────────────────────┐
  │  tinyalsa / libasound / pulseaudio       │
  └──────────────────────────────────────────┘
                │ open("/dev/snd/pcmC0D0p")
                │ snd_pcm_writei / pcm_write
  ┌─────────────▼────────────────────────────┐
  │         ASoC PCM (soc-pcm.c)             │
  └──────────────────────────────────────────┘
                │ DMA trigger
  ┌─────────────▼────────────────────────────┐
  │     Rockchip I2S TX/RX FIFO (kernel)     │
  └──────────────────────────────────────────┘
                │ BCLK / LRCLK / SD
  ┌─────────────▼────────────────────────────┐
  │           External Codec / HDMI          │
  └──────────────────────────────────────────┘

3. tinyalsa 编程实战

external/tinyalsa/ 在 AOSP 与瑞芯微 BSP 中均自带。

3.1 打开 PCM 设备

#include <tinyalsa/pcm.h>

struct pcm *pcm_open(unsigned int card,
                     unsigned int device,
                     unsigned int flags,        /* PCM_OUT / PCM_IN / PCM_MMAP */
                     const struct pcm_config *config);

struct pcm_config cfg = {
    .channels        = 2,
    .rate            = 48000,
    .format          = PCM_FORMAT_S24_LE,   /* S16_LE / S24_LE / S32_LE */
    .period_size     = 1024,
    .period_count    = 4,
    .start_threshold = 1024,
    .silence_threshold = 0,
    .stop_threshold  = (1024 * 4),
};

struct pcm *p = pcm_open(0, 0, PCM_OUT, &cfg);
if (!p || !pcm_is_ready(p)) {
    fprintf(stderr, "pcm_open failed: %s\n", pcm_get_error(p));
    return -1;
}

3.2 播放与录制

while (1) {
    if (fread(buf, 1, frames_to_bytes(p, cfg.period_size), fp) <= 0)
        break;
    if (pcm_write(p, buf, cfg.period_size) != 0) {
        fprintf(stderr, "write error: %s\n", pcm_get_error(p));
        break;
    }
}
pcm_close(p);

录制只需把 PCM_OUT 换成 PCM_IN、pcm_write 换成 pcm_read。

3.3 Mixer 控制

#include <tinyalsa/mixer.h>

struct mixer *m = mixer_open(0);
struct mixer_ctl *ctl = mixer_get_ctl(m, MIXER_OUT_VOL);

unsigned int num_values = mixer_ctl_get_num_values(ctl);
for (int i = 0; i < num_values; i++)
    mixer_ctl_set_value(ctl, i, 100);     /* 0..100 */

mixer_close(m);

注意 tinyalsa 的 mixer 是把 kcontrol 抽象成线性 0~100,并不一定等于硬件寄存器。需要通过 tinymix 看到 control 的真实范围(dB、寄存器值等)。

3.4 路由切换(headphone ↔ speaker)

struct mixer_ctl *spk = mixer_get_ctl(m, "Speaker Playback Switch");
struct mixer_ctl *hp  = mixer_get_ctl(m, "Headphone Playback Switch");

mixer_ctl_set_value(spk, 0, enable_speaker ? 1 : 0);
mixer_ctl_set_value(hp,  0, enable_headphone ? 1 : 0);

Speaker Playback Switch / Headphone Playback Switch 来自 Codec DAPM widget,被 Codec 驱动注册为 kcontrol。


4. ALSA Lib 编程实战

4.1 PCM 打开

#include <alsa/asoundlib.h>

snd_pcm_t *pcm;
snd_pcm_hw_params_t *hw;
int err;

err = snd_pcm_open(&pcm, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);
if (err < 0) { /* handle */ }

snd_pcm_hw_params_alloca(&hw);
snd_pcm_hw_params_any(pcm, hw);

snd_pcm_hw_params_set_access(pcm, hw, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S24_LE);
snd_pcm_hw_params_set_channels(pcm, hw, 2);
snd_pcm_hw_params_set_rate(pcm, hw, 48000, 0);

snd_pcm_uframes_t period = 1024;
snd_pcm_hw_params_set_period_size(pcm, hw, period, 0);
snd_pcm_hw_params_set_periods(pcm, hw, 4, 0);   /* buffer = 4 × period */

snd_pcm_hw_params(pcm, hw);

4.2 写入与恢复

snd_pcm_sframes_t frames;
while (1) {
    frames = snd_pcm_writei(pcm, buf, period);
    if (frames == -EPIPE) {
        snd_pcm_prepare(pcm);   /* underrun 恢复 */
    } else if (frames < 0) {
        snd_pcm_recover(pcm, frames, 0);
    }
}

4.3 非阻塞与异步通知

int poll_fd = snd_pcm_poll_descriptors_count(pcm);
struct pollfd pfds[poll_fd];
snd_pcm_poll_descriptors(pcm, pfds, poll_fd);

while (1) {
    int err = poll(pfds, poll_fd, 1000);
    unsigned short revents;
    snd_pcm_poll_descriptors_revents(pcm, pfds, poll_fd, &revents);
    if (revents & POLLOUT) { /* 可写 */ }
    if (revents & POLLERR) { /* 处理 */ }
}

4.4 硬件参数协商

当应用请求的格式/采样率与硬件不匹配时,ALSA Lib 会自动选择 nearest:

snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_FLOAT_LE);
/* 通常会协商失败并报错,由 snd_pcm_hw_params_test_format 提前检测 */

更稳的做法是先 snd_pcm_hw_params_test_xxx 探测,再 set:

if (snd_pcm_hw_params_test_format(pcm, hw, fmt) < 0)
    fmt = snd_pcm_hw_params_get_format(hw);

5. 调试方法论

5.1 第一步:信息收集

工具用途
aplay -l / arecord -l列出所有 card / device
cat /proc/asound/cards查看 sound card 注册情况
ls /dev/snd/检查设备节点
tinymix列出所有 kcontrol
tinycap / tinyplay脱离 PA 直接播放录制
dmesg | grep -iE 'i2s|asoc|snd'观察驱动 probe 顺序与错误

5.2 链式验证

原则:从下到上逐层确认。先硬件时钟、再格式、再 DMA、最后软件参数。

  ┌─ MCLK 是否输出?示波器/频率计 ─┐
  │                                ▼
  ├─ BCLK / LRCLK 频率是否正确?───┤  ← Clock 阶段
  │                                ▼
  ├─ 抓 SDOUT 数据,是否有 PCM?───┤  ← Format 阶段
  │                                ▼
  ├─ DMA 是否触发?FIFO 水位?─────┤  ← DMA 阶段
  │                                ▼
  └─ ALSA 是否能 open / hw_params ┘  ← Software 阶段

5.3 抓取 I2S 信号

逻辑分析仪 / 示波器:

  • BCLK 与 LRCLK 比例是否符合 BCLK = Fs × BitWidth × Channels;
  • LRCLK 极性:L 低 R 高;
  • SDOUT 在 BCLK 上升沿采样(Master 模式),下降沿采样(Slave 模式);
  • MCLK 抖动是否异常(晶振问题)。

5.4 ftrace 关键路径

echo 1 > /sys/kernel/debug/tracing/events/sound/enable
echo 1 > /sys/kernel/debug/tracing/events/asoc/enable
echo funcgraph-abstime > /sys/kernel/debug/tracing/current_tracer
echo snd_pcm_* >> /sys/kernel/debug/tracing/set_ftrace_filter
echo snd_soc_* >> /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace > /tmp/audio.trace

典型关注点:

  • snd_pcm_period_elapsed:周期中断频率;
  • snd_soc_dapm_power_event:widget 上电顺序;
  • rockchip_i2s_trigger:START/STOP 时序。

5.5 /sys/kernel/debug/asoc/

ls /sys/kernel/debug/asoc/
# codecs / dai-links / dapm / bias_level

cat /sys/kernel/debug/asoc/dapm/dapm_widget

可直接读到当前所有 widget 的上电情况,比 tinymix 更接近硬件真相。


6. 典型问题排查

6.1 完全无声

  ┌─ 是否注册了 sound card?─────────── dmesg | grep asoc
  │                                     ls /dev/snd/
  ▼
  ┌─ pcm_open 是否成功?─────────────── tinyplay test.wav
  ▼
  ┌─ hw_params 是否成功?────────────── aplay -D hw:0,0 test.wav
  ▼
  ┌─ DAPM widget 是否上电?──────────── tinymix → 打开 DAC/HP switch
  ▼
  ┌─ MCLK / BCLK 是否输出?──────────── 示波器
  ▼
  ┌─ Codec 是否正确响应(I2C ACK)?─── i2cdetect

6.2 噪声 / 爆音

常见根因:

症状排查方向
规律性 clickMCLK 启停时序差,驱动 trigger 顺序有 bug
持续性嘶嘶声电源纹波、PCB 地线、Codec 偏置电压
间隔性 popDAPM widget 上电顺序、pop 抑制电路
高频白噪声采样率不匹配、PLL 失锁、BCLK jitter

6.3 延迟过大

总延迟 ≈ 软件缓冲 + DMA 缓冲 + Codec 内部延迟 + 功放延迟
       = period_size × 2 / Fs + Codec group delay

调小 period_size 可降低延迟,但会增大 CPU 占用与中断频率。常用经验值:

  • 实时语音:period_size = 256,延迟 ≈ 10 ms;
  • 音乐播放:period_size = 1024,延迟 ≈ 21 ms;
  • 专业音频:period_size = 64,延迟 ≈ 2.5 ms(要求硬件响应快)。

6.4 采样率切换失败

从 48 kHz 切到 44.1 kHz:

  1. ALSA 通知所有打开的 PCM stream;
  2. ASoC 调用 snd_soc_dai_hw_params 重新协商;
  3. Codec 切换 PLL/BCLK;
  4. I2S 控制器重新设置 divider。

如果 Codec 不支持 44.1 kHz 的整数 MCLK,会出现 click 或静音。解决方案:在驱动中保存常用采样率表,跳过不合理的组合。

6.5 多应用抢占

PulseAudio 通常调度到最小延迟流;裸跑 tinyalsa 时,多个 writer 会争抢 PCM 设备:

  • ASoC 顶层只允许一个 writer;
  • 第二个 snd_pcm_open 会返回 -EBUSY;
  • 需要 mixer 抽象层把多个客户端混音。

7. 性能调优

7.1 period_size 与 buffer_size 选择

场景period_sizebuffer_sizeFs延迟
普通播放1024409648 kHz~21 ms
游戏512204848 kHz~10 ms
实时通话256102416 kHz~16 ms
专业6425648 kHz~2.5 ms

7.2 DMA burst 与中断合并

dma_data->maxburst = 8;          /* 与 FIFO 触发水位一致 */

过小:中断频繁;过大:underrun 时丢帧多。经验值 = FIFO depth / 8。

7.3 关闭无用的 DAPM widget

/* 在 .init 中强制下电 */
snd_soc_dapm_disable_pin(dapm, "Mic");
snd_soc_dapm_sync(dapm);

7.4 CPU 占用

  • 启用 mmap 模式,应用与内核共享 buffer;
  • 提升线程优先级(SCHED_FIFO);
  • 在多核平台把音频线程绑到固定 CPU,避免调度抖动。

8. 多路并发与跨接口同步

8.1 Speaker + Headphone 同时输出

DTS 中配置两个 DAI link:

&i2s0_8ch { status = "okay"; };

sound {
    compatible = "audio-graph-card";
    label = "rk3588-multi";
    dais = <&i2s0_hp_port>, <&es8388_hp_port>,
           <&i2s1_spk_port>, <&amp_port>;
};

驱动中:

static const struct snd_kcontrol_new rk3588_controls[] = {
    SOC_DAPM_PIN_SWITCH("Headphone"),
    SOC_DAPM_PIN_SWITCH("Speaker"),
};

8.2 HDMI 视频音频同步

HDMI 音视频必须同源 PLL:

assigned-clocks = <&cru PLL_AUD>, <&cru I2S1_MCLK>;
assigned-clock-parents = <&cru GPLL>;

驱动中 hw_params 调用 snd_soc_dai_set_sysclk:

snd_soc_dai_set_sysclk(cpu_dai, 0, 12288000, SND_SOC_CLOCK_OUT);

8.3 蓝牙 + 本地回声消除

  • I2S 接本地 Codec;
  • PCM/BT 接蓝牙模块(注意 BT 通常用 PCM 而非 I2S);
  • ASoC 中建一条虚拟 DAI link,从 PCM 接口读取 AEC 参考。

8.4 多麦克风阵列

TDM 模式 + PDM 控制器组合:

i2s2_8ch: i2s@fe4a0000 {
    compatible = "rockchip,rk3588-i2s";
    dmas = <&dmac1 2>, <&dmac1 3>;
    rockchip,tdm-num-slots = <8>;
    rockchip,tdm-slot-width = <32>;
};

9. 功耗与低功耗

9.1 闲置通路下电

/* 用户空间检测无 stream 时 */
snd_pcm_drain(pcm);
snd_pcm_close(pcm);

ASoC 自动 dapm_bias_level = SND_SOC_BIAS_OFF,关闭 Codec/MCLK。

9.2 Suspend / Resume

Codec 驱动实现 suspend / resume,顺序:

Stream STOP
  → Codec widget OFF
    → MCLK OFF
      → Controller reset / clock OFF
        → PM runtime put

恢复顺序相反。任何顺序错误都会导致 resume 后无声。

9.3 VAD 唤醒词

RK 芯片内置 VAD(Voice Activity Detection):

vad: vad@fe100000 {
    compatible = "rockchip,rk3588-vad";
    clocks = <&cru PCLK_VAD>;
    pinctrl-names = "default";
    pinctrl-0 = <&i2s0_mclk>;
    status = "okay";
};

VAD 工作时仅需 MCLK,I2S TX/RX 全部关闭,整机功耗可降至 < 5 mA。


10. 测试与验收

10.1 功能测试清单

  • 喇叭 / 耳机单独播放;
  • 喇叭 + 耳机同时播放;
  • 录音(含 MIC 偏置、AGC);
  • HDMI 输出;
  • 蓝牙 A2DP 输入;
  • 采样率切换(8k/16k/44.1k/48k/96k/192k);
  • 位宽切换(16/24/32);
  • 通道切换(stereo / mono / 5.1 / 7.1)。

10.2 性能测试

# CPU 占用
top -H -p $(pidof tinyplay)

# 延迟
tinyplay --latency=100000 file.wav &

# 抖动
cat /sys/kernel/debug/asoc/rockchip-es8388/pcm0p/substream/hw_ptr

10.3 自动化测试脚本(CI)

#!/bin/bash
set -e
amixer -c 0 set 'Headphone' 80%
amixer -c 0 set 'Speaker' 80%
tinyplay /usr/share/sounds/test_48k.wav
sleep 2
tinycap /tmp/rec.wav 5
aplay /tmp/rec.wav

10.4 音质验收

  • THD+N:≤ 0.005 %;
  • SNR:≥ 95 dB;
  • 频响:20 Hz ~ 20 kHz ±0.5 dB;
  • Crosstalk:≤ -90 dB。

仪器:APx555、Audio Precision、RightMark、RMAA 软件测试套件。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐