基于FPGA设计的音乐播放器

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 基于FPGA设计的音乐播放器
  • 一、生成PCM音频格式的音乐文件
    • 1.PCM音频文件格式
    • 2.Matlab读取WAV文件
  • 二、音频文件储存
    • 1.ROM存储简单音频文件
    • 2. I2S数据传输协议
  • 三、PCM5102解码模块介绍


基于FPGA设计的音乐播放器

本文将介绍如何使用FPGA和PCM5102音频解码模块来制作音乐播放器,从生成PCM格式的音频文件开始,到如何编写I2S总线协议代码,音频数据的储存等。

一、生成PCM音频格式的音乐文件

1.PCM音频文件格式

PCM(Pulse Code Modulation,脉冲编码调制)音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准数字音频数据。PCM音频的几个关键参数如下。

参数 描述
采样率(Sample Rate) 表示一帧音频数据的出现频率,在每一个周期期间内,传输完所有声道的音频信息。常用的有44.1kHz
量化位数(Sample Size) 表示对音频数据的量化位数,即单个通道的位宽。常用的有16bit,24bit,32bit
通道个数(Channels ) 表示音频数据的通道个数,双通道即可完成立体声效果,但双通道不一定是立体声,而立体声一定是多通道
数据符号(Sign) 表示音频数据是否带符号,有符号和无符号数的表示范围不同。例如当位宽为8bit时,有符号的话表示范围为-128 ~ 127,无符号是0 ~ 255

简单介绍如下:假设正弦波形为音频模拟信号,发出的声音为"滴",按照如图所示的采样、量化、编码为32bit有符号的数据,即为单通道PCM音频数据。
基于FPGA设计的音乐播放器_第1张图片
当然我们制作音乐播放器肯定不是只能播放出"滴",所以需要先生成满足需求的音频数据,就选取周杰伦的七里香钢琴曲作为音频源文件。下载WAV格式的音频文件,这里可以使用qq音乐下载MP3格式后,在进行音频转码为WAVE格式,如图所示。
基于FPGA设计的音乐播放器_第2张图片

2.Matlab读取WAV文件

当音频转码完成后,生成的WAV文件可直接使用matlab进行读取,使用函数audioread即可,下面附上完整读取、采样、量化、进制转换代码。

%% 读取wav音频文件,写入mif/coe
clear all ;
clc ;
wav  = audioread('周杰伦 - 七里香(钢琴版).wav') ;
% 低采样
sr = 3 ;
m = floor( max(wav) ) / 3 ;
wav_catch = zeros(m , 2) ;
wav_catch(: , 1) = wav(1:m , 1) ;
wav_catch(: , 2) = wav(1:m , 2) ;
save wav_catch wav_catch ;
% 截取部分时间,同时增大幅度值,位宽为32bit
l = 16384 ;
audio_l = wav_catch(1:l , 1) * 2^31 ;
audio_r = wav_catch(1:l , 2) * 2^31 ;
% 十进制 -> 十六进制
audio_l(find(audio_l<0)) = audio_l(find(audio_l<0)) + 2^32 ;
audio_r(find(audio_r<0)) = audio_r(find(audio_r<0)) + 2^32 ;
audio_l_hex = dec2hex(audio_l) ;
audio_r_hex = dec2hex(audio_r) ;

其中各个模块已经给出了中文注释,由于负数在FPGA中是以补码的形式进行储存,对于其中的十进制转换十六进制有不懂的地方可自行百度。到这里就已经得到了满足PCM格式的音频数据文件,由于数据量过大,如若使用ROM来存储,将会耗费大量的BRAM资源以及底层逻辑资源,甚至造成资源不够的情况。

当然若使用此种方式,需要生成COE或MIF文件可参考MATLAB生成COE或MIF文件代码
在本设计中需要完整的播放整个音乐文件,即使已经将采样率压缩到16KHz,FPGA上的ROM资源依然不够,所以采用SD卡的方式,或是通过串口写入到FLASH上面进行音频文件的储存,该部分内容会在后续完成后上传。

2022/03/22 毕业论文初稿完成,得闲,不愿荒废时光,却又找不到什么实际意义的事情,随便记录一下,留着以后怀念。

二、音频文件储存

1.ROM存储简单音频文件

