再谈 threadX 移植

ThreadX 再介绍

一些此前的内容可见最全认证 RTOS——azure_threadX 移植教程 - 土星环的基地。

由于 threadX 捐赠给 eclipse 基金会，现在已经不叫 azure_threadX 改为 eclipse_threadX 。但就像桥本有菜改名新有菜，有栖露露改名露露茶，只是换个厂牌而已，东西没啥区别

最近我的新的 CMAKE 工程结构逐渐趋于完善，也是时候介绍一下相对现代的移植方式了。过去使用的 IDE (点名批评 KEIL) 过于落后，通常只能手动指定所有的源码文件和 includPath，而 CMAKE 则可以提供更加自由更加自动化的方式。

在以前的博文里提到过，“大多数好的 c 语言库都会提供搜集了所有源码文件的 CMakeLists.txt”，而优秀的库会提供包括编译条件和按照编译条件选择源码文件的 CMakeLists.txt。所以在一个 CMAKE 项目中引入 threadX 软件包将非常方便。

前提条件

首先读者应该有基本的 CMAKE 认知以及简单的实践经验，移植前先构建一个能基础闪灯的工程并且烧录成功，保证闪灯的频率大致与预想相同。

对于 CMAKE 子目录和 project 的概念有基本的理解

完成过一个 CMAKE 项目的搭建和编译

没有这方面知识的可以先学习CMAKE 扫盲 - 土星环的基地。

大致思路

threadX 软件包已经提供了几乎最全面的支持。我们只要将整个代码库弄进工程，少量给出几个参数，随后以其为子目录，再自行添加链接库，就算是完成移植了。

将代码库弄进工程

对于初学者或者不想要使用 git 管理代码的工程师，只要将这个代码库完全地下载和复制到项目中即可。
eclipse-threadx/threadx: Eclipse ThreadX is an advanced real-time operating system (RTOS) designed specifically for deeply embedded applications.

例如我的这个项目，使用 6_Rtos 文件夹存放 RTOS 相关的源码，threadx-master 文件夹就是直接下载源码包并且解压得到的。当然如果读者熟悉 git 子模块或者 CMAKE 在线导入的方法，也是极好的，我这里只做尽可能简单的最小系统搭建。

在顶层的 CMakeLists.txt 中使用这样的语句，即可引入 threadX。我这里的顶层 CMakeLists.txt 在上一层的 src 文件夹下，反正依据 CMAKE 项目结构，需要比解压的源码包更上层。

其中 THREADX_ARCH 指定了内核型号，THREADX_TOOLCHAIN 指定了编译器型号，这两个参数将由 threadx-master 中的 CMAKE 脚本自动选定 port 文件，详细可以自行研究 threadX 库的根 CMakeLists.txt 。

Copy
# threadx
set(THREADX_ARCH cortex_m7)
set(THREADX_TOOLCHAIN gnu)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/6_Rtos/threadx-master) # 添加子目录

添加链接库

[!NOTE]

再次提醒，读者必须完成过一个 CMAKE 项目的搭建和编译，否则以下的 CMAKE 基础脚本写法也不会懂的

由于我们在顶层 CMakeLists.txt 引入的子目录，所以需要在顶层项目中添加链接库。使用类似如下的语句

Copy
target_link_libraries( MY_CMAKE_PROJECT_NAME
# 省略其他的链接库，例如 user_src，在最后添加
azrtos::threadx
)

如果希望使用经典的 tx_user.h 配置文件。那么还需要在引入子目录前使用类似如下的语句。预先设置一个 TX_USER_FILE 参数用于导向我们自己创建的 tx_user.h 配置文件，而库内的 cmake 脚本会依据 TX_USER_FILE 参数是否被设置进行不同的操作，可以自行了解。

Copy
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 如下

Copy
# 指定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 两个中断，所以需要将原工程中的这两个中断函数的定义给注释掉。

其次这些中断的实现，threadx 不会直接地在库中引入，而是需要自己实现。

推荐是创建一个 tx_initialize_low_level.S 文件在自己工程的其他文件夹，然后在 threadX 软件包中找一个合适的同名文件 (不同架构都有示例)，将这个文件复制出来到自己创建的里面。不要直接引入他的示例，会破坏源码包的独立性。

然后依据编译报错修改自己的文件，主要是链接符号的匹配问题，例如中断向量表的名字。

以下是一个简单的示例。其实讲道理是所有他给出的中断函数都应该进行替换屏蔽，但是要修改自己的中断函数名没必要，只屏蔽必要的两个中断函数由其接管就好，其他函数也没什么很大的意义。

Copy
/**************************************************************************/
/* */
/* 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 两个参数，与主频和期望的任务时间分辨力匹配。例如我这个文件中，主频是 600M，tx_thread_sleep (1) 期望是 1ms。

GNU 部分

gcc 编译链与 AC 编译器的 MicroLib 不同，需要自己实现许多系统级接口。一个经典的 io 函数是 printf ，如果不实现接口，编译链是无法通过的。在项目中增加以下两个文件就好，当然我给出的源码并没有 printf 的接口支持。

Copy
#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;
}

Copy
#include <errno.h>
#include <stdint.h>
#include <stddef.h>

/**
* Pointer to the current high watermark of the heap usage
*/
static uint8_t *__sbrk_heap_end = NULL;

/**
* @brief _sbrk() allocates memory to the newlib heap and is used by malloc
* and others from the C library
*
* @verbatim
* ############################################################################
* # .data # .bss # newlib heap # MSP stack #
* # # # # Reserved by _Min_Stack_Size #
* ############################################################################
* ^-- RAM start ^-- _end _estack, RAM end --^
* @endverbatim
*
* This implementation starts allocating at the '_end' linker symbol
* The '_Min_Stack_Size' linker symbol reserves a memory for the MSP stack
* The implementation considers '_estack' linker symbol to be RAM end
* NOTE: If the MSP stack, at any point during execution, grows larger than the
* reserved size, please increase the '_Min_Stack_Size'.
*
* @param incr Memory size
* @return Pointer to allocated memory
*/
void *_sbrk(ptrdiff_t incr)
{
extern uint8_t _end; /* Symbol defined in the linker script */
extern uint8_t _estack; /* Symbol defined in the linker script */
extern uint32_t _Min_Stack_Size; /* Symbol defined in the linker script */
const uint32_t stack_limit = (uint32_t)&_estack - (uint32_t)&_Min_Stack_Size;
const uint8_t *max_heap = (uint8_t *)stack_limit;
uint8_t *prev_heap_end;

/* Initialize heap end at first call */
if(NULL == __sbrk_heap_end)
{
__sbrk_heap_end = &_end;
}

/* Protect heap from growing into the reserved MSP stack */
if(__sbrk_heap_end + incr > max_heap)
{
errno = ENOMEM;
return (void *)-1;
}

prev_heap_end = __sbrk_heap_end;
__sbrk_heap_end += incr;

return (void *)prev_heap_end;
}

此文由 Mix Space 同步更新至 xLog
原始链接为 https://www.yono233.cn/posts/novel/25_5_23_threadX