基于FPGA的数码管静态显示

描述

设计规划

设计一个8位数码管静态显示:采用共阳极(低电平点亮)8段数码管,控制八位数码管让其以00000000、11111111、22222222一直到FFFFFFFF循环显示。每个字符显示0.5s。

硬件资源

数码管

常见数码管分为七段或八段,八段是多一个小数点。也可分为共阴极或共阳极,观察下面的原理图,共阴极数码管对应端口为高电平时对应二极管点亮,共阳极则相反。

寄存器

段式数码管工作方式有两种:静态显示和动态显示。静态显示是指将8个数码管的段选信号连接在一起,就可以显示相同的数字。每个数码管的段选必须接一个8位数据线来显示字形,显示字形可一直保持,直到送入新字形码为止。如果每个数码管都接8位段选数据线,那么8个数码管就需要64根数据线。(例如8个数码管都显示0,那么每个数码管都要接一个8位的段选数据线,且控制段选信号为1100_0000)这样占用的I/O接口太多。如下图所示,我们将8个数码管的段选信号连接在一起,由位选信号去控制,每一个数码管上都有一个位选信号。那么在同一时刻8个数码管显示的字符都一样了。(例如8个数码管都显示0,那么8位位选信号为1111_1111,这样才能控制8个共阳数码管都能点亮。段选信号是连接在一起的,当它为1100_0000时,8个数码管都显示0)

寄存器

即使这样控制数码管仍然需要使用16个I/O口资源。如果想要节省I/O口,可以通过74HC595芯片(位移缓存器)来实现。

74HC595芯片

寄存器

寄存器

寄存器

使用一个串行输入口就可以并行输出八个输入的串行数据。但是一片芯片只能并行输出8位数据,但是8个数码管需要16位数据线,因此需要级联两片74HC595芯片进行输出:将Q7S引脚接入下一片的DS引脚,这样我们最少使用3个I/O口就可以控制多片芯片了。

10号引脚是主复位,低电平有效将移位寄存器的数据清零,通常接到Vcc防止数据清零。SHCP为移位寄存器时钟输入,上升沿时将输入的串行数据(DS端输入)移入移位寄存器中。如果一次输入的数据超过8bit,后面的数据会通过Q7S端口传到下一级芯片的DS端口。74HC595内部有一个8位 存储寄存器 ,由STCP(存储寄存器时钟)控制,STCP上升沿时移位寄存器的数据会进入数据存储寄存器中,令第13引脚为低即可让存储寄存器中的数据进行输出。

总结一下使用步骤:

  1. 首先把要传输的数据通过引脚DS输入到74HC595中。
  2. 产生SHCP时钟,将DS上的数据串行移入移位寄存器。
  3. 产生STCP时钟,将移位寄存器里的数据送入存储寄存器。
  4. 将13引脚置为低电平,存储寄存器的数据会在Q0-Q7并行输出,同时并行输出的数据会被锁存起来。

寄存器

AC620 开发板上的数码管采用串行移位寄存器芯片将串行数据转化为 16 位并行数据后进行驱动。Cyclone IV E 通过 3 根数据线,连接到两片级联的串行移位器芯片 74HC595 上,再由 74HC595 将每次 16 位串行的数据转化为 16 位并行的数据,分别用以驱动8 段 8 位数码管的段选和位选。(用户手册是7段,但是有小数点,这里统一称为8段)

寄存器

我们采用的是低电平点亮的共阳极数码管。首先看位选信号,SEL[0]对应开发板最右侧的数码管,以此类推。位选信号为1111_1111才能点亮数码管。再看段选信号,以显示0为例,需要将abcdef点亮,按照左边高位右边低位的顺序,dp、g、f、e、d、c、b、a就对应1100_0000。0-F对应(0)1100_0000、(1)1111_1001、(2)1010_0100、(3)1011_0000、(4)1001_1001、(5)1001_0010、(6)1000_0010、(7)1111_1000、(8)1000_0000、(9)1001_0000、(A)1000_1000、(B)1000_0011、(C)1100_0110、(D)1010_0001、(E)1000_0110、(F)1000_1110。

寄存器

编写代码

