IC:

概述

概念

  • 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操作,具体例子可以参考 修改现有组件的编译配置

 1##########################################################################################
 2## * This part defines public part of the component
 3## * Public part will be used as global build configures for all component
 4
 5set(public_includes)                #public include directories, NOTE: relative path is OK
 6set(public_definitions)             #public definitions
 7set(public_libraries)               #public libraries(files), NOTE: linked with whole-archive options
 8
 9#------------------------------------------------------------------#
10# Component public part, user config begin(DO NOT remove this line)
11
12# set public_includes, public_definitions, public_libraries in this section
13
14# Component public part, user config end(DO NOT remove this line)
15#------------------------------------------------------------------#
16
17#WARNING: Fixed section, DO NOT change!
18ameba_global_include(${public_includes})
19ameba_global_define(${public_defines})
20ameba_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部分 可以提高复用性。

 1##########################################################################################
 2## * This part defines private part of the component
 3## * Private part is used to build target of current component
 4## * NOTE: The build API guarantees the global build configures(mentioned above)
 5## *       applied to the target automatically. So if any configure was already added
 6## *       to public above, it's unnecessary to add again below.
 7
 8#NOTE: User defined section, add your private build configures here
 9# You may use if-else condition to set these predefined variable
10# They are only for ameba_add_internal_library/ameba_add_external_app_library/ameba_add_external_soc_library
11set(private_sources)                 #private source files, NOTE: relative path is OK
12set(private_includes)                #private include directories, NOTE: relative path is OK
13set(private_definitions)             #private definitions
14set(private_compile_options)         #private compile_options
15
16#------------------------------#
17# Component private part, user config begin
18
19 # set private_sources, private_includes, private_definitions, private_compile_options in this section
20
21# Component private part, user config end
22#------------------------------#
23
24#WARNING: Select right API based on your component's release/not-release/standalone
25
26###NOTE: For open-source component, always build from source
27ameba_add_internal_library(foo
28    p_SOURCES
29        ${private_sources}
30    p_INCLUDES
31        ${private_includes}
32    p_DEFINITIONS
33        ${private_definitions}
34    p_COMPILE_OPTIONS
35        ${private_compile_options}
36)

备注

最佳实践

修改现有组件的编译配置

可以参考 组件编译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 中定义了一些常量可以直接用于编写脚本,列举部分如下:

通用的常量

Variable Name

Value

c_BASEDIR

Root dir of the repo

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项目相关的常量

Variable Name

Value

c_SOC_TYPE

One of [“amebadplus”, “amebalite”, “amebasmart”]

c_SOC_TYPE_UPPER

One of [“AMEBADPLUS”, “AMEBALITE”, “AMEBASMART”]

c_SOC_TYPE_CAMEL

One of [“AebaDplus”, “AmebaLite”, “AmebaSmart”]

c_SOC_PROJECT_DIR

${c_BASEDIR}/${c_SOC_TYPE}_gcc_project

MCU项目相关的常量

Variable Name

Value

c_MCU_TYPE

One of [“km0”, “km4”, “kr4”, “ca32”, …]

c_MCU_PROJECT_NAME

If MCU project folder name is project_xxx, value is xxx

c_CURRENT_IMAGE

Current image target name, value like: target_img2_km4

c_MCU_KCONFIG_FILE

Kconfig file path for current MCU project

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

组件相关的常量

Variable Name

Value

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

 1[5/42] Linking C executable project_km4/asdk/make/image2/target_img2_km4.axf
 2FAILED: project_km4/asdk/make/image2/target_img2_km4.axf
 3ccache /opt/rtk-toolchain/asdk-10.3.1-4354/linux/newlib/bin/arm-none-eabi-gcc
 4-O2
 5-o project_km4/asdk/make/image2/target_img2_km4.axf
 6
 7-Wl,--whole-archive
 8project_km4/asdk/make/image2/at_cmd/lib_at_cmd.a
 9project_km4/asdk/make/image2/swlib/lib_swlib.a
10project_km4/asdk/make/image2/file_system/fatfs/lib_fatfs.a
11project_km4/asdk/make/image2/file_system/littlefs/lib_littlefs.a
12project_km4/asdk/make/image2/file_system/kv/lib_kv.a
13project_km4/asdk/make/image2/file_system/vfs/lib_vfs.a
14project_km4/asdk/make/image2/fwlib/lib_fwlib.a
15project_km4/asdk/make/image2/hal/lib_hal.a
16project_km4/asdk/make/image2/misc/lib_misc.a
17project_km4/asdk/make/image2/lwip/lib_lwip.a
18
19-Wl,--no-whole-archive
20
21-lm
22-O2
23-o project_km4/asdk/make/image2/target_img2_km4.axf
24
25-Wl,--whole-archive
26project_km4/asdk/make/image2/at_cmd/lib_at_cmd.a
27project_km4/asdk/make/image2/cmsis-dsp/lib_cmsis_dsp.a
28
29-Wl,--no-whole-archive
30
31-lm
32-lstdc++
33ld: amebadplus_gcc_project/project_km4/asdk/lib/soc/lib_chipinfo.a(ameba_rom_patch.o): in function `io_assert_failed':
34(.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:

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

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

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

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

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

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

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

解决方法:

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

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

进阶阅读

特殊的编译配置

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

仅对某个或某些源文件设置编译配置,如预定义等,例如为每个源文件添加一个预定义 __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 报错。

  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 项时才会生效。

  • 进一步地,还可以通过条件表达式丰富 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

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 文件。

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 文件夹后采用默认配置编译。