平台:ubuntu 16.04,kernel版本是4.15.0, 理论任何平台都可以,甚至是android,只要能编译通过。

需要完成的功能:传说中的回采,做过语音方案的童鞋应该能懂,就是播放的音频,录音录回去。因为是虚拟的声卡,不涉及硬件操作,也只能这样看点效果。

目的:当然是为了能更直观的理解alsa驱动框架。虚拟出一个声卡,不涉及复杂的硬件操作,不涉及复杂的硬件调试,只关心数据流怎么一步一步传给应用的。

1.数据是怎么交互的

以playback为例

驱动程序分配一个buffer

APP不断写入一个period的数据到buffer 一个period含有多个frame 一个frame就是一个采样数据

驱动不断从buffer里取出一个period,并发送给codec

app更新appl_ptr指针, 驱动更新hw_ptr指针,当指针更新到buffer尾部,从头开始。

2.分配DMA内存

分配dma内存的是.pcm_new

static int vplat_pcm_new(struct snd_soc_pcm_runtime *rtd) {
	struct snd_card *card = rtd->card->snd_card;
	struct snd_pcm *pcm = rtd->pcm;
	
	struct snd_pcm_substream *substream;
	struct snd_dma_buffer *buf;
	
	int ret = 0;

	if (!card->dev->dma_mask)
		card->dev->dma_mask = &dma_mask;
	if (!card->dev->coherent_dma_mask)
		card->dev->coherent_dma_mask = 0xffffffff;

	if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) {

		playback_info.buf_max_size = vplat_pcm_hardware.buffer_bytes_max;
		substream = pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream;
		playback_info.substream = substream;
		buf = &substream->dma_buffer;
		
		buf->area = dma_alloc_coherent(pcm->card->dev, playback_info.buf_max_size,
					&buf->addr, GFP_KERNEL);
		if (!buf->area) {
			printk(KERN_ERR"plaback alloc dma error!!!\n");
			return -ENOMEM;
		}

    	buf->dev.type = SNDRV_DMA_TYPE_DEV;
    	buf->dev.dev = pcm->card->dev;
    	buf->private_data = NULL;
        buf->bytes = playback_info.buf_max_size;
		
		playback_info.addr = buf->area;
	}

	if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) {
		
		capture_info.buf_max_size = vplat_pcm_hardware.buffer_bytes_max;
		
		substream = pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream;
		capture_info.substream = substream;
		buf = &substream->dma_buffer;
		
		buf->area = dma_alloc_coherent(pcm->card->dev, capture_info.buf_max_size,
					&buf->addr, GFP_KERNEL);
		if (!buf->area) {
			printk(KERN_ERR"catpure alloc dma error!!!\n");
			return -ENOMEM;
		}

    	buf->dev.type = SNDRV_DMA_TYPE_DEV;
    	buf->dev.dev = pcm->card->dev;
    	buf->private_data = NULL;
        buf->bytes = capture_info.buf_max_size;	
		
		capture_info.addr = buf->area;
	}
	return ret;
}

调用dma_alloc_coherent()分配dma buffer,其实就是一块连续的物理内存。

dma在声卡的使用可以看: [RK3288][Android6.0] Audio的DMA调用实例流程 [IMX6DL][Android4.4] Linux dmaengine 使用方法

3.启动定时器,模拟数据传输中断

定时器在vplat_pcm_trigger()启动

