Yosys 数组展开成寄存器:memory_map 后的 stat 排查
更新时间:2026-10-30。本文讨论一种容易误判的现场:
memory_collect已经出现$mem_v2,后面的memory_map仍把数组变成触发器和多路选择器。它与 Yosys 数组展开成寄存器之前就没推断成功 不同:本篇的数组在前端和收集阶段都活着,展开发生在更后面。
本文要回答的问题
- 已经有
$mem_v2,为什么最终还看到大量$dff和$mux? memory_dff、memory_collect、memory_map各自改变了什么?- 异步读、整组复位什么时候是功能需求,而不是必须消灭的错误?
- 怎样改写同步读模板,并用复跑结果确认问题处在哪个阶段?
一、现象:Yosys 数组展开成寄存器发生在 memory_map
最小反例是 64×16 位数组,写口在时钟沿工作,读口直接组合输出。它没有异步写,也没有前端 Replacing memory 告警,因此不属于「根本没推断出 memory」。
module top (
input wire clk, input wire we,
input wire [5:0] waddr, input wire [5:0] raddr,
input wire [15:0] din, output wire [15:0] dout
);
reg [15:0] mem [0:63];
always @(posedge clk) begin
if (we) mem[waddr] <= din;
end
assign dout = mem[raddr];
endmodule对异步读与同步读两个版本执行同一条流水线,下面这张对比卡先给出结论:两者都能成为 $mem_v2;一旦进入通用 memory_map,两者都会展开,不能拿最终都有触发器来反推「同步读改写无效」。
这里每个 $dff 的宽度是 16 位,64 个 $dff 对应 1024 个存储位,不要把 cell 数误读成只有 64 个触发器位。上述数字只是 RTL 中间网表统计,不是标准单元面积,更没有证明某种工艺下的资源占用。
二、证据链:三个 pass 各看一次 stat
运行命令依次执行 proc; opt; memory_dff; stat; memory_collect; stat; memory_map; stat。异步读版本在 memory_dff 报告 no output FF found,因为组合读之后本来就没有输出寄存器可合并;随后 memory_collect 正常生成一个 $mem_v2。
读这段输出要盯住边界:第 8 步统计里 $mem_v2 为 1,说明收集成功;第 9 步明确写着 converting memories to logic and flip-flops,并报告创建 64 个 16 位 $dff、读接口使用 63 个 $mux。所以「谁展开了数组」有确定答案,不需要从前端告警猜起。
展开后的完整统计还显示写译码逻辑并未消失。下面保留原始输出,便于核对 $and、$eq 与 $mux 的构成:
=== top ===
Number of wires: 491
Number of wire bits: 5336
Number of public wires: 70
Number of public wire bits: 1070
Number of memories: 0
Number of memory bits: 0
Number of processes: 0
Number of cells: 358
$and 152
$dff 64
$eq 12
$mux 130另一个容易看反的细节是 Number of memories:在 memory_dff 后它仍为 1,并有 1024 个 memory bits;执行 memory_collect 后反而变成 0。这里不是数组被删了,而是统计口径从「独立的 memory 对象」切换成「已经收集的 $mem_v2 cell」。判断这一阶段要看 $mem_v2 那一行,不能只搜 memories 字段。到 memory_map 后 $mem_v2 也消失,才表示存储抽象真正被通用逻辑替代。把这三个字段按顺序读,就不会把正常的收集动作误认成优化丢失。
memory_map 的目标就是用通用逻辑表达存储语义:每个字需要写保持通路,异步读需要 64 选 1 选择树。对小数组、寄存器文件或目标器件没有可用 RAM 的情况,这是正确实现,不是综合器故障;只有预期使用块 RAM 的大数组走到这里,才是面积膨胀的前兆。
三、根因定位:展开动作与映射条件要分开
排查顺序应当是「在哪一步变成什么」,而不是看到最终 $dff 就直接改 RTL。蓝色表示检查动作,绿色表示已确认的中间状态,橙色表示需要修改,紫色用于固化复验;红色只标最终异常统计。
若 memory_collect 后没有 $mem_v2,回到前端推断问题;若有,再检查目标流程。异步读可能适合 LUT RAM,却通常不符合块 RAM 的同步读接口;整组复位清零也可能超出硬核 RAM 能力,工具只能增加外围逻辑或展开存储。展开有时是保持功能所必需,不应为了统计好看而删除功能。
还要区分两个层次:保留 $mem_v2 只是得到一种可映射抽象,并不等于已经分配物理 RAM;只有后续厂商规则接受了宽度、深度、读写时钟、使能和初始化等条件,并得到目标资源 cell,才能说映射到了 RAM。
如果业务必须同拍异步读,合理选择可能是接受寄存器与选择树,或者改用支持异步读的分布式 RAM,而不是机械套用同步模板。反之,大容量缓存若本来允许一拍延迟,同步读通常更符合块 RAM 接口。整组清零也应先确认规格:可以分批清、用有效位使旧数据失效,还是必须一个沿全部归零。不同答案对应不同电路成本,stat 只负责把代价显形,不能替设计者决定功能。
这里还要防止一种「修复过度」:为了让最终统计出现 memory,随意从脚本删掉 memory_map,却没有补上目标 RAM 映射。这样只是让流程停在中途,下游无法得到通用门级网表,也没有得到真实 RAM 宏。正确做法是让完整的目标综合流程决定哪些 $mem_v2 被资源规则接收、哪些剩余 memory 必须展开。
四、修复:同步读改写与复跑结果
若目标就是同步块 RAM,把读数据放入同一个时钟过程,接受输出晚一拍的接口变化:
always @(posedge clk) begin
if (we)
mem[waddr] <= din;
dout <= mem[raddr];
end复跑日志中,memory_dff 输出 merging output FF to cell,证明读寄存器已被并入存储读口;memory_collect 后仍保留一个 $mem_v2。但本实验继续显式执行通用 memory_map,所以同步读版本最终也变成 360 cells。这不是改写失败,而是测试流程没有执行任何厂商 RAM 映射,反而清楚展示了抽象 memory 与物理资源之间的缺口。
| 检查点 | 异步读 | 同步读 | 能得出的结论 |
|---|---|---|---|
memory_dff | 无输出 FF 可合并 | 合并输出 FF | 读口形态不同 |
memory_collect 后 | 1 个 $mem_v2,4 cells | 1 个 $mem_v2,5 cells | 两者均推断成功 |
通用 memory_map 后 | 358 cells | 360 cells | 两者均被通用逻辑实现 |
| 末尾退出码 | 0 | 0 | 两次命令都正常完成 |
实际修复验收应在目标综合流程中检查 RAM cell 或利用率报告。本文没有运行工艺映射,因此只把「同步读已被 memory_dff 识别」作为通过条件,不声称节省了面积或 BRAM 数量。
五、排查清单
- 在
memory_collect后立即stat:没有$mem_v2才查前端推断,别与后续展开混为一谈。 - 搜索
Executing MEMORY_MAP和created ... $dff:确认展开由哪个 pass 主动执行。 - 核对目标 RAM 的读延迟、写使能、复位与初始化限制;异步读和整组复位先问功能是否可改。
- 将
memory_map前的 stat 与最终资源报告分开验收;中间 cell 数不能替代面积报告。 - 改为同步读时同步修改测试平台和上游协议,额外一拍延迟本身就是功能变更。
实验环境
- 机器/系统:Mac mini M4 / macOS 15,实验在 geek-eda:0.2 Linux 容器中运行。
- 工具版本:Yosys 0.33 (git sha1 2584903a060)。
- 日期:2026-10-30。
- 完整命令:在实验目录运行
docker run --rm -v "$PWD":/work -w /work geek-eda:0.2 bash run.sh;脚本对两个源文件分别执行read_verilog; hierarchy -top top; proc; opt; memory_dff; stat; memory_collect; stat; memory_map; stat,合并记录 stdout/stderr,末尾退出码为 0。
代码获取:两个 RTL 版本、复跑脚本和完整日志位于公开仓 geek-doc.cn/experiments/ts-yosys-memory-replaced-by-registers,进入目录后运行 bash run.sh 即可复现三段统计。
这一篇在知识体系里的位置
本篇负责「已经识别为 memory,之后为何仍展开」这一阶段;前端直接摊平、没有 $mem_v2 的情况见 Yosys 数组展开成寄存器:未推断专篇。其他综合报错可从 Yosys 数组展开成寄存器及相关问题导航 分流,系列目录见 EDA 报错排查。
总结
Yosys 数组展开成寄存器不一定是推断失败:memory_collect 后的 $mem_v2 证明数组已被识别,而 memory_map 的职责就是把它改成触发器、选择器和译码逻辑。用三段 stat 定位阶段,再核对目标 RAM 是否支持异步读、复位等语义;同步读改写只有进入合适的目标映射流程才有资源意义,通用展开本身常常是功能要求而非错误。