第一章:硬件描述语言入门(Verilog)——模块结构、数据类型、always块与assign语句、组合逻辑设计
各位同学,欢迎来到基础设施芯片开发的第一课。我是你们的讲师,一个在数字电路设计里摸爬滚打了十几年的老工程师。今天咱们聊Verilog,这门硬件描述语言。
说实话,我见过太多新人一上来就抱着语法书啃,结果越看越懵。为什么?因为Verilog不是软件编程语言,它描述的是硬件。你写的是电路,不是算法。这个观念不转变,后面会走很多弯路。
好,咱们直接进入正题。
1.1 模块结构——芯片设计的“积木块”
Verilog里最基本的设计单元叫模块(module)。你可以把它想象成一块乐高积木。每个模块都有输入、输出,内部封装了具体的逻辑功能。
一个完整的模块长这样:
module counter (
input wire clk, // 时钟信号
input wire rst_n, // 复位信号(低有效)
output reg [3:0] cnt // 4位计数器输出
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 4'b0000;
else
cnt <= cnt + 1'b1;
end
endmodule
注意看,module和endmodule是一对括号,所有代码都写在这中间。端口列表里,input和output指明了信号方向。我个人习惯把时钟和复位放在端口列表的最前面,这样一眼就能看到时序逻辑的驱动源。
uart_tx、spi_master。端口声明时,每个信号单独一行,方便后期维护和代码审查。
1.2 数据类型——wire和reg,别再搞混了
很多新手问我:“老师,wire和reg到底啥区别?”
嗯,这个问题我当年也纠结过。其实很简单:
- wire:代表连线。它本身不存储值,只是把两个端口连起来。比如模块A的输出接到模块B的输入,中间这根线就是wire。
- reg:代表寄存器。它能存储一个值,直到被新的赋值覆盖。注意,reg不一定真的综合成寄存器,它只是行为描述里的一个变量。
举个例子:
wire a, b, c;
assign c = a & b; // c是wire,用assign驱动
reg [7:0] data_reg;
always @(posedge clk) begin
data_reg <= data_in; // data_reg是reg,在always块里赋值
end
我在项目中遇到过最坑的事:有人把组合逻辑的输出声明成reg,结果综合工具报了一堆警告。其实组合逻辑用wire就够了,除非你非要在always块里写组合逻辑——那确实得用reg。
1.3 always块与assign语句——两种描述方式
Verilog里描述逻辑有两种主要方式:assign和always。
assign语句:连续赋值,用于组合逻辑。只要右边信号变化,左边立即更新。
assign sum = a + b;
assign carry = a & b;
always块:过程赋值,可以描述组合逻辑和时序逻辑。敏感列表决定了它什么时候被触发。
// 组合逻辑:敏感列表里所有信号
always @(*) begin
if (sel)
out = a;
else
out = b;
end
// 时序逻辑:敏感列表里只有时钟和复位
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
你想想看,为什么组合逻辑用@(*)?因为所有输入信号变化都要触发。而时序逻辑只关心时钟沿,因为寄存器只在时钟沿采样。
=,时序逻辑用非阻塞赋值<=。这个规则必须遵守,否则仿真结果会完全错误。我见过有人混用,结果仿真波形乱成一团,查了三天才找到原因。
1.4 简单的组合逻辑设计——从真值表到代码
组合逻辑就是输出只取决于当前输入,没有记忆功能。比如加法器、多路选择器、译码器。
咱们设计一个4选1多路选择器:
module mux4 (
input wire [1:0] sel, // 选择信号
input wire [3:0] a, b, c, d, // 四个输入
output reg [3:0] out // 输出
);
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
default: out = 4'b0000;
endcase
end
endmodule
注意,我用了default分支。为什么?因为case语句如果不写全,综合工具会生成锁存器(latch)。锁存器这东西,能不用就别用。我曾经在一个项目里因为漏了default,导致芯片功耗异常,最后定位到是latch在作怪。
再来看一个加法器:
module adder (
input wire [7:0] a, b,
input wire cin, // 进位输入
output wire [7:0] sum, // 和
output wire cout // 进位输出
);
assign {cout, sum} = a + b + cin;
endmodule
这里用拼接运算符{}把进位和结果拼在一起,简洁明了。组合逻辑用assign写,代码更直观。
知识体系总览
下面这张图总结了本章的核心知识点,你可以把它当作学习路线图:
从这张图可以看出,模块结构是外壳,数据类型是材料,描述方式是工具,最终目标就是设计出正确的组合逻辑电路。
本章小结
咱们今天讲了四个核心点:
- 模块结构:module/endmodule是基本框架,端口要分清楚方向。
- wire和reg:wire用于连线,reg用于存储。assign左边是wire,always块里被赋值的是reg。
- assign和always:assign描述组合逻辑,always可以描述组合和时序。组合逻辑用
=,时序逻辑用<=。 - 组合逻辑设计:从真值表到代码,注意case语句要写default,避免生成latch。
说实话,这些基础概念看起来简单,但真正用好需要大量练习。我建议你课后把今天讲的代码都敲一遍,跑一下仿真。遇到问题别怕,每个工程师都是从踩坑中成长起来的。
好,今天就到这里。记住,Verilog不是写代码,是画电路。带着这个思路去学,你会豁然开朗。