咖啡图片
正在将巧克力泡入咖啡
ntainer" style="display: none">
文章

Verilog建模设计

<摘要>

Verilog建模设计

建模的内功心法

写 Verilog 强调建模而不是编程,思想是用 Verilog 去建立清晰可理解的硬件结构,模块是始终运行的硬件,顺序仅存在于观察结果而不是结构本身。建立了正确的建模直觉,后续的接口封装、系统组合、高级抽象都会变得顺理成章。你是否真的知道自己在描述什么硬件资源,你的设计是否能被画成结构图,你的模块是否在并行地、各司其职地运行。先拆、再分、再并行、再封装、最后组合成系统。

在有一定熟练度之后,写之前最好是先设计好电路大概长什么样(到加法器、ram、fifo 这个层级就可以),然后再用 verilog 实现。一个 always 块可以看作一个电路或者实物上的一个芯片。另外,Verilog 开发遵循自顶向下的模块化设计,模块划分要功能单一,且要留出使能、复位等接口以便系统搭建。有可能的话,把握编译器的理解方式也有助于开发,写出编译器能清晰理解的代码可以大大降低错误率。

功能划分

在实现上,一个模块只允许承担一个功能。功能模块只干具体的事,控制模块只负责调度,组合模块只负责拼装。功能模块 = 自己知道“怎么做”,但不知道“什么时候做”;控制模块 = 自己知道“什么时候做”,但不知道“具体怎么做”。

  • 功能模块是一种高度自洽的、近似“硬件器件”的存在。它的最大特点是:只要输入条件满足,它就自然地、持续地工作,而不需要外部有人一步一步指挥。功能模块通常具备自己的时序、自己的计数器、自己的寄存器状态,但这些状态都是为了完成“它这一件事”,而不是为了配合系统流程。它们的行为可以脱离“系统上下文”而存在。从设计风格上看,功能模块往往是“条件驱动”的,而不是“步骤驱动”的。也就是说,它更像是在回答:“当某个条件成立时,我该输出什么”,而不是:“现在轮到我做第几步了”。

  • 控制模块更像一个时序协调者、流程调度者,最大的特征是:它本身不干具体活,它只决定谁在什么时候该干活。控制模块关心的不是“这个模块内部怎么算”,而是“它什么时候开始”“它什么时候结束”“下一个该谁上”。因此,控制模块往往非常依赖外部模块的反馈信号,比如 done、busy、valid、ready 一类信号。从结构上看,控制模块通常是“强顺序感”的,是可以合理使用状态机思维的地方。控制模块只负责状态切换和调度决策,而不会把具体的功能逻辑塞进去。

    所谓的控制模块,就是用来产生 enable 信号的模块,其它功能模块中加入使能信号的设计,这样就可以由控制模块来调度功能模块。同时,功能模块起到沟通协调的作用,可以灵活调整,以接收各种功能模块的输入,产生各种功能的使能信号,保证了可扩展性。

串行和并行的理解

串行操作有步骤的概念,前后行为具有依赖关系;并行操作则是独立执行互不影响,各自在固定的时间执行操作。顺序操作的语言很多都是高级语言,一些调用的指令都是被隐性处理,如函数的调用指令和返回指令等。而在 Verilog 建模中,主要用状态机这种仿顺序的方式实现这种看起来是顺序的操作。

基本所有外设驱动的编写逻辑,都是晶振脉冲计时输出标志位,然后状态机根据计时标志位进行状态转换和相应动作。

封装和 FIFO

模块的封装,定义为某个基础的最后建模工作,以及封装过后的模块具有独立性。FIFO 可用于消除上下游模块的依赖关系,以缓冲的方式摆脱反馈的束缚。下层模块完成内部工作后,可以直接从 FIFO 中读取操作信息而不用等待上层模块等到下层反馈后下达新的指令。事实上,FIFO 是用来缓冲两个时钟域不同的访问,可以利用 FIFO 这个仓库作为接口的输入,使得接口具备独立性。上层模块调用某个接口时,只需要把信息写入仓库即可,上层模块不用与接口互动而被束缚,如果发现仓库有信息则处理即可。

image-20260131215406381

完整来说应该是,对于接收上游模块指令的模块,可以在最初环节之前引入 FIFO,而对于向下游模块发出指令的模块,可以在最后环节之后引入 FIFO,使得封装之后的接口具有独立性。不过理论上说,基本上所有模块都需要接收上游指令并向下游模块发出指令,所以在最开头还是最末尾引入 FIFO 也是相对的,还是根据具体需求来设计。这样,上层模块只管往 FIFO 中写入操作信息,不用等待下层模块的反馈信息就可以执行其它操作。控制模块接收到反馈模块后再从 FIFO 读取信息。

Verilog综合

  1. 非阻塞赋值在触发时是同时赋值的,很符合触发器在上升沿到来后将 D 输出到 Q 的实际情况。时序 always 块中左侧变量被综合成触发器,右侧的表达式用逻辑电路实现后连接到触发器的 D 端。与 if 复位配对的 else if 中的变量会被综合成使能信号。

    image-20260130150256115

  2. 相较于 case,if else if 是有优先级的,会综合出多个 MUX2 来实现。

  3. 组合逻辑如果有分支没有输出则会综合出锁存器,因为没有时钟但是又需要存储的功能。锁存器会占用大量资源且时序上不稳定。

    image-20260130151051946

  4. 在Xilinx FPGA开发中,加减乘的运算可以直接写,但不要直接写除法或取余的操作。因为加法和乘法在Xilinx的FPGA芯片中都有对应的硬件资源(比如乘法有DSP Slice),而除法和取余这些操作则没有。这些操作符被综合后,结构和时序往往不易控制。应该使用相关优化后的 ip 模块或工艺库中的集成模块。但是 parameter 类型的常量就可以使用此类操作符,因为在编译之初编译器就会计算出常量运算的结果,不会消耗多余的硬件资源。

  5. 多个数的加法或乘法可以进行展开,因为硬件资源只支持两个操作数,将多个操作数的加法或乘法展开成两个操作数有助于时序收敛并提高频率,即用面积换速度。

复位电路设计

为确保系统上电后有一个明确、稳定的初始状态,或系统运行状态紊乱时可以恢复到正常的初始状态,数字系统设计中一定要有复位电路模块。

另一方面,复位电路会消耗更多的硬件逻辑和面积资源。所以在一些初始值不影响逻辑正确性的数字设计中,建议去掉复位信号。比如数据路径不需要复位逻辑,因为其正确性会由伴随的控制信号保障,加上复位没有必要反而会增加硬件负担浪费资源。

复位电路可分类为同步复位和异步复位。

同步复位

1
2
3
4
5
6
7
8
9
10
11
12
13
module sync_reset(
    input       rstn,  //同步复位信号
    input       clk,   //时钟
    input       din,   //输入数据
    output reg  dout   //输出数据
    );

    always @(posedge clk) begin   //复位信号不要加入到敏感列表中
        if(!rstn)  dout <= 1'b0 ; //rstn 信号与时钟 clk 同步
        else       dout <= din ;
    end

endmodule

对于没有同步复位端的寄存器,会综合出以下电路

image-20260201153105974

同步复位的优点:信号间是同步的,能滤除复位信号中的毛刺,有利于时序分析。

同步复位的缺点:大多数触发器单元是没有同步复位端的,采用同步复位会多消耗部分逻辑资源。且复位信号的宽度必须大于一个时钟周期,否则可能会漏掉复位信号。(然而 xilinx 的 FPGA 是有同步复位端的,并且官方也建议采用同步复位而不是异步复位)。

异步复位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module async_reset(
    input       rstn,  //异步复位信号
    input       clk,   //时钟
    input       din,   //输入数据
    output reg  dout   //输出数据
    );

    //复位信号要加到敏感列表中
    always @(posedge clk or negedge rstn) begin
        if(!rstn)  dout <= 1'b0 ; //rstn 信号与时钟 clk 异步
        else       dout <= din ;
    end

endmodule

image-20260201153353483

异步复位的优点:大多数触发器单元有异步复位端,不会占用额外的逻辑资源。且异步复位信号不经过处理直接引用,设计相对简单,信号识别快速方便。

异步复位的缺点:复位信号与时钟信号无确定的时序关系,异步复位很容易引起时序上 removal 和 recovery 的不满足。且异步复位容易受到毛刺的干扰,产生意外的复位操作。

异步复位同步释放

一般数字系统设计时都会使用异步复位。为消除异步复位的缺陷,复位电路往往会采用 “异步复位、同步释放” 的设计方法。即复位信号到来时不受时钟信号的同步,复位信号释放时需要进行时钟信号的同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module areset_srelease
    (
    input       rstn,  //异步复位信号
    input       clk,   //时钟
    input       din,   //输入数据
    output reg  dout   //输出数据
    );

    reg   rstn_r1, rstn_r2;
    always @ (posedge clk or negedge rstn) begin
        if (!rstn) begin
            rstn_r1 <= 1'b0;     //异步复位
            rstn_r2 <= 1'b0;
        end
        else begin
            rstn_r1 <= 1'b1;     //同步释放
            rstn_r2 <= rstn_r1;  //同步打拍,时序差可以多延迟几拍
        end
    end

    //使用 rstn_r2 做同步复位,复位信号可以加到敏感列表中
    always @ (posedge clk or negedge rstn_r2) begin
        if (!rstn_r2) dout <= 1'b0; //同步复位
        else          dout <= din;
    end

endmodule

image-20260201153452106

局部复位设计

复位有全局复位和局部复位两种。需要复位的信号,建议设计成局部复位。局部的模块化复位寄存器容易被综合器优化,可以用 KEEP 属性强制保留,保证复位逻辑不被优化掉。

1
2
3
4
5
6
7
8
9
(* keep="true" *) reg my_modular_reset1;
(* keep="true" *) reg my_modular_reset2;
(* keep="true" *) reg my_modular_reset3;

always @(posedge clkA) begin
  my_modular_reset1 <= synchronized_reset;
  my_modular_reset2 <= synchronized_reset;
  my_modular_reset3 <= synchronized_reset;
end

参考资料

内功心法部分来自黑金动力社区的《Verilog那些事儿——建模篇》。

本文由作者按照 CC BY 4.0 进行授权
/body>