😃 内容生长中......
😋 个人学习笔记,如有错误请大佬指出。
基础语法
基础线路
连线
以模块为单元,建立元件和连线:
// eg1:连线
module Grammer(input in1, in2, output out1, output out2);
wire a, b; //使用wire创建连线
wire c;
assign a = in1; // assign 是连线操作/信号传递,并不产生新的线
assign b = in2; // 注意input, output即input wire, output wire, 所以也产生线
assign c = a + b; // 加法器
assign {out1, out2} = {c, b}; //已知位宽时可以一次性赋值
endmodule
综合之后为:

如果不定义线a,b,c的话呢?
// eg2:看连线的区别
module Grammer(input in1, in2, output out1, output out2);
assign out1 = in1 + in2; // 加法器,但是直接用输入加
assign out2 = in2;
endmodule

然后少了几个缓冲器(buf),这是为什么呢?Q1
非门
使用assign但是传递反相信号:
// eg3:使用非门
module Learn_Verilog(input in1, in2, output out1, output out2);
assign out1 = !in1;
endmodule
与门
还是assign,但是注意根线不能等于不同的信号叠加方案,比如a不能即等于b又等于c;
// eg4:使用与门
module Learn_Verilog(input in1, in2, output out1, output out2);
assign out1 = !in1;
assign out2 = in1&in2; //与门不是加法器,所以不能是加号
endmodule

其他门
assign out = !(in1|in2); // 或非门 NOR
assign out = !(in1+in2); // 异或非门 XNOR,in1+in2 是异或门,因为是线加线
综合练习
// eg5:综合练习
module Learn_Verilog(
input a,
input b,
input c,
input d,
output out,
output out_n );
wire a_n_b = a & b;
wire c_n_d = c & d;
wire anb_o_cnd = a_n_b | c_n_d;
assign out = anb_o_cnd;
assign out_n = !anb_o_cnd;
endmodule

