2. Verilog基础与组合逻辑:模块结构、数据类型、assign语句、always块、组合逻辑电路设计

各位同学好,我是你们的FPGA讲师。今天咱们来聊聊Verilog的基础,特别是组合逻辑这块。说实话,很多初学者一上来就被各种语法吓住了,其实没那么复杂。你想想看,Verilog说白了就是描述硬件的一种语言,跟C语言写软件完全是两码事。

我刚开始学的时候也犯过迷糊——总觉得代码写出来就能跑,结果综合出来一堆莫名其妙的锁存器。嗯,今天咱们就把这些坑一个个填上。

2.1 模块结构——Verilog的基本单元

每个Verilog设计,都是从模块开始的。模块就像是一个黑盒子,有输入、有输出,内部实现具体的功能。我在项目中习惯把每个功能块都封装成一个模块,这样后期调试和复用都方便。

一个典型的模块结构长这样:

module adder (
    input  [3:0] a,      // 4位输入
    input  [3:0] b,      // 4位输入
    output [4:0] sum     // 5位输出(考虑进位)
);
    // 功能描述
    assign sum = a + b;
endmodule

注意看,module和endmodule是成对出现的。端口声明里,input、output、inout三种类型要分清。我个人习惯把位宽写在端口声明里,这样一眼就能看出数据宽度。

小技巧:模块名最好用有意义的英文单词,别用a、b、c这种。我在一个项目里见过"module m1(...)",三个月后自己都看不懂了。

2.2 数据类型——wire和reg的区别

这是新手最容易搞混的地方。我简单说:

  • wire:线网类型,用于组合逻辑。说白了就是一根导线,连续赋值。
  • reg:寄存器类型,用于时序逻辑。但注意,在always块里赋值必须用reg。

等等,这里有个坑——reg不一定是寄存器!在组合逻辑的always块里,reg只是语法要求,综合出来还是组合电路。我曾经被这个坑过,以为always块里赋值就一定是触发器,结果综合出来一堆组合逻辑。

看个例子:

wire [3:0] data_in;      // 组合逻辑信号
reg  [3:0] data_out;     // 可以是组合,也可以是时序

// 组合逻辑的always块
always @(*) begin
    data_out = data_in;   // 这里data_out虽然是reg,但综合出来是wire
end
注意:在always @(*)中,如果某个分支没有赋值,综合工具会推断出锁存器!我曾经在状态机里漏了一个default分支,结果多出8个锁存器,查了两天才发现。

2.3 assign语句——连续赋值

assign是组合逻辑最常用的语句。它表示连续驱动,只要右边表达式变化,左边立即更新。我习惯用assign实现简单的逻辑运算、数据拼接、三态门等。

举个例子:

assign sum = a + b;           // 加法器
assign sel_out = sel ? a : b; // 二选一多路器
assign {carry, result} = a + b + cin; // 带进位的加法

这里要注意,assign的左边必须是wire类型。如果你用reg,综合器会报错。嗯,我刚开始也犯过这个错。

2.4 always块——过程赋值

always块是Verilog的核心。它有两种主要用法:

  • 组合逻辑:always @(*),敏感列表用星号,表示所有输入信号变化都触发。
  • 时序逻辑:always @(posedge clk),只在时钟上升沿触发。

我个人建议:组合逻辑用阻塞赋值=,时序逻辑用非阻塞赋值<=。这个规则一定要遵守,否则仿真和综合结果可能不一致。

看个组合逻辑的例子:

always @(*) begin
    if (sel)
        y = a;
    else
        y = b;
end

这个代码综合出来就是一个二选一多路器。注意,if-else必须写全,否则会生成锁存器。我曾经在项目里偷懒,只写了if没写else,结果综合报告里多了一堆警告。

2.5 组合逻辑电路设计实战

好了,理论讲完了,咱们来点实际的。我挑两个最常用的组合逻辑电路:加法器和多路选择器。

2.5.1 加法器

加法器是数字电路的基础。最简单的就是直接用+运算符,但要注意位宽扩展。

module adder_4bit (
    input  [3:0] a,
    input  [3:0] b,
    input        cin,      // 进位输入
    output [3:0] sum,      // 和
    output       cout      // 进位输出
);
    assign {cout, sum} = a + b + cin;
endmodule

这里用花括号{}做位拼接,把进位和结果拼在一起。我在实际项目中,经常用这种写法,简洁又高效。

如果追求高性能,可以用进位选择加法器或超前进位加法器。但初学者先掌握这个基础版本就够了。

2.5.2 多路选择器

多路选择器(MUX)也是常用电路。用assign或always都可以实现。

用assign实现:

module mux_4to1 (
    input  [3:0] data0, data1, data2, data3,
    input  [1:0] sel,
    output [3:0] y
);
    assign y = (sel == 2'b00) ? data0 :
               (sel == 2'b01) ? data1 :
               (sel == 2'b10) ? data2 :
                                data3;
endmodule

用always实现:

always @(*) begin
    case (sel)
        2'b00: y = data0;
        2'b01: y = data1;
        2'b10: y = data2;
        2'b11: y = data3;
        default: y = 4'b0;  // 一定要有default!
    endcase
end
核心要点:
  • 组合逻辑中,case语句必须写全所有分支,或者加default。
  • if-else必须成对出现,否则生成锁存器。
  • assign和always @(*)都可以实现组合逻辑,看个人习惯。

2.6 知识体系总览

下面这张图是我自己画的,把本章的知识点串起来了。你看一眼就能明白各个概念之间的关系。

Verilog组合逻辑知识体系 组合逻辑设计 模块结构 数据类型 assign语句 always块 端口声明 参数化 wire reg 连续赋值 三态门 组合 时序 实战电路 加法器 多路选择器

这张图里,组合逻辑设计是核心,四个分支分别是模块结构、数据类型、assign语句和always块。最下面是两个实战电路——加法器和多路选择器。你看,整个知识体系其实很清晰。

2.7 避坑指南

最后,我总结几个常见坑,都是我在项目里踩过的:

  1. 锁存器推断:组合逻辑中,if没有else,case没有default,都会生成锁存器。解决办法:写全所有分支。
  2. 位宽不匹配:加法运算时,结果位宽要足够。比如两个4位数相加,结果要用5位来存,否则进位会丢失。
  3. 阻塞与非阻塞混用:同一个always块里,不要同时用=和<=。我见过有人这么写,仿真结果完全不对。
  4. 敏感列表不全:如果用always @(a, b)但漏了c,综合结果可能和仿真不一致。建议直接用@(*)。

好了,这一章的内容就到这里。记住,组合逻辑是FPGA设计的基础,多写多练自然就熟了。下一章咱们会讲时序逻辑,到时候再聊触发器和状态机。

我的建议:学完这一章,你可以试着写一个4位全加器,再用case语句实现一个8选1多路器。写完后用仿真看看波形,确认功能正确。实践出真知。

可可精品资料库