2. ThreadX基础
好,咱们正式开始动手。这一章我带你走完ThreadX内核在STM32H743上的移植全流程。说白了,就是把ThreadX的调度器、中断管理、时钟节拍这些核心部件,跟你的硬件平台对接起来。
我在项目里移植过不下五个RTOS,ThreadX算是文档最清晰的一个。但清晰归清晰,该踩的坑一个不少。咱们一步步来。
移植前的准备工作
先确认你手头的东西齐了没有:
- STM32H743开发板(我习惯用Nucleo-H743ZI2)
- ThreadX源码包(从Azure RTOS仓库拉下来)
- ARM GCC工具链或IAR/Keil
- STM32CubeMX生成的HAL库基础工程
我的习惯:先用CubeMX生成一个能点灯的基础工程,确认硬件没问题,再往上叠ThreadX。别一上来就搞移植,万一板子本身有问题,你排查起来会疯掉。
ThreadX内核文件结构
打开ThreadX源码包,你真正需要关心的文件其实不多。我列个表给你看:
| 文件 | 作用 | 是否需要修改 |
|---|---|---|
| tx_initialize_low_level.S | 底层初始化汇编代码 | 是 |
| tx_thread_schedule.S | 任务调度器入口 | 否(直接使用) |
| tx_thread_context_restore.S | 上下文恢复 | 否 |
| tx_thread_context_save.S | 上下文保存 | 否 |
| tx_timer_interrupt.c | 时钟节拍中断处理 | 是 |
| tx_port.h | 平台相关的宏定义 | 是 |
嗯,核心就这几个。其他文件你暂时不用管。
启动文件与链接脚本修改
这是移植的第一步,也是最容易出问题的地方。
启动文件修改
STM32H743的启动文件(startup_stm32h743xx.s)里,你需要做两件事:
- 增加栈空间——ThreadX本身需要一块独立的系统栈,我建议至少分配4KB。
- 修改复位向量——让系统启动后先进入ThreadX的初始化函数,再跳转到main。
具体改法:
; 在启动文件中找到栈定义
Stack_Size EQU 0x00002000 ; 从默认的1KB改为8KB
; 修改复位向量
; 原来:IMPORT __main
; LDR R0, =__main
; BX R0
; 改为:
IMPORT tx_application_define ; ThreadX应用定义函数
IMPORT __main
LDR R0, =tx_application_define
BLX R0
LDR R0, =__main
BX R0
我曾经踩过的坑:第一次移植时没改栈大小,结果系统跑起来后任务一多就莫名其妙死机。查了两天才发现是栈溢出。记住,ThreadX的系统栈和任务栈是分开的,系统栈给中断和调度器用,小了绝对不行。
链接脚本修改
链接脚本(STM32H743_FLASH.ld)主要改两个地方:
- 内存区域定义——确认Flash和RAM的起始地址和大小跟你的板子一致
- 增加ThreadX专用段——有些版本需要放置tx_application_define的入口
H743的Flash是2MB,RAM是1MB(分DTCM、AXI SRAM等区域)。我一般这么配:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2M
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
RAM_D1 (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K
}
你想想看,H743的内存架构其实挺复杂的,分了好几个域。我建议把ThreadX的堆和任务栈放在RAM_D1(0x24000000),这块区域访问速度最快,而且不会被DMA干扰。
系统时钟配置
时钟配置是移植的灵魂。ThreadX需要一个周期性的定时器中断来驱动任务调度,这个中断源通常用SysTick或者TIM。
选择时钟源
我个人偏好用TIM2而不是SysTick。为什么?因为SysTick在H743上只有24位,如果你系统主频跑400MHz,SysTick的溢出时间太短,频繁进中断会影响实时性。TIM2是32位的,可以设置更长的周期。
配置方法:
// 在tx_initialize_low_level.S或tx_timer_interrupt.c中
// 假设HCLK = 400MHz,我们需要1ms的节拍
// 使用TIM2
#define TIM2_BASE_ADDR 0x40000000UL
#define TIM2_PSC 399 // 预分频:400MHz / (399+1) = 1MHz
#define TIM2_ARR 999 // 自动重装:1MHz / (999+1) = 1KHz = 1ms
void tx_timer_interrupt_setup(void)
{
// 使能TIM2时钟
RCC->APB1ENR1 |= RCC_APB1ENR1_TIM2EN;
// 配置定时器
TIM2->PSC = TIM2_PSC;
TIM2->ARR = TIM2_ARR;
TIM2->DIER |= TIM_DIER_UIE; // 使能更新中断
TIM2->CR1 |= TIM_CR1_CEN; // 启动定时器
// 配置NVIC
NVIC_SetPriority(TIM2_IRQn, 0); // 最高优先级
NVIC_EnableIRQ(TIM2_IRQn);
}
关键点:定时器中断优先级必须设为最高(0),否则高优先级中断会阻塞节拍中断,导致系统时间漂移。我在一个工业控制项目里吃过这个亏,当时电机控制中断优先级比节拍高,结果系统跑了半小时后任务调度完全乱套。
中断处理函数
在中断服务函数里,你只需要调用ThreadX的节拍处理函数:
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR = ~TIM_SR_UIF; // 清除中断标志
_tx_timer_interrupt(); // ThreadX节拍处理
}
}
就这么简单。别在中断里干别的事,节拍中断要快进快出。
整体移植流程
我把整个流程画了张图,你对照着走:
验证移植是否成功
移植完了怎么知道对不对?写个最简单的测试:
// tx_application_define.c
#include "tx_api.h"
TX_THREAD test_thread;
UCHAR test_stack[1024];
void test_entry(ULONG input)
{
while(1)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 点灯
tx_thread_sleep(500); // 延时500ms
}
}
void tx_application_define(void *first_unused_memory)
{
tx_thread_create(&test_thread, "test", test_entry, 0,
test_stack, sizeof(test_stack),
1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);
}
编译下载,如果LED以1秒周期闪烁(500ms亮+500ms灭),恭喜你,移植成功了。
一个小技巧:第一次跑的时候把节拍频率设慢一点,比如10ms。这样即使调度有问题,你也能从LED闪烁节奏上看出来。等确认移植没问题了,再改回1ms。
移植检查清单
我每次移植完都会过一遍这个清单,你也照着来:
- 启动文件:栈大小是否足够?复位向量是否正确跳转到tx_application_define?
- 链接脚本:内存区域地址是否匹配芯片型号?ThreadX堆空间是否分配?
- 时钟配置:定时器时钟源是否使能?预分频和重装值计算是否正确?
- 中断优先级:节拍中断优先级是否设为最高(0)?
- 中断处理:中断标志是否清除?是否调用了_tx_timer_interrupt?
- 任务创建:栈对齐是否4字节?优先级是否在有效范围内?
嗯,这一章的内容就到这儿。移植这件事,说白了就是让ThreadX的代码跟你的硬件对上话。只要启动、时钟、中断这三个环节不出错,后面的事情就顺了。