前言
昨天说过,硬件描述语言与Python、Java等高级语言不同,Verilog HDL这类硬件描述语言的每条语句都直接对应开发板内的电路连接。要想将长程序优化,其中一种方法就是将程序的内容按照功能划分,将常用的功能写成子程序,并在主程序中实例化。
这一过程就像是在数字电路领域,一般不会有人直接使用二极管搭建门电路,而是购买门电路集成芯片。
将常用的特定的模块写成子程序并封装后,更加方便后续的调用和调试,节省设计、修改的时间。
这次学习的例程是:将昨日《给定时间的计时》优化,并使动态数码管显示的内容中去除首位的‘0’。
一、数码管的使用
这里的数码管使用与上一篇的不同,这里将不再讲述动态数码管的使用方法,而是将数码管的显示封装成一个子程序。
首先定义子程序文件,包括文件名、输入输出变量等。点亮数码管,我就用通俗数码管的"seg"和亮"light"组合;输出变量包括数码管的段选和位选;输入变量包括预想点亮的数码管数字及其位置。这里我假设每次输出只输出一位。segnum、dignum是输入到子程序中,利用这两个参数获取能够输出到数码管上的特定编码,类似于简单的编译,seg和dig就是输出的“可执行文件”,能直接连接到数码管上。
module seglight
(
output reg [7:0] seg,
output reg [5:0] dig,
input wire [5:0] segnum,
input wire [4:0] dignum
);
随后定义数码管段选和位选的常量。注意,定义常量的"localparam"和"parameter"的区别是"localparam"只能在当前模块(程序文件)中使用,主程序调用这个子程序时,主程序不识别子程序localparam定义的常量。
localparam seg_0 = 8'b11111100;
localparam seg_1 = 8'b01100000;
localparam seg_2 = 8'b11011010;
localparam seg_3 = 8'b11110010;
localparam seg_4 = 8'b01100110;
localparam seg_5 = 8'b10110110;
localparam seg_6 = 8'b10111110;
localparam seg_7 = 8'b11100000;
localparam seg_8 = 8'b11111110;
localparam seg_9 = 8'b11110110;
localparam dig_1 = 6'b111110;
localparam dig_2 = 6'b111101;
localparam dig_3 = 6'b111011;
localparam dig_4 = 6'b110111;
localparam dig_5 = 6'b101111;
localparam dig_6 = 6'b011111;
最后就是熟悉的位选和段选。利用case语句实现。注意,case语句不能直接出现在module中,但是可以出现在always、generate等条件模块中。always的@(*)代表任何值改变了都能触发always块中的内容执行。
always @(*)
begin
case(segnum)
0:seg <= seg_0;
1:seg <= seg_1;
2:seg <= seg_2;
3:seg <= seg_3;
4:seg <= seg_4;
5:seg <= seg_5;
6:seg <= seg_6;
7:seg <= seg_7;
8:seg <= seg_8;
9:seg <= seg_9;
endcase
case(dignum)
1:dig <= dig_1;
2:dig <= dig_2;
3:dig <= dig_3;
4:dig <= dig_4;
5:dig <= dig_5;
6:dig <= dig_6;
endcase
end
endmodule
以上就是seglight.v的子程序,我们将其封装,后续可能会用到。
二、去除首位的‘0’
由于昨日的内容中没有将数码管显示的首位‘0’消除,不符合我们对数字的认知,因此需要去除。
在这一去除首位‘0’的部分中,我将再拆分成两步:一步是判断当前数字的位数,一步是获取不同位数的数字。由于FPGA使用的大多是数字电路,多以二进制组成,部分操作可能比Python、Java等高级语言复杂。
1、判断数字位数
在我们的理解中,判断数字位数的方式:先把数字看做是一个字组,每个数组位存放一个数字,高位补零,从高位往低位找,第一个非零位就是这个数字的位数。在今天的例程中也一样,但是不需要这样高精度的求法,因为Verilog中的数组每一位的值都只有0或1,利用高精度的判断相当困难。从实际出发,我的开发板最多显示6位数,我就能一个一个枚举。即先整除100_000,再对10求余,就能得到十万位上的数值,判断是否非零,非零则记录6;为零则将原数整除10_000,在对10求余,得到万位的数值,判断非零……
always @(*)
begin
if (number / 100000 % 10 != 0) digit <= 6;
else if (number / 10000 % 10 != 0) digit <= 5;
else if (number / 1000 % 10 != 0) digit <= 4;
else if (number / 100 % 10 != 0) digit <= 3;
else if (number / 10 % 10 != 0) digit <= 2;
else digit <= 1;
end
endmodule
文件的头部文件名则是判断judge和数字位dig的组合,输入数字number,输出位数digit。
module judgedig
(
input wire [20:0] number,
output reg [4:0] digit
);
这样就是judgedig.v的子程序,将其封装。
2、获取不同位上的数字
在正常的理解中,获取一个数字某个位n上的值,就直接整除10^n,再对10求余就行。但是经过测试我发现,利用循环累乘10总是会出错,且即使不循环乘10,连续对同一变量进行运算都会使这一变量变成0,不知道为什么。但是累乘的方法得到10^n不行,那就直接枚举获取10^n。由于之前说过,数码管最大显示数值999_999,因此根据输入的位数进行枚举,最多枚举6次,就能得到10^n。
always @(*)
begin
case(digit)
1:temp <= 1;
2:temp <= 10;
3:temp <= 100;
4:temp <= 1000;
5:temp <= 10000;
6:temp <= 100000;
endcase
numout <= numin / temp % 10;
end
endmodule
头上就是定义文件名,获取get和数字number组合,输入整个数字numin、指定的位digit,输出数字指定位上的数值numout。并定义临时变量temp,用于存放10^n。
module getnumber
(
input wire [20:0] numin,
output reg [5:0] numout,
input wire [4:0] digit
);
reg [20:0] temp;
将getnumber.v子程序封装起来。
三、主程序计时环节
在主程序中,就只需要完成时钟的计数、实例化子程序获取当前时间的位数、实例化子程序获取当前时间的不同位上的数值、实例化子程序对不同位上的不同的数值向数码管中输出这些环节。
首先还是需要定义文件、输入输出、定义变量等内容。时钟信号clk、数码管位选dig、数码管段选seg、开关输入key,时钟计数器counter、当前时间count、数码管段选值segnum、数码管位选值dignum、上一状态数码管预位选dignum1、为防止出现Error10663的必须连接到网络结构表达式而定义的中间变量seg1和dig1、确定当前时间的总位数tdig1、确定dignum位上的数值tnum1(连接到网络结构表达式的segnum)。initial内部的是对整个程序中部分变量的初始化。其中当前时间和数码管的段选值的初值是0,数码管的位选值是1。
module pro250125_2
(
input wire clk,
output reg [5:0] dig,
output reg [7:0] seg,
input wire [7:0] key
);
reg [25:0] counter;
reg [20:0] count;
reg [5:0] segnum;
reg [4:0] dignum;
reg [4:0] dignum1;
wire [7:0] seg1;
wire [5:0] dig1;
wire [4:0] tdig1;
wire [5:0] tnum1;
initial
begin
dignum <= 1;
count <= 0;
segnum <= 0;
end
定义、初始化结束后,就是实例化子程序了,三个子程序直接并行排列。
seglight u_seglight
(
.seg(seg1),
.dig(dig1),
.segnum(segnum),
.dignum(dignum1)
);
judgedig u_judgedig
(
.number(count),
.digit(tdig1)
);
getnumber u_getnumber
(
.numin(count),
.numout(tnum1),
.digit(dignum)
);
最后就是对时钟的计数。利用结构网络表达式的幅值,通过seg、dig直接连到数码管点亮;counter计数器计数,达到计数50_000_000次清零,并是当前时间+1;设定动态数码管的变换频率0.5MHz,交替获取不同位上的数值。
always @(posedge clk)
begin
seg <= seg1;
dig <= dig1;
counter <= counter + 1;
if (counter == 49_999_999)
begin
counter <= 0;
count <= (count + 1) % key;
end
if (counter % 100 == 0)
begin
dignum1 <= dignum;
dignum <= dignum % tdig1 + 1;
segnum <= tnum1;
end
end
endmodule
四、优化效果
这次的学习例程主要是优化昨天的内容,去除首位‘0’是附加内容。通过Quaters II生成的RTL图来对比一下电路的复杂性。
这个是优化前的。
这个是优化后的。
如果不关注实例化的子程序的内部图,可以明显发现优化后的原理图简单很多。当打开不同的子程序的原理图后,就有人会说了,诶你这个不是明显复杂了吗?
其实,就像上面说过的那样,在现实中,很少有人直接用二极管搭建门电路使用,也很少有人直接用门电路堆出类似74138译码器这类的功能的电路使用,有芯片就直接用芯片了。写子程序并对子程序的实例化的过程就可以认作是把二极管组成的门电路并封装,把门电路搭成具有74138译码器的功能并封装成74138芯片的过程,能够简化电路图。