static int vplat_pcm_trigger(struct snd_pcm_substream *substream, int cmd) {
	int ret = 0;
	static u8 is_timer_run = 0;
	
	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
		switch (cmd) {
		case SNDRV_PCM_TRIGGER_START:
		case SNDRV_PCM_TRIGGER_RESUME:
		case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
			/* 启动定时器, 模拟数据传输 */
			printk("playback running...\n");
			playback_info.is_running = 1;
			if(!is_timer_run) {
				is_timer_run = 1;
				start_timer();
			}
			break;
		case SNDRV_PCM_TRIGGER_STOP:
		case SNDRV_PCM_TRIGGER_SUSPEND:
		case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
			/* 停止定时器 */
			printk("playback stop...\n");
			playback_info.is_running = 0;
			if(!capture_info.is_running){
				is_timer_run = 0;
				del_timer(&vtimer);
			}
			break;
		default:
			ret = -EINVAL;
			break;
		}
	} else {
		switch (cmd) {
		case SNDRV_PCM_TRIGGER_START:
		case SNDRV_PCM_TRIGGER_RESUME:
		case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
			/* catpure开始接收数据 */		
			printk("capture running...\n");
			capture_info.is_running = 1;
			if(!is_timer_run) {
				is_timer_run = 1;
				start_timer();
			}
			break;
		case SNDRV_PCM_TRIGGER_STOP:
		case SNDRV_PCM_TRIGGER_SUSPEND:
		case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
			/* catpure停止接收数据 */
			printk("capture stop...\n");
			capture_info.is_running = 0;
			if(!playback_info.is_running){
				is_timer_run = 0;
				del_timer(&vtimer);
			}
			break;
		default:
			ret = -EINVAL;
			break;
		}
	}
	return ret;
}

定时器处理函数就是模拟DMA传输完成的中断处理函数,如下:

static void vplat_timer_function(struct timer_list *t) {
	schedule_work(&vplat_work);
}

启动一个work,work的处理函数如下:

static void work_function(struct work_struct *work){
	
	struct snd_pcm_substream *pb_substream = playback_info.substream;
	
	if (capture_info.is_running) {
		load_buff_period();
	}
        
    // 更新状态信息
	if(playback_info.is_running){
		playback_info.buf_pos += playback_info.period_size;
		if (playback_info.buf_pos >= playback_info.buffer_size)
			playback_info.buf_pos = 0;
		
		// 更新hw_ptr等信息,
		// 并且判断:如果buffer里没有数据了,则调用trigger来停止DMA 
		snd_pcm_period_elapsed(pb_substream); 
	}

    if (playback_info.is_running || capture_info.is_running) {
        //再次启动定时器
        mod_timer(&vtimer, jiffies + HZ/10);
    }
}

如果此时正在录音,调用load_buff_period()将playback的buffer数据拷贝到capture的buffer。 调用snd_pcm_period_elapsed()更新hw_ptr指针; load_buff_period()实现如下:

static int load_buff_period(void) {
	struct snd_pcm_substream *cp_substream = capture_info.substream;
	int size = 0;
	
	if(capture_info.addr == NULL) {
		printk(KERN_ERR"catpure addr error!!!\n");
		return -1;
	}

	if (playback_info.is_running) {
		if(capture_info.period_size != playback_info.period_size) {
			printk(KERN_ERR"capture_info.period_size(%d) != playback_info.period_size(%d)\n",
					capture_info.period_size,playback_info.period_size);
		}
		
		size = capture_info.period_size <= playback_info.period_size ?
				capture_info.period_size :
				playback_info.period_size;
		
		//复制playback的一帧数据到catpure
		memcpy(capture_info.addr+capture_info.buf_pos,
				playback_info.addr+playback_info.buf_pos,
				size);
	} else {
		memset(capture_info.addr+capture_info.buf_pos,0x00,capture_info.period_size);
	}
	
	//更新capture当前buffer指针位置
	capture_info.buf_pos += capture_info.period_size;
	if (capture_info.buf_pos >= capture_info.buffer_size)
		capture_info.buf_pos = 0;
	
	snd_pcm_period_elapsed(cp_substream);
	return 0;
}

通过memcpy()复制playback的一帧数据到catpure,完事之后调用snd_pcm_period_elapsed()更新hw_ptr同时唤醒等待的录音进程。

4.测试

和上一篇一样安装完驱动之后, 一个终端执行命令:aplay -D hw:1,0 play.wav

参数解析: -D 指定了录音设备,1,0 是card 1 device 0的意思

不知道是card几,可以执行aplay -l查看

