文章目录
本小节的内容就是完成无源蜂鸣器的驱动实验,目的是教会大家如何驱动我们的无源蜂鸣器。
本讲视频的主要内容分为两个部分:理论学习和实战演练。那么在理论部分,我们会对蜂鸣器的相关知识以及无源蜂鸣器的驱动原理做一个详细的讲解;那么在实战演练部分,我们会通过实验工程实现无源蜂鸣器的驱动实验。
首先是理论学习。
1 理论学习
1.1 蜂鸣器简介
那么蜂鸣器大家都见过,它是一种一体化结构的电子讯响器,具有体积小、重量轻、价格低、结构牢靠的特点,被广泛应用在报警器、电子玩具、定时器等电子产品当中,作为一个发声器件;那么蜂鸣器按照它的结构可以分为电磁式蜂鸣器、压电式蜂鸣器两种,如果说按照其是否带有信号源又可以分为有源蜂鸣器和无源蜂鸣器,那么有源蜂鸣器它内部是装有集成电路的,不需要音频驱动电路,只要接通直流电源就能够直接发出声响;而我们的无源蜂鸣器只有外部加音频驱动信号才能发出声响,我们征途系列开发板上使用的就是我们的无源蜂鸣器,那么下面这张图就是有源蜂鸣器与无源蜂鸣器,左边的是有源蜂鸣器、右边的是无源蜂鸣器
单单从外观上来看,我们的有源蜂鸣器它的高度高一些,在引脚部位它是黑胶封闭;而我们的无源蜂鸣器引脚部位是有裸露的电路板。我们征途系列开发板使用的是无源蜂鸣器,我们的实验也是无源蜂鸣器的驱动实验,所以说我们要说一下无源蜂鸣器的驱动原理。
1.2 无源蜂鸣器的驱动原理
无源蜂鸣器与有源蜂鸣器不同,它内部不带振荡源,所以说它无法像有源蜂鸣器那样直接用直流信号就可以驱动,它这里需要使用 PWM 方波才能驱动其发声。如果我们给无源蜂鸣器输入不同频率和占空比的 PWM 方波,它发出的声音是不同的;其中的频率对它的音调是有影响的,方波的占空比对音量大小是有影响的,我们来看一下下面这张表
音调 | 1(Do) | 2(Re) | 3(Mi) | 4(Fa) | 5(So) | 6(La) | 7(Si) |
---|---|---|---|---|---|---|---|
频率(Hz) | 262 | 294 | 330 | 349 | 392 | 440 | 494 |
这张表展示的是七个音阶:Do、Re、Mi、Fa、So、La、Si 对应的七个频率值。它表示什么意思呢?我们来举个例子
比如说音调 Fa 它对应的频率值是 349Hz,这就表示:如果给我们的无源蜂鸣器输入一个频率为 349Hz 的 PWM 方波,它就可以发出 Fa 的声音;如果输入的信号频率值是 392Hz,它就会发出 So 的声音。
以上内容就是理论部分对蜂鸣器以及无源蜂鸣器驱动原理的讲解,那么下面开始我们的实战演练
2 实战演练
在实战演练部分,我们将完成无源蜂鸣器的驱动实验。
2.1 实验目标
我们的实验目标是驱动开发板上的无源蜂鸣器,进行七个基本音调:Do、Re、Mi、Fa、So、La、Si 的循环发声,每个音调发声 0.5s 占空比是 50%。
2.2 硬件资源
我们使用开发板上的无源蜂鸣器进行该实验的验证,如下图所示
由原理图可知,我们只需要在 BEEP 端输入 PWM 方波即可驱动无源蜂鸣器进行鸣叫了。如下图所示
2.3 程序设计
了解了实验目标之后,下面就开始程序的设计。
首先,建立一个文件体系
然后打开 doc 文件夹建立一个 Visio 文件,绘制我们的框图和我们的波形图
2.3.1 框图绘制
那么首先是模块框图的绘制
下面就是端口列表。我们来看一下我们的实验目标:我们的目的是驱动我们的无源蜂鸣器进行发声,其实就是输出不同频率的 PWM 方波。那么这个感觉就和我们的分频器非常类似,所以说它的输入、输出信号也应该和分频器相同,输入就是时钟和复位信号;那么输出就是不同频率的信号,因为是输出到蜂鸣器,那么信号命名为 beep
2.3.2 波形绘制
那么下面就是波形图的绘制。首先是我们的输入信号
那么这样输入信号的波形绘制完成。下面应该怎么绘制呢?我们先来分析一下:
我们在实验目标中提到:我们要驱动无源蜂鸣器进行七个基本音调的发声,那么每个音调发声是
0.5
s
0.5\text{s}
0.5s。那么这儿就可以确定要使用一个计数器,计时
0.5
s
0.5\text{s}
0.5s,我们把它取名为 cnt
;那么它的初值应该是
0
0
0 初值是
0
0
0 每个周期自加一,计数到最大值就归零,进行下一个循环的计数,那么它的计数最大值是多少呢?之前已经教过大家如何计算了,那么经过计算,在
50
MHz
50\text{MHz}
50MHz 的频率下完成
0.5
s
0.5\text{s}
0.5s 的计数,需要计数
2.5
×
1
0
7
2.5\times10^{7}
2.5×107 个周期;因为是从
0
0
0 开始计数,所以说计数的最大值应该是
2.5
×
1
0
7
−
1
2.5\times10^{7}-1
2.5×107−1 那么这儿采用省略的画法,我们画一下
那么当计数到最大值它就归零,然后开始下一个周期的计数
那么到了这里,0.5s 计数器的波形绘制完成。
我们继续分析我们的实验目标。我们的实验目标是驱动我们的蜂鸣器进行七个基本音调:Do、Re、Mi、Fa、So、La、Si 的循环发声,每个音调发声 0.5s;也就是说,在计数的第一个 0.5s 发 Do 第二个 0.5s 发 Re 然后第七个 0.5s 是发 Si,然后发完 Si 之后下一个 0.5s 又回到了 Do 那么怎么实现这个循环呢?
我们就想到了可以对我们的 0.5s 周期进行计数。我们声明一个计数器对 0.5s 的计数器进行一个计数,那么这样就可以了
它的初值是 0,当我们的 0.5s 计数器完成第一个 0.5s 的计数之后它加一;那么 0 到 6 就完成了七个周期的一个计数。我们的计数器 cnt_500ms 是对我们的 0.5s 计数器进行一个计数,那么它每加一就代表完成了一个 0.5s 的一个计数;当它的计数值为 0 就代表这个周期要发 Do 音,为 1 就是 Re 音,然后后面就是 Mi、Fa、So、La、Si;那么发完 Si 音之后,再下一个 0.5s 继续发 Do 音,就完成了下一个七个音调的一个循环。
那么声明了这两个计数器,就能保证完成七个音调:Do、Re、Mi、Fa、So、La、Si 的发声,而且保证每个音调发声的时间为 0.5s。
那么接下来我们继续分析。我们的目的是使我们的蜂鸣器发出 Do、Re、Mi、Fa、So、La、Si 七个音调的声音;我们已经知道,我们的无源蜂鸣器想要让它发出不同音调的声音,就要输出不同频率的 PWM 方波。那么在前面的讲解当中我们已经提到了 Do、Re、Mi、Fa、So、La、Si 七个音调对应的各自的频率,那么知道了它们各自对应的频率,怎么产生这个频率信号呢?
其实它的产生就和分频是一样的。那么既然是分频,就需要一个分频计数器
那么有了系统时钟和我们的分频计数器,就可以产生我们想要的频率的信号。怎么产生呢?我们来看一下
首先,我们的计数器有一个初值是 0 然后它每个周期会自加一,那么它计数的最大值是多少呢?这儿就需要计算一下
我们以 Do 为例:那么音调 Do 它对应的频率信号频率是 262 Hz 262\text{Hz} 262Hz,而我们的系统时钟是 50 MHz 50\text{MHz} 50MHz, 50 MHz 50\text{MHz} 50MHz 对应的时钟周期是 20 ns 20\text{ns} 20ns, 262 Hz 262\text{Hz} 262Hz 对应的时钟周期经过计算 1 262 Hz ≈ 3816794 ns \frac{1}{262\text{Hz}}\approx3816794\text{ns} 262Hz1≈3816794ns 得到 3816794 ns 3816794\text{ns} 3816794ns;那么得到这两个参数,使用 3816794 ns 3816794\text{ns} 3816794ns 这个值除以 20 ns 20\text{ns} 20ns 就可以得到我们计数器计数的最大值: 3816794 ns / 20 ns ≈ 190840 3816794\text{ns}/20\text{ns}\approx190840 3816794ns/20ns≈190840。也就是说在 50 MHz 50\text{MHz} 50MHz 的时钟频率下要输出频率为 262 Hz 262\text{Hz} 262Hz 的信号,我们的计数器需要计数 190840 190840 190840 个时钟周期。
因为我们的计数器是从 0 开始计数,所以说计数的最大值应该是 190840 − 1 = 190839 190840-1=190839 190840−1=190839 那么这儿就应该这样画
如果发出其他音调,freq_cnt
这个计数值是会改变的。那么经过计算,我们得到下面这样一张表
频率(Hz) | 音调分频计数值(freq_cnt) | 占空比计数值(duty_cnt) |
---|---|---|
262 | 190840 | 95420 |
294 | 170068 | 85034 |
330 | 151515 | 75757 |
349 | 143266 | 71633 |
392 | 127551 | 63775 |
440 | 113636 | 56818 |
494 | 101214 | 50607 |
那么这张表显示的是 Do、Re、Mi、Fa、So、La、Si 这七个音调频率对应的计数最大值;那么这儿也计算出了占空比的计数值,我们前面实验目标已经提到了,我们的占空比是 50%,所以说 duty_cnt
这个值是 freq_cnt
的二分之一。
那么根据上面这张表格,我们可以完善我们的频率计数器的波形
那么这样,频率计数器的波形绘制完成。由绘制的波形图可知,我们的频率计数器它计数的最大值有 7 种情况,我们的频率计数器只有一个;那么为了能够准确的产生不同频率的 PWM 方波,我们需要灵活的控制清零的条件,那么这个清零条件我们就需要用一个中间变量 freq_data
来进行传递;那么它的初值我们就让它等于 Do 对应的这个值,然后当我们的计数器计数到 1 我们就让它等于 Re 对应的这个最大值,然后参照这个规律绘制我们的波形图
那么这样频率计数最大值的波形绘制完成。下面我们再声明一个变量,声明一个占空比计数值,使用这个变量来控制我们的占空比;我们的占空比已经设置为 50% 所以说它的值应该等于 freq_data
这个变量的一半,如果说你想要输出其他占空比的波形,就可以通过计算对它进行修改
那么这样,占空比计数器的波形也绘制完成了。那么下面就可以开始绘制输出信号的波形,我们单单拿出一个周期来看,我们拿出 Do 这个周期来看
那么在音阶 Do 这个发声范围内,我们的输出信号应该怎么变化呢?我们来看一下。
首先,要给它一个初值,初值是低电平;当我们的频率计数器它的计数值小于占空比计数值时,让它保持低电平,当我们的频率计数器它的计数值大于占空比计数值时,把低电平拉高,让它保持高电平;然后下一个周期也是这样变化,那么参照这个规律,我们完善我们的波形图
那么这样,在音阶 Do 发声周期内它的输出波形我们绘制完成。在其他音阶发声周期内它的波形变化也是类似的:当计数值小于占空比计数值时保持低电平,大于时保持高电平;那么这样就能够得到,各个音阶对应的频率信号,这样就可以驱动我们的蜂鸣器实现 Do、Re、Mi、Fa、So、La、Si 七个循环的发声。
2.3.3 RTL 代码编写
那么接下来,参照我们绘制的波形图进行代码的编写
那么首先是我们的模块开始,然后是模块名称、端口列表,然后是模块结束
由图可知输入信号有两路:时钟信号和复位信号;然后有一路输出信号
那么接下来定义我们的变量,首先是 0.5s 的计数器 cnt
我们后面将使用 always
语句对它进行赋值,所以说它的变量类型是 reg
型;它计数的最大值是
(
24
_
999
_
999
)
10
(24\_999\_999)_{10}
(24_999_999)10 换算成二进制就是
(
1
_
0111
_
1101
_
0111
_
1000
_
0011
_
1111
)
2
(1\_0111\_1101\_0111\_1000\_0011\_1111)_{2}
(1_0111_1101_0111_1000_0011_1111)2 那么它的位宽是 25 位宽,就是 [24:0]
然;后是变量 cnt_500ms
对 0.5s 的个数进行计数,那么计数的最大值是 6 对应位宽就应该是 3 位宽就,是 [2:0]
;然后是我们的频率计数器 freq_cnt
,最大值应该是
(
190
_
839
)
10
(190\_839)_{10}
(190_839)10 然后换算成二进制就是
(
10
_
1110
_
1001
_
0111
_
0111
)
2
(10\_1110\_1001\_0111\_0111)_{2}
(10_1110_1001_0111_0111)2 位宽是 18 位宽,就是 [17:0]
;那么下面是计数最大值 freq_data
它的位宽应该和 freq_cnt
是一样的;然后是占空比计数值 duty_cnt
我们的占空比是 50%,它的值等于 freq_data
的一半,最大值应该是
(
95
_
419
)
10
(95\_419)_{10}
(95_419)10 换算成二进制就是
(
1
_
0111
_
0100
_
1011
_
1011
)
2
(1\_0111\_0100\_1011\_1011)_{2}
(1_0111_0100_1011_1011)2 位宽是 17 位宽,就是 [16:0]
那么这样变量声明完成,下面开始变量的赋值。
那么首先是我们的计数器 cnt
使用 always
语句进行赋值
那么下面开始变量 cnt_500ms
的赋值,同样使用 always 语句
那么下面开始频率计数器的赋值,我们同样使用 always
语句
那么接下来就是变量计数最大值的赋值,就是 freq_data
这个变量,它的赋值我们要使用我们的 case
条件分支语句
那么这样,变量计数器最大值它的赋值就完成了。那么接下来就是占空比计数值的赋值,就是 duty_cnt
。我们的占空比已经规定是 50% 它的值就是 freq_data
的二分之一,我们可以使用除法来实现;但是这儿我们使用移位的方法来实现,我们使用 assign
语句
那么最后就是输出信号的赋值,我们同样使用 always
语句
那么这样,代码编写完成,我们保存。
2.3.4 RTL 代码编译
那么保存之后我们回到桌面,建立一个新的实验工程,然后添加我们的文件就是我们编写的代码
然后全编译
编译通过点击 OK
查看警告信息,提示位宽问题,回到代码发现忘记编写参数的位宽,修改完成并且保存后回到工程重新进行编译
beep.v
module beep
#(
parameter CNT_MAX = 25'd24_999_999,
parameter DO = 18'd190839,
parameter RE = 18'd170067,
parameter MI = 18'd151514,
parameter FA = 18'd143265,
parameter SO = 18'd127550,
parameter LA = 18'd113635,
parameter SI = 18'd101213
)
(
input wire sys_clk ,
input wire sys_rst_n ,
output reg beep
);
reg [24:0] cnt;
reg [2:0] cnt_500ms;
reg [17:0] freq_cnt;
reg [17:0] freq_data;
wire[16:0] duty_cnt;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt <= 25'd0;
else if (cnt == CNT_MAX)
cnt <= 25'd0;
else
cnt <= cnt + 25'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt_500ms <= 3'd0;
else if ((cnt == CNT_MAX)
&&(cnt_500ms == 3'd6))
cnt_500ms <= 3'd0;
else if (cnt == CNT_MAX)
cnt_500ms <= cnt_500ms + 3'd1;
else
cnt_500ms <= cnt_500ms;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
freq_cnt <= 18'd0;
else if ((cnt == CNT_MAX)
||(freq_cnt == freq_data))
freq_cnt <= 18'd0;
else
freq_cnt <= freq_cnt + 18'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
freq_data <= DO;
else case(cnt_500ms)
3'd0:freq_data <= DO;
3'd1:freq_data <= RE;
3'd2:freq_data <= MI;
3'd3:freq_data <= FA;
3'd4:freq_data <= SO;
3'd5:freq_data <= LA;
3'd6:freq_data <= SI;
default:freq_data <= DO;
endcase
assign duty_cnt = freq_data >> 1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
beep <= 1'b0;
else if (freq_cnt >= duty_cnt)
beep <= 1'b1;
else
beep <= 1'b0;
endmodule
2.3.5 仿真验证
2.3.5.1 仿真代码编写
然后接下来编写我们的仿真代码
那么首先是我们的时间参数,然后是模块开始、模块名称、端口列表(空的),然后是模块结束
然后是变量的定义
然后是初始化
然后是我们的时钟
然后是实例化。在这个位置我们对所有的参数缩小十倍;在这个位置,还要将我们的信号引出来
那么这样,我们的仿真代码编写完成,保存。
tb_beep.v
`timescale 1ns/1ns
module tb_beep();
reg sys_clk;
reg sys_rst_n;
wire beep;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
beep
#(
.CNT_MAX (25'd24_999_99),
.DO (18'd19083),
.RE (18'd17006),
.MI (18'd15151),
.FA (18'd14326),
.SO (18'd12755),
.LA (18'd11363),
.SI (18'd10121)
)
beep_inst
(
.sys_clk (sys_clk),
.sys_rst_n(sys_rst_n),
.beep (beep)
);
endmodule
2.3.5.2 仿真代码编译
回到我们的实验工程,然后添加我们的文件,然后全编译检查语法错误
2.3.5.3 仿真设置
编译通过之后,点击 OK。然后进行仿真设置
2.3.5.4 波形仿真
然后开始仿真
仿真编译完成之后,打开 sim 选项卡,添加我们的模块波形;然后回到波形界面,全选、分组、消除前缀
然后点击 Restart,那么这儿有一点要注意:我们的代码当中参数的数值是比较大的,所以说,它的运行时间稍微长一点;我们这儿先设置为 100ms 先运行一次
那么 100ms 运行完成,全局视图。我们已经运行了 100ms,这儿只跑了两个周期,我们需要至少八个周期;那么这儿先改为 400ms,运行一次。那么 400ms 运行完成,我们全局视图
来看一下,首先调整一下进制
2.3.5.5 波形查看对比
接下来参照我们的绘制波形图,看一下我们的仿真波形。
那么首先看一下变量 cnt
那么变量 cnt
它的波形是没有问题的
那么接下来看一下变量 cnt_500ms
那么变量 cnt_500ms
它的波形也没有问题
那么下面看一下频率计数器 freq_cnt
那么接下来看一下输出信号 beep
那么这样,模块仿真验证通过。
2.4 上板验证
2.4.1 管脚绑定
我们回到我们的实验工程,开始绑定我们的管脚。蜂鸣器——>J11、时钟信号——>E1、复位信号——>M15
那么这样绑定完成,回到我们的实验工程,全编译
2.4.2 结果验证
那么在这个时间,我们可以连接我们的开发板。
开发板连接电源和下载器,那么下载器的另一端连接到我们的电脑
那么编译完成点击 OK。打开下载界面,然后添加 SOF 文件,给我们的开发板上电,点击 Start 下载程序
蜂鸣器实验上板验证
那么刚才我们就听到了我们的开发板发出了 Do、Re、Mi、Fa、So、La、Si 循环发声。
那么这样,上板验证成功
参考资料: