ThreadX 再入門
以前の関連内容は、最も充実した認証を持つ RTOS——azure_threadX 移植チュートリアル - 土星環の基地で確認できます。
threadX は Eclipse 財団に寄贈されたため、現在は azure_threadX ではなく eclipse_threadX と呼ばれるようになりました。
最近になって私の新しい CMAKE プロジェクト構成も徐々に安定し、充実してきました。そろそろ、より現代的な移植方法を紹介するころ合いです。以前使っていた IDE(KEIL を名指しで批判しておく)はあまりにも旧式で、すべてのソースファイルと includePath を手動で指定するのが当たり前でした。一方 CMAKE なら、より自由で自動化された方法を取ることができます。
以前のブログ記事でも述べたように、「ほとんどの優れた C 言語ライブラリは、すべてのソースファイルを収集した CMakeLists.txt を提供している」ものです。さらに優秀なライブラリは、コンパイル条件を含み、その条件に応じてソースファイルを選択する CMakeLists.txt まで用意しています。そのため、CMAKE プロジェクトへの threadX パッケージの導入は非常に簡単です。
前提条件
まず、読者には CMAKE の基本的な理解と簡単な実践経験があることが前提です。移植を始める前に、LED を点滅させる基礎プロジェクトを構築して書き込みに成功し、点滅の周期がおおよそ想定どおりになることを確認してください。
- CMAKE のサブディレクトリと project の概念を基本的に理解している
- CMAKE プロジェクトの構築とコンパイルを一通り完了したことがある
このあたりの知識がない場合は、CMAKE 入門 - 土星環の基地で先に学んでおくとよいでしょう。
大まかな方針
threadX パッケージは、ほぼ最も包括的と言えるサポートをすでに提供してくれています。コードベース全体をプロジェクトに取り込み、いくつかのパラメータを指定し、サブディレクトリとして追加して、あとはリンクライブラリを自分で追加するだけ。これで移植は完了です。
コードベースをプロジェクトに取り込む
初心者や git でコードを管理したくないエンジニアは、このコードベースを丸ごとダウンロードしてプロジェクトにコピーするだけで OK です。
たとえば私のこのプロジェクトでは、6_Rtos フォルダに RTOS 関連のソースコードを置き、threadx-master フォルダはソースパッケージをそのままダウンロードして解凍したものです。もちろん git のサブモジュールや CMAKE のオンラインインポートに慣れている読者なら、それを使うのも大いに結構です。ここでは可能な限りシンプルな最小構成の構築だけを行います。

