Skip to content

Verilog

第一部分:基础语法与组合逻辑

1. 模块 (Module) 与端口

  • 定义: module 和 endmodule 定义电路模块 。

  • 引脚 (Ports): 使用 input 定义输入端口,output 定义输出端口 。

  • 端口声明方式:

    • 在模块内部声明类型。

2. 线网 (wire) 与连续赋值 (assign)

  • wire: 表示物理导线。

    • 电气特性: wire 必须被有且仅有一个信号源(如 assign 或模块输出)驱动 。

    • 可以有 0 到多个输出 。

  • assign: 用于将 wire 连接起来或赋予逻辑值 。

    • 有效连接: 模块输入连到线路、线路连到线路、线路连到模块输出 。

    • 常数赋值: 1'b1 相当于接高电平 (Vcc),1'b0 相当于接低电平 (GND) 。

  • 错误连接示例:

    • 给 input 端口赋值。

    • 形成组合逻辑环路 (如 assign wire2 = wire3; assign wire3 = wire2;) 。

    • 多重驱动 (Multi-driven): 同一个 wire 被多个 assign 赋值,导致短路 。

    • 将导线赋值给常数 (如 assign 1'b0 = wire8;) 。

    • 未赋值的 wire 处于高阻态 。

3. 模块调用与实例化

  • 模块实例化时,主要关注输入输出关系,而忽略内部物理实现。

  • 连接方式:

    • 声明端口 (推荐): 显式指明连接关系,如 .input1(s_logisimNet2) 。悬空输出端口可写为 .result() 。

    • 不声明端口: 按照模块定义的端口顺序连接,容易出错,不推荐 。

    • 声明端口和不声明端口的连接方法不可以混用 。

4. 运算符与逻辑门

  • 运算符: ~ (非), & (与), | (或) 。

  • 核心思想: Verilog 描述的是并行执行的物理电路,绝不能像写 C 语言那样仅仅考虑逻辑顺序。代码的物理位置不决定执行顺序,通电后所有电路同时工作。

第二部分:时序逻辑与寄存器

1. 寄存器 (reg)

  • 定义: 用 reg 定义寄存器,可以是单比特或多比特向量/数组 。

  • reg 通常使用 FPGA 内置的触发器 (Flip Flop) 实现。

2. 触发器与 always 块

  • 边沿触发: always@(posedge clk) (上升沿) 或 always@(negedge clk) (下降沿) 。

  • 时序约束: 寄存器输入端的数据必须在时钟边沿到来前 (slack time) 和到来后 (hold time) 保持稳定,否则会导致时序错误。

  • 统一时钟沿: 建议统一使用上升沿或下降沿设计电路,避免因半个时钟周期窗口导致的时序违例。

3. 寄存器初始化 (复位)

  • 异步复位: 复位信号无需等待时钟边沿。

    • 高电平复位: always@(posedge clk or posedge rst),if(rst) 。

    • 低电平复位: always@(posedge clk or negedge rstn),if(~rstn) 。

  • 同步复位: 复位信号仅在时钟边沿生效。

    • 例如: always@(posedge clk),if(~rstn) 。

4. always 块常见错误

  • 多时钟触发: 综合时无法实现一个 reg 既在 clk1 触发又在 clk2 触发。

  • 多边沿触发: always@(clock) 无法综合出有效的时序电路。always@(a or b or c) 虽然可以综合为组合逻辑,但不推荐 。

  • 多 always 块赋值: 同一个 reg 不能在多个 always 块内被赋值 (multi-driven) 。

  • 异步触发(数据竞争): 由组合逻辑产生的信号(如 assign problem = cond1 & cond2; always@(posedge problem))作为时钟,由于传播延迟不一致,容易产生毛刺,导致电路处于不稳定中间态,应改用同步时钟(如 always@(posedge clk) 搭配 if(problem)) 。

第三部分:阻塞赋值 (=) 与非阻塞赋值 (<=)

1. 核心差异

  • 非阻塞赋值 (<=): 同一个 always 块内的赋值同时执行,没有先后依赖。常用于构建时序电路 (寄存器) 。

  • 阻塞赋值 (=): 语句按顺序执行,后面的语句会使用前面语句更新后的值。常用于构建组合电路 。

2. 阻塞赋值的特点与陷阱

  • 电路重命名: 阻塞赋值中多次给同一个变量赋值时,综合工具会重命名变量,得到有先后关系的电路 。

  • 组合电路 vs 寄存器: 在阻塞赋值中,成环路的变量才会被综合为寄存器(reg),其他中间变量会被综合为导线(wire) 。

  • 优点: 类似 C 语言,便于实现复杂的计算(如嵌套循环、条件判断) 。

  • 缺点:

    • 忽略硬件开销: 照搬 C 代码的 for 循环(例如乘法器),会导致电路被大量复制展开,占用巨大面积 。

    • 行为未定义: 在 always 块外部使用阻塞赋值产生的中间变量,由于其可能是寄存器输入、输出或 wire,行为难以界定,应尽量避免使用 。

3. 使用建议

  • 组合逻辑: always@(*) 搭配 = (阻塞)。

  • 时序逻辑: always@(posedge clk) 搭配 <= (非阻塞)。

第四部分:高级与 SystemVerilog 特性

1. 代码生成与循环

  • generate: 用于生成并行的硬件电路 。结合 genvar i 和 for 循环,可大幅简化重复连接的代码。

  • integer 仿真循环: 在 initial 块中使用,带有时间先后关系,主要用于仿真激励生成 。

2. 参数化设计

  • 宏定义 (define): 简单的全局文本替换 。

  • 本地参数 (localparam): 模块内部常量,不支持外部配置 。

  • 模块参数 (parameter): 允许在实例化模块时传入参数,实现高可配置的电路 。

3. SystemVerilog 扩展

  • logic: 可全面替代 wire 和 reg 。在 assign 中被综合为 wire ,在带时钟的 always 块中被综合为 reg 。

  • 明确的 always 块:

    • always_comb: 专用于组合逻辑,替代 always@(*),能检查 latch 等问题 。

    • always_ff: 专用于时序逻辑 。

    • always_latch: 专用于锁存器逻辑 。

  • typedef: 为数据类型定义别名,增强代码语义和可维护性 。

  • struct: 将相关的信号(如一组向量)打包为一个结构体,极大简化模块端口声明和连线 。

  • package: 集中定义 parameter, typedef, struct 等,供多个模块通过 import 共享使用 。

  • queue: 仅用于仿真的队列结构,支持 push_back, pop_front 等操作 。

  • interface: 将相关联的输入输出信号打包(包含 modport 端口方向定义),极大简化模块间复杂握手信号的连接 。

第五部分:常用模块示例

  • 分频器: 使用计数器 clk_div 的不同位,可以获取频率依次减半的衍生时钟 。

  • 按键去抖器: 通过移位寄存器记录连续多个周期的电平状态,过滤按键机械抖动产生的毛刺 。

  • 边沿采样: 通过记录前一个周期的信号并与当前周期对比 (~old_data & data),检测上升沿 。