Foreword
使用CMkae+Kconfig最小化的创建一个可以模块化的工程,可以适用于大部分MCU类型的工程,并且有一定程度的扩展性。
需求环境
环境需要的东西比较多,要安装4个独立程序才行,对比IDE一键安装,是复杂了一些
Arm GNU Toolchain
https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
Arm交叉编译的环境,这个是编译的必需品,选择10.3的经典版本,实际上选择最新版(13.3和14.2)也没问题
Version 10.3-2021.10
https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/downloads
14.2.Rel1 版本
https://developer.arm.com/-/media/Files/downloads/gnu/14.2.rel1/binrel/arm-gnu-toolchain-14.2.rel1-mingw-w64-x86_64-arm-none-eabi.exe
安装时勾选加入环境变量,省的手动添加
MinGW64
https://github.com/niXman/mingw-builds-binaries/releases
还需要mingw64的make工具链,需要手动加入环境变量,最好顺手把mingw32-make.exe
改成make.exe
,不然用起来很麻烦
CMake
https://cmake.org/download/
安装时勾选加入环境变量,省的手动添加
Kconfig-Python
还需要python环境和对应的包,某些情况可能会用到离线包,这里也一起说明
注意python安装时的tcl/tk 这个需要安装,否则会缺少tkinter,打不开kconfig gui
核心就是这两个包得安装
pip install kconfiglib
pip install windows-curses
如果使用whl的离线安装包,就可以这样安装
pip install windows_curses-2.4.1-cp313-cp313-win_amd64.whl
pip install kconfiglib-2.2.2-py2.py3-none-any.whl
架构
结构
核心就几个文件夹,把各个模块解耦,组合到一起就行了
- application,系统的入口,启动文件,调用各个module完成业务
- driver,驱动层,驱动各种具体的设备
- module,任务层,实现各种业务细节
- platform,硬件层,给driver层提供硬件接口,实际使用的MCU的HAL或者标准驱动层
- rtos,使用的操作系统
- tools,完成Kconfig转换的代码
- cmake,编译器或者跨平台的相关CMake代码
这里重点是如何利用CMake和Kconfig进行组合,不是架构内的细节代码要怎么写
使用方法
配置工程
python -m guiconfig
生成头文件
python tools/kconfig.py Kconfig .config autoconf.h kconfigLog.txt .config
预设
cmake -S . -B build/Debug --preset Debug
make
cmake --build build/Debug --target project-debug-make
编译
cmake --build build/Debug --target project-debug-build
清空
cmake --build build/Debug --target project-debug-clean
解析
实现细节
如果是Makefile+Kconfig,平常也是把Kconfig的宏引入到Makefile中,同理CMake+Kconfig也需要,否则只能通过宏去括起来整个文件,那种方式就非常麻烦
ifdef CONFIG_XXX
...
endif
- 有很多代码都需要挨个加这个宏,那可太费劲了
最关键的点就是这里,将Kconfig生成的宏利用CMake读取进来,并且转换成CMake的宏
# Add sources and includes
file(READ "autoconf.h" config_content)
#message(STATUS "Found config macro: ${config_content}")
string(REGEX MATCHALL "CONFIG_[A-Z0-9_]+" config_macros ${config_content})
foreach(macro ${config_macros})
#message(STATUS "Found config macro: ${macro}")
set(${macro} ${macro})
#target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE ${macro})
endforeach()
还有一种方式就是通过python独去Kconfig,然后转成一个CMake文件,再被主CMakeLists引入,不过这样就需要依赖python,我不太喜欢,实现代码如下
import os
def parse_config_file_cmake(config_file):
config_vars = {}
with open(config_file, 'r') as f:
for line in f:
# Skip comments and empty lines
if line.startswith('#') or line.strip() == '':
continue
# Parse configuration line
key, value = line.strip().split('=', 1)
# Remove 'CONFIG_' prefix from the key name
key = key[7:] if key.startswith('CONFIG_') else key
# Handle quotes in the value
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
# Add to dictionary
config_vars[key] = value
return config_vars
def write_cmake_file(config_vars, cmake_file):
with open(cmake_file, 'w') as f:
f.write('# Automatically generated by config_to_cmake.py\n')
for key, value in config_vars.items():
f.write('set({} "{}")\n'.format(key, value))
def convert_config_to_cmake(config_file, cmake_file):
config_vars = parse_config_file_cmake(config_file)
write_cmake_file(config_vars, cmake_file)
def parse_config_file_header(config_file):
config_vars = []
with open(config_file, 'r') as f:
for line in f:
# Skip comments and empty lines
if line.startswith('#') or line.strip() == '':
continue
# Parse configuration line
key, value = line.strip().split('=', 1)
# If the value is 'y', replace it with a space
value = value.strip()
if value == 'y':
value = '1'
# Remove 'CONFIG_' prefix from the key name
key = key[7:] if key.startswith('CONFIG_') else key
# Add to list
config_vars.append((key, value))
return config_vars
def write_config_header(config_vars, header_file):
with open(header_file, 'w') as f:
f.write('// Automatically generated by config_to_header.py\n')
f.write('#ifndef __KCONFIG_H__\n')
f.write('#define __KCONFIG_H__\n\n')
for key, value in config_vars:
f.write('#define {} {}\n'.format(key, value))
f.write('\n#endif // __KCONFIG_H__')
def convert_config_to_header(config_file, header_file):
config_vars = parse_config_file_header(config_file)
write_config_header(config_vars, header_file)
config_file = '.config' # Path to your .config file
cmake_file = 'kconfig.cmake' # Path to the generated kconfig.cmake file
header_file = 'kconfig.h' # Path to the generated kconfig.h file
# Convert .config file to kconfig.cmake file
convert_config_to_cmake(config_file, cmake_file)
# Convert .config file to kconfig.h file
convert_config_to_header(config_file, header_file)
每个需要做选择的地方都放置一个kconfig,用来生成宏,在对应上级的位置引入这个kconfig
menu "Driver Configuration"
config LED
bool "LED"
config Motor
bool "Motor"
endmenu
有了宏以后就可以通过宏来控制文件编译
# sources
if(CONFIG_LED)
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/driver/led.c
)
# include
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/driver
)
endif()
if(CONFIG_MOTOR)
file(GLOB_RECURSE DRIVER_SOURCES
# 加入文件夹
${CMAKE_CURRENT_SOURCE_DIR}/driver/motor/*.*
)
target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${DRIVER_SOURCES})
# include
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/driver/motor
)
endif()
小技巧
利用 dir /b
指令可以快速获取当前文件夹内各文件的名称,从而给AI去帮我们写模板化的代码(AI无法获取文件结构)
不仅仅kconfig可以这么处理,实际上cmake也可以这么处理,快速将已有的文件加入到某个编译选型内
- 注意检查,copilot偶尔会写错,比如之前把某两个字母顺序写倒了,编译半天总是报错,才发现路径不对
其他
记录一些CMake的操作
CMake排除文件,使用正则的方式进行排除
file(GLOB_RECURSE USER_SOURCES
"libs/*.*"
)
将含有math/vector路径、desperate的文件排除
list(FILTER USER_SOURCES EXCLUDE REGEX "desperated")
list(FILTER USER_SOURCES EXCLUDE REGEX "math/vector")
还有一种使用删除进行删除
file(GLOB_RECURSE FILE *.cpp *.c *.h)
file(GLOB_RECURSE WEBRTC_FILE webrtc_test.* ${CMAKE_CURRENT_SOURCE_DIR}/src/webrtc_*)
file(GLOB_RECURSE WEBRTC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/webrtc/*)
list(REMOVE_ITEM FILE ${WEBRTC_FILE})
list(REMOVE_ITEM FILE ${WEBRTC_DIR})
CMake有两种方式添加库,或者说叫添加.a文件
# Add linked libraries
target_link_libraries(${CMAKE_PROJECT_NAME}
# Add user defined libraries
-l:xxxx.a
libyyyy.a
)
一种是需要你把原本的库命名为lib开头的,一种是通过-l指定非lab开头的库
.cmake和CMakeLists的区别
- .cmake,是通过
include()
方式进行引入的,本质上是嵌入主cmake文件中的,所以是按照顺序执行的 - CMakeLists,是通过
add_subdirectory()
方式引入,不嵌入主cmake中,执行顺序是比主cmake优先的
.cmake由于是嵌入,所以也就不存在什么局部变量全局变量一说,都是在主cmake中的,所以通用,但是CMakeLists不是,他作为子部分,默认情况下他的变量只能他自己使用,只有cmake自己的全局变量是可以被使用的,用户自定义的是无法使用的。
但是也有办法使用主cmake的变量
set(LINKER_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/xxx/link.ld CACHE STRING "Linker Script Path" FORCE)
首先是将变量变成CACHE,这样这个变量就会被全局缓存,但是这种方式只是会保存这个变量,也就是子cmake可以用主cmake的这个变量的值,但是子cmake不能反过来设置这个值
如果要设置这个值,必须使用后面的FORCE
关键字来修改
总而言之子CMakeLists的作用域类似于编程语言中的函数,写在其中的内容的作用域也类似于一个函数内的各种变量的作用域
Summary
还没有完全优化完成,比如同时编译四五个配置,怎么把这四五个配置暂存下来,方便后续编译,而不是每次都要切换配置去编译,还有CMake和Kconfig能不能只写其中一个,另外一个自动生成?
目前对于模块的引入能不能自动识别加入,而不用手动写引入的部分。
CMake和Kconfig这种模式还有一个小问题,生成的宏是直接文件级别跳过编译的,但是如果要同时兼容IDE或者VSCode这类的时候,能不能将这些关联导入到这些IDE里,否则IDE里总是显示不正常的,快速跳转也会跳到错误的位置上
Quote
https://github.com/LuckkMaker/apm32-kconfig-example