Linux 内核音频数据传递主要流程 (下)
来而不往非礼也。前面看到了用户空间应用程序和 DMA buffer 之间交换数据,并更新 runtime->control->appl_ptr 指针的过程,这里看一下硬件设备驱动程序在完成 DMA buffer 和硬件设备的数据交换之后,更新 runtime->status->hw_ptr 的过程。
用户空间应用程序,在内核的 __snd_pcm_lib_xfer() 函数中,调用 snd_pcm_start() 函数触发音频数据传递启动,音频硬件设备驱动程序的 trigger 操作被调用。音频硬件设备驱动程序通过 DMA 等机制发起从内存到硬件设备的数据传递。当一波数据传递结束,如 DMA 将一个描述符指定的数据块传完上报一个中断,在相应的中断处理程序中调用 snd_pcm_period_elapsed() 函数,通知 ALSA 框架更新有关指针,以便于用户空间应用程序可以进行下一轮的数据传递。
用户空间应用程序和 Linux 内核,Linux 内核和硬件设备,每次传递的数据量,一般为 runtime->period_size 帧,如音频流为双通道,音频数据的格式为 16 位,runtime->period_size 为 1024,则每次传递的数据量以字节为单位为 4096 字节。通过 snd_pcm_lib_period_bytes() 函数可以执行这种转换。
snd_pcm_period_elapsed() 函数为下一个数据传递周期更新 PCM 状态。当 PCM 处理了 runtime->period_size 帧音频数据时,这个函数在中断处理程序中调用。它将更新相关的指针,唤醒等待的休眠者等。即使自上次调用以来,经过了多个周期,也只应调用一次这个函数。snd_pcm_period_elapsed() 函数定义 (位于 sound/core/pcm_lib.c) 如下:
void snd_pcm_period_elapsed(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime;
unsigned long flags;
if (snd_BUG_ON(!substream))
return;
snd_pcm_stream_lock_irqsave(substream, flags);
if (PCM_RUNTIME_CHECK(substream))
goto _unlock;
runtime = substream->runtime;
if (!snd_pcm_running(substream) ||
snd_pcm_update_hw_ptr0(substream, 1) < 0)
goto _end;
#ifdef CONFIG_SND_PCM_TIMER
if (substream->timer_running)
snd_timer_interrupt(substream->timer, 1);
#endif
_end:
kill_fasync(&runtime->fasync, SIGIO, POLL_IN);
_unlock:
snd_pcm_stream_unlock_irqrestore(substream, flags);
}
EXPORT_SYMBOL(snd_pcm_period_elapsed);
这个函数做的最主要的事情是,在流状态为 RUNNING 时,调用 snd_pcm_update_hw_ptr0() 函数更新指针。在内核的 __snd_pcm_lib_xfer() 函数中,调用 snd_pcm_start() 函数触发音频数据传递启动之后,struct snd_pcm_substream 的状态会更新为 RUNNING。snd_pcm_update_hw_ptr0() 函数的第二个参数用来表示调用的来源是中断处理程序,还是其它,这里传 1 表示调用是在中断上下文进行的。__snd_pcm_lib_xfer() 函数也调用了 snd_pcm_update_hw_ptr0() 函数更新指针,但传的 in_interrupt 参数为 0。
snd_pcm_update_hw_ptr0() 函数的定义 (位于 sound/core/pcm_lib.c) 如下:
static int snd_pcm_update_hw_ptr0(struct snd_pcm_substream *substream,
unsigned int in_interrupt)
{
struct snd_pcm_runtime *runtime = substream->runtime;
snd_pcm_uframes_t pos;
snd_pcm_uframes_t old_hw_ptr, new_hw_ptr, hw_base;
snd_pcm_sframes_t hdelta, delta;
unsigned long jdelta;
unsigned long curr_jiffies;
struct timespec64 curr_tstamp;
struct timespec64 audio_tstamp;
int crossed_boundary = 0;
old_hw_ptr = runtime->status->hw_ptr;
/*
* group pointer, time and jiffies reads to allow for more
* accurate correlations/corrections.
* The values are stored at the end of this routine after
* corrections for hw_ptr position
*/
pos = substream->ops->pointer(substream);
curr_jiffies = jiffies;
if (runtime->tstamp_mode == SNDRV_PCM_TSTAMP_ENABLE) {
if ((substream->ops->get_time_info) &&
(runtime->audio_tstamp_config.type_requested != SNDRV_PCM_AUDIO_TSTAMP_TYPE_DEFAULT)) {
substream->ops->get_time_info(substream, &curr_tstamp,
&audio_tstamp,
&runtime->audio_tstamp_config,
&runtime->audio_tstamp_report);
/* re-test in case tstamp type is not supported in hardware and was demoted to DEFAULT */
if (runtime->audio_tstamp_report.actual_type == SNDRV_PCM_AUDIO_TSTAMP_TYPE_DEFAULT)
snd_pcm_gettime(runtime, &curr_tstamp);
} else
snd_pcm_gettime(runtime, &curr_tstamp);
}
if (pos == SNDRV_PCM_POS_XRUN) {
__snd_pcm_xrun(substream);
return -EPIPE;
}
if (pos >= runtime->buffer_size) {
if (printk_ratelimit()) {
char name[16];
snd_pcm_debug_name(substream, name, sizeof(name));
pcm_err(substream->pcm,
"invalid position: %s, pos = %ld, buffer size = %ld, period size = %ld\n",
name, pos, runtime->buffer_size,
runtime->period_size);
}
pos = 0;
}
pos -= pos % runtime->min_align;
trace_hwptr(substream, pos, in_interrupt);
hw_base = runtime->hw_ptr_base;
new_hw_ptr = hw_base + pos;
if (in_interrupt) {
/* we know that one period was processed */
/* delta = "expected next hw_ptr" for in_interrupt != 0 */
delta = runtime->hw_ptr_interrupt + runtime->period_size;
if (delta > new_hw_ptr) {
/* check for double acknowledged interrupts */
hdelta = curr_jiffies - runtime->hw_ptr_jiffies;
if (hdelta > runtime->hw_ptr_buffer_jiffies/2 + 1) {
hw_base += runtime->buffer_size;
if (hw_base >= runtime->boundary) {
hw_base = 0;
crossed_boundary++;
}
new_hw_ptr = hw_base + pos;
goto __delta;
}
}
}
/* new_hw_ptr might be lower than old_hw_ptr in case when */
/* pointer crosses the end of the ring buffer */
if (new_hw_ptr < old_hw_ptr) {
hw_base += runtime->buffer_size;
if (hw_base >= runtime->boundary) {
hw_base = 0;
crossed_boundary++;
}
new_hw_ptr = hw_base + pos;
}
__delta:
delta = new_hw_ptr - old_hw_ptr;
if (delta < 0)
delta += runtime->boundary;
if (runtime->no_period_wakeup) {
snd_pcm_sframes_t xrun_threshold;
/*
* Without regular period interrupts, we have to check
* the elapsed time to detect xruns.
*/
jdelta = curr_jiffies - runtime->hw_ptr_jiffies;
if (jdelta < runtime->hw_ptr_buffer_jiffies / 2)
goto no_delta_check;
hdelta = jdelta - delta * HZ / runtime->rate;
xrun_threshold = runtime->hw_ptr_buffer_jiffies / 2 + 1;
while (hdelta > xrun_threshold) {
delta += runtime->buffer_size;
hw_base += runtime->buffer_size;
if (hw_base >= runtime->boundary) {
hw_base = 0;
crossed_boundary++;
}
new_hw_ptr = hw_base + pos;
hdelta -= runtime->hw_ptr_buffer_jiffies;
}
goto no_delta_check;
}
/* something must be really wrong */
if (delta >= runtime->buffer_size + runtime->period_size) {
hw_ptr_error(substream, in_interrupt, "Unexpected hw_ptr",
"(stream=%i, pos=%ld, new_hw_ptr=%ld, old_hw_ptr=%ld)\n",
substream->stream, (long)pos,
(long)new_hw_ptr, (long)old_hw_ptr);
return 0;
}
/* Do jiffies check only in xrun_debug mode */
if (!xrun_debug(substream, XRUN_DEBUG_JIFFIESCHECK))
goto no_jiffies_check;
/* Skip the jiffies check for hardwares with BATCH flag.
* Such hardware usually just increases the position at each IRQ,
* thus it can't give any strange position.
*/
if (runtime->hw.info & SNDRV_PCM_INFO_BATCH)
goto no_jiffies_check;
hdelta = delta;
if (hdelta < runtime->delay)
goto no_jiffies_check;
hdelta -= runtime->delay;
jdelta = curr_jiffies - runtime->hw_ptr_jiffies;
if (((hdelta * HZ) / runtime->rate) > jdelta + HZ/100) {
delta = jdelta /
(((runtime->period_size * HZ) / runtime->rate)
+ HZ/100);
/* move new_hw_ptr according jiffies not pos variable */
new_hw_ptr = old_hw_ptr;
hw_base = delta;
/* use loop to avoid checks for delta overflows */
/* the delta value is small or zero in most cases */
while (delta > 0) {
new_hw_ptr += runtime->period_size;
if (new_hw_ptr >= runtime->boundary) {
new_hw_ptr -= runtime->boundary;
crossed_boundary--;
}
delta--;
}
/* align hw_base to buffer_size */
hw_ptr_error(substream, in_interrupt, "hw_ptr skipping",
"(pos=%ld, delta=%ld, period=%ld, jdelta=%lu/%lu/%lu, hw_ptr=%ld/%ld)\n",
(long)pos, (long)hdelta,
(long)runtime->period_size, jdelta,
((hdelta * HZ) / runtime->rate), hw_base,
(unsigned long)old_hw_ptr,
(unsigned long)new_hw_ptr);
/* reset values to proper state */
delta = 0;
hw_base = new_hw_ptr - (new_hw_ptr % runtime->buffer_size);
}
no_jiffies_check:
if (delta > runtime->period_size + runtime->period_size / 2) {
hw_ptr_error(substream, in_interrupt,
"Lost interrupts?",
"(stream=%i, delta=%ld, new_hw_ptr=%ld, old_hw_ptr=%ld)\n",
substream->stream, (long)delta,
(long)new_hw_ptr,
(long)old_hw_ptr);
}
no_delta_check:
if (runtime->status->hw_ptr == new_hw_ptr) {
runtime->hw_ptr_jiffies = curr_jiffies;
update_audio_tstamp(substream, &curr_tstamp, &audio_tstamp);
return 0;
}
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
runtime->silence_size > 0)
snd_pcm_playback_silence(substream, new_hw_ptr);
if (in_interrupt) {
delta = new_hw_ptr - runtime->hw_ptr_interrupt;
if (delta < 0)
delta += runtime->boundary;
delta -= (snd_pcm_uframes_t)delta % runtime->period_size;
runtime->hw_ptr_interrupt += delta;
if (runtime->hw_ptr_interrupt >= runtime->boundary)
runtime->hw_ptr_interrupt -= runtime->boundary;
}
runtime->hw_ptr_base = hw_base;
runtime->status->hw_ptr = new_hw_ptr;
runtime->hw_ptr_jiffies = curr_jiffies;
if (crossed_boundary) {
snd_BUG_ON(crossed_boundary != 1);
runtime->hw_ptr_wrap += runtime->boundary;
}
update_audio_tstamp(substream, &curr_tstamp, &audio_tstamp);
return snd_pcm_update_state(substream, runtime);
}
这个函数会更新多个和硬件操作相关的指针,这些指针也都和 runtime->control->appl_ptr 指针一样,是单调递增的。这里的指针更新,基于硬件设备驱动程序的 pointer 操作返回的 position 完成。position 是当前硬件操作所在的数据,在 DMA buffer 中的偏移量,单位为音频帧,不是字节。这里更新的指针主要有如下这些:
runtime->status->hw_ptr:与应用程序指针
runtime->control->appl_ptr对应的硬件指针,单位是音频帧,它表示当前硬件操作所在的数据,在整个音频流中的位置。runtime->hw_ptr_base:硬件指针基指针,单位是音频帧,它表示,
position计算所基于的基指针,也是当前硬件已经处理的对齐到runtime->buffer_size的音频帧个数。这个指针每次更新增加整数个runtime->buffer_size。一般情况下,硬件指针基指针与position的和是新的硬件指针。runtime->hw_ptr_interrupt:以中断记的硬件指针,单位是音频帧。一般来说,硬件设备平稳地发送数据,每发送
runtime->period_size个音频帧上报一个中断。这个指针用来记录硬件已经处理的对齐到runtime->period_size的音频帧个数。这个指针每次更新增加整数个runtime->period_size。runtime->hw_ptr_jiffies:jiffies 是 Linux 内核的一种计时,每秒钟被分割为
HZ个 jiffy,每个 jiffy 大约为 10 ms。这个指针是以 jiffy 为单位的硬件指针,即它表示按时间计的硬件指针。
snd_pcm_update_hw_ptr0() 函数处理许多种情况下的硬件指针更新,它的执行过程如下:
先保存一下旧的硬件指针。
调用硬件设备驱动程序的
pointer操作获得position。获得以 jiffy 计的当前时间。
音频流的时间戳模式为
SNDRV_PCM_TSTAMP_ENABLE时,则获取时间戳。如果获得的
position为 -1,则调用__snd_pcm_xrun函数报告错误,停止流,更新流的状态为SNDRV_PCM_STATE_XRUN,并返回;如果position大于等于runtime->buffer_size,则把它置为 0。之后将position对齐到runtime->min_align。
一般来说,从硬件设备驱动程序获得的position值不会大于runtime->buffer_size,大于runtime->buffer_size的position值实际上意味着硬件访问内存越界。当硬件设备驱动程序访问的数据到达了 DMA 缓冲区的边界时,可以在pointer操作中返回 0,也可以返回runtime->buffer_size,如这里做的处理。根据
runtime->hw_ptr_base和获得的position计算新的硬件指针。最简单情况下,这个函数到这里已经完成了它的绝大部分职责了。如果函数是在中断上下文调用的,则根据情况重新计算硬件指针基指针和新的硬件指针。对于可以实时获得硬件操作的数据位置的硬件设备,一般来说,计算获得的
delta值应该等于或略小于上面第 6 步中计算获得的硬件指针;对于只能在中断处理函数中,周期性更新position的硬件设备,一般来说,计算获得的delta值应该等于上面第 6 步中计算获得的硬件指针。这里处理delta大于第 6 步中计算获得的硬件指针的情况。硬件操作的数据越过 DMA 缓冲区绕回到缓冲区开头部分,但runtime->hw_ptr_base还没有来得及更新?这里通过计算距离上次更新硬件指针经过的时间,来确认这种情况。如果是这种情况,则重新计算硬件指针基指针和新的硬件指针。处理第 6 步中计算获得的硬件指针小于旧的硬件指针的情况。一般来说,硬件操作的数据越过 DMA 缓冲区绕回到缓冲区开头部分时,会出现这种情况。此时重新计算硬件指针基指针和新的硬件指针。这一步和上面的第 7 步处理不同情况下硬件操作的数据越过 DMA 缓冲区绕回到缓冲区开头部分的情况。
计算新的硬件指针和老的硬件指针的差值,以备后用。(
runtime->boundary一般来说是一个巨大的值,暂不关注各个值和它的比较。)处理 no_period_wakeup 的情况。音频数据传输不使用 DMA 或使用了 DMA 但不使用 DMA 的完成中断属于这种情况。这一步的处理与上面第 7 步的处理互斥。这里检测 xrun 的情况,即数据实际消耗的量,比按照时间计算预期应该消耗的量少了很多的情况。
执行时间检查,根据需要重新计算硬件指针基指针和新的硬件指针。在上面的第 10 步中,
hdelta用作以 jiffy 时间计的临时变量,这里用作以帧计的临时变量。这里主要处理数据的预期消耗时间比实际时间大的情况。此时会根据数据预期消耗的时间,基于旧的硬件指针重新计算硬件指针基指针和新的硬件指针。执行
delta检查。发现硬件指针不更新时,简单地更新
runtime->hw_ptr_jiffies指针,获得时间戳并返回。如果函数是在中断上下文调用的,更新
runtime->hw_ptr_interrupt指针。更新
runtime->hw_ptr_base、runtime->status->hw_ptr和runtime->hw_ptr_jiffies指针。更新时间戳,并更新流的状态。
snd_pcm_update_hw_ptr0() 函数主要基于经过的时间和帧数来更新硬件指针等。音频数据是时间敏感的,相关各个指针的更新按照其含义进行,但需要处理硬件数据访问越过 DMA 缓冲区边界,绕回到缓冲区开始部分的情况,以及硬件数据处理,相对于上层应用程序与 DMA 缓冲区交换数据过快或过慢的情况。
硬件指针更新之后,上层应用程序可以检测到 DMA 缓冲区中有新的空间可用,继而推动上层应用程序执行下一轮和 DMA 缓冲区的数据交换。硬件设备驱动程序的 pointer 操作是一个非常重要的操作,需要仔细处理。
Done.
Linux 内核音频数据传递主要流程 (下)的更多相关文章
- Linux内核网络数据包处理流程
Linux内核网络数据包处理流程 from kernel-4.9: 0. Linux内核网络数据包处理流程 - 网络硬件 网卡工作在物理层和数据链路层,主要由PHY/MAC芯片.Tx/Rx FIFO. ...
- Linux内核二层数据包接收流程
本文主要讲解了Linux内核二层数据包接收流程,使用的内核的版本是2.6.32.27 为了方便理解,本文采用整体流程图加伪代码的方式从内核高层面上梳理了二层数据包接收的流程,希望可以对大家有所帮助.阅 ...
- linux 内核网络数据包接收流程
转:https://segmentfault.com/a/1190000008836467 本文将介绍在Linux系统中,数据包是如何一步一步从网卡传到进程手中的. 如果英文没有问题,强烈建议阅读后面 ...
- linux内核打印数据到串口控制台,printk数据不打印问题
linux内核打印数据到串口控制台问题 原文来源:http://i.cnblogs.com/EditPosts.aspx?opt=1 1.查看当前控制台的打印级别 cat /proc/sys/kern ...
- Linux内核之数据双链表
导读 Linux 内核中自己实现了双向链表,可以在 include/linux/list.h 找到定义.我们将会首先从双向链表数据结构开始介绍内核里的数据结构.为什么?因为它在内核里使用的很广泛,你只 ...
- Bluedroid: 音频数据的传输流程
一. UIPC: Audio Flinger获取到a2dp的hw module,然后蓝牙协议栈有专用于发送和接收media数据的线程,名称:btif_media_task. 蓝牙与Audio的 ...
- Linux内核 网络数据接收流程图
各层主要函数以及位置功能说明: 1)sock_read:初始化msghdr{}的结构类型变量msg,并且将需要接收的数据存放的地址传给msg.msg_iov->iov_base. ...
- linux内核分析笔记----上半部与下半部(下)
接着上节的来,我们在上节说了软中断和tasklet,那这最后就是工作队列了哦.. 工作队列和前面讨论的其他形式都不相同,它可以把工作推后,交由一个内核线程去执行----该工作总是会在进程上下文执行.这 ...
- Linux内核hlist数据结构分析
在内核编程中哈希链表hlist使用非常多,比方在openvswitch中流表的存储中就使用了(见[1]).hlist的表头仅有一个指向首节点的指针.而没有指向尾节点的指针,这样在有非常多个b ...
- vuex数据传递的流程
当组件修改数据的时候必须通过store.dispacth来调用actions中的方法. 当actions中的方法被触发的时候通过调用commit的方法来触发mutations里面的方法 mutatio ...
随机推荐
- 小知识:设置archive_lag_target参数强制日志切换
为客户测试一个ADG场景问题,发现测试环境的日志切换频率过低,总是需要定期手工切换,这非常影响测试心情. 实际上,可以设置archive_lag_target参数强制日志切换. 比如设置: alter ...
- 基于go语言的声明式流式ETL,高性能和弹性流处理器
简要介绍Benthos,并给出若干示例,指导如何安装和运行. Benthos Benthos 是一个开源的.高性能和弹性的数据流处理器,能够以各种代理模式连接各种源和汇,可以帮助用户在不同的消息流之间 ...
- PictureBox保存图片照片到数据库
Private Sub PAPHOTO_SAVE() Try If TxtPictureURL.Text.ToString <> "" Then Dim SQL_Str ...
- Vue根据时间戳制作倒计时15分钟
废话不多说直接上代码 <script> export default { data() { return { downTimeShow: true, timer: null, downTi ...
- 二进制部署k8s集群
部署k8s有多种方式,本章我们采取二进制的部署方式来部署k8s集群,二进制部署麻烦点,但是可以在我们通过部署各个组件的时候,也通知能让我们更好的深入了解组件之间的关联,也利于后期维护 主机环境 系统: ...
- RStuido Server 选择不同的 R 版本(conda 中的不同 R 版本)
自从上一次服务器重装系统之后,总感觉缺少了一些东西,安装R包很多依赖库报错,也可以解决,但总是存在,烦. 一天,一个同事问我说ggpubr包安装不成功,我就自己试了一下,真的是--安装不成功. 当你到 ...
- 让ChatGPT来写今年的高考作文,能得几分?
使用最新的ChatGPT4模型,做2023年全国甲卷的高考作文. 作文考试题目如下 人们因技术发展得以更好地掌控时间,但也有人因此成了时间的仆人.这句话引发了你怎样的联想与思考?请写一篇文章. 要求: ...
- java后端接入微信小程序登录功能
前言 此文章是Java后端接入微信登录功能,由于项目需要,舍弃了解密用户信息的session_key,只保留openid用于检索用户信息 后端框架:spring boot 小程序框架:uniapp 流 ...
- R数据分析:解决科研中的“可重复危机”,理解Rmarkdown
不知道刚接触科研的大伙儿有没有这么一个感觉,别人的研究很大可能你重复不出来,尤其是社科实证研究,到现在我都还觉得所谓的实证是个很玄乎的东西: 如果是刚开始做数据分析,很多时候你会发现自己的分析结果过几 ...
- 前端vue可以左右滚动的切换的tabs tabs选项卡 滑动动画效果 自动宽度
前端vue可以左右滚动的切换的tabs tabs选项卡 滑动动画效果 自动宽度, 下载完整代码请访问https://ext.dcloud.net.cn/plugin?id=13003 效果图如下: ...