はじめに
🎉やっと、やっと、Keil の緑の呪縛に耐えかねて、会社は GCC コンパイルへの移行を進めています。GCC ツールチェーンの統合に、CMAKE より良い方法はないと思っています(もちろん EIDE プラグインも悪くないですが)。長い間、独自に模索して、ついに Keil を完全に追い出すことができました。Eclipse はまだ完全には追い出せません。TI の MCU はどうしても CCS が必要ですからね。いつになったら VSCode が全面普及するのでしょうか。
ツール
ブログのファイル配布ページにポータブル版 VSCode のダウンロードがあります。必要なツールはすべて統合済みで、インストール不要のポータブル版です。
1_首次打开请运行.bat というファイルがあるので、ダブルクリックして実行すれば、VSCode をそのまま使えます。このスクリプトの主な役割は、管理者権限に昇格し、各ツールチェーンの PATH を環境変数へ自動的に追加することです。
VSCode ワークスペースの合理化
.code-workspace ファイルの settings セクションには、次のような部分を追加できます。
"settings": {
"C_Cpp.formatting": "clangFormat",
"cmake.configureEnvironment": {
"PROGRAM_NAME": "H7_GCC_PIONEER"
}
},
ここではフォーマッタとして clangFormat を指定しています。このフォーマッタの実行ファイルは、1_首次打开请运行.bat の実行時に環境変数へ追加されます。
CMAKE の環境変数を1つ指定しています。これは、生成する書き込み用ファイルの名前を設定しやすくするためです。
メインの CMakeLists.txt(ルートディレクトリ)には、以前は次のような内容がありました。
set(CMAKE_PROJECT_NAME DebugBuild)
次のように変更します。
if(DEFINED ENV{PROGRAM_NAME})
set(CMAKE_PROJECT_NAME $ENV{PROGRAM_NAME})
else()
message(WARNING "PROGRAM_NAME environment variable is not set. Using default project name.")
set(CMAKE_PROJECT_NAME "DefaultProjectName")
endif()
こうすることで、ワークスペースの .code-workspace ファイル内の PROGRAM_NAME が使われるようになります。
このような運用にする主な目的は、このプロジェクトを開発する人に CMAKE 関連のファイルをなるべく変更させないことです。以降も、この目的で行う操作があります。
メインビルドの分岐
CMAKEBUILDTYPE には、CMAKE プラグインから実際のビルドタイプが渡されます。ただし、このビルドタイプとは別に、独自のビルドタイプも欲しい場合は、次のような操作ができます。
実行ファイルを生成するメインビルドの、比較的前方の部分で、独自のプロパティ変数を使用します。
set(CMAKE_BUILD_BL "YES")
set(CMAKE_BUILD_BL "NO")
一方、ツールチェーンを指定するファイルでは次のように使えます(もちろん他のファイルでも同様です。メインビルドのプロパティ変数はサブディレクトリやサブビルドに伝播します)。
if(CMAKE_BUILD_BL MATCHES YES)
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -T \"${CMAKE_SOURCE_DIR}/CPU/GNU/stm32H743XI_INCBL.ld\"") # 添加链接脚本
endif()
if(CMAKE_BUILD_BL MATCHES NO)
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -T \"${CMAKE_SOURCE_DIR}/CPU/GNU/stm32H743XI.ld\"") # 添加链接脚本
endif()
この操作で2種類のリンカスクリプトを切り替えています。YES のときのリンカスクリプトには bootload 部分が含まれており、主に出荷時の書き込みに使用します。NO のときは bootload 部分を含まず、アップデート用になります。これでまたひと手間減りました。EIDE プラグインではここまで便利にはできず、毎回スクリプトを少し手で変更する必要があります。もちろん、開発者に CMAKE 関連のファイルをなるべく変更させないためには、先ほどのように環境変数を新設する方法もあります。
メインビルド内で定義するビルド後タスク
私のメインビルドの末尾には、次の2行があります。
# 为单独的文件添加编译标签
include("${CMAKE_CURRENT_LIST_DIR}/8_WorkSpace/CMake/toolCmake/extra-compile-flags.cmake")
# 运行一下构建后任务
include("${CMAKE_CURRENT_LIST_DIR}/8_WorkSpace/CMake/toolCmake/post-build-tasks.cmake")
extra-compile-flags.cmake
このファイルの内容は次のとおりです。
include(${CMAKE_CURRENT_LIST_DIR}/cmake_func/functions.cmake)
set_compile_flags_for_matching_files(${CMAKE_PROJECT_NAME} "6_Rtos|7_Exlib" "-w")
set_compile_flags_for_matching_files(user_src "6_Rtos|7_Exlib" "-w")
この処理は、メインビルドの ${CMAKE_PROJECT_NAME} と、ソース取り込み用プロジェクト user_src のうち、6Rtos または 7Exlib フォルダにあるソースに対して、-w のコンパイルフラグ(すべての警告を無視する)を追加するものです。超厳しい全警告設定を有効にしているため、OS や他人が書いたライブラリには警告がいくつか出ることがあります。うっとうしい上に書き換えもできないので、こういう対応をしています。
注意点として、ソース取り込み用プロジェクト user_src はもともとすべて INTERFACE 属性で定義されていたため、user_src 内のソースにはコンパイルフラグを正しく追加できません。したがって、target_sources の部分は PUBLIC 属性で定義する必要がありますが、今のところ許容できない副作用は見つかっていません。
これでまた新しいファイル functions.cmake を導入します。内容は次のとおりです。
# FUNC
# 递归包含头文件的函数
function(include_sub_directories_recursively root_dir)
if (IS_DIRECTORY ${root_dir}) # 当前路径是一个目录吗,是的话就加入到包含目录
# if (${root_dir} MATCHES "include")
message("include dir: " ${root_dir})
target_include_directories(${PROJECT_NAME} INTERFACE
${root_dir}
)
# endif()
endif()
file(GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*) # 获得当前目录下的所有文件,让如 ALL_SUB 列表中
foreach(sub ${ALL_SUB})
if (IS_DIRECTORY ${root_dir}/${sub})
include_sub_directories_recursively(${root_dir}/${sub}) # 对子目录递归调用,包含
endif()
endforeach()
endfunction()
# 给某个目标,路径带有关键词的源文件,添加期望添加的标签(例如"-w")
function(set_compile_flags_for_matching_files target_name keywords compile_flags)
# 获取目标的原始源文件列表
get_target_property(src_list ${target_name} SOURCES)
# 检查目标是否存在并有源文件
if(NOT src_list)
message(WARNING "Target '${target_name}' does not exist or has no sources.")
return()
endif()
# 创建一个新的列表用于存放需要设置编译标志的文件
set(filtered_src_list)
# 遍历原始的源文件列表,筛选出需要的文件
foreach(src_file IN LISTS src_list)
if("${src_file}" MATCHES "${keywords}")
list(APPEND filtered_src_list ${src_file})
endif()
endforeach()
# 为筛选出的源文件设置编译标志
foreach(src_file IN LISTS filtered_src_list)
set_source_files_properties(${src_file} PROPERTIES COMPILE_FLAGS "${compile_flags}")
endforeach()
endfunction()
# FUNC END
これは、私が CMake 関数を書くために専用にしているファイルです。詳しくは説明しませんが、関数が必要になったら include するだけです。
post-build-tasks.cmake
このファイルは、ビルド後のタスクを管理しやすくするためのものです。内容は次のとおりです。
# 单独管理构建后的任务
# 定义工具链工具
set(OBJCOPY arm-none-eabi-objcopy)
set(OBJDUMP arm-none-eabi-objdump)
# 添加自定义命令生成 bin 和 hex 文件
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${OBJCOPY} -O binary $<TARGET_FILE:${CMAKE_PROJECT_NAME}> ${CMAKE_PROJECT_NAME}.bin
COMMAND ${OBJCOPY} -O ihex $<TARGET_FILE:${CMAKE_PROJECT_NAME}> ${CMAKE_PROJECT_NAME}.hex
COMMAND ${CMAKE_CURRENT_LIST_DIR}/gccMapView.exe ${CMAKE_BINARY_DIR}
COMMENT "Generating bin and hex files from elf"
)
これは、bin ファイルと hex ファイルを生成するためのものです。続けて gccMapView.exe を実行しています。これは map ファイルを整理するために私が作った小さなソフトウェアです。GCC 標準の map ファイルはとても見づらいので、この小物を作りました。オープンソースの公開先はこちらです。出来はなかなか良いです。

