基于FPGA的SD卡音乐播放器之完结篇

基于FPGA的SD卡音乐播放器之完结篇


前言

        前面已经介绍了WM8731芯片和SD卡的使用,这里介绍怎么利用这两个东西完成基于FPGA的SD卡音乐播放器并且添加按键完成对音乐播放效果的控制。


提示:以下是本篇文章正文内容,写文章实属不易,希望能帮助到各位。

一、WAV格式

         信号经采样后,进行量化、编码。量化位数代表用多少bit位表示采样点的值,常用的有8位(低品质)、12位、16位(高品质)等,16bit是最常见的采样精度。WM8731支持的位数有16位、20位、24位、32位,我们采用的是16位。编码在这里指信源编码,即数据压缩。对于音
乐而言,有无压缩的音乐格式如WAV和有压缩的音乐格式如MP3。
         无压缩的音乐格式WAV文件是波形文件,是微软公司推出的一种音频储存格式,主要用于保存Windows平台下的音频源。WAV文件储存的是声音波形的二进制数据,由于没有经过压缩,使得WAV波形声音文件的体积很大。WAV文件占用的空间大小计算公式是[(采样频率×量化位数×声道数)÷8]×时间(秒),单位是字节(Byte)。理论上,采样频率和量化位数越高越好,但是所需的磁盘空间就更大。通用的WAV格式(即CD音质的WAV)是44100Hz的采样频率,16Bit 的量化位数,双声道,这样的WAV声音文件储存一分钟的音乐需要10.34MB,占用空间大,但无需解码就可直接播放。
        作为数字音乐文件格式的标准,WAV格式容量过大,因而使用起来不方便。因此,一般情 况下我们把它压缩为MP3或WMA格式。压缩方法有无损压缩与有损压缩。MP3,OGG就属于有损压
缩,如果把压缩的音频还原回去,音频其实是不一样的。当然,人耳很难分辨出这种细微的差别。因此,如果把MP3,OGG格式从压缩的状态还原回去的话,就会产生损失。而像APE和FLAC这类音频格式即使还原,也能毫无损失地保留原有音质。所以,APE和FLAC可以无损失高音质地压缩和还原。这些压缩的音频格式由于都采用了各自的压缩算法,若要播放音乐需要先用相应的算法进行解压缩。
        本工程是把存放在SD卡里的音乐通过FPGA读出来并输出给WM8731进行播放。如果采用压缩的音乐格式而不通过相应的解压缩算法处理的话听到的就是噪声了,所以本工程使用无压缩的WAV格式。由于一般WAV格式的音乐采样率为44.1kHz,而WM8731支持的采样率中没有44.1kHz,所以我们需要把存放在SD卡里的音乐采样率转换成WM8731支持的48kHz,如果不转换成48kHz,直接使用原始的44.1kHz进行播放,播放的速度会快一些,相当于1.088倍的速度播放,转换可以用音乐格式转换类的软件。本工程使用的WAV格式音乐的采样率为48kHz,量化位数为16位。

二、FIFO模块

        FIFO模块负责暂存从SD卡读出的数据,因为SD卡读数据的时钟频率为50MHz,但WM8731采样频率仅为48K或96K,二者频率不一致,读的速度快于DA转换的速度,所以需要用一个FIFO模块对读出的数据暂存一下。FIFO模块设定的大小为1024个16bit,设置成16bit是将读出的数据每两个字节拼接成一个字,方便WM8731使用。

FIFO模块的配置如图1所示。

图1 FIFO模块的配置

 三、音频-SD卡交互模块

        音频-SD卡交互模块用于控制从SD卡读取音频数据给FIFO,防止FIFO写满或读空,并对从FIFO读出的音频数据进行处理以送给WM8731进行播放。注意歌曲起始地址查看用winhex软件。

        以下为音频-SD卡交互模块的代码:

