你好!这里是风筝的博客,

欢迎和我一起交流。


PCM 数据管理可以说是 ALSA 系统中最核心的部分。
不管是录音还是播放,都要用到buffer管理数据。

  • 播放:copy_from_user 把用户态的音频数据拷贝到 buffer 中,启动 dma 设备把音频数据从 buffer 传送到 I2S tx FIFO。
  • 录音:启动 dma 设备把音频数据从 I2S rx FIFO 传送到 buffer, copy_to_user 把 buffer 中音频数据拷贝到用户态。

ALSA buffer是采用ring buffer来实现的。ring buffer有多个HW buffer(虚拟)组成。
之所以采用多个HW buffer来组成ring buffer,是防止读写指针的前后位置频繁的互换(即写指针到达HW buffer边界时,就要回到HW buffer起始点)。
这里采用droidphone博客里的一段话作为描述:
(本来想去alsa官网找下解释的,结果里面就只有这个:https://www.alsa-project.org/wiki/PCM_Ring_Buffer)

理想情况下,大小为Count的缓冲区具备一个读指针和写指针,我们期望他们都可以闭合地做环形移动,但是实际的情况确实:缓冲区通常都是一段连续的地址,他是有开始和结束两个边界,每次移动之前都必须进行一次判断,当指针移动到末尾时就必须人为地让他回到起始位置。在实际应用中,我们通常都会把这个大小为Count的缓冲区虚拟成一个大小为n*Count的逻辑缓冲区,相当于理想状态下的圆形绕了n圈之后,然后把这段总的距离拉平为一段直线,每一圈对应直线中的一段,因为n比较大,所以大多数情况下不会出现读写指针的换位的情况(如果不对buffer进行扩展,指针到达末端后,回到起始端时,两个指针的前后相对位置会发生互换)。扩展后的逻辑缓冲区在计算剩余空间可条件判断是相对方便。alsa driver也使用了该方法对dma buffer进行管理:

  • hw_ptr_base:当前HW buffer在Ring buffer中的起始位置。当读指针到达HW buffer尾部时,hw_ptr_base按buffer size移动.
  • hw_ptr:硬件逻辑位置,播放时相当于读指针,录音时相当于写指针。
  • appl_ptr:应用逻辑位置,播放时相当于写指针,录音时相当于读指针。
  • boundary:扩展后的逻辑缓冲区大小,通常是(2^n)*size。
  • buffer_size:HW buffer的大小,大小为period_size * period_count 。
  • avail:HW buffer中空闲的地址,我们可以稳定的通过一个公式获取avail:
static inline snd_pcm_uframes_t snd_pcm_playback_avail(struct snd_pcm_runtime *runtime)
{snd_pcm_sframes_t avail = runtime->status->hw_ptr + runtime->buffer_size - runtime->control->appl_ptr;if (avail < 0)avail += runtime->boundary;else if ((snd_pcm_uframes_t) avail >= runtime->boundary)avail -= runtime->boundary;return avail;
}

HW buffer的size可以通过ALSA library的API进行修改,即修改period_size 和 period_count。
如果buffer设得太大,那么一次数据的传输需要的延迟会增加,为了解决这个问题,ALSA将buffer分为一系列的period(在OSS/Free语境中称为fragment),然后以period为单位进行数据的传输。
关于period的介绍在alsa官网有,我之前也有翻译过:Frames Periods

HW buffer的硬件逻辑指针(hw_ptr)主要由 snd_pcm_update_hw_ptr0函数跟新。

  • DMA传输完成一个period_size之后通过在中断里snd_pcm_period_elapsed调用snd_pcm_update_hw_ptr0跟新。
  • 数据读/写/重置(snd_pcm_lib_read1/snd_pcm_lib_write1/snd_pcm_lib_ioctl_reset)时通过snd_pcm_update_hw_ptr调用snd_pcm_update_hw_ptr0跟新。
  • snd_pcm_playback_forward/snd_pcm_capture_forward通过调用snd_pcm_update_hw_ptr跟新。
  • snd_pcm_do_pause暂停时通过调用snd_pcm_update_hw_ptr跟新。

HW buffer的应用逻辑指针(appl_ptr)更新有两种:

  • 用户空间调用write函数往缓冲区中写入数据时, 在内核层snd_pcm_write -> snd_pcm_lib_write -> snd_pcm_lib_write1函数会计算appl_ptr的新位置, 并更新该参数。
  • 用户空间通过mmap的方式往缓冲区中写入数据时, 在mmap方式下, 内核并不知道用户空间何时完成写入了, 因此用户空间完成写入时需要通过某种方式告知内核. alsa提供了ioctl SNDRV_PCM_IOCTL_SYNC_PTR, 供用户空间通知内核更新appl_ptr, 例如tinyalsa中的pcm_sync_ptr采用的就是这种方式. 在内核层, snd_pcm_common_ioctl1 -> snd_pcm_sync_ptr 会最终更新该参数。
    .

log演示

这里我们通过配置XRUN_DEBUG和TRACE,用trace工具抓取一段hw_ptr更新过程的log:

        tinyplay-2528  [000] d..2   587.028041: hwptr: pcmC0D0p/sub0: POS: pos=32, old=0, base=0, period=1024, buf=4096<idle>-0     [000] d.h3   587.048548: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=32, base=0, period=1024, buf=4096Sadbd-2531  [000] d.h4   587.069895: hwptr: pcmC0D0p/sub0: IRQ: pos=2048, old=1024, base=0, period=1024, buf=4096<idle>-0     [000] d.h3   587.091223: hwptr: pcmC0D0p/sub0: IRQ: pos=3072, old=2048, base=0, period=1024, buf=4096<idle>-0     [000] d.h3   587.112541: hwptr: pcmC0D0p/sub0: IRQ: pos=0, old=3072, base=0, period=1024, buf=4096tinyplay-2528  [000] d..2   587.112764: hwptr: pcmC0D0p/sub0: POS: pos=0, old=4096, base=4096, period=1024, buf=4096<idle>-0     [000] d.h3   587.133875: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=4096, base=4096, period=1024, buf=4096<idle>-0     [000] d.h3   587.155209: hwptr: pcmC0D0p/sub0: IRQ: pos=2048, old=5120, base=4096, period=1024, buf=4096<idle>-0     [000] d.h3   587.176541: hwptr: pcmC0D0p/sub0: IRQ: pos=3072, old=6144, base=4096, period=1024, buf=4096<idle>-0     [000] d.h3   587.197872: hwptr: pcmC0D0p/sub0: IRQ: pos=0, old=7168, base=4096, period=1024, buf=4096tinyplay-2528  [000] d..2   587.198069: hwptr: pcmC0D0p/sub0: POS: pos=0, old=8192, base=8192, period=1024, buf=4096<idle>-0     [000] d.h3   587.219212: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=8192, base=8192, period=1024, buf=4096<idle>-0     [000] d.h3   587.240541: hwptr: pcmC0D0p/sub0: IRQ: pos=2048, old=9216, base=8192, period=1024, buf=4096<idle>-0     [000] d.h3   587.261876: hwptr: pcmC0D0p/sub0: IRQ: pos=3072, old=10240, base=8192, period=1024, buf=4096<idle>-0     [000] d.h3   587.283201: hwptr: pcmC0D0p/sub0: IRQ: pos=0, old=11264, base=8192, period=1024, buf=4096
  • hwptr: pcmC0D0p/sub0: POS:代表用户层读写数据等操作时更新hw_ptr的log。
  • hwptr: pcmC0D0p/sub0: IRQ:代表DMA传输中断时更新hw_ptr的log。

这段log里面实时记录了pos、old_hw_ptr、hw_ptr_base、period_size、buf_size的更新过程,可以结合我们的代码一起看:

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 timespec curr_tstamp;struct timespec audio_tstamp;int crossed_boundary = 0;old_hw_ptr = runtime->status->hw_ptr;//保存上一次的hw_ptrpos = substream->ops->pointer(substream);//DMA以及搬运了的数据量,正常情况下pos每次递增period_size,最大为buf_size,但是递增到buf_size时pos会清零,因为pos=buf_size-DMA搬运数据量。curr_jiffies = jiffies;//......if (pos == SNDRV_PCM_POS_XRUN) {//发生XRUNxrun(substream);return -EPIPE;}if (pos >= runtime->buffer_size) {//按pos计算的描述,理论上pos不会>=buf_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;//pos地址对齐trace_hwptr(substream, pos, in_interrupt);//通过trace打印调试hw_base = runtime->hw_ptr_base;//当前的hw_basenew_hw_ptr = hw_base + pos;//当前的hw_ptrif (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;//期望下一个hw_ptr的值if (delta > new_hw_ptr) {//如果期望的hw_ptr比当前计算出来的hw_ptr大的话,则说明上一次中断没处理/* check for double acknowledged interrupts */hdelta = curr_jiffies - runtime->hw_ptr_jiffies;if (hdelta > runtime->hw_ptr_buffer_jiffies/2 + 1) {//距离上一次的jiffies大于整个buffer 的jiffies的一半hw_base += runtime->buffer_size;//hw_base需要更新到下一个HW buffer的基地址if (hw_base >= runtime->boundary) {//超过Ring Buffer总和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 *///传输完成一个buf_size的话,pos此时为0,hw_ptr超过了HW buffer边界,此条件则成立。hw_base需要更新到下一个HW buffer的基地址。if (new_hw_ptr < old_hw_ptr) {hw_base += runtime->buffer_size;if (hw_base >= runtime->boundary) {//如果hw_base > boundary,那hw_base回跳到Ring Buffer起始位置hw_base = 0;crossed_boundary++;}new_hw_ptr = hw_base + pos;//重新更新正确的new_hw_ptr}
__delta:delta = new_hw_ptr - old_hw_ptr;//hw_ptr相较上一次的偏移值,理论上为period_sizeif (delta < 0)//如果当前计算出来的hw_ptr任然比上一的hw_ptr小,说明hw_ptr走完了Ring buffer一圈delta += runtime->boundary;//....../* something must be really wrong */if (delta >= runtime->buffer_size + runtime->period_size) {//如果当前hw_ptr比较上一次相差buffer size + peroid 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;}//......
no_jiffies_check://delta(如果当前hw_ptr比较上一次之差)>1.5个peroid size,可能是interupt丢失?理论上delta == period_sizeif (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) {//hw_ptr没变化,直接返回,等待下一次更新posupdate_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);//播放silence静音if (in_interrupt) {//更新hw_ptr_interruptdelta = 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中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);
}

