小梅哥Xilinx FPGA学习笔记20——无源蜂鸣器驱动设计与验证(音乐发生器设计)

2024-01-07 21:05:53

目录

一:章节导读

二:无源蜂鸣器驱动原理

三:PWM 发生器模块设计

3.1 PWM 发生器模块框图

3.2 PWM 发生器模块接口功能描述

3.3 PWM波生成设计文件代码

3.4 测试仿真文件

3.5 测试仿真结果

3.6 板级调试与验证之顶层文件设计

四:基于 PWM 波的音乐发生器设计

4.1 “天空之城”乐谱

4.2 get_pitch 模块的代码

4.3 rom配置

4.4 coe文件

4.5 顶层文件设计

4.6 仿真验证代码

4.7 仿真结果

4.8 板上验证?


一:章节导读

?????? 蜂鸣器是一种产生声音的器件。其广泛应用于报警器、电子玩具、汽车电子设备、定时器等电子产品中作为发声器件。本章将介绍 ACZ702 开发板上使用的蜂鸣器电路,并使用 FPGA 来实现驱动 无源蜂鸣器按照“哆来咪发梭拉西”7个音调发声。并在此基础上实现 “天空之城”的音乐发生器。

???? 蜂鸣器按照构造方式的不同,可分为压电式蜂鸣器电磁式蜂鸣器两种类型,由于两种蜂鸣器发音原理不同,压电式结构简单耐用但音调单一音色差,适用于报警器等设备。而电磁式由于音色好,所以多用于语音、音乐等设备。?

?????? 蜂鸣器按照驱动电路的不同可以分为有源蜂鸣器无源蜂鸣器。有源蜂鸣器内部带震荡源,所以只要通电就会鸣叫;而无源蜂鸣器内部不带震荡源,因此如果用直流信号无法令其鸣叫,这就需要用 2K-5K 的方波(声音频率)去驱动。ACZ702 开发板上使用了一枚 3.3V 直流电压驱动的电磁式无源蜂鸣器。其原理图如下图所示:

BEEP 端口接FPGA 输出管脚,使用时只需要在 BEEP 信号上输入 2~5KHz PWM 波,就能
驱动蜂鸣器按照既定的频率产生振动信号。

二:无源蜂鸣器驱动原理

?????? 通过前面对无源蜂鸣器的特点介绍可知,要使无源蜂鸣器能够正常发声,需要在控制端 BEEP 给出相应频率的 PWM 波。因此,对于无源蜂鸣器的控制, 就转化为了设计一个 PWM 波发生电路。因此,接下来将介绍 PWM 波的发生部 分相关设计。
?????? PWM 波即脉冲宽度调制,以下为周期为 1KHz ,脉冲宽度(占空比)为 20%的示意图

?????? 由上图可知,当信号周期一定,信号高电平时间所占信号周期的百分比不一样,即为不同占空比的 PWM 波。而除了调整 PWM 信号的占空比, PWM 信号的周期也是可以调整的, 对于不同的器件,对驱动信号的频率要求也不一样,还需要能够对 PWM 波的频率进行调整。
?????? 通过以上分析,可以看出,要设计一个 PWM 发生电路,需要能够实现对信号的频率和占空比的调节。 单片机或者 DSP 中,产生 PWM 波的方法就是 使用片上定时器进行循环计数,通过设定定时器的一个定时周期时长来确定对 应输出 PWM 信号的频率,同时还有一个比较器,该比较器比较定时器的实时 计数值与用户设定的比较值的大小,根据比较结果来控制输出信号的电平高低。
通过设定不同的比较值,即可实现不同占空比的 PWM 信号输出。使用定时器产生 PWM 原理图如下:

三:PWM 发生器模块设计

对于 FPGA 来说,要产生 PWM 波,也可以借鉴单片机或 DSP 使用定时器产生 PWM 波的思路。依据 PWM 模块发生器原理可知需设计两个主要电路:定 时器 / 计数器电路以及输出比较电路。

3.1 PWM 发生器模块框图

3.2 PWM 发生器模块接口功能描述

最终输出 PWM 波的频率计算公式为:

