Reintroducing ThreadX
Some earlier content can be found in The Most Fully Certified RTOS — azure_threadX Porting Tutorial - Saturn Ring Base.
Since ThreadX was donated to the Eclipse Foundation, it is no longer called azure_threadX but eclipse_threadX.
Recently, my new CMake project structure has gradually become stable and mature, so it's time to introduce a more modern way of porting. The IDE I used before (KEIL, naming and shaming) is too outdated — it usually only allows you to manually specify all source files and include paths, whereas CMake offers a freer and more automated approach.
As mentioned in a previous post, “most good C libraries provide a CMakeLists.txt that collects all their source files”, and the excellent ones provide a CMakeLists.txt that includes compile conditions and selects source files based on them. So bringing the ThreadX package into a CMake project is very convenient.
Prerequisites
First, readers should have a basic understanding of CMake and some simple hands-on experience. Before porting, build a basic LED-blinking project and flash it to the board successfully, making sure the blink frequency roughly matches your expectation.
- Have a basic understanding of CMake subdirectories and the concept of project
- Have built and compiled a CMake project before
If you don't have this knowledge yet, you can start with CMake 101 - Saturn Ring Base.
Overall Approach
The ThreadX package already provides near-complete support. All we need to do is bring the entire codebase into the project, pass a few parameters, add it as a subdirectory, and then link the library ourselves — that's basically the port done.
Bringing the Codebase into the Project
For beginners, or engineers who don't want to manage code with git, just download the entire codebase and copy it into the project.
For example, in my project I use the 6_Rtos folder to store the RTOS-related source code, and the threadx-master folder is the source package downloaded and extracted directly. Of course, if you're familiar with git submodules or CMake's online import method, that's great too — I'm just demonstrating the simplest possible minimal setup here.

image-20250526145554852
Using statements like these in the top-level CMakeLists.txt brings in ThreadX. In my case, the top-level CMakeLists.txt is in the src folder one level up — in any case, depending on your CMake project structure, it needs to be at a higher level than the extracted source package.
Among these, THREADX_ARCH specifies the core architecture and THREADX_TOOLCHAIN specifies the compiler toolchain. The CMake scripts in threadx-master will automatically select the port files based on these two parameters. For details, you can study the root CMakeLists.txt of the ThreadX library yourself.
# threadx
set(THREADX_ARCH cortex_m7)
set(THREADX_TOOLCHAIN gnu)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/6_Rtos/threadx-master) # 添加子目录
Adding the Link Library
One more reminder: readers must have built and compiled a CMake project before, otherwise the basic CMake script syntax below won't make sense.
Since we added the subdirectory in the top-level CMakeLists.txt, we need to add the link library at the top-level project. Use statements like the following:
target_link_libraries( MY_CMAKE_PROJECT_NAME
# 省略其他的链接库,例如 user_src,在最后添加
azrtos::threadx
)
If you want to use the classic tx_user.h configuration file, you also need statements like the following before adding the subdirectory. Set a TX_USER_FILE parameter in advance to point to the tx_user.h configuration file you created yourself, and the CMake scripts inside the library will behave differently depending on whether TX_USER_FILE is set — you can look into that on your own.
set(TX_USER_FILE "${CMAKE_CURRENT_LIST_DIR}/6_Rtos/UserCfg/tx_user.h")
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/6_Rtos/threadx-master) # 添加子目录
A relatively complete top-level CMakeLists.txt is as follows:
# 指定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")
Some Chip-Level Support
Interrupts
First, ThreadX takes over the two interrupts SysTick_Handler and PendSV_Handler, so you need to comment out the definitions of these two interrupt functions in your original project.
Second, ThreadX doesn't include the implementations of these interrupts directly in the library — you need to implement them yourself.
The recommended approach is to create a tx_initialize_low_level.S file in another folder of your own project, then find a suitable file with the same name in the ThreadX package (there are examples for different architectures) and copy it into the file you created. Don't include their example directly, as it will break the independence of the source package.
Then modify your own file based on the compilation errors — mainly linker symbol matching issues, such as the interrupt vector table name.
The following is a simple example. Strictly speaking, all the interrupt functions they provide should be replaced and masked out, but there's no need to rename your own interrupt functions. Just mask out the two necessary interrupt functions and let ThreadX take them over; the others don't have much significance anyway.
/**************************************************************************/
/* */
/* 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
Note that you need to modify the two parameters SYSTEM_CLOCK and SYSTICK_CYCLES to match the main clock frequency and the desired task time resolution. For example, in my file, the main clock is 600 MHz, and tx_thread_sleep(1) is expected to be 1 ms.
The GNU Toolchain
The GCC toolchain differs from the MicroLib of the AC compiler — you need to implement many system-level interfaces yourself. A classic I/O function is printf; if you don't implement the interface, the build won't pass. Just add the following two files to the project. Of course, the source code I provide here doesn't include printf interface support.
For printf, a private implementation is recommended, such as the library below.
#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;
}