本来之前想把整首曲子存储在sd卡或是ddr里面,然中间很多事情耽搁了,现在又投身工作了,难以抽身。无意间看到有私信求更,才想起来,那就简单的更完吧。

在FPGA中,只读存储器ROM(read only memory)常用于存放初始数据。ROM中的数据需要先进行初始化,即要先将数据写入到ROM内部的存储单元中,然后系统正常工作时,只能读出其中存储的数据,而不能写入信息,且其中储存的数据掉电不会丢失。由前面章节介绍可知,我们需要在ROM中存入音频数据,格式为PCM音频数据格式,如下图所示。

基于FPGA设计的音乐播放器_第3张图片

为了体现立体声的效果,我们设计传输两个声道的音频信息,选用双声道的数据格式。音频数据都选用正弦波形数据,其播放出来的声音为“滴”,但两个声道传输的正弦波形数据的幅度不同,在播放时两边声音的音量大小不同。设计时先使用Matlab生成幅度变化满足需求的音频数据,并创建内存初始化(mif)文件,将正弦波数据写入。然后使用EDA工具quartus直接调用已经封装好的ROM存储器IP核,如图所示。
我们设定ROM的输出端口位宽为32bit,指定存储器的深度为256,表示本IP核可以存储256个32bit位宽的音频数据。通过载入mif文件为存储器提供存储器初始化数据,并编写仿真文件对该ROM进行测试验证。

基于FPGA设计的音乐播放器_第4张图片

如下图所示为ROM的功能仿真波形图。从图中可知,左右声道中传输的数据不同,故在经过PCM5102解码模块解码后的音频信息中,可以明显的感受到立体声效果,这是由于双耳播放的音频不同导致的。
基于FPGA设计的音乐播放器_第5张图片

2. I2S数据传输协议

PCM5102解码模块是基于I2S传输协议,选择左对齐模式下的I2S传输协议进行代码编写和功能仿真。
首先,I2S总线共拥有三条数据信号线,一条系统时钟线,各信号简要介绍如下:
(1)BCK:串行时钟信号,每一个脉冲周期对应于数字音频文件中的每一位数据,所以也称为位同步时钟信号。在本设计中,声道数为2,音频数据位宽为32位,那么BCK的频率可通过如下公式计算:
在这里插入图片描述

其中f(lrck)为采样频率。
(2)LRCK:声道选择信号,用于切换左右声道的数据,也称为帧同步信号。命令选择线表明了正在被传输的声道,LRCK为低电平表示正在传输的是左声道的数据,LRCK为高电平表示正在传输的是右声道的数据。该信号的频率即是采样频率。
(3)SDIN:串行数据信号,即将音频数据按照串行的方式进行传输,先传输数据的最高位,最低位的位置则是依赖于数据的有效位数,本设计中有效位数是32位,那么不存在无效位,传输前音频数据都应转换为二进制补码的形式。
(4)SCK:系统时钟信号,当处于主模式时,可用于为外部设备提供系统时钟,工作为从模式时,不可用。

本文根据解码模块的系统时钟要求,如下图所示,图片来自pcm5102模块文档说明。我们选择采样时钟为16KHz,系统时钟为4.096MHz,进行时序设计。
基于FPGA设计的音乐播放器_第6张图片
其中基于i2s协议的数据发送代码如下:

module I2S_SEND (
        input           clk     ,   // 50M
        input           rstn    ,
        input   [31:0]  audio_l ,
        input   [31:0]  audio_r ,
        output          flag    ,
        output          bck     ,
        output          lrck    ,
        output          sck     ,
        output          dout
);

//----------------------
reg         [31:0]  r_audio_l ; 
reg         [31:0]  r_audio_r ; 


reg                 SDIN ;

reg                 BCLK ;
reg                 WCLK ;
reg                 SCLK ;
reg         [5:0]   SCLK_cnt ;
reg         [11:0]  BCLK_cnt ;
reg         [15:0]  WCLK_cnt ;


reg         [1:0]   pos_BCLK ;
reg         [1:0]   neg_BCLK ;
reg         [1:0]   pos_WCLK ;
reg         [1:0]   neg_WCLK ;

//----------------------
assign      bck = BCLK ;
assign      lrck = WCLK ;
assign      sck = SCLK ;
assign      dout = SDIN ;

