写在前面的话
经过前面内容的学习,梦翼师兄相信大家的基础知识水平一定已经很扎实。那么本节,我们就一起来庆祝一下,用播放器奏响一曲《欢乐颂》,奏响我们凯旋的乐章。
什么是蜂鸣器?
蜂鸣器是一种一体化结构的电子讯响器,采用直流电压供电,广泛应用于计算机、打印机、电子玩具、定时器等电子产品中作为发声器件。蜂鸣器分为有源蜂鸣器和无源蜂鸣器两种,在电路中用字母“H”或“HA”(旧标准用“FM”、“ZZG”、“LB”、“JD”等)表示。那么,怎么区分有源蜂鸣器和无源蜂鸣器呢?有源蜂鸣器内部带震荡源,所以只要一通电就会叫;而无源内部不带震荡源,所以如果用直流信号无法令其鸣叫。必须用2K-5K的方波去驱动它,有源蜂鸣器往往比无源的贵,就是因为里面多个震荡电路。无源蜂鸣器的优点是:
1. 便宜;
2. 声音频率可控,可以做出“多来米发索拉西”的效果;
3. 在一些特例中,可以和LED复用一个控制口
有源蜂鸣器的优点是:程序控制方便。
在FPGA应用的设计上,很多方案都会用到蜂鸣器,大部分都是使用蜂鸣器来做提示或报警,比如按键按下、开始工作、工作结束或是故障等等。当然,我们也可以让它为我们演奏喜欢的音乐。在设计之前,我们先来了解一下声音是怎么播放出来的。我们在本次设计中,用到的是一个无源蜂鸣器,如下图所示:
由于蜂鸣器的工作电流一般比较大,以至于FPGA的I/O 口是不能很好地直接驱动它的,所以要利用三极管的开关特性来引入电源电压信号,我们知道无源蜂鸣器的主要特点是内部不带振荡源,所以如果使用直流信号是无法驱动无源蜂鸣器鸣叫的,因此必须使用方波去驱动它。
现在我们明白了,只要给蜂鸣器发送一定频率的方波,就可以使得蜂鸣器发出声音,然而现在的问题就转化为-我们究竟要给蜂鸣器发送什么频率的方波信号呢?具体的频率可以查表如下:
现在我们知道了如何让蜂鸣器发出声音,又知道发送多大的频率可以让蜂鸣器响起什么样的声音,所以我相信我们已经有能力让蜂鸣器响起我们需要的音乐了。
设计任务
下面我们用FPGA来设计一个蜂鸣器的播放器,梦翼师兄开发板上的晶振是50Mhz的,所以我们需要一个锁相环模块(PLL)来得到低频率的时钟,然后再用这个比较低的频率来分出所需要的频率来驱动蜂鸣器。有了锁相环(PLL)后,我们还需要一个空间来保存乐谱,由于乐谱是确定的,不需要更改,所以我们选择一个简单的存储器就可以了,这里我们使用的是 ROM,可以直接通过IP 核来创建。
既然是播放音乐,那么就需要节拍(一般为 4 拍),因此我们还需要一个节拍控制器, 例如,我们现在需要发出一个低音 1 并维持一秒,那怎么办呢? 我们可以设计一个模块,控制每 0.25s, ROM 的地址加一,若要使发出的低音 1 维持 1 秒 钟,我们仅需在 ROM 的四个连续地址中写入低音 1 的对应信息即可
具体的设计架构图如下所示:
由上图可知,整个FPGA设计里面一共有6个模块,其中PLL模块,用来将50Mhz的晶振时钟信号分频得到1Mhz的系统时钟。ROM模块用来存储想要播放的音乐数据,time_counter模块是一个计数器模块,当计数到0.25秒之后,time_counter输出的time_finsh信号会变一个时钟周期的高电平,发送给 addr_gen模块。当addr_gen接收到这个高电平之后,就会给Rom发送一个地址信号,让ROM输出数据,简单来说,就是在0.25秒之后,把ROM的地址加一,读取下一个数据。 decode解码模块,是把从Rom中读出来的数据解码然后发送到music_gen模块,产生特定的方波频率。
为了方便我们往ROM里保存数据,比如保存的数据是8’hAB,其中A设定只有3个值,分别是1,2,4来表示低音、中音和高音,而B设定有7个值,分别是1,2,3,4,5,6,7,比如现在要产生一个低音1,仅仅需要在ROM里面写8’h11,需要产生一个高音5,则需要在ROM里面写8’h45依次类推。但是实际上,你要产生一个低音1,然后把ROM里面的8’h11发给music_gen模块是不行的,因为在ROM里面的数据是为了方便我们才定义成这样的格式,所以我们这里需要一个decode模块(解码模块)来把ROM的数据还原成music_gen所需要的数据。
time_counter的功能很简单,就不断的计数,当计数到249_999(1Mhz/4Hz=250_000-1,4Hz=0.25S)的时候数据选择器选择18’d0,让count归零,然后又继续累加,直到计数到249_999,不断这样循环。
下面附上一首《欢乐颂》的ROM数据:
端口介绍
顶层端口及其意义如下:
端口名 |
端口说明 |
clk_50M |
系统50MHz时钟输入 |
rst_n |
系统低电平复位 |
beep_out |
蜂鸣器输入 |
内部连线及其意义如下
端口名 |
端口说明 |
clk_1M |
系统驱动时钟 |
time_finish |
0.25s节拍定时标志 |
addr |
Rom读数据地址 |
rom_data |
解码前音符编码数据 |
music_data |
分频后解码预置数 |
代码解释
time_counter 模块
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function : 产生节拍定时模块 *****************************************************/ 01 module time_counter( 02 clk, //输入1Mhz时钟信号 03 rst_n, //输入复位信号 04 time_finsh //输出时间计数标志位(没0.25s变高电平一次) 05 ); 06 07 input clk, rst_n; //输入1Mhz时钟信号,复位信号 08 output time_finsh; //输出时间计数标志位(没0.25s变高电平一次) 09 10 reg [17:0]count; //计数器count 11 12 always@(posedge clk or negedge rst_n) 13 begin 14 if(!rst_n) 15 count <= 18'd0; //计数器复位 16 else if(time_finsh) 17 count <= 18'd0; //每到0.25s计数器归零 18 else 19 count <= count + 1'd1; //未到0.25s,计数器继续累加 20 end 21 22 //每到0.25s,time_finsh拉高,表示已经达到0.25s 23 assign time_finsh = (count == 18'd249_999)? 1'd1 : 1'd0; 24 //用于仿真,因为真正的0.25s会仿真很长 25 //assign time_finsh = (count == 22'd25_00)? 1'd1 : 1'd0; 26 27 endmodule |
这个模块是一个计数模块,每次当计数到0.25s的时候,计数器清零,并输出一个周期的高电平用作标志信号。
addr_gen模块
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function : 地址发生器模块 *****************************************************/ 01 module addr_gen( 02 clk, //输入1Mhz时钟信号 03 rst_n, //输入复位信号 04 05 addr, //输出给ROM的地址信号 06 time_finsh //输入时间计数标记位(每0.25s变高电平一次) 07 ); 08 09 input clk, rst_n; //输入1Mhz时钟信号,复位信号 10 input time_finsh; //输入时间计数标记位(每0.25s变高电平一次) 11 12 output reg [6:0]addr; //输出给ROM的地址信号 13 14 always@(posedge clk or negedge rst_n) 15 begin 16 if(!rst_n) 17 addr <= 7'd0; //输出给ROM的地址信号复位 18 else if(time_finsh) 19 addr <= addr + 1'd1; 20 else //输出给ROM的地址信号自加1(每0.25s自加1) 21 addr <= addr; //未够0.25s,ROM的地址信号不变 22 end 23 24 endmodule |
这个模块功能是当接收到计数到0.25s的标志信号time_finish之后,输出一个新的地址,没有接受到标志信号的时候,输出地址不变。
decode模块
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function : 音符数据解码模块 *****************************************************/ 01 module decode( 02 clk, //输入1Mhz时钟信号 03 rst_n, //输入复位信号 04 05 rom_data, //输入的ROM的数据 06 music_data //输出ROM的解码数据 07 ); 08 09 input clk, rst_n; //输入1Mhz时钟信号,复位信号 10 input [7:0]rom_data; //输入的ROM的数据 11 12 output reg [10:0]music_data; //输出ROM的解码数据 13 14 always@(posedge clk or negedge rst_n) 15 begin 16 if(!rst_n) 17 music_data <= 11'd0; //输出ROM的解码数据复位 18 else case (rom_data) 19 8'h11 : music_data <= 11'd1911; //(1Mhz/261.63Hz)/2)=1191 低音1 20 8'h12 : music_data <= 11'd1702; //(1Mhz/293.67Hz)/2)=1702 低音2 21 8'h13 : music_data <= 11'd1517; //(1Mhz/329.63Hz)/2)=1517 低音3 22 8'h14 : music_data <= 11'd1431; //(1Mhz/349.23Hz)/2)=1431 低音4 23 8'h15 : music_data <= 11'd1276; //(1Mhz/391.99Hz)/2)=1276 低音5 24 8'h16 : music_data <= 11'd1136; //(1Mhz/440.00Hz)/2)=1136 低音6 25 8'h17 : music_data <= 11'd1012; //(1Mhz/493.88Hz)/2)=1012 低音7 26 27 8'h21 : music_data <= 11'd939; //(1Mhz/532.25Hz)/2)=939 中音1 28 8'h22 : music_data <= 11'd851; //(1Mhz/587.33Hz)/2)=851 中音2 29 8'h23 : music_data <= 11'd758; //(1Mhz/659.25Hz)/2)=758 中音3 30 8'h24 : music_data <= 11'd716; //(1Mhz/698.46Hz)/2)=716 中音4 31 8'h25 : music_data <= 11'd638; //(1Mhz/783.99Hz)/2)=638 中音5 32 8'h26 : music_data <= 11'd568; //(1Mhz/880.00Hz)/2)=568 中音6 33 8'h27 : music_data <= 11'd506; //(1Mhz/987.76Hz)/2)=506 中音7 34 35 8'h41 : music_data <= 11'd478; //(1Mhz/1046.50Hz)/2)=478 高音1 36 8'h42 : music_data <= 11'd425; //(1Mhz/1174.66Hz)/2)=425 高音2 37 8'h43 : music_data <= 11'd379; //(1Mhz/1318.51Hz)/2)=379 高音3 38 8'h44 : music_data <= 11'd358; //(1Mhz/1396.51Hz)/2)=358 高音4 39 8'h45 : music_data <= 11'd319; //(1Mhz/1567.98Hz)/2)=319 高音5 40 8'h46 : music_data <= 11'd284; //(1Mhz/1760.00Hz)/2)=284 高音6 41 8'h47 : music_data <= 11'd253; //(1Mhz/1975.52Hz)/2)=253 高音7 42 43 8'h00 : music_data <= 11'd0; //0HZ,停止节拍 44 endcase 45 end 46 47 endmodule |
这个模块是把从ROM中读出来的数据进行解码并输出,其实就是一个数据选择器,每一个音符对应一个解码数据,那么这个解码数据是怎么计算的呢?比如高音5,从ROM中读出来的数据是45,它的方波驱动频率是 1567.98Hz,一个周期是637763ns,本模块的时钟频率是1MHz一个周期是1000ns,所以需要计数637763/1000=637.763次,因为是方波,我们取637.763的一半318.8,这里我们取319就可以产生频率是1567.98Hz的方波了。
music_gen模块
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function : 驱动频率输出模块 *****************************************************/ 01 module music_gen( 02 clk, //输入1Mhz时钟信号 03 rst_n, //输入复位信号 04 05 music_data, //输入音乐频率控制字 06 beep //输出方波 07 ); 08 09 input clk, rst_n; //输入1Mhz时钟信号,复位信号 10 input [10:0] music_data; //输入音乐频率控制字 11 12 output reg beep; //输出方波 13 14 reg [10:0] data, count; //寄存音乐控制字的data,计数器count 15 16 always@(posedge clk or negedge rst_n) 17 begin 18 if(!rst_n) 19 data <= 11'd0; //寄存器data复位 20 else 21 data <= music_data; //data寄存音乐控制字 22 end 23 24 always@(posedge clk or negedge rst_n) 25 begin 26 if(!rst_n) 27 begin 28 count <= 11'd1; //计数器复位 29 beep <= 1'd0; //输出方波复位 30 end 31 else if(data == 11'd0) //当data==11'd0,(停止节拍) 32 begin 33 count <= 11'd1; //计数器归一 34 beep <= 1'd0; //输出方波归零 35 end 36 else if(count <= data) //当计数器小于等于data的值 37 count <= count + 1'd1; //计数器继续累加 38 else 39 begin 40 count <= 11'd1; //当计数器大于data的值,计数器归一 41 beep <= ~beep; //输出方波取反 42 end 43 end 44 45 endmodule |
在music_gen模块中,music_data的值会寄存在data寄存器里面,当data=11’d0(停止节拍)的时候,计数器count归1,输出的方波信号beep归零,当data!=11’d0的时候,就判断当前计数器的值是否小于等于data的值,如果计数器的值小于等于data的值,计数器继续累加,输出的方波信号beep不变,当计数器的值大于等于data的值,计数器归1,输出的方波信号beep取反,通过改变data的值来改变输出的方波频率。
顶层模块
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function : 顶层模块 *****************************************************/ 01 module beep( 02 clk, //输入50Mhz时钟信号 03 rst_n, //输入低电平复位信号 04 05 beep_data //输出的方波 06 ); 07 08 input clk, rst_n; //输入50Mhz时钟信号,复位信号 09 10 output beep_data; //输出的方波 11 12 wire clk_1m, time_finsh; //1Mhz时钟信号线,0.25s时间计数标记位 13 wire [6:0]addr; //ROM地址线 14 wire [7:0]rom_data; //ROM数据线 15 wire [10:0]music_data; //ROM数据解码数据线 16 17 my_pll my_pll( //PLL模块 18 .areset(!rst_n), 19 .inclk0(clk), 20 .c0(clk_1m) 21 ); 22 23 my_rom my_rom( //ROM模块 24 .address(addr), 25 .clock(clk_1m), 26 .q(rom_data) 27 ); 28 29 decode decode( //解码模块 30 .clk(clk_1m), 31 .rst_n(rst_n), 32 .rom_data(rom_data), 33 .music_data(music_data) 34 ); 35 36 music_gen music_gen( //音乐发生器模块 37 .clk(clk_1m), 38 .rst_n(rst_n), 39 .music_data(music_data), 40 .beep(beep_data) 41 ); 42 43 time_counter time_counter( //0.25s时间计数器模块 44 .clk(clk_1m), 45 .rst_n(rst_n), 46 .time_finsh(time_finsh) 47 ); 48 49 addr_gen addr_gen( //ROM地址发生器 50 .clk(clk_1m), 51 .rst_n(rst_n), 52 .addr(addr), 53 .time_finsh(time_finsh) 54 ); 55 56 endmodule |
编写完可综合代码之后,查看RTL视图如下:
由RTL视图可以看出,代码综合生成的电路和我们设计的系统框图一致,说明顶层连接关系正确,接下来编写测试代码如下:
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function : 测试模块 *****************************************************/ 01 `timescale 1ns/1ps //仿真时间单位是ns,精度是ps 02 module beep_tb; 03 04 reg clk, rst_n; //仿真激励的时钟与复位信号 05 06 wire beep_data; //输出的方波信号 07 08 initial 09 begin 10 clk = 0; //时钟信号初始化 11 rst_n = 0; //复位信号有效 12 #200.1 rst_n=1; //复位结束 13 end 14 15 always #10 clk = ~clk; //产生50Mhz时钟信号 16 17 beep beep( //把激励信号送进beep模块 18 .clk(clk), 19 .rst_n(rst_n), 20 .beep_data(beep_data) 21 ); 22 23 endmodule |
仿真分析
由仿真波形可知,当ROM输出的数据q等于23的时候,代表着输出中音3,解码后的结果等于758,输出的方波周期等于1.518ms,方波频率等于658.76Hz,约等于中音3的659.25Hz;当ROM输出的数据q等于00的时候,代表着输出停止节拍,解码后的结果等于0,输出一个低电平;当ROM输出的数据q等于24的时候,代表着输出中音4,解码后的结果等于716,输出的方波周期等于1.434ms,等于697.35Hz,约等于中音4的698.46Hz,虽然有误差存在,但并不影响音乐播放器的播放效果,若大家想把精度做的更高一点,可以把锁相环的输出时钟频率1Mhz增大,时钟频率越高,分频精度就越高,误差就越小。