numworks移植记录:4.使用 CLion + ESP-IDF 编译,添加模块并集成编译出 bin 文件

9 阅读6分钟

使用 CLion + ESP-IDF 编译,添加模块并集成编译出 bin 文件

在之前的文章中,我们已经搭建好了开发环境,下载了 NumWorks 源码,并分析了 ESP32-S3 的资源情况。现在,终于到了最关键的一步:让代码真正编译起来,生成可以在硬件上运行的固件

本篇将详细介绍如何在 CLion 中配置一个 ESP-IDF 项目,并将 NumWorks 的庞大代码库组织成模块化的 ESP-IDF 组件,最终成功编译出完整的 bin 文件。这是移植工作中承上启下的核心环节。

1. CLion 中创建 ESP-IDF 项目的正确姿势

虽然你可以手动编写 CMakeLists.txt,但更推荐使用 CLion 的 ESP-IDF 项目向导,它能自动完成大部分基础配置 。

步骤 1:新建项目

  • 打开 CLion,选择 File -> New Project
  • 在左侧模板列表中,选择 Espressif IDF
  • 设置项目名称(例如 epsilon-esp32s3)和存储路径。
  • SDK 配置中,选择你之前安装的 ESP-IDF 路径。
  • Target Chip 选择 esp32s3
  • 点击 Create

步骤 2:首次加载与工具链配置

  • CLion 会自动加载 CMake 项目。如果提示找不到工具链,按照第一篇文章的方法配置好 ESP-IDF 工具链。
  • Settings -> Build, Execution, Deployment -> CMake 中,确认 Toolchain 已选择为 ESP-IDF,并可以在 CMake options 中添加 -DIDF_TARGET=esp32s3 显式指定目标芯片 。
  • 等待 CMake 配置完成。如果出现错误,通常是工具链路径问题,检查环境变量加载是否正确。

2. 将 NumWorks 代码组织成 ESP-IDF 组件

ESP-IDF 的核心思想是 组件化(Components)。所有功能模块都以组件形式存在,每个组件包含自己的源码、头文件和 CMakeLists.txt 。这正是 NumWorks 代码结构的天然映射——我们可以将 ion, kandinsky, poincare 等核心模块分别封装成独立的组件。

建议的组件划分方案:

在你的项目根目录下创建 components 文件夹,内部按模块划分:

text

epsilon-esp32s3/
├── CMakeLists.txt          # 顶层 CMake
├── main/                    # 主程序入口
│   ├── CMakeLists.txt
│   └── epsilon_main.cpp     # 新写的入口函数
└── components/              # 存放所有 NumWorks 模块
    ├── ion/                  # Ion 硬件抽象层
    │   ├── CMakeLists.txt
    │   ├── include/
    │   └── src/              # 存放 ion 源码,并添加 esp32s3 适配文件
    ├── kandinsky/             # 图形库
    │   ├── CMakeLists.txt
    │   ├── include/
    │   └── src/
    ├── poincare/              # 数学引擎
    │   ├── CMakeLists.txt
    │   ├── include/
    │   └── src/
    └── ...                    # 其他模块

如何为每个模块编写 CMakeLists.txt

components/ion/CMakeLists.txt 为例,它需要告诉构建系统:这个组件包含哪些源文件,需要包含哪些头文件目录,以及依赖哪些其他组件 。

cmake

# components/ion/CMakeLists.txt
idf_component_register(
    SRCS
        "src/display.cpp"          # 原有的 ion 源文件
        "src/keyboard.cpp"
        "src/timing.cpp"
        "src/esp32s3/display_esp32s3.cpp"   # 针对 esp32s3 的新增适配文件
        "src/esp32s3/keyboard_esp32s3.cpp"
    INCLUDE_DIRS
        "include"                   # 本组件的公共头文件
        "include/ion"               
    PRIV_REQUIRES
        "driver"                    # 依赖 ESP-IDF 的驱动组件(如 SPI, GPIO)
        "esp_timer"                  # 依赖 ESP-IDF 的定时器组件
)

关键点解析

  • SRCS:列出该组件需要编译的所有源文件。你可以将原有的 NumWorks 源码放在这里,同时新增 esp32s3/ 子目录存放适配 ESP32-S3 的实现 。
  • INCLUDE_DIRS:指定公共头文件路径,其他组件可以通过 #include <ion/display.h> 来引用。
  • PRIV_REQUIRESREQUIRES:声明该组件的依赖项。例如 ion 组件需要调用 ESP-IDF 的 driver 组件来控制 GPIO 和 SPI 屏幕,就需要在这里声明 。构建系统会自动处理依赖顺序。