//----------------------
assign      flag = (WCLK_cnt == 16'd3070) ? 1 : 0 ;

//- BCLK gen
always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            BCLK_cnt <= 12'd0 ;
        end
    else if(BCLK_cnt == 12'd47)
        begin
            BCLK_cnt <= 12'd0 ;
        end
    else
        begin
            BCLK_cnt <= BCLK_cnt + 1'b1 ;
        end
end

always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            BCLK <= 1'b0 ;
        end
    else if(BCLK_cnt < 12'd24)
        begin
            BCLK <= 1'b0 ;
        end
    else
        begin
            BCLK <= 1'b1 ;
        end
end
//- WCLK gen
always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            WCLK_cnt <= 16'd0 ;
        end
    else if(WCLK_cnt == 16'd3071)
        begin
            WCLK_cnt <= 16'd0 ;
        end
    else
        begin
            WCLK_cnt <= WCLK_cnt + 1'b1 ;
        end
end

always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            WCLK <= 1'b1 ;
        end
    else if(WCLK_cnt < 16'd1536)
        begin
            WCLK <= 1'b1 ;
        end
    else
        begin
            WCLK <= 1'b0 ;
        end
end
//- SCLK gen
always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            SCLK_cnt <= 6'd0 ;
        end
    else if(SCLK_cnt == 6'd11)
        begin
            SCLK_cnt <= 6'd0 ;
        end
    else
        begin
            SCLK_cnt <= SCLK_cnt + 1'b1 ;
        end
end

always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            SCLK <= 1'b0 ;
        end
    else if(SCLK_cnt < 6'd6)
        begin
            SCLK <= 1'b0 ;
        end
    else
        begin
            SCLK <= 1'b1 ;
        end
end
//-
always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            r_audio_l <= 32'd0 ;
            r_audio_r <= 32'd0 ;
        end
    else if(WCLK_cnt == 16'd3071)
        begin
            r_audio_l <= audio_l ;
            r_audio_r <= audio_r ;
        end
    else if(BCLK_cnt == 12'd0)
        begin
            r_audio_l <= {r_audio_l[30:0] , r_audio_l[31]} ;
            r_audio_r <= {r_audio_r[30:0] , r_audio_r[31]} ;
        end
end
//- DOUT gen
always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            SDIN <= 1'b0 ;
        end
    else if(WCLK_cnt < 16'd1536)
        begin
            if(BCLK_cnt == 12'd0)
                begin
                    SDIN <= r_audio_l[31] ;
                end
            else 
                begin
                    SDIN <= SDIN ;
                end
        end
    else if(WCLK_cnt > 16'd1535)
        begin
            if(BCLK_cnt == 12'd0)
                begin
                    SDIN <= r_audio_r[31] ;
                end
            else 
                begin
                    SDIN <= SDIN ;
                end
        end
end
endmodule

以下是从ROM中读取数据传给I2S的代码:

module MP3_TEST(
        input           clk     ,   // 50M
        input           rstn    ,
        input           enable  ,
        output  [31:0]  audio_l ,
        output  [31:0]  audio_r 
    );

// -
wire        [31:0]      dout1 ;
wire        [31:0]      dout2 ;
reg         [4:0]       switch_state ;
reg                     switch_flag ;
reg         [7:0]       addr ;
reg         [23:0]      cnt ;
//- binary twos-complement
// assign  audio_l = (dout1[31]) ? {dout1[31] , ~dout1[30:0] + 1} :  dout1 ;
assign  audio_l = dout1 ;
assign  audio_r = dout2 ;

//-
blk_mem_gen1 u1_blk_mem_gen1 (
        .clka               ( clk  ),          
        .rsta               ( ~rstn ),          
        .ena                ( 1'b1 ),           
        .addra              ( addr ),        
        .douta              ( dout1 ),          
        .rsta_busy          (  )  
);
//-
blk_mem_gen2 u2_blk_mem_gen2 (
        .clka               ( clk  ),          
        .rsta               ( ~rstn ),          
        .ena                ( 1'b1 ),           
        .addra              ( addr ),        
        .douta              ( dout2 ),          
        .rsta_busy          (  )  
);
//- switch frequence 
always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            cnt <= 24'd0 ;
            switch_flag <= 1'b0 ;
        end
    else if(cnt == 24'd6_144_000)
        begin
            cnt <= 24'd0 ;
            switch_flag <= 1'b1 ;
        end
    else 
        begin
            cnt <= cnt + 1'b1 ;
            switch_flag <= 1'b0 ;
        end
end
//- switch
always @(posedge clk or negedge rstn) 
begin
    if(rstn == 1'b0)
        begin
            addr <= 8'd0 ;
            switch_state <= 5'd0 ;
        end
    else begin
        case(switch_state)
            0:begin addr <= 8'd0 ; switch_state <= 5'd1; end
            1:begin
                if(switch_flag)
                    begin addr <= 8'd16 ; switch_state <= 5'd2 ; end
                else if(addr == 8'd15)
                    addr <= 8'd0 ; 
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            2:begin
                if(switch_flag)
                    begin addr <= 8'd32 ;switch_state <= 5'd3 ; end
                else if(addr == 8'd31)
                    addr <= 8'd16 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            3:begin
                if(switch_flag)
                    begin addr <= 8'd48 ;switch_state <= 5'd4 ; end
                else if(addr == 8'd47)
                    addr <= 8'd32 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            4:begin
                if(switch_flag)
                    begin addr <= 8'd64 ; switch_state <= 5'd5 ; end
                else if(addr == 8'd63)
                    addr <= 8'd48 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            5:begin
                if(switch_flag)
                    begin addr <= 8'd80 ; switch_state <= 5'd6 ; end
                else if(addr == 8'd79)
                    addr <= 8'd64 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            6:begin
                if(switch_flag)
                    begin addr <= 8'd96 ; switch_state <= 5'd7 ; end
                else if(addr == 8'd95)
                    addr <= 8'd80 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            7:begin
                if(switch_flag)
                    begin addr <= 8'd112 ; switch_state <= 5'd8 ; end
                else if(addr == 8'd111)
                    addr <= 8'd96 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            8:begin
                if(switch_flag)
                    begin addr <= 8'd128 ; switch_state <= 5'd9 ; end
                else if(addr == 8'd127)
                    addr <= 8'd112 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end

            9:begin
                if(switch_flag)
                    begin addr <= 8'd144 ; switch_state <= 5'd10 ; end
                else if(addr == 8'd143)
                    addr <= 8'd128 ; 
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            10:begin
                if(switch_flag)
                    begin addr <= 8'd160 ;switch_state <= 5'd11 ; end
                else if(addr == 8'd159)
                    addr <= 8'd144 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            11:begin
                if(switch_flag)
                    begin addr <= 8'd176 ;switch_state <= 5'd12 ; end
                else if(addr == 8'd175)
                    addr <= 8'd160 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            12:begin
                if(switch_flag)
                    begin addr <= 8'd192 ; switch_state <= 5'd13 ; end
                else if(addr == 8'd191)
                    addr <= 8'd176 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            13:begin
                if(switch_flag)
                    begin addr <= 8'd208 ; switch_state <= 5'd14 ; end
                else if(addr == 8'd207)
                    addr <= 8'd192 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            14:begin
                if(switch_flag)
                    begin addr <= 8'd224 ; switch_state <= 5'd15 ; end
                else if(addr == 8'd223)
                    addr <= 8'd208 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            15:begin
                if(switch_flag)
                    begin addr <= 8'd240 ; switch_state <= 5'd16 ; end
                else if(addr == 8'd239)
                    addr <= 8'd224 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            16:begin
                if(switch_flag)
                    begin addr <= 8'd0 ; switch_state <= 5'd1 ; end
                else if(addr == 8'd255)
                    addr <= 8'd240 ;
                else if(enable)
                    addr <= addr + 1'b1 ;
            end
            default:begin addr <= 8'd0 ; switch_state <= 5'd0; end
        endcase
    end
end
endmodule

三、PCM5102解码模块介绍

pcm5102的芯片手册和原理图,网上应该很容易搜到。在这里我也创建了一份网盘分享文件,里面有相关的文档和完整的工程文件,工程是vivado2018.3版本下开发的,可供有相关兴趣的爱好者参考。

仓促结尾,实属无奈。

链接:https://pan.baidu.com/s/1cu_7JYVaHr0OJrM5-iiuqg
提取码:k87f

你可能感兴趣的:(FPGA设计音乐播放器,matlab读取音频文件,PCM音频数据生成,fpga开发,matlab)