2. Verilog基础语法(上):模块结构、数据类型、运算符、assign语句、always块
各位同学,欢迎来到第二讲。今天咱们要啃的,是Verilog最核心的几块骨头——模块结构、数据类型、运算符,还有两个最重要的语句块:assign和always。
说实话,我刚开始学Verilog那会儿,总觉得这些东西太琐碎。后来做项目多了才发现,基础不牢,地动山摇。很多bug追到最后,其实就是wire和reg用混了,或者always块里漏了敏感列表。嗯,咱们今天就把这些坑一个个填平。
本章核心脉络:理解Verilog的模块化设计思想,掌握wire和reg的本质区别,熟练使用assign和always两种建模方式。
2.1 模块结构——Verilog的"黑盒子"
每个Verilog设计,说白了就是一个模块(module)。你可以把它想象成一个黑盒子——外面只看到管脚(端口),里面藏着逻辑电路。
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
// 内部逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
我个人习惯,写模块时先把端口声明写完,再写内部逻辑。这样结构清晰,别人看你的代码也舒服。
2.2 数据类型——wire 和 reg 到底怎么选?
这是新手最容易懵的地方。我当年也纠结过:什么时候用 wire?什么时候用 reg?
说白了就一句话:
- wire:物理连线。它不保存值,只是把两个点连起来。assign 语句左边必须是 wire。
- reg:寄存器/变量。它可以在 always 块里被赋值,能保存状态。
但注意!reg 不一定是真正的硬件寄存器。在 always 的组合逻辑块里,reg 综合出来只是连线。嗯,这里容易混淆,我单独列个表:
| 类型 | 默认值 | 赋值方式 | 综合结果 |
|---|---|---|---|
| wire | 高阻态 z | assign 连续赋值 | 连线 |
| reg(时序逻辑) | 不定态 x | always @(posedge clk) | 触发器 |
| reg(组合逻辑) | 不定态 x | always @(*) | 连线(无寄存器) |
2.3 运算符——不只是加减乘除
Verilog的运算符跟C语言很像,但有几个特殊的你得注意:
- 算术运算符:+ - * / %。注意除法综合出来资源很大,我一般不用。
- 逻辑运算符:&& || !。结果是1bit的布尔值。
- 位运算符:& | ~ ^。按位操作,这个在图像处理里用得特别多。
- 关系运算符:> < >= <= == !=。比较大小。
- 移位运算符:<< >>。左移1位相当于乘2,右移1位相当于除2。
- 拼接运算符:{a, b}。把多个信号拼在一起,做图像处理时经常用来组合RGB数据。
- 条件运算符:? :。相当于if-else的简写。
// 举个例子:图像像素的RGB分离
wire [23:0] pixel; // {R, G, B} 各8bit
wire [7:0] red = pixel[23:16];
wire [7:0] green = pixel[15:8];
wire [7:0] blue = pixel[7:0];
// 拼接回去
wire [23:0] new_pixel = {red, green, blue};
重点:拼接运算符在图像处理里简直是神器。你想把多个通道的数据打包成一个总线,或者从总线里拆出各个通道,用 {} 一行搞定。
2.4 assign语句——连续赋值
assign 是组合逻辑的"直通车"。它描述的是:只要右边信号变化,左边立刻更新。没有延迟,没有时钟。
// 简单的加法器
assign sum = a + b;
// 带条件的赋值
assign max = (a > b) ? a : b;
// 多bit选择
assign data_out = sel ? data1 : data2;
我个人习惯,简单的组合逻辑用 assign,复杂的用 always 块。为什么?因为 assign 写多了,代码可读性会下降。你想想看,一个 assign 里塞了五六个条件运算符,别人看代码得多痛苦。
2.5 always块——时序与组合的"万能工具箱"
always 块是 Verilog 最灵活的语句块。它有两种主要用法:
2.5.1 组合逻辑的 always 块
// 组合逻辑:敏感列表用 @(*)
always @(*) begin
if (sel)
out = a;
else
out = b;
end
这里用阻塞赋值 =。注意,组合逻辑的 always 块里,所有输入信号都要出现在敏感列表里。用 @(*) 可以自动包含所有输入,省心。
2.5.2 时序逻辑的 always 块
// 时序逻辑:时钟沿触发
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 0;
else
q <= d;
end
这里用非阻塞赋值 <=。为什么?因为非阻塞赋值能模拟触发器的行为——所有赋值在时钟沿同时发生,不会互相干扰。
组合逻辑用 =,时序逻辑用 <=,绝不混用。
2.6 实战小例子:边沿检测
咱们来写一个实用的电路——检测信号的上升沿。这在图像处理里经常用到,比如检测帧同步信号。
module edge_detect (
input wire clk,
input wire rst_n,
input wire sig_in,
output wire pos_edge,
output wire neg_edge
);
reg sig_dly;
// 延迟一拍
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
sig_dly <= 0;
else
sig_dly <= sig_in;
end
// 检测边沿
assign pos_edge = sig_in & ~sig_dly; // 上升沿
assign neg_edge = ~sig_in & sig_dly; // 下降沿
endmodule
这个电路的核心思想很简单:把信号延迟一个时钟周期,然后比较当前值和历史值。如果当前是1、历史是0,那就是上升沿。反过来就是下降沿。
嗯,到这里,Verilog基础语法的上半部分就讲完了。这些内容看起来简单,但每个点都值得你亲手敲一遍代码、跑一遍仿真。我在做图像滤波项目时,这些基础语法几乎每天都在用——模块化设计让代码可复用,wire/reg的正确使用避免了很多仿真bug,assign和always块则是构建所有逻辑的基石。
记住:基础越扎实,后面写复杂算法时就越从容。
课后练习建议:
- 写一个8位加法器模块,用assign实现
- 写一个带同步复位的D触发器,用always块实现
- 把上面两个模块例化到顶层,跑一遍仿真