CMake 架构和用法

概述

概念

  • SOC 项目:每个 SOC 项目根目录都包含一个 CMakeLists.txt 作为编译入口,其下所有 MCU 项目都是在这里添加的。

  • MCU 项目:每个 MCU 项目使用统一的工具链和配置参数(个别配置会因固件或 ROM 有差异),不同 MCU 项目可能使用不同的工具链,MCU 项目下又包括了一个或多个固件的编译。

  • 固件:主要包括 image1image2image3,三者区别如下:

    • image1: 定义了 MCU 系统中引导加载程序(bootloader)和系统闪存布局的通用基础架构,并提供了一个安全引导加载程序,支持便捷的软件升级。

    • image2: 是该 SOC 的主应用固件,通常包含实时操作系统(FreeRTOS)和应用程序任务。

    • image3: 具有高度可配置性,允许用户仅包含所需的安全服务与功能。它受到读保护(RDP)的保护,将在安全引导加载程序中解密,并被加载至由 TrustZone 技术保护的安全静态随机存储器(SRAM)中。

  • 组件:组件是在和 SOC 项目同级的 component 目录下,按照不同的功能划分为不同的子目录,每个组件都有相应的 CMakeLists.txt 定义其编译及被链接等配置。

  • 固件编译:在各自 MCU 项目下 [asdk|vsdk]/make/[image1|image2|image3]/CMakeLists.txt 中根据需要添加所需组件并定义编译规则。

SDK CMake 结构图

整个项目 CMakeLists.txt 目录结构和调用关系如下图所示:

../../../_images/build_cmake_project_structure.svg

注意

在上图中, image1image2image3 目录中的 CMakeLists.txt 中可以为 各自固件添加相应的组件,他们是相互独立的

cmake 目录结构如下:

cmake
├── flags                              # Global compile and link options
│   ├── ca32
│   │   ├── compile_options.cmake
│   │   └── link_options.cmake
│   ├── common
│   │   ├── compile_options.cmake
│   │   └── link_options.cmake
│   ├── km0
│   │   ├── compile_options.cmake
│   │   └── link_options.cmake
│   ├── km4
│   │   ├── compile_options.cmake
│   │   └── link_options.cmake
│   └── kr4
│       ├── compile_options.cmake
│       └── link_options.cmake
├── CMakeLists-template.cmake        # CMakeLists.txt template for component
├── common.cmake                     # Project related APIs
├── global_define.cmake              # Global defined parameters
├── utility_base.cmake               # Utility APIs (lower level)
├── utility.cmake                    # Utility APIs (upper level)
└── toolchain                        # Toolchain defines
    ├── ameba-toolchain-asdk-10.3.1.cmake
    ├── ameba-toolchain-asdk-12.3.1.cmake
    ├── ameba-toolchain-check.cmake
    ├── ameba-toolchain-common.cmake
    └── ameba-toolchain-vsdk-10.3.1.cmake

全局编译配置

全局编译配置指在一定范围内,所有的组件共享的编译配置内容。

编译配置主要内容

  • 源文件 (sources)

  • 头文件路径 (include directories)

  • 编译选项 (compile options)

  • 预定义 (definitions)

  • 链接选项 (link options)

  • 链接库 (link libraries)

备注

  • 源文件、头文件路径、编译选项、预定义 用于组件编译。

  • 链接选项链接库 用于固件链接。

全局编译配置来源

主要有两部分来源:

注意

  • 最终全局编译配置中各组件追加部分的顺序(如头文件路径的顺序),取决于组件被固件 add 的顺序。

  • 这个顺序是不可靠的,在顺序敏感的配置中(如头文件路径),最好通过别的方式处理,详见 组件 private 部分

全局编译配置作用域

全局编译配置的作用域为各 MCU 项目,如 RTL8721Dxkm0km4 各有一套全局编译配置, RTL8730Ekm0km4ca32 各有一套全局编译配置,它们之间相互隔离。

同一 MCU 项目下的组件编译时使用相同的全局编译配置,参考如下示意图:

../../../_images/build_cmake_project_structure_with_config.svg

其中:

  • 可以看到每个 MCU 项目都各有一个独立的编译配置作用域。

  • 同一个 MCU 项目下的不同固件 image1image2image3 使用相同的编译配置。

  • 同一个组件在不同的 MCU 项目中会使用 不同的全局编译配置 分别编译,如上图中的 at_cmdproject_mcu1project_mcu2image2 中分别被编译。

组件编译 CMakeLists.txt

组件 public 部分组件 private 部分 两部分组成:

  • 前者描述了该组件向 全局编译配置 中追加的内容

  • 后者描述了该 组件库文件 的编译配置。

组件 public 部分

该部分用于向 全局编译配置追加 如下编译配置:

  • 头文件路径:如果其他组件要使用当前组件的头文件,则可以将当前组件头文件目录加入全局。

  • 预定义:如果当前组件被添加编译时,需要同步在全局加入某些预定义(使用场景较少)。

  • 链接库:如果 当前组件给下游客户时不提供源码,只提供库文件,则链接时需要链接库文件而不是 target,所以需要将库文件路径加入全局以便链接时取出。库文件的编译可以在 组件 private 部分 中调用 ameba_add_external_app_library 完成。

上述三类配置在如下代码块中分别由 public_includespublic_definitionspublic_libraries 三个变量设置,用户可以在如下高亮代码行区域内对其进行更新。 可以参考模板 cmake/CMakeLists-template.cmake 中的一些示例,相关 API 可参考 list 操作,具体示例可以参考 修改现有组件的编译配置

