在数据从缓冲区复制到音频控制器的过程中,通常会使用DMA, DMA对声卡而言非常重要。例如在放音时,驱动设置完DMA控制器的源数据地址(内存中DMA缓冲区)、目标地址(音频控制器FIFO)和DMA的数据长度,DMA控制器会自动发送缓冲区的数据填充FIFO,直到发送完相应的数据长度后才中断一次。
中断在声卡中,表现为一个period的数据传输完毕会触发中断,然后更新 position
在azx_first_init()中会有interrupt的初始化工作
if (azx_acquire_irq(chip, 0) < 0)
在azx_acquire_irq()中通过下面方式来设置interrupt handler
if (request_irq(chip->pci->irq, azx_interrupt,
chip->msi ? 0 : IRQF_SHARED,
KBUILD_MODNAME, chip))
驱动程序可以通过request_irq()函数注册一个中断处理程序,并激活给定的中断线,以处理中断。上面的参数分别表示:1)irq表示要分配的中断号,2)表示实际的的中断处理程序,3)中断线可以共享,4)设备名,5)传递my_dev变量给dev形参。
下面具体分析azx_interrupt函数:
status = azx_readl(chip, INTSTS);
INTSTS 位于spec offset 24h,这是读取中断状态标志位。这个interrupt status register 是一个4byte的寄存器,具体有以下状态信息:
bit31: 这是一个全局的中断标志位
bit30: Controller interrupt status, 1表示中断发生,而且是一个response 中断,可能是response overrun,或者codec state change,这需要查询RIRB状态寄存器来得知。
bit29-0: stream interrupt status, stream是被编号过的,这个SIS位就是根据stream的order来以此分配的。
sd_status = azx_sd_readb(chip, azx_dev, SD_STS);
azx_sd_writeb(chip, azx_dev, SD_STS, SD_INT_MASK);
上面是用来读取 stream descriptors status 寄存器的值,那stream descriptors 寄存器是用来干嘛的呢,它是用来控制DMA engine的,以SD_status寄存器为例,里面有关于DMA FIFO 的一些设置。关于FIFO、DMA的一些内容在下面链接里有比较详细的描述https://www.cnblogs.com/yfz0/p/5865565.html
读取状态信息以后,就根据mask的值来分析判断是什么错误,然后就是中断判断和处理
if (!azx_dev->substream || !azx_dev->running ||
!(sd_status & SD_INT_COMPLETE))
continue;
/* check whether this IRQ is really acceptable */
if (!chip->ops->position_check ||
chip->ops->position_check(chip, azx_dev)) {
spin_unlock(&chip->reg_lock);
snd_pcm_period_elapsed(azx_dev->substream);
spin_lock(&chip->reg_lock);
}
}
首先上面涉及到一个函数:position_check(),这是hda_controller_ops中的一个操作,用来检查当前的position是否是可接受的(acceptable),具体的实现函数就是azx_position_check()函数。
/* called from IRQ */
static int azx_position_check(struct azx *chip, struct azx_dev *azx_dev)
{
int ok;
ok = azx_position_ok(chip, azx_dev);
if (ok == 1) {
azx_dev->irq_pending = 0;
return ok;
} else if (ok == 0 && chip->bus && chip->bus->workq) {
/* bogus IRQ, process it later */
azx_dev->irq_pending = 1;
queue_work(chip->bus->workq, &chip->irq_pending_work);
}
return 0;
}
这里主要就是确定这个当前的DMA position是否已经可以用于更新periods。许多HDA controller 似乎对更新irq时间相当不准确。IRQ在实际处理数据之前发布。所以,我们需要在工作队列中处理它。
/*
* Check whether the current DMA position is acceptable for updating
* periods. Returns non-zero if it's OK.
*
* Many HD-audio controllers appear pretty inaccurate about
* the update-IRQ timing. The IRQ is issued before actually the
* data is processed. So, we need to process it afterwords in a
* workqueue.
*/
static int azx_position_ok(struct azx *chip, struct azx_dev *azx_dev)
{
u32 wallclk;
unsigned int pos;
wallclk = azx_readl(chip, WALLCLK) - azx_dev->start_wallclk;
if (wallclk < (azx_dev->period_wallclk * 2) / 3)
return -1; /* bogus (too early) interrupt */
pos = azx_get_position(chip, azx_dev, true);
if (WARN_ONCE(!azx_dev->period_bytes,
"hda-intel:ero azx_dev->period_bytes"))
return -1; /* this shouldn't happen! */
if (wallclk < (azx_dev->period_wallclk * 5) / 4 &&
pos % azx_dev->period_bytes > azx_dev->period_bytes / 2)
/* NG - it's below the first next period boundary */
return chip->bdl_pos_adj[chip->dev_index] ? 0 : -1;
azx_dev->start_wallclk += wallclk;
return 1; /* OK, it's fine */
}
这里需要解释几个概念,wallclk、period、bdl_pos_adj.
wallclk
这里的wallclk 是定义为 period 的计数器
period—周期(https://alsa-project.org/wiki/FramesPeriods)
一个frame就等于一个要被播放的声音样本。frame与通道数channels和样本长度(sample bits)无关。
1帧立体声48khz 16位的pcm数据长度为4bytes
1frame 5.1声道48khz 16 bits PCM stream 长度为12bytes
一个period就是每两次硬件中断之间的帧数,poll()会每个周期return一次。(就是处理一个period中断一次)
buffer是一个环形buffer,大小一般来说比一个period size大,一般设做 2 * period size,但是一些硬件可以支持到8个周期大小的buffer,也可以设为非整数倍的period的大小。
现在如果我们的硬件设置为48khz,2周期,每个周期1024个帧,那么buffer size 就是2048个帧。硬件每处理一个buffer会中断2次,alsa会一直让buffer保持一个满的状态,每当第一个周期的样本播放完成,第二个周期的数据就会被播放,同时第三个周期的数据就会进入到第一个周期数据的位置。
另一个例子:
假设我们将要使用一个立体声 16位 44.1k的音频流,单向(录音或者播放),那么我们就有
立体声 = 2通道
1个样本长度 16bits = 2bytes
1个帧 代表 所有通道的一个样本。那么我们现在是双通道,所以 1帧 = (通道数) * (样本大小bytes) = 2 * 2 = 4bytes
为了能支持2 * 44.1k的采样率(一秒钟的采样frams),系统必须支持如下的速度
bsp_rate = (通道数) * (1个样本长度) * (采样率) = 1帧 * 采样率 = 2 * 2 * 44.1k = 176400bytes/sec
现在 alsa每秒都中断。那么我们每秒都需要176400byte数据准备好,才能供上一个 双通道 16 位 44.1k的音频流。
如果半秒中断一次,那么每次中断就是 176400 / 2 = 88200 bytes
如果100ms中断一次,那么我们就需要 176400 * 0.1 = 17640 byte
我们可以通过设置period size 来控制pcm中断的产生。 如果我们设置一个16位双通道44.1k的音频流 并且每次都有44100帧数据 => 4 byte * 44100frams = 176400字节 => 一次中断会需要176400字节的数据 => 那么他就是100ms中断一次。
alsa会自己根据runtime时的信息定义实际的buffer_size 和period_size,这取决于:请求的channel数、它们各自的属性(速率和采样分辨率)以及snd_pcm_hardware struct(在驱动程序中)中设置的参数。
On major sound hardwares, a ring-bufferis divided to several parts and an irq is issued on each boundary.The period_size defines the size of this chunk.
不过实际使用中period size的概念有点不一样,HDA使用dma来控制数据的传输,中断的触发是根据period size来产生的,比如period size设置成1024bytes,那么dma搬运完1024bytes后,中断就会被触发,而不是像上面的例子固定时间触发的!
下面是网上的一些明细
关于period和buffer_size之间的关系:
帧代表一个单位 1帧 = 通道数 * 样本长度
在你的情况下,1帧占据了 2 通道* 16位 = 4个字节
periods(周期数)就是在环形buffer里面的 period的数量
buffer_size = period_size * periods
period_bytes = period_size * bytes_per_frame
bytes_per_frame = channels * bytes_per_sample
bdl_pos_adj(int bdl_pos_adj; /* BDL position adjustment */)
buffer descriptor list,是一个用来描述内存中 buffer 的 structure,他是由一些 entry 组成,在代码中是这样定义的:
hda_intel.c 中这样定义:
static int bdl_pos_adj[SNDRV_CARDS] = {[0 ... (SNDRV_CARDS-1)] = -1};
azx_create(){
.....
if (bdl_pos_adj[dev] < 0) {
switch (chip->driver_type) {
case AZX_DRIVER_ICH:
case AZX_DRIVER_PCH:
bdl_pos_adj[dev] = 1;
break;
default:
bdl_pos_adj[dev] = 32;
break;
}
}
chip->bdl_pos_adj = bdl_pos_adj;
....
}
上面这三个概念搞清楚以后,再回到azx_position_ok()函数中,上下是判断中断发生的条件,其实最重要的就是中间的azx_get_position()这个函数。zhaoxin的获取DMA_position方式是通过下面方式获得的,因为是用的via82xxx的声卡
case POS_FIX_VIACOMBO:
pos = azx_via_get_position(chip, azx_dev);
/* get the current DMA position with correction on VIA chips */
static unsigned int azx_via_get_position(struct azx *chip,
struct azx_dev *azx_dev)
{
unsigned int link_pos, mini_pos, bound_pos;
unsigned int mod_link_pos, mod_dma_pos, mod_mini_pos;
unsigned int fifo_size;
link_pos = azx_sd_readl(chip, azx_dev, SD_LPIB);
if (azx_dev->substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
/* Playback, no problem using link position */
return link_pos;
}
/* Capture */
/* For new chipset,
* use mod to get the DMA position just like old chipset
*/
mod_dma_pos = le32_to_cpu(*azx_dev->posbuf);
mod_dma_pos %= azx_dev->period_bytes;
/* azx_dev->fifo_size can't get FIFO size of in stream.
* Get from base address + offset.
*/
fifo_size = readw(chip->remap_addr + VIA_IN_STREAM0_FIFO_SIZE_OFFSET);
if (azx_dev->insufficient) {
/* Link position never gather than FIFO size */
if (link_pos <= fifo_size)
return 0;
azx_dev->insufficient = 0;
}
if (link_pos <= fifo_size)
mini_pos = azx_dev->bufsize + link_pos - fifo_size;
else
mini_pos = link_pos - fifo_size;
/* Find nearest previous boudary */
mod_mini_pos = mini_pos % azx_dev->period_bytes;
mod_link_pos = link_pos % azx_dev->period_bytes;
if (mod_link_pos >= fifo_size)
bound_pos = link_pos - mod_link_pos;
else if (mod_dma_pos >= mod_mini_pos)
bound_pos = mini_pos - mod_mini_pos;
else {
bound_pos = mini_pos - mod_mini_pos + azx_dev->period_bytes;
if (bound_pos >= azx_dev->bufsize)
bound_pos = 0;
}
/* Calculate real DMA position we want */
return bound_pos + mod_dma_pos;
}
所以可以看到,在播放情况下,是通过LPIB(link position in buffer)方式返回 DMA position。在capture情况下,使用VIACOMBO的方式来获取DMA position。
获得position以后,回到azx_interrupt函数中的snd_pcm_period_elapsed(azx_dev->substream);该函数是用来更新pcm status
/**
* snd_pcm_period_elapsed - update the pcm status for the next period
* @substream: the pcm substream instance
*
* This function is called from the interrupt handler when the
* PCM has processed the period size. It will update the current
* pointer, wake up sleepers, etc.
*
* Even if more than one periods have elapsed since the last call, you
* have to call this only once.
*/
void snd_pcm_period_elapsed(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime;
unsigned long flags;
if (PCM_RUNTIME_CHECK(substream))
return;
runtime = substream->runtime;
if (runtime->transfer_ack_begin)
runtime->transfer_ack_begin(substream);
snd_pcm_stream_lock_irqsave(substream, flags);
if (!snd_pcm_running(substream) ||
snd_pcm_update_hw_ptr0(substream, 1) < 0)
goto _end;
if (substream->timer_running)
snd_timer_interrupt(substream->timer, 1);
_end:
snd_pcm_stream_unlock_irqrestore(substream, flags);
if (runtime->transfer_ack_end)
runtime->transfer_ack_end(substream);
kill_fasync(&runtime->fasync, SIGIO, POLL_IN);
}
这里面最重要的就是snd_pcm_update_hw_ptr0()函数
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;
/*
* 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
*/
//dma硬件读当前相对dma buffer address的偏移,
//每次拿到的pos相对上一次的偏移量其实就是peroid_size,
//也就是说每次读的size受peroid_size控制。
//而最多的pos是受buffer_size控制,当到达buffer_size之后,就会重置
pos = substream->ops->pointer(substream);
......
//runtime->hw_ptr_base以buffer_size的单位移动
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;
......
}
/* new_hw_ptr might be lower than old_hw_ptr in case when */
/* pointer crosses the end of the ring buffer */
//当dma buffer完成一个buffer_size之后,pos又从0开始计算,这时就会成立。
if (new_hw_ptr < old_hw_ptr) {
//hw_base要往后挪
hw_base += runtime->buffer_size;
//防止移动后超过边界
if (hw_base >= runtime->boundary) {
hw_base = 0;
crossed_boundary++;
}
//重新更新正确的new_hw_ptr
new_hw_ptr = hw_base + pos;
}
__delta:
//delta其实就是0或者一个peroid_size
delta = new_hw_ptr - old_hw_ptr;
......
/* Do jiffies check only in xrun_debug mode */
if (!xrun_debug(substream, XRUN_DEBUG_JIFFIESCHECK))
goto no_jiffies_check;
......
no_jiffies_check:
......
no_delta_check:
//当dma没有读到数据,会直接返回,等待下一次中断到来更新pos.
//当用户调用读写接口不更新pos的时候,会是这种情况
if (runtime->status->hw_ptr == new_hw_ptr)
return 0;
......
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;
}
//更新base和hw_ptr值
runtime->hw_ptr_base = hw_base;
runtime->status->hw_ptr = new_hw_ptr;
......
return snd_pcm_update_state(substream, runtime);
}