3. 编写主程序入口

原来的 NumWorks 程序有一个 main() 函数,但 ESP-IDF 的入口是 app_main()。我们需要在 main/ 组件中创建一个适配层,调用 NumWorks 的初始化函数。

main/CMakeLists.txt 示例:

cmake

# main/CMakeLists.txt
idf_component_register(
    SRCS
        "epsilon_main.cpp"
    INCLUDE_DIRS
        "."
    REQUIRES
        "ion"          # 依赖我们的自定义组件
        "kandinsky"
        "poincare"
)

main/epsilon_main.cpp 示例:

c

#include <stdio.h>
#include "ion/display.h"
#include "ion/events.h"
#include "ion/timing.h"
#include "poincare/init.h"

extern "C" void app_main() {
    printf("NumWorks Epsilon starting on ESP32-S3...\n");

    // 初始化 Poincaré 数学引擎
    Poincare::Init();

    // 初始化 Ion 硬件抽象层(屏幕、键盘等)
    Ion::Display::init();
    Ion::Events::init();

    // NumWorks 的主事件循环
    while (true) {
        Ion::Events::Event event = Ion::Events::getEvent();
        // 处理事件、更新屏幕等...
        Ion::Display::waitForVBlank(); // 简单的帧同步
    }
}

4. 配置顶层 CMakeLists.txt

项目的顶层 CMakeLists.txt 需要引入 ESP-IDF 的构建系统,并指定项目名称 。

cmake

cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(epsilon_esp32s3)

就是这么简单!project.cmake 会自动处理组件的搜索、依赖分析和构建。

5. 编译与生成 bin 文件

当所有 CMakeLists.txt 配置完毕后,剩下的工作就交给 CLion 和 ESP-IDF 了。

方法一:在 CLion 中直接编译

  • 在 CLion 右上角的运行配置中选择 app(或 epsilon_esp32s3.elf)。
  • 点击绿色的锤子图标(Build)。
  • CLion 会调用底层的 idf.py build 命令,编译过程会实时显示在 Messages 窗口中。
  • 编译成功后,你会在 build/ 目录下看到生成的 epsilon_esp32s3.bin 文件。

方法二:使用终端命令行

  • 打开 CLion 内置终端(Alt+F12)。
  • 直接运行 idf.py build
  • 同样,生成的 bin 文件位于 build/ 目录下 。

编译输出解读: 当编译完成时,终端会显示类似这样的信息:

text

Project build complete. To flash, run:
 idf.py flash
or
 idf.py -p PORT flash

同时会列出需要烧录的所有 bin 文件及其烧录地址 。对于 ESP32-S3,典型的烧录文件包括:

  • bootloader/bootloader.bin -> 0x1000
  • partition_table/partition-table.bin -> 0x8000
  • ota_data_initial.bin -> 0xd000(如果使用 OTA)
  • epsilon_esp32s3.bin -> 0x10000

6. 合并多个 bin 文件(可选)

如果你需要将多个 bin 文件合并成一个完整的固件文件(例如用于生产烧录),可以使用 esptool.pymerge_bin 功能 。ESP-IDF 在编译后会在 build/ 目录下生成一个 flash_args 文件,里面包含了所有需要合并的文件及其地址。

进入 build 目录,执行:

bash

esptool.py --chip esp32s3 merge_bin -o merged-flash.bin @flash_args

这会生成一个名为 merged-flash.bin 的合并文件,可以直接使用 Flash Download Tools 烧录 。

7. 验证与烧录

  • 验证:你可以先用 idf.py size 查看各组件的内存占用情况,确保没有溢出。
  • 烧录:连接开发板,在 CLion 的运行配置中选择 flash,点击运行按钮,即可自动编译并烧录到设备 。

至此,你已经成功将 NumWorks 的代码集成到 ESP-IDF 的构建系统中,并生成了可以在 ESP32-S3 上运行的 bin 文件。当然,这只是一个开始——你可能会遇到大量编译错误,因为原有的代码依赖于特定的硬件和链接脚本。这正是我们下一篇要解决的问题:Ion 硬件抽象层的移植