我应该算是一个硬件工程师,涉及比较底层的技术。从原理图上放电阻,到设计PCB和写PCB设计任务书,到焊接调试电路板,再到写设备驱动,写FPGA程序,写各类文档,写项目申请书,所有的事情都干过。使用Verilog也有一些年头了,虽然只占整个工作的一部分,但是相对于其他事情,写Verilog程序、仿真和在FPGA中跑起来应该是我更喜欢做的事情。在这个过程中也积累了不少经验,有了一些个人的见解。
工作挺累的,但还是想把这些东西写出来,与大家分享,也有机会听听大家的意见。
本来想先从Verilog语法开始写,不过这类资料、文章、讨论、帖子恐怕比我敲过的代码还要多。所以还是以个人的经验和见解为主吧,以后这一系列的文章也是这样。既然以个人经验和见解为主,就不涉及“道路”之争,对错之争,只是把他们抛出来,让大家有所借鉴,或加以指点。所以还是回到本文的主题——代码风格。
代码风格的主要要素有:命名、括号的使用、疏密的控制、缩进、注释。代码风格首先为可读性服务,其次还是为可读性服务。至于美观,我想看着美的程序,大多数可读性都不差。
1. 命名
在程序中,凡是用户自己定义、设计、例化的东西都需要命名,比如变量、module、task、function等等。说起命名这件事儿,第一条不能违背的规则就是:不要使用拼音,不要使用拼音!!!
这倒不是咱觉得英文高大上,而是语言之间存在的天然的区别。到目前为止,世界上几乎所有被广泛使用的和被在一定范围内使用的编程语言都是基于英文的。这是历史原因决定的,这是工业革命、电子技术革命、信息革命发生在英语国家决定的。这些编程语言都只接受字母作为输入,不接受汉字。中文是象形文字的巅峰,但是在表音方面并不擅长,当命名数多以后会非常混乱。如果哪天有一种编程语言是基于中文的,那时候用汉字来命名变量将是一件非常惬意的事情。
每个人都有自己的命名偏好,以下是我自己在Verilog程序中遵循的规则和建议。
1.1 不要使用拼音,不要使用拼音!!!
我不只一次见到过使用拼音定义变量和module的源程序,为了探究shuzhi、jieguo、chengji、wendu、shidu、du、mi......到底是什么意思,花了很多时间在翻看上下文、查询电话号码、打电话和等待上面。中文并不是擅长表音的文字,所以shuzhi到底是“数值”还是“树脂”,抑或是“竖直”?chengji到底是“成绩”还是“乘积”?如果遇到个普通话不标准的,打错了zh、ch、sh、s的,或者干脆上方言的,为此而消耗的精力都够写封辞职信然后再找份工作了。
一个复杂点的设计,如果一个月不碰,再看自己的代码都不一定很快看懂。何况使用这种命名方法的程序呢?
1.2 使用合理的命名规则
至少在一个项目中要使用相同的命名规则。我一般采用的命名规则如下。
1.2.1 变量
对于变量,我始终使用小写英文单词、数字和下划线的组合。涵义简单的变量就直接使用单词或单词的简写表示,涵义比较复杂的,会使用单词或单词简写+下划线的方式。比如在我曾经的项目中,设备中有驱动单元和比较单元,驱动单元负责产生时序和格式复杂的向量,比较单元负责按照一套预下载的动态规则对采样数据进行处理。我人为,绝大部分命名和缩写行为都是和项目的实际情况相关的,不能完全孤立的讨论这个问题。在该项目中,我的很多变量名如下。
drv_strength、cmp_thresh、cur_cmp_sts、rdy4rd、rdy4wr、pat_start、event_pos、hmc7044_wr、hmc7044_sdo......
围绕命名,我们将从下面几个角度来讨论。
(1)通用的单词简化
通用的单词简化我会在绝大部分项目中使用,即使和我其他的一些缩写产生冲突,也会维持通用缩写不变。比如下表这些常用的缩写。
完整的单词 | 缩写后的单词 |
---|---|
reset | rst |
asynchronous | async |
status | sts |
valid | vld |
write | wr |
read | rd |
package | pkg |
(2)与项目相关的单词的简化
这类简化与项目的应用背景相关。这不意味着这种简化只能在本项目中用,只是表明该缩写与应用背景的关系更密切而已。比如drv、drv_、cmp、cmp_、pat、pat_等等,这些缩写和命名我会贯彻整个项目,如果允许的话,在别的项目中也可以这么使用。
(3)前缀和后缀
前缀和后缀可以增强代码的可读性,具有以下作用。
- 通过前缀和后缀可以对该变量所属的功能模块一目了然;
- 可以非常便捷的为特定信号施加时序约束。
比如我前面的例子,驱动时序、驱动格式、驱动向量都会冠以drv_前缀;比较时序、比较格式、比较向量都会冠以cmp_前缀。在配置寄存器译码模块中,所有寄存器我都会使用cfg_前缀。需要跨越异步时钟域的变量,我一般会使用async_前缀。对于async_开头的信号,在时序约束时我可以更容易的获得这些对象:
set_max_delay -from [get_cells -hier -regexp {ConfigRegs_i/.*_reg}] -to [get_cells -hier -regexp {StoreCtrl_i/async_.*}] 40
还有一些特殊的功能性寄存器变量,比如状态机,我一般会使用s_和nxt_s前缀;模块内寄存器类型的信号使用_r后缀;用于延时的信号会使用_dly后缀......
(4)单词缩写的规则
单词缩写并不是随意进行的。需要或者可以进行缩写的单词绝大部分都是多音节单词,比如drive、read、write、timing、format、control、combination等等。
一般而言,缩写多音节单词时取其主要的、有代表性、歧义少的辅音,忽略掉元音。比如drive-->drv、comparator-->cmp、format-->fmt、package-->pkg、control-->ctrl、temporary-->tmp......
或者取该单词比较特殊的一部分,比如write-->wr、combination-->comb、temperature-->temper、voltage-->vol......
一些短语,可以直接取每个单词的首字母,比如over temperature protection-->otp、large vector memory-->lvm、dynamic reconfiguration ports-->drp......
一些连接词,可以根据习惯进行简化和替换,比如ready for start-->rdy4start、binary to complement-->bin2comp......
(5)在整个项目中保持命名方式的一致性
命名需要遵守一定的规则,也应该根据实际情况进行变通。只要大的原则不出错,整个命名体系不会出大的问题。在这一前提下,最重要的就是在一个项目中,或者至少在一个大的功能模块内保持命名方式的一致性。这样不管自己还是其他人,在看代码的时候,只要理解和掌握这种规则,就可以比较顺畅的阅读。
1.2.2 module
很长一段时间,我在命名module时都使用和命名变量同样的规则。后来随着项目规模变大,个人觉得IDE环境里面的一堆全部由小写字母组成的层次结构难以区分,所以采用了帕斯卡命名法。我的规则是,module名字全部使用帕斯卡命名法。当这些module在例化时,全部遵循变量命名的规则,并且添加_i后缀。
这样,当我看到VecDecode、PhysInf、DelayAdjust等名字时就能明白这是module,看到vec_decode_i、phys_inf_i、delay_adjust_i时就知道这是前述module的实例。
这部分只是个人习惯。
1.2.3 参数定义
我一般使用三种参数定义,parameter、localparam、`define。这三种定义中我全部使用大写字母,各部分间使用下划线连接,其他方面遵循变量的命名规则。
2. 括号的使用
以下也是个人使用括号的习惯和遵守的规则。
- 括号中只有单一变量时,变量和括号之间添加空格,如if( rst );
- 括号中有表达式时,操作符和变量间高优先级的运算,在不影响阅读的情况下不加空格,低优先级的加空格,如
if( a+b <= c );
- 括号中表达式较长时,适当使用括号增强可读性,如
if( (st_cnt!=store_depth) && (
(bstpd_pre && (dly_cnt==store_delay) && cmp_vld_outter[2]) ||
(bstf_pre && cmp_vld_outter[2]) ||
(stf_pre && cmp_vld_outter[2]) )
)
- 同一层次的括号尽量对齐。
3. 疏密的控制
代码必须疏密得当。这一半依赖编辑器,一半靠自己。太密了,代码挤作一团,不好读,看着还烦躁;太疏了,就常常在关键部分断片儿,就像电视剧和小说常在关键位置翻篇儿。所以选择一个适当的编辑器,在配合适当的换行,才能让代码更易读。
由于写代码只是诸多工作的一部分,至少目前看没有必要学习vim。我用的是sublime,个人觉得入门级里面算挺不错的。疏密的控制主要通过换行实现,我一般遵从以下原则。
(1)信号定义时,除了关系非常紧密的信号外,其他每定义一个信号都会换行。
(2)同一个功能或同一个模块的信号集中定义,不同功能或不同模块间插入一定的特殊间隔符。
(3)当信号数量非常多时,在适当的位置(比如不同的功能之间)除了插入一定的特殊间隔符外,在插入2到3个空行。如以下代码。
//
// HSIO signals
wire [`CH_PER_SUB*`SER_FACTOR-1: 0] format_cmph_o, format_cmpl_o, format_cmph_i, format_cmpl_i;
wire [`CH_PER_SUB*`SER_FACTOR-1: 0] format_drv_i, format_drv_o, format_rcv_i, format_rcv_o;
wire [FINE_DELAY_WIDTH-1: 0] align_value_h, align_value_l, total_value_h, total_value_l;
//
// HSIO pattern map memory ports
wire [`PAT_MAP_MEM_ADDR-1: 0] start_pat_addr;
wire [`PAT_MAP_MEM_ADDR-1: 0] end_pat_addr;
wire start_pat_map_wr;
wire [`PAT_MAP_MEM_ADDR-1: 0] start_pat_map_addr;
wire [`USED_ADDR-1: 0] start_pat_map_data;
wire end_pat_map_wr;
wire [`PAT_MAP_MEM_ADDR-1: 0] end_pat_map_addr;
wire [`USED_ADDR-1: 0] end_pat_map_data;
//
// Pattern memory control signals
wire pat_start;
wire [`PAT_MAP_MEM_ADDR-1: 0] m_addr_start;
wire [`PAT_MAP_MEM_ADDR-1: 0] m_addr_end;
wire match_done;
wire loop_done;
wire ddr4_stop_o, lvm_stop_o;
wire s_wen_a, e_wen_a;
wire [`PAT_MAP_MEM_ADDR-1: 0] s_addr_a, e_addr_a;
wire [`DDR4_MEM_ADDR_WIDTH-1: 0] s_data_a, e_data_a;
wire svm_wen_a;
wire [`SVM_MEM_DEPTH-1: 0] svm_addr_a;
wire [`PAT_WID-1: 0] svm_data_a;
wire vec_rd, vec_rdy;
wire [`PAT_WID-1: 0] vec;
//
// Vector processor signals
wire vec_rdy;
wire [`PAT_WID-1: 0] vec;
wire vec_rd;
wire cmp_vld;
wire [`SER_FACTOR*`CH_PER_SUB-1: 0] cmph_in;
wire [`CH_PER_SUB*`THRESH_NUM-1: 0] cmp_logic;
wire [`CH_PER_SUB-1: 0] user_cmp_mask[`CMP_EDGE_NUM-1: 0];
wire [`STORE_MODE_WID-1: 0] store_mode;
wire [`STORE_DATA_MODE_WID-1: 0] store_data_mode;
wire [`HISTORY_ADDR_WID: 0] store_depth;
wire [`USED_ADDR-1: 0] store_delay;
wire main_cmp_status;
(4)例化的模块前会插入特定的间隔符、说明部分和3行空行,如以下代码。
//
//
// Format instance
Format #(
.FACTOR(`SER_FACTOR),
.CHAN_NUM(`CH_PER_SUB),
.CHAN_WIDTH(CHAN_WIDTH),
.COARSE_DELAY_WIDTH(COARSE_DELAY_WIDTH) )
format_i(
.clk (clk_ls),
.clk_sys (clk150m),
.drv_in (format_drv_i),
.drv_out (format_drv_o),
.rcv_in (format_rcv_i),
.rcv_out (format_rcv_o),
.cmph_in (format_cmph_i),
.cmph_out (format_cmph_o),
.cmpl_in (format_cmpl_i),
.cmpl_out (format_cmpl_o),
.load (bus_hsio_dly.coarse_load),
.delay (bus_hsio_dly.coarse_delay),
.chan_sel (bus_hsio_dly.coarse_chan_sel)
);
//
//
// PhysInf instance
PhysInf #(
.FACTOR(`SER_FACTOR),
.DELAY_WIDTH(FINE_DELAY_WIDTH),
.CHAN_NUM(`CH_PER_SUB),
.CHAN_WIDTH(CHAN_WIDTH) )
phys_inf_i(
.rst (rst_global),
.clk (clk_hs),
.clk_div (clk_ls),
.clk_ref (clk300m),
.clk_sys (clk150m),
.cmph_p (io_cmph_p),
.cmph_n (io_cmph_n),
.cmpl_p (io_cmpl_p),
.cmpl_n (io_cmpl_n),
.rcv_p (io_rcv_p),
.rcv_n (io_rcv_n),
.drv_p (io_drv_p),
.drv_n (io_drv_n),
.drv (format_drv_o),
.rcv (format_rcv_o),
.cmph (format_cmph_i),
.cmpl (format_cmpl_i),
.cmph_delay (cmph_fine_delay),
.cmph_load (cmph_fine_load),
.cmph_chan_sel (cmph_fine_sel),
.cmpl_delay (cmpl_fine_delay),
.cmpl_load (cmpl_fine_load),
.cmpl_chan_sel (cmpl_fine_sel),
.align_value_h (cmph_align_value),
.total_value_h (cmph_total_value),
.align_value_l (cmpl_align_value),
.total_value_l (cmpl_total_value),
.drv_delay (drv_fine_delay),
.drv_load (drv_fine_load),
.drv_chan_sel (drv_fine_sel),
.rcv_delay (rcv_fine_delay),
.rcv_load (rcv_fine_load),
.rcv_chan_sel (rcv_fine_sel),
.rst_complete ()
);
(5)当每行代码过多时进行换行处理,换行后的内容尽量对齐。可以参考“括号的使用”章节的例子。
(6)代码中比较关键的小段程序,或者关系紧密的一段程序可以写的比较紧密,这样不会打断看代码的思路;关系不是很紧密的部分可以像第(4)条一样添加一定的间隔。比如以下代码是我的某个程序中的第四级流水线的操作,该部分关系紧密,所以仅在不同块之间插入一个空行。
//
//
// Pipeline 4
(* USE_DSP = "yes" *) logic [`USED_ADDR-1: 0] dly_cnt;
logic [`HISTORY_ADDR_WID: 0] st_cnt;
always_ff @ (posedge clk) begin
if( rst ) dly_cnt <= {`USED_ADDR{1'b0}};
else begin
if( bstpd_pre && cmp_vld_outter[2] && (dly_cnt!=store_delay) ) dly_cnt <= dly_cnt+1;
end
end
always_ff @ (posedge clk) begin
if( rst ) st_cnt <= {(`HISTORY_ADDR_WID+1){1'b0}};
else begin
if( (st_cnt!=store_depth) && (
(bstpd_pre && (dly_cnt==store_delay) && cmp_vld_outter[2]) ||
(bstf_pre && cmp_vld_outter[2]) ||
(stf_pre && cmp_vld_outter[2])) ) begin
st_cnt <= st_cnt+1;
end
end
end
always_ff @ (posedge clk) begin
cmp_vld_outter[3] <= cmp_vld_outter[2];
cmph_in[3] <= cmph_in[2];
cmp_logic[3] <= cmp_logic[2];
cmp_mode[3] <= cmp_mode[2];
pat_label[3] <= pat_label[2];
if( (st_cnt!=store_depth) && stf_pre && cmp_vld_outter[2] ) stf <= 1'b1;
else stf <= 1'b0;
if( (st_cnt!=store_depth) && bstf_pre && cmp_vld_outter[2] ) bstf <= 1'b1;
else bstf <= 1'b0;
if( (st_cnt!=store_depth) && bstpd_pre && (dly_cnt==store_delay) && cmp_vld_outter[2] ) bstpd <= 1'b1;
else bstpd <= 1'b0;
end
(7)减少不必要的begin和end。比如if、else、elseif块中不包含多条语句,那么不必要写出该块的begin和end。
(8)即使在一个程序块中,也应该在适当的位置插入空行或者插入一行注释,避免代码过于密集。
4. 缩进
缩进在任何程序代码中都非常重要。Verilog中最常用的缩进量是4个空格,当然也有使用2个空格的。个人喜欢使用4个空格。当程序比较复杂以后,2个空格缩进的程序可读性会降低。层次比较多的时候,如果使用2空格缩进,需要使用更多的精力努力识别层次。
另外,在变量定义、赋值的时候,赋值操作符右侧的变量最好对齐。这样做首先让代码更美观,其次需要进行列选中和列操作时更加方便。可以参考上文中的代码。
例化module时,为端口赋值时,所有的括号最好都对齐,理由同上。可以参考上文代码中对Format和PhysInf的例化。
5. 注释
注释的重要性不再多言。这里列一下个人写注释的习惯。
//
// Company:
// Engineer:
// Yinye
// Create Date: 2020/04/04 01:04:59
// Design Name:
// Module Name: CmpProcess
// Project Name: DIO_CARD
// Target Devices: XCVU3P
// Tool Versions:
// Description:
// This module implements the comparator function.
// Dependencies:
// None
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 1. Assume that the 'PhysInf' has complemented the delay from stimulus to the
// acquisition to a integer value. So the delay of the "inner" signals( pat_seq, pat_label) will
// be a integer value too, and the value may be vary in one cycle scope.
// 2. The scope of 'store_delay' is from 0 to max length of a pattern. Because the 'store_delay'
// could be 0, so remove the 'bstp' command. 'bstp' can be merged to 'bstpd'.
//
首先,每个module都应该有上述的文件头,用来描述设计人员、时间、名称、工程名、目标器件(可选)、描述、其他说明等等内容。
关键程序段、关键变量都应该有注释,有的还需要详细说明这样设计的原因。
当然,对于更复杂、更关键、更难的部分,应该写单独的说明文档。
关于代码风格暂时就写这么多,如果有新的想法或者遗漏再做修改。希望这些内容对看的人有所帮助,也欢迎提出意见和建议。