image-20250526145554852
トップレベルの CMakeLists.txt に次のような記述を追加すれば、threadX を導入できます。私のプロジェクトでは、このトップレベルの CMakeLists.txt はひとつ上の src フォルダにあります。いずれにせよ CMAKE のプロジェクト構造に従い、解凍したソースパッケージより上の階層に置く必要があります。
ここで THREADX_ARCH はコアの種類を、THREADX_TOOLCHAIN はコンパイラの種類を指定します。この 2 つのパラメータに基づいて threadx-master 内の CMAKE スクリプトが port ファイルを自動的に選択します。詳しくは threadX ライブラリのルート CMakeLists.txt を自分で調べてみてください。
# threadx
set(THREADX_ARCH cortex_m7)
set(THREADX_TOOLCHAIN gnu)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/6_Rtos/threadx-master) # 添加子目录
リンクライブラリの追加
あらためて注意します。読者はCMAKE プロジェクトの構築とコンパイルを一通り完了した経験が必須です。そうでなければ、以下の CMAKE の基本スクリプトの書き方も理解できないでしょう。
トップレベルの CMakeLists.txt でサブディレクトリとして導入しているため、トップレベルプロジェクト側でリンクライブラリを追加する必要があります。次のような記述を使います。
target_link_libraries( MY_CMAKE_PROJECT_NAME
# 省略其他的链接库,例如 user_src,在最后添加
azrtos::threadx
)
従来の tx_user.h 設定ファイルを使いたい場合は、サブディレクトリを導入する前に、次のような記述を追加して、TX_USER_FILE パラメータで自分で作成した tx_user.h 設定ファイルを指定します。ライブラリ内の cmake スクリプトは TX_USER_FILE パラメータが設定されているかどうかで動作が変わるので、詳しくは各自で確認してみてください。
set(TX_USER_FILE "${CMAKE_CURRENT_LIST_DIR}/6_Rtos/UserCfg/tx_user.h")
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/6_Rtos/threadx-master) # 添加子目录
ひととおり揃ったトップレベル CMakeLists.txt の例は次のとおりです。
# 指定CMake的最低版本要求为3.22
cmake_minimum_required(VERSION 3.22)
#
# 该文件是cmake调用的主构建文件
# 用户可以根据需要自由修改此文件。
#
# 设置编译器设置部分
set(CMAKE_C_STANDARD 11) # 设置C标准为C11
set(CMAKE_C_STANDARD_REQUIRED ON) # 要求使用指定的C标准
set(CMAKE_C_EXTENSIONS ON) # 启用编译器扩展
# set(CMAKE_BUILD_TYPE "Release")
# 定义构建类型
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Debug") # 如果未设置CMAKE_BUILD_TYPE,则默认设置为"Debug"。该参数可以在使用类似"cmake ../"生成原生构建系统时添加-DCMAKE_BUILD_TYPE=Release指定
endif()
# 包含工具链文件
include("${CMAKE_CURRENT_LIST_DIR}/8_WorkSpace/CMake/gcc-arm-none-eabi.cmake")
# 设置项目名称
# set(CMAKE_PROJECT_NAME H7_GCC_BASE) # 设置项目名称
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()
# 启用编译命令生成,以便于其他工具进行索引例如clangd
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) # 生成compile_commands.json,以便IDE或工具使用
# 核心项目设置
project(${CMAKE_PROJECT_NAME}) # 定义项目,使用之前设置的项目名称
message("Build type: " ${CMAKE_BUILD_TYPE}) # 消息输出构建类型
# 启用CMake对ASM和C语言的支持
enable_language(C ASM) # 启用C和汇编(ASM)语言支持
# 创建两个可执行对象
# add_executable(${CMAKE_PROJECT_NAME}) # 不携带BL部分
add_executable(${CMAKE_PROJECT_NAME}_BL) # 携带BL部分
foreach(target IN ITEMS
# ${CMAKE_PROJECT_NAME}
${CMAKE_PROJECT_NAME}_BL)
# 链接目录设置
target_link_directories(${target} PRIVATE
# 添加用户定义的库搜索路径
# e.g., "/path/to/libs"
)
# 向可执行目标添加源文件
target_sources(${target} PRIVATE
# 添加额外的源文件
# e.g., "src/main.c"
)
# 添加包含路径
target_include_directories(${target} PRIVATE
# 添加用户定义的包含路径
# e.g., "include"
)
# 添加项目符号(宏)
target_compile_definitions(${target} PRIVATE
# 添加用户定义的符号
# e.g., "MY_MACRO=1"
)
# 添加链接库
target_link_libraries(${target}
user_src # 链接user_src库 实际上也是以project()项目的形式存在
Dataflow
azrtos::threadx
# modbusx
# 添加用户定义的库
# e.g., "mylib"
)
endforeach()
# target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
# -T "${CMAKE_SOURCE_DIR}/5_PhysicalChip/CPU/GNU/GD32H7xx.ld"
# )
target_link_options(${CMAKE_PROJECT_NAME}_BL PRIVATE
-T "${CMAKE_SOURCE_DIR}/5_PhysicalChip/CPU/GNU/GD32H7xx.ld"
)
# 添加子目录部分,这会自动处理子目录中的CMakeLists.txt文件
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/8_WorkSpace/CMake/toolCmake) # 添加子目录
# Dataflow GNU
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/7_Exlib/Dataflow-main/common) # 添加子目录
# threadx
set(THREADX_ARCH cortex_m7)
set(THREADX_TOOLCHAIN gnu)
set(TX_USER_FILE "${CMAKE_CURRENT_LIST_DIR}/6_Rtos/UserCfg/tx_user.h")
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/6_Rtos/threadx-master) # 添加子目录
# modbusx GNU
# add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/7_Exlib/modbusX/common) # 添加子目录
# 为单独的文件添加编译标签
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")
チップレベルのサポート
割り込み
まず、threadX は SysTick_Handler と PendSV_Handler の 2 つの割り込みを引き継ぎます。そのため、元のプロジェクトにあるこの 2 つの割り込み関数の定義をコメントアウトする必要があります。
また、これらの割り込みの実装は、threadX がライブラリ内で直接提供するわけではなく、自分で実装する必要があります。
おすすめは、自分のプロジェクトの別のフォルダに tx_initialize_low_level.S ファイルを作成し、threadX パッケージの中から該当する同名ファイル(アーキテクチャごとにサンプルがあります)を探して、それを自分の作成したファイルにコピーしてくる方法です。サンプルをそのまま取り込むのは避けてください。ソースパッケージの独立性が損なわれます。
その後、コンパイルエラーに従って自分のファイルを修正します。主にリンクシンボルの一致に関する問題、たとえば割り込みベクタテーブルの名前などです。
以下は簡単なサンプルです。本来は、threadX が提供する割り込み関数をすべて置き換えて無効化すべきなのですが、自分の割り込み関数の名前を変更する必要はありません。必要な 2 つの割り込み関数だけを無効化して threadX に任せれば十分で、他の関数も大した意味はありません。
/**************************************************************************/
/* */
/* Copyright (c) Microsoft Corporation. All rights reserved. */
/* */
/* This software is licensed under the Microsoft Software License */
/* Terms for Microsoft Azure RTOS. Full text of the license can be */
/* found in the LICENSE file at https://aka.ms/AzureRTOS_EULA */
/* and in the root directory of this software. */
/* */
/**************************************************************************/
/**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** Initialize */
/** */
/**************************************************************************/
/**************************************************************************/
.global _tx_thread_system_stack_ptr
.global _tx_initialize_unused_memory
.global __RAM_segment_used_end__
.global _tx_timer_interrupt
.global __main
.global __gVectors
.global __tx_NMIHandler // NMI
.global __tx_BadHandler // HardFault
.global __tx_DBGHandler // Monitor
.global __tx_PendSVHandler // PendSV
.global __tx_SysTickHandler // SysTick
.global __tx_IntHandler // Int 0
SYSTEM_CLOCK = 600000000
SYSTICK_CYCLES = ((SYSTEM_CLOCK / 1000) -1)
.text 32
.align 4
.syntax unified
/**************************************************************************/
/* */
/* FUNCTION RELEASE */
/* */
/* _tx_initialize_low_level Cortex-M7/GNU */
/* 6.1.2 */
/* AUTHOR */
/* */
/* William E. Lamie, Microsoft Corporation */
/* */
/* DESCRIPTION */
/* */
/* This function is responsible for any low-level processor */
/* initialization, including setting up interrupt vectors, setting */
/* up a periodic timer interrupt source, saving the system stack */
/* pointer for use in ISR processing later, and finding the first */
/* available RAM memory address for tx_application_define. */
/* */
/* INPUT */
/* */
/* None */
/* */
/* OUTPUT */
/* */
/* None */
/* */
/* CALLS */
/* */
/* None */
/* */
/* CALLED BY */
/* */
/* _tx_initialize_kernel_enter ThreadX entry function */
/* */
/* RELEASE HISTORY */
/* */
/* DATE NAME DESCRIPTION */
/* */
/* 09-30-2020 William E. Lamie Initial Version 6.1 */
/* 11-09-2020 Scott Larson Modified comment(s), */
/* resulting in version 6.1.2 */
/* */
/**************************************************************************/
// VOID _tx_initialize_low_level(VOID)
// {
.global _tx_initialize_low_level
.thumb_func
_tx_initialize_low_level:
/* Disable interrupts during ThreadX initialization. */
CPSID i
/* Set base of available memory to end of non-initialised RAM area. */
LDR r0, =_tx_initialize_unused_memory // Build address of unused memory pointer
LDR r1, =__RAM_segment_used_end__ // Build first free address
ADD r1, r1, #4 //
STR r1, [r0] // Setup first unused memory pointer
/* Setup Vector Table Offset Register. */
MOV r0, #0xE000E000 // Build address of NVIC registers
LDR r1, =__gVectors // Pickup address of vector table
STR r1, [r0, #0xD08] // Set vector table address
/* Enable the cycle count register. */
// LDR r0, =0xE0001000 // Build address of DWT register
// LDR r1, [r0] // Pickup the current value
// ORR r1, r1, #1 // Set the CYCCNTENA bit
// STR r1, [r0] // Enable the cycle count register
/* Set system stack pointer from vector value. */
LDR r0, =_tx_thread_system_stack_ptr // Build address of system stack pointer
LDR r1, =__gVectors // Pickup address of vector table
LDR r1, [r1] // Pickup reset stack pointer
STR r1, [r0] // Save system stack pointer
/* Configure SysTick. */
MOV r0, #0xE000E000 // Build address of NVIC registers
LDR r1, =SYSTICK_CYCLES
STR r1, [r0, #0x14] // Setup SysTick Reload Value
MOV r1, #0x7 // Build SysTick Control Enable Value
STR r1, [r0, #0x10] // Setup SysTick Control
/* Configure handler priorities. */
LDR r1, =0x00000000 // Rsrv, UsgF, BusF, MemM
STR r1, [r0, #0xD18] // Setup System Handlers 4-7 Priority Registers
LDR r1, =0xFF000000 // SVCl, Rsrv, Rsrv, Rsrv
STR r1, [r0, #0xD1C] // Setup System Handlers 8-11 Priority Registers
// Note: SVC must be lowest priority, which is 0xFF
LDR r1, =0x40FF0000 // SysT, PnSV, Rsrv, DbgM
STR r1, [r0, #0xD20] // Setup System Handlers 12-15 Priority Registers
// Note: PnSV must be lowest priority, which is 0xFF
/* Return to caller. */
BX lr
// }
/* Define shells for each of the unused vectors. */
.global __tx_BadHandler
.thumb_func
__tx_BadHandler:
B __tx_BadHandler
/* added to catch the hardfault */
.global __tx_HardfaultHandler
.thumb_func
__tx_HardfaultHandler:
B __tx_HardfaultHandler
/* Generic interrupt handler template */
.global __tx_IntHandler
.thumb_func
__tx_IntHandler:
// VOID InterruptHandler (VOID)
// {
PUSH {r0, lr}
#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
BL _tx_execution_isr_enter // Call the ISR enter function
#endif
/* Do interrupt handler work here */
/* BL <your C Function>.... */
#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
BL _tx_execution_isr_exit // Call the ISR exit function
#endif
POP {r0, lr}
BX LR
// }
/* System Tick timer interrupt handler */
.global __tx_SysTickHandler
.global SysTick_Handler
.thumb_func
__tx_SysTickHandler:
.thumb_func
SysTick_Handler:
// VOID TimerInterruptHandler (VOID)
// {
PUSH {r0, lr}
#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
BL _tx_execution_isr_enter // Call the ISR enter function
#endif
BL _tx_timer_interrupt
#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
BL _tx_execution_isr_exit // Call the ISR exit function
#endif
POP {r0, lr}
BX LR
// }
/* NMI, DBG handlers */
.global __tx_NMIHandler
.thumb_func
__tx_NMIHandler:
B __tx_NMIHandler
.global __tx_DBGHandler
.thumb_func
__tx_DBGHandler:
B __tx_DBGHandler
このファイルの SYSTEM_CLOCK と SYSTICK_CYCLES の 2 つのパラメータは、動作周波数と期待するタスクの時間分解能に合わせて修正してください。たとえば私のこのファイルでは、動作周波数が 600MHz で、tx_thread_sleep(1) が 1ms になることを想定しています。
GNU 編
gcc のツールチェーンは AC コンパイラの MicroLib とは異なり、多くのシステムレベルインターフェースを自分で実装する必要があります。代表的な io 関数が printf です。このインターフェースを実装しないと、コンパイル/リンクは通りません。プロジェクトに次の 2 つのファイルを追加すればよいでしょう。もちろん、私が示したソースコードには printf のインターフェースサポートは含まれていません。
printf は独自実装を使うことをおすすめします。たとえば以下のライブラリです。
#include <sys/stat.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <sys/times.h>
/* Variables */
// #undef errno
extern int errno;
extern int __io_putchar(int ch) __attribute__((weak));
extern int __io_getchar(void) __attribute__((weak));
// register unsigned char *__stack_ptr (__ASM("sp"));
// register unsigned char *__stack_ptr asm("sp");
char *__env[1] = {0};
char **environ = __env;
/* Functions */
void initialise_monitor_handles( )
{
}
int _getpid(void)
{
return 1;
}
int _kill(int pid, int sig)
{
errno = EINVAL;
return -1;
}
void _exit(int status)
{
_kill(status, -1);
while(1)
{
} /* Make sure we hang here */
}
__attribute__((weak)) int _read(int file, char *ptr, int len)
{
int DataIdx;
for(DataIdx = 0; DataIdx < len; DataIdx++)
{
*ptr++ = __io_getchar( );
}
return len;
}
__attribute__((weak)) int _write(int file, char *ptr, int len)
{
int DataIdx;
for(DataIdx = 0; DataIdx < len; DataIdx++)
{
__io_putchar(*ptr++);
}
return len;
}
int _close(int file)
{
return -1;
}
int _fstat(int file, struct stat *st)
{
st->st_mode = S_IFCHR;
return 0;
}
int _isatty(int file)
{
return 1;
}
int _lseek(int file, int ptr, int dir)
{
return 0;
}
int _open(char *path, int flags, ...)
{
/* Pretend like we always fail */
return -1;
}
int _wait(int *status)
{
errno = ECHILD;
return -1;
}
int _unlink(char *name)
{
errno = ENOENT;
return -1;
}
int _times(struct tms *buf)
{
return -1;
}
int _stat(char *file, struct stat *st)
{
st->st_mode = S_IFCHR;
return 0;
}
int _link(char *old, char *new)
{
errno = EMLINK;
return -1;
}
int _fork(void)
{
errno = EAGAIN;
return -1;
}
int _execve(char *name, char **argv, char **env)
{
errno = ENOMEM;
return -1;
}