原创 FPGA播放声音和音乐

2010-6-3 16:22 3967 1 1 分类: FPGA/CPLD
这里我们将让我们的FPGA播放声音和音乐。我们从产生一个单频音开始。然后,逐步让它实现一些更加有趣的功能,例如播放警笛和曲子。

这个工程中用到的硬件器件包括:一块Pluto板、一个扬声器(speaker)以及一个1千欧姆的电阻(resistor)。


FPGA播放声音和音乐 - 清 - FIGHTING FOREVE!!!! 


关于此硬件系统的一个更加正式的表示方法如下图所示:


FPGA播放声音和音乐 - 清 - FIGHTING FOREVE!!!! 


振荡器(oscillator)产生一个固定频率输入到FPGA,FPGA将此频率分频后驱动一个I/O口。这个I/O口通过一个1千欧姆的电阻连接到一个扬声器。通过改变这个I/O口的输出频率,就可以使扬声器发出各种声音。


HDL(硬件描述语言)设计


这里将分三部分来描述它:



  •  第一部分 - 简单的哔哔声
  •  第二部分 - 警笛声
  •  第三部分 - 曲调


简单的哔哔声


FPGA 可以很容易就实现二进制的计数。让我们从一个16位的计数器开始。首先从25MHz的时钟开始,对于这个时钟信号,我们可以简单的应用计数器来实现“分频”。一个16位的计数器从0计到65535(一共65536个不同的值)。计数器的最高位将以25000000/65536=381Hz的频率翻转。


对应的Verilog HDL语言如下所示:


代码




  1. module music(clk, speaker);   
  2. input clk;   
  3. output speaker;   
  4.   
  5. // 16位的2进制计数器   
  6. reg [15:0] counter;   
  7. always @(posedge clk) counter <= counter+1;   
  8.   
  9. // 使用计数器的最高有效位驱动扬声器   
  10. assign speaker = counter[15];   
  11.   
  12. endmodule  


计数器的最低有效位(counter[0])以12.5MHz的频率翻转,类似的counter[1]以6.125MHz的频率翻转,以此类推。我们使用最高有效位(counter[15])来驱动扬声器。这样就可以给扬声器输出一个很好的381Hz的方波。


"A" 调 (440Hz)


好了,与其产生一个随机的频率,为何不试试得到一个440Hz的频率。这个频率就是“A”调的频率。这样一来,我们需要将25MHz的信号56818分频,下面是对应的Verilog HDL代码。


代码




  1. module music(clk, speaker);   
  2. input clk;   
  3. output speaker;   
  4.   
  5. reg [15:0] counter;   
  6. always @(posedge clk) if(counter==56817) counter <= 0; else counter <= counter+1;   
  7.   
  8. assign speaker = counter[15];   
  9.   
  10. endmodule  


问题来了,输出信号的频率虽然是希望的440Hz,但是其占空比不再是50%。因为低电平从0一直维持到32767(期间counter[15]等于0),而高电平则从32768维持到56817。这样输出信号中,高电平的占空比仅为42%。


最简单的得到50%占空比的办法是添加一个状态,使输出信号先28409分频(56818的一半),然后再2分频。以下是修改后的Verilog HDL代码。


代码




  1. module music(clk, speaker);   
  2. input clk;   
  3. output speaker;   
  4.   
  5. reg [14:0] counter;   
  6. always @(posedge clk) if(counter==28408) counter <= 0; else counter <= counter+1;   
  7.   
  8. reg speaker;   
  9. always @(posedge clk) if(counter==28408) speaker <= ~speaker;   
  10.   
  11. endmodule  


添加一个参数


下面的代码跟上面的代码效果完全一样,一个名为“clkdivider”的参数被添加到代码中,而计数器则变为向下技术(这个只是个人爱好问题).


代码




  1. module music(clk, speaker);   
  2. input clk;   
  3. output speaker;   
  4. parameter clkdivider = 25000000/440/2;   
  5.   
  6. reg [14:0] counter;   
  7. always @(posedge clk) if(counter==0) counter <= clkdivider-1; else counter <= counter-1;   
  8.   
  9. reg speaker;   
  10. always @(posedge clk) if(counter==0) speaker <= ~speaker;   
  11.   
  12. endmodule


  


救护车笛声


让我们交替发出两个音调。首先我们使用一个24位的计数器“tone”来产生一个低频的方波。其最高有效位(tone[23])以大约1.5Hz的频率翻转。


我们使用这一位(tone[23])来控制主计数器产生在两个频率之间切换的输出波形,这样一来就可以交替发出两个音调。下面是对应的Verilog HDL代码。


代码




  1. module music(clk, speaker);   
  2. input clk;   
  3. output speaker;   
  4. parameter clkdivider = 25000000/440/2;   
  5.   
  6. reg [23:0] tone;   
  7. always @(posedge clk) tone <= tone+1;   
  8.   
  9. reg [14:0] counter;   
  10. always @(posedge clk) if(counter==0) counter <= (tone[23] ? clkdivider-1 : clkdivider/2-1); else counter <= counter-1;   
  11.   
  12. reg speaker;   
  13. always @(posedge clk) if(counter==0) speaker <= ~speaker;   
  14.   
  15. endmodule  


警车笛声


问题现在变得复杂起来。我们需要产生一个音调的变化,使之听起来像是警车的笛声。


仍然从“tone”计数器开始。我们仅使用23位,这样便可以得到两倍与前面的频率(最高有效位大约以3Hz的频率翻转)


下面是如何产生变化的音调的技巧。使用一个寄存器“ramp”来表征当前的音调,则要求ramp的值在某一区间来回变化,例如...-2-1-0- 1-2-3-...-127-126-125-...-2-1-0-1-2-...。考虑“tone”计数器的15到21位(tone[21:15]), 这是一个在0到127之间循环递增的值,0-1-2-...-127-0-。再考虑这几位的反转,即~tone[21:15],这是一个在127-0之间循环递减的值。如果能控制ramp在这两个值之间来回切换,即可得到一个形如...-0-1-2-...-127-126-125-...的计数器。而这个变化规律正好符合警车笛声的音调变化规律。为了让ramp在这两个值之间来回切换,我们使用tone[22]来控制。可以这样考虑,tone[22: 15]从0计数,对于前128个值(0-127),tone[22]等于0,后128个值(128-255),tone[22]等于1。于是我们就可以使用tone[22]来控制ramp的取值,当tone[22]等于0时,让ramp等于tone[21:15],当tone[22]等于1时,让ramp 等于~tone[21:15]。具体的硬件描述语言如下:


代码




  1. wire [6:0] ramp = (tone[22] ? tone[21:15] : ~tone[21:15]);   
  2.   
  3. // 含义   
  4. // 当 tone[22]等于1 取 ramp=tone[21:15] 否则 ramp=~tone[21:15]  


这样一来ramp就会在7b'0000000与7b'1111111之间来回变化. 为了得到一个对于产生声音有用的值, 我们在其前面补上两位数据"01",并且在其尾部也补上6个0,即"000000"。


代码




  1. wire [14:0] clkdivider = {2'b01, ramp, 6'b000000};  


通过这样的处理,"clkdivider" 就拥有了一个在15'b010000000000000 与 15'b011111111000000之间来回变化的值(或者以16进制表示在15'h2000 与 15'h3FC0,以十进制表示在8192 到 16320之间变化)。当输入频率为25MHz时,将产生频率在765Hz到1525Hz之间变化的音调,从而产生类似于警车笛声的声音。下面是整个模块的Verilog HDL语言描述。


代码




  1. module music(clk, speaker);   
  2. input clk;   
  3. output speaker;   
  4.   
  5. reg [22:0] tone;   
  6. always @(posedge clk) tone <= tone+1;   
  7.   
  8. wire [6:0] ramp = (tone[22] ? tone[21:15] : ~tone[21:15]);   
  9. wire [14:0] clkdivider = {2'b01, ramp, 6'b000000};   
  10.   
  11. reg [14:0] counter;   
  12. always @(posedge clk) if(counter==0) counter <= clkdivider; else counter <= counter-1;   
  13.   
  14. reg speaker;   
  15. always @(posedge clk) if(counter==0) speaker <= ~speaker;   
  16.   
  17. endmodule  


高速追击


现在让我们看看如何让FPGA发出“高速追击”的声音。这个时候警笛声时快时慢。因此使用"tone[21:15]" 来得到一个快速的变调, 而使用 "tone[24:18]"来得到一个慢速的变调。


代码




  1. wire [6:0] fastsweep = (tone[22] ? tone[21:15] : ~tone[21:15]);   
  2. wire [6:0] slowsweep = (tone[25] ? tone[24:18] : ~tone[24:18]);   
  3. wire [14:0] clkdivider = {2'b01, (tone[27] ? slowsweep : fastsweep), 6'b000000};  


完整的Verilog HDL代码是这样子的:


代码




  1. module music(clk, speaker);   
  2. input clk;   
  3. output speaker;   
  4.   
  5. reg [27:0] tone;   
  6. always @(posedge clk) tone <= tone+1;   
  7.   
  8. wire [6:0] fastsweep = (tone[22] ? tone[21:15] : ~tone[21:15]);   
  9. wire [6:0] slowsweep = (tone[25] ? tone[24:18] : ~tone[24:18]);   
  10. wire [14:0] clkdivider = {2'b01, (tone[27] ? slowsweep : fastsweep), 6'b000000};   
  11.   
  12. reg [14:0] counter;   
  13. always @(posedge clk) if(counter==0) counter <= clkdivider; else counter <= counter-1;   
  14.   
  15. reg speaker;   
  16. always @(posedge clk) if(counter==0) speaker <= ~speaker;   
  17.   
  18. endmodule  



弹奏曲子


现在我们希望通过FPGA来弹奏曲子。首先我们需要一个类似与键盘的东西来弹奏音符。如果我们使用6位去编码一个音符,那么我们可以得到64个音符。每个音阶有12个音符,所以64个音符可以包括比5个还要多的音阶,这对与弹奏一小曲子来说已经足够了。


第一步


为了实现一升调的方式依次64个音符,我们使用一个28位的计数器,使用它的最高6位来作为我们希望弹奏的音符。


代码




  1. reg [27:0] tone;   
  2. always @(posedge clk) tone <= tone+1;   
  3.   
  4. wire [5:0] fullnote = tone[27:22];  


当输入时钟频率为25MHz时,每个音符持续167ms,总共需要10.6s才能播放完全部64个音符。


第二步.


我们将“fullnote”除以12,从而得到八度音阶(五个音阶(0-4),所以3位就足够了)和12个音符(0-11,所以4位就够了)。


代码




  1. wire [2:0] octave;   
  2. wire [3:0] note;   
  3. divide_by12 divby12(.numer(fullnote[5:0]), .quotient(octave), .remain(note));  


可以看到,这里我们使用了除法模块divby12来完成除法,具体的细节将在后面谈到。


第三步.


当从一个音阶跳到下一个音阶,频率需要乘以2时,这个在硬件上很容易实现,具体将在第四步中讨论。


但是当需要乘以“1.0594”时,这个在硬件上很难实现。因此我们使用一个存储了预先计算好的值的查找表来实现。


我们将主时钟除以512得到A调,除以483得到A#调,除以456得到B调。除以一个越小的值,得到的音调越高。


代码




  1. always @(note)   
  2. case(note)   
  3.  0: clkdivider = 512-1; // A   
  4.  1: clkdivider = 483-1; // A#/Bb   
  5.  2: clkdivider = 456-1; // B   
  6.  3: clkdivider = 431-1; // C   
  7.  4: clkdivider = 406-1; // C#/Db   
  8.  5: clkdivider = 384-1; // D   
  9.  6: clkdivider = 362-1; // D#/Eb   
  10.  7: clkdivider = 342-1; // E   
  11.  8: clkdivider = 323-1; // F   
  12.  9: clkdivider = 304-1; // F#/Gb   
  13.  10: clkdivider = 287-1; // G   
  14.  11: clkdivider = 271-1; // G#/Ab   
  15.  12: clkdivider = 0; // 永远不会发生   
  16.  13: clkdivider = 0; // 永远不会发生   
  17.  14: clkdivider = 0; // 永远不会发生   
  18.  15: clkdivider = 0; // 永远不会发生   
  19. endcase  
  20.   
  21. always @(posedge clk) if(counter_note==0) counter_note <= clkdivider; else counter_note <= counter_note-1;  


每次"counter_note"等于0,都意味着将要转到下一个音阶,对应到程序中就是counter_octave除以2。


第四步.


好了。现在我们来处理一下音阶。


对于最低的音阶,我们将"counter_note"除以256。对于音阶1,除以128... 以此类推...


代码




  1. reg [7:0] counter_octave;   
  2. always @(posedge clk)   
  3. if(counter_note==0)   
  4. begin  
  5. if(counter_octave==0)   
  6.  counter_octave <= (octave==0?255:octave==1?127:octave==2?63:octave==331:octave==4?15:7);   
  7. else  
  8.  counter_octave <= counter_octave-1;   
  9. end  
  10.   
  11. reg speaker;   
  12. always @(posedge clk) if(counter_note==0 && counter_octave==0) speaker <= ~speaker;  



完整的代码如下所示:


代码




  1. module music(clk, speaker);   
  2. input clk;   
  3. output speaker;   
  4.   
  5. reg [27:0] tone;   
  6. always @(posedge clk) tone <= tone+1;   
  7.   
  8. wire [5:0] fullnote = tone[27:22];   
  9.   
  10. wire [2:0] octave;   
  11. wire [3:0] note;   
  12. divide_by12 divby12(.numer(fullnote[5:0]), .quotient(octave), .remain(note));   
  13.   
  14. reg [8:0] clkdivider;   
  15. always @(note)   
  16. case(note)   
  17. 0: clkdivider = 512-1; // A   
  18. 1: clkdivider = 483-1; // A#/Bb   
  19. 2: clkdivider = 456-1; // B   
  20. 3: clkdivider = 431-1; // C   
  21. 4: clkdivider = 406-1; // C#/Db   
  22. 5: clkdivider = 384-1; // D   
  23. 6: clkdivider = 362-1; // D#/Eb   
  24. 7: clkdivider = 342-1; // E   
  25. 8: clkdivider = 323-1; // F   
  26. 9: clkdivider = 304-1; // F#/Gb   
  27. 10: clkdivider = 287-1; // G   
  28. 11: clkdivider = 271-1; // G#/Ab   
  29. 12: clkdivider = 0; // 永远不会发生   
  30. 13: clkdivider = 0; // 永远不会发生   
  31. 14: clkdivider = 0; // 永远不会发生   
  32. 15: clkdivider = 0; // 永远不会发生   
  33. endcase  
  34.   
  35. reg [8:0] counter_note;   
  36. always @(posedge clk) if(counter_note==0) counter_note <= clkdivider; else counter_note <= counter_note-1;   
  37.   
  38. reg [7:0] counter_octave;   
  39. always @(posedge clk)   
  40. if(counter_note==0)   
  41. begin  
  42. if(counter_octave==0)   
  43. counter_octave <= (octave==0255:octave==1127:octave==263:octave==331:octave==415:7);   
  44. else  
  45. counter_octave <= counter_octave-1;   
  46. end  
  47.   
  48. reg speaker;   
  49. always @(posedge clk) if(counter_note==0 && counter_octave==0) speaker <= ~speaker;   
  50.   
  51. endmodule  


除以12:


“除以12”这么模块完成将一个6位的数(number)除以12这个功能。结果我们将得到一个3位的商(0..5)和一个 4位的余数(0..11)。我们尝试使用厂商提供的除法模块,但是它提供的是一个针对通用除法优化的模块。而这里,除数是固定不变的。所以需要设计一个定制的除法模块。


为了完成处理12,我们采用以下技巧,先将数除以4,然后再除以3。


除以4只需要将数据右移2位即可,移出的2位作为余数。这样我们只剩下6-2=4位数据,只要将他们除以3即可。除以3的操作是用查找表的方法实现的。(为什么这么做:避免使用除法器,可以获得更高的速度,并节省器件资源)


代码




  1. module divide_by12(numer, quotient, remain);   
  2. input [5:0] numer;   
  3. output [2:0] quotient;   
  4. output [3:0] remain;   
  5.   
  6. reg [2:0] quotient;   
  7. reg [3:0] remain_bit3_bit2;   
  8.   
  9. assign remain = {remain_bit3_bit2, numer[1:0]}; // the first 2 bits are copied through   
  10.   
  11. always @(numer[5:2]) // 查找表实现除以3的供呢个   
  12. case(numer[5:2])   
  13. 0: begin quotient = 0; remain_bit3_bit2 = 0; end  
  14. 1: begin quotient = 0; remain_bit3_bit2 = 1; end  
  15. 2: begin quotient = 0; remain_bit3_bit2 = 2; end  
  16. 3: begin quotient = 1; remain_bit3_bit2 = 0; end  
  17. 4: begin quotient = 1; remain_bit3_bit2 = 1; end  
  18. 5: begin quotient = 1; remain_bit3_bit2 = 2; end  
  19. 6: begin quotient = 2; remain_bit3_bit2 = 0; end  
  20. 7: begin quotient = 2; remain_bit3_bit2 = 1; end  
  21. 8: begin quotient = 2; remain_bit3_bit2 = 2; end  
  22. 9: begin quotient = 3; remain_bit3_bit2 = 0; end  
  23. 10: begin quotient = 3; remain_bit3_bit2 = 1; end  
  24. 11: begin quotient = 3; remain_bit3_bit2 = 2; end  
  25. 12: begin quotient = 4; remain_bit3_bit2 = 0; end  
  26. 13: begin quotient = 4; remain_bit3_bit2 = 1; end  
  27. 14: begin quotient = 4; remain_bit3_bit2 = 2; end  
  28. 15: begin quotient = 5; remain_bit3_bit2 = 0; end  
  29. endcase  
  30.   
  31. endmodule  


音乐


现在让我们来让FPGA播放一段音乐。这个很容易实现,只要再用一块ROM来存放希望播放的音乐的乐谱就可以了。


代码




  1. reg [30:0] tone;   
  2. always @(posedge clk) tone <= tone+1;   
  3.   
  4. wire [7:0] fullnote;   
  5.  music_rom rom(.inclock(clk), .outclock(clk), .address(tone[29:22]), .q(fullnote));  


其中“music_rom”使用FPGA厂商提供的工具生成的,如Altera的Quartus II 及其宏功能生成的这些文件。


另外,我们还希望实现以下功能:


    * 播放音乐时,在ROM的结尾处暂停


    * "fullnote"值为0时,表示静音


所以我们将原来的程序的最后一行从


代码




  1. always @(posedge clk) if(counter_note==0 && counter_octave==0) speaker <= ~speaker;  


修改为:


代码




  1. always @(posedge clk) if(counter_note==0 && counter_octave==0 && tone[30]==0 && fullnote!=0) speaker <= ~speaker;  


设计的剩下部分是一样的。你能认得这首音乐吗?

PARTNER CONTENT

文章评论0条评论)

登录后参与讨论
EE直播间
更多
我要评论
0
1
关闭 站长推荐上一条 /3 下一条