31.Linux-wm9876声卡驱动(移植+测试)-程序员宅基地

技术标签: 操作系统  

本节学习目的

  • 1)分析Linux中的OSS声卡系统
  • 2)移植wm9876声卡
  • 3)使用madplay应用程序播放mp3

 


1.声音三要素

采样频率

音频采样率是指录音设备在一秒钟内对声音信号的采样次数, 常用的采样率有:

  • 8KHz      - 电话所用采样率, 对于人的说话已经足够清除
  • 22.05KHz - 无线电广播所用采样率
  • 32KHz   -  miniDV 数码视频、DAT所用采样率
  • 44.1KHz - 音频 CD, 也常用于 MPEG-1 音频(VCD, SVCD, MP3)所用采样率
  • 48KHz   - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率
  • 50KHz   - 商用数字录音机所用采样率
  • 96K     -   BD-ROM(蓝光盘)音轨、和 HD-DVD (高清晰度 DVD)音轨等所用采样率

而2440开发板的采样频率IISRCK最高可以达到96KHz,满足了很多常用的采样场合,如下图所示:

其中CODECLK便是MCLK

 

量化位数

指每个采样点里传输的数字信号次数,如下图所示, 其中蓝线表示模拟信号,红线表示数字信号,量化位越高,数字信号就越可能接近原始信号,音质越好

 

一般的量化位数为:

  • 8位:  分成 256 次;
  • 16位: 分为65536次, 已到 CD 标准;
  • 32位: 分为 4294967296次,很少用到

2440的开发板只支持8位,16位,如下图所示:

其中LRCK就是采样频率,当LRCK为低时,表示传输的采样数据是左声道,当LRCK为高时,表示传输的采样数据是右声道,每个采样点,SD(serial data)都可以传输8位,或16位数字信号(从低位到高位传输)

 

声道数

常有单声道和立体声之分,(有的也处理成两个喇叭输出同一个声道的声音),而立体声更能感受到空间效果,但数据量翻倍

 

所以,声音的每秒数据量(字节/s)= (采样频率 × 量化位数 × 声道数) / 8;

2. WM9876声卡硬件分析

声卡是负责录音、播音、调节音量和声音合成等的一种多媒体板卡 

本节使用的声卡是2440板上自带的WM9876声卡

 

 

当我们播放声音时 ,将数字信号传入I2SDO脚,声卡便通过解码,产生模拟信号到喇叭/耳机

录音时,声卡便获取麦克风的模拟信号,编码出数字信号到I2SDI引脚上

WM8976接口分为两种:I2S接口(提供音频接收和发送)、控制接口(控制音量大小,使能各个输出通道等)

IIS接口相关的引脚如下    

  • MCLK :    主机为编解码芯片提供的系统同步时钟 ( Master/system clock input ) ,在2440中,一般设置为256fs,或者384fs
  • BCLK:      编解码芯片提供的串行时钟信号 ( Audio bit clock output ) ,也就是量化位深,比如I2SIRCK=44.1khz,量化位为32位,则BCLK=44.1khz*32*2
  • I2SLRCK: 采样频率信号,当为低电平时是采样的是左声道信号,为高电平是右声道信号,常见有44.1Khz(1fs)
  • I2SDI :  ADC数据输入
  • I2SDO :DAC数据输出

如下图所示:

控制接口相关的引脚如下

  • CSB/GPIO1: 3线 控制数据使能引脚
  • SCLK: 3线/2线 时钟引脚
  • SDIN: 3线/2线 数据输入输出引脚
  • MODE: 3线/2线 控制选择,当MODE为高,表示为3线控制,MODE位低,表示2线控制,如下图所示:

                           

其它引脚如下:

  • R/LOUT1:音频左/右输出通道1,外接耳机插孔
  • R/LOUT2:音频左/右输出通道2,未接
  • OUT3:单声道输出通道3,未接
  • OUT4:单声道输出通道4,未接
  • LIP/LIN:音频输入通道,外接麦克风

 

那么3线和2线的控制引脚又有什么区别?

3线控制:

如下图所示,3线控制,每周期都要传输16位数据(7位寄存器地址+9位寄存器数据),传输完成后,给CSB一个上升沿便完成一次数据的传输

 

2线控制:

如下图所示,2线控制就是I2C通信方式

 

本节的WM8976的MODE脚接的高电平,所以是3线控制

 

3.接下来便来分析linux内核的声卡系统

