4. Verilog基础与并行思维:组合逻辑与时序逻辑、阻塞与非阻塞赋值、并行语句块、generate语句
各位同学,欢迎来到第四章。这一章,咱们要聊点真正“硬核”的东西——并行思维。
说实话,很多从软件转过来的工程师,刚接触FPGA时最大的坎儿就是这个。我当年也一样,脑子里全是“顺序执行”的惯性。写个简单的计数器,第一反应是“先判断,再累加”。结果呢?综合出来一堆莫名其妙的锁存器。嗯,那都是血泪史。
今天,我们就来彻底掰正这个思维。把组合逻辑、时序逻辑、阻塞赋值、非阻塞赋值这些基础概念,跟“并行”这个核心思想串起来。
4.1 组合逻辑 vs 时序逻辑:硬件世界的“即时”与“记忆”
FPGA里的逻辑,说白了就两种:组合逻辑和时序逻辑。
- 组合逻辑:输出只取决于当前输入。就像一根导线,输入一变,输出立刻跟着变。没有记忆,没有延迟(理想情况下)。
- 时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。它需要时钟沿来“拍一下”,才能更新状态。说白了,它有记忆。
你想想看,一个简单的与门,就是组合逻辑。一个D触发器,就是时序逻辑。FPGA里大部分电路,都是这两种逻辑的组合。
核心区别一句话:组合逻辑是“纯函数”,时序逻辑是“状态机”。
我在项目中遇到过一个问题:一个同事用组合逻辑写了很长的级联加法器,结果路径延迟太大,时序跑不过。后来改成流水线(时序逻辑),一拍一拍地算,频率一下就上去了。这就是用“空间换时间”的典型例子。
4.2 阻塞赋值与非阻塞赋值:Verilog的“灵魂拷问”
这是新手最容易翻车的地方。我见过太多人,因为赋值用错,仿真和实际行为完全不一样。
先看定义:
- 阻塞赋值(=):顺序执行。上一条赋值没完成,下一条就等着。像C语言里的赋值。
- 非阻塞赋值(<=):并行执行。所有赋值语句同时计算右值,然后在时钟沿统一更新左值。
听起来简单?但实际用起来,很多人就懵了。
我的铁律:
- 描述组合逻辑,用
always @(*)+ 阻塞赋值(=)。 - 描述时序逻辑,用
always @(posedge clk)+ 非阻塞赋值(<=)。
为什么?
你想想看,时序逻辑里,我们希望所有寄存器在同一个时钟沿同时更新。如果用了阻塞赋值,就可能出现“先更新A,再用A的新值更新B”这种顺序行为,综合出来的电路会多出不必要的级联,甚至产生竞争。
警告:千万不要在同一个always块里混用阻塞和非阻塞赋值。我曾经调试一个SPI接口,仿真怎么都对,上板就错。查了两天,发现就是在一个always块里混用了两种赋值,导致综合结果和仿真不一致。切记!
来看个例子:
// 正确的时序逻辑写法
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
reg_a <= 1'b0;
reg_b <= 1'b0;
end else begin
reg_a <= data_in;
reg_b <= reg_a; // 这里用的是reg_a的旧值
end
end
// 错误的写法(如果意图是移位寄存器)
always @(posedge clk) begin
reg_a = data_in;
reg_b = reg_a; // 这里用的是reg_a的新值,变成了两级流水
end
看到了吗?非阻塞赋值下,reg_b拿到的是reg_a的旧值,实现了真正的移位寄存器。而阻塞赋值下,reg_b拿到的是reg_a的新值,相当于只用了两个寄存器,但逻辑上是一级延迟。
4.3 并行语句块:硬件描述语言的“灵魂”
Verilog里,always块、assign语句、initial块,它们之间是并行执行的。这一点,跟软件完全不一样。
你写三个always块,它们会同时开始,同时运行。没有谁先谁后。这就是并行思维的核心。
举个例子:
// 三个并行执行的always块
always @(posedge clk) begin
counter_a <= counter_a + 1;
end
always @(posedge clk) begin
counter_b <= counter_b + 2;
end
always @(posedge clk) begin
if (counter_a == 10) begin
flag <= 1'b1;
end
end
这三个块,在同一个时钟沿,会同时计算自己的右值,然后同时更新。你不需要关心哪个块先执行,因为它们本来就是同时的。
小技巧:写Verilog时,把每个always块想象成一个独立的硬件模块。它们之间通过信号(wire/reg)通信,而不是通过“调用”。
我个人习惯,把功能相关的逻辑放在同一个always块里。比如,一个计数器的计数逻辑和清零逻辑,我会放在一起。这样代码可读性更好,也方便维护。
4.4 generate语句:批量生成,减少重复劳动
当你需要例化多个相同模块,或者重复写很多相似逻辑时,generate语句就是你的救星。
它有点像软件里的循环,但生成的是硬件。注意,它是在编译时展开的,不是运行时。
常用的两种:
- generate for:批量例化或生成逻辑。
- generate if / case:根据参数选择性地生成硬件。
来看一个generate for的例子:
// 生成8个并行的加法器
genvar i;
generate
for (i = 0; i < 8; i = i + 1) begin : adder_gen
adder u_adder (
.a(data_a[i*8 +: 8]),
.b(data_b[i*8 +: 8]),
.sum(data_sum[i*8 +: 8])
);
end
endgenerate
这个代码,相当于把8个加法器“复制”了8份。每个加法器处理8位数据。如果没有generate,你得手动写8遍例化代码,又臭又长。
再来看一个generate if的例子:
// 根据参数选择不同的实现
parameter USE_PIPELINE = 1;
generate
if (USE_PIPELINE) begin : pipe_impl
// 流水线实现
always @(posedge clk) begin
result <= a + b;
end
end else begin : comb_impl
// 组合逻辑实现
assign result = a + b;
end
endgenerate
这个技巧,我在做多协议接口时经常用。同一个模块,通过参数配置,可以生成不同的硬件结构,既节省了代码量,又保持了灵活性。
注意:generate块里的begin...end必须带标签(比如上面的adder_gen、pipe_impl)。这是语法要求,也是为了方便你通过层次化路径引用内部信号。
4.5 本章知识体系:一张图看懂
下面这张图,把本章的核心知识点串了起来。你可以看到,组合逻辑和时序逻辑是基础,阻塞/非阻塞赋值是具体实现手段,并行语句块是描述方式,而generate语句是批量化的利器。
这张图,我建议你保存下来。每次写代码前,看一眼,提醒自己:我现在写的是组合逻辑还是时序逻辑?该用哪种赋值?
4.6 避坑指南:我的血泪经验
最后,分享几个我踩过的坑,希望能帮你少走弯路。
- 锁存器(Latch)的意外生成:组合逻辑的
always块里,如果if没有else,或者case没有default,综合工具会给你生成一个锁存器。我刚开始学的时候,经常犯这个错。后来养成了习惯:组合逻辑的always块,if必有else,case必有default。 - 多驱动问题:同一个信号,不能在多个
always块里赋值。这是Verilog的硬性规定。我曾经为了图方便,在两个always块里都写了counter <= ...,结果综合报错,查了半天才发现。 - 仿真与综合不一致:这通常是因为阻塞/非阻塞赋值用错了。记住我前面说的铁律,基本能避免90%的问题。
我的习惯:写代码前,先在纸上画出电路结构。想清楚哪些是组合逻辑,哪些是时序逻辑。然后再动笔写Verilog。这样思路清晰,不容易出错。
好了,这一章的内容就到这里。并行思维不是一天能练成的,多写、多仿真、多上板调试,慢慢就习惯了。下一章,我们会把这些基础应用到实际的PMSM控制模块中,到时候你会更深刻地体会到并行思维的力量。