numworks移植记录:5.从 Makefile 到 CMake —— 提取模块依赖并集成到 ESP-IDF

11 阅读5分钟

从 Makefile 到 CMake —— 提取模块依赖并集成到 ESP-IDF

在上一篇文章中,我们规划了如何将 NumWorks 代码组织成 ESP-IDF 组件。然而,NumWorks 本身使用 Makefile 构建,要将其移植到基于 CMake 的 ESP-IDF 环境,我们必须先搞清楚:每个模块究竟包含哪些源文件?需要哪些编译选项?模块之间如何依赖?

本篇将详细讲解如何分析 NumWorks 原有的 Makefile,提取这些关键信息,并手工(或半自动)地转换为 ESP-IDF 组件所需的 CMakeLists.txt,最终成功编译出可以在 ESP32-S3 上运行的固件。


1. 理解 NumWorks 的 Makefile 体系

NumWorks 的构建系统位于项目根目录的 Makefile,以及各子目录下的 makefile(注意是小写)。顶层 Makefile 通过 include 引入各种配置文件,例如:

  • build/platform.mak:平台相关配置(如芯片型号、工具链)
  • build/config.mak:全局编译选项
  • build/module.mak:模块定义规则

核心思路是:每个模块(如 ionkandinsky)都在其目录下有一个 makefile,该文件定义了模块的源文件、头文件路径和依赖。顶层 Makefile 会递归地包含这些子 makefile,最终生成编译命令。

ion 模块为例,查看 ion/makefile 可能看到类似内容:

make

ION_ROOT = $(ROOT)/ion

ION_SRC = \
  $(ION_ROOT)/src/display.cpp \
  $(ION_ROOT)/src/keyboard.cpp \
  $(ION_ROOT)/src/timing.cpp \
  $(ION_ROOT)/src/device/shared/crc32.cpp \
  $(ION_ROOT)/src/device/n0110/display.cpp   # 针对具体型号

ION_CCFLAGS = -I$(ION_ROOT)/include
ION_LDFLAGS = 

其他模块如 poincaremakefile 则可能包含更复杂的源文件列表和依赖(如 libalibm 等)。

编译选项则定义在 build/config.mak 或通过环境变量传入,例如:

make

CFLAGS += -Wall -Werror -Os -mcpu=cortex-m4
CXXFLAGS += -std=c++11 -fno-exceptions -fno-rtti
DEFINES += -DTARGET_DEVICE_N0110

2. 提取源文件列表

有两种方法可以获取每个模块的实际源文件列表:

方法一:直接阅读 makefile

手动打开每个模块的 makefile,记录下 *_SRC 变量中列出的所有源文件。这种方法适用于模块数量不多、结构清晰的情况,但容易遗漏条件编译的文件(比如针对不同平台的文件)。

方法二:利用 make 的调试输出

在 NumWorks 根目录执行以下命令,可以打印出整个构建过程的详细信息:

bash

make -n --print-data-base > build_log.txt

或者更精确地,使用 make -p 打印所有变量和规则。你可以在输出中搜索特定模块的 SRC 变量,例如 grep -A 10 "ION_SRC" build_log.txt

不过,这种方法输出的信息量巨大,需要耐心筛选。

推荐做法:先快速浏览每个模块的 makefile,记录下核心源文件,然后通过后续的编译错误来补充遗漏的文件(因为 CMake 会提示未定义的符号)。


3. 提取编译选项

编译选项通常集中在以下几个地方:

  • build/config.mak:全局的 CFLAGSCXXFLAGSLDFLAGS
  • build/platform.mak:针对具体平台(如 n0110)的优化选项。
  • 各个模块的 makefile 中可能追加的 *_CCFLAGS

需要重点关注:

  • 目标架构:原版使用 -mcpu=cortex-m4 等,我们需替换为 -march=xtensa 相关选项(ESP-IDF 会自动设置,一般不需要手动添加)。
  • 标准库选项-fno-exceptions-fno-rtti 等需要保留,以匹配原版代码的假设。
  • 宏定义:如 -DTARGET_DEVICE_N0110,我们需要将其改为 -DTARGET_DEVICE_ESP32S3 或类似的标识。
  • 优化等级:原版通常用 -Os(优化尺寸),ESP-IDF 默认使用 -Os,可以保持一致。
  • 链接选项-Wl,-gc-sections-Wl,-Map=output.map 等。

将这些选项整理成列表,后续在组件的 CMakeLists.txt 中通过 target_compile_optionstarget_compile_definitions 添加。


4. 提取依赖关系

模块间的依赖通常在 makefile 中以 LDFLAGS 或显式的 DEPENDS 变量体现。例如,poincare 模块可能依赖于 ionkandinsky 和外部数学库 gmpmpfr

查看 poincare/makefile,可能找到类似:

make

POINCARE_LDFLAGS = -L$(LIBGMP_PATH) -lgmp -L$(LIBMPFR_PATH) -lmpfr
POINCARE_DEPENDS = ion kandinsky

这些信息将帮助我们在 CMake 中设置组件间的依赖关系。


5. 转换为 ESP-IDF 组件 CMakeLists.txt

ion 组件为例,根据提取的信息,我们编写 components/ion/CMakeLists.txt

cmake

