RISC-V GCC GUI

使用说明

开源

安装

  • 选择安装位置,默认安装于C:\Program Files\RiscvCompiler

image-20250808110732132

  • 选择快捷方式存放位置

image-20250808110911551

  • 安装预计用时1分钟

image-20250808111219456

使用

image-20250808111335704

  • 指定项目目录:C语言主程序、启动文件、链接脚本
  • 指定主程序文件名(不带文件类型后缀),指定链接脚本
  • GCC编译文件自动存入指定项目目录
image-20250808110227069

开发准备

安装

原始脚本

工具链变量

1
2
3
4
5
# TOOLCHAIN
$TOOLCHAIN_DIR = "C:\riscv\xpack-riscv-none-elf-gcc-14.2.0-3\bin"
$RISCV_GCC = "$TOOLCHAIN_DIR\riscv-none-elf-gcc.exe"
$OBJCOPY = "$TOOLCHAIN_DIR\riscv-none-elf-objcopy.exe"
$OBJDUMP = "$TOOLCHAIN_DIR\riscv-none-elf-objdump.exe"
  • gcc:RISC-V架构的交叉编译器前端
    • 将C/C++源代码编译为RISC-V目标平台的机器码
    • 管理整个编译流程(预处理→编译→汇编→链接)
    • 支持RISC-V RV32/RV64指令集及扩展(如M/C/F/D等)
  • objcopy:目标文件格式转换工具
    • 提取/转换ELF文件中的特定段(如从.elf提取.bin固件)
    • 生成可烧录的二进制镜像(如用于Flash编程)
    • 修改文件头信息或段属性
  • objdump:目标文件分析诊断工具
    • 反汇编机器码为RISC-V汇编指令
    • 显示ELF文件结构(段头、符号表等)
    • 生成带地址的汇编列表文件

文件变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# PROJECT
$PROJECT_DIR = "your_project_path"
$MAIN_CODE = "main_file_name_without_extension"

# SOURCE
$LINKER_SCRIPT = "$PROJECT_DIR\sections.lds"
$SOURCE_FILES = @(
"$PROJECT_DIR\start.S",
"$PROJECT_DIR\$MAIN_CODE.c"
)

# OUTPUT
$MAP_FILE = "$PROJECT_DIR\sys.map"
$ELF_OUTPUT = "$PROJECT_DIR\$MAIN_CODE.elf"

编译选项

1
2
3
4
5
6
7
8
9
10
11
12
# OPTIONS
$CFLAGS = @(
"-O3", # 启用最高级别优化
"-nostdlib", # 禁用标准库链接
"-flto", # 链接时优化
"-ffreestanding", # 独立环境编译
"-ffunction-sections", # 函数分段,配合链接器GC移除未使用函数
"-fdata-sections", # 数据分段,支持链接时移除未用数据
"-fno-builtin", # 禁用内置函数
"--specs=nano.specs", # 轻量库配置
"--specs=nosys.specs" # 无系统调用配置
)
  • 性能强化:编译、链接双阶段优化
    • -O3+-flto
  • 裸机环境适配:确保编译结果不依赖操作系统
    • -nostdlib+-ffreestanding+nosys.specs
  • 代码尺寸优化:移除未使用代码、数据
    • -ffunction-sections+-fdata-sections
  • 精简库:减小二进制体积
    • nano.specs

链接选项

1
2
3
4
5
6
7
$LDFLAGS = @(
"-Wl,--build-id=none", # 禁用构建ID,减少二进制体积
"-Wl,-Bstatic", # 强制静态链接
"-Wl,-T,$LINKER_SCRIPT", # 指定链接脚本,自定以内存布局
"-Wl,-Map=$MAP_FILE", # 生成链接映射,用于调试与内存分析
"-L." # 添加库搜索路径
)
  • 部署控制:强制静态链接(-Bstatic)和自定义内存布局(-T脚本)确保程序在目标硬件精确运行

编译命令

