创建工程
This commit is contained in:
+36
@@ -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/
|
||||
Vendored
+17
@@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+27
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+58
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+353
@@ -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"
|
||||
}
|
||||
}
|
||||
+406
@@ -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<void> {
|
||||
const result = await this.buildProjectWithResult();
|
||||
if (!result.success && result.exitCode !== -1) {
|
||||
vscode.window.showErrorMessage(`FMD: 编译失败,退出码 ${result.exitCode},请查看输出面板`);
|
||||
}
|
||||
}
|
||||
|
||||
async buildProjectWithResult(): Promise<FmdBuildResult> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<FmdOutputArtifacts | undefined> {
|
||||
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<typeof getConfig>
|
||||
): 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<number> {
|
||||
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<string | undefined> {
|
||||
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<FmdProjectInfo | undefined> {
|
||||
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} 字节)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, vscode.Diagnostic[]>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string, string>): Promise<void> {
|
||||
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<number, string[]>; 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<number, string[]> {
|
||||
const labels = new Map<number, string[]>();
|
||||
const files = new Set<string>(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<number, string[]>, 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 `<tr>
|
||||
<td>0x${logical.toString(16).toUpperCase().padStart(2, '0')}</td>
|
||||
<td>0x${absolute.toString(16).toUpperCase().padStart(4, '0')}</td>
|
||||
<td><input data-index="${index}" value="${value.toString(16).toUpperCase().padStart(2, '0')}" maxlength="2" /></td>
|
||||
<td>${escapeHtml(ascii)}</td>
|
||||
<td>${escapeHtml(label)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); padding: 12px; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid var(--vscode-panel-border); padding: 4px 8px; text-align: left; }
|
||||
input { width: 3em; font-family: monospace; }
|
||||
.actions { margin: 12px 0; display: flex; gap: 8px; }
|
||||
.meta { color: var(--vscode-descriptionForeground); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>FMD EEPROM 编辑器</h2>
|
||||
<div class="meta">文件: ${escapeHtml(eepromFile)}</div>
|
||||
<div class="meta">Base: 0x${geometry.baseAddress.toString(16).toUpperCase()},Size: ${geometry.size} bytes</div>
|
||||
<div class="actions">
|
||||
<button id="save">保存</button>
|
||||
<button id="export">导出 HEX</button>
|
||||
<button id="reload">重新加载</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>逻辑地址</th><th>HEX 地址</th><th>值</th><th>ASCII</th><th>标签</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
document.getElementById('save').addEventListener('click', () => {
|
||||
const values = {};
|
||||
document.querySelectorAll('input[data-index]').forEach(input => values[input.dataset.index] = input.value);
|
||||
vscode.postMessage({ command: 'save', values });
|
||||
});
|
||||
document.getElementById('export').addEventListener('click', () => vscode.postMessage({ command: 'export' }));
|
||||
document.getElementById('reload').addEventListener('click', () => vscode.postMessage({ command: 'reload' }));
|
||||
document.querySelectorAll('input[data-index]').forEach(input => {
|
||||
input.addEventListener('input', () => input.value = input.value.replace(/[^0-9a-fA-F]/g, '').toUpperCase());
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
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, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string>(['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<string>('compilerPath', 'C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe'),
|
||||
compilerSearchPaths: cfg.get<string[]>('compilerSearchPaths', [
|
||||
'C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe',
|
||||
'C:\\Program Files\\CCompiler\\Compiler\\data\\bin\\c.exe',
|
||||
]),
|
||||
projectFile: cfg.get<string>('projectFile', ''),
|
||||
chip: cfg.get<string>('chip', 'FT61FC6X'),
|
||||
outputDir: cfg.get<string>('outputDir', ''),
|
||||
extraArgs: cfg.get<string>('extraArgs', ''),
|
||||
autoSaveBeforeBuild: cfg.get<boolean>('autoSaveBeforeBuild', true),
|
||||
showOutputOnBuild: cfg.get<boolean>('showOutputOnBuild', true),
|
||||
programmerPath: cfg.get<string>('programmerPath', ''),
|
||||
programmerArgs: cfg.get<string[]>('programmerArgs', []),
|
||||
programmerCwd: cfg.get<string>('programmerCwd', '${projectDir}'),
|
||||
programmerUseShell: cfg.get<boolean>('programmerUseShell', false),
|
||||
programmerSuccessExitCodes: cfg.get<number[]>('programmerSuccessExitCodes', [0]),
|
||||
downloadFileType: cfg.get<'hex' | 'bin'>('downloadFileType', 'hex'),
|
||||
autoBuildBeforeDownload: cfg.get<boolean>('autoBuildBeforeDownload', false),
|
||||
showOutputOnDownload: cfg.get<boolean>('showOutputOnDownload', true),
|
||||
eepromBaseAddress: cfg.get<string>('eepromBaseAddress', '0x2100'),
|
||||
eepromStart: cfg.get<string>('eepromStart', '0x00'),
|
||||
eepromSize: cfg.get<number>('eepromSize', 112),
|
||||
eepromFill: cfg.get<string>('eepromFill', '0xFF'),
|
||||
eepromImageFile: cfg.get<string>('eepromImageFile', ''),
|
||||
eepromReadArgs: cfg.get<string[]>('eepromReadArgs', []),
|
||||
eepromWriteArgs: cfg.get<string[]>('eepromWriteArgs', []),
|
||||
};
|
||||
}
|
||||
+101
@@ -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('');
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
const result = await this.compiler.buildProjectWithResult();
|
||||
if (!result.success) {
|
||||
vscode.window.showErrorMessage('FMD: 编译失败,已取消下载');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.download(result.artifacts);
|
||||
}
|
||||
|
||||
async download(artifacts?: FmdOutputArtifacts): Promise<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<string, string | undefined>;
|
||||
|
||||
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<ToolRunResult> {
|
||||
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$/, ''));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user