??????????????????? ? ? ? ? ?? ? ?? ??

其中,这里的 counter_arr 是自减计数器的预重装值,计数器是从 counter_arr开始递减到 1 。因此,当输出频率确定时,可计算得到预重装值,计算公式为:

? ? ? ? ? ? ? ? ? ? ? ? ?? ? ? ? ? ??

例如,当希望设置输出信号频率为 5KHz

因此,只需要设置 counter_arr 值为 10000 即可使得最终输出信号频率为5KHz
?????? 当输出 PWM 频率确定后,其输出占空比计算则为输出比较值与预重装值
之商。计算公式为:

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? ??

因此,当输出占空比确定时,可计算得到输出比较值,计算公式为:

? ? ? ? ? ? ? ? ?? ?

?????? 在运行过程中,修改预重装值可以设置输出 PWM 信号的频率,并将同时影响输出占空比,而在预重装值确定的情况下,修改输出比较值,则可以设置 输出占空比。

3.3 PWM波生成设计文件代码

module pwm_gen(
    input clk,
    input reset_n,
    input pwm_gen_en,//使能信号
    input [31:0]counter_arr,//预重装值,用来设定频率
    input [31:0]counter_crr,//比较值,用来调节占空比
    output reg pwm_out//输出pwm波
    );
   
reg [31:0]pwm_gen_cnt;
    
always@(posedge clk or negedge reset_n)
    if(!reset_n)    
        pwm_gen_cnt <= counter_arr;
    else if(pwm_gen_en)begin
        if(pwm_gen_cnt <= 1)    
            pwm_gen_cnt <= counter_arr;  //计数减到 1,加载预重装寄存器值  
        else
            pwm_gen_cnt <= pwm_gen_cnt - 1;   //计数器自减 1
    end
    else               //未使能时,计数器值等于预重装寄存器值
        pwm_gen_cnt <= counter_arr;
        
 always@(posedge clk or negedge reset_n)
    if(!reset_n)      
        pwm_out <= 0;//复位时,PWM 输出低电平
    else if(pwm_gen_cnt <= counter_crr)  //计数值小于比较值,PWM 输出高电平
        pwm_out <= 1;   
    else   
        pwm_out <= 0;        //计数值大于比较值,PWM 输出低电平     
endmodule

3.4 测试仿真文件

