一、硬件描述语言基础(Verilog):从模块到赋值
各位同学,欢迎来到Verilog的世界。说实话,很多初学者一上来就被各种语法吓住了。其实没那么复杂。你想想看,Verilog说白了就是描述硬件的一种语言。我们不是在写软件,而是在画电路。
今天这一章,我带你搞定四个核心概念:模块结构、端口定义、常用数据类型、赋值语句。掌握了这些,你就拿到了FPGA设计的入场券。
1.1 模块(module)结构——Verilog的基本单元
在Verilog里,模块就是最小的设计单元。每个模块代表一个硬件电路块。我习惯把模块想象成一个黑盒子:有输入、有输出,里面是具体的逻辑实现。
一个完整的模块长这样:
module 模块名 (
端口列表
);
// 端口声明
// 内部信号声明
// 逻辑功能描述
endmodule
嗯,这里要注意:module和endmodule必须成对出现。少了任何一个,编译器都会报错。我曾经有个学生,写了半天代码,结果编译一直报错,最后发现是endmodule拼成了endmodulee——多了一个e。这种低级错误,排查起来真的很浪费时间。
核心要点:每个模块必须有一个唯一的模块名,且文件名最好和模块名保持一致。这是行业惯例,也是好习惯。
1.2 端口定义——input、output、inout
端口就是模块和外界通信的通道。有三种类型:
| 端口类型 | 方向 | 说明 |
|---|---|---|
| input | 输入 | 数据从外部流入模块内部 |
| output | 输出 | 数据从模块内部流出到外部 |
| inout | 双向 | 既可以输入也可以输出(常用于总线) |
举个例子:
module counter (
input clk, // 时钟信号
input rst_n, // 复位信号(低有效)
output reg [3:0] cnt // 4位计数器输出
);
// 内部逻辑
endmodule
我个人习惯把时钟和复位放在端口列表的最前面。这样一眼就能看到关键信号。另外,inout类型我建议新手慎用。为什么?因为双向端口涉及到三态门控制,处理不好容易出问题。我在项目中就遇到过因为inout端口没处理好,导致总线冲突,整个板子直接冒烟——嗯,夸张了点,但确实烧了一个IO口。
避坑指南:inout端口必须配合三态控制信号使用。如果不需要双向功能,尽量用input或output代替。
1.3 常用数据类型——wire、reg、parameter
Verilog里数据类型很多,但最常用的就三个:wire、reg、parameter。
wire(线网类型)
wire相当于物理连线。它本身不存储值,只是传递信号。在assign语句中,赋值号左边必须是wire类型。
wire a, b, c;
assign c = a & b; // c是wire类型,由a和b的与运算驱动
reg(寄存器类型)
reg代表寄存器,可以存储值。注意:reg不一定综合成寄存器,它只是行为描述中的变量。在always块中,被赋值的变量必须是reg类型。
reg [7:0] data_reg; // 8位寄存器
always @(posedge clk) begin
data_reg <= data_in;
end
这里有个坑:很多新手以为reg就一定是寄存器。其实不然。如果reg在组合逻辑中使用(比如always@(*)),它综合出来只是连线。我曾经在面试时问过这个问题,十个有八个答错。
parameter(参数)
parameter用来定义常量。它的好处是让代码更灵活、可复用。
parameter WIDTH = 8; // 定义数据宽度为8
reg [WIDTH-1:0] data; // 使用参数定义位宽
个人经验:我建议把所有可能变化的常量都用parameter定义。比如数据位宽、计数器最大值、状态编码等。这样后期修改只需要改一个地方,不用满篇找数字。
1.4 赋值语句——assign和always
赋值语句是Verilog的灵魂。主要有两种:连续赋值(assign)和过程赋值(always)。
assign(连续赋值)
assign描述组合逻辑。它右边的表达式一旦变化,左边立即更新。说白了就是:输入变了,输出马上跟着变。
assign sum = a + b; // 加法器
assign flag = (cnt == 0); // 比较器
assign语句的左边必须是wire类型。这个规则很严格,别搞混了。
always(过程赋值)
always块可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表:
- 组合逻辑:always @(*) 或 always @(a, b, c)
- 时序逻辑:always @(posedge clk) 或 always @(negedge rst_n)
看个例子:
// 时序逻辑:D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
// 组合逻辑:多路选择器
always @(*) begin
case(sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
default: out = d;
endcase
end
注意看:时序逻辑我用的是<=(非阻塞赋值),组合逻辑我用的是=(阻塞赋值)。这个区别很重要。非阻塞赋值在always块结束时才更新,而阻塞赋值是立即更新。用错了,仿真结果会完全不一样。
黄金法则:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。这是FPGA设计的铁律,违反它,你的仿真和综合结果可能天差地别。
知识体系总览
下面这张图,是我为你整理的本章知识结构。建议你保存下来,学完本章后对照着复习:
好了,这一章的内容就到这里。你可能会觉得信息量有点大,但没关系。Verilog的学习就像搭积木,先把这几块基础砖头拿稳了,后面盖高楼就顺了。下一章我们会深入讨论组合逻辑和时序逻辑的具体写法,到时候你会发现自己已经能看懂大部分代码了。
公众号:蓝海资料掘金营,微信deep3321