idf_component_register(
    SRCS
        "src/display.cpp"
        "src/keyboard.cpp"
        "src/timing.cpp"
        "src/device/shared/crc32.cpp"
        # 注意:原有的 n0110 文件不再需要,替换为 esp32s3 适配文件
        "src/esp32s3/display_esp32s3.cpp"
        "src/esp32s3/keyboard_esp32s3.cpp"
        "src/esp32s3/timing_esp32s3.cpp"
    INCLUDE_DIRS
        "include"
        "include/ion"
    PRIV_REQUIRES
        "driver"          # 依赖 ESP-IDF 驱动组件(用于 GPIO, SPI 等)
        "esp_timer"
)

target_compile_definitions(${COMPONENT_LIB} PRIVATE
    TARGET_DEVICE_ESP32S3
    # 保留原版必要的宏
    $<$<CONFIG_DEBUG>:DEBUG>
)

target_compile_options(${COMPONENT_LIB} PRIVATE
    -fno-exceptions
    -fno-rtti
    -Os
)

关键点

  • 使用 idf_component_register 注册组件,列出源文件和包含目录。
  • 通过 PRIV_REQUIRES 声明对 ESP-IDF 官方组件的依赖。
  • 使用 target_compile_definitionstarget_compile_options 设置宏和编译选项。

其他模块类似。对于外部库(如 gmpmpfr),可以将其视为独立的组件,或者使用 ESP-IDF 的组件仓库(如果存在)。如果没有,需要手动将库源码放入 components 并编写 CMakeLists.txt。


6. 处理平台特定代码

NumWorks 原版针对 STM32 的硬件实现(在 ion/src/device/n0110/ 下)需要被替换为 ESP32-S3 的实现。因此,在提取源文件时,不要包含这些原平台文件,而是新建 ion/src/esp32s3/ 目录,放入我们适配 ESP32-S3 的代码。

例如:

  • display_esp32s3.cpp:使用 ESP-IDF 的 SPI 驱动初始化 LCD,实现 Ion::Display::pushRect 等。
  • keyboard_esp32s3.cpp:读取 GPIO 或触摸屏,转换为按键事件。
  • timing_esp32s3.cpp:使用 esp_timer 实现 msleepusleep

适配代码的编写将在后续文章中详细展开,本篇重点在于构建系统。


7. 处理外部库

NumWorks 使用了多个第三方库,它们位于 lib/ 目录下,包括:

  • liba:可能是辅助库(需查看)
  • gmpmpfrmpc:多精度数学库
  • zlib:压缩库
  • lz4:压缩库

对于这些库,有两种处理方式:

方式一:直接使用 ESP-IDF 的组件版本 如果 ESP-IDF 已经提供了这些库的组件(如 mbedtlsspiffs),可直接在 PRIV_REQUIRES 中引用。但 gmpmpfr 通常需要自行移植。

方式二:将库源码复制到 components 并编写 CMakeLists.txtgmp 为例,在 components/gmp/CMakeLists.txt 中:

cmake

idf_component_register(
    SRCS
        "src/memory.c"
        "src/mp_set_fns.c"
        # ... 列出所有源文件
    INCLUDE_DIRS
        "include"
    PRIV_REQUIRES
        "newlib"
)

同时需要传递原版的配置头文件(通常由 configure 生成,但我们可以手动定义宏)。

为了简化,也可以考虑将 lib/ 下的库作为一个整体,编写一个 external_libs 组件,一次性包含所有库的源码和头文件路径。


8. 在 CLion 中验证编译

当所有组件的 CMakeLists.txt 编写完毕后,回到 CLion,重新加载 CMake 项目(File -> Reload CMake Project)。此时应该能够看到项目结构,并且可以尝试编译:

  • 点击构建按钮,观察编译输出。
  • 如果出现“未定义的引用”错误,说明有源文件遗漏或依赖顺序不对。可以逐步添加缺失的源文件,并调整 PRIV_REQUIRES 中的依赖顺序(ESP-IDF 会自动处理顺序,但有时需要显式指定)。
  • 如果出现“找不到头文件”错误,检查 INCLUDE_DIRS 是否正确设置。

常见编译错误示例

  • 未定义的宏:需要在 target_compile_definitions 中添加。
  • 使用了原平台特定的寄存器操作:这些代码必须被 ESP32-S3 适配代码替代。
  • 链接时找不到数学函数:添加 m 组件(ESP-IDF 提供 m 组件,链接 libm)。

9. 生成最终的 bin 文件

当所有模块编译通过后,在 CLion 终端中运行:

bash

idf.py build

或直接使用 CLion 的构建按钮。成功后,你将在 build/ 目录下得到 epsilon_esp32s3.bin

此时,固件虽然生成了,但很可能无法正常运行(例如黑屏、死机),因为硬件适配尚未完成。不过,构建成功是移植工作的一个重要里程碑,意味着代码已经正确集成到新平台。


10. 总结与后续

从 Makefile 到 CMake 的转换,本质上是将原有的、针对特定平台的构建信息重新组织成 ESP-IDF 能够理解的组件形式。这个过程虽然繁琐,但为后续的硬件适配扫清了障碍。

下一步,我们将深入 Ion 硬件抽象层的移植,真正让屏幕亮起来、键盘动起来。敬请期待第五篇:点亮屏幕 —— 移植 Ion 显示驱动。