##########################################################################################
## * This part defines public part of the component
## * Public part will be used as global build configures for all component

set(public_includes)                #public include directories, NOTE: relative path is OK
set(public_definitions)             #public definitions
set(public_libraries)               #public libraries(files), NOTE: linked with whole-archive options

#------------------------------------------------------------------#
# Component public part, user config begin(DO NOT remove this line)

# set public_includes, public_definitions, public_libraries in this section

# Component public part, user config end(DO NOT remove this line)
#------------------------------------------------------------------#

#WARNING: Fixed section, DO NOT change!
ameba_global_include(${public_includes})
ameba_global_define(${public_defines})
ameba_global_library(${public_libraries}) #default: whole-achived

备注

如果当前组件没有必要对上述三类配置进行更新,上述高亮代码部分可以留空。

组件 private 部分

该部分描述了该组件库文件的编译配置,主要包括:

  • 源文件:当前组件编译的源文件

  • 头文件路径:仅对当前组件有效

  • 预定义:仅对当前组件有效

  • 编译选项:仅对当前组件有效

上述四类配置在如下代码块中分别由 private_sourcesprivate_includes , private_definitionsprivate_compile_options 四个变量设置,用户可以在高亮代码行区域内对其进行更新。 可以参考模板 cmake/CMakeLists-template.cmake 中的一些用法,相关 API 可参考 list 操作

注意

组件最终的编译配置(特别是 头文件路径预定义编译选项)是由 组件 private 部分组件 public 部分 组成,头文件路径和编译选项前者优先级高于后者,例如组件实际头文件搜索路径包括:

  • 当前组件 private 部分添加的头文件路径。

  • 全局编译配置 中的头文件路径, 包含 当前组件 的和 其他组件 追加的。

所以在 组件 public 部分 已经添加的编译配置就无需在 组件 private 部分 重复添加。

一般情况下建议:

  • 倾向于将头文件路径放在 组件 private 部分,以避免头文件污染。

  • 除非是比较通用或底层组件,会被很多其他组件用到,此时放在 组件 public 部分 可以提高复用性。

##########################################################################################
## * This part defines private part of the component
## * Private part is used to build target of current component
## * NOTE: The build API guarantees the global build configures(mentioned above)
## *       applied to the target automatically. So if any configure was already added
## *       to public above, it's unnecessary to add again below.

#NOTE: User defined section, add your private build configures here
# You may use if-else condition to set these predefined variable
# They are only for ameba_add_internal_library/ameba_add_external_app_library/ameba_add_external_soc_library
set(private_sources)                 #private source files, NOTE: relative path is OK
set(private_includes)                #private include directories, NOTE: relative path is OK
set(private_definitions)             #private definitions
set(private_compile_options)         #private compile_options

#------------------------------#
# Component private part, user config begin

 # set private_sources, private_includes, private_definitions, private_compile_options in this section

# Component private part, user config end
#------------------------------#

#WARNING: Select right API based on your component's release/not-release/standalone

###NOTE: For open-source component, always build from source
ameba_add_internal_library(foo
    p_SOURCES
        ${private_sources}
    p_INCLUDES
        ${private_includes}
    p_DEFINITIONS
        ${private_definitions}
    p_COMPILE_OPTIONS
        ${private_compile_options}
)

备注

最佳实践

修改现有组件的编译配置

可以参考 组件编译 CMakeLists.txt 相关说明,也可以使用 一些可用于复杂逻辑处理的判断类型,并且 list 操作 API 可以多次调用。

以下是一些具体的例子:

  • 添加 public 头文件路径,建议使用相对路径:

    ameba_list_append(public_includes
        ${CMAKE_CURRENT_SOURCE_DIR}        # Access current dir by CMAKE_CURRENT_SOURCE_DIR, same as .
        ${CMAKE_CURRENT_SOURCE_DIR}/foo    # Access sub dir by CMAKE_CURRENT_SOURCE_DIR, same as ./foo
        foo                                # Access sub dir directly
    )
    
  • 添加链接库路径,这里用到的变量可以参考 MCU 项目相关常量

    ameba_list_append(public_libraries
        ${c_SDK_LIB_APP_DIR}/lib_foo.a
    )
    
  • 添加源文件,建议使用相对路径:

    ameba_list_append(private_sources
        foo/foo.c
        bar/bar.c
    )
    
  • 添加 private 头文件路径:

    ameba_list_append(private_includes
        ../common  # Access parent dir
        foo
        bar
    )
    
  • 添加预定义:

    ameba_list_append(private_definitions
        __RTOS__
        MBEDTLS_CONFIG_FILE="mbedtls/config.h"
    )
    
  • 添加编译选项:

    ameba_list_append(private_compile_options
        -Wno-unused-parameter
    )
    

适配具备独立编译系统的代码

对于具备独立编译系统(如采用 CMake/Makefile 的第三方组件),可通过非侵入式集成方案实现链接进入固件,具体实施可依据其编译系统类型选择适配策略。

  1. 拷贝模板 cmake/CMakeLists-template.cmake 到一个合适的位置(比如为该代码组件新建一个 wrap 目录),并改名为 CMakeLists.txt

  2. 参考 修改现有组件的编译配置 设置 组件 public 部分

  3. 组件 private 部分 通过 ameba_add_subdirectory 添加原有 CMakeLists.txt 编译,然后使用 ameba_port_standalone_internal_library 进行适配

    ameba_add_subdirectory(path/to/your/cmakelists) # Add the real CMakeLists.txt dir of the wrapped component
    ameba_port_standalone_internal_library(foo)     # Add the real target of the wrapped component to link
    
  4. 参考 CMakeLists.txt 调用关系图 在相应固件的 CMakeLists.txt 中添加上述 CMakeLists.txt 所在目录进行编译

    ameba_add_subdirectory(path/to/your/wrap/cmakelists)
    
  5. 编译,测试

