上面两图分别是对应八段共阴、共阳的数码管内部等效图,共阴是将八个LED数码管的阴极连接在一起接低,阳极segment信号只需要输入高电平,相应的数码管就被点亮;将上面八个数码管按照下面形状排列,就构成了数码管。
如何让数码管显示出对应的数据?
拿八段共阴数码管显示2举例,如上图显示2需要点亮a、b、g、e、d这五个LED,其余LED全部熄灭。所以segment信号应该输出的数据是0101_1011。显示其余数字也是类似的原理,而h一般是用作小数点处理,不需要的时候一般都不点亮这个LED。下面是不包括h段LED的数码管译码对应表。
显示数字 | 共阳gfedcba 2进制 | 共阳gfedcba 16进制 | 共阴gfedcba 2进制 | 共阴gfedcba 16进制 |
---|---|---|---|---|
0 | 8’b11000000 | 8’hC0 | 8’b00111111 | 8’h3f |
1 | 8’b11111001 | 8’hF9 | 8’b00000110 | 8’h06 |
2 | 8’b10100100 | 8’hA2 | 8’b01011011 | 8’h5b |
3 | 8’b10110000 | 8’bB0 | 8’b01001111 | 8’h4f |
4 | 8’b10011001 | 8’h99 | 8’b01100110 | 8’b66 |
5 | 8’b10010010 | 8’h92 | 8’b01101101 | 8’h6d |
6 | 8’b10000010 | 8’h82 | 8’b01111101 | 8’h7d |
7 | 8’b11111000 | 8’hF8 | 8’b00000111 | 8’h07 |
8 | 8’b10000000 | 8’h80 | 8’b01111111 | 8’h7f |
9 | 8’b10010000 | 8’h90 | 8’b01101111 | 8’h6f |
经过上面可知,想让数码管显示对应的数据,就必须给此数码管的八个数据线(八个LED输入端)输入对应的数据,将这8根数据线称为一组数据线。那如果需要同时使用两个数码管呢?此时就需要控制16个LED,所以就需要2组数据线。由此需要n个数码管显示n个数字,就需要n组数据线,这会极大消耗数据线资源,但是平常单片机或者FPGA这些开发板上面的数码管很多,却只有一组数据线,这是怎么做到的呢?
为了解决上面问题,一般是将四个数码管制作成下图所示的三线制数码管,将四个独立数码管四组数据线接在一起,四个数码管共用一组数据线,同时每个数码管增加一个DIG片选信号,如下图当DIG1引脚为低电平时,此时数据线HEX的数据驱动第1个数码管显示对应的数字。通过控制4个DIG引脚实现一组数据线驱动多个数码管,比如20us点亮第一个数码管,之后每隔20us点亮下一个数码管,最后循环点亮四个数码管,在点亮不同数码管的时候控制数据线HEX输入不同的数据,就可以实现一组数据线驱动多个数码管显示不同的数据。
信号 | 位宽 | 输入/输出 | 描述 |
---|---|---|---|
clk | 1 | I | 系统时钟,1MHz |
rst_n | 1 | I | 系统复位,低电平有效 |
din | 4*数码管个数 | I | 待显示的BCD码数据 |
segment | 8 | O | 八段数码管的数据线 |
seg_sel | 数码管个数 | O | 数码管片选信号,电平有效 |
通过参数实现该模块可以驱动任意个数码管,通过自动计算位宽函数自动计算寄存器的位宽。
该设计实现FPGA可以驱动SEG_NUM个数码管,每个数码管显示的时间为TIME_20MS。所以本设计以20us计数器cnt_20us和点亮的第几个计数器sel为主体架构,其余信号对齐这两个计数器。计数器示意图如下,cnt_20us计数计数时,sel计数器加一,表示点亮下一个数码管。例如使用8个数码管同时显示12345678,则SEG_NUM=8,din=64‘h12345678,之后sel计数器为0时,segment译码显示4’d8,此时第0个数码管显示8,之后计数器cnt_20us计数结束,sel+1,此时点亮第一个数码管,则数据线segment应该将4’d7译码进行输出,之后各位显示类似。
参数设计以及自动计算位宽函数如下:
parameter TIME_20US = 20 ,//数码管刷新时间,默认20us。
parameter SEG_NUM = 6 //需要显示的数码管个数。
//参数定义
localparam TIME_W = clogb2(TIME_20US);//计算数码管扫描时间的时钟数据位宽;
localparam SEG_W = clogb2(SEG_NUM);
localparam ZERO = 8'hC0 ; //8'h3F;前面的数据是共阳数码管使用的,后面数据是共阴数码管使用的;
localparam ONE = 8'hF9 ; //8'h06;
localparam TWO = 8'hA4 ; //8'hB;
localparam THREE = 8'hB0 ; //8'hF;
localparam FOUR = 8'h99 ; //8'h66;
localparam FIVE = 8'h92 ; //8'h6D;
localparam SIX = 8'h82 ; //8'h7D;
localparam SEVEN = 8'hF8 ; //8'h07;
localparam EIGHT = 8'h80 ; //8'h7F;
localparam NINE = 8'h90 ; //8'h6F;
localparam ERR = 8'h86 ; //8'h79;
//自动计算位宽的函数;
function integer clogb2(input integer depth);begin
if(depth==0)
clogb2=1;
else if(depth!=0)
for(clogb2=0; depth>0;clogb2=clogb2+1)
depth=depth>>1;
end
endfunction
计数器cnt_20us初始值为0,对系统时钟进行计数,计数到TIME_20US-1时结束,对应代码如下:
//20us计数器,用于对一个数码管点亮的持续时间进行计数,计数器初始值为0,对
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin//计数器初始值为0;
cnt_20us <= 0;
end
else if(end_cnt_20us)begin//当计数器计数到20us时,表示一个数码管已经被点亮20US了,将计数器清零;
cnt_20us <= 0;
end
else begin//否则,计数器加一;
cnt_20us <= cnt_20us + 1'b1;
end
end
//计数器结束条件,当计数器计数到TIME_20US-1时表示20US已经到了,将计数器清零;
assign end_cnt_20us = cnt_20us == TIME_20US -
计数器sel初始值为0,表示最初点亮第0个数码管,当计数器cnt_20us计数计数结束时,表示一个数码管被点亮20us了,此时计数器sel加一,开始点亮下一个数码管,当计数器sel计数到SEG_NUM-1并且cnt_20us计数结束时,表示所有数码管都被点亮一次了,将计数器sel清零,从第一个数码管在此循环点亮;
综上:计数器sel初始值为0,加一条件add_sel=end_cnt_20us,结束条件end_sel=add_sel && sel==SEG_NUM-1,对应代码如下:
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
sel <= 0;
end
else if(add_sel) begin
if(end_sel)//当计数器sel计数结束时,计数器清零;
sel <= 0;
else
sel <= sel + 1;
end
end
assign add_sel = end_cnt_20us;//计数器sel的加一条件是,计数器cnt_20us计数器结束;
assign end_sel = add_sel && sel == SEG_NUM - 1;//计数器sel计数到SEL_NUM-1时,计数器sel清零;
接下来还需要做两件事:
输入数据din是BCD码,所以只需要在计数器sel为0是将din[3:0]译码成segment输出给数码管即可。根据这个原理,sel_result信号先根据计数器sel的值从din取出对应的四位数据,代码如下:
//sel_result信号是当前被点亮数码管需要显示的数据,根据计数器sel的值确定此时应该将输入信号的哪几位数据译码输出给数码管进行显示;
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
sel_result <= 4'd0;
end
else begin//取输入信号din[4*sel+3 : 4*sel]信号给译码部分进行译码,之后输出给数码管数据信号驱动数码管显示该数据;
sel_result <= din[4*sel+3 -: 4];//{din[4*sel+3],din[4*sel+2],din[4*sel+1],din[4*sel]};
end
end
之后将sel_result译码成能驱动数码管显示对应数据信号segment,对应代码如下:
//译码器部分,将sel_result十进制信号译码成数码管显示该数字对应的八位数据信号;
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始上电时,所有数码管显示数据0;
segment <= ZERO;
end
else begin
case(sel_result)//将sel_result译码成对应的segment数据,segment数据驱动数码管才能显示sel_result代表的数字;
0: segment <= ZERO ;//想要数码管显示0,就要给数码管数据信号segment输入ZERO数据,其余类似;
1: segment <= ONE ;
2: segment <= TWO ;
3: segment <= THREE;
4: segment <= FOUR ;
5: segment <= FIVE ;
6: segment <= SIX ;
7: segment <= SEVEN;
8: segment <= EIGHT;
9: segment <= NINE ;
default: segment <= ERR;
endcase
end
end
对应仿真如下,六个数码管显示24’h044039,sel为0时,sel_result=4’d9,segment将sel_result通过译码表1译码成NINE对应的数据8’h90,则第一个数码管显示9,其余类推;
计数器sel表示此时应该点亮第几个数码管,所以计数器sel=0时,应该将第一个数码管的片选拉低,其余数码管的片选拉高,此时数据线segment的数据用于驱动第一个数码管显示对应数据。segment信号滞后计数器sel变化两个时钟,所以seg_sel也需要滞后sel两个时钟,才能使得segment与seg_sel信号对齐。所以先将sel延迟一个时钟得到sel_ff0,代码如下:
//为了与段选动态扫描,保持同步,此时位选应该打一拍再赋给位选信号 seg_sel
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
sel_ff0 <= 0;
end
else begin
sel_ff0 <= sel;
end
end
之后根据sel_ff0得到数码管片选信号,将1左移sel_ff0之后取反,就可以将第sel_ff0位数码管片选拉低,其余数码管片选拉高,对应代码如下:
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0,全部数码管被点亮;
seg_sel <= {{SEG_NUM}{1'b0}};
end
else begin//将1左移sel_ff0位之后取反,seg_sel的第sel_ff0输出低电平,对应的第sel_ff0个数码管被点亮了,其余位输出高电平,对应的数码管熄灭;
seg_sel <= ~({{{SEG_NUM-1}{1'b0}},1'b1} << sel_ff0);//~(6'h1<
在公众号” 数字站 “回复“数码管fpga”获取下载链接。
下载后解压得到源文件如下图:
其中seg_disp.v是数码管驱动文件,test.v文件是仿真测试激励生成文件。可以直接使用modelsim打开该文件夹下的test.mpf文件进行仿真,如下图操作:
一定要先修改查找的文件类型,不然很可能找不到该类型的文件,然后选择test.mpf文件打开即可,步骤如下所示:
之后点击Library,然后选中work库里面的test右键选择Simulate即可。
之后可以自己在sim选项卡里面添加文件,也可以加入我之前配置好的页面,下面是使用我之前配置好页面的方法。点击file,之后点击load,最后点击Macro File,如下图所示:
首先还是选择打开文件夹显示所有类型的文件,之后选中wave.do文件打开,如下图所示:
如果要重新仿真,进行如下图步骤操作即可:
由于test.v文件有仿真结束设置,所以运行到时间后会自动停止仿真,回到该页面即可。
仿真结果正确,在使用该模块时,例化时只需要更改SEG_NUM参数确定数码管个数以及是共阴数码管还是共阳数码管即可,其余信号可变位宽可以由自动计算位宽函数自动计算得出,不需要认为更改,该过程是在综合工具编译是完成的,不会消耗额外资源。
1、注意:主要是以两个数码管为主体架构,需要注意数码管数据信号要与片选信号对齐,如果没有对齐则会出现数码管显示数据奇怪的问题,原因在于一个数码管在某个别时钟显示了上一个数码管或者下一个数码管该显示的数据。
2、扩展:后续可以加入小数点的控制电路,以及使用控制信号选择共阴还是共阳的数码管。