一、理论概念
1、数码管的分类
- 数码管是一种半导体发光器件,其基本单元是发光二极管,常见的数码管有七段数码管(八段数码管少一个小数点位置)、八段数码管和其他类型数码管(下图中的“米”字管)如下图所示:
- 本次实验使用的是八段数码管,八段数码管如下所示,是一个“8”型数码管,分为八段,对应abcdefg和小数点dp,每一段就是一个发光二极管,这样的八段我们称为段选,一位八段数码管常用十个管脚,没有一段对应一个管脚,另外两个管脚对应的是公共端(com端,内部导通)如下所示:
- 八段数码管还分为共阴极数码管和共阳极数码管。对于共阴极数码管来说,八个发光二极管的阴极在数码管内部全部连接在一起,所以称为“共阴”,而阳极是独立的。而对于共阳极数码管来说,它的八个发光二极管的阳极在数码管内部是连接在一起的,而阴极是独立的,所以称为“共阳”
2、显示原理
- 我们以共阳极数码管为例,下面这个表格是共阳极数码管对应的编码形式,我们的数码管可以显示数字0-9,字母A-F共16种字符,中间是对应的二级制段码,最后是对应的十六进制段码。例如,我们想要显示数字0,我们使用的是共阳极数码管,其阳极都是连在一块的,只有在每段对应的端口输入低电平,才能点亮对应的二极管,我们从数字0对应的二级制格式可以看出,abcdef这六段都是低电平,所以是点亮,所以在八字形的图中,周围六段就是点亮,g和dp是熄灭状态,所以就在数码管上显示了一个数字0.以此类推,同理,各种数字和字母都可以用这个方式将十六进制的段码写出来。(注意,当我们将二级制转换成十六进制的时候,最高位是dp最低位是a与下图中二级制段码的顺序正好相反)
- 我们今天实验使用的是四位八段数码管,我们在使用多位数码管的时候,要尽量减少数码管使用的IO口,我们将段选连接在一起后引出来,而位选信号独立控制,这就是位选信号,这样我们就可以通过位选信号控制哪几个数码管点亮,而在同一时刻位选选通的数码管显示的数字都是一样的,因为他们的段选是引脚是连在一起的。现在把这个公共端连接到了FPGA 的 I/O 脚上,这便是数码管的片选信号。如果 FPGA 的这个 I/O 脚输出低电平0,那么这个数码管就能够显示数字;如果这个 I/O 输出高电平 1,那么无论数码管的 8 个段选端输出 0 还是 1,都无法将 8个发光二极管的任意一个点亮,这也达到了关闭数码管显示的效果。这样一来,这个数码管的公共端被我们当做了数码管片选引脚使用了。
二、功能概述
- 我们这次实验的功能是让 4 个数码管每隔 1s 不断的递增计数显示,计数范围为 0-F。为了便于代码编写控制 7个用于段选(不包括小数点)的发光二极管显示不同的字符,这里只做了一个简单的对应表,把不同字符显示时的 7 个 I/O值进行编码,数码管显示字符与驱动编码映射表(共阴)如下图所示:
- 本实例的功能框图如图下图所示。PLL 产生的 25MHz 时钟,分别供给两个子模块,秒计数器模块产生一个每秒递增的 16 位数据,这 16 位数据以 16 进制形式通过数码管显示驱动模块显示到数码管上。数码管显示驱动模块以分时复用的片选方式,将数据送到数码管的各个段选位上。
三、代码解析
- 从上次我们学习了FPGA的模块化设计之后,我们在之后的学习中尽量使用这个技巧,尽可能快的学习并掌握,首先第一个模块顶层模块
1、cy4.v模块代码解析
- 顶层模块的作用就是定义接口信号以及对各个子模块进行互连。我们设计在pll_controller.v 模块中进行例化 PLL IP 核,产生 FPGA 内部其它逻辑工作所需的时钟信号 clk_25m 和复位信号 sys_rst_n;在counter.v 模块进行 1 秒的定时计数,产生数码管显示所需的每秒递增的 4 位 16 进制数据display_num;在seg7.v 模块中对需要显示到数码管的数据 display_num 进行译码,并驱动数码管显示。有了基本的逻辑之后,我们可以先看一下下图中的数码管模块互联接口,形象的将各个模块以及各个模块之间的关系表现出来了。
- 可以根据上图,依次将我们想要的信号、端口等编写到顶层模块中例程如下:
module cy4(
input ext_clk_25m, //外部输入 25MHz 时钟信号
input ext_rst_n, //外部输入复位信号,低电平有效
output[3:0] dtube_cs_n, //7 段数码管位选信号
output[7:0] dtube_data //7 段数码管段选信号(包括小数点为 8 段)
);
//-------------------------------------
//PLL 例化
wire clk_12m5; //PLL 输出 12.5MHz 时钟
wire clk_25m; //PLL 输出 25MHz 时钟
wire clk_50m; //PLL 输出 50MHz 时钟
wire clk_100m; //PLL 输出 100MHz 时钟
wire sys_rst_n; //PLL 输出的 locked 信号,作为 FPGA 内部的复位信号,低电平复位,高
电平正常工作
pll_controller pll_controller_inst (
.areset ( !ext_rst_n ),
.inclk0 ( ext_clk_25m ),
.c0 ( clk_12m5 ),
.c1 ( clk_25m ),
.c2 ( clk_50m ),
.c3 ( clk_100m ),
.locked ( sys_rst_n )
);
//-------------------------------------
//25MHz 时钟进行分频,产生每秒递增的 16 位数据
wire[15:0] display_num; //数码管显示数据,[15:12]--数码管千位,[11:8]--数码管百
位,[7:4]--数码管十位,[3:0]--数码管个位
counter uut_counter(
.clk(clk_25m), //时钟信号
.rst_n(sys_rst_n), //复位信号,低电平有效
.display_num(display_num) //LED 指示灯接口
);
//-------------------------------------
//4 位数码管显示驱动
seg7 uut_seg7(
.clk(clk_25m), //时钟信号
.rst_n(sys_rst_n), //复位信号,低电平有效
.display_num(display_num), //LED 指示灯接口
.dtube_cs_n(dtube_cs_n),//7 段数码管位选信号
.dtube_data(dtube_data) //7 段数码管段选信号(包括小数点为 8 段)
);
endmodule
2、counter.v模块代码解析
- counter.v 模块接口定义如下,输入时钟信号 clk、复位信号 rst_n,输出递增的数码管显示数据寄存器 display_num。
module counter(
input clk, //时钟信号,25MHz
input rst_n, //复位信号,低电平有效
output reg[15:0] display_num //数码管显示数据,[15:12]--数码管千位,[11:8]--数码管百位,[7:4]--数码管十位,[3:0]--数码管个位
);
- counter.v模块逻辑分为两个部分,为了解析方便,可以看清楚以及更好的理解,就一个方面一个方面的写,但是这两个方面都属于counter.v模块。
- 第一部分如下,计数器 timer_cnt 不停递增,以 1 秒为周期循环计数,每个周期(即 1 秒)产生定时信号 timer_1s_flag,该信号每秒只有一个时钟周期为高电平。例程如下:
//-------------------------------------------------
//1s 定时产生逻辑
reg[24:0] timer_cnt; //1s 计数器,0-24999999
//1s 定时计数
always @(posedge clk or negedge rst_n)
if(!rst_n) timer_cnt <= 25'd0;
else if(timer_cnt < 25'd24_999_999) timer_cnt <= timer_cnt+1'b1;
else timer_cnt <= 25'd0;
wire timer_1s_flag = (timer_cnt == 25'd24_999_999); //1s 定时到标位,高有效一个时钟周期
- 第二部分代码如下,秒定时信号 timer_1s_flag 拉高时,display_num 递增。
//-------------------------------------------------
//递增数据产生逻辑
//显示数据每秒递增
always @(posedge clk or negedge rst_n)
if(!rst_n) display_num <= 16'd0;
else if(timer_1s_flag) display_num <= display_num+1'b1;
endmodule
3、seg7.v模块代码解析
- seg7.v 模块对数据 display_num 进行译码,驱动数码管显示。该模块的接口如下,其中 输入信号 display_num 由counter.v 模块产生,输出信号 dtube_cs_n[3:0]是数码管的位选信号, dtube_data[7:0]为数码管的段选信号。
module seg7(
input clk, //时钟信号,25MHz
input rst_n, //复位信号,低电平有效
input[15:0] display_num, //数码管显示数据,[15:12]--数码管千位,[11:8]--数码管百位,[7:4]--数码管十位,[3:0]--数码管个位
output reg[3:0] dtube_cs_n, //7 段数码管位选信号
output reg[7:0] dtube_data //7 段数码管段选信号(包括小数点为 8 段)
);
- 以下参数定义数码管显示 0~F 对应的数码管段选输出值,即“译码”。
//参数定义
//数码管显示 0~F 对应段选输出
parameter NUM0 = 8'h3f,//c0,
NUM1 = 8'h06,//f9,
NUM2 = 8'h5b,//a4,
NUM3 = 8'h4f,//b0,
NUM4 = 8'h66,//99,
NUM5 = 8'h6d,//92,
NUM6 = 8'h7d,//82,
NUM7 = 8'h07,//F8,
NUM8 = 8'h7f,//80,
NUM9 = 8'h6f,//90,
NUMA = 8'h77,//88,
NUMB = 8'h7c,//83,
NUMC = 8'h39,//c6,
NUMD = 8'h5e,//a1,
NUME = 8'h79,//86,
NUMF = 8'h71,//8e;
NDOT = 8'h80; //小数点显示
- 以下参数则定义数码管不同位选单独选中的位选输出值,由于数码管的段选是 4 个位显 示复用的,因此我们采用了“4个位分时点亮”的办法来驱动数码管。实际上,4 位数码管 在同一时刻,只有 1 位是点亮的,4位轮流循环被点亮。点亮某一位时,则“译码”对应的 输出数据。虽然数码管时分时点亮的,但是当它的频率控制在一定的范围内,人眼很容易被“欺骗”,看到 4 位一直点亮的显示数据。
//数码管位选 0~3 对应输出
parameter CSN = 4'b1111,
CS0 = 4'b1110,
CS1 = 4'b1101,
CS2 = 4'b1011,
CS3 = 4'b0111;
- 分时计数器 div_cnt 就是用来计数分频,产生 4 个不同的时间段,分别点亮 4 位的数码管,并且在单独选中某个位的数码管时current_display_num[3:0]寄存器则用于缓存相应的位要显示的数据(display_num[15:0]的其中 4 位)。
//分时显示数据控制单元
reg[3:0] current_display_num; //当前显示数据
reg[7:0] div_cnt; //分时计数器
//分时计数器
always @(posedge clk or negedge rst_n)
if(!rst_n) div_cnt <= 8'd0;
else div_cnt <= div_cnt+1'b1;
//显示数据
always @(posedge clk or negedge rst_n)
if(!rst_n) current_display_num <= 4'h0;
else begin
case(div_cnt)
8'hff: current_display_num <= display_num[3:0];
8'h3f: current_display_num <= display_num[7:4];
8'h7f: current_display_num <= display_num[11:8];
8'hbf: current_display_num <= display_num[15:12];
default: ;
endcase
end
- 接下来的 always 语句中进行数据段选“译码”逻辑的实现,根据显示数据 current_display_num 获得段选输出 dtube_data。
//段选数据译码
always @(posedge clk or negedge rst_n)
if(!rst_n) dtube_data <= NUM0;
else begin
case(current_display_num)
4'h0: dtube_data <= NUM0;
4'h1: dtube_data <= NUM1;
4'h2: dtube_data <= NUM2;
4'h3: dtube_data <= NUM3;
4'h4: dtube_data <= NUM4;
4'h5: dtube_data <= NUM5;
4'h6: dtube_data <= NUM6;
4'h7: dtube_data <= NUM7;
4'h8: dtube_data <= NUM8;
4'h9: dtube_data <= NUM9;
4'ha: dtube_data <= NUMA;
4'hb: dtube_data <= NUMB;
4'hc: dtube_data <= NUMC;
4'hd: dtube_data <= NUMD;
4'he: dtube_data <= NUME;
4'hf: dtube_data <= NUMF;
default: ;
endcase
end
- 最后一个 always 语句则对位选信号进行分时“开启”的处理。
//位选译码
always @(posedge clk or negedge rst_n)
if(!rst_n) dtube_cs_n <= CSN;
else begin
case(div_cnt[7:6])
2'b00: dtube_cs_n <= CS0;
2'b01: dtube_cs_n <= CS1;
2'b10: dtube_cs_n <= CS2;
2'b11: dtube_cs_n <= CS3;
default: dtube_cs_n <= CSN;
endcase
end
endmodule
四、实验现象
- 我们可以看到数码管从最低位开始依次从 0 到 F(16 进制形式)不断的递增。
参考链接
https://www.bilibili.com/video/BV17z411i7er?p=31
https://www.bilibili.com/video/BV17z411i7er?p=32
https://www.bilibili.com/video/BV17z411i7er?p=33
https://www.bilibili.com/video/BV17z411i7er?p=34
https://www.bilibili.com/video/BV17z411i7er?p=35