4. Verilog基础(一):模块化设计、数据类型(wire/reg)、always块与assign语句
各位同学,今天我们正式进入Verilog的世界。说实话,很多初学者一上来就被各种语法细节搞晕了。我当年也是这样,总觉得这语言怎么这么别扭,又像C又不像C。但做了十几年FPGA设计后,我越来越觉得,Verilog其实很纯粹——它就是在描述硬件,不是在写软件。
这一节我们重点讲三个核心概念:模块化设计、wire和reg的区别、以及always块和assign语句。这三样东西,你搞明白了,Verilog就算入门了。
4.1 模块化设计——把大问题拆成小零件
FPGA设计跟盖房子很像。你不会直接去砌一整面墙,而是先做砖头、做门窗、做梁柱,最后再组装起来。模块化设计就是这个道理。
每个Verilog模块,就是一个功能单元。它有输入、有输出,内部实现具体的逻辑。模块之间通过端口连接,互相配合完成整个系统的功能。
我个人的习惯是:一个模块只干一件事。比如计数器模块只管计数,FIFO模块只管缓存数据,SPI控制器只管收发。这样设计的好处是——调试的时候,你只需要盯着一个小模块看,问题很快就能定位。
来看一个最简单的模块例子:
module counter (
input wire clk,
input wire rst_n,
output reg [7:0] cnt
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 8'd0;
else
cnt <= cnt + 1'b1;
end
endmodule
这个模块叫counter,输入是时钟和复位,输出是一个8位的计数器值。内部就是一个简单的always块,每个时钟上升沿加1。
4.2 wire和reg——别再傻傻分不清了
这个问题,几乎每个初学者都会问:wire和reg到底有什么区别?
说白了,wire是连线,reg是存储单元。但这句话太抽象,我换个说法:
- wire:它本身不保存值,只是把某个信号的值传递到另一个地方。你可以把它想象成一根导线。
- reg:它能保存值,在时钟沿到来之前,它的值不会变。你可以把它想象成一个小盒子,里面存着数据。
但这里有个坑,我当年就踩过:reg不一定被综合成寄存器。什么意思呢?如果你在always块里写组合逻辑,reg综合出来就是连线。只有当时钟沿触发的always块,reg才会变成真正的寄存器。
来看个对比:
| 特性 | wire | reg |
|---|---|---|
| 默认值 | 高阻态z | 不定态x |
| 赋值方式 | assign语句 | always块内赋值 |
| 能否存储值 | 不能 | 能(但要看上下文) |
| 综合结果 | 连线 | 可能是连线,也可能是寄存器 |
4.3 assign语句——连续赋值,简单粗暴
assign语句,说白了就是把右边的表达式结果,实时地赋给左边的wire。它没有时钟概念,只要右边变了,左边立刻跟着变。
举个例子:
module adder (
input wire [3:0] a,
input wire [3:0] b,
output wire [4:0] sum
);
assign sum = a + b;
endmodule
这个模块就是一个加法器。a和b变了,sum立刻跟着变。没有时钟,没有复位,就是纯组合逻辑。
assign语句适合做什么?
- 简单的逻辑运算(与、或、非、加法等)
- 数据选择(比如用条件表达式)
- 信号拼接和位宽转换
但要注意,assign不能用于reg类型的赋值。你想想看,reg需要保存值,而assign是连续赋值,这两者天生矛盾。
4.4 always块——时序逻辑的核心
always块是Verilog里最灵活、也最容易出错的语法结构。它有两种主要用法:
第一种:组合逻辑
always @(*) begin
if (sel)
out = a;
else
out = b;
end
这里的@(*)表示敏感列表包含所有输入信号。只要a、b、sel任何一个变了,always块就会重新执行。注意,这里用的是阻塞赋值=。
第二种:时序逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里用的是非阻塞赋值<=。为什么?因为时序逻辑里,所有赋值应该同时发生,而不是顺序执行。非阻塞赋值正好满足这个要求。
- 组合逻辑用阻塞赋值
= - 时序逻辑用非阻塞赋值
<= - 千万别混用,否则仿真和综合结果可能不一致
嗯,这里我要多说一句。我见过很多新手在同一个always块里既用=又用<=,结果仿真跑得好好的,综合出来就是错的。为什么?因为综合工具对混合赋值的处理方式跟仿真器不一样。所以我的建议是:一个always块里只用一种赋值方式。
4.5 实战小例子——边沿检测器
光说不练假把式。我们来看一个实际项目中经常用到的电路:边沿检测器。它能检测信号的上升沿或下降沿。
module edge_detector (
input wire clk,
input wire rst_n,
input wire sig_in,
output reg pos_edge,
output reg neg_edge
);
reg sig_dly;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sig_dly <= 1'b0;
pos_edge <= 1'b0;
neg_edge <= 1'b0;
end
else begin
sig_dly <= sig_in;
pos_edge <= sig_in & ~sig_dly;
neg_edge <= ~sig_in & sig_dly;
end
end
endmodule
这个电路的核心思想是:把输入信号延迟一拍,然后用当前值和延迟值做比较。如果当前是1、延迟是0,那就是上升沿;反过来就是下降沿。
我在项目中用过这个电路无数次。比如SPI接口的片选信号检测、按键消抖后的边沿提取、数据有效信号的同步等等。别看它简单,但非常实用。
4.6 避坑指南——我踩过的那些雷
最后,分享几个我亲身经历过的坑,希望能帮大家少走弯路:
- 敏感列表不完整:写组合逻辑时,always@后面忘了加
*,结果漏掉某个信号,仿真和综合结果不一致。后来我统一用always @(*),再也没出过问题。 - reg忘记赋初值:FPGA上电后,reg的初始值是不定态x。如果不加复位,仿真时就会出现一堆红色波浪线。我的习惯是:每个reg都在复位时赋一个确定值。
- wire和reg混用:在assign语句左边用reg,或者在always块里给wire赋值。这种错误综合工具会报错,但仿真器可能不报。所以一定要养成好习惯。
好了,这一节的内容就到这里。下一节我们会深入讲Verilog的运算符和常用结构,包括阻塞与非阻塞赋值的底层原理。到时候我会用一个实际项目中的例子来演示,保证让你印象深刻。