元の map.png

整理後の出力.png
プロジェクトのソースコード取り込みは再帰検索で自動化
プロジェクトのソースコード内でヘッダファイルのパスを走査してインクルードするのに、次のような部分があります。
# cmake FUNC
include(${CMAKE_CURRENT_LIST_DIR}/cmake_func/functions.cmake)
# 递归包含头文件
include_sub_directories_recursively(${CMAKE_CURRENT_LIST_DIR}/../../../1_App)
include_sub_directories_recursively(${CMAKE_CURRENT_LIST_DIR}/../../../2_Contorl)
include_sub_directories_recursively(${CMAKE_CURRENT_LIST_DIR}/../../../3_Module)
include_sub_directories_recursively(${CMAKE_CURRENT_LIST_DIR}/../../../4_Driver)
これは CMake 関数を呼び出すので、共通の関数定義ファイルにあります。その役割は、この4つのフォルダ以下のすべてのパス(サブディレクトリを含む)をインクルードパスへ追加することです。
さらに、次のような部分もあります。
# 递归查找所有源码文件
file(GLOB_RECURSE 1_APP_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../1_App/*.c)
file(GLOB_RECURSE 2_CONTORL_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../2_Contorl/*.c)
file(GLOB_RECURSE 3_MODULE_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../3_Module/*.c)
file(GLOB_RECURSE 4_DRIVER_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../4_Driver/*.c)
file(GLOB_RECURSE 5_DRIVER_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../5_PhysicalChip/Stm32H7/*.c)
target_sources(${CMAKE_PROJECT_NAME} PUBLIC
../../../main.c
${1_APP_SRC}
${2_CONTORL_SRC}
${3_MODULE_SRC}
${4_DRIVER_SRC}
${5_DRIVER_SRC}
)
これは、この4つのフォルダ以下(サブディレクトリを含む)のすべてのソースをソースファイル一覧へ追加する処理です。
一方、より下位層や RTOS、導入ライブラリについては、ソースの取り込みやインクルードパスの指定が今も手動です。
こうすることで、ゴミのような業務ソースコードでも自由に書いたり修正したりできます。以前は名前や構造を変えるたびにあちこちのプロジェクト設定を変更しなければなりませんでしたが、今は業務コードを滞りなく開発できます。業務コードとドライバのサポートをさらに疎結合にできます。
ここに先ほど説明した変更があります。target_sources 部分は PUBLIC 属性で定義する必要があります。そうしないと、個別のファイルにコンパイルフラグを指定できないからです。INTERFACE 属性でも実際のコンパイルは行われ、メインビルドプロジェクトに紐づきはしますが、CMAKE システム上にはそのファイルの実体がありません。本質的には、ソース取り込み用プロジェクト user_src に仮にぶら下がっているだけです。user_src 自体も INTERFACE の仮想ライブラリで、実際にはコンパイルされないため、すべてのソースはプロジェクト user_src ごとメインビルドプロジェクトへリンクされ、正確なファイルを特定できません。メインビルドプロジェクトのコンパイルフラグを継承した状態でコンパイルに参加するだけです。