本文是针对verilog基础语法做进一步的学,通过网站HDLbits中的代码和例子来展开本文要讨论的内容。HDLbits是一个多伦多大学的学生做的网站,该学生把课程中的习题汇总起来,一共有178道题目,其中包括verilog的组合逻辑、时序逻辑、状态机、testbench等等。对于FPGA初学者,很有必要借助该网站来学习verilog。
素材来源:HDLbits
在always块中,可以是时序逻辑,也可以是组合逻辑。时序逻辑和组合逻辑的区别在于always敏感列表中有没有时钟信号参与,有时钟信号参与的是时序逻辑,没有时钟信号参与的是组合逻辑。wire使用assign赋值,reg型适用于过程语句赋值,在always过程块中要求被赋值变量必须为reg型。
module top_module(
input a,
input b,
output wire out_assign,
output reg out_alwaysblock
);
assign out_assign = a & b;
always@(*)begin
out_alwaysblock = a & b;
end
endmodule
在always中使用组合逻辑,与assign连续赋值作用是一样的,如下图1所示中out_assign,out_alwaysblock输出的波形是同步并且一致的。由此说明,always中使用组合逻辑,实现的功能和assign连续赋值实现的功能是一样的。
注意:如上代码和如下图1仿真所示,always和assign代码之间是并行运行的,assign与always放置位置不会影响程序最终的结果。
在下面模块中加入有时钟信号的always模块,从图2仿真中out_assign,out_always_comb输出信号,可以发现always的敏感列表(*)没有时钟,最终实现的功能和assign是一致的。但是在加入时钟信号的always块中,设置信号对时钟上升沿有效(posedge clk),输出的out_always_ff信号是有延迟的,要等到时钟上升沿开始才进行非阻塞赋值<=,并不是和assign与always(*)一样马上给出结果。
module top_module(
input clk,
input a,
input b,
output wire out_assign,
output reg out_always_comb,
output reg out_always_ff );
always@(*)begin
out_always_comb = a ^ b;
end
always@(posedge clk)begin
out_always_ff <= a ^ b;
end
assign out_assign = a ^ b;
endmodule
if语句实现的电路是选择器,out输出受条件condition控制,condition为真的时候out=x,condition为假的时候out=y。
以下两种写法,always(*)组合逻辑与三目运算实现的功能是一样的。
always @(*) begin
if (condition) begin
out = x;
end
else begin
out = y;
end
end
assign out = (condition) ? x : y;
问题:构建一个2-1选择器,在a和b之间进行选择。如果sel_b1和sel_b2都为true,则选择b。否则,选择a。分别使用assgign和always(*)实现。
module top_module(
input a,
input b,
input sel_b1,
input sel_b2,
output wire out_assign,
output reg out_always );
always@(*)begin
if(sel_b1 & sel_b2)begin
out_always = b;
end
else begin
out_always = a;
end
end
assign out_assign = (sel_b1 & sel_b2)? b:a;
endmodule
在编写代码之前,要提前思考一下,你需要一个什么电路,verilog最终的目的是将代码综合成门级网表。在编写代码的时候,使用if或者case语句一定要把所有条件考虑周全,什么条件输出什么结果,如果条件考虑不周全就会产生毛刺(latch)。在if和case中,要使用else和default把其他条件包括进来,这是同学们在编写代码的时候容易忽略的地方,这与编写c、c++、python、java软件代码不一样的地方。
以下是一个考虑不周全的代码示范。
always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end
always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
end
从仿真中可以发现,cpu_overheated信号不管是高电平还是低电平,shut_off_ computer的信号始终为高电平。我们想实现的功能是,如果cpu的温度过高,那么就关掉电脑,如果cpu的温度是正常,就不关电脑。仿真给我们的结果是不管cpu温度是否正常都关掉电脑,这是不符合现实情况的。arrived信号和gas_tank_empty的信号共同影响keep_driving信号,实现的功能是,如果没有到达目的地,并且车的油箱还有油,那么就可以继续保持开车这个状态。如果没有到达目的,并且油箱里面没有油,也会停止保持开车这个状态。如果到达目的地,不管油箱里面是否有油,停止保持开车这个状态。仿真结果显示并没有实现这一功能,造成这样的情况主要是我们没有把所有的情况考虑周全。
以下是上述考虑不周全代码修改过后的代码。
module top_module (
input cpu_overheated,
output reg shut_off_computer,
input arrived,
input gas_tank_empty,
output reg keep_driving);
always @(*) begin
if (cpu_overheated)begin
shut_off_computer = 1;
end
else if(~cpu_overheated)begin
shut_off_computer = 0;
end
else begin
shut_off_computer = shut_off_computer;
end
end
always @(*) begin
if (~arrived)begin
keep_driving = ~gas_tank_empty;
end
else if(arrived)begin
keep_driving = 0;
end
else begin
keep_driving = keep_driving;
end
end
endmodule
我们在条件if语句中添加了其他条件,并且也把else的情况考虑进来,最后实现了我们想要得结果,如下图7功能仿真所示。
case语句实现的功能和if是一致的,在case语句中的default和if语句中的else是一样的。要注意一点,如果冒号(:)后面有多条语句,需要用begin和end将代码块包裹起来。
always @(*) begin
case (in)
1'b1: begin
out = 1'b1;
end
1'b0: out = 1'b0;
default: out = 1'bx;
endcase
end
问题:在有大量条件的情况下,case语句比if语句更方便。所以,在这个练习中,创建一个6-1的多路选择器。当sel在0到5之间时,选择相应的数据输入,否则默认条件输出0。数据输入和输出都是4位宽。编写程序时注意毛刺。
module top_module (
input [2:0] sel,
input [3:0] data0,
input [3:0] data1,
input [3:0] data2,
input [3:0] data3,
input [3:0] data4,
input [3:0] data5,
output reg [3:0] out);
always@(*) begin
case(sel)
3'd0: out = data0;
3'd1: out = data1;
3'd2: out = data2;
3'd3: out = data3;
3'd4: out = data4;
3'd5: out = data5;
default:out = 1'd0 ;
endcase
end
endmodule
问题:在这个问题中,我们要实现优先编码器,需要找出信号比特为1的位置,例如,给定输入8’b10010000的8位优先级编码器将输出3’d4,因为[4]位是第一个高的位。注意比特的位置是从0开始编号。如果信号中没有比特1,默认输出的位置为0。
module top_module (
input [3:0] in,
output reg [1:0] pos);
always@(*)begin
case(in)
4'b0000: pos = 2'd0;
4'b0001: pos = 2'd0;
4'b0010: pos = 2'd1;
4'b0011: pos = 2'd0;
4'b0100: pos = 2'd2;
4'b0101: pos = 2'd0;
4'b0110: pos = 2'd1;
4'b0111: pos = 2'd0;
4'b1000: pos = 2'd3;
4'b1001: pos = 2'd0;
4'b1010: pos = 2'd1;
4'b1011: pos = 2'd0;
4'b1100: pos = 2'd2;
4'b1101: pos = 2'd0;
4'b1110: pos = 2'd1;
4'b1111: pos = 2'd0;
default: pos = 2'd0;
endcase
end
endmodule
在章节三中种使用case,实现4位的优先编码器,需要17个情况,包括默认条件default。如果信号是8位,信号的组合情况是2的8次方256种,要把每种情况都考虑,需要编写256个情况。如果我们使用casez可以将256中情况减少至9种情况,其中包括默认default项。casez它将值为z的位视为不关心的项,如下面的代码所示。
注意:如果把8’bzzzzzz10换成8’bzzzzzz1z是不行的,因为8’bzzzzzz11、8’bzzzzzz10都会匹配到8’bzzzzzz1z项。
module top_module (
input [7:0] in,
output reg [2:0] pos);
always@(*)
begin
casez(in[7:0])
8'bzzzzzzz1: pos = 3'b000;
8'bzzzzzz10: pos = 3'b001;
8'bzzzzz100: pos = 3'b010;
8'bzzzz1000: pos = 3'b011;
8'bzzz10000: pos = 3'b100;
8'bzz100000: pos = 3'b101;
8'bz1000000: pos = 3'b110;
8'b10000000: pos = 3'b111;
default: pos = 3'b000;
endcase
end
endmodule
经过测试,casex和casez的用法是一致的,只是要把z改为x。
module top_module (
input [7:0] in,
output reg [2:0] pos);
always@(*)
begin
casex(in[7:0])
8'bxxxxxxx1: pos = 3'b000;
8'bxxxxxx10: pos = 3'b001;
8'bxxxxx100: pos = 3'b010;
8'bxxxx1000: pos = 3'b011;
8'bxxx10000: pos = 3'b100;
8'bxx100000: pos = 3'b101;
8'bx1000000: pos = 3'b110;
8'b10000000: pos = 3'b111;
default: pos = 3'b000;
endcase
end
endmodule
也可以在casez和casex中用?代替z或者x,如下代码所示。
module top_module (
input [7:0] in,
output reg [2:0] pos);
always@(*)
begin
casex(in[7:0])
8'b???????1: pos = 3'b000;
8'b??????10: pos = 3'b001;
8'b?????100: pos = 3'b010;
8'b????1000: pos = 3'b011;
8'b???10000: pos = 3'b100;
8'b??100000: pos = 3'b101;
8'b?1000000: pos = 3'b110;
8'b10000000: pos = 3'b111;
default: pos = 3'b000;
endcase
end
endmodule
问题:为游戏构建一个电路来处理来自PS/2键盘的扫描代码。给定接收到的最后两个字节的扫描码,您需要指示是否按下了键盘上的哪一个方向键。
module top_module (
input [15:0] scancode,
output reg left,
output reg down,
output reg right,
output reg up);
always @(*) begin
up = 1'b0 ;
down = 1'b0 ;
left = 1'b0 ;
right = 1'b0;
case (scancode)
16'he06b: left = 1'b1;
16'he072: down = 1'b1;
16'he074: right = 1'b1;
16'he075: up = 1'b1;
default:;
endcase
end
endmodule
问题:求出四个输入中最小的值,将最小值赋值给输出。
注意:输入的信号位宽是8位。
module top_module (
input [7:0] a, b, c, d,
output [7:0] min);
wire [7:0] inter1;
wire [7:0] inter2;
assign inter1 = a > b ? b : a ;
assign inter2 = c > d ? d : c ;
assign min = inter1 > inter2 ? inter2 : inter1;
endmodule
奇偶校验(Parity Check)是一种校验代码传输正确性的方法。根据被传输的一组二进制代码的数位中“1”的个数是奇数或偶数来进行校验。通过递逐位异或就可以判断出信号中“1”是奇数还是偶数。
module top_module (
input [7:0] in,
output parity);
assign parity = ^in;
endmodule
我们还可以使用&a判断信号a是不是最大,通过|a判断a信号是不是最小。
问题:递减符&、|、^构建一个有100位宽的输入信号的组合电路。
module top_module(
input [99:0] in,
output out_and,
output out_or,
output out_xor );
assign out_and = ∈
assign out_or = |in;
assign out_xor = ^in;
endmodule
问题:使用for循环语句对100位宽的向量位逆序排列,代码如下。
module top_module(
input [99:0] in,
output [99:0] out);
integer i;
always@(*)begin
for(i=0;i<100;i=i+1)begin
out[99-i] = in[i] ;
end
end
endmodule
问题:统计信号中位为1的个数。
module top_module(
input [254:0] in,
output [7:0] out);
integer i;
always@(*)begin
out = 7'd0;
for(i=0; i<255; i = i+1)begin
if(in[i] == 1'b1)begin
out = out + 1'd1;
end
end
end
endmodule
问题:使用for循环创建100位的多位加法器,有进位信号。在2位的全加器中,要考虑有没有进位,如表2所示。
module top_module(
input [99:0] a, b,
input cin,
output [99:0] cout,
output [99:0] sum);
integer i;
always @(*)begin
for(i = 1;i<100;i++)begin
sum[i] = a[i] ^ b[i] ^ cout[i-1];
cout[i] = a[i] & b[i] | a[i] & cout[i-1] | b[i] & cout[i-1];
end
end
assign sum[0] = a[0] ^ b[0] ^ cin ;
assign cout[0] = a[0] & b[0] | a[0] & cin | b[0] & cin;
endmodule
问题:实例化多个BCD模块,使用generate和for循环实现。
注意:generate 需要使用genvar类型变量。在for循环中begin后需要加上标识符比如gen,其他标识符也可以,不然会报错。
module bcd_fadd (
input [3:0] a,
input [3:0] b,
input cin,
output cout,
output [3:0] sum );
module top_module(
input [399:0] a, b,
input cin,
output cout,
output [399:0] sum);
wire [100:0] cin_inter;
genvar i ;
generate
for(i=1;i<=100;i=i+1)
begin : gen
bcd_fadd u_bcd_fadd (
.a (a[(i*4-1)-:4]) ,
.b (b[(i*4-1)-:4]) ,
.cin (cin_inter[i-1]),
.cout (cin_inter[i]) ,
.sum (sum[(i*4-1)-:4]));
end
endgenerate
assign cin_inter[0]=cin ;
assign cout =cin_inter[100];
endmodule
以上就是verilog进阶使用的例子、代码、仿真。后期文章将延续使用HDLbits网站内容,做为课程的内容来源。后期将推出verilog中的组合电路,敬请期待。谢谢你的观看!