1
2
3
4
5
# COMPILE
& $RISCV_GCC -march=rv32e -mabi=ilp32e $CFLAGS $LDFLAGS -o $ELF_OUTPUT $SOURCE_FILES
& $OBJDUMP -S $ELF_OUTPUT > ($ELF_OUTPUT -replace '\.elf$', '.s')
& $OBJCOPY -O binary $ELF_OUTPUT ($ELF_OUTPUT -replace '\.elf$', '.bin')
& $OBJCOPY -O verilog --verilog-data-width 4 $ELF_OUTPUT ($ELF_OUTPUT -replace '\.elf$', '.hex')

开发记录

  • Eclipse新建Java Project,不勾选Create module-info.java
  • 先实现调用本地GCC的GUI原型,再封装GCC及GUI

原型

  • 部分原型GUI代码展示,调用本地RISC-V GCC实现C程序编译

界面

  • 组件声明
1
2
3
4
5
6
private final JTextField toolchainDirField = new JTextField(30); // 工具链路径输入框
private final JTextField projectDirField = new JTextField(30); // 项目路径输入框
private final JTextField mainCodeField = new JTextField("主文件名(无后缀)", 15); // 主代码文件名
private final JTextField linkerScriptField = new JTextField(30); // 链接脚本路径输入框
private final JTextArea logArea = new JTextArea(15, 60); // 日志显示区域
private final JButton compileButton = new JButton("开始编译"); // 核心功能按钮
  • 窗口布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public RiscvCompilerGUI() {
super("RISC-V编译工具"); // 窗口标题
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout(10, 10)); // 边界布局:上(输入区)、中(日志区)

// 添加组件到窗口
add(createInputPanel(), BorderLayout.NORTH); // 顶部:参数输入面板
add(new JScrollPane(logArea), BorderLayout.CENTER); // 中间:日志区域(带滚动条)

// 日志区域配置(只读+等宽字体)
logArea.setEditable(false);
logArea.setFont(new Font("Consolas", Font.PLAIN, 12));

// 窗口属性设置
pack(); // 自动调整大小
setLocationRelativeTo(null); // 居中显示
}
  • 输入区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private JPanel createInputPanel() {
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5); // 组件间距
gbc.anchor = GridBagConstraints.WEST; // 左对齐

// 输入行1:工具链目录(标签+输入框+浏览按钮)
addInputRow(panel, gbc, 0, "工具链目录:", toolchainDirField, e -> chooseDirectory(toolchainDirField));
// 输入行2:项目目录
addInputRow(panel, gbc, 1, "项目目录:", projectDirField, e -> chooseDirectory(projectDirField));
// 输入行3:主代码文件名
addInputRow(panel, gbc, 2, "主代码文件名:", mainCodeField, null);
// 输入行4:链接脚本(文件过滤.lds)
addInputRow(panel, gbc, 3, "链接脚本:", linkerScriptField, e -> chooseFile(linkerScriptField, "lds"));
// 输入行5:编译按钮(跨列居中)
gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 3; gbc.anchor = GridBagConstraints.CENTER;
panel.add(compileButton, gbc);

return panel;
}

交互

  • 目录选择
1
2
3
4
5
6
7
8
private void chooseDirectory(JTextField field) {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); // 仅允许选择目录
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
// 将选中的目录路径显示到输入框
field.setText(chooser.getSelectedFile().getAbsolutePath());
}
}
  • 文件选择
1
2
3
4
5
6
7
8
9
private void chooseFile(JTextField field, String ext) {
JFileChooser chooser = new JFileChooser();
// 过滤文件类型(仅显示.lds文件)
chooser.setFileFilter(new FileNameExtensionFilter(ext.toUpperCase() + "文件", ext));
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
// 将选中的文件路径显示到输入框
field.setText(chooser.getSelectedFile().getAbsolutePath());
}
}
  • 编译按钮
1
2
3
4
5
6
private void initButtonAction() {
compileButton.addActionListener(e -> {
logArea.setText(""); // 清空旧日志
new CompileWorker().execute(); // 启动后台任务(避免界面卡死)
});
}

编译

  • 识别路径