module audio_sd_ctrl(
	input                  mode           ,      //当前歌曲播放模式
    input                  sd_clk         ,      // SD卡时钟信号
    input                  aud_bclk       ,      // WM8731位时钟信号
    input                  rst_n          ,      // 复位信号
    input                  sd_init_done   ,      // SD卡初始化完成
    input                  rd_busy        ,      // 读忙信号
    input                  tx_done        ,
    input          [15:0]  music_data     ,      // 音乐数据
    input          [ 9:0]  wrusedw_cnt    ,      // fifo内剩余写入的字数
	input          [ 4:0]  select         ,      //选择播放哪首音乐
    output   reg           rd_start_en    ,      // 开始读出使能
    output   reg   [31:0]  rd_sec_addr    ,      // 读SD卡扇区地址
    output   reg   [15:0]  dac_data       ,      // 音频数据
	output   reg           playdone              //当前歌曲播放完标志
	 
);

//reg define
reg    [ 1:0]   flow_cnt       ;                 // 状态流计数
reg    [19:0]   rd_sec_cnt     ;                 // 读扇区次数计数器
reg             rd_busy_d0     ;                 // 读忙信号打拍d0
reg             rd_busy_d1     ;                 // 读忙信号打拍d1
reg    [24:0]   START_ADDR=25'd10496     ;       // 音乐存放的起始地址
reg    [19:0]   AUDIO_SEC=20'd79076      ;       // 音乐占用的扇区数

//wire define
wire            neg_rd_busy    ;                 // 读忙信号的下降沿

assign  neg_rd_busy = rd_busy_d1 & (~rd_busy_d0);  // 采读忙信号下降沿

//以下操作循环赋值便于当前歌曲播放完后根据歌曲模式调整歌曲播放下一首还是循环播放
reg  rst1=1,rst2=1;
wire rstnege;
assign rstnege=rst2&(!rst1);
reg [1:0] num=2'b0;

always @(posedge sd_clk)
begin
	rst1 <= rst_n;
    rst2 <= rst1;
end

always @(posedge sd_clk)
	if(rstnege)
		num<=2'b0;
	else if(num<4)
		num<=num+1;

//歌曲选择,SD卡中32首歌曲的起始地址以及占用的扇区数,需视实际情况修改
always @(posedge sd_clk) 
if(num<4)
begin
	case(select)
		5'd0:  begin  START_ADDR<=25'd10496   ;AUDIO_SEC<=20'd79076  ;  end
		5'd1:  begin  START_ADDR<=25'd89600   ;AUDIO_SEC<=20'd99018  ;  end
		5'd2:  begin  START_ADDR<=25'd188672  ;AUDIO_SEC<=20'd104912 ;  end
		5'd3:  begin  START_ADDR<=25'd293632  ;AUDIO_SEC<=20'd85467  ;  end
		5'd4:  begin  START_ADDR<=25'd379136  ;AUDIO_SEC<=20'd74344  ;  end
		5'd5:  begin  START_ADDR<=25'd2216192 ;AUDIO_SEC<=20'd76292  ;  end
		5'd6:  begin  START_ADDR<=25'd859904  ;AUDIO_SEC<=20'd61837  ;  end
		5'd7:  begin  START_ADDR<=25'd2556864 ;AUDIO_SEC<=20'd66882  ;  end
		
		5'd8:  begin  START_ADDR<=25'd1987584 ;AUDIO_SEC<=20'd94012  ;  end
		5'd9:  begin  START_ADDR<=25'd2292544 ;AUDIO_SEC<=20'd94077  ;  end
		5'd10: begin  START_ADDR<=25'd453504  ;AUDIO_SEC<=20'd63007  ;  end
		5'd11: begin  START_ADDR<=25'd791936  ;AUDIO_SEC<=20'd67917  ;  end
		5'd12: begin  START_ADDR<=25'd1679360 ;AUDIO_SEC<=20'd99596  ;  end
		5'd13: begin  START_ADDR<=25'd1080000 ;AUDIO_SEC<=20'd102338 ;  end
		5'd14: begin  START_ADDR<=25'd2746880 ;AUDIO_SEC<=20'd107172 ;  end
		5'd15: begin  START_ADDR<=25'd1593856 ;AUDIO_SEC<=20'd85475  ;  end
		
		5'd16: begin  START_ADDR<=25'd1394368 ;AUDIO_SEC<=20'd106058 ;  end
		5'd17: begin  START_ADDR<=25'd1289984 ;AUDIO_SEC<=20'd104327 ;  end
		5'd18: begin  START_ADDR<=25'd1500480 ;AUDIO_SEC<=20'd93340  ;  end
		5'd19: begin  START_ADDR<=25'd2386624 ;AUDIO_SEC<=20'd105090 ;  end
		5'd20: begin  START_ADDR<=25'd2081600 ;AUDIO_SEC<=20'd67992  ;  end
		5'd21: begin  START_ADDR<=25'd690880  ;AUDIO_SEC<=20'd100999 ;  end
		5'd22: begin  START_ADDR<=25'd516544  ;AUDIO_SEC<=20'd73181  ;  end
		5'd23: begin  START_ADDR<=25'd921792  ;AUDIO_SEC<=20'd68690  ;  end
		
		5'd24: begin  START_ADDR<=25'd2149632 ;AUDIO_SEC<=20'd66534  ;  end
		5'd25: begin  START_ADDR<=25'd1870208 ;AUDIO_SEC<=20'd117358 ;  end
		5'd26: begin  START_ADDR<=25'd990528  ;AUDIO_SEC<=20'd89449  ;  end
		5'd27: begin  START_ADDR<=25'd2491776 ;AUDIO_SEC<=20'd65043  ;  end
		5'd28: begin  START_ADDR<=25'd2623808 ;AUDIO_SEC<=20'd123020 ;  end
		5'd29: begin  START_ADDR<=25'd589760  ;AUDIO_SEC<=20'd101109 ;  end
		5'd30: begin  START_ADDR<=25'd1779008 ;AUDIO_SEC<=20'd91140  ;  end
		5'd31: begin  START_ADDR<=25'd1182400 ;AUDIO_SEC<=20'd107569 ;  end
		
		default: begin  START_ADDR<=25'd10496 ;AUDIO_SEC<=20'd79076  ;  end
	endcase
