【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解

目录

  • 一、知识了解
  • 二、模块设计
  • 三、程序实现
  • 四、管脚配置及结果展示
  • 五、写给小白看的

上一篇博文:【入门学习一】基于 FPGA 使用 Verilog 实现按键点灯代码及原理讲解

功能描述:通过前面一篇学习的按键使用,本篇文章进一步使用按键,通过点击按键后,可以让蜂鸣器播放不同的曲子

一、知识了解

PWM 技术控制蜂鸣器不同声响

  • 所谓 PWM 就是脉冲宽度调制,本文通过变化输出的脉冲频率来使得蜂鸣器发出不同的音符声音。
  • 下表是每个音符所对应的频率及半周期。
音符 对应频率(Hz) 时钟周期数
1 523 95600
2 587 85150
3 659 75850
4 698 71600
5 784 63750
6 880 56800
7 988 50600
  • 音符对应的时钟周期数越多,那么它的一个周期就越长,从下图可以看出来,音符从 1 到 7,它的一个周期逐次递减。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第1张图片
  • 所以说,如何让蜂鸣器发声,只需要往蜂鸣器的管脚输出连续的高低变化电平即可。
  • 就拿我这个板子的蜂鸣器来说,图中有个 PNP 型三极管,当基极为高电平时截止,也就是高电平的发射极 Q7 无法导通到集电极使蜂鸣器发声,低电平时导通,此时高电平的发射极 Q7 可以导通到集电极使蜂鸣器发声。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第2张图片
  • 要让蜂鸣器发出不同的声音,采用 PWM 原理改变基极的脉冲宽度即可,也就是前面提到的,不同音符的一个周期所对应不同宽度脉冲。

设置每个音符持续时长

  • 当输出一个周期的脉冲给蜂鸣器,它肯定会响,但是有一点,一个周期的脉冲时长只有几百毫秒,我们能听到吗?当然听不到,所以需要不断地重复一个周期的脉冲,使它连续输出波形长达 1 s 或者 0.5 s,也就是一个音符的持续时长,那么我们就肯定能听到了。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第3张图片
  • 这里我以音符 1 为基准,让它的一个周期重复 250 次,那么它总时长为 95600 × 250 = 23 , 900 , 000 95600×250=23,900,000 95600×250=23,900,000 个时序周期,其它的音符总时长也为 23 , 900 , 000 23,900,000 23,900,000 个时序周期,所以不难得出
    每 个 音 符 的 重 复 次 数 = 23 , 900 , 000 该 音 符 一 个 周 期 时 序 数 每个音符的重复次数=\frac{23,900,000}{该音符一个周期时序数}\\ =23,900,000
  • 音符1 = 23 , 900 , 000 95600 ≈ 250 ( 次 ) = \frac{23,900,000}{95600}≈250(次) =9560023,900,000250
    音符2 = 23 , 900 , 000 85150 ≈ 281 ( 次 ) = \frac{23,900,000}{85150}≈281(次) =8515023,900,000281
    音符3 = 23 , 900 , 000 75850 ≈ 315 ( 次 ) = \frac{23,900,000}{75850}≈315(次) =7585023,900,000315
    音符4 = 23 , 900 , 000 71600 ≈ 334 ( 次 ) = \frac{23,900,000}{71600}≈334(次) =7160023,900,000334
    音符5 = 23 , 900 , 000 63750 ≈ 375 ( 次 ) = \frac{23,900,000}{63750}≈375(次) =6375023,900,000375
    音符6 = 23 , 900 , 000 56800 ≈ 421 ( 次 ) = \frac{23,900,000}{56800}≈421(次) =5680023,900,000421
    音符7 = 23 , 900 , 000 50600 ≈ 472 ( 次 ) = \frac{23,900,000}{50600}≈472(次) =5060023,900,000472

歌谱

  • 歌谱如下:
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第4张图片
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第5张图片

二、模块设计

  • 由于程序比较简单,所以就只需要一个蜂鸣器 .v 文件即可,它也是顶层模块,再在其中引用按键模块。
  • 其中按键模块 key_debounce.v 在前一篇博文中已经贴出了,这里就不再重复贴出代码以及讲解了。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第6张图片

三、程序实现

蜂鸣器模块 pwm_buzzer.v