主要流程参考注释,这里简单对着之前的log说下:

        tinyplay-2528  [000] d..2   587.028041: hwptr: pcmC0D0p/sub0: POS: pos=32, old=0, base=0, period=1024, buf=4096<idle>-0     [000] d.h3   587.048548: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=32, base=0, period=1024, buf=4096Sadbd-2531  [000] d.h4   587.069895: hwptr: pcmC0D0p/sub0: IRQ: pos=2048, old=1024, base=0, period=1024, buf=4096<idle>-0     [000] d.h3   587.091223: hwptr: pcmC0D0p/sub0: IRQ: pos=3072, old=2048, base=0, period=1024, buf=4096<idle>-0     [000] d.h3   587.112541: hwptr: pcmC0D0p/sub0: IRQ: pos=0, old=3072, base=0, period=1024, buf=4096tinyplay-2528  [000] d..2   587.112764: hwptr: pcmC0D0p/sub0: POS: pos=0, old=4096, base=4096, period=1024, buf=4096<idle>-0     [000] d.h3   587.133875: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=4096, base=4096, period=1024, buf=4096

一开始log是hwptr: pcmC0D0p/sub0: POS,表明是write里面调用snd_pcm_update_hw_ptr跟新hw_ptr,
此时write里面发送了32frames,pos也就是32,上一次hw_ptr是0,HW buffer基地址base是0,推知当前hw_ptr是32,period_size是1024,period_count是4,buf_size是4096。

接下来就是DMA中断产生,在中断里调用snd_pcm_update_hw_ptr0函数跟新hw_ptr:

第一次中断,传输了period_size,所以pos是1024,old是上一次的hw_ptr,也就是32,HW buffer基地址base还是是0。
第二次中断,再次传输了period_size,所以pos是2048,old是上一次的hw_ptr,也就是1024,HW buffer基地址base还是是0。
第三次中断,再次传输了period_size,所以pos是3072,old是上一次的hw_ptr,也就是2048,HW buffer基地址base还是是0。
第四次中断,再次传输了period_size,此时dma数据传完了(因为buf是4096,一次传1024,一共传4次)所以pos是0,old是上一次的hw_ptr,也就是3072,HW buffer基地址base还是是0。

接下来的log就不是由DMA中断里跟新hw_ptr了,因为hwptr: pcmC0D0p/sub0: POS
所以这条log里面,pos还是0,没更新,但是old是上一次的hw_ptr,是4096,HW buffer基地址base就变成4096了!!!
然后往复循环,周而复始~~

至此,alsa dma buffer里hw_ptr的更新梳理就到此结束了,完结撒花~


trace文件在这:sound/core/pcm_trace.h

参考:
Linux ALSA声卡驱动之八:ASoC架构中的Platform
ALSA driver–HW Buffer
ALSA & ASOC

