进阶项目(4)蜂鸣器程序设计讲解

写在前面的话

经过前面内容的学习,梦翼师兄相信大家的基础知识水平一定已经很扎实。那么本节,我们就一起来庆祝一下,用播放器奏响一曲《欢乐颂》,奏响我们凯旋的乐章。

什么是蜂鸣器

蜂鸣器是一种一体化结构的电子讯响器,采用直流电压供电,广泛应用于计算机、打印机、电子玩具、定时器等电子产品中作为发声器件。蜂鸣器分为有源蜂鸣器和无源蜂鸣器两种,在电路中用字母“H”或“HA”(旧标准用“FM”、“ZZG”、“LB”、“JD”等)表示。那么,怎么区分有源蜂鸣器和无源蜂鸣器呢?有源蜂鸣器内部带震荡源,所以只要一通电就会叫;而无源内部不带震荡源,所以如果用直流信号无法令其鸣叫。必须用2K-5K的方波去驱动它,有源蜂鸣器往往比无源的贵,就是因为里面多个震荡电路。无源蜂鸣器的优点是:

1. 便宜;

2. 声音频率可控,可以做出“多来米发索拉西”的效果;

3. 在一些特例中,可以和LED复用一个控制口

有源蜂鸣器的优点是:程序控制方便。

在FPGA应用的设计上,很多方案都会用到蜂鸣器,大部分都是使用蜂鸣器来做提示或报警,比如按键按下、开始工作、工作结束或是故障等等。当然,我们也可以让它为我们演奏喜欢的音乐。在设计之前,我们先来了解一下声音是怎么播放出来的。我们在本次设计中,用到的是一个无源蜂鸣器,如下图所示:

进阶项目(4)蜂鸣器程序设计讲解由于蜂鸣器的工作电流一般比较大,以至于FPGA的I/O 口是不能很好地直接驱动它的,所以要利用三极管的开关特性来引入电源电压信号,我们知道无源蜂鸣器的主要特点是内部不带振荡源,所以如果使用直流信号是无法驱动无源蜂鸣器鸣叫的,因此必须使用方波去驱动它。

现在我们明白了,只要给蜂鸣器发送一定频率的方波,就可以使得蜂鸣器发出声音,然而现在的问题就转化为-我们究竟要给蜂鸣器发送什么频率的方波信号呢?具体的频率可以查表如下:

进阶项目(4)蜂鸣器程序设计讲解

 

现在我们知道了如何让蜂鸣器发出声音,又知道发送多大的频率可以让蜂鸣器响起什么样的声音,所以我相信我们已经有能力让蜂鸣器响起我们需要的音乐了。

设计任务

下面我们用FPGA来设计一个蜂鸣器的播放器,梦翼师兄开发板上的晶振是50Mhz的,所以我们需要一个锁相环模块(PLL)来得到低频率的时钟,然后再用这个比较低的频率来分出所需要的频率来驱动蜂鸣器。有了锁相环(PLL)后,我们还需要一个空间来保存乐谱,由于乐谱是确定的,不需要更改,所以我们选择一个简单的存储器就可以了,这里我们使用的是 ROM,可以直接通过IP 核来创建。 

既然是播放音乐,那么就需要节拍(一般为 4 拍),因此我们还需要一个节拍控制器, 例如,我们现在需要发出一个低音 1 并维持一秒,那怎么办呢? 我们可以设计一个模块,控制每 0.25s, ROM 的地址加一,若要使发出的低音 1 维持 1 秒 钟,我们仅需在 ROM 的四个连续地址中写入低音 1 的对应信息即可

具体的设计架构图如下所示:

 进阶项目(4)蜂鸣器程序设计讲解

由上图可知,整个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数据:

进阶项目(4)蜂鸣器程序设计讲解

 

 

 

端口介绍

顶层端口及其意义如下:

 

端口名

端口说明

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视图如下:

 

 

 进阶项目(4)蜂鸣器程序设计讲解

由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

仿真分析

进阶项目(4)蜂鸣器程序设计讲解

由仿真波形可知,当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增大,时钟频率越高,分频精度就越高,误差就越小。

 


 

 

上一篇:基础项目(3)三态门程序设计讲解


下一篇:简易项目(1)流水灯项目讲解