module pwm_buzzer(
	input				clk		,		//时钟输入
	input				rst_n	,		//复位按键输入
	input				key_in	,		//按键输入
	
	output	reg			buzzer			//驱动蜂鸣器
	);
	
	wire				press	;		//线,连接按键标志信号
	
	//引用按键模块
	key_debounce u_key_debounce(
		.clk			(clk	),
		.rst_n			(rst_n	),
		.key			(key_in	),
		.press			(press	)
	);
	
	//定义音符时序周期数
	localparam			M0 	= 98800,
						M1	= 95600,
						M2	= 85150,
						M3	= 75850,
						M4	= 71600,
						M5  = 63750,
						M6	= 56800,
						M7	= 50600;
	
	//信号定义
	reg		[16:0]		cnt0		;	//计数每个音符对应的时序周期
	reg		[10:0]		cnt1		;	//计数每个音符重复次数
	reg		[5 :0]		cnt2		;	//计数曲谱中音符个数
	
	reg		[16:0]		pre_set		;	//预装载值
	wire	[16:0]		pre_div		;	//占空比
	
	reg		[10:0]		cishu		;	//定义不同音符重复不同次数
	wire	[10:0]		cishu_div	;	//音符重复次数占空比
	
	reg 				flag		;	//歌曲种类标志:0小星星,1两只老虎
	reg		[5 :0]		YINFU		;	//定义曲谱中音符个数
	
	//歌曲种类标志位
	always @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			flag <= 1'b0;
		end
		else if(press) begin
			flag <= ~flag;
		end
	end
	
	//重设音符的个数
	always @(posedge clk or negedge rst_n) begin
		if(!rst_n)
			YINFU <= 48;
		else if(flag == 1'b1)
			YINFU <= 36;
		else
			YINFU <= 48;
	end
	
	//计数每个音符的周期,也就是表示音符的一个周期
	always @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			cnt0 <= 0;
		end
		else if(press)
			cnt0 <= 0;
		else begin
			if(cnt0 == pre_set - 1)
				cnt0 <= 0;
			else
				cnt0 <= cnt0 + 1;
		end
	end
	
	//计数每个音符重复次数,也就是表示一个音符的响鸣持续时长
	always @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			cnt1 <= 0;
		end
		else if(press)
			cnt1 <= 0;
		else begin
			if(cnt0 == pre_set - 1)begin
				if(cnt1 == cishu)
					cnt1 <= 0;
				else
					cnt1 <= cnt1 + 1;
			end
		end
	end
	
	//计数有多少个音符,也就是曲谱中有共多少个音符
	always @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			cnt2 <= 0;
		end
		else if(press)
			cnt2 <= 0;
		else begin
			if(cnt1 == cishu && cnt0 == pre_set - 1) begin
				if(cnt2 == YINFU - 1) begin
					cnt2 <= 0;
				end
				else
					cnt2 <= cnt2 + 1;
			end
		end
	end
	
	//定义音符重复次数
	always @(*) begin
		case(pre_set)
			M0:cishu = 242;
			M1:cishu = 250;
			M2:cishu = 281;
			M3:cishu = 315;
			M4:cishu = 334;
			M5:cishu = 375;
			M6:cishu = 421;
			M7:cishu = 472;
		endcase
	end
	
	//曲谱定义
	always @(*) begin
		if(flag == 1'b0) begin
			case(cnt2)	//小星星歌谱
				0 : pre_set = M1;
				1 : pre_set = M1;
				2 : pre_set = M5;
				3 : pre_set = M5;
				4 : pre_set = M6;
				5 : pre_set = M6;
				6 : pre_set = M5;
				7 : pre_set = M0;
				
				8 : pre_set = M4;
				9 : pre_set = M4;
				10: pre_set = M3;
				11: pre_set = M3;
				12: pre_set = M2;
				13: pre_set = M2;
				14: pre_set = M1;
				15: pre_set = M0;
				
				16: pre_set = M5;
				17: pre_set = M5;
				18: pre_set = M4;
				19: pre_set = M4;
				20: pre_set = M3;
				21: pre_set = M3;
				22: pre_set = M2;
				23: pre_set = M0;
				
				24: pre_set = M5;
				25: pre_set = M5;
				26: pre_set = M4;
				27: pre_set = M4;
				28: pre_set = M3;
				29: pre_set = M3;
				30: pre_set = M2;
				31: pre_set = M0;
				
				32: pre_set = M1;
				33: pre_set = M1;
				34: pre_set = M5;
				35: pre_set = M5;
				36: pre_set = M6;
				37: pre_set = M6;
				38: pre_set = M5;
				39: pre_set = M0;
				
				40: pre_set = M4;
				41: pre_set = M4;
				42: pre_set = M3;
				43: pre_set = M3;
				44: pre_set = M2;
				45: pre_set = M2;
				46: pre_set = M1;
				47: pre_set = M0;
			endcase
		end
		else begin
			case(cnt2)	//两只老虎歌谱
				0 : pre_set = M1;
				1 : pre_set = M2;
				2 : pre_set = M3;
				3 : pre_set = M1;
				4 : pre_set = M1;
				5 : pre_set = M2;
				6 : pre_set = M3;
				7 : pre_set = M1;
				8 : pre_set = M3;
				9 : pre_set = M4;
				10: pre_set = M5;
				11: pre_set = M0;
				
				12: pre_set = M3;
				13: pre_set = M4;
				14: pre_set = M5;
				15: pre_set = M0;
				
				16: pre_set = M5;
				17: pre_set = M6;
				18: pre_set = M5;
				19: pre_set = M4;
				20: pre_set = M3;
				21: pre_set = M1;
				22: pre_set = M5;
				23: pre_set = M6;
				24: pre_set = M5;
				25: pre_set = M4;
				26: pre_set = M3;
				27: pre_set = M1;
				28: pre_set = M2;
				29: pre_set = M5;
				30: pre_set = M1;
				31: pre_set = M0;
				
				32: pre_set = M2;
				33: pre_set = M5;
				34: pre_set = M1;
				35: pre_set = M0;
			endcase
		end
	end
	
	assign pre_div = pre_set >> 1;	//除以2
	assign cishu_div = cishu * 4 / 5;
	
	//向蜂鸣器输出脉冲
	always @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			buzzer <= 1'b1;
		end
		else if(pre_set != M0) begin
			if(cnt1 < cishu_div) begin
				if(cnt0 < pre_div) begin
						buzzer <= 1'b1;
				end
				else begin
						buzzer <= 1'b0;
				end
			end
			else begin
				buzzer <= 1'b1;
			end
		end
		else
			buzzer <= 1'b1;
	end
	
endmodule
	
  • 其中要说明的一点是,曲谱中有一个 “-” 的符号,我将它定位 M0,可以在向蜂鸣器输出波形的时序逻辑中,可以看到当音符为 M0 时,它输出高电平,也就是让它此时不发声。
  • 还有一点时,我设置了一个时间占空比为 4/5,也就是前 4/5 发声,后 1/5 的时间不发声,如果不设置时间占空比也可以,只不过它会连续不断的播放曲谱,感觉就不好听了,所以这里设置了个时间占空比。

代码执行过程:

  • 首先设定 flag 的默认值为 0,那么它最开始会自动开始循环播放小星星。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第7张图片
  • 其中,最开始会计数一个音符的周期,直至计满不同音符所对应的周期数,它就会归零,重新开始计数。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第8张图片
  • 计满一个周期后,计数周期重复次数的计数器就会 +1,直至计满不同音符所对应的重复次数,它就会归零,重新开始计数。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第9张图片
  • 然后开始该歌曲的下一个音符。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第10张图片
  • 随着 cnt2 的值不断累加,其所代表的音符类别也在不断改变,这里使用一个 case 语句来罗列一整首歌的音符顺序。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第11张图片
  • 当按下按键后,press 脉冲信号传递过来,改变 flag 的值,以及重设 cnt0、cnt1、cnt2 的值归零。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第12张图片
  • 当 flag 的值变为 1’b1,那么 if 条件语句中,就会选择第二个 case ,也就是第二首歌。
  • 其实我这样讲解代码执行过程并不正确,这样讲只是便于理解,应该配置时钟去看语句的执行情况,说明一点,assign 语句是随时钟变化的,也就是说,时钟信号变化一次,它就执行一次,品一品这个意思。

四、管脚配置及结果展示

管脚配置

  • 要按照自己开发板的管脚进行配置。
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第13张图片

结果展示

  • 最后结果是指唱歌发生,所以没办法展示出来,这个代码是我自己手写的,亲测可以成功,按键按下后,也可以切歌。
  • 如果你烧录程序后还是无法发声,那么就是管脚的配置问题,具体查一下你自己开发板的管脚原理图,并配置正确。
  • 当然,如果想实现发一个声,LED 灯闪亮一下,也是很简单可以做到的,如果有兴趣可以做一下。

下一篇博文:【入门学习三】基于 FPGA 使用 Verilog 实现按键状态机代码及原理详解

五、写给小白看的

Verilog 语言如何执行的?????

  • Verilog 语言其实和 C 语言差不多,只不过多一个时钟信号而已
  • 在 FPGA 中,有一个时钟模块会一直产生一个时钟信号,就像下面这样
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第14张图片
  • 信号由电平变为电平时,为上升沿;由电平变电平时,为下降沿

时序逻辑

always @(posedge clk or negedge rst_n) begin
  • 这表示时序逻辑,什么意思呢?
  • posedge 表示检测上升沿
    clk 是文件首部定义的时钟信号接入,也就是说此时的 clk 就是上图所示的时钟信号
    posedge clk 就是检测时钟信号 clk 的上升沿,也就是说每遇到一个时钟信号的上升沿,这个时序逻辑中的代码就执行一遍
  • negedge 表示检测下降沿
    rst_n 是文件首部定义的按键信号接入,也就是说此时的 rst_n 就是按键信号,一个按键在没有按下的情况一直都是高电平,按下后就变为低电平,抬起后又变为高电平
    negedge rst_n 就是检测按键信号 rst_n 的下降沿,也就是说每遇到一个按键信号的下降沿,这个时序逻辑中的代码就执行一遍
  • 是不是很像 C 语言的函数,括号中的内容其实就是参数,只不过传递的参数是时钟模块产生的连续不断的时钟信号以及按键信号
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第15张图片
  • 总结一下它什么时候执行:①每一个周期的时钟信号的上升沿到了,时序逻辑中的代码都会执行一次,或许一秒内,它执行了几千上万次了②按下 rst_n 按键后,它会执行一次

组合逻辑

always @(*)
  • 什么是组合逻辑?看代码来得更快
    【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解_第16张图片
  • 任凭时钟信号再多,这个组合逻辑中的代码都不会执行,除非!!!!cnt2 这个变量的值改变了,always 中的代码才会从头开始执行,比如 cnt2 从 0 变为 1 ,那么组合逻辑就执行一次,从 if 条件开始执行
  • 那个 * 啥意思呢?就是声明我组合逻辑中的所有变量来自组合逻辑块的外面
  • 总结一下它什么时候执行:只有当组合逻辑中的变量的值改变了,组合逻辑才会执行

计数器

  • FPGA 中最基础的便是计数器,计数有啥用呢?怎么计数呢?
  • 计数有啥用?计数的应用领域十分广泛,比如设计一个时钟肯定要计数吧,VGA显示肯定要计数吧(涉及到行同步和列同步),蜂鸣器要计数吧等等等,基本上可以说,计数完全是个基础,灵活的计数可以产生不同的效果
  • 怎么计数呢?很简单啊,那个时序逻辑的特点是什么?就是每一个周期的时钟信号的上升沿有了,它就执行一次,那么完全可以这样做。定义一个变量,在时序逻辑中不断地累加+1,就可以对时钟周期进行计数了,当然不可能永无止境的计数下去,可以设定一个阈值啊,比如我让它计数到 200 就归零,这样不断地计数到 200 个时钟周期,它就归零,200 它就归零,200 它就归零······
  • 这样就可以产生一个条件,什么条件呢?比如说 cnt 是个计数变量,cnt == 200 -1(减 1 是因为它是从 0 开始计数的),这就是一个计数器计满 200 个时钟周期的条件
  • 有这个条件能干啥呢?我可以对 200 计数啊!我可以再定义个计数变量,计数有多少个 200 ,1 个 200,2 个 200······
  • 如此反复套娃,就是计数器的基本使用了,接下来我们就可以通过计数器计满的条件去改变其它自己想改变的值
  • 比如本文中的歌曲,也是套娃,一层一层的套:首先计数一个音符对应频率的时钟周期数(比如音符 1 的时钟周期数是 95600),然后计数一个音符持续时长(也就是 N 个 95600,差不多也就 0.8 秒左右时间),然后再计数一首歌有多少个音符,最后再计数歌曲的种类
  • 是不是很像一层层的套娃,计数器的总结就到这里了,希望各位小白能够理解透彻~

你可能感兴趣的:(FPGA新手入门,verilog,fpga)