ThreadX内核笔记02
ThreadX的接口基本都定义在"tx_api.h"文件中
1. 任务管理
1.1 创建任务
移植章节说创建任务的模板如下:
TX_THREAD thread_0;
UCHAR thread_0_area[1024] = {0};
void thread_0_entry(ULONG thread_input)
{
while(1)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_RESET);
tx_thread_sleep(1000);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_SET);
tx_thread_sleep(1000);
}
}
// 直接把这个函数内的内容全部注释掉 只写个启动函数即可
VOID tx_application_define(VOID *first_unused_memory)
{
tx_thread_create(&thread_0, "thread 0", // 任务控制块和任务名
thread_0_entry, 0, // 任务入口和任务参数
thread_0_area, 1024, // 任务堆栈和大小
1, 1, // 优先级和任务抢占阈值
TX_NO_TIME_SLICE, // 不开启时间片
TX_AUTO_START); // 立即启动
}函数参数定义如下
UINT tx_thread_create(
TX_THREAD *thread_ptr, // 线程控制块指针
CHAR *name_ptr, // 线程名称
VOID (*entry_function)(ULONG), // 线程入口函数
ULONG entry_input, // 入口函数参数
VOID *stack_start, // 堆栈起始地址
ULONG stack_size, // 堆栈大小
UINT priority, // 线程优先级
UINT preempt_threshold, // 抢占阈值
ULONG time_slice, // 时间片
UINT auto_start // 自动启动标志
);其中需要解释一下的是
- priority:任务优先级,从0到
TX_MAX_PRIORITIES - 1,数字越小优先级越高 - preempt_threshold:抢占阈值。只有优先级严格高于此阈值的线程才能抢占当前线程。该值必须小于或等于线程的初始优先级。如果设置为与优先级相同,则禁用抢占阈值功能
- time_slice:时间片大小。用于相同优先级线程之间的时间片轮转调度。设置为
TX_NO_TIME_SLICE(值为 0)将禁用该线程的时间片轮转 - auto_start:自动启动标志
- 设置为
TX_AUTO_START:线程创建后立即进入就绪状态参与调度 - 设置为
TX_DONT_START:线程创建后处于挂起状态,后续需调用tx_thread_resume才能运行
- 设置为
- 返回值:函数返回
TX_SUCCESS表示创建成功;如果参数无效(如指针为 NULL、栈大小不足、优先级越界等),则会返回相应的错误码(如TX_THREAD_ERROR,TX_PTR_ERROR,TX_PRIORITY_ERROR等)
1.2 任务栈使用情况
从任务的 TCB 结构体中可以直接获取到任务的对战使用情况,可以通过 TCB 指针直接打印出堆栈使用,来调整堆栈大小。
注意:这里所有任务的 TX_THREAD 是一个**双向链表**,因此从任意一个任务开始一直循环就能遍历所有的任务。但是遍历期间不能创建或者删除任务,指针容易跑飞。
static void disp_task_info(void)
{
// 从任意一个任务处开始
TX_THREAD *p_tcb = &thread_0;
/* 打印标题 */
bsp_printf("===============================================================\r\n");
// bsp_printf(" 任务优先级 任务栈大小 当前使用栈 最大栈使用 任务名\r\n");
bsp_printf("\t Prio \t StackSize \t CurStack \t MaxStack \t Taskname\r\n");
/* 遍历任务控制列表TCB list),打印所有的任务的优先级和名称 */
while (p_tcb != (TX_THREAD *)0)
{
bsp_printf(" \t %2d \t %5d \t\t %5d \t\t %5d \t\t %s \r\n",
p_tcb->tx_thread_priority,
p_tcb->tx_thread_stack_size,
(int)p_tcb->tx_thread_stack_end - (int)p_tcb->tx_thread_stack_ptr,
(int)p_tcb->tx_thread_stack_end - (int)p_tcb->tx_thread_stack_highest_ptr,
p_tcb->tx_thread_name);
p_tcb = p_tcb->tx_thread_created_next;
// 回到原任务借书
if(p_tcb == &thread_0) break;
}
bsp_printf("===============================================================\r\n");
}1.3 任务调度阈值
这个是 ThreadX 特有的用法。
任务属性的preempt_threshold常规情况下设置和priority相同即可,就是正常的抢占式调度了,然后设置时间片time_slice大于 0 就可以让相同优先级任务之间切换调度了。
相对于 FreeRTOS 没有任务时间片大小的说法,RT-Thread 中有,这里 ThreadX 也是有的。
但是特殊需求下,假设有一个优先级为 20 的数据采集任务,它只与一组优先级介于 15 到 20 之间的任务进行交互。在它的核心数据处理阶段,可以将其抢占阈值动态修改为 15。此时,优先级为 15 到 19 的任务都无法抢占它(开启了这个功能之后,也不会在时间片用完之后调度),从而保证了数据采集的连续性,防止了频繁的上下文切换开销。但如果此时有一个优先级为 5 的真正紧急任务(如系统报警处理)就绪,它依然可以立即抢占该数据采集任务,保证了系统的实时响应速度。
下面例子中,thread_1_entry优先级比较低,但是当设置调度阈值为 5 时,thread_0_entry虽然优先级也是5,但是因为不允许调度,所以在阻塞期间,也是无法被执行的。
// 任务0
#define THREAD_0_MEM 1024
static TX_THREAD thread_0;
static UCHAR thread_0_memory[THREAD_0_MEM];
void thread_0_entry(ULONG thread_input)
{
while(1)
{
led_write(0, 0);
tx_thread_sleep(500);
led_write(0, 1);
tx_thread_sleep(500);
}
}
// 任务1
#define THREAD_1_MEM 1024
static TX_THREAD thread_1;
static UCHAR thread_1_memory[THREAD_1_MEM];
void thread_1_entry(ULONG thread_input)
{
bsp_printf("thread_1_entry running \r\n");
UINT old_threshold;
UINT status;
while(1)
{
/* 设置阀值,关闭所有高优先级任务抢占 */
status = tx_thread_preemption_change(&thread_1, 5, &old_threshold);
if(status == TX_SUCCESS)
{
/* 这里仅是个演示目的,大家可以加入不允许被抢占的代码(在此期间 任务优先级5及以下的都不会被调度到) */
HAL_Delay(5000);
/* 恢复阀值 */
tx_thread_preemption_change(&thread_1, 10, &old_threshold);
}
// 释放cpu 继续允许闪烁
tx_thread_sleep(5000);
}
}
void tx_application_define(void *first_unused_memory)
{
bsp_init();
// 第1个任务
tx_thread_create(&thread_0, "thread_0", // 任务控制块和任务名
thread_0_entry, 0, // 任务入口和任务参数
thread_0_memory, THREAD_0_MEM, // 任务堆栈和大小
5, 5, // 优先级和任务抢占阈值
TX_NO_TIME_SLICE, // 不开启时间片
TX_AUTO_START); // 立即启动
// 第2个任务
tx_thread_create(&thread_1, "thread_1", // 任务控制块和任务名
thread_1_entry, 0, // 任务入口和任务参数
thread_1_memory, THREAD_1_MEM, // 任务堆栈和大小
10, 10, // 优先级和任务抢占阈值
TX_NO_TIME_SLICE, // 不开启时间片
TX_AUTO_START); // 立即启动
}1.4 其他接口
任务相关的操作接口基本都在这,绝大部分其实用的都不多。
#define tx_thread_create(t,n,e,i,s,l,p,r,c,a) _txe_thread_create((t),(n),(e),(i),(s),(l),(p),(r),(c),(a),(sizeof(TX_THREAD)))
#define tx_thread_delete _txe_thread_delete
#define tx_thread_entry_exit_notify _txe_thread_entry_exit_notify
#define tx_thread_identify _tx_thread_identify
#define tx_thread_info_get _txe_thread_info_get
#define tx_thread_performance_info_get _tx_thread_performance_info_get
#define tx_thread_performance_system_info_get _tx_thread_performance_system_info_get
#define tx_thread_preemption_change _txe_thread_preemption_change
#define tx_thread_priority_change _txe_thread_priority_change
#define tx_thread_relinquish _txe_thread_relinquish
#define tx_thread_reset _txe_thread_reset
#define tx_thread_resume _txe_thread_resume
#define tx_thread_sleep _tx_thread_sleep
#define tx_thread_stack_error_notify _tx_thread_stack_error_notify
#define tx_thread_suspend _txe_thread_suspend
#define tx_thread_terminate _txe_thread_terminate
#define tx_thread_time_slice_change _txe_thread_time_slice_change
#define tx_thread_wait_abort _txe_thread_wait_abort- 删除任务
tx_thread_delete- 只可以在任务或者定时器组中调用,不能在中断中调用
- 只能删除终止或完成态任务,因此不能自己删除自己
- 挂起任务
tx_thread_suspend- 允许在中断,任务,定时器组和初始化中调用
- 恢复任务
tx_thread_resume- 允许在中断,任务,定时器组和初始化中调用
- 复位任务
tx_thread_reset- 仅可以在任务中调用,不允许在中断中调用
- 只能复位终止或完成态任务
- 终止任务
tx_thread_terminate- 不允许在中断中调用,仅可以在任务和定时器组中调用
- 终止后,必须调用函数
tx_thread_reset复位任务以使其再次执行 - 应用程序有责任确保任务处于适合终止的状态
2.printf打印
串口打印适配
#include <stdio.h>
// 根据编译器版本,自动选择禁用半主机模式的语法
#if defined (__CC_ARM) || (defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050))
#if (__ARMCC_VERSION >= 6010050)
// AC6 编译器:使用内联汇编
__asm(".global __use_no_semihosting\n");
__asm(".global __ARM_use_no_argv \n");
#else
// AC5 编译器:使用 #pragma
#pragma import(__use_no_semihosting)
struct __FILE { int handle; };
#endif
#endif
// 提供底层空壳函数,防止链接报错
FILE __stdout;
// 重写 _sys_exit,避免程序退出时触发半主机
void _sys_exit(int x)
{
x = x;
}
// 重写 _ttywrch,避免字符输出时调用半主机
int _ttywrch(int ch)
{
ch = ch;
return ch;
}
int fputc(int ch, FILE *f)
{
USART3->TDR = ch;
while((USART3->ISR & USART_ISR_TC) == 0);
// HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}直接使用printf是很容易发生互斥问题
这里使用一个互斥量来解决,初始化完成后,应用程序使用bsp_printf来进行打印。
// printf互斥
static TX_MUTEX bsp_print_mux;
int printf_init(void)
{
tx_mutex_create(&bsp_print_mux,"bsp print mux", TX_NO_INHERIT);
return 0;
}
void bsp_printf(const char *fmt, ...)
{
// 特别注意,如果printf的变量较多,注意此局部变量的大小是否够用
char buf_str[200 + 1];
va_list v_args;
// 格式化数据
va_start(v_args, fmt);
(void)vsnprintf((char *)&buf_str[0], (size_t)sizeof(buf_str), (char const *) fmt, v_args);
va_end(v_args);
/* 互斥操作 */
tx_mutex_get(&bsp_print_mux, TX_WAIT_FOREVER);
printf("%s", buf_str);
tx_mutex_put(&bsp_print_mux);
}比如这样
bsp_printf("led off \r\n");3. 优先级配置
3.1 中断优先级
NVIC(Nested vectored interrupt controller)嵌套向量中断控制器
对于 M3、M4 内核,中断优先级可以使用寄存器中的8个位来设置,最多可以设置256级不同的中断。但是厂商不会用完,STM32 实际上用了高4位,可以设置出16级中断。
这4个位可以用来设置抢占优先级和子优先级,有如下四种设置情况
- 全部用于子优先级:即所有中断优先级相同,纯看顺序
- 0-1 级抢占优先级,0-7 级别子优先级
- 0-3 级抢占优先级,0-3 级别子优先级
- 0-7 级抢占优先级,0-1 级别子优先级
- 0-15 级抢占优先级,0 级别子优先级(一般默认也推荐使用这个)
如下图所示,默认是4个位都用于抢占优先级。

优先级数字越小,表示优先级越高,上图中为系统提供时钟的定时器优先级是最低的 15。
3.2 中断管理
这里和 FreeRTOS 其实差不多。
以电机控制器为例,单片机程序分为两部分;
- 第一部分不依靠 RTOS 运行,主要在中断中运行,进行电机控制
- 第二部分为 RTOS 系统下的应用(包含中断),运行通信或控制逻辑
当 RTOS 下的应用在处理临界区时,最常见的处理措施就是关中断。但是直接关闭全部中断就会导致不依靠 RTOS 运行的程序也会被迫中断。当关中断的地方越来越多,实时性就越得不到保证。
关闭中断的寄存器常用的有如下几个:
- primask:这个寄存器只有 1 个位,置1屏蔽所有可关闭中断(除了
NMI和HardFault);0 则不屏蔽 - faultmask:这个寄存器只有 1 个位,置1屏蔽除了
NMI之外的所有中断;0 则正常不屏蔽 - basepri:这个寄存器最多 9 个位,表示被屏蔽中断的阈值,设置之后,则优先级数值(数值越大优先级越低)大于等于此值的中断全部关闭,0 则表示不关闭任何中断
如果使用primask来关闭中断,则应用程序的所有逻辑都无法执行;如果使用basepri来关闭中断,则可以只关闭 RTOS 应用控制下的中断,降低其他裸机的中断延迟。
一般可以将中断优先级按照如下设置:
- 0-4:用于裸机程序,不受 RTOS 的管理,也不可调用 RTOS 中的 API,
裸机程序的时钟中断优先级设置为 4,其他中断则按照需求设置在这个区间内即可 - 5-15:用于 RTOS 应用,受 RTOS 管理,可以使用 RTOS 中的 API,
RTOS时钟比如滴答定时器中断优先级设置为15,其他中断则按照需求设置在这个区间内即可
强烈推荐。
threadx中已经做了相关的实现,只需要配置打开即可。在tx_port.h中,能看到如下这段。
__attribute__( ( always_inline ) ) static inline unsigned int __get_interrupt_posture(void)
{
unsigned int posture;
#ifdef TX_PORT_USE_BASEPRI
__asm__ volatile ("MRS %0, BASEPRI ": "=r" (posture));
#else
__asm__ volatile ("MRS %0, PRIMASK ": "=r" (posture));
#endif
return(posture);
}
#ifdef TX_PORT_USE_BASEPRI
__attribute__( ( always_inline ) ) static inline void __set_basepri_value(unsigned int basepri_value)
{
__asm__ volatile ("MSR BASEPRI,%0 ": : "r" (basepri_value) : "memory");
}
#else
__attribute__( ( always_inline ) ) static inline void __enable_interrupts(void)
{
__asm__ volatile ("CPSIE i": : : "memory");
}
#endif
__attribute__( ( always_inline ) ) static inline void __restore_interrupt(unsigned int int_posture)
{
#ifdef TX_PORT_USE_BASEPRI
__set_basepri_value(int_posture);
__asm__ volatile ("" : : : "memory");
#else
__asm__ volatile ("MSR PRIMASK,%0": : "r" (int_posture): "memory");
#endif
}
__attribute__( ( always_inline ) ) static inline unsigned int __disable_interrupts(void)
{
unsigned int int_posture;
int_posture = __get_interrupt_posture();
#ifdef TX_PORT_USE_BASEPRI
__set_basepri_value(TX_PORT_BASEPRI);
#else
__asm__ volatile ("CPSID i" : : : "memory");
#endif
return(int_posture);
}
__attribute__( ( always_inline ) ) static inline void _tx_thread_system_return_inline(void)
{
unsigned int interrupt_save;
/* Set PendSV to invoke ThreadX scheduler. */
*((volatile ULONG *) 0xE000ED04) = ((ULONG) 0x10000000);
__asm__ volatile ("dsb 0xF \n isb 0xF " : : : "memory");
if (__get_ipsr_value() == 0)
{
interrupt_save = __get_interrupt_posture();
#ifdef TX_PORT_USE_BASEPRI
__set_basepri_value(0);
#else
__enable_interrupts();
#endif
__restore_interrupt(interrupt_save);
}
}
#define TX_INTERRUPT_SAVE_AREA UINT interrupt_save;
#define TX_DISABLE interrupt_save = __disable_interrupts();
#define TX_RESTORE __restore_interrupt(interrupt_save);其中TX_DISABLE和TX_RESTORE就是应用中使用的关闭和打开中断接口,他调用的函数中有宏定义TX_PORT_USE_BASEPRI,因此我们只需要使能这个定义就可以了。
但是他关闭的中断优先级为多少呢?这个文件中也有一段描述。
/* By default, ThreadX for Cortex-M uses the PRIMASK register to enable/disable interrupts.
If using BASEPRI is desired, define the following two symbols for both c and assembly files:
TX_PORT_USE_BASEPRI - This tells ThreadX to use BASEPRI instead of PRIMASK.
TX_PORT_BASEPRI = (priority_mask << (8 - number_priority_bits)) - this defines the maximum priority level to mask.
Any interrupt with a higher priority than priority_mask will not be masked, thus the interrupt will run.
*/结论:
- 默认使用
PRIMASK来关闭中断,定义TX_PORT_USE_BASEPRI就可以使用BASEPRI来屏蔽中断 - 屏蔽的优先级使用
TX_PORT_BASEPRI定义,高于此优先级的中断不会被屏蔽
因此我们添加如下两行即可,就可以实现只屏蔽优先级为 5-15 中断的作用
#define TX_PORT_USE_BASEPRI // 使用basepri寄存器屏蔽中断
#define TX_PORT_BASEPRI (5 << 4) // 0-4用于裸机程序 5-15用于rtos程序
4. RTOS组件
和所有的RTOS一样,就那几个
- 信号量
- 互斥量
- 消息队列
- 事件标志组
- 定时器组
4.1 信号量
接口
// 定义
TX_SEMAPHORE sem;
// 初始化(对象 名称 初始值)
tx_semaphore_create(&sem, "test sem", 0);
// 释放信号量
tx_semaphore_put(&sem);
// 获取信号量(TX_NO_WAIT:立即返回不等待 TX_WAIT_FOREVER:一直等待 其他:节拍数)
UINT status = tx_semaphore_get(&sem, TX_WAIT_FOREVER);
if(status == TX_SUCCESS)
{
led_write(0, 1);
}可以在中断中释放,别的没啥。
4.2 互斥量
如果不使能优先级继承,则等同于初始值为 1 且最大只能为 1 的信号量
接口
// 定义
TX_MUTEX mux;
// 初始化(对象 名称 是否启用优先级继承)
// TX_NO_INHERIT:不支持优先级继承
// TX_INHERIT:支持优先级继承
tx_mutex_create(&mux,"test mux", TX_NO_INHERIT);
// 获取信号量(TX_NO_WAIT:立即返回不等待 TX_WAIT_FOREVER:一直等待 其他:节拍数)
tx_mutex_get(&bsp_print_mux, TX_WAIT_FOREVER);
// 释放信号量
tx_mutex_put(&bsp_print_mux);比如printf打印
// printf互斥
static TX_MUTEX bsp_print_mux;
int printf_init(void)
{
tx_mutex_create(&bsp_print_mux,"bsp print mux", TX_NO_INHERIT);
return 0;
}
void bsp_printf(const char *fmt, ...)
{
// 特别注意,如果printf的变量较多,注意此局部变量的大小是否够用
char buf_str[200 + 1];
va_list v_args;
// 格式化数据
va_start(v_args, fmt);
(void)vsnprintf((char *)&buf_str[0], (size_t)sizeof(buf_str), (char const *) fmt, v_args);
va_end(v_args);
/* 互斥操作 */
tx_mutex_get(&bsp_print_mux, TX_WAIT_FOREVER);
printf("%s", buf_str);
tx_mutex_put(&bsp_print_mux);
}可以在中断中释放,别的没啥。
4.3 消息队列
这个稍微有点特殊的是,大小和长度都有限制,并且还需要自己创建缓冲区。
接口
// 创建
TX_QUEUE queue; // 创建队列
uint32_t queue_buf[10]; // 定义队列使用的缓冲区
// 初始化队列(对象 名称 元素大小 缓冲区指针 缓冲区长度)
/*
元素大小的单位是32bit,也就是4字节,范围是1-16,也就是队列中的每个元素最大为4*16=64字节
缓冲区必须要4字节对齐,因此建议定义为uint32_t
缓冲区长度单位是8bit,也就是1字节,因此可以直接使用sizeof
*/
tx_queue_create(&queue, "queue",
1, /* 每次消息队列发送的数据大小,单位32bit,范围1-16 */
(VOID *)queue_buf, sizeof(queue_buf)); /* 消息队列大小,单位字节 */
// 往队列放数据(对象 元素地址 等待节拍数)
uint32_t num = 0;
UINT status = tx_queue_send(&queue, &num, 2000);
if(status == TX_SUCCESS)
{
bsp_printf(" send OK %d \r\n", num);
}
// 从队列取数据(对象 存数据地址 等待节拍数)
uint32_t rev_num;
UINT status = tx_queue_receive(&queue, &rev_num, 1100);
if(status == TX_SUCCESS)
{
bsp_printf("recv_num = %d \r\n", rev_num);
}元素大小定义,在tx_api.h中默认定义了最大元素大小,可以自己在tx_port.h或者tx_user.h中重新定义。
#ifndef TX_QUEUE_MESSAGE_MAX_SIZE
#define TX_QUEUE_MESSAGE_MAX_SIZE TX_16_ULONG
#endif存放结构体,比如 can 报文
// 要存入的对象
typedef struct _can_message
{
uint32_t id;
uint32_t dlc;
uint8_t data[8];
} can_message_t;
// 计算can结构体需要占用几个u32空间 向上取整
#define CAN_MSG_WORDS ((sizeof(can_message_t) + sizeof(ULONG) - 1) / sizeof(ULONG))
TX_QUEUE can_queue; // 队列
#define CAN_QUEUE_DEPTH 32 // 队列长度
static ULONG can_queue_buf[CAN_QUEUE_DEPTH * CAN_MSG_WORDS]; // 申请的空间
// 初始化
tx_queue_create(&can_queue, "CAN Queue",
CAN_MSG_WORDS,
(VOID *)can_queue_buf,
sizeof(can_queue_buf));
// 发送
can_message_t message;
UINT status = tx_queue_send(&can_queue, &message, TX_NO_WAIT);
if(status != TX_SUCCESS)
{
bsp_printf(" send Failed %d \r\n", status);
}
// 接收
can_message_t message;
UINT status = tx_queue_receive(&can_queue, &message, 1100);
if(status == TX_SUCCESS)
{
bsp_printf("id=%x, dlc=%d, data=%d %d %d %d %d %d %d %d \r\n",
message.id, message.dlc,
message.data[0], message.data[1], message.data[2], message.data[3],
message.data[4], message.data[5], message.data[6], message.data[7]);
}4.4 事件任务组
接口
#define BIT_0 (1 << 0)
#define BIT_1 (1 << 1)
#define BIT_ALL (BIT_0 | BIT_1)
// 创建
TX_EVENT_FLAGS_GROUP event_group;
// 初始化
tx_event_flags_create(&event_group, "EventGroupName");
// 设置(对象 bit位 与或操作)
// #define TX_AND ((UINT) 2)
// #define TX_OR ((UINT) 0)
tx_event_flags_set(&event_group, BIT_0, TX_OR); // BIT_1与当前状态或,即bit1置1,其他不变
tx_event_flags_set(&event_group, BIT_1, TX_AND);// BIT_1与当前状态与,即bit1不变,其他清空
// 获取(对象 等待的位 等待条件 获取到的状态 等待时间)
// #define TX_AND ((UINT) 2) // 等待的位必须都为1
// #define TX_AND_CLEAR ((UINT) 3) // 等待的位必须都为1,用完后清除
// #define TX_OR ((UINT) 0) // 等待的位任意为1
// #define TX_OR_CLEAR ((UINT) 1) // 等待的位任意为1,用完后清除对应的1
ULONG actual_events;
UINT status = tx_event_flags_get(&event_group, /* 事件标志控制块 */
BIT_ALL, /* 等待标志 */
TX_OR_CLEAR , /* 等待任意bit满足即可 */
&actual_events, /* 获取实际值 */
TX_WAIT_FOREVER);/* 永久等待 */
if(status == TX_SUCCESS)
{
switch (actual_events)
{
case BIT_ALL:
/* 接收到bit1和bit0都被设置的消息 */
bsp_printf("0 1 \r\n");
break;
case BIT_0:
/* 接收到bit0被设置的消息 */
bsp_printf("0 \r\n");
break;
case BIT_1:
/* 接收到bit1被设置的消息 */
bsp_printf("1 \r\n");
break;
default:
break;
}
}4.3 定时器组
接口
// 创建
TX_TIMER timer;
// 超时回调函数
void timer_callback(ULONG thread_input)
{
led_toggle(0);
}
// 初始化
tx_timer_create(&timer, "App Timer",
timer_callback, 0,
100, // 首次定时器计时节拍数
200, // 后续定时器计时节拍数 0表示单次定时器
TX_AUTO_ACTIVATE);// TX_AUTO_ACTIVATE:激活 TX_NO_ACTIVATE:不激活
也可以初始化之后不激活,在需要的时候激活
其他常用接口
_txe_timer_change:修改时间_txe_timer_activate:激活_txe_timer_deactivate:不激活
5. 任务通知(事件链)
默认不打开这个宏定义就是使能的
// #define TX_DISABLE_NOTIFY_CALLBACKS
事件链支持的对象包括
- 队列:有消息进入队列时触发
- 信号量:有信号量释放时触发
- 事件标志组:事件表示被设置时触发
demo
// 定义被监听的对象 1个队列 1个信号量 1个事件标志组
#define QUEUE_SIZE 10
ULONG queue_memory[QUEUE_SIZE];
TX_QUEUE queue; // 被监听的队列
TX_SEMAPHORE data_semaphore; // 被监听的数据信号量
TX_EVENT_FLAGS_GROUP event_flags; // 被监听的事件标志组
TX_SEMAPHORE notification_semaphore; // 用于唤醒接收者线程的信号量
// 初始化所有对象
// 1. 创建唤醒信号量(初始0)
tx_semaphore_create(¬ification_semaphore, "NotifySem", 0);
// 2. 初始化被监听对象
tx_semaphore_create(&data_semaphore, "DataSem", 0);
tx_event_flags_create(&event_flags, "EventGroup");
tx_queue_create(&queue, "Q1", TX_1_ULONG, queue_memory, sizeof(queue_memory));
// 3. 注册所有通知回调
tx_queue_send_notify(&queue, queue_notify);
tx_semaphore_put_notify(&data_semaphore, semaphore_notify);
tx_event_flags_set_notify(&event_flags, event_flags_notify);
// 对象的回调函数(不可阻塞)
void queue_notify(TX_QUEUE *queue_ptr)
{
tx_semaphore_put(¬ification_semaphore);
}
void semaphore_notify(TX_SEMAPHORE *semaphore_ptr)
{
tx_semaphore_put(¬ification_semaphore);
}
void event_flags_notify(TX_EVENT_FLAGS_GROUP *group_ptr)
{
tx_semaphore_put(¬ification_semaphore);
}
// 需要一个任务进行处理
void thread_0_entry(ULONG input)
{
UINT status;
ULONG received_msg;
ULONG actual_flags;
while (1)
{
// 1. 等待唤醒信号量(可由任何回调释放)
tx_semaphore_get(¬ification_semaphore, TX_WAIT_FOREVER);
// 2. 被唤醒后,检查所有事件源(非阻塞轮询)
printf("Receiver: Woken up! Checking all sources...\n");
// 检查队列
while (tx_queue_receive(&queue, &received_msg, TX_NO_WAIT) == TX_SUCCESS)
{
printf("Receiver: Got 0x%02lX from Queue 1\n", received_msg);
}
// 检查信号量(获取所有可用信号量)
while (tx_semaphore_get(&data_semaphore, TX_NO_WAIT) == TX_SUCCESS)
{
printf("Receiver: Got a data_semaphore\n");
}
// 检查事件标志组(获取并清除标志位0)
status = tx_event_flags_get(&event_flags, 0x01, TX_OR_CLEAR, &actual_flags, TX_NO_WAIT);
if (status == TX_SUCCESS)
{
printf("Receiver: Got event flag 0x%08lX\n", actual_flags);
}
}
}
// 另一个线程演示发送消息
void thread_1_entry(ULONG thread_input)
{
(void)thread_input;
bsp_printf("thread_1_entry running \r\n");
ULONG message = 0x01;
ULONG sem_count = 0;
ULONG flag_value = 0x01;
while (1)
{
// 发送到队列
tx_thread_sleep(1000);
tx_queue_send(&queue, &message, TX_WAIT_FOREVER);
// bsp_printf("Sender 1: Sent 0x%02lX to Queue 1\n", message);
message++;
// 每3次循环释放一次信号量
sem_count++;
if (sem_count % 3 == 0)
{
tx_semaphore_put(&data_semaphore);
// bsp_printf("Sender: Put data_semaphore\n");
}
// 每5次循环设置一次事件标志
if (sem_count % 5 == 0)
{
tx_event_flags_set(&event_flags, flag_value, TX_OR);
// bsp_printf("Sender: Set event flag 0x%02lX\n", flag_value);
flag_value <<= 1; // 循环移位演示不同标志位
if (flag_value > 0x80) flag_value = 0x01;
}
}
}6. 动态内存管理
分两种,内存块和内存池
| 特性 | TX_BLOCK_POOL (块池) | TX_BYTE_POOL (字节池) |
|---|---|---|
| 分配粒度 | 固定大小 | 可变大小 |
| 分配速度 | 极快且确定(O(1)) | 较慢且不确定(O(n)) |
| 内存碎片 | 无外部碎片 | 存在外部碎片风险 |
| 内存利用率 | 可能产生内部碎片 | 利用率高,按需分配 |
| 适用场景 | 固定大小对象、高频分配释放 | 变长数据、临时缓冲、字符串 |
内存块
// 定义内存块(里面分为多个块 大小相等)
TX_BLOCK_POOL AppBlock;
uint32_t AppBlockBuf[1024]; // 定义缓存区域
// 初始化
// 总的内存单元个数是 total blocks = (total bytes) / (block_size + sizeof(void *))
tx_block_pool_create(&AppBlock, "AppBlock",
4, /* 内存单元大小,单位字节 */
(VOID *)AppBlockBuf, /* 内存块地址,需要保证4字节对齐 */
sizeof(AppBlockBuf));/* 内存块总大小,单位字节 */
// 申请一块区域(4字节)
uint8_t *BlockPtr;
status = tx_block_allocate(&AppBlock, (VOID **)&BlockPtr, TX_NO_WAIT);
if(status == TX_SUCCESS)
{
bsp_printf("AppBlock内存块申请成功\r\n");
}
// 释放一块区域
status = tx_block_release(BlockPtr);
if(status == TX_SUCCESS)
{
bsp_printf("AppBlock内存块释放成功\r\n");
}内存池
// 定义
TX_BYTE_POOL AppPool;
uint32_t AppPoolBuf[1024];// 定义缓存区域
// 创建
tx_byte_pool_create(&AppPool, "AppPool",
(VOID *)AppPoolBuf, /* 内存池地址,需要保证4字节对齐 */
sizeof(AppBlockBuf)); /* 内存池大小 */
// 申请
ULONG available;
status = tx_byte_allocate(&AppPool, (VOID **)&PoolPtr, 77, TX_NO_WAIT);
if(status == TX_SUCCESS)
{
printf("AppPool内存池申请成功\r\n");
tx_byte_pool_info_get(&AppPool, TX_NULL, &available, TX_NULL, TX_NULL, TX_NULL, TX_NULL);
printf("内存池剩余大小 = %d字节\r\n", (int)available);
}
// 释放
status = tx_byte_release(PoolPtr);
if(status == TX_SUCCESS)
{
printf("AppPool内存池释放成功\r\n");
tx_byte_pool_info_get(&AppPool, TX_NULL, &available, TX_NULL, TX_NULL, TX_NULL, TX_NULL);
printf("内存池剩余大小 = %d字节\r\n", (int)available);
}使用内存池创建任务
源码中的demo也是使用内存池创建的线程,这里也尝试使用这个方式创建,只不过使用了循环的方式。
// 栈空间 分配给所有任务使用
#define DEMO_BYTE_POOL_SIZE 9120
UCHAR memory_area[DEMO_BYTE_POOL_SIZE];
TX_BYTE_POOL byte_pool_0;
// 任务结构体定义
typedef struct _task
{
TX_THREAD tcb; /* ThreadX 任务控制块 */
const char *name; /* 任务名称 */
void (*entry)(ULONG);/* 任务入口函数 (ThreadX 参数为 ULONG) */
ULONG entry_input; /* 入口参数 */
uint8_t *stack_ptr; /* 任务栈指针 */
uint32_t stack_size; /* 任务栈大小 (字节) */
uint32_t priority; /* 任务优先级 */
uint32_t tick; /* 时间片 (ThreadX 中对应 time_slice) */
} task_t;
typedef task_t *task_ptr;
task_t tasks[] =
{
{{0}, "thread_1_entry", thread_1_entry, NULL, NULL, 1024, 4, 0},
{{0}, "thread_0_entry", thread_0_entry, NULL, NULL, 1024, 5, 0},
};
void tx_application_define(void *first_unused_memory)
{
UINT status;
CHAR *pointer = TX_NULL;
// 创建内存池
tx_byte_pool_create(&byte_pool_0, "byte pool 0", memory_area, DEMO_BYTE_POOL_SIZE);
for (int i = 0; i < (sizeof(tasks) / sizeof(task_t)); i++)
{
// 获取内存池剩余空间
ULONG available;
tx_byte_pool_info_get(&byte_pool_0, TX_NULL, &available, TX_NULL, TX_NULL, TX_NULL, TX_NULL);
if(available < tasks[i].stack_size)
{
// 空间不够 提前退出
bsp_printf("%s is not enough = %d. \r\n", byte_pool_0.tx_byte_pool_name, available);
break;
}
// 根据任务栈大小申请一块内存
tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, tasks[i].stack_size, TX_NO_WAIT);
// 保存任务栈地址
tasks[i].stack_ptr = (uint8_t *)pointer;
// 创建任务
status = tx_thread_create(&tasks[i].tcb, /* 任务控制块指针 */
(CHAR *)tasks[i].name, /* 任务名称 */
tasks[i].entry, /* 任务入口函数 */
tasks[i].entry_input, /* 入口参数 */
tasks[i].stack_ptr, /* 栈起始地址 */
tasks[i].stack_size, /* 栈大小 */
tasks[i].priority, /* 优先级 */
tasks[i].priority, /* 抢占阈值 (通常与优先级相同) */
tasks[i].tick, /* 时间片 */
TX_AUTO_START); /* 自动启动 */
if (status != TX_SUCCESS)
{
bsp_printf("%s create failed! status = %d. \r\n", tasks[i].name, status);
}
}
bsp_printf("------------------------------------------------ \r\n");
/* 遍历所有任务,打印栈地址范围 */
/* 以故障任务为起点和终点,确保遍历安全终止 */
bsp_printf("All Task Stack Ranges:\r\n");
TX_THREAD *p_tcb = &tasks[0].tcb;
do {
bsp_printf(" %-20s [0x%08X - 0x%08X] (Size: %d)\r\n",
p_tcb->tx_thread_name,
(UINT)p_tcb->tx_thread_stack_start,
(UINT)p_tcb->tx_thread_stack_end,
p_tcb->tx_thread_stack_size);
p_tcb = p_tcb->tx_thread_created_next;
} while (p_tcb != &tasks[0].tcb);
bsp_printf("------------------------------------------------ \r\n");
}7. 低功耗
暂时未用到
8. 异常处理
8.1 运行时间统计
工程中添加文件
threadx-6.5.1.202602a_rel\utility\execution_profile_kit\tx_execution_profile.cthreadx-6.5.1.202602a_rel\utility\execution_profile_kit\tx_execution_profile.h
keil 配置中添加头文件宏定义
- ,TX_CORTEX_M_EPK
- TX_EXECUTION_PROFILE_ENABLE
keil 配置中添加 ASM 头文件定义
- TX_EXECUTION_PROFILE_ENABLE
用到的接口定义在头文件中,4个get和4个reset接口,见名知意。
UINT _tx_execution_thread_time_reset(struct TX_THREAD_STRUCT *thread_ptr);
UINT _tx_execution_thread_total_time_reset(void);
UINT _tx_execution_isr_time_reset(void);
UINT _tx_execution_idle_time_reset(void);
UINT _tx_execution_thread_time_get(struct TX_THREAD_STRUCT *thread_ptr, EXECUTION_TIME *total_time);
UINT _tx_execution_thread_total_time_get(EXECUTION_TIME *total_time);
UINT _tx_execution_isr_time_get(EXECUTION_TIME *total_time);
UINT _tx_execution_idle_time_get(EXECUTION_TIME *total_time);测试跑起来还有点问题
8.2 栈溢出检测
系统自带的栈溢出检测,tx_port.h中有如下定义
#ifndef TX_MISRA_ENABLE
#ifdef TX_ENABLE_STACK_CHECKING
#undef TX_DISABLE_STACK_FILLING
#endif
#endif自己在tx_user.h中打开宏定义#define TX_ENABLE_STACK_CHECKING使能
/* Determine whether or not stack checking is enabled. By default, ThreadX stack checking is
disabled. When the following is defined, ThreadX thread stack checking is enabled. If stack
checking is enabled (TX_ENABLE_STACK_CHECKING is defined), the TX_DISABLE_STACK_FILLING
define is negated, thereby forcing the stack fill which is necessary for the stack checking
logic. */
#define TX_ENABLE_STACK_CHECKING // 开启堆栈检查
使用方法
下面测试方法需要关闭编译器优化!!!否则复现不出来!!!
// 创建异常处理回调函数
void my_stack_error_handler(TX_THREAD *thread_ptr)
{
/* 打印故障信息 */
bsp_printf("!!! STACK OVERFLOW DETECTED !!!\r\n");
bsp_printf("Fault Task: %s, Prio: %d. \r\n",
thread_ptr->tx_thread_name,
thread_ptr->tx_thread_priority);
/* 计算故障任务的栈范围 */
bsp_printf("Fault Stack Range: [0x%08X - 0x%08X], Stack Size: %ld bytes. \r\n",
(UINT)thread_ptr->tx_thread_stack_start,
(UINT)thread_ptr->tx_thread_stack_end,
(UINT)thread_ptr->tx_thread_stack_size);
bsp_printf("Current Used: %d, Max Used: %d. \r\n",
(UINT)(thread_ptr->tx_thread_stack_end - thread_ptr->tx_thread_stack_ptr),
(UINT)(thread_ptr->tx_thread_stack_end - thread_ptr->tx_thread_stack_highest_ptr));
bsp_printf("------------------------------------------------\r\n");
/* 遍历所有任务,打印栈地址范围 */
/* 以故障任务为起点和终点,确保遍历安全终止 */
bsp_printf("All Task Stack Ranges:\r\n");
TX_THREAD *p_tcb = thread_ptr; // 故障任务本身
do {
bsp_printf(" %-20s [0x%08X - 0x%08X] (Size: %d)\r\n",
p_tcb->tx_thread_name,
(UINT)p_tcb->tx_thread_stack_start,
(UINT)p_tcb->tx_thread_stack_end,
p_tcb->tx_thread_stack_size);
p_tcb = p_tcb->tx_thread_created_next;
} while (p_tcb != thread_ptr); // 回到故障任务时停止
bsp_printf("------------------------------------------------\r\n");
while(1)
{
// 可选复位
}
}
// 在tx_application_define中绑定回调
tx_thread_stack_error_notify(my_stack_error_handler);
// 在任务中模拟栈溢出
// 任务0
#define THREAD_0_MEM 1024
static TX_THREAD thread_0;
static UCHAR thread_0_memory[THREAD_0_MEM];
void thread_0_entry(ULONG input)
{
UINT status;
tx_thread_sleep(1000);
/* 故意制造栈溢出:声明 2048 字节数组,但栈只有 1024 字节 */
volatile char big_array[2048];
/* 访问数组以触发溢出检测 */
// 其实一旦数组创建 就已经能触发栈溢出了 执行不到这
big_array[2047] = 0xAA;
while(1)
{
tx_thread_sleep(1000);
}
}运行结果如下,可以看到是定时器任务上报了栈溢出错误。
!!! STACK OVERFLOW DETECTED !!!
Fault Task: System Timer Thread, Prio: 0.
Fault Stack Range: [0x20000314 - 0x2000070F], Stack Size: 1020 bytes.
Current Used: 75, Max Used: 536872709.
------------------------------------------------
All Task Stack Ranges:
System Timer Thread [0x20000314 - 0x2000070F] (Size: 1020)
thread_0 [0x200008E0 - 0x20000CDB] (Size: 1020)
thread_1 [0x20000D94 - 0x2000158F] (Size: 2044)为什么不是thread_0呢?
在 Cortex-M 架构中,栈是向下增长的。使用debug模式查看内存分布。
在创建数组前,定时器任务栈除了开头(高地址)处有部分不是没有被0xEF填充,为用到的部分都被填充了。

继续往下运行到创建数组,立刻跳转到栈溢出检测,可以看到栈顶内存被修改了,不是0xEF了,因此检测出栈溢出异常。

ThreadX的栈溢出检测,是通过将栈空间使用0xEF填充,并检测栈顶数据是否被修改实现的。
显然这种方式其实并不靠谱,在这里甚至都看不出来到底被谁改了。