feat:接入iverilog工具
- 将iverilog可以随着插件的下载而下载 - 用户输入自然语言就可以控制生成对应的VCD文件
This commit is contained in:
@ -41,7 +41,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
(message) => {
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
handleUserMessage(panel, message.text);
|
||||
handleUserMessage(panel, message.text, context.extensionPath);
|
||||
break;
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
|
||||
400
src/utils/iverilogRunner.ts
Normal file
400
src/utils/iverilogRunner.ts
Normal file
@ -0,0 +1,400 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
// 使用 spawn 来执行命令,正确处理路径空格
|
||||
function execCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { cwd: string; env?: any }
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 在 Windows 上,如果路径包含空格,不使用 shell,直接用 spawn
|
||||
// spawn 在不使用 shell 时会正确处理带空格的路径
|
||||
const child = spawn(command, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env || process.env,
|
||||
windowsHide: true,
|
||||
shell: false, // 不使用 shell,让 spawn 直接执行
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
// 在 Windows 上使用 GBK 编码解码输出
|
||||
const encoding = process.platform === 'win32' ? 'gbk' : 'utf8';
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
try {
|
||||
// 尝试使用 iconv-lite 解码(如果可用)
|
||||
const iconv = require('iconv-lite');
|
||||
stdout += iconv.decode(data, encoding);
|
||||
} catch {
|
||||
// 如果 iconv-lite 不可用,使用默认解码
|
||||
stdout += data.toString('utf8');
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
try {
|
||||
const iconv = require('iconv-lite');
|
||||
stderr += iconv.decode(data, encoding);
|
||||
} catch {
|
||||
stderr += data.toString('utf8');
|
||||
}
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
const error: any = new Error(`Command failed with exit code ${code}`);
|
||||
error.stdout = stdout;
|
||||
error.stderr = stderr;
|
||||
error.code = code;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verilog 项目文件检查结果
|
||||
*/
|
||||
export interface VerilogProjectCheck {
|
||||
isComplete: boolean;
|
||||
hasTopModule: boolean;
|
||||
hasTestbench: boolean;
|
||||
topModuleFile?: string;
|
||||
testbenchFile?: string;
|
||||
allVerilogFiles: string[];
|
||||
missingFiles: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* VCD 生成结果
|
||||
*/
|
||||
export interface VCDGenerationResult {
|
||||
success: boolean;
|
||||
vcdFilePath?: string;
|
||||
outputPath?: string;
|
||||
message: string;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查项目中的 Verilog 文件完整性
|
||||
*/
|
||||
export async function checkVerilogProject(
|
||||
projectPath: string
|
||||
): Promise<VerilogProjectCheck> {
|
||||
const result: VerilogProjectCheck = {
|
||||
isComplete: false,
|
||||
hasTopModule: false,
|
||||
hasTestbench: false,
|
||||
allVerilogFiles: [],
|
||||
missingFiles: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
try {
|
||||
// 检查项目路径是否存在
|
||||
if (!fs.existsSync(projectPath)) {
|
||||
result.errors.push(`项目路径不存在: ${projectPath}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 查找所有 Verilog 文件 (.v, .sv)
|
||||
const verilogFiles = findVerilogFiles(projectPath);
|
||||
result.allVerilogFiles = verilogFiles;
|
||||
|
||||
if (verilogFiles.length === 0) {
|
||||
result.errors.push("项目中没有找到 Verilog 文件 (.v 或 .sv)");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 分析文件内容,查找 top module 和 testbench
|
||||
for (const file of verilogFiles) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
const fileName = path.basename(file).toLowerCase();
|
||||
|
||||
// 检查是否是 testbench 文件
|
||||
if (
|
||||
fileName.includes("tb") ||
|
||||
fileName.includes("test") ||
|
||||
content.includes("$dumpfile") ||
|
||||
content.includes("$dumpvars")
|
||||
) {
|
||||
result.hasTestbench = true;
|
||||
result.testbenchFile = file;
|
||||
}
|
||||
|
||||
// 检查是否包含 module 定义(可能是 top module)
|
||||
if (content.match(/module\s+\w+/)) {
|
||||
if (!result.hasTopModule && !fileName.includes("tb")) {
|
||||
result.hasTopModule = true;
|
||||
result.topModuleFile = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查完整性
|
||||
if (!result.hasTopModule) {
|
||||
result.missingFiles.push("缺少顶层模块文件");
|
||||
}
|
||||
|
||||
if (!result.hasTestbench) {
|
||||
result.missingFiles.push("缺少 testbench 文件");
|
||||
}
|
||||
|
||||
result.isComplete = result.hasTopModule && result.hasTestbench;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
result.errors.push(
|
||||
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找目录下所有 Verilog 文件
|
||||
*/
|
||||
function findVerilogFiles(dir: string): string[] {
|
||||
const verilogFiles: string[] = [];
|
||||
|
||||
function searchDir(currentDir: string) {
|
||||
const files = fs.readdirSync(currentDir);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(currentDir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// 跳过 node_modules 等目录
|
||||
if (!file.startsWith(".") && file !== "node_modules") {
|
||||
searchDir(filePath);
|
||||
}
|
||||
} else if (stat.isFile()) {
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
if (ext === ".v" || ext === ".sv") {
|
||||
verilogFiles.push(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchDir(dir);
|
||||
return verilogFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 iverilog 可执行文件路径
|
||||
*/
|
||||
function getIverilogPath(extensionPath: string): string {
|
||||
const platform = process.platform;
|
||||
let iverilogBin = "";
|
||||
|
||||
if (platform === "win32") {
|
||||
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
|
||||
} else if (platform === "darwin") {
|
||||
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
|
||||
} else {
|
||||
// Linux
|
||||
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
|
||||
}
|
||||
|
||||
// 如果插件包中没有,尝试使用系统安装的 iverilog
|
||||
if (!fs.existsSync(iverilogBin)) {
|
||||
return "iverilog"; // 使用系统 PATH 中的 iverilog
|
||||
}
|
||||
|
||||
return iverilogBin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 vvp 可执行文件路径
|
||||
*/
|
||||
function getVvpPath(extensionPath: string): string {
|
||||
const platform = process.platform;
|
||||
let vvpBin = "";
|
||||
|
||||
if (platform === "win32") {
|
||||
vvpBin = path.join(extensionPath, "tools", "iverilog", "bin", "vvp.exe");
|
||||
} else if (platform === "darwin") {
|
||||
vvpBin = path.join(extensionPath, "tools", "iverilog", "bin", "vvp");
|
||||
} else {
|
||||
// Linux
|
||||
vvpBin = path.join(extensionPath, "tools", "iverilog", "bin", "vvp");
|
||||
}
|
||||
|
||||
// 如果插件包中没有,尝试使用系统安装的 vvp
|
||||
if (!fs.existsSync(vvpBin)) {
|
||||
return "vvp"; // 使用系统 PATH 中的 vvp
|
||||
}
|
||||
|
||||
return vvpBin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 iverilog 生成 VCD 文件
|
||||
*/
|
||||
export async function generateVCD(
|
||||
projectPath: string,
|
||||
extensionPath: string
|
||||
): Promise<VCDGenerationResult> {
|
||||
try {
|
||||
// 1. 检查项目完整性
|
||||
const projectCheck = await checkVerilogProject(projectPath);
|
||||
|
||||
if (!projectCheck.isComplete) {
|
||||
return {
|
||||
success: false,
|
||||
message: `项目文件不完整:\n${projectCheck.missingFiles.join("\n")}\n${projectCheck.errors.join("\n")}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 获取 iverilog 和 vvp 路径
|
||||
const iverilogPath = getIverilogPath(extensionPath);
|
||||
const vvpPath = getVvpPath(extensionPath);
|
||||
|
||||
// 3. 准备输出路径
|
||||
const outputDir = projectPath;
|
||||
const outputFile = path.join(outputDir, "simulation.vvp");
|
||||
|
||||
// 4. 设置环境变量,确保 iverilog 能找到库文件
|
||||
const libPath = path.join(extensionPath, "tools", "iverilog", "lib");
|
||||
const env = {
|
||||
...process.env,
|
||||
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
|
||||
};
|
||||
|
||||
// 5. 构建 iverilog 编译参数
|
||||
const compileArgs = ["-o", outputFile, ...projectCheck.allVerilogFiles];
|
||||
|
||||
console.log("执行编译命令:", iverilogPath, compileArgs.join(" "));
|
||||
console.log("IVERILOG_ROOT:", env.IVERILOG_ROOT);
|
||||
|
||||
// 6. 执行编译
|
||||
let compileResult;
|
||||
try {
|
||||
compileResult = await execCommand(iverilogPath, compileArgs, {
|
||||
cwd: projectPath,
|
||||
env: env,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: `iverilog 编译失败:\n${error.message}`,
|
||||
stderr: error.stderr,
|
||||
stdout: error.stdout,
|
||||
};
|
||||
}
|
||||
|
||||
// 7. 执行仿真生成 VCD
|
||||
const simArgs = [outputFile];
|
||||
console.log("执行仿真命令:", vvpPath, simArgs.join(" "));
|
||||
|
||||
let simResult;
|
||||
try {
|
||||
simResult = await execCommand(vvpPath, simArgs, {
|
||||
cwd: projectPath,
|
||||
env: env,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: `VVP 仿真失败:\n${error.message}`,
|
||||
stderr: error.stderr,
|
||||
stdout: error.stdout,
|
||||
};
|
||||
}
|
||||
|
||||
// 8. 查找生成的 VCD 文件
|
||||
const vcdFiles = fs.readdirSync(projectPath).filter(file => file.endsWith('.vcd'));
|
||||
|
||||
if (vcdFiles.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
|
||||
stdout: simResult.stdout,
|
||||
};
|
||||
}
|
||||
|
||||
// 使用找到的第一个 VCD 文件
|
||||
const vcdFile = path.join(projectPath, vcdFiles[0]);
|
||||
|
||||
// 9. 清理中间文件
|
||||
try {
|
||||
if (fs.existsSync(outputFile)) {
|
||||
fs.unlinkSync(outputFile);
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略清理错误
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
vcdFilePath: vcdFile,
|
||||
outputPath: projectPath,
|
||||
message: `VCD 文件生成成功!\n路径: ${vcdFile}`,
|
||||
stdout: simResult.stdout,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `生成 VCD 文件时出错: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 iverilog 是否可用
|
||||
*/
|
||||
export async function checkIverilogAvailable(
|
||||
extensionPath: string
|
||||
): Promise<{ available: boolean; version?: string; message: string }> {
|
||||
try {
|
||||
const iverilogPath = getIverilogPath(extensionPath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(iverilogPath)) {
|
||||
return {
|
||||
available: false,
|
||||
message: `iverilog 不可用。未找到文件: ${iverilogPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 设置环境变量
|
||||
const env = {
|
||||
...process.env,
|
||||
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
|
||||
};
|
||||
|
||||
const result = await execCommand(iverilogPath, ["-V"], {
|
||||
cwd: path.dirname(iverilogPath),
|
||||
env: env,
|
||||
});
|
||||
const version = result.stdout.trim().split("\n")[0];
|
||||
|
||||
return {
|
||||
available: true,
|
||||
version: version,
|
||||
message: `iverilog 可用: ${version}`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
available: false,
|
||||
message: `iverilog 执行失败: ${error.message}\n${error.stderr || ""}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -8,16 +8,28 @@ import {
|
||||
renameFile,
|
||||
replaceFile,
|
||||
} from "./createFiles";
|
||||
import {
|
||||
generateVCD,
|
||||
checkVerilogProject,
|
||||
checkIverilogAvailable,
|
||||
} from "./iverilogRunner";
|
||||
|
||||
/**
|
||||
* 处理用户消息
|
||||
*/
|
||||
export async function handleUserMessage(
|
||||
panel: vscode.WebviewPanel,
|
||||
text: string
|
||||
text: string,
|
||||
extensionPath?: string
|
||||
) {
|
||||
console.log("收到用户消息:", text);
|
||||
|
||||
// 检查是否是 VCD 生成命令
|
||||
if (isVCDGenerationCommand(text)) {
|
||||
await handleVCDGeneration(panel, extensionPath || "");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是文件操作命令
|
||||
const fileOperation = parseFileOperation(text);
|
||||
|
||||
@ -466,3 +478,161 @@ export function insertCodeToEditor(code: string) {
|
||||
vscode.window.showWarningMessage("请先打开一个编辑器");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是 VCD 生成命令
|
||||
*/
|
||||
function isVCDGenerationCommand(text: string): boolean {
|
||||
const lowerText = text.toLowerCase().trim();
|
||||
|
||||
// 匹配各种 VCD 生成命令
|
||||
const vcdPatterns = [
|
||||
/生成\s*vcd/,
|
||||
/创建\s*vcd/,
|
||||
/运行\s*仿真/,
|
||||
/执行\s*仿真/,
|
||||
/iverilog/,
|
||||
/生成\s*波形/,
|
||||
/仿真\s*生成/,
|
||||
];
|
||||
|
||||
return vcdPatterns.some((pattern) => pattern.test(lowerText));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 VCD 生成请求
|
||||
*/
|
||||
async function handleVCDGeneration(
|
||||
panel: vscode.WebviewPanel,
|
||||
extensionPath: string
|
||||
) {
|
||||
try {
|
||||
// 获取当前工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "❌ 请先打开一个工作区文件夹",
|
||||
});
|
||||
vscode.window.showErrorMessage("请先打开一个工作区文件夹");
|
||||
return;
|
||||
}
|
||||
|
||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||
|
||||
// 发送开始消息
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "🔍 正在检查项目文件...",
|
||||
});
|
||||
|
||||
// 1. 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(extensionPath);
|
||||
if (!iverilogCheck.available) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ ${iverilogCheck.message}\n\n请参考插件文档安装 iverilog 工具。`,
|
||||
});
|
||||
vscode.window.showErrorMessage(iverilogCheck.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 检查项目文件完整性
|
||||
const projectCheck = await checkVerilogProject(projectPath);
|
||||
|
||||
if (!projectCheck.isComplete) {
|
||||
let errorMsg = "❌ 项目文件不完整:\n\n";
|
||||
|
||||
if (projectCheck.allVerilogFiles.length === 0) {
|
||||
errorMsg += "• 未找到任何 Verilog 文件 (.v 或 .sv)\n";
|
||||
} else {
|
||||
errorMsg += `• 找到 ${projectCheck.allVerilogFiles.length} 个 Verilog 文件\n`;
|
||||
|
||||
if (!projectCheck.hasTopModule) {
|
||||
errorMsg += "• ❌ 缺少顶层模块文件\n";
|
||||
} else {
|
||||
errorMsg += `• ✅ 顶层模块: ${projectCheck.topModuleFile}\n`;
|
||||
}
|
||||
|
||||
if (!projectCheck.hasTestbench) {
|
||||
errorMsg += "• ❌ 缺少 testbench 文件\n";
|
||||
errorMsg += "\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
||||
} else {
|
||||
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (projectCheck.errors.length > 0) {
|
||||
errorMsg += "\n错误信息:\n" + projectCheck.errors.join("\n");
|
||||
}
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: errorMsg,
|
||||
});
|
||||
vscode.window.showWarningMessage("项目文件不完整,无法生成 VCD");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 显示项目检查结果
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `✅ 项目检查通过!\n\n找到 ${projectCheck.allVerilogFiles.length} 个 Verilog 文件\n• 顶层模块: ${projectCheck.topModuleFile}\n• Testbench: ${projectCheck.testbenchFile}\n\n🚀 开始编译和仿真...`,
|
||||
});
|
||||
|
||||
// 4. 生成 VCD 文件
|
||||
const result = await generateVCD(projectPath, extensionPath);
|
||||
|
||||
if (result.success) {
|
||||
let successMsg = `✅ ${result.message}`;
|
||||
|
||||
if (result.stdout) {
|
||||
successMsg += `\n\n仿真输出:\n${result.stdout}`;
|
||||
}
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: successMsg,
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`VCD 文件生成成功: ${result.vcdFilePath}`,
|
||||
"打开文件"
|
||||
).then((selection) => {
|
||||
if (selection === "打开文件" && result.vcdFilePath) {
|
||||
vscode.workspace.openTextDocument(result.vcdFilePath).then((doc) => {
|
||||
vscode.window.showTextDocument(doc);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let errorMsg = `❌ ${result.message}`;
|
||||
|
||||
if (result.stderr) {
|
||||
errorMsg += `\n\n错误输出:\n${result.stderr}`;
|
||||
}
|
||||
|
||||
if (result.stdout) {
|
||||
errorMsg += `\n\n标准输出:\n${result.stdout}`;
|
||||
}
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: errorMsg,
|
||||
});
|
||||
|
||||
vscode.window.showErrorMessage("VCD 文件生成失败");
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `❌ 生成 VCD 文件时出错: ${
|
||||
error instanceof Error ? error.message : "未知错误"
|
||||
}`;
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: errorMsg,
|
||||
});
|
||||
|
||||
vscode.window.showErrorMessage(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user