commit f66e9959e5b9182d688c85fe105b5e1f033f8bb2 Author: 吴文峰 Date: Mon Jun 8 17:56:41 2026 +0800 创建工程 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1a2b72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules/ + +# TypeScript build output +out/ +*.tsbuildinfo + +# VS Code extension packages +*.vsix + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Test and coverage output +coverage/ +.nyc_output/ +.vscode-test/ + +# Local environment files +.env +.env.* + +# OS files +.DS_Store +Thumbs.db + +# Editor local state +.vscode/settings.json +.history/ + +# Claude/local workspace files +.claude/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..00b2c07 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + // 调试时打开 A2288 工程目录 + "C:\\Users\\wuwen\\Documents\\prj\\A2288\\2288_test" + ], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1c4b004 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,27 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "compile", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$tsc" + }, + { + "type": "npm", + "script": "watch", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "isBackground": true, + "problemMatcher": "$tsc-watch" + } + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5970c2a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,58 @@ +{ + "name": "fmd-c-compiler", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fmd-c-compiler", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.3.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.42.tgz", + "integrity": "sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.120.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.120.0.tgz", + "integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d7fefa4 --- /dev/null +++ b/package.json @@ -0,0 +1,353 @@ +{ + "name": "fmd-c-compiler", + "displayName": "FMD C Compiler", + "description": "FMD/FT61FC6X 系列 MCU 编译器支持(C.exe 工具链)", + "version": "0.2.0", + "engines": { + "vscode": "^1.85.0" + }, + "categories": ["Other"], + "activationEvents": [], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "fmdCompiler.build", + "title": "FMD: Build Project", + "icon": "$(play)" + }, + { + "command": "fmdCompiler.buildFile", + "title": "FMD: Build Current File" + }, + { + "command": "fmdCompiler.clean", + "title": "FMD: Clean Project" + }, + { + "command": "fmdCompiler.selectProject", + "title": "FMD: Select Project (.prj)" + }, + { + "command": "fmdCompiler.openOutput", + "title": "FMD: Show Build Output" + }, + { + "command": "fmdCompiler.setCompilerPath", + "title": "FMD: Set Compiler Path" + }, + { + "command": "fmdCompiler.detectCompilerPath", + "title": "FMD: Detect Compiler Path" + }, + { + "command": "fmdCompiler.selectChip", + "title": "FMD: Select Target Chip" + }, + { + "command": "fmdCompiler.syncChipFromProject", + "title": "FMD: Use Chip From Project File" + }, + { + "command": "fmdCompiler.configureProgrammer", + "title": "FMD: Configure Programmer" + }, + { + "command": "fmdCompiler.download", + "title": "FMD: Download/Program MCU", + "icon": "$(cloud-upload)" + }, + { + "command": "fmdCompiler.buildAndDownload", + "title": "FMD: Build and Download MCU" + }, + { + "command": "fmdCompiler.openEeprom", + "title": "FMD: Open EEPROM Editor" + }, + { + "command": "fmdCompiler.readEeprom", + "title": "FMD: Read EEPROM From MCU" + }, + { + "command": "fmdCompiler.writeEeprom", + "title": "FMD: Write EEPROM To MCU" + }, + { + "command": "fmdCompiler.exportEepromHex", + "title": "FMD: Export EEPROM HEX" + } + ], + "keybindings": [ + { + "command": "fmdCompiler.build", + "key": "f7", + "when": "editorTextFocus && resourceExtname =~ /\\.[cChH]$/" + } + ], + "menus": { + "editor/title": [ + { + "command": "fmdCompiler.build", + "when": "resourceExtname =~ /\\.[cChH]$/", + "group": "navigation" + }, + { + "command": "fmdCompiler.download", + "when": "resourceExtname =~ /\\.[cChH]$/", + "group": "navigation" + } + ], + "editor/context": [ + { + "command": "fmdCompiler.build", + "when": "resourceExtname =~ /\\.[cChH]$/", + "group": "fmd@1" + }, + { + "command": "fmdCompiler.buildFile", + "when": "resourceExtname =~ /\\.[cC]$/", + "group": "fmd@2" + }, + { + "command": "fmdCompiler.clean", + "when": "resourceExtname =~ /\\.[cChH]$/", + "group": "fmd@3" + }, + { + "command": "fmdCompiler.buildAndDownload", + "when": "resourceExtname =~ /\\.[cChH]$/", + "group": "fmd@4" + }, + { + "command": "fmdCompiler.openEeprom", + "when": "resourceExtname =~ /\\.[cChH]$/", + "group": "fmd@5" + } + ], + "explorer/context": [ + { + "command": "fmdCompiler.build", + "when": "resourceExtname == '.prj' || resourceExtname =~ /\\.[cC]$/", + "group": "fmd@1" + }, + { + "command": "fmdCompiler.selectProject", + "when": "resourceExtname == '.prj'", + "group": "fmd@2" + }, + { + "command": "fmdCompiler.download", + "when": "resourceExtname == '.prj' || resourceExtname == '.hex' || resourceExtname == '.bin'", + "group": "fmd@3" + }, + { + "command": "fmdCompiler.buildAndDownload", + "when": "resourceExtname == '.prj' || resourceExtname =~ /\\.[cC]$/", + "group": "fmd@4" + }, + { + "command": "fmdCompiler.openEeprom", + "when": "resourceExtname == '.prj' || resourceExtname == '.hex'", + "group": "fmd@5" + } + ] + }, + "configuration": { + "title": "FMD Compiler", + "properties": { + "fmdCompiler.compilerPath": { + "type": "string", + "default": "C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe", + "description": "编译器 c.exe 的路径" + }, + "fmdCompiler.compilerSearchPaths": { + "type": "array", + "default": [ + "C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe", + "C:\\Program Files\\CCompiler\\Compiler\\data\\bin\\c.exe" + ], + "items": { + "type": "string" + }, + "description": "自动检测编译器时检查的候选路径" + }, + "fmdCompiler.projectFile": { + "type": "string", + "default": "", + "description": "当前工程 .prj 文件路径(留空则自动搜索)" + }, + "fmdCompiler.chip": { + "type": "string", + "default": "FT61FC6X", + "description": "目标芯片型号" + }, + "fmdCompiler.outputDir": { + "type": "string", + "default": "", + "description": "输出目录(留空则与工程同目录)" + }, + "fmdCompiler.extraArgs": { + "type": "string", + "default": "", + "description": "额外的编译器参数" + }, + "fmdCompiler.autoSaveBeforeBuild": { + "type": "boolean", + "default": true, + "description": "编译前自动保存所有文件" + }, + "fmdCompiler.showOutputOnBuild": { + "type": "boolean", + "default": true, + "description": "编译时自动显示输出面板" + }, + "fmdCompiler.programmerPath": { + "type": "string", + "default": "", + "description": "外部烧录/下载工具路径" + }, + "fmdCompiler.programmerArgs": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "烧录工具参数,支持 ${chip}、${hexFile}、${binFile}、${downloadFile} 等变量" + }, + "fmdCompiler.programmerCwd": { + "type": "string", + "default": "${projectDir}", + "description": "烧录工具工作目录" + }, + "fmdCompiler.programmerUseShell": { + "type": "boolean", + "default": false, + "description": "是否通过 shell 执行烧录工具" + }, + "fmdCompiler.programmerSuccessExitCodes": { + "type": "array", + "default": [0], + "items": { + "type": "number" + }, + "description": "认为烧录成功的退出码" + }, + "fmdCompiler.downloadFileType": { + "type": "string", + "enum": ["hex", "bin"], + "default": "hex", + "description": "下载程序时使用的文件类型" + }, + "fmdCompiler.autoBuildBeforeDownload": { + "type": "boolean", + "default": false, + "description": "下载前是否自动编译" + }, + "fmdCompiler.showOutputOnDownload": { + "type": "boolean", + "default": true, + "description": "下载时自动显示输出面板" + }, + "fmdCompiler.eepromBaseAddress": { + "type": "string", + "default": "0x2100", + "description": "EEPROM 在 HEX 文件中的基地址" + }, + "fmdCompiler.eepromStart": { + "type": "string", + "default": "0x00", + "description": "EEPROM 逻辑起始地址" + }, + "fmdCompiler.eepromSize": { + "type": "number", + "default": 112, + "description": "EEPROM 字节数" + }, + "fmdCompiler.eepromFill": { + "type": "string", + "default": "0xFF", + "description": "新建 EEPROM 镜像的默认填充值" + }, + "fmdCompiler.eepromImageFile": { + "type": "string", + "default": "", + "description": "EEPROM 镜像文件路径(留空自动使用工程名 .eep.hex)" + }, + "fmdCompiler.eepromReadArgs": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "外部工具读取 EEPROM 的参数,支持 ${eepromFile} 等变量" + }, + "fmdCompiler.eepromWriteArgs": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "外部工具写入 EEPROM 的参数,支持 ${eepromFile} 等变量" + } + } + }, + "languages": [ + { + "id": "c", + "extensions": [".C", ".c"], + "aliases": ["C", "c"] + } + ], + "problemMatchers": [ + { + "name": "fmd-gcc", + "owner": "fmd", + "fileLocation": ["absolute"], + "pattern": [ + { + "regexp": "^(.+\\.(?:[cChH]|C|H)):(\\d+):\\s+(error|warning|note):\\s+(.+)$", + "file": 1, + "line": 2, + "severity": 3, + "message": 4 + } + ] + }, + { + "name": "fmd-linker", + "owner": "fmd", + "fileLocation": ["absolute"], + "pattern": [ + { + "regexp": "^.*?:\\s+(Error|Warning)\\s+\\[(\\w+)\\]\\s+(.+)$", + "severity": 1, + "code": 2, + "message": 3 + } + ] + } + ], + "taskDefinitions": [ + { + "type": "fmd-build", + "properties": { + "projectFile": { + "type": "string", + "description": ".prj 文件路径" + } + } + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/vscode": "^1.85.0", + "@types/node": "^20.0.0", + "typescript": "^5.3.0" + } +} diff --git a/src/compiler.ts b/src/compiler.ts new file mode 100644 index 0000000..df346c7 --- /dev/null +++ b/src/compiler.ts @@ -0,0 +1,406 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { spawn } from 'child_process'; +import { getConfig } from './extension'; +import { FmdDiagnostics } from './diagnostics'; +import { FmdProjectInfo, FmdProjectManager } from './projectManager'; + +export interface FmdOutputArtifacts { + projectDir: string; + projectName: string; + outputDir: string; + hexFile: string; + binFile: string; +} + +export interface FmdBuildResult { + success: boolean; + exitCode: number; + artifacts?: FmdOutputArtifacts; +} + +export class FmdCompiler { + private outputChannel: vscode.OutputChannel; + private diagnostics: FmdDiagnostics; + private projectManager: FmdProjectManager; + private building = false; + + constructor( + outputChannel: vscode.OutputChannel, + diagnostics: FmdDiagnostics, + projectManager: FmdProjectManager + ) { + this.outputChannel = outputChannel; + this.diagnostics = diagnostics; + this.projectManager = projectManager; + } + + reloadConfig() { + this.outputChannel.appendLine('[FMD] 配置已更新'); + } + + /** + * 编译整个工程(所有 .c/.C 文件 + 链接) + */ + async buildProject(): Promise { + const result = await this.buildProjectWithResult(); + if (!result.success && result.exitCode !== -1) { + vscode.window.showErrorMessage(`FMD: 编译失败,退出码 ${result.exitCode},请查看输出面板`); + } + } + + async buildProjectWithResult(): Promise { + if (this.building) { + vscode.window.showWarningMessage('FMD: 编译正在进行中...'); + return { success: false, exitCode: -1 }; + } + + const cfg = getConfig(); + + // 自动保存 + if (cfg.autoSaveBeforeBuild) { + await vscode.workspace.saveAll(false); + } + + const projectInfo = await this.getProjectInfo(); + const projectDir = projectInfo?.projectDir || await this.getProjectDir(); + if (!projectDir) { + return { success: false, exitCode: -1 }; + } + + const projectName = projectInfo?.projectName || path.basename(projectDir); + const outputDir = cfg.outputDir || projectDir; + const artifacts = this.getOutputArtifacts(projectDir, projectName, outputDir); + + this.building = true; + this.diagnostics.clear(); + + if (cfg.showOutputOnBuild) { + this.outputChannel.show(true); + } + + this.outputChannel.appendLine(''); + this.outputChannel.appendLine(`========== FMD 开始编译: ${projectName} ==========`); + this.outputChannel.appendLine(`工程目录: ${projectDir}`); + this.outputChannel.appendLine(`芯片: ${cfg.chip}`); + if (projectInfo?.device && projectInfo.device !== cfg.chip) { + this.outputChannel.appendLine(`[警告] 配置芯片 ${cfg.chip} 与工程文件 Device ${projectInfo.device} 不一致,本次编译使用配置芯片 ${cfg.chip}`); + } + this.outputChannel.appendLine(new Date().toLocaleString()); + this.outputChannel.appendLine(''); + + try { + const cFiles = this.getSourceFiles(projectDir, projectInfo); + + if (cFiles.length === 0) { + vscode.window.showErrorMessage('工程目录中没有找到 .c/.C 文件'); + return { success: false, exitCode: -1, artifacts }; + } + + this.outputChannel.appendLine(`源文件 (${cFiles.length} 个):`); + cFiles.forEach(f => this.outputChannel.appendLine(` ${path.basename(f)}`)); + this.outputChannel.appendLine(''); + + // 构建编译命令 + // c.exe 是 XC8-like 驱动,可以接受多文件一次编译+链接 + const args = this.buildCompileArgs(cFiles, projectDir, outputDir, projectName, cfg); + + this.outputChannel.appendLine('编译命令:'); + this.outputChannel.appendLine(` ${cfg.compilerPath} ${args.join(' ')}`); + this.outputChannel.appendLine(''); + + const exitCode = await this.runCompiler(cfg.compilerPath, args, projectDir); + + if (exitCode === 0) { + this.outputChannel.appendLine(''); + this.outputChannel.appendLine('========== 编译成功 =========='); + + this.logArtifact(artifacts.hexFile); + this.logArtifact(artifacts.binFile); + + vscode.window.showInformationMessage('FMD: 编译成功 ✓'); + return { success: true, exitCode, artifacts }; + } + + this.outputChannel.appendLine(''); + this.outputChannel.appendLine(`========== 编译失败 (退出码: ${exitCode}) ==========`); + return { success: false, exitCode, artifacts }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + this.outputChannel.appendLine(`[错误] ${msg}`); + vscode.window.showErrorMessage(`FMD 编译异常: ${msg}`); + return { success: false, exitCode: -1, artifacts }; + } finally { + this.building = false; + } + } + + /** + * 仅编译单个 .c 文件(生成 .obj,不链接) + */ + async buildFile(filePath: string): Promise { + if (this.building) { + vscode.window.showWarningMessage('FMD: 编译正在进行中...'); + return; + } + + const cfg = getConfig(); + if (cfg.autoSaveBeforeBuild) { + await vscode.workspace.saveAll(false); + } + + const projectDir = path.dirname(filePath); + const fileName = path.basename(filePath); + + this.building = true; + this.diagnostics.clear(); + + if (cfg.showOutputOnBuild) { + this.outputChannel.show(true); + } + + this.outputChannel.appendLine(''); + this.outputChannel.appendLine(`========== FMD 编译文件: ${fileName} ==========`); + + try { + // 单文件编译:只预处理+编译到 .obj,不链接 + const args = [ + `--chip=${cfg.chip}`, + '-Q', // 静默链接器 + '-P', // 仅编译,不链接(生成 .p1 和 .obj) + `-I${path.join(path.dirname(cfg.compilerPath), '..', 'include')}`, + filePath, + ]; + + if (cfg.extraArgs) { + args.push(...cfg.extraArgs.split(' ').filter(a => a)); + } + + this.outputChannel.appendLine(`命令: ${cfg.compilerPath} ${args.join(' ')}`); + + const exitCode = await this.runCompiler(cfg.compilerPath, args, projectDir); + + if (exitCode === 0) { + this.outputChannel.appendLine('单文件编译成功'); + vscode.window.showInformationMessage(`FMD: ${fileName} 编译成功 ✓`); + } else { + vscode.window.showErrorMessage(`FMD: ${fileName} 编译失败`); + } + } finally { + this.building = false; + } + } + + /** + * 清理编译产物 + */ + async cleanProject(): Promise { + const projectDir = await this.getProjectDir(); + if (!projectDir) { + return; + } + + const cleanExts = ['.obj', '.p1', '.pre', '.d', '.lpp', '.cmf', '.sym', '.map', '.rlf', '.sdb', '.asm']; + let count = 0; + + try { + const files = fs.readdirSync(projectDir); + for (const f of files) { + const ext = path.extname(f).toLowerCase(); + if (cleanExts.includes(ext)) { + fs.unlinkSync(path.join(projectDir, f)); + count++; + } + } + this.outputChannel.appendLine(`[FMD] 清理完成,删除 ${count} 个中间文件`); + vscode.window.showInformationMessage(`FMD: 清理完成,删除 ${count} 个中间文件`); + } catch (err) { + vscode.window.showErrorMessage(`FMD 清理失败: ${err}`); + } + } + + async resolveOutputArtifacts(): Promise { + const cfg = getConfig(); + const projectInfo = await this.getProjectInfo(); + const projectDir = projectInfo?.projectDir || await this.getProjectDir(); + if (!projectDir) { + return undefined; + } + + const projectName = projectInfo?.projectName || path.basename(projectDir); + const outputDir = cfg.outputDir || projectDir; + return this.getOutputArtifacts(projectDir, projectName, outputDir); + } + + getOutputArtifacts(projectDir: string, projectName: string, outputDir: string): FmdOutputArtifacts { + return { + projectDir, + projectName, + outputDir, + hexFile: path.join(outputDir, projectName + '.hex'), + binFile: path.join(outputDir, projectName + '.bin'), + }; + } + + /** + * 构造编译参数 + * 基于对 .map 文件的分析,c.exe 是 XC8-style 驱动器 + * 标准调用:c.exe --chip=CHIP [options] file1.c file2.c ... + */ + private buildCompileArgs( + cFiles: string[], + projectDir: string, + outputDir: string, + projectName: string, + cfg: ReturnType + ): string[] { + const compilerBinDir = path.dirname(cfg.compilerPath); + const compilerDataDir = path.dirname(compilerBinDir); // data/ + const includeDir = path.join(compilerDataDir, 'include'); + + const args: string[] = [ + `--chip=${cfg.chip}`, + `-I${includeDir}`, + `-o${path.join(outputDir, projectName + '.hex')}`, + ]; + + // 额外参数 + if (cfg.extraArgs) { + args.push(...cfg.extraArgs.split(' ').filter(a => a)); + } + + // 源文件 + args.push(...cFiles); + + return args; + } + + /** + * 执行编译器,实时输出日志 + */ + private runCompiler( + compilerPath: string, + args: string[], + cwd: string + ): Promise { + return new Promise((resolve, reject) => { + // 检查编译器是否存在 + if (!fs.existsSync(compilerPath)) { + reject(new Error(`编译器不存在: ${compilerPath}\n请检查 fmdCompiler.compilerPath 配置`)); + return; + } + + const proc = spawn(compilerPath, args, { + cwd, + shell: false, + env: { ...process.env }, + }); + + let stdout = ''; + let stderr = ''; + + proc.stdout.on('data', (data: Buffer) => { + const text = data.toString(); + stdout += text; + // 实时输出,逐行过滤空行 + text.split('\n').forEach(line => { + if (line.trim()) { + this.outputChannel.appendLine(line.replace(/\r$/, '')); + } + }); + }); + + proc.stderr.on('data', (data: Buffer) => { + const text = data.toString(); + stderr += text; + text.split('\n').forEach(line => { + if (line.trim()) { + this.outputChannel.appendLine(line.replace(/\r$/, '')); + } + }); + }); + + proc.on('close', (code: number | null) => { + const allOutput = stdout + stderr; + // 解析错误/警告,推送到诊断 + this.diagnostics.parse(allOutput); + resolve(code ?? 1); + }); + + proc.on('error', (err: Error) => { + reject(err); + }); + }); + } + + /** + * 获取工程目录 + */ + private async getProjectDir(): Promise { + const cfg = getConfig(); + + // 优先用配置指定的 .prj 文件目录 + if (cfg.projectFile && fs.existsSync(cfg.projectFile)) { + return path.dirname(cfg.projectFile); + } + + // 其次用 ProjectManager 自动检测到的 + const detected = this.projectManager.getProjectDir(); + if (detected) { + return detected; + } + + // 最后用当前打开的文件目录 + const editor = vscode.window.activeTextEditor; + if (editor) { + return path.dirname(editor.document.fileName); + } + + vscode.window.showErrorMessage('FMD: 未找到工程目录,请打开 .c 文件或配置 fmdCompiler.projectFile'); + return undefined; + } + + private async getProjectInfo(): Promise { + const cfg = getConfig(); + try { + if (cfg.projectFile && fs.existsSync(cfg.projectFile)) { + return this.projectManager.getProjectInfo(cfg.projectFile); + } + return this.projectManager.getProjectInfo(); + } catch (err) { + this.outputChannel.appendLine(`[警告] 读取工程文件失败: ${err}`); + return undefined; + } + } + + private getSourceFiles(projectDir: string, projectInfo?: FmdProjectInfo): string[] { + const projectSources = projectInfo?.sourceFiles + .filter(f => /\.[cC]$/.test(f) && fs.existsSync(f)); + + if (projectSources && projectSources.length > 0) { + return this.sortSourceFiles(projectSources); + } + + // 收集所有 .c/.C 文件 + return this.sortSourceFiles(fs.readdirSync(projectDir) + .filter(f => /\.[cC]$/.test(f)) + .map(f => path.join(projectDir, f))); + } + + private sortSourceFiles(files: string[]): string[] { + // 这个 8 位 MCU 编译器对多源文件顺序较敏感。历史 IDE 输出中源文件按文件名稳定排序, + // 例如 1028.c 在 2288_test.C 前;保持该顺序可避免部分工程触发工具链内部错误。 + return [...files].sort((a, b) => path.basename(a).localeCompare(path.basename(b), undefined, { + numeric: false, + sensitivity: 'base', + })); + } + + private logArtifact(filePath: string): void { + if (fs.existsSync(filePath)) { + const stat = fs.statSync(filePath); + this.outputChannel.appendLine(`输出: ${filePath} (${stat.size} 字节)`); + } + } +} diff --git a/src/diagnostics.ts b/src/diagnostics.ts new file mode 100644 index 0000000..4f7009d --- /dev/null +++ b/src/diagnostics.ts @@ -0,0 +1,110 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; + +interface DiagEntry { + file: string; + line: number; + col: number; + severity: vscode.DiagnosticSeverity; + message: string; + code?: string; +} + +export class FmdDiagnostics { + private collection: vscode.DiagnosticCollection; + + constructor(collection: vscode.DiagnosticCollection) { + this.collection = collection; + } + + clear() { + this.collection.clear(); + } + + /** + * 解析编译器输出,提取错误和警告 + * + * 支持的格式: + * 1. GCC 风格: file.c:12: error: xxx + * 2. XC8 风格: file.c:12:5: error: xxx + * 3. Linker 风格:(1234) Error [L1234] xxx + */ + parse(output: string) { + const entries: DiagEntry[] = []; + + // GCC/XC8 风格错误 + // 匹配:文件路径:行号: severity: 消息 + // 或: 文件路径:行号:列号: severity: 消息 + const gccPattern = /^((?:[A-Za-z]:)?[^:]+\.[cChH]):(\d+)(?::(\d+))?:\s*(error|warning|note):\s*(.+)$/gm; + + let m: RegExpExecArray | null; + while ((m = gccPattern.exec(output)) !== null) { + const [, file, lineStr, colStr, sev, msg] = m; + entries.push({ + file: this.normalizePath(file), + line: parseInt(lineStr, 10) - 1, + col: colStr ? parseInt(colStr, 10) - 1 : 0, + severity: this.mapSeverity(sev), + message: msg.trim(), + }); + } + + // XC8 linker 错误:如 :error: (1234) xxx + const linkerPattern = /^.*?(error|warning)\s*\[([A-Z]\d+)\]\s*(.+)$/gim; + while ((m = linkerPattern.exec(output)) !== null) { + const [, sev, code, msg] = m; + // 链接器错误没有文件位置,用一个特殊处理 + entries.push({ + file: '', + line: 0, + col: 0, + severity: this.mapSeverity(sev), + message: `[${code}] ${msg.trim()}`, + code, + }); + } + + // 按文件分组,推送到 VSCode + const fileMap = new Map(); + + for (const entry of entries) { + if (!entry.file) { + continue; + } + + const diag = new vscode.Diagnostic( + new vscode.Range(entry.line, entry.col, entry.line, entry.col + 999), + entry.message, + entry.severity + ); + if (entry.code) { + diag.code = entry.code; + } + + const key = entry.file; + if (!fileMap.has(key)) { + fileMap.set(key, []); + } + fileMap.get(key)!.push(diag); + } + + for (const [filePath, diags] of fileMap) { + const uri = vscode.Uri.file(filePath); + this.collection.set(uri, diags); + } + } + + private normalizePath(filePath: string): string { + // 确保路径分隔符统一 + return filePath.replace(/\//g, path.sep); + } + + private mapSeverity(sev: string): vscode.DiagnosticSeverity { + switch (sev.toLowerCase()) { + case 'error': return vscode.DiagnosticSeverity.Error; + case 'warning': return vscode.DiagnosticSeverity.Warning; + case 'note': return vscode.DiagnosticSeverity.Information; + default: return vscode.DiagnosticSeverity.Error; + } + } +} diff --git a/src/eepromManager.ts b/src/eepromManager.ts new file mode 100644 index 0000000..58e4128 --- /dev/null +++ b/src/eepromManager.ts @@ -0,0 +1,397 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { getConfig } from './extension'; +import { FmdCompiler } from './compiler'; +import { FmdProjectInfo, FmdProjectManager } from './projectManager'; +import { expandArgs, expandTokens, TokenContext, ToolRunner } from './toolRunner'; +import { extractRange, writeIntelHex } from './intelHex'; + +interface EepromGeometry { + baseAddress: number; + start: number; + size: number; + fill: number; +} + +export class FmdEepromManager { + private panel: vscode.WebviewPanel | undefined; + private data: Uint8Array | undefined; + private eepromFile: string | undefined; + private geometry: EepromGeometry | undefined; + private runner = new ToolRunner(); + + constructor( + private outputChannel: vscode.OutputChannel, + private projectManager: FmdProjectManager, + private compiler: FmdCompiler + ) {} + + async openEditor(): Promise { + const state = await this.loadState(); + if (!state) { + return; + } + + this.data = state.data; + this.eepromFile = state.eepromFile; + this.geometry = state.geometry; + + if (!this.panel) { + this.panel = vscode.window.createWebviewPanel( + 'fmdEeprom', + 'FMD EEPROM', + vscode.ViewColumn.One, + { enableScripts: true } + ); + this.panel.onDidDispose(() => this.panel = undefined); + this.panel.webview.onDidReceiveMessage(async message => { + if (message.command === 'save') { + await this.saveFromWebview(message.values || {}); + } else if (message.command === 'reload') { + await this.openEditor(); + } else if (message.command === 'export') { + await this.exportEepromHex(); + } + }); + } + + this.panel.webview.html = this.renderWebview(state.data, state.geometry, state.labels, state.eepromFile); + this.panel.reveal(); + } + + async exportEepromHex(): Promise { + if (!this.data || !this.geometry) { + const state = await this.loadState(); + if (!state) { + return; + } + this.data = state.data; + this.geometry = state.geometry; + this.eepromFile = state.eepromFile; + } + + const target = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(this.eepromFile || 'eeprom.eep.hex'), + filters: { + 'Intel HEX': ['hex', 'eep', 'eep.hex'], + '所有文件': ['*'], + }, + }); + + if (!target) { + return; + } + + fs.writeFileSync(target.fsPath, writeIntelHex(this.data, this.geometry.baseAddress)); + this.eepromFile = target.fsPath; + vscode.window.showInformationMessage(`FMD: EEPROM 已导出: ${target.fsPath}`); + } + + async readEeprom(): Promise { + const cfg = getConfig(); + const state = await this.loadState(); + if (!state) { + return; + } + + if (!cfg.programmerPath || cfg.eepromReadArgs.length === 0) { + vscode.window.showWarningMessage('FMD: 未配置 EEPROM 读取命令,请设置 fmdCompiler.programmerPath 和 fmdCompiler.eepromReadArgs'); + return; + } + + const artifacts = await this.compiler.resolveOutputArtifacts(); + if (!artifacts) { + return; + } + + const context = this.createTokenContext(artifacts, state.eepromFile, state.geometry); + const result = await this.runner.run({ + executable: cfg.programmerPath, + args: expandArgs(cfg.eepromReadArgs, context), + cwd: expandTokens(cfg.programmerCwd || '${projectDir}', context), + shell: cfg.programmerUseShell, + outputChannel: this.outputChannel, + label: 'FMD 读取 EEPROM', + successExitCodes: cfg.programmerSuccessExitCodes, + }); + + if (result.success) { + vscode.window.showInformationMessage('FMD: EEPROM 读取完成 ✓'); + await this.openEditor(); + } else { + vscode.window.showErrorMessage(`FMD: EEPROM 读取失败,退出码 ${result.exitCode}`); + } + } + + async writeEeprom(): Promise { + if (this.data && this.geometry && this.eepromFile) { + fs.writeFileSync(this.eepromFile, writeIntelHex(this.data, this.geometry.baseAddress)); + } + + const cfg = getConfig(); + const state = await this.loadState(); + if (!state) { + return; + } + + if (!cfg.programmerPath || cfg.eepromWriteArgs.length === 0) { + vscode.window.showWarningMessage('FMD: 未配置 EEPROM 写入命令,请设置 fmdCompiler.programmerPath 和 fmdCompiler.eepromWriteArgs'); + return; + } + + const artifacts = await this.compiler.resolveOutputArtifacts(); + if (!artifacts) { + return; + } + + const context = this.createTokenContext(artifacts, state.eepromFile, state.geometry); + const result = await this.runner.run({ + executable: cfg.programmerPath, + args: expandArgs(cfg.eepromWriteArgs, context), + cwd: expandTokens(cfg.programmerCwd || '${projectDir}', context), + shell: cfg.programmerUseShell, + outputChannel: this.outputChannel, + label: 'FMD 写入 EEPROM', + successExitCodes: cfg.programmerSuccessExitCodes, + }); + + if (result.success) { + vscode.window.showInformationMessage('FMD: EEPROM 写入完成 ✓'); + } else { + vscode.window.showErrorMessage(`FMD: EEPROM 写入失败,退出码 ${result.exitCode}`); + } + } + + private async saveFromWebview(values: Record): Promise { + if (!this.data || !this.geometry || !this.eepromFile) { + return; + } + + for (const [key, value] of Object.entries(values)) { + const index = parseInt(key, 10); + const byte = parseInt(value, 16); + if (!Number.isNaN(index) && !Number.isNaN(byte) && index >= 0 && index < this.data.length) { + this.data[index] = byte & 0xff; + } + } + + fs.writeFileSync(this.eepromFile, writeIntelHex(this.data, this.geometry.baseAddress)); + vscode.window.showInformationMessage(`FMD: EEPROM 已保存: ${this.eepromFile}`); + } + + private async loadState(): Promise<{ data: Uint8Array; geometry: EepromGeometry; labels: Map; eepromFile: string } | undefined> { + const projectInfo = this.getProjectInfo(); + const artifacts = await this.compiler.resolveOutputArtifacts(); + const projectDir = projectInfo?.projectDir || artifacts?.projectDir; + const projectName = projectInfo?.projectName || artifacts?.projectName; + + if (!projectDir || !projectName) { + vscode.window.showErrorMessage('FMD: 未找到工程,无法打开 EEPROM'); + return undefined; + } + + const geometry = this.resolveGeometry(projectDir, projectName); + const eepromFile = this.resolveEepromFile(projectDir, projectName, projectInfo); + const data = this.loadEepromData(eepromFile, artifacts?.hexFile, geometry); + const labels = this.loadLabels(projectInfo, projectDir); + + return { data, geometry, labels, eepromFile }; + } + + private resolveGeometry(projectDir: string, projectName: string): EepromGeometry { + const cfg = getConfig(); + const mapFile = path.join(projectDir, projectName + '.map'); + const configured = { + baseAddress: parseNumber(cfg.eepromBaseAddress, 0x2100), + start: parseNumber(cfg.eepromStart, 0), + size: cfg.eepromSize, + fill: parseNumber(cfg.eepromFill, 0xff), + }; + + if (!fs.existsSync(mapFile)) { + return configured; + } + + const text = fs.readFileSync(mapFile, 'utf8'); + const m = /-AEEDATA=([0-9A-Fa-f]+)h-([0-9A-Fa-f]+)h\/([0-9A-Fa-f]+)h/.exec(text); + if (!m) { + return configured; + } + + const start = parseInt(m[1], 16); + const end = parseInt(m[2], 16); + const baseAddress = parseInt(m[3], 16); + return { + baseAddress, + start, + size: end - start + 1, + fill: configured.fill, + }; + } + + private resolveEepromFile(projectDir: string, projectName: string, projectInfo?: FmdProjectInfo): string { + const cfg = getConfig(); + if (cfg.eepromImageFile) { + return path.isAbsolute(cfg.eepromImageFile) ? cfg.eepromImageFile : path.join(projectDir, cfg.eepromImageFile); + } + if (projectInfo?.eeFile) { + return projectInfo.eeFile; + } + return path.join(projectDir, `${projectName}.eep.hex`); + } + + private loadEepromData(eepromFile: string, hexFile: string | undefined, geometry: EepromGeometry): Uint8Array { + if (fs.existsSync(eepromFile)) { + return extractRange(fs.readFileSync(eepromFile, 'utf8'), geometry.baseAddress, geometry.size, geometry.fill); + } + + if (hexFile && fs.existsSync(hexFile)) { + return extractRange(fs.readFileSync(hexFile, 'utf8'), geometry.baseAddress, geometry.size, geometry.fill); + } + + const data = new Uint8Array(geometry.size); + data.fill(geometry.fill & 0xff); + return data; + } + + private loadLabels(projectInfo: FmdProjectInfo | undefined, projectDir: string): Map { + const labels = new Map(); + const files = new Set(projectInfo ? [...projectInfo.sourceFiles, ...projectInfo.headerFiles] : []); + + if (files.size === 0) { + for (const file of fs.readdirSync(projectDir)) { + if (/\.[cChH]$/.test(file)) { + files.add(path.join(projectDir, file)); + } + } + } + + for (const file of files) { + if (!fs.existsSync(file)) { + continue; + } + const text = fs.readFileSync(file, 'utf8'); + const pattern = /^\s*#\s*define\s+(eeprom_[A-Za-z0-9_]+)\s+(0x[0-9A-Fa-f]+|\d+)/gm; + let m: RegExpExecArray | null; + while ((m = pattern.exec(text)) !== null) { + const address = parseNumber(m[2], -1); + if (address >= 0) { + const arr = labels.get(address) || []; + arr.push(m[1]); + labels.set(address, arr); + } + } + } + + return labels; + } + + private renderWebview(data: Uint8Array, geometry: EepromGeometry, labels: Map, eepromFile: string): string { + const rows = Array.from(data).map((value, index) => { + const logical = geometry.start + index; + const absolute = geometry.baseAddress + index; + const label = labels.get(logical)?.join(', ') || ''; + const ascii = value >= 32 && value <= 126 ? String.fromCharCode(value) : '.'; + return ` + 0x${logical.toString(16).toUpperCase().padStart(2, '0')} + 0x${absolute.toString(16).toUpperCase().padStart(4, '0')} + + ${escapeHtml(ascii)} + ${escapeHtml(label)} + `; + }).join(''); + + return ` + + + + + + +