通过系统框图可以看出,分为3个模块:数码管驱动模块,芯片控制模块和数码管显示模块。数码管显示模块是 顶层模块 ,实质上完成两个子模块的实例化。上图里整个系统连接方式是Cyclone IV E连接74HC595芯片连接数码管。我们看数码管驱动和芯片控制这两个子模块,下图中数码管的引脚图需要段选信号和位选信号去驱动,74HC595芯片,除了几个常接高低电平的引脚外,它的输入引脚是接Cyclone IV E的,输出引脚是控制数码管的。顶层模块中只需要考虑芯片和Cyclone IV E之间的连线,因此顶层模块的输入是时钟和复位信号,输出是芯片的输入引脚:寄存器时钟,存储器时钟,数据输入。尤其要注意的是,因为我用的开发板没有使能引脚,他不能作为输出存在。我们再将顶层模块拆分为数码管驱动模块和芯片控制模块。数码管驱动模块考虑数码管和芯片之间的连线,因此它的输入是时钟和复位信号,输出是段选和位选信号。芯片控制模块考虑的是芯片和数码管以及Cyclone IV E的连线,因此它的输入是时钟、复位、段选和位选信号,使能信号,输出是芯片的输入引脚(寄存器时钟,存储器时钟,数据输入)。理清楚了这些,就很容易编写代码了。

寄存器

寄存器

寄存器

1、数码管驱动模块seg_static

寄存器

寄存器

包括输入:时钟信号、复位信号,输出:段选信号和位选信号。这个模块最重要的是弄清楚怎么产生段选和位选信号。每隔0.5s,我们要实现00000000-FFFFFFFF的循环显示,那么位选信号我们之前探讨了必须为1111_1111才能使数码管正常工作,而段选信号我们也探讨了从0-F的段选信号,就可以画出波形图。根据波形图可以编写代码。

寄存器

我们需要三个中间信号:计数器cnt_wait,标志信号add_flag,显示信号num。其中,cnt_wait从0计数到24999999即0.5s,每到计满时标志信号add_flag拉高,且显示信号跳转到下一个状态,显示信号需要从0-F循环。

