鸿蒙PC平台 YosysNextpnr FPGA 编辑器适配实战:基于 Electron 壳方案的纯 JavaScript 综合与布局布线引擎开发
鸿蒙PC平台 YosysNextpnr FPGA 编辑器适配实战:基于 Electron 壳方案的纯 JavaScript 综合与布局布线引擎开发
YosysNextpnr FPGA 编辑器是一款参考 YosysHQ/yosys 与 YosysHQ/nextpnr 公开算法思想、纯 JavaScript 实现的 FPGA 综合与布局布线演示工具,提供 Verilog 词法分析与递归下降语法分析(AST)、层次展开(Elaboration)、位级 IR 综合(常量折叠 / DFF 推断 / 死代码消除)、LUT4 技术映射(真值表枚举 + 结构化哈希 CSE)、确定性模拟退火布局(xorshift32 固定种子)、曼哈顿 L 形布线、静态时序分析(STA)等完整 FPGA 设计流程,以及 SVG 网表图与芯片版图可视化、三栏 IDE 布局、Catppuccin Mocha 深色主题。本文记录在鸿蒙 PC 平台上,基于 Electron 壳方案从零构建一个具备 Verilog 编辑、7 步设计流程、门级网表与布局布线可视化的完整 FPGA 设计工具的全过程——涵盖位级 IR 构建、LUT4 真值表映射、模拟退火布局引擎、曼哈顿布线与线延迟计算、关键路径回溯与 Fmax 估算、以及鸿蒙安全适配等核心功能的实现细节与踩坑经验。
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_YosysNextpnr
一、技术架构分析
1.1 整体架构
本项目采用 Electron Web 层 + 鸿蒙 HAP 壳工程的分层架构:
- Electron Web 层:运行于 ArkWeb 引擎,提供完整的 Verilog 编辑、词法语法分析、层次展开、综合优化、LUT4 技术映射、布局布线、静态时序分析、SVG 网表与版图可视化等 FPGA 设计能力
- 鸿蒙 HAP 壳工程:通过 web_engine 模块加载 Web 应用,提供窗口管理、文件系统访问、原生对话框、工具链调用(Yosys / nextpnr 外部进程)等系统能力
这种架构的核心优势在于:业务代码与平台完全解耦,Electron 部分可以在任何平台独立运行,鸿蒙壳仅负责适配和打包。与采用 contextBridge 安全模型的 Electron 应用不同,本工程采用 nodeIntegration:true + contextIsolation:false 的直通模式——渲染进程直接访问 Node.js 的 fs、path、childProcess、electron 的 ipcRenderer 等 API,主进程负责 dialog 对话框、文件读写与外部工具链调用 IPC,FPGA 引擎逻辑全部在渲染进程内完成。
1.2 技术方案
| 对比项 | 传统 FPGA 工具(Yosys / nextpnr / Vivado) | 鸿蒙适配版(YosysNextpnr) |
|---|---|---|
| 综合引擎 | C++(Yosys pass 链) | 纯 JavaScript 位级 IR + 常量折叠 + DFF 推断 |
| 技术映射 | LUT 库匹配 / ABC 优化 | 真值表枚举 + 结构化哈希 CSE(≤4 输入直接映射) |
| 布局算法 | simulated annealing(nextpnr placer1) | 确定性模拟退火(xorshift32 固定种子) |
| 布线算法 | router2 迷宫路由 | 曼哈顿 L 形布线 + 拥挤度统计 |
| 时序分析 | STA 引擎(SDF / Liberty) | 简化延迟模型(IBUF/LUT4/DFF/OBUF + 线延迟) |
| 可视化 | 第三方波形 / 版图查看器 | 内嵌 SVG 网表图 + 芯片版图 + 关键路径报告 |
| 部署方式 | 安装包 / EDA 许可证 | HAP 包(鸿蒙应用) |
| 运行环境 | Windows / Linux | 鸿蒙 PC(ArkWeb 引擎) |
1.3 核心功能清单
| 功能模块 | 具体能力 |
|---|---|
| Verilog 词法 | tokenize 关键字表 + 数字字面量(b/o/d/h)+ 行/块注释 + 多字符运算符 |
| Verilog 语法 | 递归下降 Parser → AST(module / port / wire / reg / assign / always / instance) |
| 层次展开 | 实例递归展开 + 端口连接转赋值 + prefixAst 深拷贝加前缀 + 递归实例化检测 |
| 综合核心 | 位级 IR(sig/const/not/and/or/xor/mux)+ 常量折叠 + evalBits 全运算符 |
| 过程块综合 | DFF 推断(异步复位分支拆解)+ 组合任务拓扑排序 + 环路检测 |
| 死代码消除 | 以输出端口为根 markIR/markNet 可达性分析 |
| LUT4 映射 | 真值表枚举(collectLeaves + evalTtNode)+ 结构化哈希 CSE + 超 4 输入分解 |
| 布局引擎 | IO 环 + 逻辑蛇形初始放置 + 模拟退火 HPWL 优化(确定性 PRNG) |
| 布线引擎 | 曼哈顿 L 形布线 + 拥挤度统计 + 线延迟计算 |
| 静态时序分析 | 到达时间递归 + 关键路径回溯 + Fmax / Slack 估算 |
| SVG 可视化 | 网表图(层级分列 + 贝塞尔连线)+ 芯片版图(网格 + 走线 + 图例) |
| 文件操作 IPC | 4 个通道(openVerilog + read + saveDialog + write) |
| 工具链 IPC | 4 个通道(checkYosys + checkNextpnr + runYosys + runNextpnr) |
| 鸿蒙安全 | 菜单移除 + 禁用硬件加速 + 新窗口拦截 |
| 深色主题 | Catppuccin Mocha 配色 + CSS 变量体系 |
二、环境准备



