hda driver 之 azx_interrupt 以及 framebytes DMAposition简单分析

在数据从缓冲区复制到音频控制器的过程中,通常会使用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);
}

猜你喜欢

转载自blog.csdn.net/qq_38350702/article/details/111997024