PYNQ开发板上使用USB声卡+OSS兼容层播放音频


PYNQ开发板上使用USB声卡+OSS兼容层播放音频需要经过联网装库,编译驱动模块,运行三个步骤。

联网装库

首先需要将PYNQ开发板连上网,才能安装所需的库。方法是在电脑上设置共享网络,但要注意大部分操作系统在共享网络的时候无法自己指定本机IP,所以需要修改PYNQ开发板的IP来适应电脑自动设置的IP。设置共享网络的方法网上教程有很多,这里不再赘述。

设置完共享网络后,注意不要关闭开发板,因为电脑的IP已经改了,如果重启开发板网关不对SSH就连不上了。这时需要查看电脑的ip(注意是有线局域网下的ip),Windows下使用ipconfig,Linux下使用ifconfig。如我的Ubuntu在有线局域网下的ip是10.42.0.1,由于网关是255.255.255.0,所以PYNQ开发板的IP必须设置为10.42.0.*。在我的环境下,设置IP的方法是编辑/etc/network/interfaces.d/eth0,然后将其中的address这一行的IP地址修改为10.42.0.2。

然后禁用再启用一下电脑的有线网功能并重启开发板,尝试能否连进去,连进去以后再尝试ping外网,都可以才算成功。接下来就可以装库了,执行:

sudo apt update
sudo apt install alsa-base alsa-utils alsa-oss bc

编译驱动模块

下载内核源代码,我PYNQ上的Linux内核版本为5.4.0,可以在xilinx的linux内核仓库下载,其他版本的内核源码也可以在该仓库的其他分支下载。

下载并传到开发板上后,执行:

unzip linux-xlnx-xlnx_rebase_v5.4_2020.1.zip

解压结束后进入该文件夹,然后需要将当前系统的内核配置文件复制过来,执行:

sudo cp /lib/modules/$(uname -r)/build/.config .

配置需要编译的驱动模块,执行:

make menuconfig

进入Device Drivers -> Sound card support -> Advanced Linux Sound Architecture -> USB sound devices,光标移至USB Audio/MIDI driver,按M键选择编译此模块,保存然后一直按Esc键退出。执行以下命令:

make prepare
make -C . M=sound

编译驱动模块,生成扩展名为ko的模块文件,将它们复制到内核文件夹中:

sudo mkdir -p /lib/modules/$(uname -r)/kernel/sound/core
sudo mkdir -p /lib/modules/$(uname -r)/kernel/sound/usb
sudo cp sound/core/*.ko /lib/modules/$(uname -r)/kernel/sound/core
sudo cp sound/usb/*.ko /lib/modules/$(uname -r)/kernel/sound/usb

然后安装这些模块:

sudo depmod

之后插上USB声卡,驱动模块应该会自动加载,可以用命令lsmod查看模块是否被成功加载,在我的系统上被加载的有以下4个模块:

Module                  Size  Used by
snd_usb_audio         167936  0
snd_hwdep              16384  1 snd_usb_audio
snd_usbmidi_lib        24576  1 snd_usb_audio
snd_rawmidi            24576  1 snd_usbmidi_lib

如果没有自动加载,可以手动加载:

sudo modprobe snd-usb-audio

此时可以查看声卡是否已被识别:

sudo aplay -l

如果输出中出现card 1,则需要配置一下声卡编号,因为alsa音频库默认使用的声卡是0号声卡,而USB声卡被分配的编号是1,所以需要修改alsa的配置文件,路径为/usr/share/alsa/alsa.conf,在里面找到defaults.pcm.card 0这一行,把0改成1。

这时候应该可以播放音乐了:

sudo aplay test.wav

运行

目前我们使用的音频驱动库一般是alsa,oss驱动库已经被淘汰了。但是相比于alsa,oss可能编程更简单一点,因为可以直接以传统文件读写的方式播放音乐;而且有很多老程序使用的是oss。为此,alsa提供了对oss的兼容,主要有两种方式,一种是加载驱动模块snd-pcm-oss,这种方式可以从内核层面将对oss的操作转发成对alsa的操作,使用范围广;另一种是安装alsa-oss库,然后在运行使用oss的程序时使用LD_PRELOAD把对oss的文件操作替换成这个库里对alsa的操作,只能替换固定的几个文件操作(像openat就不能替换),但是原理简单,不用编译驱动模块。

目前我不知道为什么编译出来的snd-pcm-oss模块用不了,运行时总是说找不到函数符号,但是它的依赖模块能编译出来的我都已经编译并加载了,不知道是不是链接有什么问题。所以只能用alsa-oss库,这里给出示例程序:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/soundcard.h>
int main() {    
    // 打开设备文件
    int fd = open("/dev/dsp", O_RDWR);
    if (fd < 0) {
        perror("open error\n"); return 1;
    }

    // test.wav为16位,双声道,采样率为20000的波形文件
    int bit = 16, rate = 20000, channel = 2;

    ioctl(fd, SOUND_PCM_WRITE_BITS, &bit);
    ioctl(fd, SOUND_PCM_WRITE_RATE, &rate);
    ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &channel);

    /*打开音乐文件*/
    int fp = open("test.wav", O_RDONLY);
    if(fp < 0){
        perror("open fp error!\n"); return 1;
    }

    /*求文件的大小*/
    //这里直接给出了,要计算可以用fstat函数或fseek+ftell函数
    int len = 800180;

    char *buf = (char *)malloc(len);
    memset(buf, 0, len);

    // 读文件到buf中
    int rd = read(fp, buf, len);
    if(rd < 0){
        perror("read wav error!\n"); return 1;
    }
    close(fp);

    // 把buf写到设备文件中
    int wr = write(fd, buf, len);
    if(wr < 0){
        perror("write dsp error!\n"); return 1;
    }

    free(buf); buf = NULL;
    return 0;
}

可以看到这里打开声卡直接用文件/dev/dsp,这个文件在使用alsa音频驱动库时是不存在的,alsa-oss需要你加载libaoss.so这个动态库并用库里的open函数替换掉这个open操作,而它自己的open函数里会进行检查,如果尝试打开oss的声卡文件,就会执行alsa的操作,否则按原来的文件打开方式,其他文件操作函数也是一样。所以假设上面的测试文件编译出来的程序为audio,则运行时执行:

sudo LD_PRELOAD=libaoss.so ./audio