1
2
3
4
5
6
7
8
9
10
11
12
13
private String getBuiltInToolchainDir() {
// 获取程序当前运行目录
String appDir = System.getProperty("user.dir");
String toolchainBinDir = appDir + File.separator + "toolchain" + File.separator + "bin";

// 检查工具链核心文件是否存在(避免打包时遗漏工具链)
File gccFile = new File(toolchainBinDir + File.separator + "riscv-none-elf-gcc.exe");
if (!gccFile.exists()) {
publish("错误:内置工具链缺失!请确保程序目录下存在 toolchain/bin/riscv-none-elf-gcc.exe");
return null;
}
return toolchainBinDir; // 返回工具链bin目录路径(含riscv-none-elf-gcc.exe等工具)
}
  • 编译任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 0. 自动获取内置工具链路径(无需用户输入)
String toolchainDir = getBuiltInToolchainDir();
if (toolchainDir == null) return null; // 工具链缺失,退出

// 1. 获取用户输入的参数(路径、文件名)
String projectDir = projectDirField.getText().trim();
String mainCode = mainCodeField.getText().trim();
String linkerScript = linkerScriptField.getText().trim();

// 2. 检查参数是否完整
if (projectDir.isEmpty() || mainCode.isEmpty() || linkerScript.isEmpty()) {
publish("错误:请填写所有路径和参数!");
return null;
}

// 3. 拼接工具路径(RISC-V GCC、OBJCOPY等)
String riscvGcc = toolchainDir + "\riscv-none-elf-gcc.exe";
String objdump = toolchainDir + "\riscv-none-elf-objdump.exe";
String objcopy = toolchainDir + "\riscv-none-elf-objcopy.exe";

// 4. 检查工具是否存在(避免路径错误)
if (!new File(riscvGcc).exists()) {
publish("错误:未找到riscv-none-elf-gcc.exe,请检查工具链路径!");
return null;
}

// 5. 拼接输出文件路径(和原脚本一致)
String elfOutput = projectDir + "\" + mainCode + ".elf";
String mapFile = projectDir + "\" + mainCode + ".map";
String asmOutput = elfOutput.replace(".elf", ".s");
String binOutput = elfOutput.replace(".elf", ".bin");
String hexOutput = elfOutput.replace(".elf", ".hex");
  • 文件生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 步骤1:编译生成ELF文件(核心命令)
String[] gccCommand = {
riscvGcc,
"-march=rv32e", "-mabi=ilp32e", // RISC-V架构参数(rv32e/ilp32e)
"-O3", "-nostdlib", "-flto", "-ffreestanding", // CFLAGS优化选项
"-ffunction-sections", "-fdata-sections", "-fno-builtin",
"--specs=nano.specs", "--specs=nosys.specs",
"-Wl,--build-id=none", "-Wl,-Bstatic", // LDFLAGS链接选项
"-Wl,-T," + linkerScript, // 链接脚本(用户选择的.lds)
"-Wl,-Map=" + mapFile, // 生成map文件
"-L.",
"-o", elfOutput, // 输出ELF文件
projectDir + "\start.S", // 源文件1:start.S(固定)
projectDir + "\" + mainCode + ".c" // 源文件2:主代码.c
};
publish("执行编译命令:" + String.join(" ", gccCommand));
if (!runCommand(gccCommand)) return null; // 执行命令,失败则退出

// 步骤2:生成反汇编.s文件(objdump -S)
String[] objdumpCommand = {objdump, "-S", elfOutput};
publish("\n执行反汇编:" + String.join(" ", objdumpCommand));
if (!runCommandToFile(objdumpCommand, asmOutput)) return null;

// 步骤3:生成bin文件(objcopy -O binary)
String[] binCommand = {objcopy, "-O", "binary", elfOutput, binOutput};
publish("\n生成bin文件:" + String.join(" ", binCommand));
if (!runCommand(binCommand)) return null;

// 步骤4:生成hex文件(objcopy -O verilog)
String[] hexCommand = {objcopy, "-O", "verilog", "--verilog-data-width", "4", elfOutput, hexOutput};
publish("\n生成hex文件:" + String.join(" ", hexCommand));
if (!runCommand(hexCommand)) return null;

试运行

image-20250807134828543

打包

  • 将RISCV GCC和Java环境封装进安装包,达到开箱即用的目的