FMD EEPROM 编辑器

+
文件: ${escapeHtml(eepromFile)}
+
Base: 0x${geometry.baseAddress.toString(16).toUpperCase()},Size: ${geometry.size} bytes
+
+ + + +
+ + +${rows} +
逻辑地址HEX 地址ASCII标签
+ + +`; + } + + private createTokenContext(artifacts: { projectDir: string; projectName: string; hexFile: string; binFile: string }, eepromFile: string, geometry: EepromGeometry): TokenContext { + const cfg = getConfig(); + return { + chip: cfg.chip, + projectFile: this.projectManager.getProjectFile() || cfg.projectFile, + projectDir: artifacts.projectDir, + projectName: artifacts.projectName, + compilerPath: cfg.compilerPath, + hexFile: artifacts.hexFile, + binFile: artifacts.binFile, + downloadFile: cfg.downloadFileType === 'bin' ? artifacts.binFile : artifacts.hexFile, + eepromFile, + eepromBaseAddress: `0x${geometry.baseAddress.toString(16).toUpperCase()}`, + eepromSize: String(geometry.size), + workspaceFolder: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '', + }; + } + + private getProjectInfo(): FmdProjectInfo | undefined { + const cfg = getConfig(); + try { + if (cfg.projectFile && fs.existsSync(cfg.projectFile)) { + return this.projectManager.getProjectInfo(cfg.projectFile); + } + return this.projectManager.getProjectInfo(); + } catch { + return undefined; + } + } +} + +function parseNumber(value: string, fallback: number): number { + const trimmed = value.trim(); + if (/^0x/i.test(trimmed)) { + return parseInt(trimmed.slice(2), 16); + } + if (/^[0-9A-Fa-f]+h$/i.test(trimmed)) { + return parseInt(trimmed.slice(0, -1), 16); + } + const parsed = parseInt(trimmed, 10); + return Number.isNaN(parsed) ? fallback : parsed; +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..2989643 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,247 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { FmdCompiler } from './compiler'; +import { FmdDiagnostics } from './diagnostics'; +import { FmdProjectManager } from './projectManager'; +import { FmdProgrammer } from './programmer'; +import { FmdEepromManager } from './eepromManager'; + +let outputChannel: vscode.OutputChannel; +let diagnosticsCollection: vscode.DiagnosticCollection; +let compiler: FmdCompiler; +let diagnostics: FmdDiagnostics; +let projectManager: FmdProjectManager; +let programmer: FmdProgrammer; +let eepromManager: FmdEepromManager; + +export function activate(context: vscode.ExtensionContext) { + outputChannel = vscode.window.createOutputChannel('FMD Compiler'); + diagnosticsCollection = vscode.languages.createDiagnosticCollection('fmd'); + diagnostics = new FmdDiagnostics(diagnosticsCollection); + projectManager = new FmdProjectManager(); + compiler = new FmdCompiler(outputChannel, diagnostics, projectManager); + programmer = new FmdProgrammer(outputChannel, projectManager, compiler); + eepromManager = new FmdEepromManager(outputChannel, projectManager, compiler); + + // 注册命令 + context.subscriptions.push( + vscode.commands.registerCommand('fmdCompiler.build', () => compiler.buildProject()), + vscode.commands.registerCommand('fmdCompiler.buildFile', (uri?: vscode.Uri) => { + const filePath = uri?.fsPath || vscode.window.activeTextEditor?.document.fileName; + if (filePath) { + compiler.buildFile(filePath); + } else { + vscode.window.showWarningMessage('没有可编译的文件'); + } + }), + vscode.commands.registerCommand('fmdCompiler.clean', () => compiler.cleanProject()), + vscode.commands.registerCommand('fmdCompiler.selectProject', (uri?: vscode.Uri) => { + if (uri) { + projectManager.setProjectFile(uri.fsPath); + vscode.window.showInformationMessage(`已选择工程: ${path.basename(uri.fsPath)}`); + } else { + projectManager.pickProjectFile(); + } + updateStatusBars(); + }), + vscode.commands.registerCommand('fmdCompiler.openOutput', () => { + outputChannel.show(); + }), + vscode.commands.registerCommand('fmdCompiler.setCompilerPath', () => setCompilerPath()), + vscode.commands.registerCommand('fmdCompiler.detectCompilerPath', () => detectCompilerPath()), + vscode.commands.registerCommand('fmdCompiler.selectChip', () => selectChip()), + vscode.commands.registerCommand('fmdCompiler.syncChipFromProject', () => syncChipFromProject()), + vscode.commands.registerCommand('fmdCompiler.configureProgrammer', () => programmer.configureProgrammer()), + vscode.commands.registerCommand('fmdCompiler.download', () => programmer.download()), + vscode.commands.registerCommand('fmdCompiler.buildAndDownload', () => programmer.buildAndDownload()), + vscode.commands.registerCommand('fmdCompiler.openEeprom', () => eepromManager.openEditor()), + vscode.commands.registerCommand('fmdCompiler.readEeprom', () => eepromManager.readEeprom()), + vscode.commands.registerCommand('fmdCompiler.writeEeprom', () => eepromManager.writeEeprom()), + vscode.commands.registerCommand('fmdCompiler.exportEepromHex', () => eepromManager.exportEepromHex()), + diagnosticsCollection + ); + + // 状态栏 + const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); + statusBar.command = 'fmdCompiler.build'; + const chipStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 99); + chipStatusBar.command = 'fmdCompiler.selectChip'; + const downloadStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 98); + downloadStatusBar.command = 'fmdCompiler.download'; + + const update = () => { + const cfg = getConfig(); + const projectFile = projectManager.getProjectFile() || cfg.projectFile; + statusBar.text = '$(play) FMD Build'; + statusBar.tooltip = projectFile ? `编译 FMD 工程: ${projectFile}` : '编译 FMD 工程 (F7)'; + chipStatusBar.text = `$(circuit-board) ${cfg.chip}`; + chipStatusBar.tooltip = '切换 FMD 目标芯片'; + downloadStatusBar.text = '$(cloud-upload) FMD Download'; + downloadStatusBar.tooltip = cfg.programmerPath ? `下载程序: ${cfg.programmerPath}` : '未配置烧录工具,点击配置后可下载'; + statusBar.show(); + chipStatusBar.show(); + downloadStatusBar.show(); + }; + updateStatusBars = update; + updateStatusBars(); + context.subscriptions.push(statusBar, chipStatusBar, downloadStatusBar); + + // 监听配置变化 + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('fmdCompiler')) { + compiler.reloadConfig(); + updateStatusBars(); + } + }) + ); + + // 尝试自动找工程文件 + projectManager.autoDetectProject(); + updateStatusBars(); + + outputChannel.appendLine('[FMD] 插件已激活'); + outputChannel.appendLine(`[FMD] 编译器: ${getConfig().compilerPath}`); +} + +export function deactivate() { + diagnosticsCollection?.dispose(); + outputChannel?.dispose(); +} + +let updateStatusBars = () => {}; + +async function setCompilerPath(): Promise { + const cfg = getConfig(); + const files = await vscode.window.showOpenDialog({ + canSelectMany: false, + openLabel: '选择 FMD 编译器 c.exe', + defaultUri: cfg.compilerPath ? vscode.Uri.file(path.dirname(cfg.compilerPath)) : undefined, + filters: { + 'Compiler': ['exe'], + '所有文件': ['*'], + }, + }); + + if (!files || files.length === 0) { + return; + } + + await vscode.workspace.getConfiguration('fmdCompiler').update('compilerPath', files[0].fsPath, vscode.ConfigurationTarget.Workspace); + vscode.window.showInformationMessage(`FMD: 编译器路径已设置: ${files[0].fsPath}`); +} + +async function detectCompilerPath(): Promise { + const cfg = getConfig(); + const candidates = Array.from(new Set([ + ...cfg.compilerSearchPaths, + 'C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe', + 'C:\\Program Files\\CCompiler\\Compiler\\data\\bin\\c.exe', + ])); + const found = candidates.find(file => fs.existsSync(file)); + + if (!found) { + vscode.window.showWarningMessage('FMD: 未自动找到编译器,请手动选择'); + await setCompilerPath(); + return; + } + + const answer = await vscode.window.showInformationMessage(`找到 FMD 编译器: ${found}`, '使用此路径', '取消'); + if (answer === '使用此路径') { + await vscode.workspace.getConfiguration('fmdCompiler').update('compilerPath', found, vscode.ConfigurationTarget.Workspace); + } +} + +async function selectChip(): Promise { + const cfg = getConfig(); + const chips = collectChipCandidates(); + const selected = await vscode.window.showQuickPick(chips, { + title: '选择 FMD 目标芯片', + placeHolder: cfg.chip, + }); + + if (!selected) { + return; + } + + await vscode.workspace.getConfiguration('fmdCompiler').update('chip', selected, vscode.ConfigurationTarget.Workspace); + outputChannel.appendLine(`[FMD] 已切换芯片: ${selected}`); + + const projectFile = projectManager.getProjectFile() || cfg.projectFile; + if (projectFile && fs.existsSync(projectFile)) { + const answer = await vscode.window.showInformationMessage(`是否同步修改工程文件 Device 为 ${selected}?`, '同步', '仅修改 VS Code 设置'); + if (answer === '同步') { + await projectManager.updateProjectDevice(projectFile, selected); + vscode.window.showInformationMessage(`FMD: 已更新工程芯片: ${selected}`); + } + } + + updateStatusBars(); +} + +async function syncChipFromProject(): Promise { + const projectChip = projectManager.getCurrentChip(getConfig().projectFile); + if (!projectChip) { + vscode.window.showWarningMessage('FMD: 当前工程文件中没有找到 Device 字段'); + return; + } + + await vscode.workspace.getConfiguration('fmdCompiler').update('chip', projectChip, vscode.ConfigurationTarget.Workspace); + vscode.window.showInformationMessage(`FMD: 已从工程同步芯片: ${projectChip}`); +} + +function collectChipCandidates(): string[] { + const cfg = getConfig(); + const chips = new Set(['FT61FC6X', cfg.chip]); + const projectChip = projectManager.getCurrentChip(cfg.projectFile); + if (projectChip) { + chips.add(projectChip); + } + + const includeDir = path.join(path.dirname(cfg.compilerPath), '..', 'include'); + try { + for (const file of fs.readdirSync(includeDir)) { + const m = /([A-Z]{2}\d+[A-Z0-9]+)/i.exec(file); + if (m) { + chips.add(m[1].toUpperCase()); + } + } + } catch { + // 没有 include 目录时忽略 + } + + return Array.from(chips).filter(Boolean).sort(); +} + +export function getConfig() { + const cfg = vscode.workspace.getConfiguration('fmdCompiler'); + return { + compilerPath: cfg.get('compilerPath', 'C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe'), + compilerSearchPaths: cfg.get('compilerSearchPaths', [ + 'C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe', + 'C:\\Program Files\\CCompiler\\Compiler\\data\\bin\\c.exe', + ]), + projectFile: cfg.get('projectFile', ''), + chip: cfg.get('chip', 'FT61FC6X'), + outputDir: cfg.get('outputDir', ''), + extraArgs: cfg.get('extraArgs', ''), + autoSaveBeforeBuild: cfg.get('autoSaveBeforeBuild', true), + showOutputOnBuild: cfg.get('showOutputOnBuild', true), + programmerPath: cfg.get('programmerPath', ''), + programmerArgs: cfg.get('programmerArgs', []), + programmerCwd: cfg.get('programmerCwd', '${projectDir}'), + programmerUseShell: cfg.get('programmerUseShell', false), + programmerSuccessExitCodes: cfg.get('programmerSuccessExitCodes', [0]), + downloadFileType: cfg.get<'hex' | 'bin'>('downloadFileType', 'hex'), + autoBuildBeforeDownload: cfg.get('autoBuildBeforeDownload', false), + showOutputOnDownload: cfg.get('showOutputOnDownload', true), + eepromBaseAddress: cfg.get('eepromBaseAddress', '0x2100'), + eepromStart: cfg.get('eepromStart', '0x00'), + eepromSize: cfg.get('eepromSize', 112), + eepromFill: cfg.get('eepromFill', '0xFF'), + eepromImageFile: cfg.get('eepromImageFile', ''), + eepromReadArgs: cfg.get('eepromReadArgs', []), + eepromWriteArgs: cfg.get('eepromWriteArgs', []), + }; +} diff --git a/src/intelHex.ts b/src/intelHex.ts new file mode 100644 index 0000000..f2bd7b5 --- /dev/null +++ b/src/intelHex.ts @@ -0,0 +1,101 @@ +export interface HexRecord { + address: number; + type: number; + data: number[]; +} + +export function parseIntelHex(text: string): HexRecord[] { + const records: HexRecord[] = []; + let upperAddress = 0; + + for (const rawLine of text.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line) { + continue; + } + if (!line.startsWith(':')) { + throw new Error(`无效 Intel HEX 行: ${line}`); + } + + const bytes: number[] = []; + for (let i = 1; i < line.length; i += 2) { + bytes.push(parseInt(line.slice(i, i + 2), 16)); + } + + const count = bytes[0]; + const address = (bytes[1] << 8) | bytes[2]; + const type = bytes[3]; + const data = bytes.slice(4, 4 + count); + const checksum = bytes[4 + count]; + const sum = bytes.slice(0, 4 + count).reduce((a, b) => a + b, 0); + if (((sum + checksum) & 0xff) !== 0) { + throw new Error(`Intel HEX 校验失败: ${line}`); + } + + if (type === 0x00) { + records.push({ address: upperAddress + address, type, data }); + } else if (type === 0x01) { + break; + } else if (type === 0x04) { + upperAddress = (((data[0] << 8) | data[1]) << 16) >>> 0; + records.push({ address, type, data }); + } else { + records.push({ address: upperAddress + address, type, data }); + } + } + + return records; +} + +export function extractRange(text: string, baseAddress: number, size: number, fill: number): Uint8Array { + const data = new Uint8Array(size); + data.fill(fill & 0xff); + + for (const record of parseIntelHex(text)) { + if (record.type !== 0x00) { + continue; + } + + record.data.forEach((value, index) => { + const address = record.address + index; + if (address >= baseAddress && address < baseAddress + size) { + data[address - baseAddress] = value; + } + }); + } + + return data; +} + +export function writeIntelHex(data: Uint8Array, baseAddress: number, bytesPerLine = 16): string { + const lines: string[] = []; + let currentUpper = -1; + + for (let offset = 0; offset < data.length; offset += bytesPerLine) { + const absolute = baseAddress + offset; + const upper = absolute >>> 16; + if (upper !== currentUpper) { + currentUpper = upper; + lines.push(makeRecord(0, 0x04, [(upper >> 8) & 0xff, upper & 0xff])); + } + + const chunk = Array.from(data.slice(offset, offset + bytesPerLine)); + lines.push(makeRecord(absolute & 0xffff, 0x00, chunk)); + } + + lines.push(':00000001FF'); + return lines.join('\r\n') + '\r\n'; +} + +function makeRecord(address: number, type: number, data: number[]): string { + const bytes = [ + data.length & 0xff, + (address >> 8) & 0xff, + address & 0xff, + type & 0xff, + ...data.map(v => v & 0xff), + ]; + const sum = bytes.reduce((a, b) => a + b, 0); + const checksum = ((~sum + 1) & 0xff); + return ':' + [...bytes, checksum].map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(''); +} diff --git a/src/programmer.ts b/src/programmer.ts new file mode 100644 index 0000000..7de5671 --- /dev/null +++ b/src/programmer.ts @@ -0,0 +1,132 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { getConfig } from './extension'; +import { FmdCompiler, FmdOutputArtifacts } from './compiler'; +import { FmdProjectManager } from './projectManager'; +import { expandArgs, expandTokens, parseCommandLine, TokenContext, ToolRunner } from './toolRunner'; + +export class FmdProgrammer { + private runner = new ToolRunner(); + + constructor( + private outputChannel: vscode.OutputChannel, + private projectManager: FmdProjectManager, + private compiler: FmdCompiler + ) {} + + async configureProgrammer(): Promise { + const cfg = getConfig(); + const files = await vscode.window.showOpenDialog({ + canSelectMany: false, + openLabel: '选择烧录/下载工具', + defaultUri: cfg.programmerPath ? vscode.Uri.file(path.dirname(cfg.programmerPath)) : undefined, + filters: { + '可执行文件': ['exe', 'bat', 'cmd'], + '所有文件': ['*'], + }, + }); + + if (!files || files.length === 0) { + return; + } + + const programmerPath = files[0].fsPath; + await vscode.workspace.getConfiguration('fmdCompiler').update('programmerPath', programmerPath, vscode.ConfigurationTarget.Workspace); + + const argText = await vscode.window.showInputBox({ + title: '烧录工具参数', + prompt: '可使用 ${chip}、${hexFile}、${binFile}、${downloadFile} 等变量', + value: cfg.programmerArgs.join(' '), + placeHolder: '--chip ${chip} --file ${hexFile} --program', + }); + + if (argText !== undefined) { + await vscode.workspace.getConfiguration('fmdCompiler').update('programmerArgs', parseCommandLine(argText), vscode.ConfigurationTarget.Workspace); + } + + vscode.window.showInformationMessage('FMD: 烧录工具配置已保存'); + } + + async buildAndDownload(): Promise { + const result = await this.compiler.buildProjectWithResult(); + if (!result.success) { + vscode.window.showErrorMessage('FMD: 编译失败,已取消下载'); + return; + } + + await this.download(result.artifacts); + } + + async download(artifacts?: FmdOutputArtifacts): Promise { + const cfg = getConfig(); + const resolvedArtifacts = artifacts || await this.compiler.resolveOutputArtifacts(); + if (!resolvedArtifacts) { + return; + } + + if (!cfg.programmerPath) { + vscode.window.showWarningMessage('FMD: 未配置烧录工具路径,请先运行 “FMD: Configure Programmer”'); + return; + } + + if (cfg.programmerArgs.length === 0) { + vscode.window.showWarningMessage('FMD: 未配置烧录工具参数,请先运行 “FMD: Configure Programmer”'); + return; + } + + const downloadFile = cfg.downloadFileType === 'bin' ? resolvedArtifacts.binFile : resolvedArtifacts.hexFile; + if (!fs.existsSync(downloadFile)) { + vscode.window.showErrorMessage(`FMD: 未找到下载文件: ${downloadFile},请先编译工程`); + return; + } + + if (cfg.showOutputOnDownload) { + this.outputChannel.show(true); + } + + const context = this.createTokenContext(resolvedArtifacts, downloadFile); + const cwd = expandTokens(cfg.programmerCwd || '${projectDir}', context); + const args = expandArgs(cfg.programmerArgs, context); + + try { + const result = await this.runner.run({ + executable: cfg.programmerPath, + args, + cwd, + shell: cfg.programmerUseShell, + outputChannel: this.outputChannel, + label: 'FMD 下载程序', + successExitCodes: cfg.programmerSuccessExitCodes, + }); + + if (result.success) { + vscode.window.showInformationMessage('FMD: 下载完成 ✓'); + } else { + vscode.window.showErrorMessage(`FMD: 下载失败,退出码 ${result.exitCode}`); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + this.outputChannel.appendLine(`[错误] ${msg}`); + vscode.window.showErrorMessage(`FMD 下载异常: ${msg}`); + } + } + + createTokenContext(artifacts: FmdOutputArtifacts, downloadFile?: string): TokenContext { + const cfg = getConfig(); + const projectFile = this.projectManager.getProjectFile() || cfg.projectFile; + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''; + + return { + chip: cfg.chip, + projectFile, + projectDir: artifacts.projectDir, + projectName: artifacts.projectName, + compilerPath: cfg.compilerPath, + hexFile: artifacts.hexFile, + binFile: artifacts.binFile, + downloadFile: downloadFile || (cfg.downloadFileType === 'bin' ? artifacts.binFile : artifacts.hexFile), + workspaceFolder, + }; + } +} diff --git a/src/projectManager.ts b/src/projectManager.ts new file mode 100644 index 0000000..2a283e3 --- /dev/null +++ b/src/projectManager.ts @@ -0,0 +1,215 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; + +export interface FmdProjectInfo { + projectFile: string; + projectDir: string; + projectName: string; + device?: string; + sourceFiles: string[]; + headerFiles: string[]; + eeFile?: string; + encoding: BufferEncoding; +} + +export class FmdProjectManager { + private projectDir: string | undefined; + private projectFile: string | undefined; + + /** + * 自动在工作区中搜索 .prj 文件 + */ + autoDetectProject(): void { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + return; + } + + for (const folder of workspaceFolders) { + const prjFiles = this.findPrjFiles(folder.uri.fsPath); + if (prjFiles.length === 1) { + this.projectFile = prjFiles[0]; + this.projectDir = path.dirname(prjFiles[0]); + console.log(`[FMD] 自动检测到工程: ${this.projectFile}`); + return; + } else if (prjFiles.length > 1) { + // 多个工程文件,不自动选择 + console.log(`[FMD] 发现多个工程文件,请手动选择`); + return; + } + } + } + + /** + * 在目录下搜索 .prj 文件(最多2层深度) + */ + private findPrjFiles(dir: string, depth = 0): string[] { + if (depth > 2) { + return []; + } + + const result: string[] = []; + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isFile() && entry.name.toLowerCase().endsWith('.prj')) { + result.push(path.join(dir, entry.name)); + } else if (entry.isDirectory() && !entry.name.startsWith('.')) { + result.push(...this.findPrjFiles(path.join(dir, entry.name), depth + 1)); + } + } + } catch { + // 忽略权限错误 + } + return result; + } + + setProjectFile(filePath: string): void { + this.projectFile = filePath; + this.projectDir = path.dirname(filePath); + } + + getProjectDir(): string | undefined { + // 优先用已设置的工程目录 + if (this.projectDir) { + return this.projectDir; + } + + // 其次用当前活动文件所在目录 + const editor = vscode.window.activeTextEditor; + if (editor && /\.[cChH]$/.test(editor.document.fileName)) { + return path.dirname(editor.document.fileName); + } + + return undefined; + } + + getProjectFile(): string | undefined { + if (this.projectFile && fs.existsSync(this.projectFile)) { + return this.projectFile; + } + + const dir = this.getProjectDir(); + if (!dir) { + return undefined; + } + + const files = this.findPrjFiles(dir, 2); + return files.length === 1 ? files[0] : undefined; + } + + getProjectInfo(projectFile?: string): FmdProjectInfo | undefined { + const file = projectFile || this.getProjectFile(); + if (!file || !fs.existsSync(file)) { + return undefined; + } + + return this.readProjectInfo(file); + } + + readProjectInfo(projectFile: string): FmdProjectInfo { + const { text, encoding } = this.readProjectText(projectFile); + const projectDir = path.dirname(projectFile); + const rawName = this.matchValue(text, /^\s*Projece\s+Name\s*=\s*(.+)$/im); + const projectName = path.basename(rawName || projectFile, path.extname(rawName || projectFile)); + const device = this.matchValue(text, /^\s*Device\s*=\s*(.+)$/im); + const sourceLine = this.matchValue(text, /^\s*Source\s+File\s*=\s*(.*)$/im) || ''; + const eeLine = this.matchValue(text, /^\s*EE\s+File\s*=\s*(.*)$/im) || ''; + const headerFiles: string[] = []; + const headerPattern = /^\s*Head\s+File\s+\d+\s*=\s*(.+)$/gim; + let m: RegExpExecArray | null; + + while ((m = headerPattern.exec(text)) !== null) { + const value = m[1].trim(); + if (value) { + headerFiles.push(this.resolveProjectPath(projectDir, value)); + } + } + + const sourceFiles = sourceLine + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0) + .map(s => this.resolveProjectPath(projectDir, s)); + + return { + projectFile, + projectDir, + projectName, + device, + sourceFiles, + headerFiles, + eeFile: eeLine.trim() ? this.resolveProjectPath(projectDir, eeLine.trim()) : undefined, + encoding, + }; + } + + async updateProjectDevice(projectFile: string, chip: string): Promise { + const { text, encoding } = this.readProjectText(projectFile); + const pattern = /^(\s*Device\s*=\s*).+$/im; + const nextText = pattern.test(text) + ? text.replace(pattern, `$1${chip}`) + : `${text.replace(/\s*$/, '')}\r\nDevice = ${chip}\r\n`; + + fs.writeFileSync(projectFile, Buffer.from(nextText, encoding)); + } + + getCurrentChip(projectFile?: string): string | undefined { + return this.getProjectInfo(projectFile)?.device; + } + + /** + * 弹出文件选择框让用户选择 .prj 文件 + */ + async pickProjectFile(): Promise { + const files = await vscode.window.showOpenDialog({ + canSelectMany: false, + openLabel: '选择 FMD 工程文件', + filters: { + 'FMD Project': ['prj'], + '所有文件': ['*'], + }, + }); + + if (files && files.length > 0) { + this.setProjectFile(files[0].fsPath); + vscode.window.showInformationMessage(`已选择工程: ${path.basename(files[0].fsPath)}`); + } + } + + private readProjectText(filePath: string): { text: string; encoding: BufferEncoding } { + const buf = fs.readFileSync(filePath); + const encoding = this.detectEncoding(buf); + let text = buf.toString(encoding); + if (text.charCodeAt(0) === 0xfeff) { + text = text.slice(1); + } + return { text, encoding }; + } + + private detectEncoding(buf: Buffer): BufferEncoding { + if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) { + return 'utf16le'; + } + + const sampleLength = Math.min(buf.length, 200); + let nulCount = 0; + for (let i = 0; i < sampleLength; i++) { + if (buf[i] === 0) { + nulCount++; + } + } + + return nulCount > sampleLength / 4 ? 'utf16le' : 'utf8'; + } + + private matchValue(text: string, pattern: RegExp): string | undefined { + const m = pattern.exec(text); + return m?.[1]?.trim(); + } + + private resolveProjectPath(projectDir: string, value: string): string { + return path.isAbsolute(value) ? value : path.join(projectDir, value); + } +} diff --git a/src/toolRunner.ts b/src/toolRunner.ts new file mode 100644 index 0000000..dbf0de3 --- /dev/null +++ b/src/toolRunner.ts @@ -0,0 +1,140 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import { spawn } from 'child_process'; + +export interface ToolRunOptions { + executable: string; + args: string[]; + cwd: string; + shell?: boolean; + outputChannel: vscode.OutputChannel; + label: string; + successExitCodes?: number[]; +} + +export interface ToolRunResult { + exitCode: number; + stdout: string; + stderr: string; + success: boolean; +} + +export type TokenContext = Record; + +export function expandTokens(input: string, context: TokenContext): string { + return input.replace(/\$\{([A-Za-z0-9_]+)\}/g, (match, key) => context[key] ?? match); +} + +export function expandArgs(args: string[], context: TokenContext): string[] { + return args.map(arg => expandTokens(arg, context)); +} + +export function parseCommandLine(input: string): string[] { + const args: string[] = []; + let current = ''; + let quote: string | undefined; + let escaped = false; + + for (const ch of input) { + if (escaped) { + current += ch; + escaped = false; + continue; + } + + if (ch === '\\') { + escaped = true; + continue; + } + + if (quote) { + if (ch === quote) { + quote = undefined; + } else { + current += ch; + } + continue; + } + + if (ch === '"' || ch === "'") { + quote = ch; + continue; + } + + if (/\s/.test(ch)) { + if (current.length > 0) { + args.push(current); + current = ''; + } + continue; + } + + current += ch; + } + + if (escaped) { + current += '\\'; + } + if (current.length > 0) { + args.push(current); + } + + return args; +} + +export class ToolRunner { + async run(options: ToolRunOptions): Promise { + const successExitCodes = options.successExitCodes || [0]; + + if (!options.shell && !fs.existsSync(options.executable)) { + throw new Error(`工具不存在: ${options.executable}`); + } + + options.outputChannel.appendLine(''); + options.outputChannel.appendLine(`========== ${options.label} ==========`); + options.outputChannel.appendLine(`工作目录: ${options.cwd}`); + options.outputChannel.appendLine(`命令: ${options.executable} ${options.args.join(' ')}`); + options.outputChannel.appendLine(''); + + return new Promise((resolve, reject) => { + const proc = spawn(options.executable, options.args, { + cwd: options.cwd, + shell: options.shell || false, + env: { ...process.env }, + }); + + let stdout = ''; + let stderr = ''; + + proc.stdout.on('data', (data: Buffer) => { + const text = data.toString(); + stdout += text; + this.writeLines(options.outputChannel, text); + }); + + proc.stderr.on('data', (data: Buffer) => { + const text = data.toString(); + stderr += text; + this.writeLines(options.outputChannel, text); + }); + + proc.on('close', (code: number | null) => { + const exitCode = code ?? 1; + const success = successExitCodes.includes(exitCode); + options.outputChannel.appendLine(''); + options.outputChannel.appendLine(`========== ${options.label}${success ? '成功' : '失败'} (退出码: ${exitCode}) ==========`); + resolve({ exitCode, stdout, stderr, success }); + }); + + proc.on('error', (err: Error) => reject(err)); + }); + } + + private writeLines(outputChannel: vscode.OutputChannel, text: string): void { + text.split('\n').forEach(line => { + if (line.trim()) { + outputChannel.appendLine(line.replace(/\r$/, '')); + } + }); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6348651 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "./out", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "exclude": ["node_modules", ".vscode-test"] +}