module seg_static
(
input wire sys_clk , 
input wire sys_rst_n , 


output reg [7:0] sel , 
output reg [7:0] seg 


);


 //parameter define
 parameter CNT_WAIT_MAX = 25'd24_999_999; //计数器最大值(0.5s)
 //十六进制数显示编码
 parameter SEG_0 = 8'b1100_0000, SEG_1 = 8'b1111_1001,
 SEG_2 = 8'b1010_0100, SEG_3 = 8'b1011_0000,
 SEG_4 = 8'b1001_1001, SEG_5 = 8'b1001_0010,
 SEG_6 = 8'b1000_0010, SEG_7 = 8'b1111_1000,
 SEG_8 = 8'b1000_0000, SEG_9 = 8'b1001_0000,
 SEG_A = 8'b1000_1000, SEG_B = 8'b1000_0011,
 SEG_C = 8'b1100_0110, SEG_D = 8'b1010_0001,
 SEG_E = 8'b1000_0110, SEG_F = 8'b1000_1110;
 parameter IDLE = 8'b1111_1111; //不显示状态


 //reg define
 reg add_flag ; 
 reg [24:0] cnt_wait ; 
 reg [3:0] num ; 


 //cnt_wait:0.5秒计数
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_wait <= 25'd0;
 else if(cnt_wait == CNT_WAIT_MAX)
 cnt_wait <= 25'd0;
 else
 cnt_wait <= cnt_wait + 1'b1;


 //add_flag:0.5s拉高一个标志信号
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 add_flag <= 1'b0;
 else if(cnt_wait == CNT_WAIT_MAX - 1)
 add_flag <= 1'b1;
 else
 add_flag <= 1'b0;


 //num:从 4'h0 加到 4'hf 循环
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 num <= 4'd0;
 else if(add_flag == 1'b1)
 num <= num + 1'b1;
 else
 num <= num;


 //sel:选中8个数码管
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 sel <= 8'b00000000;
 else
 sel <= 8'b11111111;


 //给要显示的值编码
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 seg <= IDLE;
 else case(num)
 4'd0: seg <= SEG_0;
 4'd1: seg <= SEG_1;
 4'd2: seg <= SEG_2;
 4'd3: seg <= SEG_3;
 4'd4: seg <= SEG_4;
 4'd5: seg <= SEG_5;
 4'd6: seg <= SEG_6;
 4'd7: seg <= SEG_7;
 4'd8: seg <= SEG_8;
 4'd9: seg <= SEG_9;
 4'd10: seg <= SEG_A;
 4'd11: seg <= SEG_B;
 4'd12: seg <= SEG_C;
 4'd13: seg <= SEG_D;
 4'd14: seg <= SEG_E;
 4'd15: seg <= SEG_F;
 default:seg <= IDLE ; //闲置状态,不显示
 endcase


 endmodule

计数器cnt_wait:0.5s计数器,计数值从0-24_999_999。复位有效时归0,计数到24_999_999计满时归0,其他情况+1。

标志信号add_flag:计满0.5s拉高一个脉冲,复位有效时归0,计数到24_999_999-1时拉高,其他情况归0。

显示信号num:每当标志信号拉高时从0-F循环,复位有效时归0,判断标志信号为高电平时+1(F是1111,+1为10000溢出,但只保留后四位0000,因此F的下一个状态还是0),其他情况保持原值。

位选信号sel:位选信号长期接高点平,数码管才能显示,因此复位有效时归0,其他情况都为高电平。

段选信号seg:段选信号与显示数值的对应关系之前已经讨论过了。除了0-F的16种显示情况外增加了一种不显示的闲置状态IDLE。复位有效时段选信号为IDLE,与num数值有关。使用CASE语句判断num值并将对应的参数赋值给段选信号seg,注意要有default语句。

2、芯片控制模块

寄存器

寄存器

包括输入:时钟信号、复位信号,段选信号,位选信号,输出使能,输出:芯片的四个输入引脚(寄存器时钟,存储器时钟,数据输入)。这个模块最重要的是弄清楚怎么通过控制Cyclone IV E和芯片连接的引脚,让芯片产生正确的输出,去驱动数码管正确的显示图形。

寄存器

注意:对于我使用的开发板,没有oe对应引脚,它是作为输入信号存在的(可阅读https://blog.csdn.net/weixin_43614528/article/details/87878938?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%2287878938%22%2C%22source%22%3A%22unlogin%22%7D)74HC595驱动模块观察其代码。而对于征途开发板,使能引脚为L11。

寄存器

shcp是寄存器时钟,上升沿时数据写入移位寄存器,它有频率限制,这里采用系统时钟的4分频,即12.5MHz。stcp是存储器时钟,串行输入16位之后拉高。 en是使能信号,需要一直维持高电平 (图中的使能信号oe刚好相反,这与开发板有关,因此要仔细阅读用户手册)

module hc595_ctrl
(
input wire sys_clk , 
input wire sys_rst_n , 
input wire [7:0] sel , 
input wire [7:0] seg , 
input wire en ,


output reg stcp , 
output reg shcp , 
output reg ds  


 );


 //reg define
 reg [1:0] cnt_4 ; //分频计数器
 reg [3:0] cnt_bit ; //传输位数计数器


 //wire define
 wire [15:0] data ; //数码管信号寄存


 //将数码管信号寄存
 assign data={sel,seg[0],seg[1],seg[2],seg[3],seg[4],seg[5],seg[6],seg[7]};


 //分频计数器:0~3循环计数
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_4 <= 2'd0;
 else if(cnt_4 == 2'd3)
 cnt_4 <= 2'd0;
 else
 cnt_4 <= cnt_4 + 1'b1;


 //cnt_bit:每输入一位数据加一
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_bit <= 4'd0;
 else if(cnt_4 == 2'd3 && cnt_bit == 4'd15)
 cnt_bit <= 4'd0;
 else if(cnt_4 == 2'd3)
 cnt_bit <= cnt_bit + 1'b1;
 else
 cnt_bit <= cnt_bit;


 //stcp:14个信号传输完成之后产生一个上升沿
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 stcp <= 1'b0;
 else if(cnt_bit == 4'd15 && cnt_4 == 2'd3)
 stcp <= 1'b1;
 else
 stcp <= 1'b0;


 //shcp:产生四分频移位时钟
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 shcp <= 1'b0;
 else if(cnt_4 >= 4'd2)
 shcp <= 1'b1;
 else
 shcp <= 1'b0;


 //ds:将寄存器里存储的数码管信号输入即
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 ds <= 1'b0;
 else if(cnt_4 == 2'd0)
 ds <= data[cnt_bit];
 else
 ds <= ds;


 endmodule

使能信号en:在这里没有给他赋值,而在顶层模块的实例化时接了高电平。

分频计数器cnt_4:4分频即0-3循环计数,复位有效时归0,计数到3计满时归0,其他情况+1。

传输位数计数器cnt_bit:每传输一个数据+1,0-15循环计数。由于分频之后四个时钟周期为一个新的时钟周期,一个新时钟周期传输一个数据。复位有效时归0,当cnt计数到3且cnt_bit计数到15计满时归0,cnt计数到3且cnt_bit没计满时+1,其他情况保持。

存储器时钟stcp:15个信号传输完成后拉高,复位有效时归0,当cnt计数到3且cnt_bit计数到15计满时拉高,其他情况保持低电平。

寄存器时钟shcp:第九节分频器中仅分频的分频器实现方法,4分频后的时钟脉冲周期是原来的4倍。复位有效时归0,计数大于等于2(2,3)时拉高,其他情况(0,1)拉低。

串行数据输出ds:对FPGA芯片来说是输出,对74HC595芯片来说是输入。复位有效时归0,当分频计数器计数值为0时开始传输数据,其他情况保持原值。传输的数据是FPGA芯片输出的16位数据,74HC595芯片会串行输出(一次输出1bit)。数据data是seg[0]...seg[7],sel拼接起来的,sel是从高位到低位的顺序,data[cnt_bit]是从低位到高位的输出,因此是从位选的低位到高位,再从段选的高位到低位依次输出。

3、顶层模块

module seg_595_static
(
input wire sys_clk ,
input wire sys_rst_n , 


output wire stcp , 
output wire shcp , 
output wire ds 


 );


 //wire define
 wire [7:0] sel;
 wire [7:0] seg;


 //---------- seg_static_inst ----------
 seg_static seg_static_inst
 (
 .sys_clk (sys_clk ), 
 .sys_rst_n (sys_rst_n ), 


 .sel (sel ), 
 .seg (seg ) 
 );


 //---------- hc595_ctrl_inst ----------
 hc595_ctrl hc595_ctrl_inst
 (
 .sys_clk (sys_clk ), 
 .sys_rst_n (sys_rst_n), 
 .sel (sel ), 
 .seg (seg ), 
 .en(1'b1),


 .stcp (stcp ), 
 .shcp (shcp ), 
 .ds (ds )
 );


 endmodule

顶层模块实质是两个实例化。

寄存器

编写testbench

`timescale 1ns/1ns
module tb_seg_595_static();


//wire define
wire stcp ; //输出数据存储寄时钟
 wire shcp ; //移位寄存器的时钟输入
 wire ds ; //串行数据输入
 wire oe ; //输出使能信号


 //reg define
 reg sys_clk ;
 reg sys_rst_n ;
 
 //对sys_clk,sys_rst_n赋初始值
 initial
 begin
 sys_clk = 1'b1;
 sys_rst_n <= 1'b0;
 #100
 sys_rst_n <= 1'b1;
 end


 //clk:产生时钟
 always #10 sys_clk <= ~sys_clk;


 //重新定义参数值,缩短仿真时间
 defparam seg_595_static_inst.seg_static_inst.CNT_WAIT_MAX = 100;


 //-------------seg_595_static_inst-------------
 seg_595_static seg_595_static_inst(


 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低电平有效


 .stcp (stcp ), //输出数据存储寄时钟
 .shcp (shcp ), //移位寄存器的时钟输入
 .ds (ds ), //串行数据输入
 .oe (oe ) //输出使能信号
 );


 endmodule

初始化:对时钟信号和复位信号赋初值,延迟100ns后复位拉高。

产生时钟:延迟10ns后取反,周期为20ns,50MHz的时钟。

为了节省仿真时间,重新定义参数。当一个模块引用另外一个模块时,高层模块可以改变低层模块用parameter定义的参数值。低层模块的参数可以通过层次路径名重新定义。

实例化

对比波形

数码管静态驱动模块仿真波形图

这里看模块的波形,可以把原来的波形delete,再把想要的如图添加进去。后面要修改可以restart后break再重新run all。

寄存器

寄存器

寄存器

可以看到数码显示的值(num)从0开始跳转到了1,再跳转到了2;同时段选信号(seg)的编码与显示的字符也相吻合

寄存器

可以看到数码管显示的数值从4’hf跳转回0

74HC595控制模块仿真波形图

寄存器

分配管脚

寄存器

寄存器

寄存器

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分