第二讲:硬件描述语言基础——Verilog HDL基本结构、模块化设计、并行与顺序语句
各位同学,欢迎来到第二讲。今天咱们聊聊Verilog HDL的基础。说实话,很多初学者一上来就被各种语法吓住了。其实没那么复杂。你想想看,Verilog说白了就是描述硬件的一种语言。它跟C语言最大的区别是什么?并行!硬件天生就是并行的,这一点必须刻在脑子里。
2.1 Verilog HDL的基本结构
一个完整的Verilog模块长什么样?我直接给你看个例子:
module counter (
input wire clk,
input wire rst_n,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else
count <= count + 1'b1;
end
endmodule
看到了吗?module和endmodule是模块的边界。端口列表里,input、output、inout三种方向。我个人习惯把时钟和复位放在端口列表的最前面,这样一眼就能看到关键信号。
这里有个坑——wire和reg的区别。很多新手搞混。我简单说:
- wire:连线型,用于组合逻辑。比如assign语句的输出。
- reg:寄存器型,用于时序逻辑。比如always块里的赋值。
但注意!在always块里赋值的变量,必须声明为reg。哪怕它综合出来只是一根线。嗯,这里要记住:reg不一定是寄存器,它只是语法上的要求。
核心要点:Verilog模块 = 端口定义 + 内部信号 + 功能描述。三者缺一不可。
2.2 模块化设计——把大问题拆小
做FPGA项目,最忌讳的就是把所有逻辑写在一个模块里。我曾经接手过一个项目,一个模块写了三千行。改一个功能,牵一发动全身,调试了整整两周。从那以后,我坚持模块化设计。
模块化设计的核心思想:高内聚、低耦合。每个模块只做一件事,做好一件事。
举个例子,一个图像采集系统可以拆成这样:
// 顶层模块
module image_capture_top (
input wire clk,
input wire rst_n,
// 摄像头接口
input wire cam_pclk,
input wire cam_vsync,
input wire cam_href,
input wire [7:0] cam_data,
// 显示接口
output wire vga_hsync,
output wire vga_vsync,
output wire [11:0] vga_rgb
);
// 实例化摄像头驱动模块
wire [7:0] pixel_data;
wire pixel_valid;
camera_driver u_cam_drv (
.pclk (cam_pclk),
.vsync (cam_vsync),
.href (cam_href),
.data_in (cam_data),
.pixel_out (pixel_data),
.valid (pixel_valid)
);
// 实例化帧缓存模块
wire [11:0] frame_data;
frame_buffer u_fbuf (
.wr_clk (cam_pclk),
.wr_data (pixel_data),
.wr_en (pixel_valid),
.rd_clk (clk),
.rd_data (frame_data)
);
// 实例化VGA显示模块
vga_driver u_vga (
.clk (clk),
.rst_n (rst_n),
.frame_in (frame_data),
.hsync (vga_hsync),
.vsync (vga_vsync),
.rgb (vga_rgb)
);
endmodule
你看,顶层模块只做连线。每个子模块独立开发、独立测试。出了问题,定位也快。这就是模块化的好处。
我的经验:每个模块的代码量控制在200行以内。超过这个数,就该考虑拆分了。另外,模块的接口信号不要超过20个,否则可读性会急剧下降。
2.3 并行与顺序语句——硬件思维的起点
这是Verilog最核心、也最容易搞混的地方。我刚开始学的时候,也在这里栽过跟头。
2.3.1 并行语句
硬件里,所有模块、所有assign语句、所有always块,都是同时执行的。没有谁先谁后。你看下面这段:
assign a = b & c;
assign d = e | f;
assign g = a ^ d;
这三条语句是同时执行的。g的值取决于a和d的当前值。如果你用C语言的思维去理解,会觉得g用的是a和d的旧值。但在硬件里,没有「旧值」这个概念——所有赋值同时发生。
为什么会这样?因为硬件里每个逻辑门都是独立工作的。时钟沿一到,所有寄存器同时更新。这就是并行。
2.3.2 顺序语句
Verilog里也有顺序执行的地方——always块内部。但注意,只有用begin...end包裹的语句才是顺序执行的。
always @(posedge clk) begin
temp <= a + b;
result <= temp + c;
end
这段代码里,temp和result是同时更新的。因为用了非阻塞赋值<=。如果你用阻塞赋值=,那才是真正的顺序执行:
always @(*) begin
temp = a + b;
result = temp + c;
end
这里result会先计算temp,再用temp加c。顺序执行。
重要提醒:时序逻辑(带时钟的always块)一定要用非阻塞赋值<=。组合逻辑(不带时钟的always块)用阻塞赋值=。混用会导致仿真和综合结果不一致。我曾经因为这个bug,浪费了整整两天时间。
2.4 知识体系总览
下面这张图,把本章的核心知识点串起来了。你看一眼,心里就有数了:
2.5 实战中的避坑指南
最后,分享几个我踩过的坑:
- 端口方向搞反——input写成output,仿真能过,综合报错。检查三遍端口列表再往下写。
- 敏感列表不全——组合逻辑的always块,敏感列表漏了信号。仿真结果对,综合结果错。用
always @(*)最保险。 - 多驱动问题——同一个信号在两个always块里赋值。综合会报multi-driver错误。一个信号只能在一个always块里赋值。
- 锁存器意外生成——组合逻辑里,if没有else,case没有default。综合会生成锁存器。我刚开始做项目时,因为这个原因导致芯片面积大了30%。
我的习惯:每写完一个模块,先做语法检查,再做功能仿真。仿真通过后,再看综合报告。三步走,能避免90%的低级错误。
好了,这一讲的内容就到这里。记住:Verilog不是编程语言,是硬件描述语言。用硬件思维去理解它,你会发现它其实很简单。
可可精品资料库