调整组件子模块组织关系

当一个组件很复杂,其中包含了多个子组件,此时组件根目录下的 CMakeLists.txt 通常用于添加各个子组件的组织关系,注意如下两点:

  • 因为 MCU 项目中原则上不检查开关,直接 add 组件,所以组件的开关检查需要在这里完成,例如 component/bluetooth/CMakeLists.txt 开头:

    if(NOT CONFIG_BT)
        ameba_info("CONFIG_BT is off, bluetooth will not be built")
        return()
    endif()
    
  • 当拆分子组件时,组件根目录下的 CMakeLists.txt 要处理好底层子组件的添加逻辑,可以使用 一些可用于复杂逻辑处理的判断类型,例如 component/bluetooth/CMakeLists.txt

    ameba_add_subdirectory_ifnot(CONFIG_BT_INIC api)
    ameba_add_subdirectory_if(CONFIG_BT_AUDIO_CODEC_SUPPORT bt_audio)
    ameba_add_subdirectory(bt_mp)
    ameba_add_subdirectory(driver)
    ameba_add_subdirectory_ifnot(CONFIG_BT_INIC example)
    ameba_add_subdirectory(osif)
    ameba_add_subdirectory(rtk_coex)
    if(CONFIG_BT_ZEPHYR)
        ameba_add_subdirectory(zephyr)
    elseif(NOT CONFIG_BT_INIC)
        ameba_add_subdirectory(rtk_stack)  # refer to ble_mesh_stack
    endif()
    

component 中新增组件

当需要新增一个独立组件时,按如下步骤操作:

  1. cmake/global_define.cmake 中添加组件的路径变量,参考如下组件:

    ameba_set(c_CMPT_WIFI_DIR         ${c_COMPONENT_DIR}/wifi)
    ameba_set(c_CMPT_WPAN_DIR         ${c_COMPONENT_DIR}/wpan)
    
    ameba_set(c_CMPT_CRASHDUMP_DIR    ${c_COMPONENT_DIR}/soc/common/crashdump)
    ameba_set(c_CMPT_LZMA_DIR         ${c_COMPONENT_DIR}/soc/common/lzma)
    
    ameba_set(c_CMPT_FOO_DIR         ${c_COMPONENT_DIR}/foo)  # new component
    

    注意

    如果组件路径与 SOC 类型MCU 类型 相关,则应将其加入到 cmake/global_define.cmakeameba_reset_global_define() 宏中,例如:

    macro(ameba_reset_global_define)
        ameba_set(c_CMPT_USRCFG_DIR ${c_COMPONENT_DIR}/soc/usrcfg/${c_SOC_TYPE})
        ameba_set(c_CMPT_BOOTLOADER_DIR ${c_CMPT_SOC_DIR}/bootloader)
    
        ameba_set(c_CMPT_FOO_DIR ${c_CMPT_SOC_DIR}/foo)  # new component
    endmacro()
    
  2. component 下新建组件目录,并参考 组件编译 CMakeLists.txt 添加相关 CMakeLists.txt

  3. 参考 CMakeLists.txt 调用关系图 在相应固件的 CMakeLists.txt 中添加

    ameba_add_subdirectory(${c_CMPT_FOO_DIR})
    

常用的 CMake 接口和预设常量

list 操作

ameba_list_append

ameba_list_append(<list_name> [<value> ...])

向 list 中追加元素,支持追加多个,参数说明:

list_name:

list 变量名

value:

待追加的值

ameba_list_append_if

ameba_list_append_if(<condition> <list_name> [<value> ...] [p_ELSE <else value> ...])

基于一定的条件向 list 中追加元素,支持追加多个,参数说明:

condition:

判定条件的变量名

list_name:

list 变量名

value:

condition 成立时向 list 中追加的值

p_ELSE:

可选的关键字参数,其后面紧跟的值会在 condition 不成立时追加到 list 中

else value:

condition 不成立时向 list 中追加的值

注意

condition 所表示的变量未定义或定义了但布尔值为 FALSE 都会被认为不成立

ameba_list_append_ifnot

ameba_list_append_ifnot(<condition> <list_name> [<value> ...] [p_ELSE <else value> ...])

基于一定的条件向 list 中追加元素,支持追加多个,与 ameba_list_append_if() 功能相反,参数说明:

condition:

判定条件的变量名

list_name:

list 变量名

value:

condition 不成立时向 list 中追加的值

p_ELSE:

可选的关键字参数,其后面紧跟的值会在 condition 成立时追加到 list 中

else value:

condition 成立时向 list 中追加的值

注意

condition 所表示的变量未定义或定义了但布尔值为 FALSE 都会被认为不成立

ameba_list_append_ifdef

ameba_list_append_ifdef(<condition> <list_name> [<value> ...] [p_ELSE <else value> ...])

基于一定的条件向 list 中追加元素,支持追加多个,参数说明:

condition:

判定条件的变量名

list_name:

list 变量名

value:

condition 被定义时向 list 中追加的值( 注意:此时 condition 可以是 FALSE )

p_ELSE:

可选的关键字参数,其后面紧跟的值会在 condition 未定义时追加到 list 中

else value:

condition 未定义时向 list 中追加的值

添加库

如下 API 用于将代码编译为静态库或者对已有 target 进行进一步处理

备注

这些 API 具有如下特点:

  • 静态库被 哪个固件链接取决于是在哪个固件CMakeLists.txt 中添加,例如在 image2/CMakeLists.txt 中添加,则相应静态库会被固件 image2 链接

  • 如下 API 中的 target 实际名称是由 name 参数及其他信息如 c_MCU_PROJECT_NAMEc_CURRENT_IMAGE 组合而成,用户可以在 API 调用过后使用变量 c_CURRENT_TARGET_NAME 来获取真实的 target 名称

  • 这些 API 内部会自动使用 全局编译配置 来编译当前 target

ameba_add_internal_library

ameba_add_internal_library(<name>
                           [p_SOURCES <sources> ...]
                           [p_INCLUDES <include dirs> ...]
                           [p_COMPILE_OPTIONS <compile options> ...]
                           [p_DEFINITIONS <definitions> ...]
                           [p_DEPENDENCIES <dependencies> ...]
)

添加一个静态库,执行编译且 相应 target 会被自动链接到当前固件,库文件输出到默认的 build 目录下,参数说明:

name:

target 名称,实际完整的库文件为 lib_${name}.a

p_SOURCES:

target 源文件

p_INCLUDES:

target 头文件路径

p_COMPILE_OPTIONS:

target 编译选项

p_DEFINITIONS:

target 预定义

p_DEPENDENCIES:

target 依赖

注意

该 API 生成的库文件在 build 目录下,其内部不会检查 CONFIG_AMEBA_RLS,始终生效

ameba_port_standalone_internal_library

ameba_port_standalone_internal_library(<name>)

将一个 已有 静态库 target 添加到当前固件的链接 :

name:

target 名称

小技巧

特别适用于非侵入性的适配第三方库的 CMakeLists.txt,参考 适配具备独立编译系统的代码

注意

该 API 其内部不会检查 CONFIG_AMEBA_RLS,始终生效

ameba_add_external_app_library

ameba_add_external_app_library(<name>
                               [p_SOURCES <sources> ...]
                               [p_INCLUDES <include dirs> ...]
                               [p_COMPILE_OPTIONS <compile options> ...]
                               [p_DEFINITIONS <definitions> ...]
                               [p_DEPENDENCIES <dependencies> ...]
)

添加一个静态库,执行编译但 相应 target 不会被自动链接到当前固件,需要在 组件 public 部分 中指定,库文件会被输出到 ${c_SDK_LIB_APP_DIR} (参考 MCU 项目相关常量)目录中,且会执行 objcopy -g -D 操作,参数说明:

name:

target 名称,实际完整的库文件为 lib_${name}.a

p_SOURCES:

target 源文件

p_INCLUDES:

target 头文件路径

p_COMPILE_OPTIONS:

target 编译选项

p_DEFINITIONS:

target 预定义

p_DEPENDENCIES:

target 依赖

注意

CONFIG_AMEBA_RLSTRUE 时该该 API 什么也不做直接返回

添加子目录

ameba_add_subdirectory

ameba_add_subdirectory(<dir>)

添加一个目录用于编译,可参考 add_subdirectory,此外,当 dir 为外部路径时会取其最后一级目录名作 binary_dir,参数说明:

dir:

要添加编译的目录

ameba_add_subdirectory_if

ameba_add_subdirectory_if(<condition> <dir>)

基于一定的条件添加一个目录用于编译,其他同 ameba_add_subdirectory,参数说明:

condition:

判定条件的变量名

dir:

条件成立时要添加编译的目录

ameba_add_subdirectory_ifnot

ameba_add_subdirectory_ifnot(<condition> <dir>)

基于一定的条件添加一个目录用于编译,其他同 ameba_add_subdirectory,参数说明:

condition:

判定条件的变量名

dir:

条件不成立时要添加编译的目录

注意

condition 所表示的变量未定义或定义了但布尔值为 FALSE 都会被认为不成立

ameba_add_subdirectory_if_exist

ameba_add_subdirectory_if_exist(<dir>)

当路径不存在时,如果 CONFIG_AMEBA_RLSTRUE 时直接静默返回,否则会报错,路径存在时同 ameba_add_subdirectory,参数说明:

dir:

条件不成立时要添加编译的目录

常量定义

CMake 脚本 cmake/global_define.cmake 中定义了一些常量可以直接用于编写脚本,列举部分如下:

常量类型

变量名

通用常量

c_BASEDIR

仓库根目录

c_CMAKE_FILES_DIR

${c_BASEDIR}/cmake

c_COMPONENT_DIR

${c_BASEDIR}/component

c_EMPTY_C_FILE

${c_CMAKE_FILES_DIR}/empty_file.c

SoC 项目相关常量

c_SOC_TYPE

["amebadplus", "amebalite", "amebasmart"] 之一

c_SOC_TYPE_UPPER

["AMEBADPLUS", "AMEBALITE", "AMEBASMART"] 之一

c_SOC_TYPE_CAMEL

["AebaDplus", "AmebaLite", "AmebaSmart"] 之一

c_SOC_PROJECT_DIR

${c_BASEDIR}/${c_SOC_TYPE}_gcc_project

MCU 项目相关常量

c_MCU_TYPE

["km0", "km4", "kr4", "ca32", ...] 之一

c_MCU_PROJECT_NAME

