4. Verilog基础语法(一):模块结构、端口定义、数据类型、赋值语句
各位同学,今天我们来聊聊Verilog最基础的东西。说实话,很多初学者一上来就被各种语法吓住了,其实没那么复杂。你想想看,Verilog说白了就是描述硬件的一种语言,跟C语言写软件完全是两码事。我刚开始学的时候也犯过糊涂,总觉得这玩意儿怎么这么别扭——后来才明白,是因为脑子里还带着软件的思维。
4.1 模块结构——CPLD设计的“骨架”
在Verilog里,模块(module)是最基本的设计单元。你可以把它想象成一个黑盒子,有输入有输出,里面装着你要实现的逻辑。每个CPLD设计,本质上就是由一个个模块拼起来的。
一个完整的模块长这样:
module 模块名 (
端口列表
);
// 端口声明
// 内部信号声明
// 逻辑功能描述
endmodule
嗯,这里要注意:module和endmodule必须成对出现,就像括号一样,少一个编译器就给你报错。我在项目中遇到过好几次,写完一大段代码发现编译不过,最后检查半天——原来是endmodule漏了。这种低级错误,新手尤其容易犯。
4.2 端口定义——模块的“手脚”
端口就是模块跟外界打交道的通道。Verilog里端口分三种:
| 端口类型 | 关键字 | 方向 | 我常用的场景 |
|---|---|---|---|
| 输入端口 | input | 只能读,不能写 | 按键、时钟、复位信号 |
| 输出端口 | output | 只能写,不能读 | LED、蜂鸣器控制 |
| 双向端口 | inout | 可读可写 | 数据总线(I2C、SPI) |
写端口列表的时候,我个人习惯把时钟和复位放在最前面,这样一眼就能看到关键信号。举个例子:
module led_controller (
input wire clk, // 系统时钟
input wire rst_n, // 复位信号,低有效
input wire btn, // 按键输入
output reg led // LED输出
);
你可能会问:为什么复位信号要写成rst_n?加个_n后缀是什么意思?这是业界约定俗成的写法,表示低电平有效。我建议你也养成这个习惯,不然过两个月回头看自己的代码,可能都分不清哪个是高有效哪个是低有效。
4.3 数据类型——wire和reg
这是新手最容易搞混的地方。我当年也在这上面栽过跟头,所以今天重点讲清楚。
4.3.1 wire(线网型)
wire代表物理连线。它本身不存储值,只是把两个点连起来。比如你把一个模块的输出接到另一个模块的输入,中间这根线就是wire。
特点:
- 只能被
assign语句驱动 - 不能在
always块里被赋值 - 默认值是高阻态
Z
wire a, b, c;
assign c = a & b; // 正确,wire用assign驱动
4.3.2 reg(寄存器型)
reg代表存储单元。它可以在时钟边沿保存数据,也可以组合逻辑里用。注意:reg不一定是寄存器,它只是表示一个变量,能不能综合成寄存器要看你怎么写。
特点:
- 只能在
always块里被赋值 - 不能在
assign语句里用 - 默认值是不定态
X
reg [3:0] counter;
always @(posedge clk) begin
counter <= counter + 1; // 正确,reg在always里赋值
end
核心记忆法:
- wire = 导线,用assign
- reg = 变量,用always
- 两者不能混用,否则编译报错
我曾经见过一个同事,把输出端口定义成wire,然后在always块里给它赋值,编译报错后折腾了半天才找到原因。说白了,端口类型和数据类型要匹配:
| 端口类型 | 常用数据类型 | 说明 |
|---|---|---|
| input | wire | 输入信号来自外部,默认就是wire |
| output | wire 或 reg | 组合逻辑用wire,时序逻辑用reg |
| inout | wire | 双向端口必须是wire |
4.4 赋值语句——assign和always
赋值语句是描述逻辑的核心。Verilog里主要有两种:连续赋值(assign)和过程赋值(always)。这两种用法的区别,直接决定了你的代码是组合逻辑还是时序逻辑。
4.4.1 assign——连续赋值
assign用来描述组合逻辑。它的特点是:只要右边信号变化,左边立刻更新。说白了就是纯硬件连线,没有延迟,没有时钟。
assign led = btn ? 1'b1 : 1'b0; // 按键按下,LED亮
这个例子很简单:按键按下去,LED就亮。没有任何时钟参与,信号变化是即时的。我在做按键消抖的时候,经常先用assign写一个简单的组合逻辑做验证,然后再改成时序逻辑。
小技巧:assign语句可以同时赋值多个信号,用逗号隔开就行:
assign {led_r, led_g, led_b} = {btn_r, btn_g, btn_b};
4.4.2 always——过程赋值
always块就灵活多了。它可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。
先看一个时序逻辑的例子:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 4'd0;
else
counter <= counter + 1;
end
这个代码描述了一个4位计数器。每次时钟上升沿,计数器加1。复位信号低电平时,计数器清零。注意这里用的是<=(非阻塞赋值),这是时序逻辑的标准写法。
再看一个组合逻辑的例子:
always @(*) begin
if (sel == 1'b0)
y = a;
else
y = b;
end
这里用的是=(阻塞赋值),敏感列表写@(*)表示所有输入信号变化都触发。这个多路选择器跟用assign写效果一样,但always块里可以写更复杂的逻辑。
重要提醒:
- 时序逻辑用
<=(非阻塞赋值) - 组合逻辑用
=(阻塞赋值) - 两者混用会导致仿真和综合结果不一致,我吃过这个亏
4.5 实战小例子——一个简单的LED闪烁
说了这么多,咱们来写个完整的例子。假设我们要让LED以1Hz的频率闪烁,系统时钟是50MHz:
module led_blink (
input wire clk,
input wire rst_n,
output reg led
);
// 分频计数器
reg [24:0] cnt;
// 计数器逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 25'd0;
else if (cnt == 25'd24_999_999)
cnt <= 25'd0;
else
cnt <= cnt + 1;
end
// LED输出
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
led <= 1'b0;
else if (cnt == 25'd24_999_999)
led <= ~led;
else
led <= led;
end
endmodule
这个例子把今天讲的知识点都用上了:模块结构、端口定义、wire和reg、always块。你仔细看看,cnt是reg类型,在always块里赋值;led也是reg类型,同样在always块里赋值。两个always块是并行执行的,这就是硬件描述语言的特点——所有always块同时运行,不像软件那样顺序执行。
好了,今天的内容就到这里。下一节我们会继续讲运算符和常用结构,到时候会用到今天学的这些基础。记住:多写代码,多仿真,慢慢就找到感觉了。