开源 FPGA 工具链:用 APIO 玩转 Tang Nano 20K
APIO 把 Yosys、GTKWave 等开源工具聚合为统一工作流,本文记录在 Tang Nano 20K 上用它综合、烧录与仿真的全过程,以及与 GOWIN 官方工具的取舍。
上一篇文章里,我们尝试用 GOWIN 官方工具在 Tang Nano 20K 这块 FPGA 开发板上跑通了第一个设计。那套软件其实做得不差,但它并非开源,而且存在一些别扭之处:仿真依赖一个第三方仿真器,而这个仿真器在被收购之后已经发生了变化。与此同时,市面上免费的优秀仿真器并不少,FPGA 也早已有了完整的开源工具链。
当然,你可以把这些组件一件件自己找齐、手动拼接。但没必要——已经有若干项目在做这种整合工作。本文要看的 APIO 就是其中之一。
APIO 是什么
与其说 APIO 是一个 FPGA 工具链项目,不如说它是一个工具链项目的聚合器。它的定位让人想起 PlatformIO,官方也承认受到了后者的启发。APIO 负责更新你需要的工具、自带一套库,并为其支持的各类 FPGA 板卡提供统一的工作流程。
它既可以从命令行使用,也可以作为 Visual Studio Code 扩展安装——我选择的是后者。你只需要写一个描述项目的简单文件,剩下的基本不用操心。
安装时踩到的坑
由于 APIO 自带库,它和系统库发生冲突是有可能的。在我的机器上,~/.apio/bin/_internal 里的 libreadline.so.8 就导致整个工具完全无法工作。我把它改名挪走(直接删掉也行)之后,问题就解决了。
需要记住的是,APIO 只负责调度 Yosys、GTKWave 这类外部工具。即便你自己装了这些工具,APIO 仍会使用它私有的副本。比如我系统里的 GTKWave 版本就和 APIO 自带的不同,如果不通过 APIO 而去直接读取波形文件,就会报错。解决办法是从 VS Code 中 APIO 面板的 Tools/Misc 菜单打开一个 shell。
项目文件长什么样
我写了一个简单的 LED 闪烁设计来测试 APIO。里面的 Verilog 细节留到下次再讲,这次把它当成黑盒即可。每个 APIO 项目都需要一个 apio.ini,我的是这样:
[env:default]
board = sipeed-tang-nano-20k
top-module=led
就这么多。当然,它支持更多配置项,但多数时候并不需要。不过在后文你会看到,我通常会给这种最简项目再加一点东西。
你也可以用 apio create 命令来新建项目,并指定板卡、顶层模块和路径。但在 VS Code 里似乎没有方便的入口。我找到的最佳做法是打开 APIO 面板,点击 Tools,再选 APIO Shell,然后在其中运行 apio create;用 apio create -h 可以看到哪些参数必填、哪些可选。
各有得失
很多人会问:既然有开源方案,为什么还要碰 GOWIN 的工具?另一批人则会问:GOWIN 的工具明明能用,何必折腾这个?答案是,两者各有适用场景。
在 Verilog 层面,开源工具表现出色。借助 APIO,你可以格式化源码,并用一个成熟的 linter 检查正确性;仿真方面,APIO 也提供了很好的驱动,能调用强大的仿真工具。
GOWIN 的工具同样够用,但它的仿真器绑定的那个第三方产品,按他们目前的用法似乎已经无法获取了。这种情况当然可能改变,但优秀的开源工具就在那里,而 APIO 让使用它们变得轻而易举。
那么 GOWIN 的优势在哪?如果你想用他们的 IP 核(从产生时钟这种平凡功能,到构建一个 CPU 这种复杂模块),把它们和 APIO 集成会比较困难,虽然大概并非不可能。此外还有一个能把逻辑分析仪嵌进设计、在运行时观察内部信号的功能,同样不容易搬到 APIO 上。开源社区有类似方案,但不如内置工具那样顺手。
你还会失去好用的图形化约束编辑器——也就是把 Verilog 信号名和实际引脚对应起来的那个界面。这算小问题,约束文件本身只是文本,自己写就行。为了省事,我做了一个电子表格来辅助生成,GitHub 上还有一份经过少量测试的 Excel 版本,另存一份自己改即可。如果你习惯留在 VS Code 里,也可以试试第三方的约束编辑扩展。
还有一个绕不开的问题:哪家的综合工具效果更好?不实际测试很难下结论,而且即便测了,也可能出现一方在某方面领先、另一方面落后的情况。你要的是更高频率、更少资源占用,还是用满器件的特殊资源?唯一可靠的办法是两边都试。另外要清楚,APIO 本身并不做综合与布局布线,它只是调用这些工具;其他类似的工具链聚合项目往往用的也是同一批底层工具,所以选哪个对最终结果影响不大。
一个开源示例
示例文件可以下载。这里我重点讲 APIO 的用法,而不是 Verilog 本身。这个设计使用 27 MHz 时钟。往 FPGA 里送入时钟有好几种方式,但时钟对 FPGA 而言是特殊信号,大多数器件(包括这块板子)都有最适合作为时钟输入的专用引脚。
这次我们不看“盒子内部”,只列出 led 顶层模块的关键部分:
module led #(
parameter integer CLOCK_FREQUENCY = 27_000_000,
parameter integer INTERVAL_MS = 500
) (
input wire reset,
input wire Clock,
input wire serialin,
input wire pushbtn,
output wire [5:0] leds,
output wire serialout
);
这样模块就能接收指定的时钟频率和闪烁间隔。输入包括 reset、Clock、serialin 和 pushbtn,输出包括 LED 和 serialout(本文暂不使用串口与按键)。
把时钟配起来
插上 Nano 20K 后,你应该能看到一个串口设备。在 Linux 上可以用 ls -l /dev/serial/by-id 确认。用你惯用的串口终端连上去,多半什么也看不到——因为 FPGA 此时既不发送也不接收任何数据。
不过这个串口是经过一颗微控制器转接的,而它正是负责给 FPGA 烧录的那颗芯片。它一直在等待一个特殊字符序列 ^X^C [Enter],输入后就会给出 TangNano20K /> 提示符。如果你怀疑固件版本偏旧,可以按 Sipeed 的说明升级。注意要对照表格选择正确的固件——有些版本号看起来更新,实际并不兼容。
板上有几个可编程时钟,我们使用 O0,它对应 FPGA 的 10 号引脚。进入提示符后输入:
pll_clk O0=27M -s
不带参数运行 pll_clk 可以查看当前状态。目前我们不用 UART,但如果要用,执行 choose uart 就能把它切回普通模式。
接下来下载这个 LED 示例项目,打开 VS Code,如果还没装,就从扩展市场安装 APIO 扩展,然后打开项目。在 APIO 面板里找到 make 相关命令并选择 build,构建成功后用 upload 就能把文件写入设备的 flash(这是默认行为)。APIO 的运作方式类似 Make:如果你改了源码再上传,它会先重新构建。
我不希望每次都写 flash,而是默认烧到 SRAM。APIO 支持环境配置,所以我把 ini 文件改成了这样:
[apio]
default-env = ram
[common]
board = sipeed-tang-nano-20k
top-module = led
[env:ram]
programmer-cmd = openFPGALoader -b tangnano20k ${BIN_FILE}
[env:flash]
programmer-cmd = openFPGALoader -b tangnano20k -f ${BIN_FILE}
现在默认下载会进 SRAM。但我在 VS Code 扩展里没找到切换环境的办法,要写 flash 只能打开 APIO shell 执行:
apio upload --env flash
另有一个小细节。我经常需要临时禁用用于仿真或调试的文件,GOWIN IDE 原生支持这一点。比如同一个模块可能有三个带不同调试输出的版本。APIO 里没有简便做法,我能想到的最佳方案是把 foo.v 和 foo-test.v 改名为 foo.v.option 和 foo-test.v.option,再用软链接把 foo.v 指向其中任意一个——这很容易脚本化。或者干脆每次手动改名,同样可以写成脚本。
先测一下再说
在上真机之前先做测试是件好事。能观察所有信号、能构造特定条件时,调试往往轻松得多。做法就是写 testbench:一个驱动“真实” Verilog 并产生输出的 Verilog 文件。
在普通 Verilog 文件里,你必须小心,只做综合器能在 FPGA 上合理实现的事情,否则它有时会直接拒绝,有时则会生成你根本不想要的糟糕结果。而在 testbench 里,延时之类的写法随便用——仿真器更像是对并行执行的软件模拟,宽容得多。
下面是一个简单的 testbench,后续文章还会回到它。目前它已经在项目里了。
`timescale 1ms/1us
// 每个“tick”为 1ms,所以 #100 即 0.1 秒延迟
`default_nettype none
module led_tb;
reg clk;
reg reset;
wire [5:0] leds;
wire serialout;
// 为测试创建顶层模块
led #(
.CLOCK_FREQUENCY(10), // 告诉它按 10 Hz 工作
.INTERVAL_MS(1000)
) uut (
.reset(reset),
.Clock(clk),
.serialin(1'b0),
.pushbtn(1'b0),
.leds(leds),
.serialout(serialout)
);
// 生成仿真时钟
initial begin
clk = 0;
forever #50 clk = ~clk;
end
// 仿真“主流程”
initial begin
$dumpvars(0, led_tb); // 保存变量
reset = 1; // 产生复位脉冲
#200;
reset = 0;
#50000 // 让仿真跑一会儿
$finish; // 退出
end
endmodule
基本思路是:我们实例化设计的顶层模块 led,但让它按 10 Hz、1 秒间隔运行。你当然可以按 27 MHz 仿真,但那会生成巨大且难以处理的文件。仿真开始时,我们每 50 毫秒翻转一次时钟,因此周期为 100 毫秒,即 10 Hz。
最后一个 initial 块同样在开头执行:先指定要转储的变量,然后给出一个短暂的复位信号,接着等待很长时间(这里的 #50000 是 50 秒,但仿真器跑得飞快,不用真的等),最后调用 $finish。没有它,仿真会一直跑下去。
如果你看过其他教程,可能会问 $dumpfile 去哪了。APIO 喜欢自己设置它。它还要求文件名必须是 something_tb.v(用 SystemVerilog 则是 _tb.sv)。前面那部分可以随意取,但必须以这个模式结尾,APIO 才能识别。
如果不写 $dumpvars,你得到的会是一个空的仿真文件。
文件就位后,打开 APIO 的 verify 并选择 sim,结果会在 GTKWave 里以图形方式呈现。你还可以把层次结构中更深处的信号加进来。
通常自己写 testbench 并不难。也有人尝试用工具自动生成,效果参差不齐。多年前我 fork 过一个这类工具并做了些修改,但很容易就能找到它无法解析的 Verilog 文件。你也可以让惯用的 LLM 代劳——我让 ChatGPT 写过一个,除了若干时间参数不同,产出和上面这个几乎一模一样。
仿真之外
仿真当然只能带你走到某一步。如果你想在线调试真实设计,GOWIN 的工具也提供了手段,我们后续再看。下次我们会更深入地拆解这个示例的工作原理,并为它添加一些重要功能。
还有一堆话题要谈:亚稳态、按键去抖、跨时钟域……都挺有意思。不过看完这一篇加上前一篇,工具链的事暂时应该够用了。欢迎来 Discord 聊聊,也请期待下一篇。
讨论
0 条评论