zoukankan      html  css  js  c++  java
  • 进阶项目(4)蜂鸣器程序设计讲解

    写在前面的话

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

    什么是蜂鸣器

    蜂鸣器是一种一体化结构的电子讯响器,采用直流电压供电,广泛应用于计算机、打印机、电子玩具、定时器等电子产品中作为发声器件。蜂鸣器分为有源蜂鸣器和无源蜂鸣器两种,在电路中用字母“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.25stime_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增大,时钟频率越高,分频精度就越高,误差就越小。


     

  • 相关阅读:
    Instruments之Core Animation学习
    Instruments之Allocations
    Instruments之Activity Monitor使用入门
    Instruments之相关介绍(一)
    快速理解Java中的五种单例模式
    iOS单例详解
    eclipse设置代码自动提示
    iOS-静态库,动态库,framework,bundle浅析(四)
    8.0docker的客户端和守护进程
    1.0 docker介绍
  • 原文地址:https://www.cnblogs.com/mengyi1989/p/11518282.html
Copyright © 2011-2022 走看看