瑞芯微 I2S 学习笔记:上层应用开发与调试实战
1. 概述
瑞芯微平台上的音频项目通常由三层构成:
- 物理层 / 控制器层:I2S 控制器 + Codec(已在第二篇拆解)。
- 内核驱动层:ASoC CPU DAI、Codec DAI、Machine(已在第二篇拆解)。
- 用户空间层: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 噪声 / 爆音
常见根因:
| 症状 | 排查方向 |
|---|---|
| 规律性 click | MCLK 启停时序差,驱动 trigger 顺序有 bug |
| 持续性嘶嘶声 | 电源纹波、PCB 地线、Codec 偏置电压 |
| 间隔性 pop | DAPM 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:
- ALSA 通知所有打开的 PCM stream;
- ASoC 调用
snd_soc_dai_hw_params重新协商; - Codec 切换 PLL/BCLK;
- 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_size | buffer_size | Fs | 延迟 |
|---|---|---|---|---|
| 普通播放 | 1024 | 4096 | 48 kHz | ~21 ms |
| 游戏 | 512 | 2048 | 48 kHz | ~10 ms |
| 实时通话 | 256 | 1024 | 16 kHz | ~16 ms |
| 专业 | 64 | 256 | 48 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>, <&_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 软件测试套件。
更多推荐
所有评论(0)