从 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:模块定义规则
核心思路是:每个模块(如 ion、kandinsky)都在其目录下有一个 makefile,该文件定义了模块的源文件、头文件路径和依赖。顶层 Makefile 会递归地包含这些子 makefile,最终生成编译命令。
以 ion 模块为例 ,查看 ion/makefile 可能看到类似内容:
make
scss
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 =
其他模块如 poincare 的 makefile 则可能包含更复杂的源文件列表和依赖(如 liba、libm 等)。
编译选项 则定义在 build/config.mak 或通过环境变量传入,例如:
make
ini
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
go
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:全局的CFLAGS、CXXFLAGS、LDFLAGS。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_options 和 target_compile_definitions 添加。
4. 提取依赖关系
模块间的依赖通常在 makefile 中以 LDFLAGS 或显式的 DEPENDS 变量体现。例如,poincare 模块可能依赖于 ion、kandinsky 和外部数学库 gmp、mpfr。
查看 poincare/makefile,可能找到类似:
make
ini
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
ruby
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_definitions和target_compile_options设置宏和编译选项。
其他模块类似。对于外部库(如 gmp、mpfr),可以将其视为独立的组件,或者使用 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实现msleep、usleep。
适配代码的编写将在后续文章中详细展开,本篇重点在于构建系统。
7. 处理外部库
NumWorks 使用了多个第三方库,它们位于 lib/ 目录下,包括:
liba:可能是辅助库(需查看)gmp、mpfr、mpc:多精度数学库zlib:压缩库lz4:压缩库
对于这些库,有两种处理方式:
方式一:直接使用 ESP-IDF 的组件版本 如果 ESP-IDF 已经提供了这些库的组件(如 mbedtls、spiffs),可直接在 PRIV_REQUIRES 中引用。但 gmp、mpfr 通常需要自行移植。
方式二:将库源码复制到 components 并编写 CMakeLists.txt 以 gmp 为例,在 components/gmp/CMakeLists.txt 中:
cmake
bash
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 显示驱动。