三、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

看到没?moduleendmodule 把整个设计包起来了。端口列表写在括号里,每个端口都要声明方向:inputoutput 或者 inout(双向口,咱们做交易加速器很少用,先不管它)。

我的小习惯:端口声明时,我总把 wirereg 类型写清楚。虽然有些编译器能自动推断,但写清楚能让代码更可读,也方便自己后期维护。

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-elsecase 时,一定要写完整。
  • 赋值用阻塞赋值 =,不要用非阻塞赋值 <=

举个例子,下面这段代码会生成锁存器:

// 错误示例:会生成 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

你看,端口声明用了 wirereg,内部用 always 块描述组合逻辑。这个模块在FPGA里综合出来,就是几个多路器和一些连线,没有寄存器。

我的建议:刚开始写Verilog,先别急着写时序逻辑。把组合逻辑练熟了,搞清楚 assignalways 的区别,再碰时钟。就像学开车,先学会挂挡踩离合,再上路。

3.5 本章小结

今天咱们聊了三个核心概念:

  • 模块与端口:硬件设计的黑盒子,输入输出要声明清楚。
  • wire 与 reg:wire是连线,reg是变量。组合逻辑用wire,时序逻辑用reg。
  • 组合逻辑实现:assign适合简单逻辑,always适合复杂逻辑。记住用 @(*),别漏分支。

下一章,咱们会深入时序逻辑,讲讲时钟、复位和寄存器。到时候你就能写出真正的“时序电路”了。嗯,期待一下。