赞
踩
基于FPGA的SD卡音乐播放器之完结篇
前面已经介绍了WM8731芯片和SD卡的使用,这里介绍怎么利用这两个东西完成基于FPGA的SD卡音乐播放器并且添加按键完成对音乐播放效果的控制。
提示:以下是本篇文章正文内容,写文章实属不易,希望能帮助到各位。
FIFO模块负责暂存从SD卡读出的数据,因为SD卡读数据的时钟频率为50MHz,但WM8731采样频率仅为48K或96K,二者频率不一致,读的速度快于DA转换的速度,所以需要用一个FIFO模块对读出的数据暂存一下。FIFO模块设定的大小为1024个16bit,设置成16bit是将读出的数据每两个字节拼接成一个字,方便WM8731使用。
FIFO模块的配置如图1所示。
音频-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的时钟便于程序编写对应代码时使用时钟的上升沿。
按键消抖通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。在处理按键抖动的程序中,必须同时考虑消除闭合和断开两种情况下的抖动。所以,对于按键消抖的处理,必须按最差的情况来考虑。机械式按键的抖动次数、抖动时间、抖动波形都是随机的。不同类型的按键其最长抖动时间也有差别,抖动时间的长短和按键的机械特性有关,按键输出的信号的跳变时间(上升沿和下降沿)最大是在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中从左往右数第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,只是板子大了点,拿起来不方便,不然感觉都可以用它来边听歌边跑步了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。