2. Verilog基础与RTL设计:模块化设计、组合逻辑、时序逻辑、阻塞与非阻塞赋值
好,咱们今天聊聊Verilog基础里最核心的几个概念。说实话,面试的时候,这块内容我几乎必问。为什么?因为这是区分「会写代码」和「会做设计」的分水岭。
很多新手觉得Verilog就是写代码,跟C语言差不多。嗯,这个想法很危险。Verilog本质上是描述硬件,不是写软件。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。脑子里没有硬件电路图,代码写得再花哨也没用。
2.1 模块化设计:把大问题拆成小积木
我个人习惯,拿到一个设计需求,第一件事不是写代码,而是画模块框图。模块化设计说白了就是「分而治之」——把复杂系统拆成功能独立的小模块,每个模块只干一件事。
举个例子,一个UART收发器,你可以拆成:波特率发生器、发送模块、接收模块、控制寄存器模块。每个模块单独设计、单独验证,最后再拼起来。我在项目中遇到过,有人把所有逻辑写在一个模块里,结果调试的时候,一个信号改了,整个模块都得重新综合,痛苦得很。
模块化设计的好处:
- 可复用:写好的模块,下个项目直接拿来用
- 可维护:改一个功能,只动一个模块
- 可验证:每个模块单独仿真,定位问题快
2.2 组合逻辑:assign和always@(*)
组合逻辑,就是输出只取决于当前输入,跟历史状态无关。说白了,就是一堆门电路连在一起,输入一变,输出立刻跟着变。
Verilog里描述组合逻辑有两种方式:
2.2.1 assign语句
适合描述简单的逻辑关系,比如数据选择、位运算、加法器。用起来很直接:
// 2选1多路选择器
assign out = sel ? a : b;
// 位拼接
assign data_out = {byte0, byte1, byte2, byte3};
// 三态门
assign data_bus = enable ? data_in : 1'bz;
你想想看,assign左边的信号必须是wire类型,右边可以是wire或者reg。这个细节面试常考。
2.2.2 always@(*) 语句
当逻辑比较复杂,需要if-else或case语句时,就用always@(*)。这里的星号表示敏感列表自动推导,所有输入信号变化都会触发这个块。
// 4选1多路选择器
always @(*) begin
case(sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
default: out = 1'b0;
endcase
end
组合逻辑设计要点:
- 所有赋值必须用阻塞赋值(=)
- 所有分支必须覆盖完整,否则会生成锁存器
- 避免组合逻辑反馈环路
2.3 时序逻辑:always@(posedge clk)
时序逻辑就不一样了,它依赖时钟边沿来更新状态。说白了,就是触发器在干活。时钟上升沿到来时,采样输入,更新输出。
最基本的写法:
// 带同步复位的D触发器
always @(posedge clk) begin
if (rst)
q <= 1'b0;
else
q <= d;
end
// 带异步复位的D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里有个关键点:时序逻辑里必须用非阻塞赋值(<=)。为什么?因为非阻塞赋值是「并行更新」的,所有赋值在时钟边沿同时生效,这正好符合触发器的行为。
2.4 阻塞赋值与非阻塞赋值:最容易翻车的地方
这块内容,我建议你反复看,直到形成肌肉记忆。面试时十个人里有八个会在这上面栽跟头。
核心规则就两条:
- 组合逻辑用阻塞赋值(=):描述纯组合电路
- 时序逻辑用非阻塞赋值(<=):描述寄存器电路
为什么会这样?咱们用代码说话:
// 错误示范:时序逻辑里用阻塞赋值
always @(posedge clk) begin
a = b;
c = a; // 这里c得到的是a更新后的值,不是b的原始值
end
// 正确示范:时序逻辑里用非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a; // 这里c得到的是a的旧值,即b的原始值
end
你想想看,阻塞赋值是「顺序执行」的,先更新a,再更新c。而非阻塞赋值是「并行执行」的,所有赋值同时采样、同时更新。这正好对应了硬件的行为——所有触发器在同一个时钟沿同时采样。
我曾经犯过的错:在一个状态机里,我混用了阻塞和非阻塞赋值。结果仿真通过,但上板后状态跳转完全乱套。查了两天才发现,是某个always块里用了阻塞赋值,导致状态更新顺序错了。从那以后,我写代码前都会先确认:这个always块是组合逻辑还是时序逻辑?
2.5 实战建议:写代码前先画电路
说了这么多,最后给个实用建议。每次写RTL代码前,先在脑子里或者纸上画出对应的电路图。组合逻辑就是一堆门,时序逻辑就是触发器加组合逻辑。电路图画清楚了,代码自然就写对了。
我面试时经常问候选人:「你写这段代码,综合出来是什么电路?」能答上来的,基本是靠谱的工程师。答不上来的,回去还得再练练。
嗯,今天就聊到这儿。下一章咱们讲讲状态机的设计,那可是数字电路设计的核心技能之一。