【Kevin原创】《基于FPGA的简易计算器设计》第一章:玩转数码管
数码管在我们的整个设计中,我们输入的数据与计算的结果都需要用数码管来显示,所以说数码管还是扮演着很重要的角色的。
1.1简析数码管的工作原理
关于数码管的工作原理,为了照顾零基础的朋友,Kevin还是再进行下简单的介绍,如果是已经了解了数码管工作原理的朋友可以直接跳过这一节。
下面我们先来一张数码管的图片:
数码管是由多个发光二极管封装在一起组成的“8”字型器件,各发光二极管已经在器件内部完成连线。
对于数码管,有共阴极和共阳极之分,共阳极及共阴极数码管内部间的连线可以简化成下图:
图中的“COM”相当于是一个片选信号,例如,共阳极的位选“COM”为高时,即选中了该数码管,此时数码管才能工作起来。当“COM”选通之后,我们就可以选通相应的段选信号使数码管显示我们想要的字符了。比如说,我们想让数码管显示一个“0”(假设数码管是共阳极的),那我们相应的h~a的十六进制代码应为0xc0。
以下是共阴极与共阳极数码管的0~f编码(顺序是为h~a):
共阳:
0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e
共阴:
0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71
1.2让数码管走起来
在这一节中,我们来做这样的一个小练习,Kevin也是想通过这样的一个小练习来让大家基本掌握数码管的工作原理,同时也了解如何使用FPGA来驱动数码管。
先说一下实验要求:数码管从0~9循环计数,变化时间的间隔为1S。
首先,咱们先画一个该设计的简单的逻辑框图:
图中“Div_cnt”模块在1s计时到了之后,就让div_flag这个信号拉高一个时钟周期。“Seg_ctrl”为数码管控制模块,产生位选sel信号及数码管的显示字符信号seg,显然我们的seg信号的变化需要根据div_flag这个信号来控制。
下面可以根据我们的这个逻辑图框图来写代码了。
首先来写div_cnt这个模块:
/******************************************************************** * Module Name: div_cnt * Engineer : Kevin * Function : 1s计数模块 * Blog Website : http://dengkanwen.com * Version : v1.0 ********************************************************************/ module div_cnt( input wire sclk, //系统时钟为50MHz,即周期为20ns input wire s_rst_n, output reg div_flag ); parameter CNT_END = 26'd4999_9999; //1s计时结束 reg [25:0] cnt; //计数器 always @(posedge sclk or negedge s_rst_n) if(s_rst_n == 1'b0) cnt <= 26'd0; else if(cnt == CNT_END) cnt <= 26'd0; else cnt <= cnt + 1'b1; always @(posedge sclk or negedge s_rst_n) if(s_rst_n == 1'b0) div_flag <= 1'b0; else if(cnt == CNT_END) div_flag <= 1'b1; else div_flag <= 1'b0; endmodule
接下来,我们来写数码管的控制模块,在写控制模块之前,我们需要来看一下我们的电路原理图:
咱们先来简单的说一下这个数码管电路。电路图中总共有6位数码管,所以呢就有6个位选控制端,而这6个位选控制端是和三极管连在一块的,看着三极管的连法,应该知道,数码管是属于共阳极的。然而这6个数码管的位选是由3-8译码器来控制的,下面咱们再看一下3-8译码器与数码管位选端的连线图:
连线图已经出来了,可能有些朋友还不太清楚3-8译码器的原理,那咱们再来一张3-8译码器的真值表:
结合电路图及真值表,咱们可以看出,我们可以控制译码器的A、B、C这三个引脚来控制输出端Y电平的高低,从而就可以来控制我们数码管的位选端。例如,我们想让位选信号为S1的那个数码管工作起来,那我们就必须要先使之位选端有效,则需要使Y0的输出为低,所以我们ABC的信号应该为000.
在Kevin讲完之后,相信大家对咱们这个数码管电路的工作原理应该了解的差不多了。
我们只是让一个数码管亮起来,那我们索性也就只让位选信号为s1的那个数码管工作,其余数码管不工作。下面咱们就动手来写数码管控制模块的代码:
/******************************************************************** * Module Name : seg_ctrl * Engineer : Kevin * Function : 数码管显示控制模块 * Blog Website : http://dengkanwen.com * Version : v1.0 *********************************************************************/ module seg_ctrl( input wire sclk, input wire s_rst_n, input wire div_flag, output wire [2:0] sel, //位选控制,连3-8译码器 output reg [7:0] seg //段选信号 ); //========================共阳极数码管编码========================================= //0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e //================================================================================= parameter ZERO = 8'hc0, ONE = 8'hf9, TWO = 8'ha4, THREE = 8'hb0, FOUR = 8'h99, FIVE = 8'h92, SIX = 8'h82, SEVEN = 8'hf8, EIGHT = 8'h80, NINE = 8'h90; reg [3:0] num_cnt; //用于寄存当前数码管应显示的值 //num_cnt always @(posedge sclk or negedge s_rst_n) if(s_rst_n == 1'b0) num_cnt <= 4'd0; else if(num_cnt == 4'd9 && div_flag == 1'b1) num_cnt <= 4'd0; else if(div_flag == 1'b1) num_cnt <= num_cnt + 1'b1; //seg always @* case(num_cnt) 4'd0: seg <= ZERO; 4'd1: seg <= ONE; 4'd2: seg <= TWO; 4'd3: seg <= THREE; 4'd4: seg <= FOUR; 4'd5: seg <= FIVE; 4'd6: seg <= SIX; 4'd7: seg <= SEVEN; 4'd8: seg <= EIGHT; 4'd9: seg <= NINE; endcase //sel assign sel = 3'b000; endmodule
这两个模块写好了之后,下面咱们再写一个顶层模块,用于连接两个模块之间的连线。
/******************************************************************** * Module Name : seg_top * Eneigneer : Kevin * Function : 数码管0~9变换顶层模块 * Blog Website : http://dengkanwen.com * Version : v1.0 *********************************************************************/ module seg_top( input wire sclk, input wire s_rst_n, output wire [2:0] sel, output wire [7:0] seg ); wire div_flag; div_cnt div_cnt_inst( .sclk (sclk), //系统时钟为50MHz,即周期为20ns .s_rst_n (s_rst_n), .div_flag (div_flag) ); seg_ctrl seg_ctrl_inst( .sclk (sclk), .s_rst_n (s_rst_n), .div_flag (div_flag), .sel (sel), //位选控制,连3-8译码器 .seg (seg) //段选信号 ); endmodule
虽然Kevin对于这种简单的设计还是比较有自信的,但是为了教给大家一个良好的设计习惯,咱们还是进行仿真,所以接下来咱们来写一个仿真文件。 当然在仿真的时候,我们可以将div_cnt中的cnt计满的值写小一点。
`timescale 1ns/1ns module tb_seg_top; reg sclk; reg s_rst_n; wire [2:0] sel; wire [7:0] seg; initial begin sclk = 1; s_rst_n <= 0; #20 s_rst_n <= 1; end always #5 sclk = ~sclk; seg_top seg_top_inst( .sclk (sclk), .s_rst_n (s_rst_n), .sel (sel), .seg (seg) ); endmodule
下面是咱们的仿真波形:
根据咱们的波形,可以说暂时是没有发现错误的,当然,Kevin下板子后也是板子也能正常工作。
1.3数码管进阶
数码管能实现0~9之间的跳转之后,下面我们来实现数码管从0~99变化,间隔的时间依然是1S。
在我们进行这个实验的时候,我们必须要清楚,这次的实验,我们需要用到2位数码管。在我们做0~9这个实验的时候,大家应该清楚,对于数码管,我们一次只能选通一个数码管,那我们又怎么能实现是两位数码管同时显示呢?
这里的道理很简单,虽然我们一次只能选通一个数码管的位选,但是由于有视觉停留效果,所以只要我们两位数码管之间的位选信号切换的速度达到了一定程度,超过了我们肉眼能分辨出来的频率,那我们眼睛看到的效果就是两个数码管在同时亮着。既然说到这了,那我们就定义位选信号之间变化的频率为1KHz。
对于这个实验,我们可以依然利用上面已经写好了的代码,只需要修改seg_crtl这个模块,在其中增加些许代码就OK了。那到底需要增加哪部分的代码呢?我们先来分析一下:首先肯定是需要有一个1KHz的“分频”部分,再有就是需要控制好seg,在选通第一位数码管时,我们需要将第一位数码管需要显示的字符赋给seg,选通第二数码管时需将第二位数码管应显示的字符赋给seg。
那接下来就来写代码:
/******************************************************************** * Module Name : seg_ctrl * Engineer : Kevin * Function : 数码管显示控制 * Blog Website : http://dengkanwen.com * Version : Content * v1.0 - initial release * v1.1 - 数码管从0~99显示 *********************************************************************/ module seg_ctrl( input wire sclk, input wire s_rst_n, input wire div_flag, output wire [2:0] sel, //位选控制,连3-8译码器 output wire [7:0] seg //段选信号 ); //========================共阳极数码管编码========================================= //0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e //================================================================================= parameter ZERO = 8'hc0, ONE = 8'hf9, TWO = 8'ha4, THREE = 8'hb0, FOUR = 8'h99, FIVE = 8'h92, SIX = 8'h82, SEVEN = 8'hf8, EIGHT = 8'h80, NINE = 8'h90, CNT_END = 4_9999; //1KHz计满 //CNT_END = 4; //1KHz计满 (用于仿真) reg [15:0] cnt_1k; reg [3:0] num_cnt_g; //用于寄存当前数码管应显示的值 reg [3:0] num_cnt_s; reg [7:0] seg_g; //个位数码管显示 reg [7:0] seg_s; //十位数码管显示 reg flag_1k; //1KHz频率标志 //cnt_1k always @(posedge sclk or negedge s_rst_n) if(s_rst_n == 1'b0) cnt_1k <= 16'd0; else if(cnt_1k == CNT_END) cnt_1k <= 16'd0; else cnt_1k <= cnt_1k + 1'b1; //flag_1k always @(posedge sclk or negedge s_rst_n) if(s_rst_n == 1'b0) flag_1k <= 1'b0; else if(cnt_1k == CNT_END) flag_1k <= ~flag_1k; //num_cnt_g always @(posedge sclk or negedge s_rst_n) if(s_rst_n == 1'b0) num_cnt_g <= 4'd0; else if(num_cnt_g == 4'd9 && div_flag == 1'b1) num_cnt_g <= 4'd0; else if(div_flag == 1'b1) num_cnt_g <= num_cnt_g + 1'b1; //num_cnt_s always @(posedge sclk or negedge s_rst_n) if(s_rst_n == 1'b0) num_cnt_s <= 4'd0; else if(div_flag == 1'b1 && num_cnt_g == 4'd9 && num_cnt_s == 4'd9) num_cnt_s <= 4'd0; else if(div_flag == 1'b1 && num_cnt_g == 4'd9) num_cnt_s <= num_cnt_s + 1'b1; //seg_g always @* case(num_cnt_g) 4'd0: seg_g <= ZERO; 4'd1: seg_g <= ONE; 4'd2: seg_g <= TWO; 4'd3: seg_g <= THREE; 4'd4: seg_g <= FOUR; 4'd5: seg_g <= FIVE; 4'd6: seg_g <= SIX; 4'd7: seg_g <= SEVEN; 4'd8: seg_g <= EIGHT; 4'd9: seg_g <= NINE; endcase //seg_s always @* case(num_cnt_s) 4'd0: seg_s <= ZERO; 4'd1: seg_s <= ONE; 4'd2: seg_s <= TWO; 4'd3: seg_s <= THREE; 4'd4: seg_s <= FOUR; 4'd5: seg_s <= FIVE; 4'd6: seg_s <= SIX; 4'd7: seg_s <= SEVEN; 4'd8: seg_s <= EIGHT; 4'd9: seg_s <= NINE; endcase //sel assign sel = (flag_1k == 1'b1) ? 3'b101 : 3'b100; //seg assign seg = (flag_1k == 1'b1) ? seg_g : seg_s; endmodule
模块代码写好了之后,我们还是需要先进行仿真,这里的话,Kevin就不把仿真的的结果截图了,仿真就留给大家自己完成了。
我们现在来个板子上的效果截图:
至此,咱们的这个实验就完成了。
课后练习
1.让数码管依次显示“I LOVE FPGA”,从左至右循环显示,滚动速度为1s滚动一次。
(备注:如果有发现内容错误,请大家及时指出,Kevin的QQ:1024726016)
Love 梦想
2016年2月21日 下午5:39
赞。
Kevin
2016年2月21日 下午8:29
感谢支持
单枪舞九州
2017年6月29日 上午10:15
支持
OMEGA
2019年3月19日 下午5:14
博主请问课后练习那个ILOVEYOU程序有吗
Kevin
2019年10月15日 下午10:14
这篇文章很久了,代码都已经找不到了呢