3. 硬件描述语言基础(Verilog):模块结构、数据类型、运算符与赋值

各位同学,咱们今天聊聊Verilog。说实话,很多初学者一上来就被各种语法吓住了。其实没那么复杂。你把它想象成画电路图,只不过是用文字来描述而已。我刚开始学的时候,也是对着代码发懵,后来发现——嗯,只要抓住几个核心概念,剩下的都是套路。

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

Verilog里最重要的东西就是模块(module)。一个模块就是一个硬件单元。你可以把它想象成一个黑盒子,有输入、有输出,里面装着逻辑。

模块的基本结构长这样:

module 模块名 (
    端口列表
);
    // 端口声明
    // 内部信号声明
    // 逻辑功能描述
endmodule

举个例子,一个最简单的与门:

module and_gate (
    input  a,
    input  b,
    output y
);
    assign y = a & b;
endmodule

你看,是不是很直观?inputoutput就是告诉工具,哪些是输入,哪些是输出。我个人习惯把端口列表写在括号里,这样一眼就能看清模块的接口。

小技巧:端口声明时,建议把输入和输出分开写,别混在一起。我在项目中见过有人把所有端口写在一行,结果后期改代码改到崩溃。

3.2 数据类型——wire和reg

Verilog里最常用的两种数据类型就是wirereg。很多新手搞不清它们的区别。我简单说:

  • wire:相当于一根导线。它只能被连续赋值(assign)驱动。说白了,就是组合逻辑的输出。
  • reg:相当于一个存储单元。它只能在always块里被赋值。说白了,就是时序逻辑的寄存器。

但注意,reg不一定会综合成寄存器。如果你在always块里写组合逻辑,reg综合出来可能只是一堆门电路。嗯,这里要小心。

举个例子:

wire sum;          // 组合逻辑信号
reg  counter;      // 时序逻辑信号

assign sum = a + b;  // wire用assign驱动

always @(posedge clk) begin
    counter <= counter + 1;  // reg在always块里赋值
end
避坑指南:我曾经犯过一个低级错误——在always块里给wire赋值。结果综合报错,查了半天才发现。记住:wire只能用assign,reg只能在always里用。

3.3 运算符——别怕,都是套路

Verilog的运算符和C语言很像。你如果写过C,上手会很快。我整理了一张表:

类别 运算符 说明
算术 + - * / % 加减乘除取模
逻辑 && || ! 与或非(返回1位)
按位 & | ~ ^ 按位与、或、非、异或
关系 > < >= <= == != 比较运算
移位 << >> 左移、右移
拼接 { } 把多个信号拼在一起

举个例子,拼接运算符特别实用:

wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b0101;
wire [7:0] c = {a, b};  // c = 10100101

你想想看,如果不用拼接,你得写多少行代码?

3.4 assign语句——连续赋值

assign是组合逻辑的标配。它的意思是:只要右边信号变了,左边立刻更新。说白了,就是一根导线。

assign y = a & b | c;

注意,assign的左边必须是wire类型。右边可以是任何表达式。我在项目中经常用assign做数据选择:

assign data_out = sel ? data_a : data_b;

这个三目运算符,其实就是个多路选择器。综合工具会自动生成对应的门电路。

3.5 always块——时序逻辑的核心

always块是Verilog里最灵活、也最容易出错的地方。它的基本格式是:

always @(敏感列表) begin
    // 逻辑代码
end

敏感列表决定了什么时候执行块里的代码。常见的写法有:

  • always @(posedge clk):时钟上升沿触发(时序逻辑)
  • always @(a or b or c):信号变化触发(组合逻辑)
  • always @(*):所有输入信号变化触发(推荐写法)

举个例子,一个D触发器:

always @(posedge clk) begin
    q <= d;
end

你看,这就是一个寄存器。时钟上升沿时,把d的值锁存到q里。

重点:always块里,如果写时序逻辑,用非阻塞赋值(<=);如果写组合逻辑,用阻塞赋值(=)。这个区别很重要,下面详细说。

3.6 阻塞赋值与非阻塞赋值——最容易踩的坑

这个问题,我当年学的时候也迷糊过。简单说:

  • 阻塞赋值(=):顺序执行。上一条赋值完,才执行下一条。适合组合逻辑。
  • 非阻塞赋值(<=):并行执行。所有赋值同时发生。适合时序逻辑。

看个例子就明白了:

// 阻塞赋值(组合逻辑)
always @(*) begin
    a = b;
    c = a;  // c得到的是b的新值
end

// 非阻塞赋值(时序逻辑)
always @(posedge clk) begin
    a <= b;
    c <= a;  // c得到的是a的旧值
end

为什么会这样?因为非阻塞赋值是在时钟沿到来时,先计算右边的值,然后统一更新左边。所以c拿到的是a更新前的值。

避坑指南:我曾经在同一个always块里混用阻塞和非阻塞赋值,结果仿真和综合结果不一致。查了两天才发现。记住:一个always块里,要么全用阻塞,要么全用非阻塞,别混着来。

3.7 知识体系总览

为了让你更直观地理解本章内容,我画了一张图:

Verilog基础 模块结构 module/endmodule input/output 数据类型 wire / reg 组合 vs 时序 运算符 算术/逻辑/按位 关系/移位/拼接 assign语句 连续赋值 组合逻辑 always块 敏感列表 时序/组合逻辑 阻塞/非阻塞赋值 = vs <= 顺序 vs 并行

这张图把本章的知识点串起来了。你从中心往外看,每个分支都是一个独立的概念,但它们又相互关联。比如,assign只能驱动wirealways块里只能用reg。这些关系搞清楚了,Verilog就算入门了。

好了,这一章就到这里。记住:多写代码,多仿真,多踩坑。经验就是这么积累出来的。

专注资料整理