鸿蒙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 可视化网表图(层级分列 + 贝塞尔连线)+ 芯片版图(网格 + 走线 + 图例)
文件操作 IPC4 个通道(openVerilog + read + saveDialog + write)
工具链 IPC4 个通道(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 应用身份信息

字段值说明
bundleNamecom.wei.yosysnextpnr应用唯一标识
vendorwei开发者
versionName1.0.0应用版本
应用名称YosysNextpnr FPGA 编辑器中文显示名
英文名yosys-nextpnrpackage.json name
许可证Apache-2.0开源协议

2.5 构建与部署

构建流程遵循鸿蒙 HAP 壳工程的标准步骤:

  1. 在 DevEco Studio 中打开 ohos_hap 工程
  2. 确保 web_engine/src/main/resources/resfile/resources/app/ 部署目录包含所有业务文件
  3. 选择 arm64-v8a 目标架构,构建 HAP 包
  4. 安装到鸿蒙 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 将上述步骤串联为完整的综合流程:

  1. 连续赋值 → 任务(assign task)
  2. always 块处理(组合推任务 / 时序推 DFF)
  3. 拓扑排序 + 求值(evaluateTasks)
  4. 死代码消除(DCE)
  5. LUT4 技术映射(mapIr + emitLut + CSE)
  6. DFF 单元生成
  7. 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 可视化网表图(层级分列 + 贝塞尔连线)+ 芯片版图(网格 + 走线)
文件操作 IPC4 个通道(openVerilog + read + saveDialog + write)
工具链 IPC4 个通道(checkYosys + checkNextpnr + runYosys + runNextpnr)
鸿蒙安全菜单移除 + 禁用硬件加速 + 新窗口拦截
深色主题Catppuccin Mocha 配色 + CSS 变量体系(17 色)

适配过程中最关键的三个点:

  1. 位级 IR 与常量折叠——综合引擎的核心是将 Verilog 表达式分解为单比特宽的 IR 节点(sig/const/not/and/or/xor/mux),每个 IR 构造函数内置即时常量折叠规则:常量与门直接返回 0、双重否定消除、选择端为常量时直接选取分支;evalBits 递归将 AST 转换为位 IR 数组,支持全加器链加减法、移位累乘、三元 mux、拼接等全部 Verilog 运算符

  2. LUT4 真值表映射与结构化哈希 CSE——collectLeaves 收集 IR 子树的所有叶子信号,evalTtNode 对 2^N 种输入组合枚举真值表,超过 4 输入的节点递归分解子原子后重新映射;irKey 生成结构化哈希键实现公共子表达式消除,相同真值表的 LUT 只生成一个,显著减少单元数量

  3. 确定性模拟退火布局与曼哈顿 L 形布线——xorshift32 固定种子 PRNG 保证布局结果可复现;IO 单元均匀分布在芯片四周环形轨道,逻辑单元蛇形初始填充后以 HPWL 为代价函数进行模拟退火优化;布线阶段对每网络生成曼哈顿 L 形走线,逐格统计拥挤度并计算线延迟(0.045 ns/格),STA 回溯关键路径计算 Fmax 与 Slack

这些问题在标准 Electron 环境中不会出现或表现不同,是鸿蒙 ArkWeb 引擎特有的兼容性挑战。通过系统性的排查和修复,最终实现了稳定可用的鸿蒙版纯 JavaScript FPGA 综合与布局布线工具。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