Skip to content

ALSA PCM 状态机与 XRUN ​

内核:对照 NXP BSP Linux 4.9.88本文:PCM 子流状态如何变迁、start_threshold / stop_threshold / avail_min 什么作用、XRUN 如何判定与恢复


目录 ​


1. 本文要回答什么 ​

一次 write / read 为什么有时直接返回、有时阻塞、有时变成 -EPIPE?PCM 子流到底处在什么状态?


2. 状态机 ​

定义见 include/uapi/sound/asound.h:

状态含义(粗)
OPEN已 open,尚未完成参数设置
SETUPhw_params 已设,尚未 prepare
PREPARED已 prepare,可以 start / 自动 start
RUNNING已 trigger,DMA/硬件在传数据
XRUNunderrun(播)或 overrun(录),传输已停
DRAINING播放侧把剩余数据播完再停
PAUSED已 pause
SUSPENDED电源挂起相关
DISCONNECTED设备断开

日常播录路径主要存在前五个状态,外加播放结束时的 DRAINING。

非法状态下的操作会失败:例如不在 PREPARED 就 start → -EBADFD(见 snd_pcm_pre_start)。


3. 正常路径怎么走 ​

以阻塞模式、aplay / arecord 直连为例(省略 libasound 细节):

text
open
  → state = OPEN
hw_params
  → state = SETUP(参数、缓冲大小等落定)
prepare
  → soc_pcm_prepare:STREAM_START,模拟电路上电
  → state = PREPARED(可以 start)
write / read
  → 若仍为 PREPARED 且满足 start_threshold
        → snd_pcm_start → ops->trigger(START)
        → state = RUNNING
  → RUNNING 下继续填/取环形缓冲
close / drain / drop
  → trigger(STOP) 停 DMA/SAI;soc_pcm_close:STREAM_STOP,模拟通路断电

snd_pcm_start 的条件(pcm_native.c):

text
snd_pcm_pre_start
  → 必须是 PREPARED,否则 -EBADFD
  → 播放还要求缓冲里已有数据,否则 -EPIPE
snd_pcm_do_start
  → substream->ops->trigger(START)   // ASoC: soc_pcm_trigger
snd_pcm_post_start
  → state = RUNNING

前面的播放文章里「写够阈值再 start」、录音文章里「读请求够阈值再 start」,都是在 PREPARED → RUNNING 这一跳上。


4. 环形缓冲与三个阈值 ​

环形缓冲上有两个指针(简化):

指针谁推进含义
appl_ptr应用(write/read)软件读写位置
hw_ptr硬件/DMA(period 回调里更新)硬件消费/生产位置

由此得到「还可写 / 还可读」的帧数:

  • 播放:snd_pcm_playback_avail ≈ 空闲可写帧数
  • 录音:snd_pcm_capture_avail ≈ 已录可读帧数

三个常用阈值(runtime 里):

参数典型作用
start_threshold从 PREPARED 自动进入 RUNNING 的门槛(播:已写够;录:本次 read 请求够大)
stop_thresholdRUNNING 下 avail 达到此值 → 判 XRUN(默认常接近 buffer_size)
avail_min阻塞 wait 时,「至少有这么多 avail 才唤醒」

阻塞 write:playback_avail 不够 → wait_for_avail,等 DMA 消费出空位。
阻塞 read:capture_avail 为 0 → 同样 wait,等 DMA 填数。


5. XRUN:何时发生、内核做什么 ​

XRUN = underrun(播放)或 overrun(录音)的统称。

方向现象avail 语义
播放 underrun应用写太慢,DMA 把缓冲抽空playback_avail 变得很大(几乎整缓冲可写)
录音 overrun应用读太慢,DMA 把缓冲塞满capture_avail 变得很大(几乎整缓冲可读)

判定入口在 snd_pcm_update_state()(pcm_lib.c):period 完成更新 hw_ptr 后调用。

text
snd_pcm_update_state
  → avail = playback_avail 或 capture_avail
  → 若 avail >= stop_threshold
        → xrun()
              → snd_pcm_stop(..., SNDRV_PCM_STATE_XRUN)
              → 返回 -EPIPE

xrun() 本身:

text
xrun(substream)
  → snd_pcm_stop(substream, SNDRV_PCM_STATE_XRUN)
  → (若开了 XRUN debug)打警告 / 可选 dump stack

之后应用再 write / read,会在状态检查里看到 XRUN,同样拿到 -EPIPE(播录文调用栈里的 case SNDRV_PCM_STATE_XRUN)。

硬件侧也可能直接上报:更新指针时若得到 SNDRV_PCM_POS_XRUN,同样进 xrun()。


6. 应用侧怎么恢复 ​

进入 XRUN 后,不能假装还在 RUNNING 里继续传。常见做法:

  1. prepare 再传(SNDRV_PCM_IOCTL_PREPARE → snd_pcm_prepare)
    回到可 trigger 的 PREPARED,再 write/read(或显式 start)。
  2. alsa-lib snd_pcm_recover()
    对 -EPIPE 做恢复封装。
  3. 调大缓冲 / period、提高写读节奏、降低负载
    减少再次踩 stop_threshold。

调试时可开 CONFIG_SND_PCM_XRUN_DEBUG,让 xrun() 打出流名甚至栈,便于确认是调度延迟还是缓冲太小。


7. 小结 ​

  • PCM 状态约束「现在能不能 start、能不能读写」;非法状态直接 -EBADFD / -EPIPE。
  • start_threshold 管何时进入 RUNNING;stop_threshold 管何时判 XRUN;avail_min 管阻塞唤醒粒度。
  • XRUN 由 snd_pcm_update_state 在 avail 过大时触发,经 xrun() → state = XRUN,I/O 见 -EPIPE;恢复通常要 prepare(或 lib 的 recover)。

关键文件:include/uapi/sound/asound.h(状态枚举)、sound/core/pcm_native.c(start / prepare)、sound/core/pcm_lib.c(update_state / xrun / write·read 等待)。

基于 VitePress 构建