论坛原始地址(持续更新):http://www.armbbs.cn/forum.php?mod=viewthread&tid=99514
对于初学者,特别是对于没有RTOS基础的同学来说,了解ThreadX的任务管理非常重要,了解任务管理的目的就是让初学者从裸机的,单任务编程过渡到带OS的,多任务编程上来。搞清楚了这一点,那么ThreadX学习就算入门了。
9.1 单任务系统
9.2 多任务系统
9.3 ThreadX的任务栈设置
9.4 ThreadX的系统栈设置
9.5 ThreadX的任务状态
9.6 ThreadX启动流程图示
9.7 ThreadX的空闲任务
9.8 ThreadX的启动函数tx_kernal_enter
9.9 ThreadX的任务创建函数tx_threadx_create
9.10 ThreadX的任务删除函数tx_threadx_delete
9.11 ThreadX的任务挂起函数tx_threadx_suspend
9.12 ThreadX的任务恢复函数tx_threadx_resume
9.13 ThreadX的任务复位函数tx_threadx_reset
9.14 ThreadX的任务终止函数tx_threadx_terminate
9.15 实验例程说明
9.16 总结
学习多任务系统之前,我们先来回顾下单任务系统的编程框架,即裸机时的编程框架。裸机编程主要是采用超级循环(super-loops)系统,又称前后台系统。应用程序是一个无限的循环,循环中调用相应的函数完成相应的操作,这部分可以看做后台行为;中断服务程序处理异步事件,这部分可以看做是前台行为。后台也可以叫做任务级,前台也叫作中断级。
对于前后台系统的编程思路主要有以下两种方式:
对于一些简单的应用,处理器可以查询数据或者消息是否就绪,就绪后进行处理,然后再等待,如此循环下去。对于简单的任务,这种方式简单易处理。但大多数情况下,需要处理多个接口数据或者消息,那就需要多次处理,如下面的流程图所示:
用查询方式处理简单的应用,效果比较好,但是随着工程的复杂,采用查询方式实现的工程就变得很难维护,同时,由于无法定义查询任务的优先级,这种查询方式会使得重要的接口消息得不到及时响应。比如程序一直在等待一个非紧急消息就绪,如果这个消息后面还有一个紧急的消息需要处理,那么就会使得紧急消息长时间得不到执行。
对于查询方式无法有效执行紧急任务的情况,采用中断方式就有效地解决了这个问题,下面是中断方式简单的流程图:
采用中断和查询结合的方式可以解决大部分裸机应用,但随着工程的复杂,裸机方式的缺点就暴露出来了:
1、 必须在中断(ISR)内处理时间关键运算:
2、 超级循环和ISR之间的数据交换是通过全局共享变量进行的:
3、 超级循环可以与系统计时器轻松同步,但:
4、 超级循环使得应用程序变得非常复杂,因此难以扩展:
针对这些情况,使用多任务系统就可以解决这些问题了。下面是一个多任务系统的流程图:
多任务系统或者说RTOS的实现,重点就在这个调度器上,而调度器的作用就是使用相关的调度算法来决定当前需要执行的任务。如上图所示的那样,创建了任务并完成OS初始化后,就可以通过调度器来决定任务A,任务B和任务C的运行,从而实现多任务系统。另外需要初学者注意的是,这里所说的多任务系统同一时刻只能有一个任务可以运行,只是通过调度器的决策,看起来像所有任务同时运行一样。为了更好的说明这个问题,再举一个详细的运行例子,运行条件如下:
下图所示是任务的运行过程,其中横坐标是任务优先级由低到高排列,纵坐标是运行时间,时间刻度有小到大。
(1) 启动RTOS,首先执行高优先级任务(tx_kernel_enter)。
(2) 高优先级任务等待事件标志(tx_event_flags_gets)被阻塞,低优先级任务得到执行。
(3) 低优先级任务执行的过程中产生USB中断,进入USB中断服务程序。
(4) 退出USB中断复位程序,回到低优先级任务继续执行。
(5) 低优先级任务执行过程中产生串口接收中断,进入串口接收中断服务程序。
(6) 退出串口接收中断复位程序,并发送事件标志设置消息(tx_event_flags_set),被阻塞的高优先级任务就会重新进入就绪状态,这个时候高优先级任务和低优先级任务都在就绪态,抢占式调度器就会让高优先级的任务先执行,所以此时就会进入高优先级任务。
(7) 高优先级任务由于等待事件标志(tx_event_flags_gets)会再次被阻塞,低优先级任务开始继续执行。
(8) 低优先级任务调用函数tx_thread_sleep,低优先级任务被挂起,从而空闲任务得到执行。
(9) 空闲任务执行期间发生滴答定时器中断,进入滴答定时器中断服务程序。
(10) 退出滴答定时器中断,由于低优先级任务延时时间到,低优先级任务继续执行。
(11) 低优先级任务再次调用延迟函数tx_thread_sleep,低优先级任务被挂起,从而切换到空闲任务。空闲任务得到执行。
通过上面实例的讲解,大家应该对多任务系统完整的运行过程有了一个全面的认识。随着教程后面对调度器,任务切换等知识点的讲解,大家会对这个运行过程有更深刻的理解。
不管是裸机编程还是RTOS编程,栈的分配大小都非常重要。局部变量,函数调用时的现场保护和返回地址,函数的形参,进入中断函数前和中断嵌套等都需要栈空间,栈空间定义小了会造成系统崩溃。
裸机的情况下,用户可以在这里配置栈大小:
不同于裸机编程,在RTOS下,每个任务都有自己的栈空间。对于ThreadX来说,支持动态内存分配方式和静态分配方式,本教程全部是静态分配方式。
具体每个任务的栈大小是在创建ThreadX的任务时进行设置的:
/*
*********************************************************************************************************
* 任务栈大小,单位字节
*********************************************************************************************************
*/
#define APP_CFG_TASK_START_STK_SIZE 4096u
#define APP_CFG_TASK_MsgPro_STK_SIZE 4096u
#define APP_CFG_TASK_COM_STK_SIZE 4096u
#define APP_CFG_TASK_USER_IF_STK_SIZE 4096u
#define APP_CFG_TASK_IDLE_STK_SIZE 1024u
#define APP_CFG_TASK_STAT_STK_SIZE 1024u
/*
*********************************************************************************************************
* 函 数 名: tx_application_define
* 功能说明: ThreadX专用的任务创建,通信组件创建函数
* 形 参: first_unused_memory 未使用的地址空间
* 返 回 值: 无
*********************************************************************************************************
*/
void tx_application_define(void *first_unused_memory)
{
/**************创建启动任务*********************/
tx_thread_create(&AppTaskStartTCB, /* 任务控制块地址 */
"App Task Start", /* 任务名 */
AppTaskStart, /* 启动任务函数地址 */
0, /* 传递给任务的参数 */
&AppTaskStartStk[0], /* 堆栈基地址 */
APP_CFG_TASK_START_STK_SIZE, /* 堆栈空间大小 */
APP_CFG_TASK_START_PRIO, /* 任务优先级*/
APP_CFG_TASK_START_PRIO, /* 任务抢占阀值 */
TX_NO_TIME_SLICE, /* 不开启时间片 */
TX_AUTO_START); /* 创建后立即启动 */
/**************创建统计任务*********************/
tx_thread_create(&AppTaskStatTCB, /* 任务控制块地址 */
"App Task STAT", /* 任务名 */
AppTaskStat, /* 启动任务函数地址 */
0, /* 传递给任务的参数 */
&AppTaskStatStk[0], /* 堆栈基地址 */
APP_CFG_TASK_STAT_STK_SIZE, /* 堆栈空间大小 */
APP_CFG_TASK_STAT_PRIO, /* 任务优先级*/
APP_CFG_TASK_STAT_PRIO, /* 任务抢占阀值 */
TX_NO_TIME_SLICE, /* 不开启时间片 */
TX_AUTO_START); /* 创建后立即启动 */
/**************创建空闲任务*********************/
tx_thread_create(&AppTaskIdleTCB, /* 任务控制块地址 */
"App Task IDLE", /* 任务名 */
AppTaskIDLE, /* 启动任务函数地址 */
0, /* 传递给任务的参数 */
&AppTaskIdleStk[0], /* 堆栈基地址 */
APP_CFG_TASK_IDLE_STK_SIZE, /* 堆栈空间大小 */
APP_CFG_TASK_IDLE_PRIO, /* 任务优先级*/
APP_CFG_TASK_IDLE_PRIO, /* 任务抢占阀值 */
TX_NO_TIME_SLICE, /* 不开启时间片 */
TX_AUTO_START); /* 创建后立即启动 */
}
实际应用中给任务开辟多大的堆栈空间合适呢,在后面章详细跟大家进行讲解。
上面跟大家讲解了什么是任务栈,这里的系统栈又是什么呢?裸机的情况下,凡是用到栈空间的地方都是在这里配置的栈空间:
在RTOS下,上面截图中设置的栈大小有了一个新的名字叫系统栈空间,而任务栈是不使用这里的空间的。任务栈不使用这里的栈空间,哪里使用这里的栈空间呢?答案就在中断函数和中断嵌套。
对于这个问题,简单的描述如下,更详细的内容待我们讲解ThreadX任务切换和双堆栈指针时再细说。
1、 由于Cortex-M3,M4,M7内核具有双堆栈指针,MSP主堆栈指针和PSP进程堆栈指针,或者叫PSP任务堆栈指针也是可以的。在ThreadX操作系统中,主堆栈指针MSP是给系统栈空间使用的,进程堆栈指针PSP是给任务栈使用的。也就是说,在ThreadX任务中,所有栈空间的使用都是通过PSP指针进行指向的。一旦进入了中断函数以及可能发生的中断嵌套都是用的MSP指针。这个知识点要记住它,当前可以不知道这是为什么,但是一定要记住。
2、 实际应用中系统栈空间分配多大,主要是看可能发生的中断嵌套层数,下面我们就按照最坏执行情况进行考虑,所有的寄存器都需要入栈,此时分为两种情况:
对于Cortex-M3内核和未使用FPU(浮点运算单元)功能的Cortex-M4/M7内核在发生中断时需要将16个通用寄存器全部入栈,每个寄存器占用4个字节,也就是16*4 = 64字节的空间。
可能发生几次中断嵌套就是要64乘以几即可。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
(注:任务执行的过程中发生中断的话,有8个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈)
对于具有FPU(浮点运算单元)功能的Cortex-M4/M7内核,如果在任务中进行了浮点运算,那么在发生中断的时候除了16个通用寄存器需要入栈,还有34个浮点寄存器也是要入栈的,也就是(16+34)*4 = 200字节的空间。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
(注:任务执行的过程中发送中断的话,有8个通用寄存器和18个浮点寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系统栈)
ThreadX有五种不同的线程状态:Ready State就绪态,Suspended State挂起状态,Executing State执行态,Terminated State终止执行态和Complete State完成态。:
当任务处于实际运行状态被称之为执行态,即CPU的使用权被这个任务占用。
处于就绪态的任务是指那些能够运行(没有被挂起),但是当前没有运行的任务,因为同优先级或更高优先级的任务正在运行。
ThreadX的挂起包含了阻塞,即由于等待信号量,消息队列,事件标志组等而处于的状态也是挂起态,
任务调用延迟函数或者对任务进行挂起操作(有专门的挂起函数)也会处于挂起状态。
任务返回的状态称之为完成态,正常情况下每个任务是死循环,独立执行,不会返回。
终止任务执行的状态称之为终止态。
ThreadX启动流程如下:
ThreadX中没有空闲任务,不过用户可以自己创建一个。使用空闲任务主要有以下几个作用:
函数原型:
#define tx_kernel_enter _tx_initialize_kernel_enter
VOID _tx_initialize_kernel_enter(VOID)
函数描述:
函数tx_kernel_enter用于启动ThreadX调度器,即启动ThreadX的多任务执行。 此函数是ThreadX初始化阶段第1个调用的函数。
此函数依次调用了下面四个主要函数:
注意事项:
使用举例:
/*
*********************************************************************************************************
* 函 数 名: main
* 功能说明: 标准c程序入口。
* 形 参: 无
* 返 回 值: 无
*********************************************************************************************************
*/
int main(void)
{
/* HAL库,MPU,Cache,时钟等系统初始 */
System_Init();
/* 内核开启前关闭HAL的时间基准 */
HAL_SuspendTick();
/* 进入ThreadX内核 */
tx_kernel_enter();
while(1);
}
函数原型:
#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)))
UINT _txe_thread_create(TX_THREAD *thread_ptr,
CHAR *name_ptr,
VOID (*entry_function)(ULONG id),
ULONG entry_input,
VOID *stack_start,
ULONG stack_size,
UINT priority,
UINT preempt_threshold,
ULONG time_slice,
UINT auto_start,
UINT thread_control_block_size)
函数描述:
函数tx_thread_create用于实现ThreadX操作系统的任务创建,并且还可以自定义任务栈的大小。
函数形参:
如果指定了TX_DONT_START,则应用程序以后必须调用tx_thread_resume才能运行线程。
注意事项:
使用举例:
/*
*********************************************************************************************************
* 任务优先级,数值越小优先级越高
*********************************************************************************************************
*/
#define APP_CFG_TASK_START_PRIO 2u
/*
*********************************************************************************************************
* 任务栈大小,单位字节
*********************************************************************************************************
*/
#define APP_CFG_TASK_START_STK_SIZE 4096u
/*
*********************************************************************************************************
* 静态全局变量
*********************************************************************************************************
*/
static TX_THREAD AppTaskStartTCB;
static uint64_t AppTaskStartStk[APP_CFG_TASK_START_STK_SIZE/8];
tx_thread_create(&AppTaskStartTCB, /* 任务控制块地址 */
"App Task Start", /* 任务名 */
AppTaskStart, /* 启动任务函数地址 */
0, /* 传递给任务的参数 */
&AppTaskStartStk[0], /* 堆栈基地址 */
APP_CFG_TASK_START_STK_SIZE, /* 堆栈空间大小 */
APP_CFG_TASK_START_PRIO, /* 任务优先级*/
APP_CFG_TASK_START_PRIO, /* 任务抢占阀值 */
TX_NO_TIME_SLICE, /* 不开启时间片 */
TX_AUTO_START); /* 创建后立即启动 */
函数原型:
#define tx_thread_delete _txe_thread_delete
UINT _txe_thread_delete(TX_THREAD *thread_ptr)
函数描述:
函数tx_thread_delete用于实现ThreadX操作系统的任务删除。
注意事项:
使用举例:
/* 任务控制块 */
static TX_THREAD AppTaskStartTCB;
tx_thread_delete(&AppTaskCOMTCB);
函数原型:
#define tx_thread_suspend _txe_thread_suspend
UINT _txe_thread_suspend(TX_THREAD *thread_ptr)
函数描述:
函数tx_thread_suspend用于实现ThreadX操作系统的任务挂起。任务也可以挂起自己。挂起后,可以通过tx_thread_resume恢复。
函数形参:
注意事项:
使用举例:
/* 任务控制块 */
static TX_THREAD AppTaskStartTCB;
tx_thread_suspend(&AppTaskCOMTCB);
函数原型:
#define tx_thread_resume _txe_thread_resume
UINT _txe_thread_resume(TX_THREAD *thread_ptr)
函数描述:
函数tx_thread_resume用于实现ThreadX操作系统的任务恢复。另外,此任务还将恢复在没有自动启动的情况下创建的任务。
函数形参:
注意事项:
使用举例:
/* 任务控制块 */
static TX_THREAD AppTaskStartTCB;
tx_thread_resume(&AppTaskCOMTCB);
函数原型:
#define tx_thread_resume _txe_thread_resume
UINT _txe_thread_resume(TX_THREAD *thread_ptr)
#define tx_thread_reset _txe_thread_reset
函数描述:
函数tx_thread_reset用于实现ThreadX操作系统的任务复位。任务必须处于TX_COMPLETED完成态或TX_TERMINATED终止态才能复位。
函数形参:
注意事项:
使用举例:
/* 任务控制块 */
static TX_THREAD AppTaskStartTCB;
tx_thread_reset(&AppTaskCOMTCB);
函数原型:
#define tx_thread_terminate _txe_thread_terminate
UINT _txe_thread_terminate(TX_THREAD *thread_ptr)
函数描述:
函数tx_thread_terminate用于实现ThreadX操作系统的任务终止。该函数终止指定任务,而不管该任务是否被挂起。任务可以调用此函数以终止自身。
函数形参:
注意事项:
使用举例:
/* 任务控制块 */
static TX_THREAD AppTaskStartTCB;
tx_thread_terminate(&AppTaskCOMTCB);
配套例子:
V7-3004_ThreadX Task Control
实验目的:
实验内容:
1、共创建了如下几个任务,通过按下按键K1可以通过串口或者RTT打印任务堆栈使用情况
===================================================
OS CPU Usage = 1.94%
=======================================================
Prio StackSize CurStack MaxStack Taskname
2 4092 383 391 App Task Start
3 4092 543 659 App Msp Pro
4 4092 391 391 App Task UserIF
5 4092 543 659 App Task COM
30 1020 519 519 App Task STAT
31 1020 143 71 App Task IDLE
0 1020 391 391 System Timer Thread
串口软件可以使用SecureCRT或者H7-TOOL RTT查看打印信息。
App Task Start任务 :启动任务,这里用作BSP驱动包处理。
App Task MspPro任务 :消息处理,这里未使用。
App Task UserIF任务 :按键消息处理。
App Task COM任务 :这里用作LED闪烁。
App Task STAT任务 :统计任务
App Task IDLE任务 :空闲任务
System Timer Thread任务:系统定时器任务
2、 (1) 凡是用到printf函数的全部通过函数App_Printf实现。
(2) App_Printf函数做了信号量的互斥操作,解决资源共享问题。
3、默认上电是通过串口打印信息,如果使用RTT打印信息
(1) MDK AC5,MDK AC6或IAR通过使能bsp.h文件中的宏定义为1即可
#define Enable_RTTViewer 1
(2) Embedded Studio继续使用此宏定义为0, 因为Embedded Studio仅制作了调试状态RTT方式查看。
实验操作:
串口打印信息方式(AC5,AC6和IAR):
波特率 115200,数据位 8,奇偶校验位无,停止位 1
RTT打印信息方式(AC5,AC6和IAR):
Embedded Studio仅支持调试状态RTT打印:
由于Embedded Studio不支持中文,所以中文部分显示乱码,不用管。
程序执行框图:
本章节主要为大家讲解了ThreadX的任务管理,此章节比较重要,望初学者用心掌握。