GCC打包

  • 将编译工具链放入项目根目录并改名toolchain
    • 目录结构为riscv_compiler/toolchain/bin/riscv-none-elf-gcc.exe
  • 将原型中工具链路径由用户输入改为自动获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 删除原工具链路径输入框相关代码

// 修改编译逻辑,使用自动获取的工具链路径
private class CompileWorker extends SwingWorker<Void, String> {
// 自动识别内置工具链路径
private String getBuiltInToolchainDir() {
// 获取程序当前运行目录(打包后为EXE解压的临时目录,工具链固定放在"toolchain/bin"下)
String appDir = System.getProperty("user.dir");
String toolchainBinDir = appDir + File.separator + "toolchain" + File.separator + "bin";

// 检查工具链核心文件是否存在(避免打包时遗漏工具链)
File gccFile = new File(toolchainBinDir + File.separator + "riscv-none-elf-gcc.exe");
if (!gccFile.exists()) {
// 此时 publish 方法属于 CompileWorker(SwingWorker子类),可正常调用
publish("错误:内置工具链缺失!请确保程序目录下存在 toolchain/bin/riscv-none-elf-gcc.exe");
return null;
}
return toolchainBinDir; // 返回工具链bin目录路径(含riscv-none-elf-gcc.exe等工具)
}

@Override
protected Void doInBackground() throws Exception {
// 0. 自动获取内置工具链路径(无需用户输入)
String toolchainDir = getBuiltInToolchainDir();
if (toolchainDir == null) return null; // 工具链缺失,退出

// 1. 获取用户输入的参数(路径、文件名)
String projectDir = projectDirField.getText().trim();
String mainCode = mainCodeField.getText().trim();
String linkerScript = linkerScriptField.getText().trim();

...

JAR打包

  • 打包为Runnable JAR file类型
    • 选择主类入口RiscvCompilerGUI - risv_compiler
    • 打包依赖项Package required libraries into generated JAR
    • 目录结构为riscv_compiler/RiscvCompiler.jar

image-20250808102045734

  • 使用JDK自带的jlink在项目目录中生成精简JRE,使封装后的GUI能在无JDK的环境中运行
    • JDK添加到环境变量后,jlink --module-path jmods --add-modules java.base,java.desktop --output jre
    • 目录结构为riscv_compiler/jre/bin/java.exe
    • CMD验证JREjre\bin\java.exe -jar RiscvCompiler.jar

image-20250808103451834

EXE打包

  • 在项目目录中新建setup.iss文件,使用Inno Setup打开并编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[Setup]
AppName=RISC-V Compiler Suite
AppVersion=1.0
DefaultDirName={pf}\RiscvCompiler
OutputDir=.\Output
OutputBaseFilename=RiscvCompiler_Setup
Compression=lzma2
SolidCompression=yes
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64

[Files]
Source: ".\RiscvCompiler.jar"; DestDir: "{app}"; Flags: ignoreversion
Source: ".\toolchain\*"; DestDir: "{app}\toolchain"; Flags: ignoreversion recursesubdirs
Source: ".\jre\*"; DestDir: "{app}\jre"; Flags: ignoreversion recursesubdirs

[Icons]
Name: "{commondesktop}\RISC-V Compiler"; Filename: "{app}\RiscvCompiler.bat"; IconFilename: "{app}\compiler_icon.ico"
Name: "{commonprograms}\RISC-V Compiler"; Filename: "{app}\RiscvCompiler.bat"; IconFilename: "{app}\compiler_icon.ico"

[Run]
Filename: "{app}\RiscvCompiler.bat"; Description: "启动编译器"; Flags: postinstall nowait skipifsilent

[Code]
// 创建强制使用内置JRE的启动脚本
procedure CreateBatchFile;
var
BatchFile: string;
begin
BatchFile := ExpandConstant('{app}\RiscvCompiler.bat');
SaveStringToFile(BatchFile,
'@echo off' + #13#10 +
'set APPDIR=%~dp0' + #13#10 +
'start "" "%APPDIR%jre\bin\javaw.exe" -jar "%APPDIR%RiscvCompiler.jar"' + #13#10,
False
);
end;

procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
CreateBatchFile;
end;