一个终端执行命令:arecord -D hw:1,0 -d 10 -r 48000 -f S16_LE test.wav

参数解析 -D 指定了录音设备,1,0 是card 1 device 0的意思 -d 指定录音的时长,单位时秒(如果不加,可以使用Ctrl + C结束录音) -f 指定录音格式 -r 指定了采样率,单位时Hz -t 指定生成的文件格式

注意:录音采样率、格式必须和play.wav相同!! 不然录出来的音频和play.wav的不一样,就是失真。

window可以用Audacity打开test.wav查看,如下图,Audacity同样可以导入原始的pcm数据,具体操作问度娘。

5.问题记录

(1) aplay: set_params:1403: Unable to install hw params

出现这问题原因是底层驱动不支持aplay设置的hw params,参照kernel其他asoc分别设置codec dai、cpu dai和platform格式相关的参数。

(2) underrun!!! (at least 660.013 ms long)

出现这问题原因是应用准备的音频数据不够,比如,驱动需要播放需要 1026 帧数据,但应用只准备好了 1024 帧。 这里设计的是虚拟声卡,不涉及硬件传输,所以用定时器模拟数据传输完成产生的中断,如果定时时间太快,就会产生此问题,时间加大点就好了,这样应用就能准备好更多音频数据。 但是笔者的电脑本身就卡,运行虚拟机跑ubuntu 16.04,再运行qemu,导致在qemu测试,也容易出现此问题。 在ubuntu 16.04直接insmod驱动,就不会出现此问题。

(3) pcm_read:2145: read error: Input/output error

这是aplay报的错,刚开始很诧异,因为驱动没有报任何错。 没有aplay源码,不好定位问题,怎么办呢? 使用strace跟踪一下系统调用,用法是: strace -o strace.log [命令], 最终生成strace.log,打开看一下,发现

ioctl(4, SNDRV_PCM_IOCTL_READI_FRAMES, 0x7e9199d4) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x1a26cb0) = 0
write(3,"\377\377\377\377\377\377\377\377\377\3\377\377\377\377\377\377"…, 12000) = 12000
ioctl(4, SNDRV_PCM_IOCTL_READI_FRAMES, 0x7e9199d4) = -1 EIO (Input/output error)
write(2, "arecord: pcm_read:2145: ", 24) = 24
write(2, “read error: Input/output error”, 30) = 30

可见,执行了两次SNDRV_PCM_IOCTL_READI_FRAMES ioctl,第一次成功,第二次失败。 查看代码发现,在只执行录音操作时,定时器只执行了一遍。第二次驱动没有及时执行snd_pcm_period_elapsed()函数,导致应用等待超时。 最终修改看最新代码,链接在本文末。

(4)驱动获取不到playback数据

驱动代码基本完成,开一个终端aplay播放,开一个终端arecord录音,看能不能录到正在播放的音频。结果录出来的音频如下图:

这根本就不是aplay播放的音频数据,猜想是因为buf没有初始化,所以是乱码,那就给它个初始化吧。 emm…, 猜想是对的,改了之后直接是0了,录不到数据。 继续分析,查看内核打印,没报错,aplay和arecord也没报错。哦豁。 要么是playback有问题,要么是capture有问题。那就先检查playback,使用vfs_write()把应用传下来的数据写到一个pcm文件里,然后在电脑上使用Audacity工具导入,发现压根没数据,所以确定playback有问题。 playback怎么会有问题呢?内核又没报错。老规矩,strace跟踪一下系统调用,打开strace.log一看,发现一大堆这种打印:

ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0

怎么没有write呢?在往上走发现了一条这样的打印:

mmap2(NULL, 98304, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x76d16000

由此看来aplay并没有调write系统调用往驱动写数据,而是通过mmap的方式,但是驱动并没有实现mmap,问题找到了。 具体修改看最新代码,链接在本文末。

6.代码

代码位置:https://gitcode.net/u014056414/myalsa

备用下载:本地下载