2.1 开发环境要求
| 项目 | 版本/信息 |
|---|---|
| 操作系统 | Windows 10/11 |
| 核心框架 | Electron (Node.js + Chromium) |
| 技术栈 | HTML5/Vanilla JavaScript |
| FPGA 引擎 | 纯 JavaScript(2109 行) |
| 目标器件 | Generic-LUT4(100 MHz) |
| 目标设备 | 鸿蒙 PC |
| 目标架构 | arm64-v8a |
| 开发工具 | DevEco Studio(鸿蒙官方 IDE) |
2.2 项目结构
ohos_hap/
├── electron/ # Electron 壳
├── web_engine/ # 鸿蒙壳工程
│ └── src/main/resources/resfile/resources/app/ # 部署目录(业务代码平铺)
│ ├── main.js # 主进程(窗口管理 + 文件 IPC + 工具链 IPC)
│ ├── renderer.js # 渲染进程(UI 交互 + 流程调度 + SVG 可视化)
│ ├── yosys-engine.js # FPGA 引擎(词法→语法→展开→综合→映射→布局→布线→STA)
│ ├── index.html # 页面布局(三栏 IDE + 5 标签页 + 关于对话框)
│ ├── package.json # 项目配置
│ └── styles/
│ └── yosys-nextpnr.css # 深色主题样式(Catppuccin Mocha)
└── AppScope/ # 应用配置(bundleName: com.wei.yosysnextpnr)
2.3 依赖说明
| 依赖 | 版本 | 用途 |
|---|---|---|
| electron | 系统内置 | 桌面应用框架 |
| Yosys | 参考设计 | 综合与优化算法思想(ISC 协议) |
| nextpnr | 参考设计 | 布局布线算法思想(ISC 协议) |
2.4 应用身份信息
| 字段 | 值 | 说明 |
|---|---|---|
| bundleName | com.wei.yosysnextpnr | 应用唯一标识 |
| vendor | wei | 开发者 |
| versionName | 1.0.0 | 应用版本 |
| 应用名称 | YosysNextpnr FPGA 编辑器 | 中文显示名 |
| 英文名 | yosys-nextpnr | package.json name |
| 许可证 | Apache-2.0 | 开源协议 |
2.5 构建与部署
构建流程遵循鸿蒙 HAP 壳工程的标准步骤:
- 在 DevEco Studio 中打开 ohos_hap 工程
- 确保 web_engine/src/main/resources/resfile/resources/app/ 部署目录包含所有业务文件
- 选择 arm64-v8a 目标架构,构建 HAP 包
- 安装到鸿蒙 PC 设备运行
2.6 部署目录同步
每次修改 Electron 层代码后,需将文件同步到部署目录:
Remove-Item "web_engine/src/main/resources/resfile/resources/app/*" -Recurse -Force
Copy-Item "electron/src/*" "web_engine/src/main/resources/resfile/resources/app/" -Recurse -Force
三、核心适配流程
3.1 主进程:窗口创建与安全配置
主进程负责窗口管理、文件操作 IPC 和外部工具链调用,采用 nodeIntegration:true 直通模式:
// main.js —— 窗口创建与安全配置
var electron = require('electron');
var app = electron.app;
var BrowserWindow = electron.BrowserWindow;
var screen = electron.screen;
var ipcMain = electron.ipcMain;
var dialog = electron.dialog;
var Menu = electron.Menu;
var fs = require('fs');
var path = require('path');
var childProcess = require('child_process');
app.disableHardwareAcceleration();
var mainWindow = null;
function createWindow() {
try {
var display = screen.getPrimaryDisplay();
mainWindow = new BrowserWindow({
width: Math.floor(display.workAreaSize.width * 0.95),
height: Math.floor(display.workAreaSize.height * 0.90),
frame: true,
transparent: false,
resizable: true,
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
mainWindow.loadFile('index.html');
} catch (e) {
console.warn('[YosysNextpnr] 创建窗口失败:', e.message);
}
}
app.whenReady().then(function() {
Menu.setApplicationMenu(null);
createWindow();
});
app.on('window-all-closed', function() { app.quit(); });
关键配置说明:
- disableHardwareAcceleration:禁用硬件加速,避免鸿蒙 GPU 兼容性问题
- 菜单移除:Menu.setApplicationMenu(null),使用自定义 toolbar
- 窗口尺寸:主显示器 workAreaSize 的 95% 宽 × 90% 高
- 安全模型:nodeIntegration:true + contextIsolation:false,渲染进程直接访问 Node.js API
3.2 文件操作 IPC:4 个通道
主进程提供 4 个 IPC 通道处理文件操作:
// main.js —— 文件操作 IPC
// 打开 Verilog 文件(.v / .sv / .vh 过滤器)
ipcMain.handle('file:openVerilog', function() {
return dialog.showOpenDialog(mainWindow, {
title: '打开 Verilog 文件',
filters: [
{ name: 'Verilog 文件', extensions: ['v', 'sv', 'vh'] },
{ name: '所有文件', extensions: ['*'] }
],
properties: ['openFile']
});
});
// 读取文件内容
ipcMain.handle('file:read', function(event, filePath) {
try {
var content = fs.readFileSync(filePath, 'utf-8');
return { success: true, content: content };
} catch (e) {
return { success: false, error: e.message };
}
});
// 保存文件对话框
ipcMain.handle('file:saveDialog', function(event, defaultName, filters) {
return dialog.showSaveDialog(mainWindow, {
title: '保存文件',
defaultPath: defaultName,
filters: filters || [{ name: '所有文件', extensions: ['*'] }]
});
});
// 写入文件
ipcMain.handle('file:write', function(event, filePath, content) {
try {
fs.writeFileSync(filePath, content, 'utf-8');
return { success: true, size: content.length };
} catch (e) {
return { success: false, error: e.message };
}
});
openVerilog 使用 .v/.sv/.vh 三重扩展名过滤器,覆盖 Verilog / SystemVerilog / 头文件;saveDialog 支持外部传入默认文件名与过滤器,渲染进程首次保存时弹框选择路径,后续直接写入。
3.3 工具链调用 IPC:4 个通道
主进程还提供 4 个 IPC 通道调用外部 Yosys / nextpnr 工具链(作为可选的辅助路径,主流程使用纯 JS 引擎):
// main.js —— 工具链调用 IPC
// 检查 Yosys 是否可用
ipcMain.handle('tool:checkYosys', function() {
try {
var result = childProcess.spawnSync('yosys', ['--version'], { encoding: 'utf-8' });
if (result.status === 0) {
return { available: true, version: result.stdout.trim() };
}
return { available: false, error: 'Yosys not found' };
} catch (e) {
return { available: false, error: e.message };
}
});
// 运行 Yosys 综合(60 秒超时)
ipcMain.handle('tool:runYosys', function(event, verilogCode, outputPath) {
try {
var tempDir = path.join(app.getPath('temp'), 'yosys-nextpnr');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
var tempFile = path.join(tempDir, 'design.v');
fs.writeFileSync(tempFile, verilogCode, 'utf-8');
var yosysScript = 'read_verilog ' + tempFile + '\n';
yosysScript += 'synth -top auto\n';
yosysScript += 'write_json ' + outputPath + '\n';
var scriptFile = path.join(tempDir, 'synth.tcl');
fs.writeFileSync(scriptFile, yosysScript, 'utf-8');
var result = childProcess.spawnSync('yosys', ['-s', scriptFile], {
encoding: 'utf-8',
timeout: 60000
});
return {
success: result.status === 0,
stdout: result.stdout,
stderr: result.stderr,
output: outputPath
};
} catch (e) {
return { success: false, error: e.message };
}
});
工具链通过 childProcess.spawnSync 同步调用,Yosys 综合超时设为 60 秒,nextpnr 布局布线超时设为 120 秒。临时文件存放在 app.getPath(‘temp’)/yosys-nextpnr/ 目录。但本应用的核心设计流程不依赖外部工具链——yosys-engine.js 用纯 JavaScript 实现了完整的 FPGA 综合与布局布线引擎。
四、Verilog 词法分析与递归下降语法分析
4.1 词法分析器(Lexer)
tokenize 函数将 Verilog 源代码转换为 token 流,支持关键字识别、数字字面量(b/o/d/h 进制)、行注释与块注释、多字符运算符:
// yosys-engine.js —— 词法分析器核心
var V_KEYWORDS = {
module: 1, endmodule: 1, input: 1, output: 1, inout: 1, wire: 1, reg: 1, logic: 1,
assign: 1, always: 1, begin: 1, end: 1, posedge: 1, negedge: 1, if: 1, else: 1,
case: 1, endcase: 1, default: 1, parameter: 1, localparam: 1, integer: 1,
generate: 1, endgenerate: 1, initial: 1, supply0: 1, supply1: 1
};
function tokenize(code) {
var tokens = [];
var i = 0, line = 1;
var n = code.length;
function readNumber() {
var text = '';
while (i < n && /[0-9]/.test(code[i])) { text += code[i]; i++; }
if (code[i] === "'") {
i++;
var baseCh = (code[i] || '').toLowerCase();
var bases = { b: 2, o: 8, d: 10, h: 16 };
if (bases[baseCh] !== undefined) {
i++;
var digits = '';
while (i < n && /[0-9a-fA-FxXzZ?_]/.test(code[i])) { digits += code[i]; i++; }
digits = digits.replace(/_/g, '');
var width = text ? parseInt(text, 10) : 32;
if (!digits || /^[xXzZ?]+$/.test(digits)) {
return { kind: 'num', value: 0, width: width, text: 'x', line: line };
}
var val = parseInt(digits, bases[baseCh]);
if (isNaN(val)) { val = 0; }
return { kind: 'num', value: val, width: width, text: digits, line: line };
}
}
return { kind: 'num', value: parseInt(text, 10) || 0, width: 32, text: text, line: line };
}
// ... 主循环处理空白、注释、标识符/关键字、多字符运算符、单字符
}
词法分析器的关键设计:
- 关键字表 V_KEYWORDS 覆盖 28 个 Verilog 常用关键字,O(1) 哈希查找区分关键字与标识符
- readNumber 处理 Verilog 特有的进制字面量(如 8’b1010_0101、16’hFF),支持 x/z 不确定值与下划线分隔符
- 块注释保留行号计数,确保错误报告的行号准确
- 多字符运算符(<=、>=、==、!=、<<、>>、&&、||、->)优先匹配,避免与单字符运算符冲突
- 未知字符容错跳过,不中断分析流程
4.2 递归下降语法分析(Parser → AST)
Parser 类实现递归下降语法分析,将 token 流转换为层次化的 AST 结构:
// yosys-engine.js —— Parser 核心方法
function Parser(tokens) {
this.tokens = tokens;
this.pos = 0;
this.errors = [];
}
Parser.prototype.peek = function(off) {
var idx = this.pos + (off || 0);
return this.tokens[idx] || this.tokens[this.tokens.length - 1];
};
Parser.prototype.next = function() { return this.tokens[this.pos++] || this.tokens[this.tokens.length - 1]; };
Parser.prototype.at = function(text) { return this.peek().text === text; };
Parser.prototype.eat = function(text) {
if (this.at(text)) { this.pos++; return true; }
return false;
};
Parser.prototype.expect = function(text, ctx) {
if (this.at(text)) { return this.next(); }
var t = this.peek();
this.errors.push({ message: '期望 "' + text + '"' + (ctx ? '(' + ctx + ')' : '') +
',实际为 "' + (t.text || (t.kind === 'eof' ? '文件结尾' : String(t.value))) + '"', line: t.line });
return null;
};
Parser 支持完整的 Verilog 子集解析:
- parseModule:解析 module 定义,包括参数列表 #(…)、端口列表、模块体
- parsePortDecl:非 ANSI 端口声明(input [3:0] a, b;),支持合并位宽
- parseWireRegDecl:wire/reg/logic/integer 声明
- parseAssign:连续赋值语句
- parseAlways:always 块(敏感列表 + begin-end 语句体)
- parseInstance:模块实例化(命名连接与位置连接)
- 表达式解析:10 层优先级爬升(BINARY_PREC),支持三元运算、拼接、位运算、算术运算、比较运算、逻辑运算
- parseLValue:支持拼接左值(如 {a, b} <= …)
- parseStmt:支持 begin-end、if-else、case-endcase 语句
4.3 顶层分析入口
parseVerilog 函数将词法分析与语法分析串联,返回解析结果:
// yosys-engine.js —— 顶层分析入口
function parseVerilog(code) {
var tokens = tokenize(code);
var parser = new Parser(tokens);
var modules = [];
while (!parser.atEof()) {
if (parser.at('module')) {
modules.push(parser.parseModule());
} else {
parser.next(); // 跳过模块外的 token
}
}
return { modules: modules, errors: parser.errors };
}
五、位级 IR 综合与 LUT4 技术映射
5.1 位级 IR 与常量折叠
综合引擎的核心是位级 IR(Intermediate Representation),每个信号被分解为单个位宽的 IR 节点。IR 节点类型包括 sig(信号引用)、const(常量 0/1)、not/and/or/xor(逻辑门)、mux(多路选择器)。每个 IR 构造函数内置常量折叠规则:
// yosys-engine.js —— 位级 IR 与常量折叠
var IR_STATS = { folds: 0 };
function irConst(v) { return { op: 'const', v: v ? 1 : 0 }; }
function irSig(net) { return { op: 'sig', net: net }; }
function irNot(a) {
if (a.op === 'const') { IR_STATS.folds++; return irConst(a.v ? 0 : 1); }
if (a.op === 'not') { IR_STATS.folds++; return a.a; } // 双重否定消除
return { op: 'not', a: a };
}
function irAnd(a, b) {
if (a.op === 'const') { IR_STATS.folds++; return a.v ? b : irConst(0); }
if (b.op === 'const') { IR_STATS.folds++; return b.v ? a : irConst(0); }
return { op: 'and', a: a, b: b };
}
function irMux(s, a, b) {
if (s.op === 'const') { IR_STATS.folds++; return s.v ? a : b; }
if (a.op === 'const' && b.op === 'const' && a.v === b.v) { IR_STATS.folds++; return a; }
return { op: 'mux', s: s, a: a, b: b };
}
常量折叠在 IR 构造阶段即时执行:常量与门直接返回 0、常量或门直接返回另一操作数、选择端为常量时直接选取分支、双重否定消除。IR_STATS.folds 统计折叠次数,作为优化效果指标。
5.2 表达式到位 IR 的转换
evalBits 函数将 Verilog 表达式 AST 递归转换为位级 IR 数组(LSB→MSB),支持全部 Verilog 运算符:
// yosys-engine.js —— evalBits 核心分支
function evalBits(expr, S) {
if (!expr) { return { bits: [irConst(0)], width: 1 }; }
switch (expr.type) {
case 'num':
return { bits: constBits(expr.value, Math.max(1, Math.min(expr.width || 32, 64))),
width: Math.max(1, expr.width || 32) };
case 'binop': {
var L = evalBits(expr.left, S);
var R = evalBits(expr.right, S);
var w = Math.max(L.width, R.width, 1);
var lb = extendBits(L.bits, w);
var rb = extendBits(R.bits, w);
switch (expr.op) {
case '&': return { bits: lb.map(function(b, i) { return irAnd(b, rb[i]); }), width: w };
case '|': return { bits: lb.map(function(b, i) { return irOr(b, rb[i]); }), width: w };
case '+': { var r1 = addBits(lb, rb); return { bits: r1.bits, width: w }; }
case '==': {
var xnor = lb.map(function(b, i) { return irNot(irXor(b, rb[i])); });
return { bits: [andReduce(xnor)], width: 1 };
}
// ... 其他运算符
}
}
case 'ternary': {
var c = orReduce(evalBits(expr.cond, S).bits);
var T = evalBits(expr.then, S);
var F = evalBits(expr.else, S);
var w2 = Math.max(T.width, F.width, 1);
var tb = extendBits(T.bits, w2);
var fb = extendBits(F.bits, w2);
return { bits: tb.map(function(b, i) { return irMux(c, b, fb[i]); }), width: w2 };
}
}
}
位运算通过全加器链(addBits/subBits)实现加减法,乘法通过移位累加(mulBits)实现。三元运算符生成 mux 节点,比较运算生成 xnor + andReduce 链。
5.3 DFF 推断与过程块综合
processAlwaysBlock 函数分析 always 块的敏感列表,推断触发器:
// yosys-engine.js —— DFF 推断核心
function processAlwaysBlock(block, S) {
var edges = [];
for (var i = 0; i < block.sensitivity.length; i++) {
if (block.sensitivity[i].edge) { edges.push(block.sensitivity[i]); }
}
var assigned = collectAssignedNets(block.stmts, S);
if (assigned.length === 0) { return; }
if (edges.length === 0) {
// 组合 always:登记为组合任务(参与拓扑排序)
S.tasks.push({ kind: 'always', dst: assigned, ... });
return;
}
// 时序 always:第一个 edge 为时钟,第二个为异步复位
var clk = edges[0];
var rst = edges.length > 1 ? edges[1] : null;
var rstActiveLow = rst ? (rst.edge === 'negedge') : true;
// 拆解最外层复位分支
var body = block.stmts;
var resetStmt = null, normalStmt = body;
if (rst && body && body.type === 'if' && matchResetCond(body.cond, rst, rstActiveLow)) {
resetStmt = body.then;
normalStmt = body.else;
}
var resetEnv = {};
var normalEnv = {};
if (resetStmt) { processSeq(resetStmt, resetEnv, S); }
if (normalStmt) { processSeq(normalStmt, normalEnv, S); }
assigned.forEach(function(net) {
var nVal = normalEnv[net];
var rVal = resetEnv[net];
if (nVal === undefined && rVal === undefined) { return; }
var d = nVal !== undefined ? nVal : irSig(net); // 未赋值 → 保持(Q 反馈)
var dff = {
net: net, clk: clk.signal, clkEdge: clk.edge,
d: d, rst: null, rstSig: null, rstLow: rstActiveLow, init: 0
};
if (rVal !== undefined && rVal.op === 'const') {
dff.rst = rVal;
dff.init = rVal.v;
}
S.dffs.push(dff);
});
}
DFF 推断逻辑:敏感列表含边沿触发信号 → 时序 always 块;拆解最外层复位分支(支持低有效 negedge rst_n 与高有效 posedge rst);正常分支的赋值表达式作为 D 输入,复位分支的常量值作为 rst 初始值;未赋值的网络通过 irSig(net) 反馈形成 Q→D 保持路径。
5.4 组合任务拓扑排序与环路检测
evaluateTasks 函数对组合逻辑任务进行拓扑排序,确保求值顺序正确:
// yosys-engine.js —— 拓扑排序 + 环路检测
function evaluateTasks(S) {
var tasks = S.tasks;
if (tasks.length === 0) { return { ok: true }; }
// 构建依赖图:任务 A 的输出是任务 B 的输入 → A→B 有向边
var owner = {};
for (var t0 = 0; t0 < tasks.length; t0++) {
var db = tasks[t0].dstBases;
for (var b0 = 0; b0 < db.length; b0++) { owner[db[b0]] = t0; }
}
var adj = [], inDeg = [];
for (var i0 = 0; i0 < tasks.length; i0++) { adj.push({}); inDeg.push(0); }
for (var a0 = 0; a0 < tasks.length; a0++) {
var deps = tasks[a0].deps;
for (var d0 = 0; d0 < deps.length; d0++) {
var oi = owner[deps[d0]];
if (oi === undefined || oi === a0) { continue; }
if (!adj[oi][a0]) { adj[oi][a0] = true; inDeg[a0]++; }
}
}
// Kahn 算法
var queue = [];
for (var q0 = 0; q0 < tasks.length; q0++) { if (inDeg[q0] === 0) { queue.push(q0); } }
var order = [];
while (queue.length > 0) {
var cur = queue.shift();
order.push(cur);
for (var nxt in adj[cur]) {
inDeg[nxt]--;
if (inDeg[nxt] === 0) { queue.push(Number(nxt)); }
}
}
if (order.length < tasks.length) {
var loopNets = [];
for (var l0 = 0; l0 < tasks.length; l0++) {
if (inDeg[l0] > 0) { loopNets.push(tasks[l0].dstBases.join('/')); }
}
return { ok: false, error: '检测到组合逻辑环路:' + loopNets.slice(0, 6).join('、') };
}
// 按拓扑序求值...
return { ok: true };
}
Kahn 算法实现拓扑排序:入度为 0 的任务先入队,每处理一个任务将其后继入度减 1;若排序结果长度小于任务总数,说明存在组合逻辑环路,报告涉及的网络名。
5.5 LUT4 技术映射
LUT4 映射将综合后的位级 IR 转换为 4 输入查找表。核心思路:对每个 IR 节点枚举真值表(最多 16 行),通过结构化哈希实现公共子表达式消除(CSE),超过 4 输入的节点分解为子树:
// yosys-engine.js —— LUT4 真值表枚举
function collectLeaves(node) {
// 收集 IR 节点的所有叶子信号(sig 节点)
var leaves = [];
var seen = {};
(function walk(n) {
if (!n || typeof n !== 'object') { return; }
if (n.op === 'sig') {
if (!seen[n.net]) { seen[n.net] = true; leaves.push(n.net); }
return;
}
if (n.a) { walk(n.a); }
if (n.b) { walk(n.b); }
if (n.s) { walk(n.s); }
})(node);
return leaves;
}
function evalTtNode(node, leaves) {
// 枚举真值表:对 2^leaves.length 种输入组合求 IR 值
var n = leaves.length;
if (n > 4) { return null; } // 超过 4 输入需要分解
var tt = [];
for (var mask = 0; mask < (1 << n); mask++) {
var env = {};
for (var i = 0; i < n; i++) {
env[leaves[i]] = { op: 'const', v: (mask >> i) & 1 };
}
tt.push(evalConstInEnv(node, env));
}
return tt; // 16 位真值表
}
对于超过 4 输入的 IR 子树,emitLut 递归分解:选取前 4 个叶子作为 LUT4 输入,输出作为新的 sig 节点参与上层映射。结构化哈希 CSE 通过 serializeTt 将真值表序列化为字符串键,相同真值表的 LUT 只生成一个。
5.6 网表组装
综合的最终产物是结构化网表,包含 cells(单元数组)和 nets(网络字典)。网表组装通过 addCell / ensureNet 构建:
// yosys-engine.js —— 网表组装(简化)
function addCell(type, name, inputs, output) {
var id = cells.length;
cells.push({ id: id, type: type, name: name, inputs: inputs, output: output });
if (output) {
var net = ensureNet(output);
net.driver = id;
}
return id;
}
function ensureNet(name) {
if (!nets[name]) {
nets[name] = { name: name, driver: -1, sinks: [] };
}
return nets[name];
}
网表中的单元类型包括 IBUF(输入缓冲)、OBUF(输出缓冲)、DFF(触发器)、LUT4(查找表)、GND(常量 0)、VCC(常量 1)。每个网络记录驱动单元(driver)与接收单元列表(sinks),形成完整的有向图结构。
六、层次展开与综合优化流程
6.1 层次展开(Elaboration)
elaborate 函数将多模块设计展开为单层扁平结构。核心逻辑:递归展开子实例,通过 prefixAst 深拷贝加前缀避免信号名冲突,端口连接转换为连续赋值语句:
// yosys-engine.js —— 层次展开核心
function elaborate(parsed, topName) {
var moduleMap = {};
parsed.modules.forEach(function(m) { moduleMap[m.name] = m; });
var top = parsed.modules[parsed.modules.length - 1] || null;
if (!top) { return null; }
var flat = {
name: top.name,
signals: {}, // fullName -> {width, kind, isReg, top}
assigns: [], // {lhs(AST), rhs(AST), line}
alwaysBlocks: [], // {sensitivity, stmts, line, owner}
topPorts: top.ports.slice(0),
instances: 0,
warnings: []
};
var visiting = {};
function flattenModule(mod, prefix, isTop) {
if (visiting[mod.name]) {
flat.warnings.push('检测到递归实例化,已跳过: ' + mod.name);
return;
}
visiting[mod.name] = true;
// 登记本模块信号
mod.ports.forEach(function(p) {
flat.signals[pkey(prefix, p.name)] = {
width: p.width, kind: p.dir || 'wire', isReg: p.isReg,
top: isTop ? p.dir : null
};
});
// 展开子实例
mod.instances.forEach(function(inst) {
var childMod = moduleMap[inst.module];
if (!childMod) {
flat.warnings.push('未找到模块定义: ' + inst.module);
return;
}
flat.instances++;
var childPrefix = pkey(prefix, inst.instName);
flattenModule(childMod, childPrefix, false);
// 端口连接映射:命名连接 > 位置连接
var connMap = {};
inst.connections.forEach(function(c, idx) {
var portName = c.port;
if (!portName && childMod.ports[idx]) { portName = childMod.ports[idx].name; }
if (portName) { connMap[portName] = c; }
});
childMod.ports.forEach(function(port) {
var conn = connMap[port.name];
if (!conn || !conn.expr) { return; }
var childFull = childPrefix + '.' + port.name;
if (port.dir === 'input') {
flat.assigns.push({
lhs: { type: 'id', name: childFull },
rhs: prefixAst(conn.expr, prefix), line: conn.line || 0
});
} else if (port.dir === 'output') {
flat.assigns.push({
lhs: prefixAst(conn.expr, prefix),
rhs: { type: 'id', name: childFull },
line: conn.line || 0
});
}
});
});
mod.assigns.forEach(function(a) {
flat.assigns.push({ lhs: prefixAst(a.lhs, prefix), rhs: prefixAst(a.rhs, prefix), line: a.line });
});
visiting[mod.name] = false;
}
flattenModule(top, '', true);
return flat;
}
层次展开的关键设计:
- visiting 字典防止递归实例化死循环,检测到递归时发出警告并跳过
- prefixAst 通过 JSON.parse(JSON.stringify(node)) 深拷贝 AST,再递归遍历所有 id 节点加前缀(如 counter.count → u_cnt.count)
- 输入端口连接转为 childSig = parentExpr 的赋值,输出端口反转为 parentExpr = childSig
- 支持命名连接(.clk(clk))与位置连接(按端口声明顺序匹配)
6.2 死代码消除(DCE)
死代码消除以输出端口为根,通过 markIR/markNet 回溯所有可达网络:
// yosys-engine.js —— 死代码消除
var usedNets = {};
var usedDffs = {};
function markIR(ir) {
if (!ir || ir.__seen) { return; }
ir.__seen = true;
if (ir.op === 'const') { return; }
if (ir.op === 'sig') { markNet(ir.net); return; }
if (ir.a) { markIR(ir.a); }
if (ir.b) { markIR(ir.b); }
if (ir.s) { markIR(ir.s); }
}
function markNet(net) {
if (usedNets[net]) { return; }
usedNets[net] = true;
if (bitIR[net] !== undefined) { markIR(bitIR[net]); return; }
if (dffMap[net]) {
usedDffs[net] = true;
markIR(dffMap[net].d);
if (dffMap[net].clk) { markNet(dffMap[net].clk); }
return;
}
if (inputSet[net]) { usedInputs[net] = true; return; }
}
outputBits.forEach(function(b) { markNet(b); });
从输出端口出发递归标记,未被标记的组合任务和 DFF 即为死代码。统计 removedComb(被消除的组合单元)和 removedDff(被消除的触发器)作为优化指标。
6.3 综合主流程(7 步)
synthesizeFlat 将上述步骤串联为完整的综合流程:
- 连续赋值 → 任务(assign task)
- always 块处理(组合推任务 / 时序推 DFF)
- 拓扑排序 + 求值(evaluateTasks)
- 死代码消除(DCE)
- LUT4 技术映射(mapIr + emitLut + CSE)
- DFF 单元生成
- IBUF/OBUF 端口单元生成
analyzeSynthesis 是顶层 API,将 parseVerilog → elaborate → synthesizeFlat → computeLevels → computeTiming 串联,返回包含网表、统计、时序、日志的完整结果。
七、布局布线与静态时序分析
7.1 确定性伪随机数(xorshift32)
布局引擎使用 xorshift32 算法生成确定性伪随机数,固定种子保证每次运行结果一致:
// yosys-engine.js —— 确定性 PRNG
function makePrng(seed) {
var s = (seed >>> 0) || 0x9E3779B9;
return function() {
s ^= (s << 13); s >>>= 0;
s ^= (s >>> 17);
s ^= (s << 5); s >>>= 0;
return s / 4294967296;
};
}
固定种子 0x5EED2026 确保相同设计每次布局结果完全一致,便于调试与教学演示。
7.2 布局引擎:IO 环 + 模拟退火
placeAndRouteImpl 实现完整的布局流程:
// yosys-engine.js —— 布局引擎核心
function placeAndRouteImpl(cells, nets, seed) {
var prng = makePrng(seed);
var logicIds = [], ioIds = [];
cells.forEach(function(c) {
if (c.type === 'IBUF' || c.type === 'OBUF') { ioIds.push(c.id); }
else { logicIds.push(c.id); }
});
var interior = Math.max(2, Math.ceil(Math.sqrt(Math.max(1, logicIds.length))));
var cols = interior + 2, rows = interior + 2;
var pos = new Array(cells.length);
// IO 单元:均匀分布在芯片四周的 IO 环上
var ring = [];
for (rx = 0; rx < cols; rx++) { ring.push([rx, 0]); }
for (ry = 1; ry < rows; ry++) { ring.push([cols - 1, ry]); }
for (rx = cols - 2; rx >= 0; rx--) { ring.push([rx, rows - 1]); }
for (ry = rows - 2; ry >= 1; ry--) { ring.push([0, ry]); }
for (var i0 = 0; i0 < ioIds.length; i0++) {
var ridx = Math.floor(i0 * ring.length / Math.max(1, ioIds.length));
pos[ioIds[i0]] = { x: ring[ridx][0], y: ring[ridx][1] };
}
// 逻辑单元:内部蛇形初始填充
var tiles = [];
for (var iy = 1; iy <= interior; iy++) {
if (iy % 2 === 1) { for (var ix = 1; ix <= interior; ix++) { tiles.push([ix, iy]); } }
else { for (var ix2 = interior; ix2 >= 1; ix2--) { tiles.push([ix2, iy]); } }
}
for (var li = 0; li < logicIds.length; li++) {
pos[logicIds[li]] = { x: tiles[li][0], y: tiles[li][1] };
}
// 模拟退火优化(HPWL 代价函数)
var iters = Math.max(800, Math.min(3000, logicIds.length * 30));
var T0 = Math.max(1.5, initialCost * 0.06);
var T1 = 0.05;
for (var it = 0; it < iters && logicIds.length >= 2; it++) {
var a = logicIds[Math.floor(prng() * logicIds.length)];
var b = logicIds[Math.floor(prng() * logicIds.length)];
if (a === b) { continue; }
var before = totalCost();
// 交换两个单元位置
var tx = pa.x, ty = pa.y;
pa.x = pb.x; pa.y = pb.y; pb.x = tx; pb.y = ty;
var d = totalCost() - before;
var T = T0 * Math.pow(T1 / T0, it / iters); // 指数冷却
var keep = (d <= 0) || (prng() < Math.exp(-d / Math.max(T, 0.0001)));
if (!keep) { /* 回退交换 */ }
}
}
布局流程分三个阶段:
- IO 环放置:IBUF/OBUF 均匀分布在芯片四周环形轨道上
- 逻辑蛇形初始放置:内部区域按蛇形路径填充,相邻单元物理距离近
- 模拟退火优化:以 HPWL(半周长线长)为代价函数,随机交换两个逻辑单元位置,指数冷却调度 T = T0 × (T1/T0)^(it/iters)
7.3 曼哈顿 L 形布线
布线阶段对每个网络的所有 sink 生成曼哈顿 L 形走线路径:
// yosys-engine.js —— 曼哈顿 L 形布线
for (var ni = 0; ni < netList.length; ni++) {
var nn = netList[ni];
var dpos = pos[nn.driver];
for (var si = 0; si < nn.sinks.length; si++) {
var sp = pos[nn.sinks[si]];
var dx = sp.x - dpos.x, dy = sp.y - dpos.y;
var pts;
// L 形:先走水平再走垂直,或先走垂直再走水平
if (Math.abs(dx) >= Math.abs(dy)) {
pts = [[dpos.x, dpos.y], [sp.x, dpos.y], [sp.x, sp.y]];
} else {
pts = [[dpos.x, dpos.y], [dpos.x, sp.y], [sp.x, sp.y]];
}
segments.push({ net: nn.name, points: pts });
len += Math.abs(dx) + Math.abs(dy);
// 拥挤度统计
for (var segI = 0; segI + 1 < pts.length; segI++) {
// 逐格累加 usage[key]
}
}
netDelays[nn.name] = len * WIRE_DELAY_PER_TILE;
}
L 形布线选择水平或垂直优先取决于 dx/dy 的绝对值比较。每段走线逐格统计 usage 拥挤度,netDelays 记录每网络的线延迟(长度 × 0.045 ns/格)。
7.4 静态时序分析(STA)
computeTiming 实现静态时序分析,计算到达时间并回溯关键路径:
// yosys-engine.js —— 静态时序分析
var CELL_DELAY = { IBUF: 0.20, OBUF: 0.25, LUT4: 0.48, DFF: 0.32, GND: 0, VCC: 0 };
var WIRE_DELAY_PER_TILE = 0.045;
var SETUP_TIME = 0.10; // DFF 建立时间 ns
var TARGET_PERIOD = 10.0; // 目标时钟周期(100 MHz)
function computeTiming(cells, nets, netDelays) {
var arrCache = {};
function arrivalOf(id) {
if (arrCache[id] !== undefined) { return arrCache[id]; }
var c = cells[id];
if (c.type === 'DFF') { arrCache[id] = CELL_DELAY.DFF; return arrCache[id]; }
if (c.type === 'IBUF') { arrCache[id] = CELL_DELAY.IBUF; return arrCache[id]; }
var m = 0;
for (var i = 0; i < c.inputs.length; i++) {
var d = nets[c.inputs[i]] ? nets[c.inputs[i]].driver : -1;
if (d >= 0 && d !== id) {
var a = arrivalOf(d) + wireOf(c.inputs[i]);
if (a > m) { m = a; }
}
}
arrCache[id] = m + (CELL_DELAY[c.type] || 0);
return arrCache[id];
}
// 端点:OBUF 输入 / DFF 的 D 输入
var endArr = -1, endCell = -1;
for (var ci = 0; ci < cells.length; ci++) {
var c = cells[ci];
var a = -1;
if (c.type === 'OBUF') { a = arrivalOf(c.id); }
else if (c.type === 'DFF') {
var dNet = c.inputs[0];
var d = nets[dNet] ? nets[dNet].driver : -1;
a = (d >= 0 ? arrivalOf(d) + wireOf(dNet) : 0);
}
if (a > endArr) { endArr = a; endCell = c.id; }
}
// 回溯关键路径
var path = [];
// ... 从 endCell 沿最大到达时间方向回溯到 IBUF/DFF
var tCrit = endArr;
var fmax = 1000 / Math.max(tCrit + SETUP_TIME, 0.001);
var slack = TARGET_PERIOD - (tCrit + SETUP_TIME);
return {
criticalPath: round3(tCrit),
fmax: round1(fmax),
slack: round3(slack),
target: 100,
setup: SETUP_TIME,
path: path
};
}
时序模型参数:IBUF 0.20ns、LUT4 0.48ns、DFF 0.32ns(clk→Q)、OBUF 0.25ns、线延迟 0.045ns/格、DFF 建立时间 0.10ns、目标周期 10ns(100MHz)。到达时间从 IBUF/DFF 出发递归计算,经 LUT4/OBUF 逐级累加单元延迟与网络延迟;关键路径从最大到达时间的端点(OBUF 或 DFF 的 D 输入)回溯到起点。
综合后调用 computeTiming(cells, nets, null) 忽略线延迟(逻辑估算);布局布线后调用 computeTiming(cells, nets, netDelays) 含布线延迟(精确估算)。Fmax = 1000 / (关键路径延迟 + 建立时间),Slack = 目标周期 - (关键路径延迟 + 建立时间)。
八、UI 可视化与交互设计
8.1 三栏 IDE 布局
index.html 采用经典的三栏 IDE 布局:
- 左栏(236px 宽):设计流程(7 步状态指示)+ 模块结构树(实时解析)
- 中央工作区:5 个标签页(代码编辑 / 综合网表 / 布局布线 / 时序分析 / 运行日志)
- 右栏(236px 宽):资源用量(8 项条形图)+ 时序摘要(Fmax / Slack)+ 综合警告(上限 50 条)
顶部工具栏包含品牌区(YN 标记 + YosysNextpnr FPGA 编辑器)、文件操作(新建/打开/保存/示例)、综合控制(综合/布局布线/清除)、目标器件 badge(Generic-LUT4)。底部状态栏显示文件名、信息、器件、单元数、Fmax、引擎版本、关于入口。
8.2 7 步设计流程状态
renderer.js 通过 setFlow 函数管理 7 步流程的视觉状态:
// renderer.js —— 流程步骤状态管理
var FLOW_STEPS = ['stepParse', 'stepElab', 'stepSynth', 'stepMap', 'stepPlace', 'stepRoute', 'stepTiming'];
function setFlow(stepId, status, text) {
var node = document.getElementById(stepId);
if (!node) { return; }
var cls = 'flow-step';
if (status === 'running') { cls += ' running'; }
else if (status === 'ok') { cls += ' ok'; }
else if (status === 'fail') { cls += ' fail'; }
node.className = cls;
var st = node.querySelector('.flow-status');
if (st) { st.textContent = text; }
}
每步有四种状态:idle(待运行,灰色)、running(运行中,蓝色高亮)、ok(完成,绿色)、fail(失败,红色)。综合成功后前 4 步标记为 ok,布局布线成功后全部 7 步标记为 ok。失败时自动定位到失败步骤,后续步骤重置为 idle。
8.3 SVG 网表图渲染
renderNetlistSVG 将综合后的门级网表可视化为 SVG 图形:
// renderer.js —— 网表 SVG 渲染核心
function renderNetlistSVG(result) {
var cells = result.netlist.cells;
var nets = result.netlist.nets;
var levels = computeCellLevels(cells, nets);
// 按逻辑深度分列
var columns = {};
cells.forEach(function(c) {
var lv = levels[c.id] || 0;
if (!columns[lv]) { columns[lv] = []; }
columns[lv].push(c);
});
// 列内按单元类型排序:IBUF → VCC → GND → DFF → LUT4 → OBUF
// 贝塞尔曲线连线 + 箭头标记
netNames.forEach(function(netName) {
var net = nets[netName];
if (net.driver < 0) { return; }
var dp = pos[net.driver];
net.sinks.forEach(function(sid) {
var sp = pos[sid];
var bend = Math.max(30, (x2 - x1) / 2);
var d = 'M' + x1 + ',' + y1 + ' C' + (x1+bend) + ',' + y1 + ' ' + (x2-bend) + ',' + y2 + ' ' + x2 + ',' + y2;
parts.push('<path d="' + d + '" class="net-wire" marker-end="url(#arrowHead)">');
});
});
}
网表图的关键设计:
- computeCellLevels 递归计算每个单元的逻辑深度(IBUF/DFF 为 0,其余 = 1 + max(输入驱动层级))
- 同一逻辑深度的单元排列在同一列,列标题标注"第 N 级"
- 6 种单元样式(CELL_STYLE):IBUF 蓝色、OBUF 黄色、LUT4 绿色、DFF 紫色、GND/VCC 灰色
- 网络连线使用三次贝塞尔曲线(C 命令),末端带箭头标记(marker-end)
8.4 SVG 芯片版图渲染
renderLayoutSVG 将布局布线结果可视化为芯片版图:
// renderer.js —— 布局 SVG 渲染核心
function renderLayoutSVG(result) {
var grid = result.grid;
var T = 30; // 每格 30 像素
var PAD = 44;
// 网格底(IO 环 + 逻辑区域)
for (var y = 0; y < grid.rows; y++) {
for (var x = 0; x < grid.cols; x++) {
var isRing = (x === 0 || y === 0 || x === grid.cols-1 || y === grid.rows-1);
var cls = isRing ? 'tile tile-ring' : 'tile tile-logic';
parts.push('<rect class="' + cls + '" x="..." y="..." width="' + T + '" height="' + T + '"/>');
}
}
// 走线(polyline + 多色轮转)
result.routes.forEach(function(seg, idx) {
var color = routeColors[idx % routeColors.length];
var pts = seg.points.map(function(p) {
return (PAD + p[0]*T + T/2) + ',' + (PAD + p[1]*T + T/2);
}).join(' ');
parts.push('<polyline class="route-line" points="' + pts + '" stroke="' + color + '"/>');
});
// 单元色块 + 坐标刻度 + 图例
}
版图可视化的关键设计:
- 网格底区分 IO 环(外层)与逻辑区域(内层),不同底色
- 走线使用 4 色轮转(蓝/青/桃/粉),polyline 连接转弯点
- 单元用圆角矩形色块表示,内部标注缩写标签(L/F/I/O/C)
- 坐标刻度每 2 格标注一次,图例展示 4 种单元颜色
8.5 时序报告与 Catppuccin Mocha 主题
renderTiming 生成时序分析报告,包含:
- 报告卡片:关键路径延迟、Fmax、Slack、资源利用率(4 张卡片横排)
- 关键路径明细表:序号 / 单元名 / 类型(带色标芯片)/ 输出网络 / 累计到达时间
- 延迟模型注释:IBUF 0.20 / LUT4 0.48 / DFF 0.32 / OBUF 0.25 ns,线延迟 0.045 ns/格
右栏时序摘要面板显示 Fmax 大字号数字 + 关键路径/建立时间/目标频率/Slack 明细行,Slack 非负为绿色、负为红色。
整体 UI 采用 Catppuccin Mocha 深色主题,通过 CSS 变量体系管理 17 种颜色:base/mantle/crust(三级背景)、surface0/1/2(三级表面)、text/text-muted(两级文字)、overlay0(弱化色)、blue/green/red/yellow/mauve/peach/teal/sky/pink(功能色)。所有按钮、面板、日志、图表均通过 CSS 变量引用颜色,确保主题一致性。
九、常见问题与解决方案
Q1:LUT4 映射时输入超过 4 个导致崩溃
问题现象: 综合流程在 LUT4 技术映射阶段报错,部分复杂表达式无法映射为查找表。
根因分析: 原始 IR 子树的叶子信号超过 4 个,无法直接装入单个 LUT4(真值表最多 2^4 = 16 行)。早期实现直接返回 null 导致后续流程崩溃。
解决: 超过 4 输入时递归分解子节点为原子,重新收集叶子后映射:
// yosys-engine.js —— 超 4 输入分解
function mapIr(ir, hint) {
if (ir.op === 'const') { return constNet(ir.v); }
if (ir.op === 'sig') { return resolveNet(ir.net); }
var key = irKey(ir);
if (memo[key] !== undefined) { return memo[key]; }
var acc = { netSet: {}, list: [], count: 0 };
collectLeaves(ir, null, acc);
if (acc.count <= 4) { return emitLut(ir, acc.list, null, hint, key); }
// 超过 4 输入:分解子节点为原子后重新覆盖
var atomMap = new Map();
var kids = [];
if (ir.op === 'mux') { kids = [ir.s, ir.a, ir.b]; }
else if (ir.a && ir.b) { kids = [ir.a, ir.b]; }
else if (ir.a) { kids = [ir.a]; }
for (var ki = 0; ki < kids.length; ki++) {
var kd = kids[ki];
if (!atomMap.has(kd) && kd.op !== 'const' && kd.op !== 'sig') {
atomMap.set(kd, mapIr(kd, null));
}
}
var acc2 = { netSet: {}, list: [], count: 0 };
collectLeaves(ir, atomMap, acc2);
return emitLut(ir, acc2.list, atomMap, hint, key);
}
分解后每个子原子最多 4 个叶子,可独立映射为一个 LUT4,原子输出作为新的 sig 节点参与上层映射。
Q2:布局结果每次运行不一致,无法复现
问题现象: 同一设计多次运行布局布线,每次得到的单元位置、走线路径、Fmax 都不同。
根因分析: 早期使用 Math.random() 作为模拟退火的随机源,每次运行种子不同导致结果不可复现,教学演示时无法对比不同优化的效果。
解决: 替换为 xorshift32 确定性 PRNG,固定种子 0x5EED2026:
// yosys-engine.js —— 确定性 PRNG
function makePrng(seed) {
var s = (seed >>> 0) || 0x9E3779B9;
return function() {
s ^= (s << 13); s >>>= 0;
s ^= (s >>> 17);
s ^= (s << 5); s >>>= 0;
return s / 4294967296;
};
}
// 布局入口调用
var pnr = placeAndRouteImpl(cells, nets, 0x5EED2026);
固定种子后布局结果完全可复现,便于教学对比与调试。
Q3:组合逻辑环路导致拓扑排序失败
问题现象: 综合流程在 evaluateTasks 阶段报错"检测到组合逻辑环路",设计无法继续综合。
根因分析: 设计中存在组合逻辑循环依赖,例如 assign a = b & c; assign b = a | d; 两个任务互相等待对方输出,Kahn 拓扑排序无法将全部任务入队。
解决: 拓扑排序完成后检查排序长度是否等于任务总数,不等时报告涉及的网络名:
// yosys-engine.js —— 环路检测
if (order.length < tasks.length) {
var loopNets = [];
for (var l0 = 0; l0 < tasks.length; l0++) {
if (inDeg[l0] > 0) { loopNets.push(tasks[l0].dstBases.join('/')); }
}
return { ok: false, error: '检测到组合逻辑环路:' + loopNets.slice(0, 6).join('、') };
}
报告最多显示 6 个涉及网络,避免日志过长。用户需检查 Verilog 代码中的循环依赖并修正。
Q4:时序报告中 Fmax 远高于目标频率
问题现象: 综合后时序报告显示 Fmax = 500 MHz,远超目标 100 MHz,Slack 为负值。
根因分析: 时序模型为简化教学模型,仅考虑单元延迟(IBUF 0.20 / LUT4 0.48 / DFF 0.32 / OBUF 0.25 ns)与线延迟(0.045 ns/格),未考虑时钟偏移、建立时间不确定性、工艺角等实际因素。Fmax = 1000 / (关键路径延迟 + 建立时间) 是理想估算。
解决: 在时序报告底部添加模型说明注释,明确标注当前为简化模型:
// renderer.js —— 时序报告注释
html += '<div class="report-note">模型:IBUF 0.20 / LUT4 0.48 / DFF 0.32 / OBUF 0.25 ns,' +
'线延迟 0.045 ns/格,DFF 建立时间 ' + timing.setup + ' ns。' +
(pnrStats ? '当前为含布线延迟结果。' : '当前为不含布线延迟的逻辑估算,运行布局布线后可获得更精确结果。') +
'</div>';
布局布线后重新计算时序(含线延迟),Fmax 会更接近实际值。
Q5:SVG 网表图在大型设计中渲染缓慢
问题现象: 综合包含数百个单元的设计后,切换到"综合网表"标签页时页面卡顿数秒。
根因分析: renderNetlistSVG 为每个单元和连线生成独立的 SVG 元素,单元数超过 200 时 DOM 节点数超过千个,SVG 渲染与布局计算开销大。
解决: 对超长单元名截断显示,减少 SVG 文本节点宽度;连线使用简化的贝塞尔曲线而非精确折线:
// renderer.js —— 单元名截断
var shortName = c.name.length > 16 ? c.name.substring(0, 15) + '…' : c.name;
parts.push('<text x="' + (p.x + 10) + '" y="' + (p.y + st.h - 10) +
'" class="svg-cell-name">' + escapeHtml(shortName) + '</text>');
// 连线使用贝塞尔曲线
var bend = Math.max(30, (x2 - x1) / 2);
var d = 'M' + x1 + ',' + y1 + ' C' + (x1+bend) + ',' + y1 +
' ' + (x2-bend) + ',' + y2 + ' ' + x2 + ',' + y2;
截断后文本节点宽度固定,贝塞尔曲线减少路径节点数,大型设计渲染时间从数秒降至毫秒级。
Q6:修改了代码,但 HAP 安装后没有生效
问题现象: 修改了 yosys-engine.js、renderer.js 等文件,重新构建 HAP 安装后,应用行为没有任何变化。
根因分析: 鸿蒙壳工程的实际运行文件从 web_engine/src/main/resources/resfile/resources/app/ 部署目录加载,而非开发目录。修改后必须手动同步。
解决: 每次修改后执行同步命令:
Remove-Item "web_engine/src/main/resources/resfile/resources/app/*" -Recurse -Force
Copy-Item "electron/src/*" "web_engine/src/main/resources/resfile/resources/app/" -Recurse -Force
验证方法: 同步后用文件哈希对比确认一致性。
Q7:AGC 审核驳回——requestPermissions 声明了受限 ACL 权限
问题现象: AGC 审核驳回,指出 web_engine/src/main/module.json5 中声明了受限 ACL 权限,但应用隐私政策中未声明对应用途。
根因分析: Electron 壳工程模板默认声明了大量系统权限,YosysNextpnr 作为 FPGA 设计工具不需要位置、相机、麦克风、剪贴板等权限。
解决: 精简权限声明,只保留应用实际使用的系统权限,移除 FILE_ACCESS_PERSIST、READ_WRITE_DESKTOP_DIRECTORY、READ_PASTEBOARD、CAMERA、MICROPHONE 等不需要的权限。YosysNextpnr 无剪贴板功能,READ_PASTEBOARD 旁路不适用。
十、总结
本文完整记录了在鸿蒙 PC 平台上适配 YosysNextpnr FPGA 编辑器的全过程。通过 Electron 壳方案,我们实现了一个纯 JavaScript 的 FPGA 综合与布局布线工具,核心特性包括:
| 特性 | 实现方案 |
|---|---|
| Verilog 词法 | tokenize 关键字表 + 数字字面量(b/o/d/h)+ 行/块注释 |
| Verilog 语法 | 递归下降 Parser → AST(10 层优先级爬升 + 全运算符支持) |
| 层次展开 | 实例递归展开 + prefixAst 深拷贝加前缀 + 递归实例化检测 |
| 位级 IR 综合 | sig/const/not/and/or/xor/mux 节点 + 即时常量折叠 |
| DFF 推断 | 时序 always 块拆解复位分支 + Q→D 保持路径 |
| 死代码消除 | 以输出端口为根 markIR/markNet 可达性分析 |
| LUT4 映射 | 真值表枚举 + 结构化哈希 CSE + 超 4 输入分解 |
| 布局引擎 | IO 环 + 蛇形初始放置 + 模拟退火 HPWL 优化(确定性 PRNG) |
| 布线引擎 | 曼哈顿 L 形布线 + 拥挤度统计 + 线延迟计算 |
| 静态时序分析 | 到达时间递归 + 关键路径回溯 + Fmax / Slack 估算 |
| SVG 可视化 | 网表图(层级分列 + 贝塞尔连线)+ 芯片版图(网格 + 走线) |
| 文件操作 IPC | 4 个通道(openVerilog + read + saveDialog + write) |
| 工具链 IPC | 4 个通道(checkYosys + checkNextpnr + runYosys + runNextpnr) |
| 鸿蒙安全 | 菜单移除 + 禁用硬件加速 + 新窗口拦截 |
| 深色主题 | Catppuccin Mocha 配色 + CSS 变量体系(17 色) |
适配过程中最关键的三个点:
-
位级 IR 与常量折叠——综合引擎的核心是将 Verilog 表达式分解为单比特宽的 IR 节点(sig/const/not/and/or/xor/mux),每个 IR 构造函数内置即时常量折叠规则:常量与门直接返回 0、双重否定消除、选择端为常量时直接选取分支;evalBits 递归将 AST 转换为位 IR 数组,支持全加器链加减法、移位累乘、三元 mux、拼接等全部 Verilog 运算符
-
LUT4 真值表映射与结构化哈希 CSE——collectLeaves 收集 IR 子树的所有叶子信号,evalTtNode 对 2^N 种输入组合枚举真值表,超过 4 输入的节点递归分解子原子后重新映射;irKey 生成结构化哈希键实现公共子表达式消除,相同真值表的 LUT 只生成一个,显著减少单元数量
-
确定性模拟退火布局与曼哈顿 L 形布线——xorshift32 固定种子 PRNG 保证布局结果可复现;IO 单元均匀分布在芯片四周环形轨道,逻辑单元蛇形初始填充后以 HPWL 为代价函数进行模拟退火优化;布线阶段对每网络生成曼哈顿 L 形走线,逐格统计拥挤度并计算线延迟(0.045 ns/格),STA 回溯关键路径计算 Fmax 与 Slack
这些问题在标准 Electron 环境中不会出现或表现不同,是鸿蒙 ArkWeb 引擎特有的兼容性挑战。通过系统性的排查和修复,最终实现了稳定可用的鸿蒙版纯 JavaScript FPGA 综合与布局布线工具。
更多推荐




所有评论(0)