向量
向量也可以叫总线,但是注意声明方式不同于C语言,这里先位宽再名称:
// eg5:总线,向量
module Learn_Verilog(
input wire [2:0] vec,
output wire [2:0] outv,
output wire o2,
output wire o1,
output wire o0 ); // Module body starts after module declaration
assign o0 = vec[0];
assign o1 = vec[1];
assign o2 = vec[2];
assign outv = vec;
endmodule
隐式声明
wire [2:0] a, c; // Two vectors
assign a = 3'b101; // a = 101
assign b = a; // b = 1 implicitly-created wire
assign c = b; // c = 001 <-- bug
my_module i1 (d,e); // d and e are implicitly one-bit wide if not declared.
// This could be a bug if the port was intended to be a vector.
即assign语句将信号传递给一个没有声明的网络时会隐式声明一个1bit信号;尽管流向这根线的信号是多bit的,最后流出的信号依然是1bit。
字节顺序
如果以[0: 3]声明,选择的时候也得是这个顺序(小在左,大在右),反之亦然;
拼接组合
{3'b111, 3'b000} => 6'b111000
{1'b1, 1'b0, 3'b101} => 5'b10101
{4'ha, 4'd10} => 8'b10101010 // 4'ha and 4'd10 are both 4'b1010 in binary
模块和层级
在一个模块中使用其他模块,有点像面向对象的方法:
// eg6:层级
module Learn_Verilog(
input in1, in2,
output out1, out2) ;
mod_a ymod_a_1(in1, in2, out1);
mod_a ymod_a_2(
.mod_a_in1(in1),
.mod_a_in2(out1),
.mod_a_out(out2)
);
endmodule
由于TD的图太小,以后改用quartus综合:

Always
always语法:
always @ (event) begin
[multiple statements]
end
其中event是敏感信号,也就是敏感信号发生变化或者event发生就会触发[multiple statements]
执行,在组合逻辑中可以用到always,时序逻辑中必用到always:
注意always块中的被赋值对象必须是reg型;
// eg7:always
module Learn_Verilog(
input clk,
input in1, in2,
output wire out_assign,
output reg out_always_comb,
output reg out_always_ff );
assign out_assign = in1+in2;
always @(*) out_always_comb = in1+in2;
always @(posedge clk) out_always_ff = in1+in2;
endmodule

值得注意的是,这里如果用a+b
和a^b
实现异或,出来结构不一样(下面是a^b
):

多路选择器——if语句
使用if可以实现选择器的效果:

注意点:
并行块和顺序块都可以写在initial 或 always@ 之后,也就是说写在块中的语句是时序逻辑的对assign之后不能加块,实现组合逻辑只能用逐句的使用assign组合逻辑如果不考虑门的延时的话当然可以理解为瞬时执行的,因此没有并行和顺序之分,并行和顺序是针对时序逻辑来说的。值得注意的是所有的时序块都是并行执行的。initial块只在信号进入模块后执行1次而always块是由敏感事件作为中断来触发执行的。
assign组合逻辑和always@(*)
组合逻辑:
Verilog描述组合逻辑一般常用的有两种:assign赋值语句和always@(\*)
语句。两者之间的差别有:
被assign赋值的信号定义为wire型,被always@(*)结构块下的信号定义为reg型,值得注意的是,这里的reg并不是一个真正的触发器,只有敏感列表为上升沿触发的写法才会综合为触发器,在仿真时才具有触发器的特性。
另外一个区别则是更细微的差别,举个例子:
wire a;
reg b;
assign a = 1'b0;
always@(*) b = 1'b0;
在这种情况下,做仿真时a将会正常为0, 但是b却是不定态。这是为什么?verilog规定,always@(*)
中的*
是指该always块内的所有输入信号的变化为敏感列表,也就是仿真时只有当always@(*)
块内的输入信号产生变化,该块内描述的信号才会产生变化,而像always@(\*) b = 1'b0;
这种写法由于1'b0一直没有变化,所以b的信号状态一直没有改变,由于b是组合逻辑输出,所以复位时没有明确的值(不定态),而又因为always@(\*)
块内没有敏感信号变化,因此b的信号状态一直保持为不定态。事实上该语句的综合结果有可能跟assign一样(本人没有去尝试),但是在功能仿真时就差之千里了。
[原文](verilog 里面,always,assign和always@(*)区别_always @-CSDN博客)
多路选择-case
使用case语句可以实现类似C语言switch的效果,但是不一样的是,C语言是从某句代码往下执行,遇到break跳出,但Verilog每个‘case’只执行一次,所以不需要break。
// eg7:always
module Learn_Verilog(
...
always @(*) begin
case(out_always_ff)
1'b1: out2 = out_assign;
1'b0: out2 = out_always_comb;
1'b1: out2 = 1'b1;
endcase
end
endmodule
综合如下:

注意-其他case
相关陈述:
Verilog 定义了三种 case 描述语句,case,casez 和 casex。其中,casez 和 casex 是支持通配符的。在 casez 和 casex 中,一般又不建议用 casex。
为什么要慎用 casex ?
我们先来看一下基本的 case 语句的写法:
case (case_expression)
case_item_1 : begin
case_statement_1a;
case_statement_1b;
end
case_item_2 : case_statement_2;
default : case_statement_default;
endcase
我们知道 casex 会把 “z”和 “x”看作“don't care”。但 注意, 不只是 case_item 的“z”和 “x”, case_expression 中的“z”和“x”也会被看作“don't care”。(在 case_item 中用通配符,我们一般又写作“?”)
这会有什么问题呢?我们通过一个例子来看一下。
always @(irq) begin
{int2, int1, int0} = 3'b000;
casex (irq)
3'b1?? : int2 = 1'b1;
3'b?1? : int1 = 1'b1;
3'b??1 : int0 = 1'b1;
default: {int2, int1, int0} = 3'b000;
endcase
end
假设仿真时输入的 irq 为 3'b00x,casex 将最低位的 x 看作通配符,所以会匹配 case_item 3'b??1,所以 int0=1。但是在实际的芯片中没有x,x 的含义是不确定,也就是说在实际芯片中可能为0也可能为1。假如 irq 在实际芯片中正好是 3'b000,那么 casex 语句便不会匹配 3'??1,这时 int0=0。
这便导致了仿真结果和实际芯片的 mismatch,容易引入 bug。其实 casez 语句也有类似的问题,但是 casez 语句不会把 “x”看作通配符,所以引入 bug 的概率会比 casex 小一些。
[原文](为什么要慎用 casex ? - 知乎 (zhihu.com))
其他特性
三元操作符
(condition ? if_true : if_false)
位运算
&|^
栗子--百输入门电路:
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
注意:
异或门是有奇数个真则为真,偶数个真则为假;
同或门与之相反;
循环
Verilog的循环语句主要有:for循环、while循环、foever循环和repeat循环。
// for语句
for (<initial_condition>; <stop_condition>; <increment>) begin //执行条件判断
/* 更新循环变量,注意只能用类似i=i+1或i=i-1,没有i++, i--的写法 */
//要循环执行的代码
end
// forever语句
forever begin
// 循环执行的语句
end
// repeat语句
repeat (<number>) begin //指定重复次数
//需要重复执行的代码
end
// while语句
while <condition> begin //判断条件
// Code to execute //要重复执行的语句
end
模块测试—Testbench与ModelSim
在 Verilog 中,Testbench是用于对设计进行仿真和验证的代码模块,它通常包含了以下几个部分:
模块声明:Testbench 本身也是一个模块,因此需要使用 module
关键字进行声明。
module tb_my_design;
实例化被测设计:在 Testbench 中需要实例化要测试的设计模块,并连接输入和输出信号。
my_design dut (
.input1(input1_tb),
.input2(input2_tb),
.output1(output1_tb)
);
时钟信号生成:如果设计中包含时钟信号,Testbench 需要生成时钟信号并将其应用到被测设计中。
always #5 clk = ~clk;
输入信号生成:在 Testbench 中需要生成输入信号,并将其应用到被测设计中。例如:
initial begin
input1_tb = 0;
#10;
input1_tb = 1;
// ...
end
输出信号监控:Testbench 需要监控被测设计的输出信号,并可能进行断言(assertion)来验证输出是否符合预期。例如:
always @(posedge clk) begin
if (output1_tb == 1'b1) begin
$display("Output is high at time %t", $time);
end
end
仿真结束条件:在仿真结束时,需要通过某种条件或者时间点结束仿真。例如:
initial begin
#1000; // Run simulation for 1000 time units
$finish; // Finish simulation
end
仿真启动:最后,在 Testbench 的末尾需要启动仿真。例如:
initial begin
$dumpfile("dump.vcd"); // Create a VCD file for waveform viewing
$dumpvars(0, tb_my_design); // Dump variables for waveform viewing
$monitor($time,, "At time %t, input1 = %b, input2 = %b, output1 = %b", $time, input1_tb, input2_tb, output1_tb);
end
其中需要说明的是:
在 Verilog 中,#
符号表示时间延迟。在 always块中,#
后面的数字表示延迟的时间量,单位通常是时间单位(time unit),即以下代码中的设置,这行代码一般写在testbench的头一行。
`timescale time_unit/precision
因此,always #5 clk = ~clk;
这行代码表示,在每次时钟clk
的上升沿到来后,等待 5 个时间单位,然后执行 clk = ~clk;
,从而模拟了一个周期为 10 个时间单位的时钟信号。
Verilog 中的系统任务(System Task)是预定义的任务,用于执行一些特定的系统级操作,例如输出调试信息、控制仿真过程等。以下是一些常见的 Verilog 系统任务:
$display:用于在仿真过程中输出调试信息。语法:$display(format_string, expression1, expression2, ...);
$monitor:类似于 $display
,用于在仿真过程中输出调试信息,但它会自动监视指定的变量,当变量的值发生变化时自动输出。语法:$monitor(format_string, expression1, expression2, ...);
$finish:用于结束仿真过程。语法:$finish;
$stop:用于暂停仿真过程。语法:$stop;
$time:用于获取当前仿真时间。语法:$time;
$random:用于生成伪随机数。语法:$random;
$strobe:用于在波形查看器中生成一个信号脉冲。语法:$strobe(signal, delay);
$fdisplay:类似于 $display
,但将输出写入文件而不是控制台。语法:$fdisplay(file, format_string, expression1, expression2, ...);
$fwrite:类似于 $fdisplay
,但允许更灵活的输出格式。语法:$fwrite(file, format_string, expression1, expression2, ...);
$fscanf:从文件中读取格式化的输入。语法:$fscanf(file, format_string, variable1, variable2, ...);
这些系统任务在 Verilog 中都有特定的用途,可以帮助进行调试、控制仿真过程以及进行文件 I/O 等操作。
在写完testbench之后,可以使用ModelSim中或是其他工具仿真:
问题列表
同样的逻辑,用wire建立几根线中继和直连为什么多了几个缓冲(buf),为什么模块input直连output还是又缓冲(buf)?
在Quartus中尝试后发现两个其实是一样的:
2.