如果 MCU 项目文件夹名称为 project_xxx,值为 xxx

c_CURRENT_IMAGE

当前固件 target 名称,如 target_img2_km4

c_MCU_KCONFIG_FILE

当前 MCU 项目 Kconfig 文件路径

c_MCU_PROJECT_DIR

${c_SOC_PROJECT_DIR}/project_${c_MCU_TYPE}

c_SDK_LIB_APP_DIR

${c_MCU_PROJECT_DIR}/[asdk|vsdk]/lib/application

c_SDK_LIB_SOC_DIR

${c_MCU_PROJECT_DIR}/[asdk|vsdk]/lib/soc

组件相关常量

c_CMPT_APP_DIR

${c_COMPONENT_DIR}/application

c_CMPT_AT_CMD_DIR

${c_COMPONENT_DIR}/at_cmd

c_CMPT_AUDIO_DIR

${c_COMPONENT_DIR}/audio

c_CMPT_BLUETOOTH_DIR

${c_COMPONENT_DIR}/bluetooth

c_CMPT_DYN_APP_DIR

${c_COMPONENT_DIR}/dynamic_app

c_CMPT_ETHERNET_DIR

${c_COMPONENT_DIR}/ethernet

c_CMPT_EXAMPLE_DIR

${c_COMPONENT_DIR}/example

c_CMPT_FILE_SYSTEM_DIR

${c_COMPONENT_DIR}/file_system

c_CMPT_LWIP_DIR

${c_COMPONENT_DIR}/lwip

c_CMPT_NETWORK_DIR

${c_COMPONENT_DIR}/network

c_CMPT_OS_DIR

${c_COMPONENT_DIR}/os

c_CMPT_RTK_COEX_DIR

${c_COMPONENT_DIR}/rtk_coex

c_CMPT_SDIO_DIR

${c_COMPONENT_DIR}/sdio

c_CMPT_SSL_DIR

${c_COMPONENT_DIR}/ssl

c_CMPT_UI_DIR

${c_COMPONENT_DIR}/ui

c_CMPT_USB_DIR

${c_COMPONENT_DIR}/usb

c_CMPT_UTILS_DIR

${c_COMPONENT_DIR}/utils

c_CMPT_TFLITE_DIR

${c_COMPONENT_DIR}/tflite_micro

c_CMPT_WIFI_DIR

${c_COMPONENT_DIR}/wifi

c_CMPT_WPAN_DIR

${c_COMPONENT_DIR}/wpan

c_CMPT_CRASHDUMP_DIR

${c_COMPONENT_DIR}/soc/common/crashdump

c_CMPT_LZMA_DIR

${c_COMPONENT_DIR}/soc/common/lzma

c_CMPT_SOC_DIR

${c_COMPONENT_DIR}/soc/${c_SOC_TYPE}

常见问题与建议

查看某个源文件的详细编译参数

在相应源文件开头加入如下代码或者加入一些语法错误然后进行编译,编译器会在相应源文件处出错并打印详细编译参数

#error debug

Undefined Reference 错误

该错误一般发生在链接生成 axf 文件阶段,常见原因有如下几类:

  • 原因一:符号所在库文件未被链接

  • 原因二:符号所在源文件未被编译

  • 原因三:符号所在代码块未被编译

下面以 RTL8721Dx 为例,针对上述原因依次进行排查,如下是一个链接错误的输出(这里仅保留了部分信息用于展示):

[5/42] Linking C executable project_km4/asdk/make/image2/target_img2_km4.axf
FAILED: project_km4/asdk/make/image2/target_img2_km4.axf
ccache /opt/rtk-toolchain/asdk-10.3.1-4354/linux/newlib/bin/arm-none-eabi-gcc
-O2
-o project_km4/asdk/make/image2/target_img2_km4.axf

-Wl,--whole-archive
project_km4/asdk/make/image2/at_cmd/lib_at_cmd.a
project_km4/asdk/make/image2/swlib/lib_swlib.a
project_km4/asdk/make/image2/file_system/fatfs/lib_fatfs.a
project_km4/asdk/make/image2/file_system/littlefs/lib_littlefs.a
project_km4/asdk/make/image2/file_system/kv/lib_kv.a
project_km4/asdk/make/image2/file_system/vfs/lib_vfs.a
project_km4/asdk/make/image2/fwlib/lib_fwlib.a
project_km4/asdk/make/image2/hal/lib_hal.a
project_km4/asdk/make/image2/misc/lib_misc.a
project_km4/asdk/make/image2/lwip/lib_lwip.a

-Wl,--no-whole-archive

-lm
-O2
-o project_km4/asdk/make/image2/target_img2_km4.axf

-Wl,--whole-archive
project_km4/asdk/make/image2/at_cmd/lib_at_cmd.a
project_km4/asdk/make/image2/cmsis-dsp/lib_cmsis_dsp.a

-Wl,--no-whole-archive

-lm
-lstdc++
ld: amebadplus_gcc_project/project_km4/asdk/lib/soc/lib_chipinfo.a(ameba_rom_patch.o): in function `io_assert_failed':
(.text.io_assert_failed+0x12): undefined reference to `rtk_log_write_nano'

针对上述错误首先确认基本信息,通过高亮行 1,2,33,34 可知:

  • 出现链接问题的 MCU 项目 是:km4

  • 出现链接问题的 固件 是:image2,文件名 是:target_img2_km4.axf

  • 未定义的符号是rtk_log_write_nano,其是在函数 io_assert_failed() 中被调用的

  • 进一步确认:

    • 包含该符号定义的 源文件log.c

    • 包含该源文件的 组件swlib

    • 该组件的生成的 库文件lib_swlib.a

