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),检测上升沿 。