在linux声卡中存在两种声卡系统,一种是OSS(开放声音系统),一种是ALSA(先

进Linux声音架构)。本节系统以OSS(Open Sound System)为例 ,

内核以linux-2.6.22.6版本为例,位于:linux-2.6.22.6\sound\Sound_core.c

3.1首先进入入口函数

如下图所示:

 

入口函数里,只注册了一个主设备号为(SOUND_MAJOR)14的“sound”字符设备和class类,这里为什么没有创建设备节点?

是因为, 当注册声卡系统的驱动后,才会有设备节点,此时这里的代码是没有驱动的,后面会分析到

3.2 再来看看“sound”字符设备的file_perations:

 

这里只有个.open,为什么没有.read,.write函数?

显然在.open函数里做了某些处理,我们进入soundcore_open()来看看

3.2 soundcore_open()代码如下:

int soundcore_open(struct inode *inode, struct file *file)
{
       int chain;
       int unit = iminor(inode);              //获取次设备号,通过次设备号来找声卡驱动
       struct sound_unit *s;
       const struct file_operations *new_fops = NULL; //定义一个新的file_operations
       chain=unit&0x0F;  
       if(chain==4 || chain==5)       /* dsp/audio/dsp16 */
       {
              unit&=0xF0;
              unit|=3;
              chain=3;                             
       }

       spin_lock(&sound_loader_lock);          
       s = __look_for_unit(chain, unit);     //里面通过chains[chain]数组里找到sound_unit结构体
                                             //一个sound_unit对应一个声卡驱动

       if (s)
              new_fops = fops_get(s->unit_fops);    //通过sound_unit,获取对应的file_operations
              ... ...

       if (new_fops) {                                           //当找到file_operations
              int err = 0;
              const struct file_operations *old_fops = file->f_op;//设上次的file_operations等于当前的

              file->f_op = new_fops;                //设置系统的file_operations等于s-> unit_fops

              spin_unlock(&sound_loader_lock);
              if(file->f_op->open)
                     err = file->f_op->open(inode,file);

              if (err) {
                     fops_put(file->f_op);
                     file->f_op = fops_get(old_fops);
              }
              fops_put(old_fops);
              return err;
       }
       spin_unlock(&sound_loader_lock);
       return -ENODEV;
}

通过上面的代码和注释分析到,系统声卡之所以只有一个open(),里面是通过次设备号来调用__look_for_unit()函数,找到chains[chain]数组里的驱动声卡sound_unit结构体,然后来替换系统声卡的file_operations,实现偷天换日的效果。

__look_for_unit()函数如下图所示:

 

其中chains[]数组定义如下所示:

 

其中, chains[0]存放的Mixers,实现调节音量,高音等,就是我们VM8976的控制接口

chains[3]存放的DSP,用来实现音频输入输出,就是我们VM8976的I2S接口

显然VM8976的驱动有2个,需要将2个file_operations放入chains[0]和chains[3]数组里,供给系统的open()来调用

3.3 我们以DSP为例,搜索chains[3]来看看

 

如上图所示,显然register_sound_dsp()函数就是被我们声卡驱动调用的,用来注册dsp设备节点,继续进入sound_insert_unit()函数看看

3.4 sound_insert_unit()函数如下