end	
//音频处理
always @(posedge aud_bclk or negedge rst_n) begin
    if(!rst_n) begin
        dac_data <= 16'd0;
    end
    else if(tx_done)
        dac_data[15:0] <= {music_data[7:0],music_data[15:8]};
end

//打拍采上升沿
always @(posedge sd_clk or negedge rst_n) begin
    if(!rst_n) begin
        rd_busy_d0 <= 1'b0;
        rd_busy_d1 <= 1'b0;
    end
    else begin
        rd_busy_d0 <= rd_busy;
        rd_busy_d1 <= rd_busy_d0;
    end
end

//SD扇区地址变更
always @(posedge sd_clk or negedge rst_n) begin
    if(!rst_n) begin
        rd_sec_addr <= 32'd0;
    end
    else if(rd_sec_addr <= START_ADDR + AUDIO_SEC)
        rd_sec_addr <= rd_sec_cnt + START_ADDR;
end

//读取音频数据
always @(posedge sd_clk or negedge rst_n) begin
    if(!rst_n) begin
        flow_cnt   <=  2'b0;
        rd_sec_cnt <= 20'd0;
		playdone   <=     0;
    end
    else begin
        rd_start_en <= 1'b0;
        case(flow_cnt)
        2'd0: begin
            if(sd_init_done == 1'b1) begin
               flow_cnt     <= flow_cnt + 1'd1;
               rd_start_en  <= 1'b1;
            end
        end
        2'd1: begin
        //读忙信号下降沿说明单次读出结束,开始读取下一扇区地址数据
            if(rd_sec_cnt < AUDIO_SEC) begin
                if(neg_rd_busy) begin
                    rd_sec_cnt <= rd_sec_cnt + 20'd1;   
                    flow_cnt   <= flow_cnt + 1'd1;
				    playdone   <=     0;
                end
            end
            else begin
						rd_sec_cnt <= 20'd0;
						flow_cnt   <=  2'd0;
						playdone   <=     1;
					end
        end
        2'd2: begin
            if(wrusedw_cnt <= 10'd255) begin
                rd_start_en <= 1'b1;
                flow_cnt <= 2'd1;
            end
        end
        default: flow_cnt <= 2'd0;
        endcase
    end
end

endmodule

四、锁相环模块

        图2是锁相环模块的RTL门级综合视图,锁相环模块负责生成驱动WM8731芯片所需要的18.432MHz主时钟,即图中的C0。C2是输入的50MHz时钟,C3是对输入的50MHz进行反相后的输出,之所以这样做,是因为用SPI模式操作SD卡时是用的第四种操作模式。在这种模式下,SD卡读数据是在时钟的上升沿,那么为了使得SD卡读到的数据稳定,就要在时钟的下降沿发送数据。程序设计中一般都是用的时钟上升沿,所以生成了一个反相的50MHz的时钟便于程序编写对应代码时使用时钟的上升沿。

图2 锁相环模块的RTL门级综合视图

 五、按键去抖模块

        按键消抖通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。在处理按键抖动的程序中,必须同时考虑消除闭合和断开两种情况下的抖动。所以,对于按键消抖的处理,必须按最差的情况来考虑。机械式按键的抖动次数、抖动时间、抖动波形都是随机的。不同类型的按键其最长抖动时间也有差别,抖动时间的长短和按键的机械特性有关,按键输出的信号的跳变时间(上升沿和下降沿)最大是在20ms左右。按键一次闭合最短的时间大概是120ms。按键消抖的关键是提取稳定的低电平(或高电平)状态,滤除按键稳定前后的抖动脉冲。软件消抖的基本原理是:在检测到有按键按下时,不是立即认定此键已被按下,而是执行一个20ms左右(具体时间应视所使用的按键进行调整)的延时程序后,再确认该键电平是否仍然保持闭合状态电平,若仍然保持,则确认该键真正被按下。

        按键去抖模块代码如下所示:

module qudou(clk_50M,keyin,keyout,Rst_n);
input wire clk_50M,keyin,Rst_n;
output reg keyout;

reg [4:0] n;
parameter N=50_000;    //分频比
reg [20:0] divider_cnt;   //分频计数
reg clk_1K;

//	分频计数器计数模块
always@(posedge clk_50M or negedge Rst_n)
if(!Rst_n)
	divider_cnt <= 21'd0;
else if(divider_cnt == (N/2-1))
	divider_cnt <= 21'd0;
else
	divider_cnt <= divider_cnt + 1'b1;

//1K扫描时钟生成模块		
always@(posedge clk_50M or negedge Rst_n)
if(!Rst_n)
	clk_1K <= 1'b0;
else if(divider_cnt == (N/2-1))
	clk_1K <= ~clk_1K;
else
	clk_1K <= clk_1K;

always @(posedge clk_1K)
	begin
		if (keyin==1)    //按键未按下时
			begin
				n<=0;
				keyout<=1;
			end
		else
			begin         //按键按下时
				if (n<19)
					begin
						n<=n+1;
						keyout<=1;
					end
				else
					begin   //20ms消抖动
						n<=19;
						keyout<=0;
					end
			end
	end 
endmodule 

六、按键控制模块

        按键控制模块通过外部按键控制音乐播放的效果。图2.22是功能的硬件说明。

图2.22 功能的硬件说明

        图2.22中从左往右数第1个数码管可显示0-5,共6个数字,分别对应6种模式,其中数字0对应调节音量调节模式、数字1对应顺序播放模式、数字2对应随机播放模式、数字3对应单曲循环模式、数字4对应倍速调节模式、数字5对应暂停模式。模式通过按键S2切换。

        图2.22中从左往右数第2个数码管可显示1-F,共15个数字,为当前播放歌曲的音量,在音量调节模式下,可通过按键S0和S1来调节音量,其中S0为音量加,S1为音量减。

        图2.22中从左往右数第3和第4个数码管组合在一起可显示00-1F,共32个数字,为当前播放歌曲的序号。在顺序播放、随机播放、单曲循环播放模式下均可通过按键S0和S1来调节歌曲序号,其中按键S0对应上一曲,按键S1对应下一曲。

        图2.22中从左往右数第5个数码管可显示1和2,其中1代表1倍速,2代表2倍速,倍速调节模式下可通过按键S0在1倍速和2倍速间来回切换,此模式下按键S1无任何作用。

        图2.22中从左往右数第6、第7和第8个数码管组合在一起可显示000-FFF,为当前序号歌曲已经播放的时间。

        关于另外两个模块译码模块和74HC595驱动数码管模块,此处不做叙述。


总结

        以上就是要分享的关于基于FPGA的SD卡音乐播放器的全部内容了,不得不说,做完后简直就是一个MP3 Player,只是板子大了点,拿起来不方便,不然感觉都可以用它来边听歌边跑步了。

猜你喜欢

转载自blog.csdn.net/m0_66360845/article/details/126502805