集成电路技术分享

 找回密码
 我要注册

QQ登录

只需一步,快速开始

搜索
查看: 1522|回复: 2

Verilog学习心得

[复制链接]
fpga_feixiang 发表于 2018-9-19 16:22:46 | 显示全部楼层 |阅读模式
因为Verilog是一种硬件描述语言,所以在写Verilog语言时,首先要有所要写的module在硬件上如何实现的概念,而不是去想编译器如何去解释这个module. 比如在决定是否使用reg定义时,要问问自己物理上是不是真正存在这个register, 如果是,它的clock是什么? D端是什么?Q端是什么?有没有清零和置位?同步还是异步?再比如上面讨论的三态输出问题,首先想到的应该是在register的输出后面加一个三态门,而不是如何才能让编译器知道要“赋值”给一个信号为三态。同样,Verilog中没有“编译”的概念,而只有综合的概念。

      写硬件描述语言的目的是为了综合,所以说要想写的好就要对综合器有很深的了解,这样写出来的代码才有效率。

      曾经接触过motorola苏州设计中心的一位资深工程师,他忠告了一句:就是用verilog描述电路的时候,一定要清楚它实现的电路,很多人只顾学习verilog语言,而不熟悉它实现的电路,这是设计不出好的电路来的.

      一般写verilog code时,对整个硬件的结构应该是很清楚了,最好有详细的电路图画出,时序问题等都应该考虑清楚了。可以看着图直接写code。

       要知道,最初Verilog是为了实现仿真而发明的.不可综合的Verilog语句也是很重要的.因为在实际设计电路时,除了要实现一个可综合的module外,你还要知道它的外围电路是怎样的,以及我的这个电路与这些外围电路能否协调工作.这些外围电路就可以用不可综合的语句来实现而不必管它是如何实现的.因为它们可能已经实际存在了,我仅是用它来模拟的.所以,在写verilog的时候应该要先明确我是用它来仿真的还是综合的.   

       要是用来综合的话,就必须要严格地使用可综合的语句,而且不同的写法可能产生的电路会有很大差别,这时就要懂一些verilog综合方法的知识.就像前面说的,脑子里要有一个硬件的概念.特别是当综合报错时,就要想一想我这种写法能不能用硬件来实现,verilog毕竟还不是C,很多写法是不可实现的.要是这个module仅是用来仿真的,就要灵活得多了,这时你大可不必太在意硬件实现.只要满足它的语法,实现你要的功能就行了.

      有网友说关于#10 clk=~clk的问题,虽然这种语句是不可综合的,但是在做simulation和verification是常常用它在estbench中来产生一个clock信号。再比如常常用到的大容量memory, 一般是不会在片上实现的,这个时候也需要一个unsynthesizable module. mengxy所言切中肯罄。

      我们设计的module的目的是为了可以综合出功能正确,符合标准的电路来。我想这是个反复的过程,就像我们在写design flow中总要注明前仿真,综合后的仿真,以及后仿真等。仿真是用来验证我们的设计的非常重要的手段。而verilog里那些看是无聊的语句这个时候就会发挥很大的作用。我想,用过verilog_xl的兄弟应该深有体会。verilog_xl里的操作,可以用verilog里的系统命令来完成。通过最近的应聘我也深有体会,很多公司看中你在写code时,是否考虑到timing,
architecture,DFT等,这也说明verilog中的任何语句都非常重要的。
     要写代码前必须对具体的硬件有一个比较清晰的概念但是想一次完成可综合代码就太夸张了,verilog的自顶向下设计方法就是从行为建模开始的,功能验证了以后再转向可综合模型.太在意与可综合令初期设计变得太累
      很同意这种看法,在做逻辑结构设计时,综合的因素是要考虑的,但是有很多东西不能考虑的过于细致,就是在设计的时候不能过于紧卡时延,面积等因素,因为这样以来综合后优化的余量就会很小,反而不利与设计的优化,如果在时延和面积要求不是很紧张的情况下,其实代码写的行为级,利用综合工具进行优化也是一种方法。偶就听说有一家很有名的公司,非常相信综合工具的优化能力,从来不作综合后仿真的,hehe.当然,如果面积和时延的要求很高,最好还是把代码写的底层一点,调用库单元时,也要充分考虑其面积和时延的因素。

Verilog与C++的类比
1. Verilog中的module对应C++中的class。它们都可以实例化。例如可以写一个FullAdder module,表示全加器这种器件。
module FullAdder(a, b, cin, sum, cout);
  input a, b, cin;
  output sum, cout;  

  assign {cout, sum} = a + b + cin;
endmodule  

然后在执行8-bit补码加减运算的ALU module中实例化8个FullAdder,表示ALU用到了8个FullAdder。

module ALU(a, b, result, cout, is_add);
  input[7:0]  a, b;
  input       is_add;
  output[7:0] result;
  output      cout;  

  wire[7:0] b_not = ~b;
  wire[7:0] b_in = is_add ? b : b_not;
  wire[7:0] carry;
  
  assign carry[0] = is_add ? 1'b0 : 1'b1;

  // module 实例化
  // 8-bit ripple adder
  FullAdder fa0(a[0], b_in[0], carry[0], result[0], carry[1]);
  FullAdder fa1(a[1], b_in[1], carry[1], result[1], carry[2]);
  FullAdder fa2(a[2], b_in[2], carry[2], result[2], carry[3]);
  FullAdder fa3(a[3], b_in[3], carry[3], result[3], carry[4]);
  FullAdder fa4(a[4], b_in[4], carry[4], result[4], carry[5]);
  FullAdder fa5(a[5], b_in[5], carry[5], result[5], carry[6]);
  FullAdder fa6(a[6], b_in[6], carry[6], result[6], carry[7]);
  FullAdder fa7(a[7], b_in[7], carry[7], result[7], cout);
endmodule  

对应在C++中先写FullAdder class,然后在ALU class中以FullAdder作为data member。

class FullAdder
{
};  

class ALU
{
  FullAdder fa[8];
};
另外一点,moudle声明port的方式,像是从早期C语言的函数定义中学来的:
char* strcpy(dst, src)
    char* dst;
    char* src;
{
    // ...
}


2. Verilog中的模块调用时,指定端口可以使用名称绑定。C++在调用函数时,参数只能按顺序书写。例如memset()的原型是:

void *memset(void *s, int c, size_t n);


如果你想将某个buf清零,应该这么写:
char buf[256];
memset(buf, 0, sizeof(buf));

但是如果你不小心写成了:
memset(buf, sizeof(buf), 0);  

编译器不会报错,但运行的实际效果是根本没有对buf清零。(记得Richard Stevens的书里提到过这一点。)  

在Verilog中,如果要写一个测试ALU的module,那么其中对ALU实例化的指令可以这么写:

module alu_test;
reg[8:0] a_in, b_in;
reg   op_in;
  
wire[7:0] result_out;
wire      carry_out;
  
ALU alu0(.a(a_in[7:0]), .b(b_in[7:0]), .is_add(op_in),
      .result(result_out), .cout(carry_out));
    // ...
endmodule   
这样就比较容易检查接线错误。

另外,在C++中,如果所有参数类型不同,而且之间没有隐式类型转换,那么可以利用C++的强类型机制在编译期检查出这种调用错误。

3. Verilog中把大括弧{}用作bit的并置,因此语句块要用begin/end标示。Verilog中小括号()和中括号[]的作用与C++中类似,前者用于函数或模块调用,后者用于下标索引。我想如果Verilog把尖括号<>用作bit并置的话,就能把大括号{}解放出来,用作标示语句块,这样写起来更舒服一些。

