本文章基于兆易创新GD32 MCU所提供的2.2.4版本库函数开发
后续项目主要在下面该专栏中发布:
感兴趣的点个关注收藏一下吧!
电机驱动开发可以跳转:
手把手教你嵌入式国产化-实战项目-无刷电机驱动(1)-CSDN博客
BMS电源系统开发可以跳转:暂未放链接
向上代码兼容GD32F303RCT6中使用
本项目配套开发板:
基于GD32F103RCT6国产GD32平台,以下教程编写基于该开发板
图片:
原理图以及例程请联系客服获取!
注意:
本教程致力于解决所有在调试中出现的所有问题,如有未包含在的问题,请联系QQ:2049363803,有奖更新文档!
群号:621154399
有问题欢迎大家加入我们一起交流,这个群是开源性技术交流群。
参考资料:
《UCOS-III开发指南_V1.5》
《µC/OS-III Documentation》
《ARM Cortex-M3 与 Cortex-M4 权威指南(第 3 版)》
《uCOS-III内核实现与应用开发实战指南—基于STM32》
介绍:
在上一小结,我们完成了对uCOSIII的任务创建,并且成功创建了三个LED任务,循环点亮,但是之前我们在完成创建任务函数以后,就立马对任务进行了启动,并且CPU重复调用我们创建的三个任务函数,没有涉及到对任务函数的挂起/删除/等待/延时等一系列操作。
uCOSIII中的每一个任务函数都有多种运行状态,例如从运行态变成阻塞态或者从阻塞态变成就绪态等;在正式使用之前,我们需要对任务的各自运行状态及其切换有一个深入的了解,才能确保我们后续在系统设计的时候不会出现灾难性错误。
任务运行状态:
1.创建任务 到 就绪态:
首先,任务在创建以后,该任务会紧接着进入就绪态,等待任务调度器进行调度。
如果该任务的优先级大于当前正在运行任务的优先级,任务调度器会将CPU使用权转接给新创建的任务;反之则等待CPU空闲或者更高优先级任务释放CPU使用权。
2.就绪态 到 运行态:
当系统发生任务切换的时候,就绪列表中优先级最高的任务会被调度器选中,从而进入运行态。
3.运行态 到 就绪态
当系统中有优先级比当前任务更高的任务创建后或者原本进入等待/阻塞等的更高优先级的任务恢复就绪态以后。任务调度器会进行调度,此时处于就绪列表中优先级最高的任务变为运行态,那么原先处于运行态的任务则进入就绪态!
但是任务依旧存储在就绪列表中,在等更高优先级的任务运行完毕或者进入阻塞/挂起等状态后恢复该任务的CPU使用权。
4.运行态 到 阻塞态
当系统中正在运行的任务发生阻塞(挂起/延时/等待信号量等)时,该任务就会从就绪列表中被系统删除,任务的状态也从运行态变为阻塞态;此时任务调度器会紧接着进行任务切换,将当前就绪列表中任务优先级最高的函数由就绪态变为运行态。
5.阻塞态 到 就绪态
当处于阻塞态的任务被恢复后(任务恢复、延时时间超时或到达、读信号量超时或者已经读取到信号量),此时被恢复的函数就会被加入到就绪列表中,由阻塞态变为就绪态;如果当前占用CPU使用权的任务的优先级小于此时被恢复的任务,就会发生任务调度,被恢复的任务又会被二次转变,由就绪态变为运行态。
6.就绪态、阻塞态、运行态 到 删除态
在uCOSIII系统中,任务通过调用系统提供的任务删除函数OSTaskDel();来将处于任何状态的任务删除,被删除后的任务将无法再次使用,其栈空间以及TCB等所有资源会被系统回收。
7.删除态 到 就绪态
任务删除后无法恢复,此过程相当于是重新创建任务。
uCOS系统的任务状态
在uCOS系统中,每一个任务都具有多种运行状态,系统在初始化完毕后,由任务管理器进行CPU资源分配。
通常存在一下几种任务状态:
就绪
任务存在于就绪列表中,就绪的任务已经可以被调度器调度执行。
延时
任务处于延时调度状态
等待
任务被用户API所使用的等待函数调用后进入等待状态,系统会设置一个等待超时时间让任务处于等待状态,如果时间为0,则代表任务将无限期的等待,直到其所需要的事件发生;如果超时时间大于0,则在该段时间内任务如果等待的事件或者信号都没有发生,就会自动退出等待状态,重新进入就绪状态。
运行
任务拥有CPU使用权,调度器会将就绪列表中任务优先级最高的任务转为运行状态,处于运行态的任务也是位于就绪列表中。
挂起
任务通过调用OSTaskSuspend()函数,允许任务挂起自己或者其他函数,但是无法挂起空闲任务;并且调用恢复函数OSTaskResume()函数将会是使得挂起的任务恢复运行的唯一办法。并且!!!挂起多少次就需要恢复多少次!!
延时+挂起
任务先产生一个延时,并且当延时还没结束的时候被其他任务挂起(挂起的次数可以叠加),当且仅当延时结束并且挂起被恢复的时候,任务才能再次运行。挂起多少次就需要恢复多少次!!
等待+挂起
当任务等待一个事件或者信号的发生(无限期等待),并且还没等待到的时候被其他任务所挂起(挂起的次数可以叠加),当且仅当任务等待到所需事件或者信号并且挂起已经被恢复了,该任务才能再次运行。挂起多少次就需要恢复多少次!!
超时等待+挂起
任务在其指定的时间内等待事件或者信号的发生,此时任务已经被其他任务挂起。
删除
从以上所有状态中删除,想恢复只能重新新建任务。
任务状态 | 就绪 | 延时 | 等待 | 挂起 | 运行 | 删除 |
就绪 | 1 | .0 | 0 | 0 | 0 | 0 |
延时 | 0 | 1 | 0 | 0 | 0 | 0 |
等待 | 0 | 0 | 1 | 0 | 0 | 0 |
挂起 | 0 | 0 | 0 | 1 | 0 | 0 |
运行 | 0 | 0 | 0 | 0 | 1 | 0 |
延时+挂起 | 0 | 1 | 0 | 1 | 0 | 0 |
等待加挂起 | 0 | 0 | 1 | 1 | 0 | 0 |
超时等待+挂起 | 0 | 1 | 1 | 1 | 0 | 0 |
删除 | 0 | 0 | 0 | 0 | 0 | 1 |
常用任务函数
任务挂起函数OS_TaskSuspend()
可以挂起用户所指定的任务,被挂起后的任务不论具有什么优先级,都无法获得CPU使用权。可以将处于任何状态的任务挂起,被挂起的函数对于调度器来说是不可见的,除非从挂起状态中被恢复。
使用该函数需要将宏定义:OS_CFG_TASK_SUSPEND_EN 使能
第一个参数指向需要挂起的任务,也可以是任务本身,其中空闲任务被禁止挂起。
第二个参数用于存放返回的错误代码用于分析发生对应什么错误。
例子:
static OS_TCB AppTaskLed1TCB;/* LED 任务句柄 */ static void KEY_Task(void* parameter) { OS_ERR err; while (1) { if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) { /* KEY1 被按下 */ printf("挂起 LED 任务!\n"); /* 挂起 LED1 任务 */ OSTaskSuspend (& AppTaskLed1TCB, & err ); } /* 延时 20 个 tick */ OSTimeDly ( 20, OS_OPT_TIME_DLY, & err ); } }
任务恢复函数OSTaskResume()
既然有任务挂起函数,那么自然而然也就存在用于任务恢复的函数,不然处于挂起态的任务,我们怎么去恢复他?任务恢复函数,实际上就是让被挂起的任务重新变成就绪状态。若是此时我们被恢复的任务在就绪列表中是优先级最高的函数,那么调度器就会切换调用该函数。
第一个参数指向需要恢复的任务,与挂起任务有一个很大的不同,该函数不被允许恢复本身;其实想想就明白,你都挂起了,CPU不调用你,你哪怕写了恢复自己也没办法;或者一个正在运行的任务,都运行了还需要恢复自己?等等之类的一听就好理解。
第二个参数还是存储错误代码。
例子:
static OS_TCB AppTaskLed1TCB;/* LED 任务句柄 */ static void KEY_Task(void* parameter) { OS_ERR err; while (1) { if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) { /* KEY1 被按下 */ printf("恢复 LED 任务!\n"); /* 恢复 LED 任务 */ OSTaskResume ( & AppTaskLed1TCB, & err ); } /* 延时 20 个 tick */ OSTimeDly ( 20, OS_OPT_TIME_DLY, & err ); } }
删除任务函数OSTaskDel()
该函数用于删除一个任务,当一个任务调用该函数去删除另外一个任务时,形参为所要删除的任务在创建时返回的任务句柄。如果是删除自身,那么输入的形参则为NULL。
要使用该函数则需要在os_cfg.h中将 OS_CFG_TASK_DEL_EN 宏定义配置为1,所删除的任务会从所有就绪、阻塞、挂起和事件列表中删除。
例子:
static OS_TCB AppTaskLed1TCB;/* LED 任务句柄 */ static void KEY_Task(void* parameter) { OS_ERR err; while (1) { if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) { /* KEY1 被按下 */ printf("删除 LED 任务!\n"); /* 删除 LED 任务 */ OSTaskDel( & AppTaskLed1TCB, & err ); } /* 延时 20 个 tick */ OSTimeDly ( 20, OS_OPT_TIME_DLY, & err ); } }
任务延时函数OSTimeDly() 和OSTimeDlyHMSM()
OSTimeDly()在我们任务中用得非常之多,每个任务都必须是死循环,并且是必须要 有阻塞的情况,否则低优先级的任务就无法被运行了,OSTimeDly() 函数常用于停止当前 任务进行的运行,延时一段时间后再运行。参数分别为延时的节拍数、选项、返回错误类型
其中选项可以有以下选择:
OS_OPT_TIME_DLY dly 为相对时间,就是从现在起延时多长时 间 , 到 时 钟 节 拍 总 计 数 OSTickCtr = OSTickCtr 当前 + dly 时延时结束。 OS_OPT_TIME_TIMEOUT 跟 OS_OPT_TIME_DLY 的作用情况一样。 OS_OPT_TIME_MATCH dly 为绝对时间,就是从系统开始运行(调用 OSStart()) 时到节拍总计数 OSTickCtr = dly 时延时结束。 OS_OPT_TIME_PERIODIC 周 期 性 延 时 , 跟OS_OPT_TIME_DLY 的作用差不多,如果是长时间延时,该选项更精准一些。例子:
/* 调用相对延时函数,阻塞 1000 个 tick */ OSTimeDly ( 1000, OS_OPT_TIME_DLY, & err );
OSTimeDlyHMSM() 函数与 OSTimeDly() 函数的功能类似,也是用于停止当前任务进
行的运行,延时一段时间后再运行,但是 OSTimeDlyHMSM()函数会更加直观,延时多少
个小时、分钟、秒、毫秒。若要使用该函数,则必须将宏 OS_CFG_TIME_DLY_HMSM_EN 设置为1。
参数分别为:延时的小时数、分钟数、秒数、毫秒数、选项、返回的错误类型。
其中选项可以有以下选择:
OS_OPT_TIME_DLY dly 为相对时间,就是从现在起延时多长时 间 , 到 时 钟 节 拍 总 计 数 OSTickCtr = OSTickCtr 当前 + dly 时延时结束。 OS_OPT_TIME_TIMEOUT 跟 OS_OPT_TIME_DLY 的作用情况一样。 OS_OPT_TIME_MATCH dly 为绝对时间,就是从系统开始运行(调用 OSStart()) 时到节拍总计数 OSTickCtr = dly 时延时结束。 OS_OPT_TIME_PERIODIC 周 期 性 延 时 , 跟OS_OPT_TIME_DLY 的作用差不多,如果是长时间延时,该选项更精准一些。 OS_OPT_TIME_HMSM_STRICT 延时时间取值比较严格 OS_OPT_TIME_HMSM_NON_STRICT 延时时间取值比较宽松例子:
OSTimeDlyHMSM( 0,0,1,0, OS_OPT_TIME_DLY, & err );
合理的任务设计
取自:《uCOS-III内核实现与应用开发实战指南—基于STM32》
作为一个嵌入式开发人员,要对自己设计的嵌入式系统要了如指掌,任务的优先级信息,任务与中断的处理,任务的运行时间、逻辑、状态等都要知道,才能设计出好的系统,所以,在设计的时候需要根据需求制定框架。在设计之初就应该考虑下面几点因素:任务运行的上下文环境、任务的执行时间合理设计。
1. 中断服务函数:
中断服务函数是一种需要特别注意的上下文环境,它运行在非任务的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式)),在这个上下文环境中不能使用挂起当前任务的操作,不允许调用任何会阻塞运行的 API 函数接口。另外需要注意的是,中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,然后通知任务,让对应任务去执行相关处理,因为中断服务函数的优先级高于任何优先级的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。所以在设计的时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合对应中断处理任务的工作。uCOS 支持中断延迟发布,使得原本在中断中发布的信息变成任务级发布,这样子会使得中断服务函数的处理更加快速,屏蔽中断的时间更短,这样子能快速响应其他的中断,真正称得上实时操作系统。
2. 任务:
任务看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。但是做为一个优先级明确的实时系统,如果一个任务中的程序出现了死循环操作(此处的死循环是指没有阻塞机制的任务循环体),那么比这个任务优先级低的任务都将无法执行,当然也包括了空闲任务,因为死循环的时候,任务不会主动让出 CPU,低优先级的任务是不可能得到CPU 的使用权的,而高优先级的任务就可以抢占 CPU。这个情况在实时操作系统中是必须注意的一点,所以在任务中不允许出现死循环。如果一个任务只有就绪态而无阻塞态,势必会影响到其他低优先级任务的执行,所以在进行任务设计时,就应该保证任务在不活跃的时候,任务可以进入阻塞态以交出 CPU 使用权,这就需要我们自己明确知道什么情况下让任务进入阻塞态,保证低优先级任务可以正常运行。在实际设计中,一般会将紧急的处理事件的任务优先级设置得高一些。
3. 空闲任务:
空闲任务(idle 任务)是 uCOS 系统中没有其他工作进行时自动进入的系统任务。因为处理器总是需要代码来执行——所以至少要有一个任务处于运行态。uCOS 为了保证这一点,当调用 OSInit()函数进行系统初始化时,系统会自动创建一个空闲任务,空闲任务是一个非常短小的循环。用户可以通过空闲任务钩子方式,在空闲任务上钩入自己的功能函数。通常这个空闲任务钩子能够完成一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。空闲任务是唯一一个不允许出现阻塞情况的任务,因为 uCOS 需要保证系统永远都有一个可运行的任务。对于空闲任务钩子上挂接的空闲钩子函数,它应该满足以下的条件:1.永远不会挂起空闲任务;
2.不应该陷入死循环,需要留出部分时间用于统计系统的运行状态等。
4. 任务的执行时间:
任务的执行时间一般是指两个方面,一是任务从开始到结束的时间,二是任务的周期。在系统设计的时候这两个时间候我们都需要考虑,例如,对于事件 A 对应的服务任务Ta,系统要求的实时响应指标是 10ms,而 Ta 的最大运行时间是 1ms,那么 10ms 就是任务Ta 的周期了,1ms 则是任务的运行时间,简单来说任务 Ta 在 10ms 内完成对事件 A 的响应即可。此时,系统中还存在着以 50ms 为周期的另一任务 Tb,它每次运行的最大时间长度是 100us。在这种情况下,即使把任务 Tb 的优先级设置比 Ta 更高,对系统的实时性指标也没什么影响,因为即使在 Ta 的运行过程中,Tb 抢占了 Ta 的资源,等到 Tb 执行完毕,消耗的时间也只不过是 100us,还是在事件 A 规定的响应时间内(10ms),Ta 能够安全完成对事件 A 的响应。但是假如系统中还存在任务 Tc,其运行时间为 20ms,假如将 Tc 的优先级设置比 Ta 更高,那么在 Ta 运行的时候,突然间被 Tc 打断,等到 Tc 执行完毕,那 Ta 已经错过对事件 A(10ms)的响应了,这是不允许的。所以在我们设计的时候,必须考虑任务的时间,一般来说处理时间更短的任务优先级应设置更高一些。
任务调度实验
本次实验我们将会对以上常用的任务函数进行实验,开发板还是选用我们自制的GD32开发板,创建LED1、LED2两个任务,两个任务的优先级均是三,采用默认时间片轮转调度运行。两个任务每隔一秒翻转一次其对应LED电平状态,其中,LED1在翻转五次后挂起LED2,LED1则继续翻转自身,当LED1翻转10次以后,恢复LED2的运行;
#include <includes.h>
/*
*************************************************************************
* 任务栈存放区域
*************************************************************************
*/
static CPU_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];
static CPU_STK AppTaskLed1Stk [ APP_TASK_LED1_STK_SIZE ];
static CPU_STK AppTaskLed2Stk [ APP_TASK_LED2_STK_SIZE ];
/*
*************************************************************************
* 定义任务控制块存放区域
*************************************************************************
*/
static OS_TCB AppTaskStartTCB;
static OS_TCB AppTaskLed1TCB;
static OS_TCB AppTaskLed2TCB;
/*
*************************************************************************
* 函数原型存放区域
*************************************************************************
*/
static void AppTaskStart (void *p_arg);
static void AppTaskLed1 ( void * p_arg );
static void AppTaskLed2 ( void * p_arg );
/*
*************************************************************************
* main 函数
*************************************************************************
*/
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
OS_ERR err;
OSInit(&err);
OSTaskCreate((OS_TCB *)&AppTaskStartTCB,
(CPU_CHAR *)"App Task Start",
(OS_TASK_PTR ) AppTaskStart,
(void *) 0,
(OS_PRIO ) APP_TASK_START_PRIO,
(CPU_STK *)&AppTaskStartStk[0],
(CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_START_STK_SIZE,
(OS_MSG_QTY ) 5u,
(OS_TICK ) 0u,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);
OSStart(&err);
}
/*
*************************************************************************
* 空闲任务存放区域
*************************************************************************
*/
static void AppTaskStart (void *p_arg)
{
CPU_INT32U cpu_clk_freq;
CPU_INT32U cnts;
OS_ERR err;
(void)p_arg;
BSP_Init(); /* Initialize BSP functions
*/
CPU_Init();
cpu_clk_freq = BSP_CPU_ClkFreq(); /* Determine SysTick reference
freq. */
cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz; /* Determine
nbr SysTick increme nts */
OS_CPU_SysTickInit(cnts); /*Init uC/OS periodic time src(SysTick).*/
Mem_Init(); /* Initialize Memory Management Module
*/
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err); /* Compute CPU capacity with no task
running */
#endif
CPU_IntDisMeasMaxCurReset();
OSTaskCreate((OS_TCB *)&AppTaskLed1TCB,/*Create the Led1 task */
(CPU_CHAR *)"App Task Led1",
(OS_TASK_PTR ) AppTaskLed1,
(void *) 0,
(OS_PRIO ) APP_TASK_LED1_PRIO,
(CPU_STK *)&AppTaskLed1Stk[0],
(CPU_STK_SIZE) APP_TASK_LED1_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_LED1_STK_SIZE,
(OS_MSG_QTY ) 5u,
(OS_TICK ) 0u,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);
OSTaskCreate((OS_TCB *)&AppTaskLed2TCB, /*Create the Led2 task*/
(CPU_CHAR *)"App Task Led2",
(OS_TASK_PTR ) AppTaskLed2,
(void *) 0,
(OS_PRIO ) APP_TASK_LED2_PRIO,
(CPU_STK *)&AppTaskLed2Stk[0],
(CPU_STK_SIZE) APP_TASK_LED2_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_LED2_STK_SIZE,
(OS_MSG_QTY ) 5u,
(OS_TICK ) 0u,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);
OSTaskDel ( & AppTaskStartTCB, & err );
}
/*
********************************************************************
* LED1 TASK
********************************************************************
*/
static void AppTaskLed1 ( void * p_arg )
{
OS_ERR err;
OS_REG time;
(void)p_arg;
while (DEF_TRUE) { /* Task body, always written as an infinite
loop.*/
LED1_TOG;
time = OSTaskRegGet ( 0, 0, & err ); //获取自身任务寄存器值
if(time<10)
{
OSTaskRegSet ( 0, 0, ++ time, & err ); //继续累加任务寄存器值
}
else
{
OSTaskRegSet ( 0, 0, 0, & err ); //将任务寄存器值归 0
OSTaskResume ( & AppTaskLed2TCB, & err ); //恢复 LED2 任务
}
OSTimeDly ( 1000, OS_OPT_TIME_DLY, & err );
}
}
/*
***********************************************************************
* LED2 TASK
************************************************************************
*/
static void AppTaskLed2 ( void * p_arg )
{
OS_ERR err;
OS_REG time;
(void)p_arg;
while (DEF_TRUE) { /* Task body, always written as an
infinite loop. */
LED2_TOG;
time = OSTaskRegGet ( 0, 0, & err ); //获取自身任务寄存器值
if(time<5)
{
OSTaskRegSet ( 0, 0, ++ time, & err ); //继续累加任务寄存器值
}
else
{
OSTaskRegSet ( 0, 0, 0, & err ); //将任务寄存器值归 0
OSTaskSuspend ( 0, & err ); //挂起自身
}
OSTimeDly ( 1000, OS_OPT_TIME_DLY, & err );
}
}
实验结果:
完成预期目标,实验完成!
GD32UCOSIII任务调度测试视频
群号:621154399
有问题欢迎大家加入我们一起交流,这个群是开源性技术交流群。