static int sound_insert_unit(struct sound_unit **list, const struct file_operations *fops, int index, int low, int top, const char *name, umode_t mode, struct device *dev)
{

       struct sound_unit *s = kmalloc(sizeof(*s), GFP_KERNEL);   //分配个新的sound_unit
       int r;
       if (!s)
              return -ENOMEM;
       
       spin_lock(&sound_loader_lock);

// __sound_insert_unit()里主要实现:将分配的新的s插入到chains[3]里,然后并放入fops操作结构体
       r = __sound_insert_unit(s, list, fops, index, low, top); 

       spin_unlock(&sound_loader_lock);
       if (r < 0)
              goto fail;

       else if (r < SOUND_STEP)
              sprintf(s->name, "sound/%s", name);         //s->name="sound/dsp"
       else
              sprintf(s->name, "sound/%s%d", name, r / SOUND_STEP);      

       device_create(sound_class, dev, MKDEV(SOUND_MAJOR, s->unit_minor),s->name+6);      
                         //s->name+6="dsp",也就是在/dev下创建"dsp"的设备节点 return r; fail: kfree(s); return r; }

所以,register_sound_dsp()函数用来创建/dev/dsp 设备节点,同时将dsp相关的file_operations放入chains[3]里面

3.5 同样, Mixers的驱动流程也是这样,它的函数是register_sound_mixer(),如下图所示

 

也是创建/dev/mixer设备节点, 同时将dsp相关的file_operations放入chains[0]里面

 

3.6 接下来,我们便搜索register_sound_dsp()函数,看看被哪些声卡驱动调用

如下图所示,找到一个支持s3c24xx板卡的声卡驱动uda1341

uda1341声卡和WM8976声卡非常相似,音频都是I2S接口,就只有控制部分不一样

uda1341声卡的硬件,如下图所示:

 

它的控制引脚只有3个:

L3MODE:模式引脚,为高表示传输的是数据,为低表示传输的是寄存器地址

L3CLOCK:时钟引脚

L3DATA:   数据输入/输出引脚

控制接口的时序如下所示:

 

和WM8976的控制时序完全不一样,WM8976控制时序如下所示:

 

 

所以接下来,便修改S3c2410-uda1341.c的控制部分,来移植为wm8976驱动

4.移植wm8976驱动

 首先进入uda1341的probe函数

static int s3c2410iis_probe(struct device *dev)
{
       struct platform_device *pdev = to_platform_device(dev);
       struct resource *res;
       unsigned long flags;
       printk ("s3c2410iis_probe...\n");
       /*获取资源*/
       res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
       if (res == NULL) {
              printk(KERN_INFO PFX "failed to get memory region resouce\n");
              return -ENOENT;
       }

       iis_base = (void *)S3C24XX_VA_IIS ;
       if (iis_base == 0) {
              printk(KERN_INFO PFX "failed to ioremap() region\n");
              return -EINVAL;
       }

/*获取I2S时钟,并使能*/
       iis_clock = clk_get(dev, "iis");
       if (iis_clock == NULL) {
              printk(KERN_INFO PFX "failed to find clock source\n");
              return -ENOENT;
       }
       clk_enable(iis_clock);

       /*进入临界区, 禁止中断,并保存中断状态*/
       local_irq_save(flags);

       /*设置管脚功能*/
       /* GPB 4: L3CLOCK, OUTPUT */
       s3c2410_gpio_cfgpin(S3C2410_GPB4, S3C2410_GPB4_OUTP);
       s3c2410_gpio_pullup(S3C2410_GPB4,1);
       /* GPB 3: L3DATA, OUTPUT */
       s3c2410_gpio_cfgpin(S3C2410_GPB3,S3C2410_GPB3_OUTP);
       /* GPB 2: L3MODE, OUTPUT */
       s3c2410_gpio_cfgpin(S3C2410_GPB2,S3C2410_GPB2_OUTP);
       s3c2410_gpio_pullup(S3C2410_GPB2,1);
       /* GPE 3: I2SSDI */
       s3c2410_gpio_cfgpin(S3C2410_GPE3,S3C2410_GPE3_I2SSDI);
       s3c2410_gpio_pullup(S3C2410_GPE3,0);
       /* GPE 0: I2SLRCK */
       s3c2410_gpio_cfgpin(S3C2410_GPE0,S3C2410_GPE0_I2SLRCK);
       s3c2410_gpio_pullup(S3C2410_GPE0,0);
       /* GPE 1: I2SSCLK */
       s3c2410_gpio_cfgpin(S3C2410_GPE1,S3C2410_GPE1_I2SSCLK);
       s3c2410_gpio_pullup(S3C2410_GPE1,0);
       /* GPE 2: CDCLK */
       s3c2410_gpio_cfgpin(S3C2410_GPE2,S3C2410_GPE2_CDCLK);
       s3c2410_gpio_pullup(S3C2410_GPE2,0);
       /* GPE 4: I2SSDO */
       s3c2410_gpio_cfgpin(S3C2410_GPE4,S3C2410_GPE4_I2SSDO);
       s3c2410_gpio_pullup(S3C2410_GPE4,0);

       /*退出临界区,使能中断,并恢复之前保存的flags中断状态*/
       local_irq_restore(flags);

       /*设置2440的I2S寄存器*/
       init_s3c2410_iis_bus();

       /*初始化uda1341声卡的控制部分*/
       init_uda1341();

 

       /*设置DMA输出通道,用来接收声音*/
       output_stream.dma_ch = DMACH_I2S_OUT;
       if (audio_init_dma(&output_stream, "UDA1341 out")) {
              audio_clear_dma(&output_stream,&s3c2410iis_dma_out);
              printk( KERN_WARNING AUDIO_NAME_VERBOSE": unable to get DMA channels\n" );
              return -EBUSY;
       }

    /*设置DMA输入通道,用来接收声音*/
       input_stream.dma_ch = DMACH_I2S_IN;
       if (audio_init_dma(&input_stream, "UDA1341 in")) {
              audio_clear_dma(&input_stream,&s3c2410iis_dma_in);
              printk( KERN_WARNING AUDIO_NAME_VERBOSE": unable to get DMA channels\n" );
              return -EBUSY;
       }
   

  /*创建/dev/dsp,/dev/mixer两个设备节点,
并将smdk2410_audio_fops和smdk2410_mixer_fops 两个file_operations放入chains[0]和chains[3]里,供给内核的声卡系统调用
*/ audio_dev_dsp = register_sound_dsp(&smdk2410_audio_fops, -1); audio_dev_mixer = register_sound_mixer(&smdk2410_mixer_fops, -1); printk(AUDIO_NAME_VERBOSE " initialized\n"); return 0; }

从上面的代码来看, uda1341的管脚和wm8976的管脚连接都是一样的,只有init_uda1341()不一样,里面是初始化uda1341的控制引脚接口,所以需要屏蔽,然后自己来写个init_wm8976()函数

 

4.1写init_wm8976()函数之前需要先写一个寄存器操作函数

参考wm8976芯片手册时序图:

 

所以,代码如下: 

static void wm8976_write_reg(unsigned char reg, unsigned int data)
{
int i;
unsigned long flags;
//对于wm8976来说,数据的高七位表示寄存器地址,低9位表示寄存器的值
unsigned short val = (reg << 9) | (data & 0x1ff);
/*wm8976引脚csb,dat,clk分别对应2440芯片的GPB2,3,4引脚*/ s3c2410_gpio_setpin(S3C2410_GPB2,1); s3c2410_gpio_setpin(S3C2410_GPB3,1); s3c2410_gpio_setpin(S3C2410_GPB4,1); /*退出临界区,使能中断,并恢复之前保存的flags中断状态*/ local_irq_save(flags);
/*把val值写入wm8976,共16位,从高到低传输*/ for (i = 0; i < 16; i++){ if (val & (1<<15)) { s3c2410_gpio_setpin(S3C2410_GPB4,0); s3c2410_gpio_setpin(S3C2410_GPB3,1); udelay(1); s3c2410_gpio_setpin(S3C2410_GPB4,1); } else { s3c2410_gpio_setpin(S3C2410_GPB4,0); s3c2410_gpio_setpin(S3C2410_GPB3,0); udelay(1); s3c2410_gpio_setpin(S3C2410_GPB4,1); } val = val << 1; } //传输完成,需要让csb信号产生低脉冲,写入wm8976 s3c2410_gpio_setpin(S3C2410_GPB2,0); udelay(1); //引脚恢复到高电平状态 s3c2410_gpio_setpin(S3C2410_GPB2,1); s3c2410_gpio_setpin(S3C2410_GPB3,1); s3c2410_gpio_setpin(S3C2410_GPB4,1); local_irq_restore(flags); }

 

4.2.参考wm8976g.pdf第87页,来初始化wm8976,使能输出声道1,2,混响器等

static void init_wm8976(void)
{
       uda1341_volume = 57;         // wm8976的音量默认值,后面会讲到
       uda1341_boost = 0;

       /* software reset */
       wm8976_write_reg(0, 0);

       /* BIT[6-5]:使能音频的输出左右通道2
        * BIT[3]: 使能mixer混音器的输出右通道  
        * BIT[2]: 使能mixer混音器的输出右通道  
        * BIT[1]: 使能DAC传输的右通道
     * BIT[0]: 使能DAC传输的左通道
        */
       wm8976_write_reg(0x3, 0x6f);
     /* BIT[4]: 使能输出麦克风电压
        */
       wm8976_write_reg(0x1, 0x1f);  
       wm8976_write_reg(0x2, 0x185);//ROUT1EN LOUT1EN, inpu PGA enable ,ADC enable
       wm8976_write_reg(0x6, 0x0);//SYSCLK=MCLK 
       wm8976_write_reg(0x4, 0x10); // [4:3]=10:使用I2S接口传输                   
       wm8976_write_reg(0x2B,0x10);//BTL OUTPUT 
       wm8976_write_reg(0x9, 0x50);//Jack detect enable 
       wm8976_write_reg(0xD, 0x21);//Jack detect 
       wm8976_write_reg(0x7, 0x01);//Jack detect
}

wm8976初始化修改完成后,还需要修改音量控制等函数,之前就分析了uda1341的probe函数,里面会注册dsp、mixer设备节点:

/dev/dsp

用来播发和录音,由于uda1341和wm8976都用了I2S接口,所以dsp的file_operations不需要修改,

/dev/mixer

用来控制音量,调低音,高音等,由于wm8976的控制接口不一样,所以需要修改mixer的file_operations->ioctl函数

4.3 mixer的file_operations->ioctl函数如下所示:

static int smdk2410_mixer_ioctl(struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg)
{
       int ret;
       long val = 0; 

switch (cmd) {
       case SOUND_MIXER_INFO:             //CASE : 获取声卡的描述信息
{
       mixer_info info;
       strncpy(info.id, "UDA1341", sizeof(info.id));
       strncpy(info.name,"Philips UDA1341", sizeof(info.name));
       info.modify_counter = audio_mix_modcnt;        
       return copy_to_user((void *)arg, &info, sizeof(info));  //上传用户层
}
    ... ...

case SOUND_MIXER_WRITE_VOLUME:                //CASE: 写音量,音量值为0~99
ret = get_user(val, (long *) arg);  //读用户层的数据,并放在val里
            if (ret)
                   return ret;

     uda1341_volume = 63 - (((val & 0xff) + 1) * 63) / 100; //转换为寄存器音量值
     uda1341_l3_address(UDA1341_REG_DATA0); //写入音量的寄存器地址
     uda1341_l3_data(uda1341_volume);          //写入转换后的寄存器值数据          
     break;

case SOUND_MIXER_READ_VOLUME:                   //CASE:  读音量,音量值为0~100
     val = ((63 - uda1341_volume) * 100) / 63;  //将寄存器音量值转换为原始数据
     val |= val << 8;                  
     return put_user(val, (long *) arg);                     //上传音量值

case SOUND_MIXER_READ_IGAIN:                     //CASE: 读(in gain)混音输入增益
     val = ((31- mixer_igain) * 100) / 31;                 
     return put_user(val, (int *) arg);
case SOUND_MIXER_WRITE_IGAIN:     //CASE: 写(in gain)混音输入增益 ret = get_user(val, (int *) arg); if (ret) return ret; mixer_igain = 31 - (val * 31 / 100); /* use mixer gain channel 1*/ uda1341_l3_address(UDA1341_REG_DATA0); uda1341_l3_data(EXTADDR(EXT0)); uda1341_l3_data(EXTDATA(EXT0_CH1_GAIN(mixer_igain))); break; default: DPRINTK("mixer ioctl %u unknown\n", cmd); return -ENOSYS; } return 0; }

从上面的代码来看,显然接下还要修改以下几个与控制接口相关的case:

  • case SOUND_MIXER_WRITE_VOLUME:            //写音量
  • case SOUND_MIXER_READ_VOLUME:             //读音量
  • case SOUND_MIXER_READ_IGAIN:                 //读(in gain)混音输入增益
  • case SOUND_MIXER_WRITE_IGAIN:                //写(in gain)混音输入增益

 

4.4修改“case SOUND_MIXER_WRITE_VOLUME:”和“case SOUND_MIXER_READ_VOLUME:”

如下图所示(参考wm8976手册的P86页):

  

其中52,53对应的输出左右通道1的音量,54,55对应的输出左右通道2的音量

而我们耳机位于输出左右通道1,如下图所示,所以我们需要设置52,53的寄存器

 

接下来,便来看看寄存器,如何读写音量

我们以53通道1寄存器为例:

 

如上图所示:

  • bit8:  为1,表示每次写入音量值,即立刻更新音量
  • bit7:  位1,表示通道1的左右声道都静音
  • bit6:       位1,表示通道1的右声道静音
  • bit5~0:   表示音量大小,默认值为57(111001),最大值为63

所以修改的内容如下所示:

case SOUND_MIXER_WRITE_VOLUME:     //音量0~100
    ret = get_user(val, (long *) arg);          //读取应用数据,存到val里
    if (ret)
            return ret;
    uda1341_volume = (((val & 0xff)) * 63) / 100;       //最大值为63,最小值为0

wm8976_write_reg(52, (1<<8)| uda1341_volume);
wm8976_write_reg(53, (1<<8)| uda1341_volume);
    break;


case SOUND_MIXER_READ_VOLUME:
val = (uda1341_volume * 100) / 63;       //最大值为99
     return put_user(val, (long *) arg);

 

4.5修改“case SOUND_MIXER_READ_IGAIN:”和“case SOUND_MIXER_WRITE_IGAIN:”

参考wm8976手册的P86页,如下图所示:

 

其中50,51对应的就是左右混音控制寄存器

我们以50左声道混音寄存器为例:

  

如上图所示:

bit8~6: 混音输入增益,默认值为0,最大值为7

所以修改的内容如下所示:

1)首先修改混音输入增益的初始默认值为0,如下图所示

 

2)修改“case SOUND_MIXER_READ_IGAIN:”和“case SOUND_MIXER_WRITE_IGAIN:”

case SOUND_MIXER_READ_IGAIN:   //混音输入:0~100

val = (mixer_igain* 100) / 7;
return put_user(val, (int *) arg);

 

case SOUND_MIXER_WRITE_IGAIN:
ret = get_user(val, (int *) arg);
if (ret)
return ret;

mixer_igain = val * 7 / 100;
/* use mixer gain channel 1*/
wm8976_write_reg(50, mixer_igain<<6);
wm8976_write_reg(51, mixer_igain<<6);
break;

 

5.配置,修改内核文件

5.1 make menuconfig 配置内核

-> Device Drivers

  -> Sound

    -> Advanced Linux Sound Architecture  // 兼容OSS

      -> Advanced Linux Sound Architecture

        -> System on Chip audio support

        <*> I2S of the Samsung S3C24XX chips              //*:将/linux-2.6.22.6/sound/soc/s3c24xx下的makefile指定的文件加入内核里

5.2 将修改好的s3c-wm8976.c放入/linux-2.6.22.6/sound/soc/s3c24xx目录下

5.3修改该目录下的makefile

obj-y += s3c2410-uda1341.o

改为:

obj-y += s3c-wm8976.o  

5.4 make uImage,如下图所示,可以看到内核已经被编译

 

最后下载并启动内核,如下图所示,可以看到该两个设备节点

 

6.测试与运行

6.1使用wav测试声卡

wav是属于一个未经压缩的音频文件,所以可以直接调用给我们声卡播放

播放:

     cat Windows.wav > /dev/dsp

录音(还需要修改下驱动才行):

   cat /dev/dsp > sound.bin 

   //然后对着麦克风说话

   ctrl+c    //退出

   cat sound.bin > /dev/dsp  // 就可以听到录下的声音

 

6.2使用madplay应用程序测试声卡

Madplay是一个根据MAD算法写的MP3播放器,而MP3属于高压缩比(11:1)的文件,所以需要madplay解码后才能给我们声卡播放,使用之前,需要先来移植madplay

步骤如下:

1)首先下载并解压3个文件

  • libid3tag-0.15.1b.tar.gz                      //mp3的解码库
  • libmad-0.15.1b.tar.gz                         //madplay的文件库
  • madplay-0.15.2b.tar.gz                      //madplay播放器的源码

2)先创建安装目录mkdir tmp