4. Verilog本质上是测试驱动开发的。对于每个module都应该有对应的test bench(或称test fixture)。比较好的情况是,一个工程师写module,另一个工程师写对应的testbench,这样很容易检查出对电路功能需求理解不一致的地方。因此还可以说Verilog主张结对编程(pair programming)。例如对前面的ALU module的test bench可以写成:
`timescale 1ns / 1ns  
module alu_test;
  
reg[8:0] a_in, b_in;
reg   op_in;
  
wire[7:0] result_out;
wire      carry_out;

ALU alu0(.a(a_in[7:0]), .b(b_in[7:0]), .is_add(op_in),
          .result(result_out), .cout(carry_out));

reg[9:0] get, expected;
reg   has_error;
  
initial begin
  has_error = 1'b0;

  op_in = 1'b1; // test addition
   
  for (a_in = 9'b0; a_in != 256; a_in = a_in + 1)
      for (b_in = 9'b0; b_in != 256; b_in = b_in + 1) begin
       #1;
       get = {carry_out, result_out};
       expected = a_in + b_in;
       if (get !== expected) begin
        $display("a_in = %d, b_in = %d, expected %d, get %d",
                 a_in, b_in, expected, get);
        has_error = 1'b1;
       end
      end
   
  op_in = 1'b0; // test subtraction
   
  // ...
  if (has_error === 1'b0) begin
   $display("ALL TESTS PASSED!");
  end
  $finish;
end
endmodule

5. Verilog比起VHDL的不足之处在于,它只能定义concrete class,不能定义abstract class。也就是说interface和implementation不能分离。这在设计大型电路时就显得表现力不足。不过这关系不大,因为可以在编译时选择同一模块的不同实现版本,间接实现了接口与实现的分离。

在VHDL中,强制将接口与实现分离。对每个模块,你都得先写接口(定义输入输出信号),即ENTITY;然后至少写一份实现,即ARCHITECTURE。每个ENTITY可以有不止一份实现,例如可以有行为描述的,也有数据流描述的。然后在配置文件中选择该ENTITY到底用哪一份实现。举例来说(选自《VHDL入门·解惑·经典实例·经验总结》一书),分频器模块可以这么写,先定义其接口FreqDevider,然后定义两份实现Behavior和Dataflow:

LIBRARY IEEE;
USE IEEE.Std_Logic_1164.All;  

ENTITY FreqDevider IS
PORT
( Clock  : IN  Std_Logic;
  Clkout : OUT Std_Logic
);
END;  

ARCHITECTURE Behavior OF FreqDevider IS
SIGNAL Clk : Std_Logic;
BEGIN
  PROCESS (Clock)
  BEGIN
    IF rising_edge(Clock) THEN
      Clk <= NOT Clk;
    END IF;
  END PROCESS;
  Clkout <= Clk;
END;

ARCHITECTURE Dataflow OF FreqDevider IS
-- signal declarations
BEGIN
-- processes
END;  

在C++中,既可以写concrete class,也可以写abstract class。比Verilog和VHDL都方便。

6. Verilog和VHDL都有模板的概念,Verilog称为参数(parameter),VHDL称为类属(generic)。不过好像都只能用整数作为模板参数,不能像C++那样用类型作为模板参数。

7. 目前来看,Verilog是硬件描述语言,不是硬件设计语言。在用Verilog设计电路的时候,我们是把脑子中想好的电路用Verilog“描述”出来:哪里是寄存器、哪里是组合逻辑、数据通路是怎样、流水线如何运作等等都要在脑子里有清晰的映象。然后用RTL代码写出来,经过综合器综合出的电路与大脑中的设想相比八九不离十。这就像说C语言是可移植的汇编语言,以前好的C程序员在写代码的时候,能够知道每条语句背后对应的汇编代码是什么。
verilog设计经验点滴
1,敏感变量的描述完备性
Verilog中,用always块设计组合逻辑电路时,在赋值表达式右端参与赋值的所有信号都必须在always @(敏感电平列表)中列出,always中if语句的判断表达式必须在敏感电平列表中列出。如果在赋值表达式右端引用了敏感电平列表中没有列出的信号,在综合时将会为没有列出的信号隐含地产生一个透明锁存器。这是因为该信号的变化不会立刻引起所赋值的变化,而必须等到敏感电平列表中的某一个信号变化时,它的作用才表现出来,即相当于存在一个透明锁存器,把该信号的变化暂存起来,待敏感电平列表中的某一个信号变化时再起作用,纯组合逻辑电路不可能作到这一点。综合器会发出警告。
Example1:
input a,b,c;
reg e,d;
always @(a or b or c)
    begin
    e=d&a&b; /*d没有在敏感电平列表中,d变化时e不会立刻变化,直到a,b,c中某一个变化*/
    d=e |c;
    end

Example2:
input a,b,c;
reg e,d;
always @(a or b or c or d)
    begin
    e=d&a&b; /*d在敏感电平列表中,d变化时e立刻变化*/
    d=e |c;
    end

2, 条件的描述完备性
如果if语句和case语句的条件描述不完备,也会造成不必要的锁存器。
Example1:
if (a==1'b1) q=1'b1;//如果a==1'b0,q=? q将保持原值不变,生成锁存器!
Example2:
if (a==1'b1) q=1'b1;
else         q=1'b0;//q有明确的值。不会生成锁存器!
Example3:
   reg[1:0] a,q;
   ....
   case (a)
      2'b00 : q=2'b00;
      2'b01 : q=2'b11;//如果a==2'b10或a==2'b11,q=? q将保持原值不变,锁存器!
   endcase
Example4:
   reg[1:0] a,q;
   ....
   case (a)
      2'b00 : q=2'b00;
      2'b01 : q=2'b11;
      default: q=2'b00;//q有明确的值。不会生成锁存器!
   endcase

Verilog中端口的描述
1,端口的位宽最好定义在I/O说明中,不要放在数据类型定义中;
Example1:
module test(addr,read,write,datain,dataout)

input[7:0]  datain;
input[15:0] addr;
input       read,write;

output[7:0] dataout;  //要这样定义端口的位宽!

wire addr,read,write,datain;

reg  dataout;

Example2:
module test(addr,read,write,datain,dataout)
input  datain,addr,read,write;
output dataout;
wire[15:0] addr;
wire[7:0]  datain;
wire       read,write;
reg[7:0]   dataout;   // 不要这样定义端口的位宽!!

2,端口的I/O与数据类型的关系:
     端口的I/O            端 口 的 数 据 类 型
                       module内部     module外部
      input              wire          wire或reg
      output         wire或reg           wire
      inout            wire              wire

3,assign语句的左端变量必须是wire;直接用"="给变量赋值时左端变量必须是reg!
Example:
assign a=b; //a必须被定义为wire!!
********
begin
   a=b; //a必须被定义为reg!
end

  VHDL 中 STD_LOGIC_VECTOR 和 INTEGER 的区别
例如 A 是INTEGER型,范围从0到255;B是STD_LOGIC_VECTOR,定义为8位。A累加到255时,再加1就一直保持255不变,不会自动反转到0,除非令其为0;而B累加到255时,再加1就会自动反转到0。所以在使用时要特别注意!

以触发器为例说明描述的规范性
1,无置位/清零的时序逻辑
    always @( posedge CLK)
       begin
       Q<=D;
       end

2,有异步置位/清零的时序逻辑
   异步置位/清零是与时钟无关的,当异步置位/清零信号到来时,触发器的输出立即被置为1或0,不需要等到时钟沿到来才置位/清零。所以,必须要把置位/清零信号列入always块的事件控制表达式。
    always @( posedge CLK or negedge RESET)
       begin
       if (!RESET)
          Q=0;
       else
          Q<=D;
       end

3,有同步置位/清零的时序逻辑
    同步置位/清零是指只有在时钟的有效跳变时刻置位/清零,才能使触发器的输出分别转换为1或0。所以,不要把置位/清零信号列入always块的事件控制表达式。但是必须在always块中首先检查置位/清零信号的电平。
    always @( posedge CLK )
    begin
       if (!RESET)
          Q=0;
       else
          Q<=D;
       end

结构规范性
  在整个芯片设计项目中,行为设计和结构设计的编码是最重要的一个步骤。它对逻辑综合和布线结果、时序测定、校验能力、测试能力甚至产品支持都有重要的影响。考虑到仿真器和真实的逻辑电路之间的差异,为了有效的进行仿真测试:
  1,避免使用内部生成的时钟
     内部生成的时钟称为门生时钟(gated clock)。如果外部输入时钟和门生时钟同时驱动,则不可避免的两者的步调不一致,造成逻辑混乱。而且,门生时钟将会增加测试的难度和时间。

  2,绝对避免使用内部生成的异步置位/清零信号
     内部生成的置位/清零信号会引起测试问题。使某些输出信号被置位或清零,无法正常测试。

3,避免使用锁存器
     锁存器可能引起测试问题。对于测试向量自动生成(ATPG),
     为了使扫描进行,锁存器需要置为透明模式(transparent mode),
     反过来,测试锁存器需要构造特定的向量,这可非同一般。

  4,时序过程要有明确的复位值
     使触发器带有复位端,在制造测试、ATPG以及模拟初始化时,可以对整个电路进行快速复位。

  5,避免模块内的三态/双向
     内部三态信号在制造测试和逻辑综合过程中难于处理.
补充不知你看了verilog 2001版本吗?现在的verilog在尽量往C语言的风格上靠拢。
1。敏感变量的描述完备性,我现在用always实现组合逻辑时,都是写成always@(*),这样很很好,自动把所有右端赋值信号加入。

2。module编写时这样更好:
module (  
input [23:0]        rx_data   ,
//CRC_chk interface                                 
output reg          CRC_en    ,                                 
output reg            CRC_init  ,
input                    CRC_err  
);
zhangyukun 发表于 2018-9-20 09:33:58 | 显示全部楼层
Verilog学习心得
大鹏 发表于 2022-4-12 14:44:18 | 显示全部楼层
Verilog学习心得
您需要登录后才可以回帖 登录 | 我要注册

本版积分规则

关闭

站长推荐上一条 /1 下一条

QQ|小黑屋|手机版|Archiver|fpga论坛|fpga设计论坛 ( 京ICP备20003123号-1 )

GMT+8, 2024-11-1 13:34 , Processed in 0.059620 second(s), 20 queries .

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表