ALSA子系统(十二)------ALSA Buffer的更新相关推荐

  1. Linux ALSA 之十:ALSA ASOC Machine Driver

    ALSA ASOC Machine Driver 一.Machine 简介 二.ASoC Machine Driver 2.1 Machine Driver 的 Platform Driver &am ...

  2. JAVA中的GridView每一个赋值,在ASP.NET 2.0中操作数据之六十二:GridView批量更新数据...

    导言: 在前面的教程,我们对数据访问层进行扩展以支持数据库事务.数据库事务确保一系列的操作要么都成功,要么都失败.本文我们将注意力转到创建一个批更新数据界面. 在本文,我们将创建一个GridView控 ...

  3. ALSA子系统(十七)------支持Type-C耳机驱动

    你好!这里是风筝的博客, 欢迎和我一起交流. 之前讲解了耳机驱动: ALSA子系统(十六)------虚拟耳机驱动 Android音频子系统(四)------耳机拔插流程 那么必然少不了现在市场上较多 ...

  4. Linux时间子系统之(十二):periodic tick

    专题文档汇总目录 Notes:TickDevice模式,以及clocckevent设备.TickDevice设备的初始化,TickDevice是如何加入到系统中的.周期性Tick的产生. 原文地址:L ...

  5. linux系统tick维护,Linux时间子系统之(十二):periodic tick

    Linux时间子系统之(十二):periodic tick 作者:linuxer 发布于:2014-12-11 18:59 分类:时间子系统 一.tick device概念介绍 1.数据结构 在内核中 ...

  6. Wix 安装部署教程(十二) -- 自动更新WXS文件

    原文:Wix 安装部署教程(十二) -- 自动更新WXS文件 上一篇分享了一个QuickWIX,用来对比两个工程前后的差异,但是这样还是很繁琐,而且昨天发现有Bug,目录对比有问题.这次改变做法,完全 ...

  7. 详解Unity中的粒子系统Particle System (十二 | 终)

    前言 终于来到了最后一篇,粒子系统宣告终结!这十来篇博客删删改改写了半个多月,真是离谱.今天该讲案例与粒子系统的应用,那么我们就进入正题吧! 目录 前言 本系列提要 一.如何做出效果 二.案例演示 1 ...

  8. 计算机系统组成怎么讲PPT,计算机组成原理第十二讲(存储子系统二)ppt课件.ppt

    <计算机组成原理第十二讲(存储子系统二)ppt课件.ppt>由会员分享,提供在线免费全文阅读可下载,此文档格式为ppt,更多相关<计算机组成原理第十二讲(存储子系统二)ppt课件.p ...

  9. 【2021/7/19更新】【梳理】简明操作系统原理 第十二章 机械硬盘 磁盘I / O的调度(docx)

    配套教材: Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Re ...

  10. 嵌入式linux alsa,嵌入式Linux下ALSA音频架构ALSA-lib移植与编译心得

    **************************************************************************************************** ...

最新文章

  1. python自动办公 pdf_Python办公自动化|批量合并PDF,拿来就用
  2. Atitit 桌面软件跨平台gui解决方案 javafx webview
  3. slqite3库查询数据处理方式_绝活!十一个优质React Hook库, 收藏备用
  4. 中国式离婚中,林与宋离婚成为定局的时刻
  5. ASP.NET MVC 5 SmartCode Scaffolding for Visual Studio.Net
  6. Noise,Error,wighted pocket Algorithm
  7. WINDOWS杀进程的命令
  8. 设置windows自动登录
  9. U8系统UFO报表无法打印
  10. 【Luat-esp32】0 快速入门
  11. #644 (Div. 3)F. Spy-string(暴力枚举)
  12. 互联网金融的信息安全(一)新环境的安全形势
  13. 不用修改flash源文件,给.swf 加链接地址方法
  14. Gym 100134L - Labyrinth of the Minotaur
  15. 如何用紧凑型语音表征打造高性能语音合成系统
  16. Unity游戏存档 (将游戏数据储存至本地文档)
  17. modelandview找不到视图_当一个测试工程师准备找工作,需要准备什么?
  18. 城市间紧急救援(C语言)
  19. 常用的软件测试工具清单,建议收藏。
  20. 《Linux内核修炼之道》精华版 之 方法论(提供pdf下载)

热门文章

  1. 公司的到一个B类地址块,需要划分成若干个包含1000台主机的子网,则可以划分成几个?...
  2. QQ返利圣诞8天购物狂欢节(12月19~12月26日)
  3. 第364章_显化一万年_神墓_辰东_玄幻小说
  4. App - Download
  5. Android System App
  6. 第2章 开锋你的绝世名刃—— Visual Studio 开发环境的安装、配置
  7. 前端性能分析 Chrome Dev Tools 之 Performance
  8. Chrome不允许在页面关闭或导航跳转时发送同步请求
  9. java根据下载地址下载文件到本地
  10. STC52单片机 第三个定时器 定时器2详解