该系类博客序言和资源简介可浏览该博客:PREFACE FPGA经典案例序言 快速了解该系列博客的内容与可用 资源。
目录
1 简介
本数字钟实则是数字钟万年历,具备以下功能:
(1)数码管可显示:时分秒,年月日,闹钟时间;
(2)可实现时分秒、年月日、闹钟时间的任意设定;
(3)自动实现大小月与闰年的判断;
2 数字钟计数方案
2.1 计数方案一
计数方案一,如图:
该计数方案,比较符合人类思维对时间的认知,在FPGA逻辑也比较好实现,对年月日时分秒的逻辑都一样好实现,尤其是在月份的大小的判断计数和闰年的判断都好处理;但是存在一个致命的缺点,产生的结果不能直接使用数码管和液晶进行显示,必须要进行运算分解数各个位,才能用于显示,分解各个位就存在一个问题就是要使用除法运算,除法运算将使用大量的逻辑资源,十分不经济。
2.2 计数方案二
计数方案二,如图:
计数方案二,与一相比单个时间单位的个位和十位等拆开进行计数,这种计数方式虽然给年份大小的判断和闰年的判断处理带来了不小的麻烦,但是其结果可直接用于显示,十分方便,带来了更优的资源配置,所以这点牺牲是可以接受的。
3 数字钟的模块设计
如图为数字钟设计框图,本章内容着重讲解FPAG内部各个模块的设计,外围硬件电路由硬件工程师设计完成。
3.1 按键消抖模块
按键消抖通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。(摘自:百度百科)
为什么要进行消抖呢?
在不同的使用场景下,原因不同,总结一句解释消除抖动带来的不利影响;例如:弱电按钮控制强电开关,若不进行消抖,强电开关在闭合时反复抖动,引起高压电拉电弧,容易引发危险。
按键抖动通常存在两种形式,第一种为高电平有效的,如图所示:
按键在没有按下时,处于常低状态;
第二种为低电平有效的,如图所示:
按键在没有按下时,处于常高状态;
如何设计一个模块同时对两种状态都可以进行消抖呢?设置一个计数器数一定长度,当发现按键状态有变化时就开始清零计数,只有当计数器的值大于设定的长度时(这时证明按键状态稳定了),才输出按键的状态,这样就可以达到消抖的目的。
3.1.1 按键消抖模块代码
代码工程位于: clock\module_test\wipe_key_shake\project中,源码如下:
/*=====================================================
*****************************************************
design name :wipe_key_shake
use :按键消抖模块
engineer :比特电子工作室
version :V0.1
change note :
****************************************************
*****************************************************
功能说明:
*****************************************************
*****************************************************
端口信号说明:
in clk :50M时钟输入
in rst_n :复位信号输入
in key_in :按键输入
out key_out :按键输出
*****************************************************
========================================================*/
module wipe_key_shake
#(
parameter WIPE_TIME = 1000_000
//消抖时间参数定义,系统时钟50M,对应的20ms参数
)
(
input clk ,
input rst_n ,
input key_in ,
output reg key_out
);
//==========================================================
//*********************打拍定义******************************
reg key_in_q1;
reg key_in_q2; //消除亚稳态
reg key_in_q3; //判断信号变化
//==========================================================
//==========================================================
//********************按键状态定义****************************
reg key_state;
//==========================================================
//==========================================================
//*********************消抖计数器定义*************************
reg [23:0] wipe_cnt;
wire add_wipe_cnt;
wire end_wipe_cnt;
//==========================================================
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//*********************漂亮的分割线***************************
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//==========================================================
//*********************打拍代码******************************
always @(posedge clk)begin
key_in_q1 <= key_in;
key_in_q2 <= key_in_q1;
key_in_q3 <= key_in_q2;
end
//==========================================================
//==========================================================
//********************按键状态代码****************************
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_state <= 0;
end
else if(key_in_q2 != key_in_q3)begin
key_state <= 1;
end
else if(wipe_cnt == WIPE_TIME - 1)begin
key_state <= 0;
end
end
//==========================================================
//==========================================================
//*********************消抖计数器代码*************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
wipe_cnt <= 0;
end
else if(add_wipe_cnt)begin
if(end_wipe_cnt)
wipe_cnt <= 0;
else
wipe_cnt <= wipe_cnt + 1;
end
end
assign add_wipe_cnt = key_state;
assign end_wipe_cnt = add_wipe_cnt && ( (wipe_cnt == WIPE_TIME - 1) || (key_in_q2 != key_in_q3) );
//计数条件:按键状态变化时开始计数
//清零条件:消抖时间计满 或 按键状态发生变化
//==========================================================
//==========================================================
//*************************输出代码**************************
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_out <= 0;
end
else if(wipe_cnt == WIPE_TIME - 1)begin
key_out <= key_in_q2;
end
end
//只有在消抖时间计满时,才输出键值
//==========================================================
endmodule
3.1.2 按键消抖模块仿真
仿真工程位于Clock\module_test\wipe_key_shake中,仿真文件位于Clock module_test\wipe_key_shake\project\simulation\modelsim\ wipe_key_shak.vt中,文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按照视频中指导可得出仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
仿真文件中模拟输入了一个常高和常低按键按下过程各一个。(注:仿真过程参见视频)
仿真结果如下图:
3.1.3 按键模块
上面的内容进行单个按键的消抖,在数值钟的设计中,一共需要使用4个按键,分别是模式按键、移位按键、数值加按键、显示切换按键;为了集中对按键进行消抖处理,单独设置一个模块,调用4个按键消抖模块,在这个模块中统一对按键进行消抖处理输出,这样可使系统设计的结构更加简明。
3.1.4 按键模块代码
代码工程位于: Clockmodule_test\key_module\project中,源码如下:
/*=====================================================
*****************************************************
design name :key_module
use :按键模块
engineer :比特电子工作室
version :V0.1
change note :
****************************************************
*****************************************************
功能说明:
对输入按键消抖进行统一处理
*****************************************************
*****************************************************
端口信号说明:
in clk :50M时钟输入
in rst_n :复位信号输入
in mode_key :模式按键输入
in move_key :移位按键输入
in add_key :数值加键输入
in switch_key :显示选择按键输入
out filter_mode_key :消抖模式按键输出
out filter_move_key :消抖移位按键输出
out filter_add_key :消抖数值加键输入
out filter_switch_key :消抖显示选择按键输出
*****************************************************
========================================================*/
module key_module
#(
parameter WIPE_TIME = 1000_000
//消抖时间参数定义,系统时钟50M,对应的20ms参数
)
(
input clk ,
input rst_n ,
input mode_key ,
input move_key ,
input add_key ,
input switch_key ,
output filter_mode_key ,
output filter_move_key ,
output filter_add_key ,
output filter_switch_key
);
//filter_mode_key
wipe_key_shake
#(
.WIPE_TIME (WIPE_TIME ) //20ms/50Mhz
)
key_low_1
(
.clk (clk ),
.rst_n (rst_n ),
.key_in (mode_key ),
.key_out (filter_mode_key )
);
//filter_move_key
wipe_key_shake
#(
.WIPE_TIME (WIPE_TIME ) //20ms/50Mhz
)
key_low_2
(
.clk (clk ),
.rst_n (rst_n ),
.key_in (move_key ),
.key_out (filter_move_key )
);
//filter_add_key
wipe_key_shake
#(
.WIPE_TIME (WIPE_TIME ) //20ms/50Mhz
)
key_low_3
(
.clk (clk ),
.rst_n (rst_n ),
.key_in (add_key ),
.key_out (filter_add_key )
);
//filter_mode_key
wipe_key_shake
#(
.WIPE_TIME (WIPE_TIME ) //20ms/50Mhz
)
key_low_4
(
.clk (clk ),
.rst_n (rst_n ),
.key_in (switch_key ),
.key_out (filter_switch_key)
);
endmodule
3.1.5 按键模块仿真
仿真工程位于Clock\module_test\key_module\project中,仿真文件位于Clock \module_test\ key_module \project\simulation\modelsim\ key_module.vt中,文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按照视频中指导可得出仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
仿真文件中模拟输入了4个抖动的按键输入。(注:仿真过程参见视频)
仿真结果如下图:
3.2 模式设置模块
在数字钟逻辑中需要一个控制当前模块的模块,该模块可以控制当前处于什么状态,通过模式值可以轻松控制逻辑流程,实现对应功能,模式分别如下:
- 正常运行模式:数字钟按照正常节拍运行;
- 时间调整模式:调整时分秒;
- 年月日调整模式:调整年月日;
- 闹钟调整模式:调整闹钟时间;
3.2.1 模式设置模块代码
/*=====================================================
*****************************************************
design name :mode_module
use :模式设置模块
engineer :比特电子工作室
version :V0.1
change note :
****************************************************
*****************************************************
功能说明:
根据模式按键,控制电子钟模式
*****************************************************
*****************************************************
端口信号说明:
in clk :50M时钟输入
in rst_n :复位信号输入
in mode_key :模式按键
out mode :模式值 0:正常显示模式 1:调整时间模式 2:调整年月日模式 3:调整闹钟模式
out mode_led1 :正常显示模式指示灯
out mode_led2 :调整时间模式指示灯
out mode_led3 :调整年月日模式指示灯
out mode_led4 :调整闹钟模式指示灯
*****************************************************
========================================================*/
module mode_module
(
input clk ,
input rst_n ,
input mode_key ,
output [1:0] mode ,
output reg mode_led1,
output reg mode_led2,
output reg mode_led3,
output reg mode_led4
);
//========================================================
//********************打拍定义*****************************
reg mode_key_q1;
reg mode_key_q2; //用于判断上升沿
//========================================================
//========================================================
//*********************模式计数器定义***********************
reg [1:0] mode_cnt;
wire add_mode_cnt;
wire end_mode_cnt;
//========================================================
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//*********************漂亮的分割线*************************
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//========================================================
//********************打拍代码*****************************
always @(posedge clk)begin
mode_key_q1 <= mode_key;
mode_key_q2 <= mode_key_q1;
end
//========================================================
//========================================================
//*********************模式计数器代码***********************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
mode_cnt <= 0;
end
else if(add_mode_cnt)begin
if(end_mode_cnt)
mode_cnt <= 0;
else
mode_cnt <= mode_cnt + 1;
end
end
assign add_mode_cnt = ( mode_key_q1 == 1 )&& ( mode_key_q2 == 0 );
assign end_mode_cnt = add_mode_cnt && mode_cnt == 4 - 1;
//计数条件:在按键脉冲的上升沿计数
//结束条件:计满
//========================================================
//========================================================
//*********************模式输出代码*************************
assign mode = mode_cnt;
//========================================================
//========================================================
//*********************模式灯代码**************************
//mode_led1
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
mode_led1 <= 0;
end
else if(mode == 0)begin
mode_led1 <= 1;
end
else begin
mode_led1 <= 0;
end
end
//mode_led2
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
mode_led2 <= 0;
end
else if(mode == 1)begin
mode_led2 <= 1;
end
else begin
mode_led2 <= 0;
end
end
//mode_led3
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
mode_led3 <= 0;
end
else if(mode == 2)begin
mode_led3 <= 1;
end
else begin
mode_led3 <= 0;
end
end
//mode_led4
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
mode_led4 <= 0;
end
else if(mode == 3)begin
mode_led4 <= 1;
end
else begin
mode_led4 <= 0;
end
end
//========================================================
endmodule
3.2.2 模式设计模块仿真
仿真工程位于Clock\module_test\mode_module中,仿真文件位于Clock \module_test\mode_module\project\simulation\modelsim\ mode_module.vt中,文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按照视频中指导可得出仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
仿真结果如下图:
3.3 位置调整模块
位置调整模块用于接入移位按键,产生一个位置值,位置用于确定在各个调整模式中,对应当前的调整位,例如在日期调整模式中,如果位置值为0,表示正在调整的是秒钟的个位,此时按下数值加键,秒钟个位就会进行加1,以此类推。
3.3.1 位置调整模块代码
/*=====================================================
*****************************************************
design name :move_site_module
use :位置调整模块
engineer :比特电子工作室
version :V0.1
change note :
****************************************************
*****************************************************
功能说明:
根据MOVE按键,确定调整时间的位数位置
*****************************************************
*****************************************************
端口信号说明:
in clk :50M时钟输入
in rst_n :复位信号输入
in mode :模式值 0:正常显示模式 1:调整时间模式 2:调整年月日模式 3:调整闹钟模式
in move_key :位置调整按键
out move_site :位置值 0~7代表数码管位置1~8
*****************************************************
========================================================*/
module move_site_module
(
input clk ,
input rst_n ,
input [1:0] mode ,
input move_key ,
output [2:0] move_site
);
//========================================================
//********************打拍定义*****************************
reg move_key_q1;
reg move_key_q2; //用于判断上升沿
reg [1:0] mode_q1;
reg [1:0] mode_q2; //用于判断模式值是否发生变化
//========================================================
//========================================================
//*********************位置计数器定义***********************
reg [2:0] move_site_cnt;
wire add_move_site_cnt;
wire end_move_site_cnt;
//========================================================
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//*********************漂亮的分割线*************************
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//========================================================
//********************打拍代码*****************************
always @(posedge clk)begin
move_key_q1 <= move_key;
move_key_q2 <= move_key_q1;
end
always @(posedge clk)begin
mode_q1 <= mode;
mode_q2 <= mode_q1;
end
//========================================================
//========================================================
//*********************模式计数器代码***********************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
move_site_cnt <= 0;
end
else if(mode_q1 != mode_q2)begin //模式值变化就将位置值清零
move_site_cnt <= 0;
end
else if(add_move_site_cnt)begin
if(end_move_site_cnt)
move_site_cnt <= 0;
else
move_site_cnt <= move_site_cnt + 1;
end
end
assign add_move_site_cnt = ( move_key_q1 == 1 )&& ( move_key_q2 == 0 ) && (mode != 0);
assign end_move_site_cnt = add_move_site_cnt && move_site_cnt == 8 - 1;
//计数条件:在按键脉冲的上升沿计数 且 此时模式不处于正常显示模式
//结束条件:计满8个清零
//========================================================
//========================================================
//********************位置值输出代码************************
assign move_site = move_site_cnt;
//========================================================
endmodule
3.3.2 位置调整模块仿真
仿真工程位于Clock\module_test\ move_site_module中,仿真文件位于Clock \module_test\ move_site_module\project\simulation\modelsim\ move_site_module.vt中,文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按照视频中指导可得出仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
仿真文件中模拟了一个移位按键调整位置值的过程。(注:仿真过程参见视频 )
仿真结果如下图:
3.4 时钟运行模块
时钟运行模块的逻辑就是按照大家所认知的时间逻辑来进行设计的,包含了时分秒、年月日的运行进位逻辑,同时可实现大小月,平润年的识别,同时可在对应调整模式下引入时钟调整模块的值,达到调整时间的功能,实则这个设计功能是一个电子钟万年历的功能,比普通的数字钟功能更为强大。
仿真工程位于Clock\module_test\clock_run中,仿真文件位于Clock \module_test\ clock_run\project\simulation\modelsim\ clock_run.vt中,文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按照视频中指导可得出仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
整个工程模拟了一个数字钟正常运行的过程。(注:仿真过程参见视频)
仿真结果如下图:
3.5 时间调整模块
时间调整模块的逻辑与时钟运行模块的逻辑很相似,不同的地方在于使用了按键去调整值,其中工包含了时分秒、年月日的调整逻辑,同样可实现大小月,平润年的识别,同时可将正常运行模式下时钟运行模块的时间引入,作为调整时间的基准,以提高调整时间的便捷程度。
仿真工程位于Clock\module_test\clock_adjust中,仿真文件位于Clock \module_test\ lock_adjust\project\simulation\modelsim\ lock_adjust.vt中,文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按照视频中指导可得出仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
整个工程模拟了一个使用按键进行时间调整的过程。(注:仿真过程参见视频)
仿真结果如下图:
3.6 闹钟调整模块
闹钟调整模块的逻辑和时间调整模块的逻辑是一样的,只是用于设置闹钟的时间,这里不做过多解释。
仿真工程位于Clock\module_test\clock_adjust中,仿真文件位于Clock \module_test\clock_adjust\project\simulation\modelsim\ clock_adjust.vt中,文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按照视频中指导可得出仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
仿真文件中模拟了一个调整闹钟时间的过程。(注:仿真过程参见视频)
仿真结果如下图:
3.7 闹钟判断模块
闹钟判断模块用于判断闹钟设定时间与当前的时间是否相同,若相同则让喇叭响5秒钟后关闭掉,到达一个闹钟的功能,源码如下:
/*=====================================================
*****************************************************
design name :alarm_judge
use :闹钟判断模块
engineer :比特电子工作室
version :V0.1
change note :
****************************************************
*****************************************************
功能说明:
通过判定闹钟设定时间与实时时间,进行闹铃控制
*****************************************************
*****************************************************
端口信号说明:
in clk :50M时钟输入
in rst_n :复位信号输入
in second_u :实时秒钟个位实时数值
in second_d :实时秒钟十位实时数值
in minute_u :实时分钟个位实时数值
in minute_d :实时分钟十位实时数值
in hour_u :实时小时个位实时数值
in hour_d :实时小时十位实时数值
in alarm_second_u :闹钟秒钟个位实时数值
in alarm_second_d :闹钟秒钟十位实时数值
in alarm_minute_u :闹钟分钟个位实时数值
in alarm_minute_d :闹钟分钟十位实时数值
in alarm_hour_u :闹钟小时个位实时数值
in alarm_hour_d :闹钟小时十位实时数值
out speaker :喇叭控制
*****************************************************
========================================================*/
module alarm_judge
(
input clk ,
input rst_n ,
input [3:0] second_u ,
input [3:0] second_d ,
input [3:0] minute_u ,
input [3:0] minute_d ,
input [3:0] hour_u ,
input [3:0] hour_d ,
input [3:0] alarm_second_u ,
input [3:0] alarm_second_d ,
input [3:0] alarm_minute_u ,
input [3:0] alarm_minute_d ,
input [3:0] alarm_hour_u ,
input [3:0] alarm_hour_d ,
output reg speaker
);
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
speaker <= 0;
end
else if( (alarm_hour_d == hour_d) && (alarm_hour_u == hour_u) && (alarm_minute_d == minute_d) &&
(alarm_minute_u == minute_u) && (alarm_second_d == second_d) && (alarm_second_u == second_u) )begin
speaker <= 1;
end
else if( second_u - alarm_second_u == 5 )begin //简易逻辑(响5秒即可关闭)
speaker <= 0;
end
end
endmodule
3.8 闰年判断模块
闰年判断是万年历中一个不可或缺功能,因为是否是闰年将决定着2月的天数,是时钟调整模块和时钟运行模块的正确执行的重要条件,闰年判定的条件如下:
- 能直接被400整除;
- 能被4整除但是不能被100整除;
满足以上两个条件的任意一个则被判定为闰年。
可见闰年的判断需要关注的点则是除法的余数,所以在这里必须要使用除法运算了,同时考虑FPGA中不能使用过多除法IP核的做法,采用分时复用的方法来执行除法运算流程,得出结果,流程如下:
- 使用运行模式来控制进入闰年判断的年份数值是来自于时钟运行模块,还是时间调整模块,因为这两个模块都需要判断是否是闰年。
- 将分离的年份数值通过算法计算为一个数 ,公式如下:year = year_k * 1000 + year_h * 100 + year_d*10 + year_u;
- 除法第一轮:计算year/400 ,并取出余数;
- 除法第一轮:计算year/4 ,并取出余数;
- 除法第一轮:计算year/100 ,并取出余数;
- 通过对三个余数的判断年份是否为闰年;
仿真工程位于Clock\module_test\ leap_year_judge中,仿真文件位于Clock \module_test\ leap_year_judge\project\simulation\modelsim\ leap_year_judge.vt中,文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按照视频中指导可得出仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
仿真文件中输入了不同的年份,进行了闰年的判断。(注:仿真过程参见视频)
仿真结果如下图:
3.9 数码管数值选择模块
外围电路设计了8个数码管用于显示,但是设计中却有很多数要显示,比如:正常运行的时分秒、年月日,调整模式下的时分秒、年月日,以及闹钟时间;于是需要在不同模式或则不同按键模式下,选择不同的数值输出,提供给数码管驱动,这样在数码管上就可以需要的数值,方案如下:
- 在正常运行模式下:输出正常运行的时分秒值;
- 在正常运行模式下,switch_key被按下:输出正常运行时的年月日值;
- 在时间调整模式下:输出被调整的时分秒值,并根据位置值对相应位进行秒闪烁,提示目前调整的是该位的值;
- 在年月日调整模式下:输出被调整的年月日值,并根据位置值对相应位进行秒闪烁,提示目前调整的是该位的值;
- 在闹钟调整模式下:输出被调整的闹钟时分秒值,并根据位置值对相应位进行秒闪烁,提示目前调整的是该位的值;
仿真工程位于Clock\module_test\ tube_num_selecte中,仿真文件位于Clock \module_test\ tube_num_select\project\simulation\modelsim\ tube_num_select.vt中,文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按照视频中指导可得出仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
仿真文件中实现了不同模式和不同按键值,仿真模块是否能根据不同的模式去完成数值输出的选择。(注:仿真过程参见视频)
仿真结果如下图:
3.10 数码管译码与动态扫描模块
这里指的数码管就是我们最常见的七段显示数码管,如图:
可以看出它是由7段小灯组成的,通过点亮小灯的组合不同可以组成不同的字符。管脚图如下:
每个管脚控制着一段灯管,数码管还分为共阴和共阳两种,共阴是说每段小灯的负极是连接在一起的(一个共地管脚),每段小灯的正级是分开的(对应着每个管脚),共阳数码管的则刚好相反,本文就以共阴的数码管为例来讲解。
数码管显示0~9这10个数字,按照管脚状态的组合,我们可以得到如下的数字管脚组合表,称它为译码表(小数点忽略,该设计不涉及小数点的使用):
图中用‘1’表示该管脚状态为高,点亮该管脚对应的段码;‘0’则相反;
数字 管脚 | a | b | c | d | e | f | g |
0 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 |
2 | 1 | 1 | 0 | 1 | 1 | 0 | 1 |
3 | 1 | 1 | 1 | 1 | 0 | 0 | 1 |
4 | 0 | 1 | 1 | 0 | 0 | 1 | 1 |
5 | 1 | 0 | 1 | 1 | 0 | 1 | 1 |
6 | 1 | 0 | 1 | 1 | 1 | 1 | 1 |
7 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
8 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
9 | 1 | 1 | 1 | 1 | 0 | 1 | 1 |
了解数码管的基本原理以后,还需要解决一个问题,频率计中一共使用了8个数码管,如果每个数码管都需要单独驱动的话,那么就需要56个管脚,管脚使用太多的话不论是对FPGA IO资源占用还是PCB布线都是不合理的,所以需要使用数码管的动态扫描来解决这个问题。
动态扫描的原理(百度百科):数码管动态显示接口是嵌入式系统中应用最为广泛的一种显示方式之一,动态驱动是将所有数码管的8个显示笔划"a,b,c,d,e,f,g,dp"的同名端连在一起,另外为每个数码管的公共极COM增加位选通控制电路,位选通由各自独立的I/O线控制,当单片机输出字形码时,所有数码管都接收到相同的字形码,但究竟是哪个数码管会显示出字形,取决于单片机对位选通COM端电路的控制,所以我们只要将需要显示的数码管的选通控制打开,该位就显示出字形,没有选通的数码管就不会亮。通过分时轮流控制各个数码管的的COM端,就使各个数码管轮流受控显示,这就是动态驱动。在轮流显示过程中,每位数码管的点亮时间为1~2ms,由于人的视觉暂留现象及发光二极管的余辉效应,尽管实际上各位数码管并非同时点亮,但只要扫描的速度足够快,给人的印象就是一组稳定的显示数据,不会有闪烁感,动态显示的效果和静态显示是一样的,能够节省大量的I/O端口,而且功耗更低。
3.10.1 数码管译码与动态扫描模块代码
/*=====================================================
*****************************************************
design name :nixie_tube
use :数码管译码与动态扫描模块
engineer :比特电子工作室
version :V0.2
change note :1,增加全灭显示; 2,增加“-”显示
****************************************************
*****************************************************
功能说明:
按照 3.3 数码管译码与动态扫描模块 的原理完成代码设计
*****************************************************
*****************************************************
端口信号说明:
in clk :50M时钟输入
in rst_n :复位信号输入
in num8 :分解数字的第8位
in num7 :分解数字的第7位
in num6 :分解数字的第6位
in num5 :分解数字的第5位
in num4 :分解数字的第4位
in num3 :分解数字的第3位
in num2 :分解数字的第2位
in num1 :分解数字的第1位
out seg_en :数码管使能端
out seg_data :数码管数据端
*****************************************************
========================================================*/
module nixie_tube
#(
parameter TIME_2_5MS = 250000 //2.5ms计时
)
(
input clk ,
input rst_n ,
input [3:0] num8 ,
input [3:0] num7 ,
input [3:0] num6 ,
input [3:0] num5 ,
input [3:0] num4 ,
input [3:0] num3 ,
input [3:0] num2 ,
input [3:0] num1 ,
output reg [7:0] seg_en ,
output reg [6:0] seg_data //从高到低位abcdefg
);
//=========================================
//*************动态扫描计数器定义*************
reg [19:0] single_cnt; //单个数码管点亮时间计数器
wire add_single_cnt;
wire end_single_cnt;
parameter SEG_NUM = 8; //扫描数码管的个数
reg [3:0] seg_cnt; //数码管扫描个数计数器
wire add_seg_cnt;
wire end_seg_cnt;
//=========================================
//=========================================
//*************中间变量定义******************
reg [3:0] num;
//=========================================
//=========================================
//*************动态扫描计数器代码*************
/******************************************
人眼分辨频率大约为30Hz,为提高观感,将数码管动态
扫描频率提高至50Hz,意味着一轮数码管的扫描时间为
20ms,总共8个数码管需要点亮,那么在一轮扫描中,单个
数码管点亮的时间为2.5ms
*******************************************/
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
single_cnt <= 0;
end
else if(add_single_cnt)begin
if(end_single_cnt)
single_cnt <= 0;
else
single_cnt <= single_cnt + 1;
end
end
assign add_single_cnt = 1;
assign end_single_cnt = add_single_cnt && single_cnt == TIME_2_5MS - 1;
//点亮时间计数器计数条件: 始终计数
//点亮计数器计数器结束条件:计满2.5ms就停止计数
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
seg_cnt <= 0;
end
else if(add_seg_cnt)begin
if(end_seg_cnt)
seg_cnt <= 0;
else
seg_cnt <= seg_cnt + 1;
end
end
assign add_seg_cnt = end_single_cnt;
assign end_seg_cnt = add_seg_cnt && seg_cnt == SEG_NUM - 1;
//数码管扫描个数计数器:一个2.5ms计数完成
//数码管扫描个数计数器:所有数码管扫描完毕
//=========================================
//=========================================
//*************中间变量代码******************
always @(*)begin
case(seg_cnt)
0: num = num8;
1: num = num7;
2: num = num6;
3: num = num5;
4: num = num4;
5: num = num3;
6: num = num2;
7: num = num1;
default: num = 4'hf;
endcase
end
//=========================================
//=========================================
//****************输出代码******************
//seg_data
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
seg_data <= 7'b0000000;
end
else begin
case(num)
4'h0 : seg_data <= 7'b1111110;
4'h1 : seg_data <= 7'b0110000;
4'h2 : seg_data <= 7'b1101101;
4'h3 : seg_data <= 7'b1111001;
4'h4 : seg_data <= 7'b0110011;
4'h5 : seg_data <= 7'b1011011;
4'h6 : seg_data <= 7'b1011111;
4'h7 : seg_data <= 7'b1110000;
4'h8 : seg_data <= 7'b1111111;
4'h9 : seg_data <= 7'b1111011;
4'he : seg_data <= 7'b0000001; //"-"
4'hf : seg_data <= 7'b0000000; //全灭
default: seg_data <= 7'b0000000;
endcase
end
end
//seg_en
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
seg_en <= 8'b1111_1111;
end
else begin
case(seg_cnt)
4'h0 : seg_en <= 8'b0111_1111; //点亮第8个数码管
4'h1 : seg_en <= 8'b1011_1111; //点亮第7个数码管
4'h2 : seg_en <= 8'b1101_1111; //点亮第6个数码管
4'h3 : seg_en <= 8'b1110_1111; //点亮第5个数码管
4'h4 : seg_en <= 8'b1111_0111; //点亮第4个数码管
4'h5 : seg_en <= 8'b1111_1011; //点亮第3个数码管
4'h6 : seg_en <= 8'b1111_1101; //点亮第2个数码管
4'h7 : seg_en <= 8'b1111_1110; //点亮第1个数码管
default: seg_en <= 8'b1111_1111;
endcase
end
end
//=========================================
endmodule
3.10.2 数码管译码与动态扫描模块仿真
仿真工程位于clock\module_test\ nixie_tube \project中,仿真文件位于clock\\module_test\ nixie_tube \project\simulation\modelsim\ nixie_tube.vt文件可以使用quartuse II打开查看。
此处使用了quartuse ii调用modelsim自动仿真,工程已经搭建好,按以下步骤即可获得仿真结果。(注:工程已经搭建好,若想学习自动仿真工程的配置,请学习quartuse ii的安装与调用modelsim的自动仿真的文档)
仿真文件中在数码管译码与动态扫描的数字输入端口num8~num1上分别输入了数字1、2、3、4、5、6、7、8,查看功能是否正确。(注:仿真过程参见视频)
仿真结果如下图:
结论:一共出现8端波形,从左至右分别是:第8位数码管显示1、第7位数码管显示2、第6位数码管显示3、第5位数码管显示4、第4位数码管显示5、第3位数码管显示6、第2位数码管显示7、第1位数码管显示8,证明显示结果正确。
4 数字钟的系统设计
上一章已经完成了数字钟的各个模块的仿真验证工作,这一章将上一章节的模块有机的结合起来就构成了一个完整的数字钟了,这里要完成的工作即是编写数字钟的顶层设计代码,将各个模块连接起来。
用quartuse ii对系统工程进行综合以后,点击RTL_VIWER系统可以自动生成设计的系统框图(RTL视图),也就是大家误以为的连线图和原理图,如图:
5 硬件设计(第四讲)
前面我们的硬件描述语言已经把整个设计的做完,功能仿真也进行完毕了。但是我们的硬件设计的核心器件还没有定下来。采用器件(EP4CE6E22C8)对工程进行综合编译,可以看到资源占用报告,如图:
可以看出设计占用了该器件22%的资源,这样我们还有78%的资源余量,那这样看来该器件是可以完成该设计的。
下面我们以FPGA为例选用EP4CE6E22C8来完成该设计,下面分模块对硬件原理图进行讲解。
5.1 电源接口电路设计
电源接口电路采用了两种供电的方式,满足不同的情况下使用。
(1)Micro Usb供电,取电方便,用智能手机的充电线或者充电宝均可取电;(2)排针供电,但是必须要实验室直流电源通过排线接入5V电源。
电路中使用D1用来防反接,防止电源接反;R1使用的是自恢复保险丝,当后面电路意外短路的时,电阻急剧增大,形成高阻状态,防止烧坏后面电路的器件,当短路情况消失后,电阻减少,形成低阻状态,从而不需要人工干预,有效地保护电路。LED作为一个电源指示灯,当电源接好并且K1开关打开时,电源指示灯常亮。
5.2 电源系统设计
FPGA芯片需要用到3种电源供电,所以电路中设计3种电源转换电路。
(1)5V电源转3.3V,E5、E6用来滤波储能,C3、C6用来滤出高频电源噪声。
(2)5V电源转2.5V,外部加的滤波电容功能同上。
(3)3V电源转1.2V,外部的电容功能同上。
5.3 时钟设计
全局主时钟采用50M有源晶振设计,频率精度高、稳定性好,C7、C8用来滤除有源晶振的电源噪声。
5.4 JTAG端口和FPGA配置电路设计
程序的下载,采用通用的JTAG方式下载程序,由于FPGA掉电易失,所以需要挂一个存储器,电路中选用的是EPCS16S18N,设计PCB板的时候需要注意的地方,就是有些脚需要上拉电阻。
5.5 FPGA芯片的电源引脚
FPGA的电源引脚分布情况,VCC3_3,VCC1_2,以及VCC2_5。
5.6 数码管电流扩展电路
此次设计的数码管电路采用动态扫描的方式来显示。由于电路中有8个数码管,如果不采用动态扫描电路,那么将会用到60多个IO口,不利用PCB布线且也浪费了FPGA芯片的IO口。由于闪烁的频率在大于40HZ后,人眼就无法识别闪烁了,这样,在程序中设置每个数码管的点亮时间大于40HZ,就会实现眼睛被欺骗的结果,数码管常亮。在使用数码管动态扫描电路的时候,一定要注意驱动电流的选择,如果不增加电流扩展电路,那么最终的结果就是数码管的显示很暗又或者显示不稳定,电路中每个公共信号都采用一个NPN三极管来扩展电流,增加驱动能力。
5.8 数码管电路
电路里面的使用了8个上图所示的数码管,采用共阴极的接法,程序中按照8个数码管从高到地位分时选择seg_en_x的管脚,点亮选择的数码管,由于分时选择的速度很快,远远大于人眼能够识别的频率,从而看上去数码管都是亮的.
5.9 复位电路
复位电路,用于程序的复位,本电路中使用的是低电平复位。
5.10 指示灯显示电路
电路设计上留了4个不同颜色的灯,均通过FPGA芯片的IO口直接供电,高电平,点亮对应的灯,这4种灯具有以下的功能:LED3(闹钟设置模式指示灯)、LED4(日期设置模式指示灯)、LED5(时间调整模式指示灯)、LED6(正常显示模式下指示灯)。
5.11 蜂鸣器电路
蜂鸣器用来配合着闹钟使用,当定时的时间到了设定的值后,蜂鸣器会鸣叫,提示闹钟设置时间到。电路中用的蜂鸣器是有源蜂鸣器,只需通电,便会鸣叫,方便易于控制,高电平导通,蜂鸣器鸣叫。
5.12 按键控制电路
根据程序的功能,设计了4个按键,分别是数码管显示切换按键,调整增加按键,移位按键以及模式调整按键;高电平有效。
5.13 PCB设计图
5.14 实物图
6 实物验证
在这里就已经到达我们设计的最后一个阶段了,我们需要把整个设计在我们的实物上进行验证。
6.1 管脚分配
Step1:打开clock\formal_code\clock中的工程
(注:formal_code正式程序中的在系统仿真的工程中加入了锁相环,以后可以方便的通过锁相环将设计应用于任何开发板)。
Step2:选择Pin Planner
Step3:根据《管脚分配手册》,对管脚进行分配
分配完成以后保存并从新编译工程。(注:在正式程序的工程中,管脚已经分配好,所以使用时无需再进行分配,这里只是带大家学习这一过程。)
6.2 实物验证
最后一步任务就是把程序下载进入我们设计的开发板中,然后就可以验证功能是否正确了。
Step1:将实物行如下连接:
Step2: 打开clock\formal_code\clock中的工程
Step3:打开programmer
Step4:设置硬件链接
Step4:点击start等待下载完成,下载完成后重新上电
运行结果:(工程中的下载配置文件已固化好,下载即可,若想学习如何进行下载文件的生成,请自行搜索相关方法)
资料合集包传送门:
至此,基于FPGA的交通灯的全部设计流程已经讲解完毕,博客中讲解了所有的设计原理和设计重点,同时介于篇幅的限制,部分源码没办法全部给出,需要进一步学习的可下载全套资料合集包,本案例资料合集包较大所以分卷压缩成了两卷,请下载两卷后放在同一文件夹下,这样才能解压成功:
第一卷传送门: Clock_Verilog.part1.rar
第二卷传送门:Clock_Verilog.part2.rar
资料包含内容如下:
文件夹名 | 功能描述 | 备注 |
bom | 元器件清单 | |
module_test | 模块级的仿真工程(可能采用不同的仿真形式和仿真工具,具体以对应的博客为准) | 需要安装Quartuse II 13.1 |
system_test | 系统级的仿真工程(可能采用不同的仿真形式和仿真工具,具体以对应的文档为准) | 需要安装Quartuse II 13.1 |
formal_code | 程序工程的实物运行版本版本,用于下载到板卡上运行的(与仿真版本只有参数上的差异,并且生成了配置文件) | 需要安装Quartuse II 13.1 |
sch&pcb | 原理图和PCB设计图 | 打开文档需要安装Adobe Acrobat |
video | 产品设计讲解课程,与document的文件使用方法相同,也需要用你的机器码来获取播放密码。并且需要“比特电子视频专用播放器”打开,否则会报文件损坏错误。 | 使用“比特电子视频专用播放器” 去打开视频 |
软件提取网址 | 工作室提供了文件使用所需要的软件下载地址和对应的软件安装使用教程提取网址 | |