Verilog基础回顾:从模块到上板
各位同学,咱们今天聊聊Verilog基础。说实话,很多初学者觉得Verilog就是写代码,跟C语言差不多。嗯,这个想法很危险。我见过太多人把Verilog写成C,结果综合出来的电路完全不是那么回事。
咱们先看一张图,把今天要讲的内容串起来:
一、模块结构——Verilog的基本单元
Verilog里最核心的概念就是模块。说白了,一个模块就是一个硬件电路的黑盒子。你给它输入,它给你输出。
module counter (
input wire clk,
input wire rst_n,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'd0;
else
count <= count + 1'b1;
end
endmodule
我个人习惯把端口声明和内部逻辑分开写。这样代码结构清晰,别人一看就懂。模块名我建议用有意义的英文,别用a、b、c这种。
二、数据类型——wire和reg的区别
很多新手搞不清wire和reg。我换个说法:
- wire:相当于一根导线。它不存储值,只是传递信号。assign语句赋值给wire。
- reg:相当于一个寄存器。它能保存值。always块里赋值给reg。
等等,这里有个坑。reg不一定会被综合成寄存器!比如下面这个例子:
reg [3:0] result;
always @(*) begin
result = a + b;
end
这个result虽然是reg类型,但综合出来只是组合逻辑,没有寄存器。为什么会这样?因为always @(*)是组合逻辑描述,没有时钟沿触发。
另外还有parameter,这个相当于C语言里的宏定义:
parameter DATA_WIDTH = 8;
reg [DATA_WIDTH-1:0] data_bus;
用parameter的好处是,改一个地方,所有地方都跟着变。我建议把经常用到的常量都定义成parameter。
三、运算符——别搞混了位宽
Verilog的运算符跟C语言很像,但有些地方完全不同。我挑几个容易出问题的说说:
| 运算符类型 | 符号 | 说明 | 常见错误 |
|---|---|---|---|
| 算术 | + - * / % | 加减乘除取模 | 乘法会消耗大量LUT |
| 位运算 | & | ^ ~ | 按位与或异或非 | 位宽不匹配时自动截断 |
| 逻辑运算 | && || ! | 逻辑与或非,结果1bit | 跟位运算搞混 |
| 拼接 | { } | 把多个信号拼在一起 | 拼接顺序搞反 |
| 移位 | << >> | 左移右移 | 移位后位宽不变 |
举个例子,你想想看:
wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b1100;
wire [3:0] c = a & b; // 结果是4'b1000
wire d = a && b; // 结果是1'b1,因为两个都不为0
嗯,这里要注意。&和&&完全是两码事。我见过有人写if (a & b)想判断两个信号都不为0,结果综合出来的电路完全不对。
四、组合逻辑与时序逻辑
这两个概念是FPGA设计的基石。我简单说说:
组合逻辑:输出只取决于当前输入。没有记忆功能。用assign或者always @(*)描述。
// 组合逻辑:二选一多路器
assign out = sel ? b : a;
时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。有记忆功能。用always @(posedge clk)描述。
// 时序逻辑:D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
我在项目中遇到过一个问题:有人把组合逻辑里的赋值写成了"<=",结果仿真时看起来没问题,但综合出来的电路多了一堆没用的锁存器。排查了两天才找到原因。
五、Testbench编写基础
写Testbench说白了就是模拟输入信号,看输出对不对。我一般这么写:
`timescale 1ns / 1ps
module tb_counter;
reg clk;
reg rst_n;
wire [3:0] count;
// 实例化被测试模块
counter uut (
.clk (clk),
.rst_n(rst_n),
.count(count)
);
// 生成时钟
initial begin
clk = 0;
forever #5 clk = ~clk; // 周期10ns
end
// 生成复位和激励
initial begin
rst_n = 0;
#20;
rst_n = 1;
#200;
$finish;
end
// 显示结果
initial begin
$monitor("time=%0t, count=%d", $time, count);
end
endmodule
写Testbench有几个要点:
- 用`timescale指定时间单位
- 用initial块生成时钟和复位
- 用forever生成周期性信号
- 用$monitor或$display查看结果
- 用$finish结束仿真
好了,Verilog基础就回顾到这里。这些内容看起来简单,但都是FPGA设计的根基。你想想看,如果连wire和reg都分不清,连阻塞和非阻塞都搞混,那后面的时序约束、状态机、接口设计根本没法做。
下一章咱们开始真正进入Vivado,从创建工程开始。到时候我会手把手带你们走一遍流程。
- 模块是Verilog的基本单元,用module/endmodule定义
- wire是连线,reg是变量,但reg不一定是寄存器
- 运算符注意位宽和类型,别把位运算和逻辑运算搞混
- 组合逻辑用"=",时序逻辑用"<="
- Testbench是仿真的基础,每个模块都要写