做 Lattice FPGA 开发最让人晕的,往往不是 RTL 写得不够好,而是每次编译完之后,工程目录里冒出来的那一堆文件:.ldf、.lpf、.sdc、.edf、.vm、.bit、.jed、.svf、.vcd、.rpt……十几年了,我见过太多新同事第一次点开 impl 目录时的那种眼神:这堆文件是干什么的?该保留哪一个?到底把哪个烧进板子?这次我就从 Lattice FPGA 开发的实际流程出发,把这十几类输出文件格式整体捋一遍,讲清楚它们各自负责什么、互相之间是什么关系,顺便分享一些我自己踩过的坑,希望能帮你少走几周弯路。
很多同学会误以为,FPGA 工程跟单片机一样,最后只要一个烧录文件就够了。其实 Lattice 这类 FPGA 开发工具之所以会输出一大把文件,是因为整个从硬件描述语言到最终可落板的比特流,中间要经历多个阶段,每个阶段都会产出一批中间格式,再加上仿真、时序分析、生产烧录等场景,还要引用或生成通用交换格式。这些文件不是乱,而是每一份都有明确的用途。下面我从文件生成链路开始,一档一档拆给你看。
1. 为什么一次编译会吐出一堆文件
1.1 不是乱,是每份文件负责一个场景
FPGA 开发本质上是一条生产流水线:你先写 Verilog 或 VHDL,这叫 RTL 源码;综合器把 RTL 翻译成门级网表;布局布线器把门级网表安放到 FPGA 内部真实存在的逻辑单元和寄存器上,并连好线;最后位流生成器把这种物理连接关系写成 FPGA 上电后能加载的配置流。每一步都会落盘,所以输出文件自然就多了。
举个例子,综合后的网表文件解决的是“逻辑对不对”的问题,布局布线后的时序报告解决的是“速度快不快”的问题,位流文件解决的是“板子能不能跑”的问题,SVF 文件解决的是“产线怎么批量烧录”的问题。不同阶段、不同岗位关注的文件根本不一样,如果只留下一个最终 bit,你想定位问题反而会非常麻烦。这也是 Lattice 一直保留这么多中间文件格式的原因:它们在调试、验证、生产环节各有用处,缺一个都可能让你多花半天时间。
1.2 两代工具 Diamond 和 Radiant 的差异
Lattice 目前有两套主流开发环境,老一代的 Diamond 主要面向 ECP5、MachXO2、MachXO3 这些器件,新一代的 Radiant 则面向 CrossLink-NX、Certus-NX 等更靠近 AI 视觉和边缘计算的器件。两套工具的大类流程几乎一致,但工程文件后缀有差别:Diamond 的工程文件是 .ldf,Radiant 的工程文件是 .rdf。
很多从 Diamond 转到 Radiant 的朋友,第一件事就是想把 .ldf 直接打开,结果工具根本不认。这不是操作问题,而是两套工具对工程文件的描述方式不同。你只需要记住:老工程用 Diamond 打开,新工程用 Radiant,约束文件 .lpf 和 .sdc 两边大体通用,但综合网表文件在不同版本里可能叫 .edf 也可能叫 .vm,具体以工具生成的为准。下面我讲的主要以 Diamond 为主,Radiant 用户把工程文件后缀替换一下,其他逻辑是一致的。
2. 最常见的文件格式逐个拆开讲
2.1 工程与约束:.ldf、.lpf、.sdc 这三个必须分清楚
先说最容易混淆的三个:.ldf、.lpf、.sdc。这三个不仅长得像,功能差别也很大。.ldf 是 Diamond 的工程文件,相当于整个工程的管理器,里面记录了源码文件列表、器件型号、实现策略、引脚约束文件路径等信息。双击 .ldf 打开工程的人,看到的就是这个入口。.lpf 是 Lattice Preference File,专门用来放引脚位置、电平标准、驱动电流、差分对等物理约束。.sdc 是时序约束文件,用来定义时钟频率、输入输出延迟、跨时钟域处理等。一个管“脚放哪”,一个管“时间够不够”,分工非常明确。
我见过有新手不小心把 .lpf 文件删了,结果重新编译后所有引脚位置回到默认值,板子自然是完全没反应。也有同学把 .sdc 里 create_clock 漏写,综合和布局布线都能过,但时序分析大片飙红。这里给你一个可以直接抄的 .lpf 约束示例:
LOCATE COMP "clk_50m" SITE "C1"; IOBUF PORT "clk_50m" IO_TYPE=LVCMOS33; LOCATE COMP "vga_r[0]" SITE "D2"; IOBUF PORT "vga_r[0]" IO_TYPE=LVCMOS33 DRIVE=8 SLEWRATE=SLOW;.sdc 里面则经常长这样:
create_clock -name clk -period 10.000 [get_ports clk_50m] set_input_delay -clock clk -max 5.0 [get_ports data_in] set_output_delay -clock clk -max 4.0 [get_ports data_out] set_false_path -from [get_clocks rst_n]很多人会在网上搜“Lattice Diamond 里 PLL 怎么用”,其实 PLL 配置只是第一步,更关键的是要把 PLL 输出时钟写回到 .sdc 里,让工具认这个新时钟。比如你用一个 50MHz 输入,PLL 倍频到 200MHz,时序约束里就得再写一条 create_clock 给 clk_200m,否则后端分析按错误时钟算,结果自然一塌糊涂。
2.2 综合网表与仿真数据:.edf、.vm、.sdf、.vcd
综合完成之后,工具会把 RTL 变成门级网表,常见输出是 .edf 和 .vm。.edf 是 EDIF 格式,全称 Electronic Design Interchange Format,是当年 EDA 工具为了互相交换网表定的一套标准格式。你在 Lattice 综合之后经常能在 impl 目录里看到它,它长得不像 Verilog,但工具内部非常依赖它去做逻辑映射。.vm 是 Verilog 网表,相当于把同样的门级连接关系用 Verilog 写出来,方便你肉眼检查或者给第三方工具用。
.sdf 是标准延时文件,布局布线完成之后,每条路径的真实物理延迟会被写到 .sdf 里。做后仿真或者门级时序仿真时,需要把这个文件反标进仿真器。很多同学做完功能仿真觉得万事大吉,一到门级仿真就对不上波形,问题往往就出在没加载 .sdf。.vcd 则是仿真器生成的波形文件,它是仿真阶段的“输出结果”,不是用来烧录的。
顺便说一句,命令行编译和仿真时经常还会用 .f 文件,它就是个文件列表,把所有 RTL、约束、IP 路径列在一起,方便给综合器或者仿真器一次性读取。这个文件虽然不起眼,但在自动化流程里非常好用,建议你养成写 .f 的习惯。
2.3 配置与烧录:.bit、.jed、.bin、.hex、.svf
这是最容易烧错的一类,也是出厂环节最关心的。.bit 是 Lattice 生成的 FPGA 配置比特流,通常通过 JTAG 直接写进 FPGA 内部的 SRAM,上电调试用它非常方便,但掉电就丢。.jed 是 JEDEC 格式,常见于 MachXO2、MachXO3 这种带内部 Flash 的器件,它会把配置写到芯片内部非易失区域,掉电后依然保留。
如果你的板子上用的是外部 SPI Flash,比如常见的 W25Q 系列,那么量产烧录一般不推荐直接烧 .bit,而是要把位流转成 .bin 或者 .hex 后再写入 Flash。.bin 是纯二进制,.hex 是 Intel HEX 文本格式,前者烧写速度快,后者更容易用普通文本工具查看内容。很多工程师第一次做样片时忘记转换,直接把 .bit 烧进外部 Flash,上电后板子毫无反应,就是因为没搞清楚 SRAM 配置和 Flash 固化的区别。
.svf 是一类更偏生产的标准文件,全称 Serial Vector Format。它不是某个 FPGA 私有格式,而是一串对 JTAG 引脚 TMS、TCK、TDI、TDO 的操作描述。产线上的烧录器或者测试治具只要支持 SVF,就可以不管 Lattice 的 Programmer 软件,直接批量刷板子。用 SVF 做量产的好处是通用、可审计、容易集成到自动化测试系统里,缺点是执行速度往往比专用格式慢一些,适合对单板烧录时间不敏感的场合。
2.4 一张速查表,收藏起来
下面这张表整理了我自己在 Lattice FPGA 开发里最常见的文件后缀,按功能大类来分,方便你随时查:
| 后缀 | 常见名称 | 主要用途 |
|---|---|---|
| .ldf | Diamond 工程文件 | 工程入口,Diamond 双击打开 |
| .rdf | Radiant 工程文件 | 新版工具工程入口 |
| .prf | 工程配置文件 | 保存工程级属性与实现选项 |
| .lpf | 物理约束文件 | 引脚位置、IO电平、驱动能力 |
| .sdc | 时序约束文件 | 时钟、延迟、路径约束 |
| .edf | EDIF 网表文件 | 综合后的标准网表交换格式 |
| .vm | Verilog 网表文件 | 综合后的 HDL 网表 |
| .sdf | 标准延时文件 | 布局布线后的延迟反标 |
| .vcd | 波形文件 | 功能/时序仿真输出波形 |
| .wlf | ModelSim 波形文件 | ModelSim/Questa 专用波形 |
| .bit | 比特流文件 | JTAG 在线调试配置 FPGA |
| .jed | JEDEC 文件 | 内部 Flash 固化配置 |
| .bin | 二进制文件 | 外部 SPI Flash 烧写 |
| .hex | 十六进制文件 | 外部 SPI Flash 烧写 |
| .svf | 串行矢量文件 | 产线批量烧录/边界扫描 |
| .rpt | 各类报告文件 | 资源、时序、功耗等报表 |
| .tcl | TCL 脚本文件 | 命令行流程自动化 |
这张表我建议你直接截图或者存到本地。等你实际跑过两三个项目,再回头看这张表,会发现大部分坑都出在“把当成另一个”。
3. 文件在完整开发流程里的位置
3.1 RTL 到比特流的五级流水线
理解了文件类型,还要知道它们在哪一步出现。我把 Lattice 的典型流程拆成五级:综合、映射、布局布线、时序分析、位流生成。RTL 源码和 IP 加上 .sdc,经过综合后生成 .edf/.vm;网表和 .lpf 一起进入映射与布局布线,这时候输出布局布线结果、资源报告、时序报告以及 .sdf;最后一步 Generate Bitstream 会根据布局布线结果生成 .bit/.jed/.bin/.hex。
这个流程在 Diamond 里可能是一键跑完的,但排查问题的时候一定要知道是哪一级出问题。比如综合报错,你的 RTL 有语法或逻辑问题,去改源码;布局布线报错,可能是引脚约束冲突或者资源不足,去看 .lpf 和报告;时序报告全红,问题基本在 .sdc 约束或代码跨时钟域处理上。如果你连哪一级产生哪个文件都不知道,看到一堆报错只能瞎猜。
3.2 版本控制里哪些该留、哪些该删
很多团队在管理 Lattice FPGA 代码时,会把整个 impl 目录都提交到 Git 里,结果仓库越滚越大,每次 diff 都是几千行乱码。我个人的习惯是:源码、约束文件和脚本必须提交,.ldf 和 .prf 这类工程描述文件也提交,但综合和布局布线产生的中间文件一律不提交,每次重新生成就行。最终要保护的 .bit/.jed 产品文件,可以放到 release 目录并标记版本。
一个需要注意的细节是,Diamond 的 .ldf 工程文件里往往会记录源码文件的绝对路径。如果你把工程从一台电脑拷到另一台电脑,双击 .ldf 可能提示找不到文件。解决思路不是去手工改几十条绝对路径,而是用文本编辑器打开 .ldf,把里面的路径统一改成相对路径,或者干脆用 TCL 脚本重新建立工程映射。
3.3 为什么报告文件值得认真看
报告文件 .rpt 是最没存在感但也最值钱的文件。很多人看到 .rpt 就直接忽略,或者只看最后有没有 PASS,这是非常亏的。布局布线报告会告诉你资源占用率、引脚映射情况、拥塞区域;时序报告会告诉你关键路径在哪、是 setup 问题还是 hold 问题;功耗报告则会给出各模块供电估算。我调 Lattice 图像采集板的时候,曾经遇到过明明功能正常但时序收敛不了的怪问题,打开布局布线报告才发现关键路径走了很远的通用布线资源,原因是某个多路选择器被综合工具安排到了芯片对角,加一个物理区域约束就解决了。如果没有看报告这一步,后面很容易把问题上升到重写 RTL,白白浪费时间。
4. 选错文件的典型场景:烧录、固化、仿真对比
4.1 SRAM 配置和 SPI Flash 固化是两回事
FPGA 内部没有像单片机那样的 Flash 程序区,多数 Lattice FPGA 是 SRAM 工艺,上电后配置数据短时间丢失。你通过 JTAG 下载 .bit 到芯片,这个数据是直接写进 SRAM 的,用来调试非常合适,改代码重新编译后再次下载就是新版本。但如果你想让它断电后自动启动,就必须把配置数据放到非易失存储里。外部 SPI Flash 是常见方案,这时要烧写 .bin 或者 .hex;带内部 Flash 的 MachXO2 等器件则用 .jed。
新手最容易犯的错误就是:调试阶段一直用 .bit,等板子要交付的时候还习惯性把 .bit 丢给产线,结果产线反馈“烧好之后断电重启就没程序”。这不是芯片坏了,是选错了文件格式。建议在项目初期就在 README 里写清楚:调试用 .bit,量产固化用 .bin/JEDEC,线上生产用 .svf。
4.2 量产为什么要用 SVF
可能有人觉得,既然 .bit 也能批量烧录,为什么还要多此一举用 .svf?关键差别在于 .bit 是厂商私有的二进制格式,通常要配合 Lattice Programmer 软件;而 .svf 是标准 JTAG 描述文本,任何支持 SVF 的烧录器、测试台、机械臂控制程序都可以按同样流程刷写。对产线来说,兼容性比速度重要得多。
我用 SVF 做过几十块板子的连续烧录,流程很简单:先用 Diamond Programmer 打开工程,选择 SVF File 作为输出,生成 .svf;然后用产线电脑的通用烧录软件加载 .svf,通过 USB-JTAG 或者治具接口逐台烧写。这样即使换一台没有装 Lattice 的电脑,也不影响产线正常生产。唯一要注意的是 SVF 文件里记录的是 JTAG 操作序列,如果 FPGA 的 JTAG 链上还有其他器件,生成时要选对链配置,否则会把别的器件也卷进来。
4.3 仿真的 VCD 和 SDF 千万别搞混
我曾经带过的一个实习生,把 VCD 文件当成 SDF 文件拿来反标,结果仿真波形怎么跑都不对,他一度以为是 Lattice 仿真库坏了。这里要分清:VCD 是仿真器根据信号变化 dump 出来的记录,它是“仿真结果”;SDF 是布局布线后计算出来的延时信息,它是“仿真输入”。功能仿真时可以不加载 SDF,因为门级延时不重要;但门级仿真或者时序仿真时,必须把布局布线后的 SDF 用 $sdf_annotate 加载进 testbench,否则你仿真的还是理想延时电路,当然对不上真实硬件行为。
initial begin $sdf_annotate("./impl_video/video.sdf", tb_top.u_ecp5); end这段代码的意思是,把布局布线产生的延时信息反标到被测模块实例上。注意路径要和你的测试平台层级一致,否则仿真器会静默忽略掉。
5. 实战案例:ECP5 视频采集项目的目录清理与自动构建
5.1 先说一个让我崩溃的真实目录
前年我给一块 ECP5 视频采集卡做升级,板子上有 MIPI 输入、DDR 读写、HDMI 输出,每次全编译完,impl 目录里能堆出几百个文件。当时我为了找最终要提交给产线的 .bin 文件,在目录里翻了好一阵。文件名的近似度非常高,有带 rpt 的、有带 tlg 的、有带 sdf 的,还有各种版本号的 bit 文件。后来我下定决心把构建流程自动化,直接一键生成产物到独立目录,再也不想手动翻 impl。
那次之后我养成了一个习惯:顶层目录固定分成 src、constr、script、build、release 五个文件夹。src 放 RTL,constr 放 .lpf 和 .sdc,script 放 TCL 和批处理脚本,build 放所有中间产物,release 放最终烧写文件和版本说明。这个结构看着简单,但能救你很多次。
5.2 用 TCL 脚本一条龙跑到底
Lattice Diamond 支持命令行工具 diamondc,可以用 TCL 脚本完成整个编译流程。下面是我当时用过的核心脚本结构,你可以直接照着改:
set impl "impl_video" prj_project open ./video.ldf prj_impl active $impl prj_run Synthesis -impl $impl prj_run Translate -impl $impl prj_run Map -impl $impl prj_run Place & Route -impl $impl prj_run Timing Analysis -impl $impl prj_run Generate Bitstream -impl $impl prj_project close在终端里执行 diamondc build.tcl,它就会把整个工程跑完。具体命令名可能会随 Diamond 版本有细微差异,但流程大差不差。跑完之后,再用一段 shell 脚本把最终产物拷贝到 release 目录:
#!/bin/bash mkdir -p release cp build/impl_video/*.bit release/video_$(date +%Y%m%d).bit cp build/impl_video/*.jed release/video_$(date +%Y%m%d).jed 2>/dev/null || true cp src/*.v release/source_snapshot/ cp constr/video.lpf release/ cp constr/video.sdc release/ echo "build done"这样每次构建完,你只需要看 release 目录,不要管中间文件。以前我连 bit 文件都可能选错,现在目录结构清楚之后,一次都没再犯过。
5.3 归档只留文件清单
归档项目时,也不要给客户或同事发整个 impl 目录,那等于把一个布满中间文件的垃圾场发给了别人。我通常只保留:RTL 源码、约束文件、TCL 脚本、README、release 目录里的 bit/jed/bin。如果对方需要复现编译过程,把源码和脚本交给他,在 Lattice 工具里重新跑一遍即可。只要 RTL 版本固定、工具版本一致,生成的比特流是可以重复的,没必要把几百兆中间文件都搬过去。
6. 常见问题与排查速查表
6.1 最容易踩的五个坑
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 打开工程后提示找不到源码或约束文件 | .ldf 记录的是绝对路径 | 用文本编辑器打开 .ldf 修正路径,或用 TCL 重新映射 |
| JTAG 下载 .bit 正常但掉电丢失 | SRAM 配置本来就是易失的 | 改用 .bin/.hex 烧外部 SPI Flash,或 .jed 烧内部 Flash |
| 引脚明明分配了,板子却无响应 | .lpf 没有参与当前实现 | 检查 Design 是否包含 .lpf 文件,并用 Floorplan 核对 |
| 时序报告全红但功能正常 | .sdc 时钟约束缺失或不准 | 检查 create_clock,特别要把 PLL 输出时钟单独约束 |
| 门级仿真波形怎么都对不上 | 没加载布局布线后的 .sdf | 在 testbench 里通过 $sdf_annotate 反标延时文件 |
6.2 排查思路指南
排查文件相关问题时,我习惯先问自己“现在这个阶段该产出的文件是什么”。综合报错就去看综合日志,布局布线报错就去看 Map/PAR 报告,时序问题就去看 Timing Report,烧录问题就去看 Programmer 的下发日志,仿真问题就去看仿真器有没有加载正确的网表和 SDF。按阶段切分之后,问题通常能缩小到一个很小的范围。
如果单纯是文件不确定,最快的办法是在工程里重新跑一步对应 Flow,观察工具此时刷新了哪个文件。比如你在 Generate Bitstream 之前刷新了 .jed,那说明 .jed 和这一步强相关;你在 Place & Route 之后看到 .sdf 更新,就知道 .sdf 是布局布线的产物。多跑几遍,你就会发现这些后缀之间其实有非常清晰的时间线关系。
说实话,真正让我把这一堆 Lattice 输出文件彻底记牢的,不是翻手册,而是选错 .bit、看漏时序报告、乱拷工程导致 .ldf 路径失效,一次次踩出来的经验。文件格式本身并不复杂,它们只是在对应各自阶段的职责罢了。你只要在项目里也按“源码、约束、中间产物、发布产物”分开归档,不出两个项目,就会觉得这十几类格式其实条理非常清楚。