开源 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 此时既不发送也不接收任何数据。

TangNano20K 的菜单

不过这个串口是经过一颗微控制器转接的,而它正是负责给 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 条评论

同分类推荐

开源