前言
前几天在开源的FPGA论坛上看到FPGA工程师前辈说的FPGA工程师的职业病,其中有一条是看见代码就想着重构;之后又刷到一篇博客专门讲写VerilogHDL代码的规范问题,我这些看一下,我大致处于有一定基础的新手与工程师之间。
在我看过的工程师写的VerilogHDL代码中,我发现一个普遍的规律,就是always块的触发条件中,与clk(时钟信号)的上升沿一定与rst(复位信号)的下降沿进行或运算,且这类alway块内的内容中,一定要先判断复位信号rst的状态再进行接下来的主要操作,其伪代码大致如下:
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
//寄存器复位
end
else
begin
//时序逻辑电路
end
end
接下来还是结合着昆明理工大学数字电子技术实验A(2024-2025下学期版),来对复位端进行一个深入的理解和应用。
一、设计目标与设计思路
数字电子技术实验A实验三分成四个子任务,简单说一下就是:1.四位拨动开关控制数码管显示0-9,显示在第一个数码管上;2.四位拨动开关控制数码管显示0-9,三位拨动开关控制字符显示在对应数码管上;3.设计分频器,分出0.5Hz、1Hz、100Hz、500Hz、1000Hz的频率,通过三位拨动开关控制输出特定频率方波,由led显示;4.利用三位拨动开关,在数码管上显示专属参数对应字符,并用分频器的输出方波对数码管的位选进行扫描,同时led进行1Hz的闪烁。
通过任务目标可以发现,前两个任务就是之前的seglight.v子模块的实例化,是组合逻辑电路。根据我的理解,组合逻辑电路的复位端一般来说就是使能端,使能端一断,组合逻辑电路就停止工作。
后两个任务是简单的时序电路的设计,对于时序逻辑电路,复位端的主要功能是清楚寄存器里面的内容,使整个电路回到初始状态。分频的原理就不细说了,极端例子就是数时间:数60个1秒就是1分钟,数3600个1秒就是1小时……
任务4有个细节的问题:分频器对数码管位选进行扫频,任务要求无明显闪烁,为实现目标可以将6位数码管全部打通,就无明显闪烁了。(这可能是实验编者的失误,也可能是我理解错误,这里还是按照要求来编写)
二、主模块
由于有四个任务,需要四个子模块的实例化,这里考虑使用按键作为不同任务的选择,也用按键来充当组合逻辑电路的使能端与时序逻辑电路的复位端,具体程序如下:
module assignment3(clk, sw, key, seg, dig, led);
input clk;
input [7:0] sw;
input [7:0] key;
output [7:0] seg;
output [5:0] dig;
output [15:0] led;
wire [15:0] led1;
wire [15:0] led2;
reg [3:0] en;
wire rst;
assign rst = key[7];
always @(key)
begin
case(key)
8'b11111110: en <= 4'b0001;
8'b11111101: en <= 4'b0010;
8'b11111011: en <= 4'b0100;
8'b11110111: en <= 4'b1000;
endcase
end
assignment3_1 u_ass1(
.enable(en[0] & rst),
.sw(sw),
.seg(seg),
.dig(dig)
);
assignment3_2 u_ass2(
.enable(en[1] & rst),
.sw(sw),
.seg(seg),
.dig(dig)
);
assignment3_3 u_ass3(
.clk(clk),
.rst(en[2] & rst),
.sw(sw),
.led(led1)
);
assignment3_4 u_ass4(
.clk(clk),
.rst(en[3] & rst),
.sw(sw),
.dig(dig),
.seg(seg),
.led(led2)
);
assign led[0] = led1 | led2;
endmodule
由于任务三和任务四都要用到同一个led灯,直接将两个模块的输出led引脚接在led[0]上会出现引脚复用的错误,因为系统认为我的两个子模块的led引脚同时接在led[0]上,状态不明确,但是我知道一个在运行、另一个不会运行,因此我可以大胆地将两值求或输出。
将key[7]作为组合逻辑电路的使能端与时序逻辑电路的复位端,与任务选择端求与输入到子模块。由于我的FPGA开发板外电路图中,按键通过上拉电阻接Vcc,因此常态高电平。
三、任务一与任务二
类似7448的组合逻辑电路的设计,用之前早已编写好的seglight.v子模块就行,这里就不展示了,具体的可以翻找之前的文章,大多都有seglight.v子模块的使用。
四、任务三
分频的原理在前面已经简单举例了,接下来演示一下1Hz的分频,代码如下:
module CLK_1Hz(clk, rst, clkout);
input clk;
input rst;
output clkout;
reg [27:0] counter;
reg state;
parameter f = 25_000_000;
always @(posedge clk or negedge rst)
begin
if (!rst)
begin
counter <= 0;
state <= 0;
end
else
begin
counter <= counter + 1;
if (counter >= f)
begin
counter <= 0;
state <= !state;
end
end
end
assign clkout = state;
endmodule
我的开发板比较老,50MHz的主频,因此每跳动25'000'000次,就是0.5s;每0.5s高电平,0.5s低电平,就是周期为1s,频率为1Hz的方波。
这里首次出现的rst复位端的使用,即复位端被按下,寄存器清零,这段程序的寄存器只有计数器和状态,将这两寄存器清零,之后的操作中,这两个寄存器就重新开始运作。复位键没有被按下时,就遵循分频器的工作,计数到指定值后就进行翻转。
在应用中,clk引脚接入主频时钟信号,rst引脚接复位信号,clkout引脚输出1Hz的方波信号,应用到任务三中,代码如下:
module assignment3_3(clk, rst, sw, led);
input clk;
input rst;
input [3:0] sw;
output [15:0] led;
wire clk_5e1Hz;
wire clk_1Hz;
wire clk_100Hz;
wire clk_500Hz;
wire clk_1kHz;
reg ledstate;
assign led[15:1] = 16'o00000;
CLK_5e1Hz u_CLK_5e1Hz(
.clk(clk),
.rst(rst),
.clkout(clk_5e1Hz)
);
CLK_1Hz u_CLK_1Hz(
.clk(clk),
.rst(rst),
.clkout(clk_1Hz)
);
CLK_100Hz u_CLK_100Hz(
.clk(clk),
.rst(rst),
.clkout(clk_100Hz)
);
CLK_500Hz u_CLK_500Hz(
.clk(clk),
.rst(rst),
.clkout(clk_500Hz)
);
CLK_1kHz u_CLK_1kHz(
.clk(clk),
.rst(rst),
.clkout(clk_1kHz)
);
always @(sw)
begin
case(sw)
0: ledstate <= clk_5e1Hz;
1: ledstate <= clk_1Hz;
2: ledstate <= clk_100Hz;
3: ledstate <= clk_500Hz;
4: ledstate <= clk_1kHz;
endcase
end
assign led[0] = rst & ledstate;
endmodule
五、任务四
任务四是任务一和任务三的组合,led以1Hz的频率闪烁和数码管的显示这里就不多说了,主要来看一下今天的主体——复位端,先上代码:
module assignment3_4(clk, rst, sw, dig, seg, led);
input clk;
input rst;
input [2:0] sw;
output [5:0] dig;
output [7:0] seg;
output [15:0] led;
reg [8:0] segnum;
reg [4:0] dignum;
wire clkout1;
wire clkout2;
assign led[15:1] = 16'o00000;
CLK_1kHz u_CLK1(
.clk(clk),
.rst(rst),
.clkout(clkout1)
);
CLK_1Hz u_CLK2(
.clk(clk),
.rst(rst),
.clkout(clkout2)
);
seglight u_seg(
.enable(rst),
.seg(seg),
.dig(dig),
.segnum(segnum),
.dignum(dignum),
.point(0)
);
always @(posedge clkout1 or negedge rst)
begin
if (!rst)
begin
segnum <= 8'h00;
dignum <= 6'o00;
end
else
begin
dignum <= dignum + 1;
case (sw)
1: segnum <= 1;
2: segnum <= 2;
3: segnum <= 3;
4: segnum <= 4;
5: segnum <= 5;
6: segnum <= 6;
default: segnum <= 10;
endcase
end
end
assign led[0] = rst & clkout2;
endmodule
任务三和任务四的末尾都将复位端rst和led状态作于输给led,这个复位端的作用不仅是将时钟信号进行复位,也将led的输出置低电平。其实更好的写法应该是(ledstate与clkout2作用相同):
assign led[0] = rst ? ledstate : 1'bz;
这样的写法能保证复位键按下时,led[0]输出保持高组态,极大可能地防止led[0]输出到此模块以外的地方之后出现混淆的情况。同样在clk1kHz的时钟时序下,rst复位端的下降沿及其后续的低电平使数码管位选、段选寄存器置初始状态。
总结
复位端的作用是将时序电路的寄存器置初始状态,这一操作就像给计算机加入“重启”功能。在时序电路出现没能预期的错误或是卡顿后,利用复位端将电路重置回初始,方便找到问题,也能更加清晰地重新演示时序逻辑的状态。
当然要说不加复位端可以吗?可以!小程序可以,大程序绝对不可以!应该说,如果能写到大程序,应该就不是普通的Verilog入门者了,工程师一般都会在时序电路中加入复位端,保证时序电路可以正常的调试。
以上的程序需要修改专属参数的地方只有任务四时序主电路的地方,自行修改。我和工程师之间的最大差距还有就是代码中的注释。(我是一点没写……)