3)接下来先安装2个库(./configure使用参考: http://www.cnblogs.com/lifexy/p/7866453.html )

cd   libid3tag-0.15.1b/ libmad-0.15.1b                          
./configure  --host=arm-linux --prefix=/app/tmp     //修改configure,设置编译器,设置安装路径
make               //编译
make install      //安装到/app/tmp目录下

4)最后安装madplay-0.15.2b

cd madplay-0.15.2b
./configure --host=arm-linux --prefix=/app/tmp  CFLAGS="-I/app/tmp/include" LDFLAGS="-L/app/tmp/lib"                
                                     // CFLAGS:指定头文件, LDFLAGS:指定库文件 make make install

 

5)把/app/tmp/bin下的所有文件复制到开发板nfs的bin目录下,

cd /app/tmp/bin /
cp  * /work/nfs_root/bin

6)把/app/tmp/lib下的带so文件 复制到开发板nfs的lib目录里:

cd /app/tmp/lib/
cp  *so* /work/nfs_root/lib/  -d              //带链接复制

 

7)使用madplay播放mp3

  ./madplay 1.mp3 2.mp3 3.mp3         //循环播放3首歌

并可以使用热键来控制,常用的有以下几种:

  • f    下一首
  • b   下一首
  • i    获取播放时间和播放歌曲名
  • p   播放暂停
  • s   停止
  • +  音量加
  • -   音量减

 

 

转载于:https://www.cnblogs.com/lifexy/p/7867782.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_30335575/article/details/97922834

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan