FPGA播放声音和音乐.docx
《FPGA播放声音和音乐.docx》由会员分享,可在线阅读,更多相关《FPGA播放声音和音乐.docx(13页珍藏版)》请在冰豆网上搜索。
![FPGA播放声音和音乐.docx](https://file1.bdocx.com/fileroot1/2023-1/23/5d904313-0a52-44f0-bd64-0dad90bf2681/5d904313-0a52-44f0-bd64-0dad90bf26811.gif)
FPGA播放声音和音乐
这里我们将让我们的FPGA播放声音和音乐。
我们从产生一个单频音开始。
然后,逐步让它实现一些更加有趣的功能,例如播放警笛和曲子。
这个工程中用到的硬件器件包括:
一块Pluto板、一个扬声器(speaker)以及一个1千欧姆的电阻(resistor)。
关于此硬件系统的一个更加正式的表示方法如下图所示:
振荡器(oscillator)产生一个固定频率输入到FPGA,FPGA将此频率分频后驱动一个I/O口。
这个I/O口通过一个1千欧姆的电阻连接到一个扬声器。
通过改变这个I/O口的输出频率,就可以使扬声器发出各种声音。
HDL(硬件描述语言)设计
这里将分三部分来描述它:
∙ 第一部分-简单的哔哔声
∙ 第二部分-警笛声
∙ 第三部分-曲调
简单的哔哔声
FPGA可以很容易就实现二进制的计数。
让我们从一个16位的计数器开始。
首先从25MHz的时钟开始,对于这个时钟信号,我们可以简单的应用计数器来实现“分频”。
一个16位的计数器从0计到65535(一共65536个不同的值)。
计数器的最高位将以25000000/65536=381Hz的频率翻转。
对应的VerilogHDL语言如下所示:
代码
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分频,下面是对应的VerilogHDL代码。
代码
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分频。
以下是修改后的VerilogHDL代码。
代码
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])来控制主计数器产生在两个频率之间切换的输出波形,这样一来就可以交替发出两个音调。
下面是对应的VerilogHDL代码。
代码
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之间变化的音调,从而产生类似于警车笛声的声音。
下面是整个模块的VerilogHDL语言描述。
代码
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};
完整的VerilogHDL代码是这样子的:
代码
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);