一、FPGA不是CPU——别用软件思维写硬件

很多刚接触FPGA的朋友,第一个坑就踩在这里。

我见过不少从软件转过来的工程师,上来就问:“我这个for循环怎么跑不起来?” 嗯,问题不在于语法,而在于思维。

CPU是顺序执行的。 它一条一条读指令,一条一条执行。你写“A=B+C; D=E+F;”,CPU先算加法1,再算加法2,时间上是串行的。

FPGA是并行执行的。 你写同样的两行代码,综合出来是两个加法器同时在工作。它们之间没有先后关系,是同时完成的。

这个区别,说白了就是“软件思维”和“硬件思维”的分水岭。

核心认知: FPGA里没有“主函数”,没有“顺序执行”。你写的每一行Verilog/VHDL,都是在描述一个硬件电路。电路一旦上电,所有模块同时开始工作。

我个人习惯,拿到一个设计需求后,先画数据流图,再写代码。画图的时候,我会问自己:哪些模块可以并行?哪些必须串行?串行的部分怎么用状态机控制?

举个例子,一个简单的图像处理任务:

  • CPU做法:逐像素读取→处理→写入,循环执行
  • FPGA做法:流水线结构,读像素、处理、写入三个模块同时工作,每个时钟周期处理一个像素

你想想看,同样的时钟频率下,FPGA的吞吐量是CPU的多少倍?这就是并行的威力。

避坑指南: 我曾经接手过一个项目,同事用for循环嵌套写了三层,想实现一个矩阵运算。综合出来资源爆了,时序也一塌糊涂。后来改成状态机+并行计算,资源降了60%,频率翻了一倍。记住:FPGA里,for循环不是循环,是展开成多个硬件实例。

二、组合逻辑与时序逻辑——别把电线当成寄存器

这个误区,几乎每个FPGA初学者都会踩。

组合逻辑:输出只取决于当前输入。说白了,就是一堆门电路连在一起。输入变了,输出立刻变(不考虑门延迟)。

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。它靠时钟沿来“记住”数据。

我刚开始学FPGA时,犯过一个低级错误:想实现一个计数器,直接用组合逻辑写了个加法器,然后发现输出一直在跳,根本停不下来。为什么?因为没有寄存器来“锁存”当前值。

来看一个典型错误:

// 错误示例:想实现一个累加器
always @(*) begin
    count = count + 1;  // 组合逻辑,没有时钟控制
end

这段代码综合出来是什么?是一个组合逻辑环路。count的输出反馈到输入,每个门延迟后count就加1,理论上会无限震荡。实际仿真中,你会看到count像疯了一样跳变。

正确的写法:

// 正确示例:时序逻辑实现累加器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        count <= 0;
    else
        count <= count + 1;
end

这里的关键区别:时钟沿触发的赋值。每个时钟上升沿,count才更新一次。这就是时序逻辑。

特性 组合逻辑 时序逻辑
输出依赖 仅当前输入 当前输入+历史状态
时钟控制 有(时钟沿触发)
典型电路 与门、或门、加法器、多路选择器 寄存器、计数器、状态机、FIFO
综合结果 门电路直接连接 触发器+门电路

个人经验: 我写代码时有个习惯,先问自己“这个信号需要记住历史值吗?”如果需要,就用时序逻辑;如果不需要,就用组合逻辑。这个简单的判断,能避免80%的逻辑错误。

三、阻塞赋值与非阻塞赋值——一字之差,天壤之别

这是Verilog里最容易踩的坑,没有之一。

阻塞赋值(=):赋值立即生效。后面的语句会等前面的执行完再执行。说白了,像C语言里的赋值。

非阻塞赋值(<=):赋值不立即生效。所有语句先计算右边的值,然后统一在时钟沿更新左边的变量。

为什么会这样?因为硬件电路的行为决定的。

来看一个经典错误:

// 错误示例:在时序逻辑中使用阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;  // 期望c得到b的值,但实际可能不是
end

这段代码,如果用阻塞赋值,a先被赋值为b,然后c被赋值为a(此时a已经是b了)。所以c确实得到了b的值。看起来没问题?

但是,综合工具会怎么处理?它会生成两个寄存器,a和c。由于阻塞赋值的特性,a和c的更新顺序是确定的:a先更新,c后更新。这实际上引入了额外的依赖关系,可能导致时序问题。

更严重的情况:

// 更隐蔽的错误
always @(posedge clk) begin
    b = a;
    a = b;  // 想交换a和b的值
end

这段代码,综合出来是什么?a和b最终都等于原来的a。因为b先被赋值为a,然后a被赋值为b(此时b已经是a了)。交换失败!

正确的写法:

// 正确示例:使用非阻塞赋值实现交换
always @(posedge clk) begin
    b <= a;
    a <= b;  // 两个赋值同时生效,完美交换
end

非阻塞赋值的行为:先计算右边的值(a和b的原始值),然后在时钟沿同时更新。所以a得到b的原始值,b得到a的原始值,交换成功。

黄金法则:

  • 时序逻辑(always @(posedge clk))中,用非阻塞赋值(<=)
  • 组合逻辑(always @(*))中,用阻塞赋值(=)
  • 千万不要混用!

避坑指南: 我曾经调试过一个SPI接口,数据总是错位。查了两天,最后发现是一个always块里同时用了阻塞和非阻塞赋值。综合工具没报错,但仿真和实际行为不一致。从那以后,我写代码前都会在文件头部加注释:// 时序逻辑用<=,组合逻辑用=。这个习惯救了我很多次。

知识体系总览

下面这张图,概括了本章的核心内容。我建议你保存下来,写代码前看一眼。

FPGA基础概念误区 · 知识体系 误区1:FPGA不是CPU 核心区别 • CPU:顺序执行,串行 • FPGA:并行执行,硬件描述 • 没有主函数,没有顺序 • 先画数据流图,再写代码 误区2:组合 vs 时序逻辑 判断标准 • 组合:输出仅看当前输入 • 时序:需要记住历史状态 • 时序逻辑需要时钟沿触发 • 计数器、状态机都是时序 误区3:阻塞 vs 非阻塞 黄金法则 • 时序逻辑用 <= (非阻塞) • 组合逻辑用 = (阻塞) • 不要混用!不要混用! • 交换数据必须用 <= 核心:用硬件思维写代码,理解电路行为,而非软件行为 实践建议 1. 写代码前先画数据流图,明确并行和串行部分 2. 每个信号问自己:需要记住历史值吗? 3. 文件头部加注释:时序用<=,组合用= 4. 仿真和综合结果不一致时,先检查赋值方式

这张图把三个误区放在一起对比,方便你快速回顾。我每次培训新人,都会先让他们看这张图,理解这三个概念后再动手写代码。

最后说一句: FPGA设计,本质上是电路设计。你写的每一行代码,最终都会变成实实在在的硬件。理解了这个,你就迈过了最难的坎。剩下的,就是多练、多踩坑、多总结。

专注资料整理