三、Verilog基础(上):模块与端口、数据类型(wire/reg)、组合逻辑(assign/always)
好,咱们正式开始写代码了。
这一章是Verilog的根基。说白了,你后面写的所有加速器、所有交易算法逻辑,都离不开这几个概念。我见过不少新手,一上来就急着写复杂的状态机,结果连wire和reg都分不清,最后仿真跑出来全是红叉叉。嗯,咱们先把地基打牢。
3.1 模块与端口:你的第一个硬件“黑盒子”
Verilog里,模块(module) 是最基本的设计单元。你可以把它想象成一个黑盒子——有输入、有输出,内部干些什么活,外面不用管。
我个人习惯,写模块之前先在脑子里画个框图:输入信号有哪些?输出信号有哪些?时钟和复位要不要?想清楚了再动键盘。
模块的基本结构:
module adder (
input wire [7:0] a, // 输入端口,8位宽
input wire [7:0] b, // 输入端口
output wire [7:0] sum // 输出端口
);
// 内部逻辑
assign sum = a + b;
endmodule
看到没?module 和 endmodule 把整个设计包起来了。端口列表写在括号里,每个端口都要声明方向:input、output 或者 inout(双向口,咱们做交易加速器很少用,先不管它)。
我的小习惯:端口声明时,我总把 wire 或 reg 类型写清楚。虽然有些编译器能自动推断,但写清楚能让代码更可读,也方便自己后期维护。
3.2 数据类型:wire 与 reg 的区别
这是新手最容易踩坑的地方。我当年也迷糊过一阵子。
wire —— 线网类型。说白了,它就是一根物理导线。你把它一端接上信号,另一端就能测到值。它不能存储数据,只能传递。
reg —— 寄存器类型。它像一个存储单元,可以保持住上一次被赋的值。注意,这里的“寄存器”不是指硬件里的Flip-Flop,它只是一个行为描述上的“变量”。
你想想看:
- 用
assign语句赋值,左边必须是wire。 - 用
always块赋值,左边必须是reg。
为什么会这样?因为 assign 描述的是组合逻辑,信号随时跟着输入变,不需要存储,所以用 wire。而 always 块里可以描述时序逻辑(比如带时钟的寄存器),需要保持状态,所以用 reg。
我曾经踩过的坑:在 always 块里给 wire 类型赋值,编译器直接报错。后来我养成习惯:写代码前先想清楚,这个信号是组合逻辑还是时序逻辑?组合逻辑用 wire,时序逻辑用 reg。
来个表格对比一下:
| 特性 | wire | reg |
|---|---|---|
| 本质 | 物理连线 | 行为变量 |
| 赋值方式 | assign 连续赋值 | always 过程赋值 |
| 默认值 | 高阻态 z | 未知态 x |
| 能否综合成寄存器 | 不能 | 可以(在时钟触发下) |
3.3 组合逻辑:assign 与 always
组合逻辑,就是输出只取决于当前输入,跟历史状态无关。说白了,输入一变,输出立刻跟着变,没有延迟(理想情况下)。
3.3.1 用 assign 实现组合逻辑
assign 是最直观的方式。它就像在硬件里直接拉了一根线,把输入和输出连起来。
// 一个简单的加法器
assign sum = a + b;
// 一个多路选择器
assign out = sel ? a : b;
// 位运算
assign result = data_in & mask;
注意,assign 语句是并行执行的。你写10条 assign,它们在硬件里是同时工作的,不是像C语言那样一条条顺序执行。这一点很重要,我刚开始做FPGA时总用软件思维想问题,结果仿真结果完全不对。
3.3.2 用 always 实现组合逻辑
always 块更灵活,但也要小心用。组合逻辑的 always 块,敏感列表里要列出所有输入信号。
// 组合逻辑的 always 块
always @(*) begin
if (sel)
out = a;
else
out = b;
end
这里 @(*) 表示“对所有输入信号变化敏感”。我个人强烈建议:写组合逻辑时,永远用 @(*),不要手动列信号列表。为什么?因为你可能漏掉某个信号,导致仿真和综合结果不一致。我吃过这个亏,查了两天才发现敏感列表少写了一个信号。
组合逻辑 always 块的注意事项:
- 所有分支都要赋值,否则会生成锁存器(latch)。
- 用
if-else或case时,一定要写完整。 - 赋值用阻塞赋值
=,不要用非阻塞赋值<=。
举个例子,下面这段代码会生成锁存器:
// 错误示例:会生成 latch
always @(*) begin
if (en)
out = data_in;
// 缺少 else 分支,out 在 en=0 时保持原值
end
正确的写法:
// 正确示例:所有分支都赋值
always @(*) begin
if (en)
out = data_in;
else
out = 8'b0; // 或者 out = 8'd0;
end
3.4 实战小例子:一个简单的数据选择器
咱们把今天学的知识串起来,写一个完整的模块。
module mux_4to1 (
input wire [7:0] in0, // 输入0
input wire [7:0] in1, // 输入1
input wire [7:0] in2, // 输入2
input wire [7:0] in3, // 输入3
input wire [1:0] sel, // 选择信号
output reg [7:0] out // 输出
);
// 用 always 实现组合逻辑
always @(*) begin
case (sel)
2'b00: out = in0;
2'b01: out = in1;
2'b10: out = in2;
2'b11: out = in3;
default: out = 8'b0; // 安全起见,加个默认值
endcase
end
endmodule
你看,端口声明用了 wire 和 reg,内部用 always 块描述组合逻辑。这个模块在FPGA里综合出来,就是几个多路器和一些连线,没有寄存器。
我的建议:刚开始写Verilog,先别急着写时序逻辑。把组合逻辑练熟了,搞清楚 assign 和 always 的区别,再碰时钟。就像学开车,先学会挂挡踩离合,再上路。
3.5 本章小结
今天咱们聊了三个核心概念:
- 模块与端口:硬件设计的黑盒子,输入输出要声明清楚。
- wire 与 reg:wire是连线,reg是变量。组合逻辑用wire,时序逻辑用reg。
- 组合逻辑实现:assign适合简单逻辑,always适合复杂逻辑。记住用
@(*),别漏分支。
下一章,咱们会深入时序逻辑,讲讲时钟、复位和寄存器。到时候你就能写出真正的“时序电路”了。嗯,期待一下。