然后依次排查上述原因:

符号所在库文件未被链接

  1. 排查述错误信息中的链接参数是否包含 库文件lib_swlib.a

  2. 通过上述错误信息第 9 行可知 project_km4/asdk/make/image2/swlib/lib_swlib.a 已经被 image2 加入了链接,如果没有则需要排查:

一些可用于复杂逻辑处理的判断类型

SOC 类型

SOC 类型如 amebadplusamebaliteamebasmart 可以通过如下变量判断: CONFIG_AMEBADPLUSCONFIG_AMEBALITECONFIG_AMEBASMART :

if(CONFIG_AMEBADPLUS)
    # Add code for amebadplus here
elseif(CONFIG_AMEBALITE)
    # Add code for amebalite here
elseif(CONFIG_AMEBASMART)
    # Add code for amebasmart here
endif()

注意

  1. SOC 类型的区分应当尽可能消除,更好的方法是使用特性之类的开关配置替代

  2. 如果一定要使用应秉持向上兼容的原则,即未来有新增 SOC 类型时 无需改动此处逻辑 (比如增加新的 elseif),例如 :file:component/rtk_coex/CMakeLists.txt 中的写法:

    if(NOT CONFIG_AMEBAD)
        if (CONFIG_COEXIST_HOST)
            include(rtk_coex_api.cmake)
        endif()
    endif()
    

MCU 类型

SOC 类型如 km0km4kr4ca32 等,可以通过 STR 类型变量 c_MCU_TYPE 获取:

if(${c_MCU_TYPE} STREUQAL "km0")
    # Add code for km0 here
elseif(${c_SOC_TYPE} STREUQAL "km4")
    # Add code for km4 here
elseif(${c_SOC_TYPE} STREUQAL "ca32")
    # Add code for ca32 here
endif()

CMake 常见的一些 debug 方法

日志

可以使用 CMake 内置 message() 或可读性更好的 ameba_debug()ameba_info()ameba_warning()ameba_fatal()

使用如下方式打印日志, CMake 在配置阶段执行到代码处会停止,通常可以用于分析代码执行情况

message(FATAL_ERROR "stop here")

常见 CMake 错误排查

小技巧

排查 CMake 错误要从终端输出中的 第一个 错误开始看起

执行外部命令参数错误

通常是在调用 CMake 接口 add_custom_target()add_custom_command()COMMAND 中的命令参数有误导致,而根源更多是因为某些 CMake 变量为空导致。典型错误输出如下, 特征是会包括 CMake 的 -E 参数的 usage:

FAILED: build/lib_atcmd.a
/usr/bin/cmake -E copy build/lib_atcmd.a
CMake Error: cmake version 3.30.2
Usage: /usr/bin/cmake -E <command> [arguments...]
Available commands:
capabilities              - Report capabilities built into cmake in JSON format
cat [--] <files>...       - concat the files and print them to the standard output
chdir dir cmd [args...]   - run command in a given directory
...

出现此错误对应的 CMake 代码如下:

add_custom_command(
    OUTPUT lib/lib_atcmd.a
    COMMAND ${CMAKE_COMMAND} -E copy build/lib_atcmd.a ${output_path}
    DEPENDS build/lib_atcmd.a
)

在上述代码中,如果变量 output_path 为空,拷贝命令就会变成了上述错误信息第 2 行缺少目的路径从而导致报错

拷贝替换了一个源文件,但没有重新编译

CMake 编译系统默认是增量编译,即源码或头文件发生变化时才会重新编译,而其检测变化的依据是 文件的修改时间是否比上次更新

当从别处拷贝了一个修改时间更老的文件,如果文件修改时间戳保持不变(Windows 下资源管理器默认行为)就不会触发增量编译

解决方法:

  • 方法一:Windows 环境中新建文件,然后拷贝内容

  • 方法二:执行 build.py -c 进行清理

使用 no-whole-archive 的方式链接某个静态库

关于 no-whole-archive 的介绍参考 静态链接,大多数 API 处理的静态库都是 whole-archive 的方式被固件链接的,如果需要强制静态库使用 no-whole-archive 针对不同的情况方案如下:

  • 情况一:使用 ameba_add_internal_library 编译的静态库

    ameba_add_internal_library() 函数中增加参数 p_NO_WHOLE_ARCHIVE:

    ameba_add_internal_library(at_cmd
        p_NO_WHOLE_ARCHIVE
        p_SOURCES
            ${private_sources}
        p_INCLUDES
            ${private_includes}
        p_DEFINITIONS
            ${private_definitions}
        p_COMPILE_OPTIONS
            ${private_compile_options}
        p_DEPENDENCIES
            ${c_BUILD_INFO}
    )
    
  • 情况二: 使用 public_libraries 加入链接的静态库

    在调用 ameba_global_library() 时加入参数 p_NO_WHOLE_ARCHIVE

    ameba_global_library(${public_libraries} p_NO_WHOLE_ARCHIVE)
    

进阶阅读

特殊的编译配置

以源文件为单位设置编译配置

仅对某个或某些源文件设置编译配置,如预定义等,例如为每个源文件添加一个预定义 __FILE_Z_STR__,该预定义展开为对应源文件的文件名字符串,实现如下:

ameba_list_append(private_sources
    foo.c
    bar.c
)

