定时器设计与层次化设计(驱动蜂鸣器)

一、定时器设计

1.定时器简介

定时器即计数器,可以循环定时,也可以单次定时。

2.定时器代码

module timer(

	Clk,		//时钟信号
	Rst_n,		//复位信号
	
	CNT_ARR,	//定时预设值
	MODE,		//该信号决定定时模式:1为循环定时;0为单次定时
	Cnt_Go,		/*	循环定时模式:高电平使能计时,低电平停止计时。
					单次计数模式:该信号的一个单基准时钟周期的脉冲使能一次定时。*/
	
	CNT_NOW,	//当前定时值
	Full_Flag	//定时满信号
);
	
	input Clk;
	input Rst_n;
	
	input [31:0]CNT_ARR;
	input MODE;
	input Cnt_Go;
	
	output [31:0] CNT_NOW;
	output reg Full_Flag;
	
	reg [31:0] cnt;		//定义一个32位的寄存器用作计数器
	reg oneshot;		//单次定时的内部使能信号
	wire Full_Flag_r;
	
	assign CNT_NOW = cnt;	//输出当前定时值
	assign Full_Flag_r = (cnt == CNT_ARR - 1)?1'b1:1'b0;
	
	always@(posedge Clk)
		Full_Flag <= Full_Flag_r;	//这里设置了一个Full_Flag_r,用于将Full_Flag信号延后一个周期,使其与记满时的值对齐
	
	always@(posedge Clk or negedge Rst_n)
	if(!Rst_n)
		cnt <= 0;
	else if(MODE == 1'b1)begin	//循环定时模式
		if((Cnt_Go == 1'b1) && (cnt < CNT_ARR))	//当计数使能且当前计数值小于预设值时,计数器+1
			cnt <= cnt + 1'b1;
		else
			cnt <= 0;
	end
	else if(!MODE)begin		//单次定时模式
		if(oneshot)
			cnt <= cnt + 1'b1;
		else
			cnt <= 0;
	end
	
//下面这个always块是针对单次计时设计的
	always@(posedge Clk or negedge Rst_n)
	if(!Rst_n)
		oneshot <= 1'b0;
	else if(!MODE)begin
		if(Cnt_Go == 1'b1)
			oneshot <= 1'b1;//允许计数时,将oneshot置1
		else if(Full_Flag_r)
			oneshot <= 1'b0;//如果计满,则将oneshot信号置0,结束计数
		else
			oneshot <= oneshot;
	end
	else
		oneshot <= 1'b0;
		
endmodule

3.定时器解读

①功能概述

输入端口:时钟端口,异步清零端口,预设值端口,计数使能端口,计数模式端口
输出端口:当前计数值端口,计满标志端口

②关于信号“Full_Flag_r”

如果将信号“Full_Flag_r”直接接到输出端口,经过仿真会发现,Full_Flag_r会比计满时刻的时钟上升沿提前一个时钟周期拉高,因此需要在此信号后连接一个D触发器,使计满信号的跳变沿延后一个周期到来,如下:

output reg Full_Flag;
……
always@(posedge Clk)
		Full_Flag <= Full_Flag_r;

即定义一个寄存器类型的变量,将其连到需要延迟的信号后即可。

③关于信号“oneshot”

“oneshot”是模块内部标志定时一次的信号,当输入信号MODE为0时,oneshot置1,两者具体逻辑关系如下:

always@(posedge Clk or negedge Rst_n)
	if(!Rst_n)
		oneshot <= 1'b0;
	else if(!MODE)begin
		if(Cnt_Go == 1'b1)
			oneshot <= 1'b1;	//允许计数时,将oneshot置1
		else if(Full_Flag_r)
			oneshot <= 1'b0;	//如果计满,则将oneshot信号置0,结束计数
		else
			oneshot <= oneshot;	//这条可以省略,为了直观上逻辑清楚的话可以写出来
	end
	else
		oneshot <= 1'b0;		//MODE为1时为循环计时,oneshot置0

二、层次化设计

我们本次目的是利用计数器模块来驱动开发板上的蜂鸣器,我们可以将刚才编写的timer模块重新命名为beep_test。但是仅有一个计数器模块是不够的,这是因为我们要控制蜂鸣器发出不同频率的声音,就要将各音调的数据事先存储在某个文件中,这样就又有一个需要使用的模块,于是需要一个顶层模块来调用它们,我们将顶层模块命名为beep_top。而将存储音调数据的模块命名为sound_lut(即查找表)

1.流程图

定时器设计与层次化设计(驱动蜂鸣器)

2.声音频率数据表(sound_lut)

调整定时设定参数输入端口,即调整预重装值,预重装值与对应频率的关系为:counter_arr=fclk/fpwm-1。

下图为各声音频率的对照表:
定时器设计与层次化设计(驱动蜂鸣器)
根据这个表,我们可以写出查找表模块(时钟频率为50MHz

module sound_lut(
	Clk,	//查找数据的频率
	Rst_n,	//复位信号
	ARR		//输出频率信息
);

	input Clk;
	input Rst_n;
	output reg[31:0]ARR;	//所谓的频率信息实际上就是计数器的模
	reg [4:0]index;			//index可以理解为每个音调数据的地址
	
	//always块中,循环逐个取出音调信息
	always@(posedge Clk or negedge Rst_n)
	if(!Rst_n)
		index <= 0;
	else if(index >= 5'd20)
		index <= 0;
	else
		index <= index + 1'b1;
		
	always@(*)begin
		case(index)
			0 : ARR = 191130;
			1 : ARR = 170241;
			2 : ARR = 151689;
			3 : ARR = 143183;
			4 : ARR = 127550;
			5 : ARR = 113635;
			6 : ARR = 101234;
			7 : ARR = 95546 ;
			8 : ARR = 85134 ;
			9 : ARR = 75837 ;
			10: ARR = 71581 ;
			11: ARR = 63775 ;
			12: ARR = 56817 ;
			13: ARR = 50617 ;
			14: ARR = 47823 ;
			15: ARR = 42563 ;
			16: ARR = 37921 ;
			17: ARR = 35793 ;
			18: ARR = 31887 ;
			19: ARR = 28408 ;
			20: ARR = 25309 ;
			default: ARR = 191130;//一定要有default语句,否则在复杂工程中可能带来毁灭性灾难
		endcase			
	end

endmodule

查找表模块比较简单,不再赘述。

3.顶层模块(beep_top)

顶层模块负责调用所有需要用到的模块,如下:

module beep_top(

	Clk,
	Rst_n,
	
	Cnt_Go,//循环定时模式:高电平使能计时,低电平停止计时。单次计数模式:该信号的一个单基准时钟周期的脉冲使能一次定时。
	beep
);
	
	input	Clk;	//接开发板上的时钟
	input  	Rst_n;	//接开发板上的复位按键

	input	Cnt_Go;	//接开发板上的计数允许信号接控制按键
	output 	beep;	//输出端beep接开发板上的蜂鸣器
	
	wire [31:0] CNT_NOW;
	wire		Full_Flag1;
	wire [31:0] CNT_ARR;

	//第一个定时器用来驱动蜂鸣器
	beep_test beep_test0(

		.Clk(Clk),
		.Rst_n(Rst_n),
		
		.CNT_ARR(CNT_ARR),	//CNT_ARR信号来自查找表模块
		.MODE(1'b1),		//设置为循环计数模式
		.Cnt_Go(Cnt_Go),
		
		.CNT_NOW(CNT_NOW),	//
		.Full_Flag()	
	);
	
	sound_lut sound_lut(
		.Clk(Full_Flag1),	//此处不推荐这种异步时序逻辑的写法
		.Rst_n(Rst_n),
		.ARR(CNT_ARR)		//输出音调数据,即计数周期
	);

	//250ms定时,用于切换音调
	beep_test beep_test1(

		.Clk(Clk),
		.Rst_n(Rst_n),
		
		.CNT_ARR(12500000),	//设定初值为12500000,由于时钟周期为1/50M=20ns,故定时为250ms
		.MODE(1'b1),		//循环计时
		.Cnt_Go(1),			//计数使能
		
		.CNT_NOW(),			//输出端悬空
		.Full_Flag(Full_Flag1)	//记满信号连到查找表的时钟端
	);
	
	assign beep = (CNT_NOW >= 24999)?1'b1:1'b0;

endmodule

①always@(*)用法

查找表模块中我们用到了“always@(*)”语句。always@(*) 描述组合逻辑,括号中直接填入敏感信号。在always语句中,如果我们对敏感信号不清楚,或者敏感信号不重要,直接填入*,这样块内语句会对所有涉及到的信号敏感。(always块中无论描述的是组合逻辑还是时序逻辑,均只能对reg型变量进行赋值,否则会报错)

②不推荐.Clk(Full_Flag1)写法

在查找表的时钟端口,我们直接连上了第二个定时器的计满信号端。这样的操作属于异步时序逻辑,在小工程(比如本次的蜂鸣器实验)中使用并无大碍,但在今后的学习中应尽量避免异步时序逻辑的出现。

③beep_test0与beep_test1

在顶层模块中我们例化了两个定时器,分别命名为beep_test0和beep_test1,这是本次实验的一个亮点。beep_test0负责输出PWM波,用于驱动蜂鸣器,而beep_test1负责控制音调切换的时间间隔。打个比方,战场上两名战士操控重机枪,一个负责开火,一个负责装子弹。beep_test0就是我们负责开火的战士,beep_test1则我们负责装子弹的战士,而sound_lut则可以看作子弹箱,为机枪源源不断地提供子弹。

三、总结

本次实验标志着正式开始系统学习FPGA,几个重要的知识点都很典型,当然本次工程也留下了一些不足的地方,后面的会不断深入学习,掌握合理完整的开发方法。

上一篇:VMware-workstation-full-16安装


下一篇:JVM有几种GC,什么情况下会触发?