RTX——第7章 任务管理
以下内容转载自安富莱电子: http://forum.armfly.com/forum.php
单任务系统
学习多任务系统之前,我们先来回顾下单任务系统的编程框架,即裸机时的编程框架。 裸机编程主要
是采用超级循环(super-loops)系统,又称前后台系统。应用程序是一个无限的循环,循环中调用相应
的函数完成相应的操作,这部分可以看做后台行为,中断服务程序处理异步事件,这部分可以看做是前台
行为。 后台也可以叫做任务级,前台也叫作中断级。
对于前后台系统的编程思路主要有以下两种方式:
轮询方式
对于一些简单的应用,处理器可以查询数据或者消息是否就绪,就绪后进行处理,然后再等待,如此
循环下去。 对于简单的任务,这种方式简单易处理。但大多数情况下,需要处理多个接口数据或者消息,
那就需要多次处理,如下面的流程图所示:
用查询方式处理简单的应用,效果比较好,但是随着工程的复杂,采用查询方式实现的工程就变的很
难维护,同时,由于无法定义查询任务的优先级,这种查询方式会使得重要的接口消息得不到及时响应。
比如程序一直在等待一个非紧急消息就绪,如果这个消息后面还有一个紧急的消息需要处理,那么就会使
得紧急消息长时间得不到执行。
中断方式
对于查询方式无法有效执行紧急任务的情况,采用中断方式就有效的解决了这个问题,下面是中断方
式简单的流程图:
采用中断和查询结合的方式可以解决大部分裸机应用,但随着工程的复杂,裸机方式的缺点就暴露出来了
必须在中断(ISR)内处理时间关键运算:
ISR 函数变得非常复杂,并且需要很长执行时间。
ISR 嵌套可能产生不可预测的执行时间和堆栈需求。
超级循环和 ISR 之间的数据交换是通过全局共享变量进行的:
应用程序的程序员必须确保数据一致性。
超级循环可以与系统计时器轻松同步,但:
如果系统需要多种不同的周期时间,则会很难实现。
超过超级循环周期的耗时函数需要做拆分。
增加软件开销,应用程序难以理解。
超级循环使得应用程序变得非常复杂,因此难以扩展:
一个简单的更改就可能产生不可预测的副作用,对这种副作用进行分析非常耗时。
超级循环 概念的这些缺点可以通过使用实时操作系统 (RTOS) 来解决。
多任务系统
针对这些情况,使用多任务系统就可以解决这些问题了。 下面是一个多任务系统的流程图:
多任务系统或者说 RTOS 的实现,重点就在这个调度器上,而调度器的作用就是使用相关的调度算法来决
定当前需要执行的任务。如上图所画的那样,创建了任务并完成 OS 初始化后,就可以通过调度器来决定
任务 A,任务 B 和任务 C 的运行,从而实现多任务系统。 另外需要初学者注意的是,这里所说的多任务系
统同一时刻只能有一个任务可以运行,只是通过调度器的决策,看起来像所有任务同时运行一样。为了更
好的说明这个问题,再举一个详细的运行例子,运行条件如下:
使用抢占式调度器。
1 个空闲任务,优先级最低。
2 个应用任务,一个高优先级和一个低优先级,优先级都比空闲任务优先级高。
中断服务程序,含 USB 中断,串口中断和系统滴答定时器中断。
下图 7.2 所示是任务的运行过程,其中横坐标是任务优先级由低到高排列,纵坐标是运行时间,时间
刻度有小到大。
(1) 启动 RTOS,首先执行高优先级任务。
(2) 高优先级任务等待事件标志(os_evt_wait_and)被挂起,低优先级任务得到执行。
(3) 低优先级任务执行的过程中产生 USB 中断,进入 USB 中断服务程序。
(4) 退出 USB 中断复位程序,回到低优先级任务继续执行。
(5) 低优先级任务执行过程中产生串口接收中断,进入串口接收中断服务程序。
(6) 退出串口接收中断复位程序,并发送事件标志设置消息(isr_evt_set),被挂起的高优先
级任务就会重新进入就绪状态,这个时候高优先级任务和低优先级任务都在就绪态,基于优
先级的调度器就会让高优先级的任务先执行,所有此时就会进入高优先级任务。
(7) 高优先级任务由于等待事件标志(os_evt_wait_and)会再次被挂起,低优先级任务开始
继续执行。
(8) 低优先级任务调用函数 os_dly_wait,低优先级任务被挂起,从而空闲任务得到执行。
(9) 空闲任务执行期间发生滴答定时器中断,进入滴答定时器中断服务程序。
(10) 退出滴答定时器中断,由于低优先级任务延时时间到,低优先级任务继续执行。
(11) 低优先级任务再次调用延迟函数 os_dly_wait,低优先级任务被挂起,从而切换到空闲任务。
空闲任务得到执行。
通过上面实例的讲解,大家应该对多任务系统完整的运行过程有了一个全面的认识。 随着教程后面对
调度器,任务切换等知识点的讲解,大家会对这个运行过程有更深刻的理解。
RTX 就是一款支持多任务运行的实时操作系统,具有时间片,抢占式和合作式三种调度方法。 通过
RTX 实时操作系统可以将程序函数分成独立的任务,并为其提供合理的调度方式。 同时 RTX 实时操作系
统为多任务的执行提供了以下重要优势:
任务调度 - 任务在需要时进行调用,从而确保了更好的程序执行和事件响应。
多任务 - 任务调度会产生同时执行多个任务的效果。
确定性的行为 - 在定义的时间内处理事件和中断。
更短的 ISR - 实现更加确定的中断行为。
任务间通信 - 管理多个任务之间的数据、内存和硬件资源共享。
定义的堆栈使用 - 每个任务分配一个堆栈空间,从而实现可预测的内存使用。
系统管理 - 可以专注于应用程序开发而不是资源管理。
任务设置
RTX 操作系统的配置工作是通过配置文件 RTX_Conf_CM.c 实现。 在 MDK 工程中打开文件
RTX_Conf_CM.c,可以看到如下图 7.4 所示的工程配置向导:
用于任务配置的主要是如下两个参数:
Number of concurrent running tasks
参数范围 0 – 250
表示同时运行的最大任务数,这个数值一定要大于等于用户实际创建的任务数,空闲任务不包含
在这个里面。比如当前的数值是 6,就表示用户最多可以创建 6 个任务。
Number of tasks with user-provided stack
参数范围 0 – 250
表示自定义任务堆栈的任务数,如果这个参数定义为 0 的话,表示所有的任务都是使用的配置向
导里面第三个参数 Task statck size 大小。 比如:
Number of concurrent running tasks = 6
Number of tasks with user-provided stack = 0
表示允许用户创建 6 个任务,所有的 6 个任务都是分配第三个参数 Task statck size 大小的任务
堆栈空间。
Number of concurrent running tasks = 6
Number of tasks with user-provided stack = 3
表示允许用户创建 6 个任务,其中 3 个任务是用户自定义任务堆栈大小,另外 3 个任务是用的第
三个参数 Task statck size 大小的任务堆栈空间。
栈溢出检测
如果怕任务栈溢出,那么此功能就非常的有用了,用户只需在 RTX 的配置向导里面使能使用任务栈检测即可:
RTX 初始化和启动
使用如下 3 个函数可以实现 RTX 的初始化:
os_sys_init()
os_sys_init_prio()
os_sys_init_user()
关于这 3 个函数的讲解及其使用方法可以看参考资料 rlarm.chm 文件 :
函数描述:
函数 os_sys_init_user 用于实现 RTX 操作系统的初始化和启动任务的创建,并且使用这个函数做初始化还
可以自定义任务栈的大小。
第 1 个参数填启动任务的函数名。
第 2 个参数是任务的优先级设置,用户可以设置的任务优先级范围是 1-254。 优先级 0 用于空闲任
务,如果用户将这个参数设置为 0 的话,RTX 系统会将其更改为 1。 优先级 255 被保留用于最重要
的任务。
第 3 个参数是任务栈地址。
第 4 个参数是任务栈大小。
使用这个函数要注意以下几个问题
1. 必须在 main 函数中调用 os_sys_init_user。
2. 任务栈空间必须 8 字节对齐,可以将任务栈数组定义成 uint64_t 类型即可。 (补充说明,这就是为什么我们定义任务栈的时候要用大小除以8,因为sizeof所求大小需要乘以元素字节,这里定义的正好是uint64_t,八个字节的,这样能保证是八字节对齐的)
3. 优先级 255 代表更重要的任务。
4. 对于 RTX 操作系统来说,优先级参数中数值越小优先级越低,也就是说空闲任务的优先级是最低的,因为它的优先级数值是 0 。
任务创建
使用如下 4 个函数可以实现 RTX 的任务创建:
os_tsk_create
os_tsk_create_ex
os_tsk_create_user
os_tsk_create_user_ex
ex后缀是extension的缩写,看英文文档就会知道的,表示扩展。
函数描述:
函数 os_tsk_create_use 用于实现 RTX 操作系统的任务创建,并且还可以自定义任务栈的大小。
第 1 个参数填创建任务的函数名。
第 2 个参数是任务的优先级设置,用户可以设置的任务优先级范围是 1-254。 优先级 0 用于空闲任
务,如果用户将这个参数设置为 0 的话,RTX 系统会将其更改为 1。 优先级 255 被保留用于更重要
的任务。 如果新创建的任务优先级比当前正在执行任务的优先级高,那么就会立即切换到高优先级
任务去执行。
第 3 个参数是任务栈地址。
第 4 个参数是任务栈大小。
函数的返回值是任务的 ID,使用 ID 号可以区分不同的任务。
使用这个函数要注意以下问题
1. 任务栈空间必须 8 字节对齐,可以将任务栈数组定义成 uint64_t 类型即可。
任务删除
使用如下 2 个函数可以实现 RTX 的任务删除:
os_tsk_delete
os_tsk_delete_self
函数描述:
函数 os_tsk_create_use 用于实现 RTX 操作系统的任务删除
第 1 个参数填要删除任务的 ID。
如果任务删除成功,函数返回 OS_R_OK,其余所有情况返回 OS_R_NOK,比如所写的任务 ID 不存在。
使用这个函数要注意以下问题:
1. 如果用往此函数里面填的任务 ID 是 0 的话,那么删除的就是当前正在执行的任务,此任务被删除后,
RTX 会切换到任务就绪列表里面下一个要执行的高优先级任务。
空闲任务
几乎所有的小型 RTOS 中都会有一个空闲任务,空闲任务应该属于系统任务,是必须要执行的,用
户程序不能将其关闭。不光小型系统中有空闲任务,大型的系统里面也有的,比如 WIN7,下面的截图就
是 WIN7 中的空闲进程。
空闲任务主要有以下几个作用:
用户不能让系统一直在执行各个应用任务,这样的话系统利用率就是 100%,系统就会一直的超负荷
运行,所以空闲任务很有必要。
为了更好的实现低功耗,空闲任务也很有必要,用户可以在空闲任务中实现睡眠,停机等低功耗措施。
代码练兵场:
K1 按键按下,删除任务 AppTaskLED。
K2 按键按下,重新创建任务 AppTaskLED。
不按键的时候,LED和Beep翻转。
int main ( void ) { Bsp_Init(); /* 创建启动任务 */ os_sys_init_user (AppTaskStart, /* 任务函数 */ 4, /* 任务优先级 */ &AppTaskStartStk, /* 任务栈 */ sizeof(AppTaskStartStk)); /* 任务栈大小,单位字节数 */ while ( 1 ) { } }
__task void AppTaskUserIF(void) { while(1) { if(key1_flag==1) { key1_flag=0; printf("K1键按下,删除任务HandleTaskLED\r\n"); if(HandleTaskLED != NULL) { if(os_tsk_delete(HandleTaskLED) == OS_R_OK) { HandleTaskLED = NULL; printf("任务AppTaskLED删除成功\r\n"); } else { printf("任务AppTaskLED删除失败\r\n"); } } } if(key2_flag==1) { key2_flag=0; printf("K2键按下,重新创建任务AppTaskLED\r\n"); if(HandleTaskLED == NULL) { HandleTaskLED = os_tsk_create_user(AppTaskLED, /* 任务函数 */ 2, /* 任务优先级 */ &AppTaskLEDStk, /* 任务栈 */ sizeof(AppTaskLEDStk)); /* 任务栈大小,单位字节数 */ } } os_dly_wait(20); } }
输出打印:
可以看到按下K1时,LED灯不再翻转,按下K2后,又开始翻转。
RTX 配置:
RTX 配置向导详情如下:
欢迎加入作者的小圈子
扫描下方左边二维码加入QQ交流群,扫描下方右边二维码关注个人微信公众号并获取更多隐藏干货,QQ交流群:816747642 微信公众号:Crystal软件学堂
作者:Crystal软件学堂 bilibili视频教程地址:https://space.bilibili.com/5782182 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在转载文章页面给出原文连接。 如果你觉得文章对你有所帮助,烦请点个推荐,你的支持是我更文的动力。 文中若有错误,请您务必指出,感谢给予我建议并让我提高的你。 |