nextpnr 单元重复定义报错(duplicate cell name)速查
更新时间:2026-10-23。FAQ 速查页,面向 FPGA 入门 阶段的同学。症状很典型:Yosys 综合一路绿灯,到 nextpnr 读 JSON 网表这一步突然停下,报某个 cell 名重复——可你翻遍自己的代码明明只定义了一次。先记住本篇的核心判断:nextpnr 只是报信的人,它不做综合,真正的重复几乎都发生在 Yosys 读源文件那一侧;nextpnr 把网表里两份同名单元摆出来给你看而已。顺着这个方向查,比在 nextpnr 的参数里打转有效得多。
本文要回答的问题
duplicate cell name和already defined是同一类问题吗?- 明明只写了一个
uart_rx,为什么网表里有两个同名 cell? read_verilog只写了一遍,为什么文件还是被读了两次?- 第三方黑盒 IP 和我手写的模块撞名时报错长什么样?
- generate 块展开出来的实例名也会冲突吗?
一、报错原文:读 JSON 网表时的统一文案
nextpnr 启动后第一件事是读 Yosys 输出的 .json 网表(read_json),把 modules 和 cells 装进自己的数据库。装载时发现同名键已存在,就直接拒绝:
ERROR: duplicate cell name 'uart_rx' in module 'top'或者在解析模块层级时:
ERROR: Module `uart_rx' already defined!不同后端(ice40 / ecp5 / gowin)文案略有出入,但锚点一致:duplicate、already defined,后面跟着那个撞名的标识符。先抓住报错里的名字,再去 Yosys 一侧问「这个名字是怎么进来两次的」。如果想亲眼确认网表里的重复,可以直接打开 .json 搜模块名——"modules" 字典里同一个键只该出现一次,出现两份就说明综合阶段已经把重复写进去了;这时再去翻 Yosys 的日志,往上找 Successfully finished Verilog frontend 出现了几次、每次读的是哪个文件,重复读的现场就摆在眼前。这个「JSON 核对 + 日志回数 read 次数」的组合,是后面所有排查动作的事实底座。
二、病因速查表
| 报错现象 | 最可能根因 | 快速验证 | 处置 |
|---|---|---|---|
| JSON 里同一模块出现两份定义 | 同一 .v 被 read_verilog 读了两次 | Yosys 脚本/Makefile 里 grep 该文件名 | 删多余 read 行,filelist 单一来源 |
报错两处路径指向同一 .vh | `include 无守卫,被多个文件展开 | 报错模块是否定义在头文件里 | 加 `ifndef 守卫 |
| 第三方 IP 与自写模块同名 | 两边各定义一份,名字撞车 | 全库 grep -r "module uart_rx" 数命中 | 自写侧改名或用 rename |
| 黑盒 IP 同时给仿真模型和综合网表 | 两份定义都进了读文件清单 | filelist 里同模块出现 .v 与 _synth.v | 综合只留一份,仿真模型不进综合 |
| generate 展开后实例名冲突 | 生成名与手工实例名相同 | 报错名是否形如 genblk 或 generate 内实例 | 手工实例改名避让 |
| Makefile 依赖把同一文件列了两条 | 构建系统把 filelist 拼重 | yosys -p 实际命令行里文件出现 ≥2 次 | 修 Makefile 去重 |
| 改名/删文件后报错依旧 | 旧 .json 没被重建 | 看 JSON 时间戳是否早于源文件 | 删中间产物全量重跑 |
三、排查决策图
读这张图抓住主线:红色入口之后,第一个蓝色判定只问「撞名的模块是谁的」——是自己的 RTL 就走左侧,查 Yosys 到底把它读了几遍;是第三方 IP 就走右侧,处理撞名与黑盒双网表。两条橙色修复线对应不同病根,最后统一走绿色收尾:删掉旧 JSON 全量重综合,因为陈旧的中间产物会让「已经修好」的假象继续报错。
四、逐条排查展开
第一步:全库 grep 同名模块,数定义份数。 这一步回答「这个名字到底有几份定义、各在哪」:
grep -rn "module uart_rx" rtl/ ip/ third_party/命中两处以上就有真重复;只命中一处但报错依旧,那重复多半发生在「读了几遍」而不是「写了几份」,进下一步。注意 grep 时连同 endmodule 前有没有被 `include 包进头文件一起确认——模块定义写进 .vh 又被多处 include,是「一份源码、多份展开」的典型。
第二步:核对 Yosys 实际读文件清单。 把综合命令打出来看(Makefile 里加 @echo,或直接看构建日志):
grep -n "read_verilog" synth.ys同一个文件出现两行——比如脚本里 read_verilog uart_rx.v 一次,后面又来一句 read_verilog rtl/*.v 通配把它再卷进去——Yosys 会把同名模块定义两遍,写进 JSON 后 nextpnr 立刻拒收。Makefile 依赖里把同一文件既列在 SRCS 又列在 IP_SRCS 然后拼在一起,是同性质的翻车。修法:filelist 只维护一处,通配与显式列举不混用。
第三步:检查 include 守卫与黑盒 IP。 Verilog 的 `include 是纯文本插入,没有自动去重。uart_defs.vh 里如果定义了模块(不该这么做,但老代码常见),被 a.v、b.v 各 include 一遍,综合时就展开成两份。守卫写法:
`ifndef UART_DEFS_VH
`define UART_DEFS_VH
// 只放 parameter / `define / function,模块定义留在 .v 里
`endif黑盒 IP 的场景更隐蔽:厂商同时给了 ip_uart_sim.v(仿真模型,带行为级 module 定义)和 ip_uart_synth.v(综合网表),两份文件里都有 module ip_uart。仿真用前者、综合用后者,两份都进 filelist 必然撞名。处置:综合清单里只留综合网表,仿真模型用宏开关或单独 filelist 隔开。还有一种容易忽略的情况:你自己从旧工程拷了一份 IP 的行为模型当参考,忘了从目录里删掉,结果它和 IP 厂商那份同时被通配符卷进综合——这就是为什么强调「通配符收文件前先目检目录」。Yosys 侧的完整对照见 Yosys 重复定义报错速查,那里的病因轴在综合期就会暴露。
第四步:实在撞名改不动时,用 rename 避让。 第三方 IP 加密或不便改源码时,Yosys 自带改名命令:
yosys> rename -enumerate uart_rx或按模块名精确改:rename uart_rx uart_rx_ip。改名要在 hierarchy 之后、write_json 之前做,且顶层例化名必须跟着同步,否则会把「重复定义」治成「找不到模块」——后者的排查见 nextpnr 找不到模块速查。generate 展开撞名同理:generate 块里的实例若与块外手工实例同名,展开后即冲突,把手工实例改名最省事。
五、修复后验证与防复发
修完先删掉旧的 .json 与构建中间产物,全量重跑综合和布局布线,确认 nextpnr 0 Error——增量构建可能复用旧网表,让已修好的问题继续报错。再打开 JSON 里 modules 一节扫一眼:每个模块名只出现一次,没有 _sim、_synth 双份并存。
防复发靠四条习惯:一是 filelist 单一来源,Yosys 脚本和 Makefile 都从同一份清单生成,不各自手写;二是所有 .vh 出生就带 include guard,且头文件里永不写 module 定义;三是第三方 IP 入库时先 grep 全库查重,撞名就在引入时改名,别等报错;四是黑盒 IP 的仿真模型与综合网表分 filelist 管理,综合只见一份。Quartus 闭源流程里同一问题的对照页见 Quartus 重复定义报错速查,两边病因几乎一一对应;信号名级别(非模块级)的未定义问题另见 nextpnr 未定义符号速查。
这一篇在知识体系里的位置
本篇属于 FPGA 工具报错速查簇,与 Yosys 重复定义报错速查(上游综合期的同一问题)、nextpnr 找不到模块速查(filelist 缺文件的反面)、nextpnr 未定义符号速查(信号级问题)互为姊妹篇;跨工具对照见 Quartus 重复定义报错速查。
一句话总结
nextpnr 的 duplicate cell 报错本质是 Yosys 产出的网表里同名单元出现了两份,排查主战场在综合一侧:先 grep 全库数定义份数,再核对 read_verilog 清单数读取次数,随后查 include 守卫、黑盒 IP 双网表与 generate 展开名,改不动时用 rename 改名避让;根治靠单一 filelist、带守卫的头文件、IP 入库查重和仿真/综合分清单,修完删旧 JSON 全量重跑闭环。