平台: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 Audio的DMA调用实例流程
IMX6DL 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
备用下载:本地下载