foreach(src ${private_sources})
    cmake_path(GET src FILENAME filename)
    set_source_files_properties(${src} PROPERTIES COMPILE_DEFINITIONS "__FILE_Z_STR__=\"${filename}\"")
endforeach()

使用 CMake 内置接口 set_source_files_properties 可以精确对每个源文件设置编译配置

按源文件类型设置编译选项

ameba_list_append(private_compile_options
    -Wno-multichar                              # for both asm/c/cpp language
    $<$<COMPILE_LANGUAGE:C>:-Wno-pointer-sign>  # for c language only
    $<$<COMPILE_LANGUAGE:CXX>:-Wno-narrowing>   # for cpp language only
    $<$<COMPILE_LANGUAGE:CXX>:-std=c++17>       # for cpp language only
)

阻止编译警告错误

全局编译配置中 cmake/flags/common/compile_options.cmake 默认使用了 -Werror 选项,即所有编译警告都会当作编译错误处理,如需解除该设定可参考下列方法:

  • 方法一:仅对当前组件生效,在当前组件的 CMakeLists.txt 添加编译选项:

    ameba_list_append(private_compile_options
        -Wno-error
    )
    
  • 方法二:对所有组件生效,修改全局编译配置 cmake/flags/common/compile_options.cmake 注释掉 -Werror 即可

配置系统

Kconfig 介绍

组织结构

本项目的配置系统采用 Kconfig 组织,通过 Kconfig 的层级引用,将各个组件的配置结构化到 amebaxxx_gcc_project/Kconfig 中,最终生成供 CMake 使用的文件。

../../../_images/kconfig_tree.svg

Kconfig 文件之间的相互引用可以采用下列语法:

  1. source :

    source 语句可以引用相对于顶层 Kconfig 的路径。例如位于 ${top_kconfig_path} 的顶层 Kconfig 使用如下语句:

    source "subdir/Kconfig"
    

    这将会把 {top_kconfig_path}/subdir/Kconfig 文件纳入到顶层 Kconfig 的引用。若 {top_kconfig_path}/subdir/Kconfig 文件不存在, 会导致 Kconfig 报错。

    备注

    不能通过 if 语句去避免引用不存在的文件,在下面的示例中,即使 DEP 没有被选中,Kconfig 仍然会尝试读取 Kconfig.other 的内容, Kconfig.other 不存在会报错。

    if DEP
       source "Kconfig.other"
    endif
    
  2. osource :

    osource 引用的文件路径即使不存在,也不会导致报错。

  3. rsource :

    通过 rsource 语句引用的路径,是相对于使用该 rsource 语句的 Kconfig 文件的路径,而不是相对顶层 Kconfig 的路径。

  4. orsource :

    该语句基于 rsource ,同时允许后面引用的文件路径不存在。

下面介绍一些 Kconfig 常见的用法和注意事项,关于 Kconfig 更详细的信息可参见: https://docs.kernel.org/kbuild/kconfig-language.html

定义 Kconfig 符号

定义一个 Kconfig 符号需要遵循下面格式:

config FOO
   bool "choose FOO"
   default n

上述 Kconfig 结构定义了一个 bool 类型的 config 符号,该符号被加载到可视化界面中显示为:

[ ]  choose FOO
  • "choose FOO" 为提示词,其将显示在可视化配置界面中,若删除 "choose FOO" ,那么可视化界面中将不会显示该配置项。除 bool 类型外,还可以定义 string, hex, int 等其他类型的配置项。

  • default n 表示 no, 代表该项默认不会被选中, default n 可缺省。注意,通过 default 定义的默认值,只有在用户没有设置过该 config 项时才会生效。

  • 如果同一个 config 中定义了多重 default 值,那么将以首次出现的有效 default 值为准。

  • 进一步地,还可以通过条件表达式丰富 Kconfig 的功能,例如下面的写法表示。当 BAR 被选中时,菜单中才会显示 FOO 项,且当 BARTIZ 同时被选中时, FOO 的默认值为 y

    config FOO
       bool "choose FOO" if BAR
       default y if BAR && TIZ
    

Kconfig 的依赖

  1. 通过 depends on 添加依赖

    通过 depends on,可以为此 config 项下的所有条目都添加依赖,即下面两种写法是等价的

    config FOO
       depends on BAR
       bool "choose FOO"
       default y
    
    ## equal to
    
    config FOO
       bool "choose FOO" if BAR
       default y if BAR
    
  2. 通过 select 添加反向依赖

    select 可以在一个 config 被选中后,强制指定另一个 config 也被选中,无论这个 config 有没有其他依赖关系。例如下面这个例子,当 BUZZ 被选中,无论 BAR 为何值, FOO 也一定会被选中,也无法通过可视化界面取消。

    config BUZZ
       bool "choose BUZZ"
       select FOO
    
    config FOO
       depends on BAR
       bool "choose FOO"
    

    备注

    • select 只能选中 bool 型的 config。

    • select 常用于不可见的 config ,或没有依赖的 config。

不可见的 config 项

当 config 下未定义提示词时,此 config 项是不可见的,意味其不可被用户手动选中或取消,其值一般通过依赖项选中,或通过 default 值定义。例如:

config BAR
   bool "BAR"
   select FOO

config FOO
   bool
   default n

##equal to

config FOO
   bool
   default y if BAR

##also equal to

config FOO
   depends on BAR
   bool
   default y

如果是非 bool 类型:

config NUM
   int
   default 255 if BAR
   default 65535 if !BAR

config 项的多重定义