`timescale 1ns / 1ps
module pwm_gen_tb();

reg clk;
reg reset_n;
reg pwm_gen_en;
reg [31:0]counter_arr;
reg [31:0]counter_crr;
wire pwm_out;

pwm_gen pwm_gen_inst(
    .clk         (clk) ,
    .reset_n     (reset_n) ,
    .pwm_gen_en  (pwm_gen_en) ,//使能信号
    .counter_arr (counter_arr) ,//预重装值,用来设定频率
    .counter_crr (counter_crr) ,//比较值,用来调节占空比
    .pwm_out     (pwm_out)    //输出pwm波
);

initial clk =1;
always #10 clk = ~clk;

initial begin
    reset_n = 0;
    pwm_gen_en = 0;
    counter_arr =0;
    counter_crr = 0;
    #201;
    reset_n = 1;
    #201;
    counter_arr = 1000; //设置输出信号频率为 50KHz
    counter_crr = 400; //设置输出 PWM 波占空比为 40%
    #100;
    pwm_gen_en = 1; //启动计数以产生 PWM 输出
    #100050;
    counter_crr = 700; //设置输出 PWM 波占空比为 70%
    #100050;
    pwm_gen_en = 0; //停止计数以关闭 PWM 输出
     
    counter_arr = 500; //设置输出信号频率为 100KHz
    counter_crr = 250; //设置输出 PWM 波占空比为 50%
    #100;
    pwm_gen_en = 1; //启动计数以产生 PWM 输出
    #50050;
    counter_crr = 100; //设置输出 PWM 波占空比为 20%
    #50050;
    pwm_gen_en = 0; //停止计数以关闭 PWM 输出
    #200;
    $stop;       
end
endmodule

3.5 测试仿真结果

放大局部波形可知满足测试仿真文件的要求。

3.6 板级调试与验证之顶层文件设计

module pwm_gen_test(
    input clk,
    input reset_n,
    output beep
    );
    
reg  [27:0]delay_500ms; //500ms延时计数器
reg  [2:0] bit_cnt;//位计数器 
reg  [31:0]counter_arr; //预重装值寄存器 
wire [31:0]counter_ccr; //输出比较值   

parameter  MCNT_500ms = 2500_0000;   
//7个音调的预重装值,也叫频率周期
localparam  D1 = 170068, //D 调音 1 
            D2 = 151515, //D 调音 2 
            D3 = 142857, //D 调音 3 
            D4 = 127227, //D 调音 4 
            D5 = 113379, //D 调音 5 
            D6 = 101010, //D 调音 6 
            D7 = 89928 ; //D 调音 7
            
//输出比较值为预重装值一半
assign   counter_ccr = counter_arr >> 1;//右移一位,数值变为一半

pwm_gen pwm_gen_inst(
    .clk                (clk)    ,
    .reset_n            (reset_n)    ,
    .pwm_gen_en         (1)    ,//使能信号
    .counter_arr        (counter_arr)    ,//预重装值,用来设定频率
    .counter_crr        (counter_ccr)    ,//比较值,用来调节占空比
    .pwm_out            (beep)      //输出pwm波
    );   
//500ms持续计数逻辑    
always@(posedge clk or negedge reset_n)
    if(!reset_n)    
        delay_500ms <= 0;
    else if(delay_500ms == MCNT_500ms - 1)
        delay_500ms <= 0;
    else 
        delay_500ms <= delay_500ms + 1;
 //每 500ms 切换一次音调   
always@(posedge clk or negedge reset_n)
    if(!reset_n)    
        bit_cnt <= 0;
    else if(delay_500ms == MCNT_500ms - 1)
        bit_cnt <= bit_cnt + 1;
    else 
        bit_cnt <= bit_cnt;    
    
//根据音调编号给预重装值给相应的值   
always@(posedge clk or negedge reset_n)
    if(!reset_n)
        counter_arr <= 1;
    else begin
        case(bit_cnt)
            0:counter_arr <= 1 ;  
            1:counter_arr <= D1;
            2:counter_arr <= D2;
            3:counter_arr <= D3;
            4:counter_arr <= D4;
            5:counter_arr <= D5;
            6:counter_arr <= D6;
            7:counter_arr <= D7;
        endcase 
    end
endmodule

3.7 管脚约束文件列表

set_property IOSTANDARD LVCMOS33 [get_ports beep]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports reset_n]
set_property PACKAGE_PIN G15 [get_ports beep]
set_property PACKAGE_PIN U18 [get_ports clk]
set_property PACKAGE_PIN F20 [get_ports reset_n]

???? 以上代码即可实现蜂鸣器即开始循环播放“哆来咪发梭拉西”7 个音。

基于 PWM 波的音乐发生器设计

前面已经实现并上板验证了使用 PWM 产生模块实现对 哆来咪发梭拉 西 ”音调的产生。 现在将在此基础上结合模块化设计思想,利用蜂鸣器实现播放 一段音乐的功能。
这里选取乐谱相对简单点的一首乐谱“天空之城”,在ACZ702 开发板上实现音乐播放。

4.1 “天空之城”乐谱

?? ? ? ? ? ? ? ? ? ? ???
从简谱看,该音乐是 D 调的,这里的各音符对应的频率对应的是上表中 D调的部分。另外,该音乐为四分之四拍,每个对应为 1 拍。(音符节奏分为一拍、 半拍、 1/4 拍、 1/8 拍,)几个特殊音符说明如下(我们规定一拍音符的时间为 1 半拍为 0.5 1/4 拍为 0.25 1/8 拍为 0.125…… )。
1 ) 普通音符。如音符 7 ,对应频率 556 ,占 1 拍。
2 ) 带下划线音符,表示 0.5 拍;两个下划线是四分之一拍( 0.25 )。
3 ) 有的音符后带一个点,表示多加 0.5 拍,即 1+0.5
4 ) 有的音符后带一个“—”,表示多加 1 拍,即 1+1
5 有的两个连续的音符上面带弧线,表示连音,可以稍微改下连音后面那个音的频率,比如减少或增加一些数值(需自己调试),这样表现会更 流畅,其实不做处理,影响也不大。
这里我们采用 ROM 来存储乐谱,然后通过依次读取 ROM 的数据,然后产生对应的音调。存储式采用 5bit 数据对音调进行编码,高 2bit 数据数值 3 2 1 作为高中低音编码,低 3bit 数据数值 1~7 作为 哆来咪发梭拉西 的编码。例如 存储的数据 5’b01_011 表示的是低音“ 3 ”。通过这种形式将乐谱进行编码存储在 ROM 中。
在节拍的设计上,如果以 250ms 时间为一拍的时间,半拍的时间是 125ms ,通过上面的乐谱来看,没有低于半拍的,那设计时就以 125ms 为基本单位时间, 1 拍就是 2 个单位时间,半拍是一个单位时间。 get_pitch 模块一方面产生这 125ms 的单位时间,另一方面在一个个单位时间的控制下产生获取 rom 里音符 的编码地址。

4.2 get_pitch 模块的代码

module get_pitch(
    input clk,
    input reset_n,
    output reg [8:0]pitch_num //获取音符 rom 的地址编号
    );
    
reg [23:0]cnt_125ms; //125ms 延时计数器,每个节拍的时间
  
parameter MCNT_125ms_MAX = 6250000; 
//125ms持续计数逻辑   
always@(posedge clk or negedge reset_n)   
    if(!reset_n)    
        cnt_125ms <= 0;       
    else if(cnt_125ms == MCNT_125ms_MAX - 1)
        cnt_125ms <= 0;
    else 
        cnt_125ms <= cnt_125ms + 1;
 
//每125ms切换一次音调的位控制逻辑 (每125ms获取一次音符 rom 的地址编号(递增))
always@(posedge clk or negedge reset_n)   
    if(!reset_n)    
        pitch_num <= 0;       
    else if(cnt_125ms == MCNT_125ms_MAX - 1)
        pitch_num <= pitch_num + 1;
    else 
        pitch_num <= pitch_num ; 
endmodule

4.3 rom配置

存放数据是以“天空之城”的乐谱实际不超过 256 个音符,也就是说初始化文件的数据不超过 512 个数据(数据以半拍为单位进行存储,一个节拍用两 个数据存储),这里 ROM 的深度设置为 512 ,没有初始化的 ROM 数据选择使用 0 来进行填充。这样正好让两次音乐播放之间还能留有一定的时间间隔。

4.4 coe文件

文件在下面的百度网盘里
链接:https://pan.baidu.com/s/1w3spnKZWKi7u2du7ML6Prw
提取码:qzof

4.5 顶层文件设计

module music_gen(
    input clk,
    input reset_n,
    output beep
    );
 
reg [31:0]counter_arr ;//预重装值寄存器
wire [31:0]counter_crr; //输出比较值

wire [8:0]pitch_num; //音乐的音调编号,0-->最大值 循环递增
wire [4:0]pitch;   //音乐的音调 
    
localparam       DL1 = 340136, //D 调低音 1
                 DL2 = 303030, //D 调低音 2
                 DL3 = 285714, //D 调低音 3
                 DL4 = 255102, //D 调低音 4
                 DL5 = 226244, //D 调低音 5
                 DL6 = 201613, //D 调低音 6
                 DL7 = 179856, //D 调低音 7
                 D1 = 170068, //D 调音 1 
                 D2 = 151515, //D 调音 2 
                 D3 = 142857, //D 调音 3 
                 D4 = 127227, //D 调音 4 
                 D5 = 113379, //D 调音 5 
                 D6 = 101010, //D 调音 6 
                 D7 = 89928 , //D 调音 7 
                 DH1 = 84889, //D 调高音 1 
                 DH2 = 75643, //D 调高音 2 
                 DH3 = 71429, //D 调高音 3 
                 DH4 = 63613, //D 调高音 4 
                 DH5 = 56689, //D 调高音 5 
                 DH6 = 50505, //D 调高音 6 
                 DH7 = 44964; //D 调高音 7
                 
//根据 rom 存储输出不同的音调输出不同的预置数
always@(posedge clk or negedge reset_n)
    if(!reset_n) 
        counter_arr <= 1;
    else begin
        case(pitch)
            5'b01_001:counter_arr = DL1; //D 调低音 1
            5'b01_010:counter_arr = DL2; //D 调低音 2
            5'b01_011:counter_arr = DL3; //D 调低音 3
            5'b01_100:counter_arr = DL4; //D 调低音 4
            5'b01_101:counter_arr = DL5; //D 调低音 5 
            5'b01_110:counter_arr = DL6; //D 调低音 6
            5'b01_111:counter_arr = DL7; //D 调低音 7
            
            5'b10_001:counter_arr = D1; //D 调音 1 
            5'b10_010:counter_arr = D2; //D 调音 2 
            5'b10_011:counter_arr = D3; //D 调音 3 
            5'b10_100:counter_arr = D4; //D 调音 4 
            5'b10_101:counter_arr = D5; //D 调音 5 
            5'b10_110:counter_arr = D6; //D 调音 6 
            5'b10_111:counter_arr = D7; //D 调音 7
            
            5'b11_001:counter_arr = DH1; //D 调高音 1 
            5'b11_010:counter_arr = DH2; //D 调高音 2
            5'b11_011:counter_arr = DH3; //D 调高音 3
            5'b11_100:counter_arr = DH4; //D 调高音 4 
            5'b11_101:counter_arr = DH5; //D 调高音 5
            5'b11_110:counter_arr = DH6; //D 调高音 6
            5'b11_111:counter_arr = DH7; //D 调高音 7
            default: counter_arr = 32'd1;//休止符
        endcase
    end    


//rom例化
blk_mem_gen_0 your_instance_name (
  .clka(clk),    // input wire clka
  .addra(pitch_num),  // input wire [8 : 0] addra
  .douta(pitch)  // output wire [4 : 0] douta
);

get_pitch  get_pitch(
    .clk(clk),
    .reset_n(reset_n),
    .pitch_num(pitch_num)
    );
              
pwm_gen pwm_gen(
    .clk             (clk),
    .reset_n         (reset_n),
    .pwm_gen_en(1),//使能信号
    .counter_arr(counter_arr),//预重装值,用来设定频率
    .counter_crr(counter_crr),//比较值,用来调节占空比
    .pwm_out(beep)//输出pwm波
);                 
 //设置输出比较值为预重装值一半                
assign  counter_crr = counter_arr >> 1;           
                 
endmodule  

4.6 仿真验证代码

`timescale 1ns / 1ps
module music_gen_tb();

reg clk;//系统时钟输入,50M
reg reset_n; //复位信号输入,低有效
wire beep;//pwm 输出信号

music_gen  music_gen(
    .clk(clk),
    .reset_n(reset_n),
    .beep(beep)
);

defparam music_gen.get_pitch.MCNT_125ms_MAX = 24'd6250;//缩短仿真时间

    initial clk = 1;
    always #10 clk = ~clk;

    initial 
    begin
    reset_n = 0;    
    #201;
    reset_n = 1;
    wait(music_gen.pitch_num == 512);//一轮播放结束
    #200;
    $stop;
 
    end
endmodule

4.7 仿真结果

从波形上可以看出,循环音乐循环播放之间有一定的时间间隔,这个与设计预期是一致的。可以放大波形后看,其波形也是与预期设计是一致的。

4.8 板上验证?

set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports reset_n]
set_property IOSTANDARD LVCMOS33 [get_ports beep]
set_property PACKAGE_PIN G15 [get_ports beep]
set_property PACKAGE_PIN F20 [get_ports reset_n]
set_property PACKAGE_PIN U18 [get_ports clk]

至此“天空之城”音乐发生器设计完毕。

文章来源:https://blog.csdn.net/m0_51430584/article/details/135383365
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。