Verilog编写的注意事项
时间 | 版本 | 内容 |
---|---|---|
2024/03/19 | V0 | 初版 |
根据写Verilog的经验记录一些注意事项,以减少产生错误的可能性。
1. 对于任何非1 bit宽的信号都要确保进行了定义
Verilog是容许对信号不声明的,这种隐含声明会认为信号是1 bit wire信号。“wire” 信号这点基本所有人都知道,但是“1 bit” 是容易忽略的。
1.1. 例子
wire [N:1] fifo_empty;
Chn_read_fifo_multi #(
.N (N),
.OFFSET (OFFSET)
) u_Chn_All9_Readout (
.SiTCP_CLK (clk_125M),
.dout (dout),
.rdclk (clk_syn),
.rst_n (GLB_RST_n),
.len_data (len_data),
.rd_en (rd_en),
.fifo_dout_pack (fifo_dout_pack),
.tsm_done (tsm_done),
.wr_done (wr_done),
.fifo_full (fifo_full),
.fifo_empty (fifo_empty)
);
fifo_combine u_fifo_combine (
.LVDS_CLK0_P (LVDS_CLK0_P),
.LVDS_CLK0_N (LVDS_CLK0_N),
.SiTCP_CLK (clk_125M),
.rst_n (rst_n),
.fifo_dout_pack (fifo_dout_pack),
.fifo_empty (fifo_empty),
.tsm_done (tsm_done),
.wr_done (wr_done),
.SFP_RX_P (SFP_RX_P),
.SFP_RX_N (SFP_RX_N),
.chn_en (chn_en),
.rd_en (rd_en),
.SFP_TX_P (SFP_TX_P),
.SFP_TX_N (SFP_TX_N)
);
注意fifo_empty
信号,它是一个实例的输入又是另一个实例的输出,因此不用从外部驱动,即使不定义检查器也不会报错。
但是假如没有wire [N:1] fifo_empty
的定义,fifo_empty
就会被误解为1 bit信号,仿真时候会发现fifo_empty[1]
正常但其余全为Z
,而这是不合理的。
2. Verilog中使用除法计算参数时请格外小心
Verilog是类C语言。C语言中关于整数除法实际上是定义为整数除法取除数的行为的,因为两整数int类型运算后必然也是int类型(除非进行类型转换)。
显然,在C语言中3/2
结果是1
,3%2
结果是1
。
在Verilog中,除和取余运算也都存在。取余时候不会犯错,但是算除法时极易忘记结果是整型这一事实。
2.1. 例子
以下是一个常见于tb中的产生时钟的代码,参数用于控制时钟周期。以下的代码中时钟希望是1 GHz。
parameter PRD_CLK_DLL = 1.0; // 1G sample rate
initial begin
clk_DLL = 0;
forever #(PRD_CLK_DLL/2)
clk_DLL = ~clk_DLL;
end
但是请注意一定不能写作PRD_CLK_DLL = 1;
,因为1/2
结果是0
。这会使得仿真时进入死循环而报错。写1
缺省类型指定为integer
,但是如果写1.0
缺省类型就会指定为real
。这也是C语言中常用的技巧。
还有一种可能的写法是parameter real PRD_CLK_DLL = 1;
,将参数定义为实数而非整数,不过我没有试验过。
3. 调用含参模块的格式
注意参数在实例名称之前,格式参考以下例子。
3.1. 例子1:简单传参
按照定义参数顺序传参。
Chn_read_fifo_multi #(9,0) u_source(
.SiTCP_CLK (SiTCP_CLK),
.dout (dout),
.rdclk (clk_syn),
.rst_n (rst_n),
.len_data (len_data),
.rd_en (rd_en),
.fifo_dout_pack (fifo_dout_pack),
.tsm_done (tsm_done),
.wr_done (wr_done),
.fifo_full (fifo_full),
.fifo_empty (fifo_empty)
);
3.2. 例子2:参数名称传参
按照名称传参。注意,还有其他方法传参,这两种方法是ANSI风格的。
Chn_read_fifo_multi #(
.N (N),
.OFFSET (OFFSET)
) u_Chn_All9_Readout (
.SiTCP_CLK (clk_125M), // SiTCP CLK 125MHz,also for SiTCP FIFO,RBCP
.dout (dout),
.rdclk (clk_syn),
.rst_n (GLB_RST_n),
.len_data (len_data),
.rd_en (rd_en),
.fifo_dout_pack (fifo_dout_pack),
.tsm_done (tsm_done),
.wr_done (wr_done),
.fifo_full (fifo_full),
.fifo_empty (fifo_empty)
);
4. TB中可以多使用force
TB的语法是自由的,不用考虑综合性。使用force
可以不用在乎某个信号的类型和原本的值而强行赋值。反向操作是release
。
5. 信号是允许跨层调用的
信号跨模块层级引出并不一定非要用input/output来进行。也可以直接调用模块内部信号。
5.1. 例子
用.
符号来表征层级关系。
initial force uut.u_fifo_combine.SiTCP_CLK = uut.clk_125M;
initial force uut.u_fifo_combine.LVDS_CLK0_P = uut.clk_125M;
initial force uut.u_fifo_combine.LVDS_CLK0_P = ~uut.clk_125M;
6. TB中使用\$stop好过使用$finish
$finish
的含义是 “结束仿真”,所以理论上是会退出仿真器的(虽然很多仿真器会二次确认是否退出)。而$stop
只是单纯的指 “仿真时间停止”,更符合TB的需求。
7. generate语句需要命名
不知道是否所有检查器都会检查这一条。
generate...for
之后紧跟的begin...end
是需要命名的。
7.1. 例子
generate
genvar i;
for (i = 1; i <= 9 ; i = i + 1)
begin : gen_Chn_read_fifo
Chn_read_fifo u_Chn_read_fifo(
.SiTCP_CLK (SiTCP_CLK),
.sca_data (dout[i]),
.sca_rdclk (rdclk),
.rst_n (rst_n),
.len_data (len_data[8:1]),
.rd_en (rd_en[i]),
.fifo_dout (fifo_dout_pack[8*i -: 8]),
.tsm_done (tsm_done[i]),
.wr_done (wr_done[i]),
.fifo_full (fifo_full[i]),
.fifo_empty (fifo_empty[i])
);
end
endgenerate
之所以需要这么做,是因为寻找实例时需要一个名字来区分generate
产生的并联模块。比如假如要寻找以上generate
语句的LSB模块,名称为<top>.gen_Chn_read_fifo[1].u_Chn_read_fifo
(<top>
代表更高层的模块实例)。
如果没有起名字,也许系统也会自动起一个名字?但是规范起见应该自己起个名。
从这个角度看,generate...if
可能不需要命名?但是没用过这个语句,无法确定。