在 Kconfig 语法中,可以允许同名 config 在多处定义,这些定义最终会解析合并成一个 config 值并被输出到 .config 文件中。 通过两个示例说明这种用法:

示例一:

config FOO
   bool "FOO"
   select BAR
...

config FOO
   bool # this line can be deleted
   select TIZ

示例一的写法中,两处的 FOO 表示的是同一个 config 项,当 FOO 被选中时, BARTIZ 也会被选中。第二个 bool 关键字也可以被省略。

示例二:

config FOO
   bool
   depends on BAR
   default n

...

config FOO
   bool
   depends on TIZ
   default y

示例二的写法中,两处的 FOO 分别依赖了 BARTIZ, 这两个依赖项只会作用于当前位置的表达式,即等价于:

config FOO
   bool
   default n if BAR

...

config FOO
   bool
   default y if TIZ

仅有 BAR 被选中时, FOO 的值为 n;仅有 TIZ 被选中, FOO 的值为 y。当 BARTIZ 同时被选中时,根据 default 取首个有效值的规则, FOO 的值为 n

conf 文件介绍

conf 文件格式

conf 文件通过将用户需要设置的配置项写入其中,代替可视化界面的 menuconfig 方式。conf 文件由多行配置项组成,这些配置项遵循以下格式:

CONFIG_<name1>=<value>

CONFIG_<name2>=<value>

...

= 号左右不允许有空格。

conf 文件写法

  • conf 文件的写法基本类似于 .config 文件,但需要注意的是, .config 文件中包含了所有 config 项,无论其是否对用户可见。但 conf 文件中只允许设置对用户可见的 config 项。

  • 和 menuconfig 交互式配置的方式类似,这种直接配置的方式本质上也是选中/取消选中一些 Kconfig 文件中定义的项,只是用参数输入的方式代替了交互的的输入。

例如有这样的 Kconfig:

config SUPPORT_ATCMD
   bool "Enable ATCMD"
   default n
if SUPPORT_ATCMD
   choice
      default ATCMD_MANUAL_TEST
      prompt "ATCMD Mode"
      config ATCMD_MANUAL_TEST
            bool "Manual Test Mode"
      config ATCMD_HOST_CONTROL
            bool "Host Control Mode"
   endchoice
   config ATCMD_NETWORK
      bool "Enable Network"
      default n
   config ATCMD_SOCKET
      bool "Enable Socket"
endif
[*] Enable ATCMD
   ATCMD Mode (Manual Test Mode)  --->
[ ]     Enable Longer CMD
[*]     Enable Network
[ ]     Enable Socket

对应的 conf 文件应写为:

CONFIG_SUPPORT_ATCMD=y

CONFIG_ATCMD_NETWORK=y

由于 ATCMD_MANUAL_TEST 是默认的 choice 值,因此 conf 文件中可省略 CONFIG_ATCMD_MANUAL_TEST=y

{SDK}/amebaxxx_gcc_project/utils/confs_daily_build 目录下,还提供了各种常见的配置集合文件,用户可参考这些示例创建自己的 conf 文件。

备注

用户可通过 menuconfig.py -s 将当前配置保存为 conf 文件,具体请参考 生成 conf 文件

default.conf

  • {SDK}/amebaxxx_gcc_project 目录下,提供了一个名为 default.conf 的配置集合文件,定义了编译本 SOC 项目的 初始配置

  • 特殊地, menuconfig.py -f 隐含了 menuconfig.py -f default.conf 的规则,这样在编写其它 conf 文件时,可省略 default.conf 文件中已经配置过的配置项,即只需编写 default.conf 的增量配置即可。如果需要关闭 default.conf 中的某些选项,请将对应的配置项赋值为 n

  • Kconfig 中的 default 值只有在未设定某个配置项时才会生效, 而 default.conf 文件相当于一系列默认的输入项,因此 default.conf 的优先级要高于 Kconfig 中的 default 值。

prj.conf

  • prj.conf 存放在 example 下或用户创建的 工程路径 下,记录此 example 或外部工程需要的配置项。 用户可通过 menuconfig.py -f /.../prj.conf 对项目进行配置。当用户未通过可视化配置或指定其他 conf 文件时, prj.conf 会作为 初始配置 生效。

  • prj.conf 的优先级高于 default.conf

Kconfig 自动检查更新

有这样一种情况, Kconfig 文件或 default.conf 文件更新后(例如从远程仓库中拉取),但 menuconfig 文件夹下的文件仍是基于更新前 Kconfig 生成,这时候直接运行 build.py 命令可能会出现预期外的效果。为避免这种情况发生,每次进行编译前都会对 Kconfig 的更新状态进行检查。

检查的锚定文件为 menuconfig/.config_default ,该文件每次编译前都会基于 default.conf 生成,内容为该 SOC 的默认配置值。如果某次生成 menuconfig/.config_default 后,发现 menuconfig 文件夹下已经存在同名文件并且内容和本次生成的内容不一致。控制台将打印两个文件的 diff 差异,并让用户根据差异内容进行选择:

  • 如果用户判断此次 Kconfig 更新可以忽略,那么可以按下 Enter,本次编译将继续使用当前的 .config 配置。

  • 如果用户认为忽略此次 Kconfig 内容可能会对编译结果造成影响,那么可以按下 Ctrl+C 退出。退出后,用户可以通过可视化配置或 menuconfig.py -f 的方式重新进行配置,亦或通过 menuconfig.py -cbuild.py -p 等方式清理 menuconfig 文件夹后采用默认配置编译。