这个系列我会带领大家有顺序的去学习UCOS系统,希望大家能够放平心态,因为这个系统主要是为了简化我们的开发难度,并没有想象中的那么难。
其实正常学习系统的步骤不应该是先移植系统,而是先熟悉其内部的封装好的函数,只有熟悉其每个常用函数的作用,才能更好的理解系统是怎样运行的。
本系列前期不会讲解移植,先从最基础的函数开始讲解。关于UCOS的系统文件官方已经封装好。
具体下载地址:下载地址。
编程环境:Visual Studio 2019 。
首先,系统已经为我们(用户层)封装好函数库了,供我们进行调用。其中可调用的函数总共也就20多种。所以这也是我之前说到系统是为了简化开发难度的原因。类似于底层函数库。系统是以多线程状态运行的。每个任务都可以视为一个独立的个体,所以第一步我们需要创建任务。
大体可分为:增、删、改、查这四种。接下来会一一进行讲解。
“增”顾名思义,即新建任务。在UCOS系统中新建任务的API函数名称为OSTaskCreate()。关于该内部函数入口参数如下所示。
INT8U OSTaskCreate (void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT8U prio)
关于该函数解释如下:
1.(*task)为函数指针,即指向任务代码的指针。所以我们如果想要使用该参数,需要在主体代码中新建一个任务函数。例如:
void Task (void *p_arg) {
for (;;) { //for (;;)也可以替代为while(1)
Task code; //该任务主体执行代码
}
}
上面给的可以看做一个模板,我们可以根据自己的喜好对任务函数名称进行修改,例如:LED_Task等等。如果我们已经设置任务函数名称为LED_Task,那么创建任务时该入口参数即为LED_Task。其他同理,这里不再赘述。
2.(*p_arg)是一个指针可选的数据区域,可以用来传递参数到任务函数中。一般设置为NULL,表示没有参数可以传递。
3.(OS_STK *ptos),OS_STK定义了其数据类型。对该参数查看底层定义时,最终程序可指向如下代码:
typedef unsigned int CPU_INT32U; /* 32-bit unsigned integer */
表示该入口参数的数据类型为无符号整数型,并且数据长度为32位,即4个字节。
*ptos是指向任务栈顶的指针。这里就涉及到一个知识点了,“堆栈”。解释如下:当该任务暂停运行时,其CPU会给该任务一个缓冲数据的空间,并且该数据空间是连续的(通俗的讲,用于保存CPU退出该任务前一时刻的所有数据)。
所以我们需要设置一个参数能够代表该任务的堆栈大小。为了程序的简便,本示例采用宏定义的方式。具体实现代码如下:
#define Task1_STK_NUM 128
OS_STK Task1_STK[Task1_STK_NUM];
如上所示,本示例设置堆栈大小为数组形式,并且由前面已知,该入口参数的数据大小为32位,所以如果设置数组大小为128,则意味着有128*4个字节的堆栈空间。并且这里需要特别注意一点:该入口参数是指向任务栈顶,但是我们定义的数组默认状态是先从栈底存储数据,最终到栈顶。正好顺序相反,所以我们为了符合其入口参数的具体要求,需要设置为Task1_STK[Task1_STK_NUM - 1]。即Task1_STK[127]。代表为该堆栈空间的最后一个数据,也为最后4个字节。最终由于该入口参数为指针,所以需要对该入口参数进行取地址操作。即&Task1_STK[Task1_STK_NUM - 1]。
4.(INT8U prio),INT8U定义了其数据类型,跟前面同理,最终查看定义指向如下代码:
typedef unsigned char CPU_INT08U; /* 8-bit unsigned integer */
代表入口参数的数据类型为无符号字符整形8位。
prio表示该任务所处的优先级大小,优先级数目大小越小,所代表的优先级越高,即越优先运行。并且任务所处的优先级大小是有限制的,不能超过254。该优先级大小范围在底层文件有定义,具体定义位置为“os_cfg.h”。具体定义代码如下:
#define OS_LOWEST_PRIO 63u
同样为了程序简便,我们需要人为宏定义一个参数。具体实现代码如下:
#define Task1_PRIO 2
最终,该新建任务函数具体实现代码如下:
OSTaskCreate(task1_task, NULL, &Task1_STK[Task1_STK_NUM - 1], Task1_PRIO);
但是在使用该新建函数之前需要对该函数进行声明,具体实现代码如下:
void task1_task(void* p_arg); //任务函数
至此一个比较完整的新建任务函数流程已经完毕。接下来为了体现系统的多线程运行。所以按照之前的步骤,另外创建一个任务函数。具体实现代码如下:
OSTaskCreate(task2_task, NULL, &Task2_STK[Task2_STK_NUM - 1], Task2_PRIO);
每个任务主体执行功能都不一样,本示例为了最终的效果比较明显,所以直接采用打印字符串的方式来呈现。
任务1:
void task1_task(void* p_arg) {
while (1) {
printf("Task1 Run...\r\n"); //输出一句话
//一般是延迟一下
OSTimeDlyHMSM(0, 0, 1, 0);
}
}
大家可能发现在任务1中出现了一个陌生的函数OSTimeDlyHMSM()。该函数的作用主要是起到延时的作用。关于该内部函数代码如下:
INT8U OSTimeDlyHMSM (INT8U hours,
INT8U minutes,
INT8U seconds,
INT16U ms)
很明显,该函数的四个入口参数分别代表小时、分钟、秒、毫秒。如果我们想要任务函数延时1s,则需要将第三个入口参数seconds设置为1,其他入口参数都设置为0即可。该函数除了这个作用之外,还有另外一个比较重要的作用,即该函数可进行任务调度。那什么事任务调度呢?解释如下:
由于没有使用系统之前,单片机为单线程的,总是先执行完初始化,再依次执行while循环。总是一个函数执行完才执行另外一个函数。但是加了系统之后,单片机变成多线程,但是任务1不能总是处于运行状态,如果任务1一直处于运行状态,那么就会失去多线程的意义。所以此时任务就需要一会儿暂停,一会儿运行。所以任务调度就是既不浪费CPI资源也不干扰其他线程。具体关于延时函数会在之后的章节中进行详细讲解。
任务2:
void task2_task(void* p_arg) {
while (1) {
printf("Task2 Run...\r\n");
OSTimeDlyHMSM(0, 0, 1, 0);
}
}
在成功创建这两个任务函数之后,我们还需要考虑一个问题,就是如果此时中断发生在创建函数之前呢,即初始化部分。这是完全有可能的, 最终会导致任务创建失败,这是我们都不愿意看到的。所以为了解决该问题,可以分别对任务进行临界区处理。即如果任务处于临界区之内,即使发生中断,也不会影响该函数的运行。所以我们需要再单独创建一个任务函数用来新建任务。具体实现代码如下:
OSTaskCreate(start_task, NULL, &Start_STK[Start_STK_NUM - 1], Start_PRIO);
启动任务:
void start_task(void* p_arg) {
//禁止任务被打断(主要是被中断打断)
OS_ENTER_CRITICAL(); //进入临界状态,换言之,就是关闭了所有中断
//可以在这里面进行其他任务开启
OSTaskCreate(task1_task, NULL, &Task1_STK[Task1_STK_NUM - 1], Task1_PRIO);
OSTaskCreate(task2_task, NULL, &Task2_STK[Task2_STK_NUM - 1], Task2_PRIO);
OS_EXIT_CRITICAL(); //退出中断
}
#include
#include
#include
#include "app_cfg.h"
#define Start_PRIO 1 //这个是任务优先级,任务优先级的编号,1-63,且不能重复,它代表了任务的唯一性
#define Start_STK_NUM 128 //指任务的OS_STK的数量
OS_STK Start_STK[Start_STK_NUM]; //有Task1_STK_NUM 个4个字节的空间,128*4
void start_task(void* p_arg); //任务函数
#define Task1_PRIO 2 //这个是任务优先级,任务优先级的编号,1-63,且不能重复,它代表了任务的唯一性
#define Task1_STK_NUM 128 //指任务的OS_STK的数量
OS_STK Task1_STK[Task1_STK_NUM]; //有Task1_STK_NUM 个4个字节的空间,128*4
void task1_task(void* p_arg); //任务函数
#define Task2_PRIO 3 //这个是任务优先级,任务优先级的编号,1-63,且不能重复,它代表了任务的唯一性
#define Task2_STK_NUM 128 //指任务的OS_STK的数量
OS_STK Task2_STK[Task2_STK_NUM]; //有Task1_STK_NUM 个4个字节的空间,128*4
void task2_task(void* p_arg); //任务函数
int main (void)
{
#if OS_TASK_NAME_EN > 0u
CPU_INT08U os_err;
#endif
//与RTOS无关,用于初始化Win32系统-->
//就有可能会触发各种中断
CPU_IntInit();
Mem_Init(); /* Initialize Memory Managment Module */
CPU_IntDis(); /* Disable all Interrupts */
CPU_Init(); /* Initialize the uC/CPU services */
//<--与RTOS无关,用于初始化Win32系统
OSInit(); /* Initialize uC/OS-II */
//创建各种任务
//增 删 改 查
//1.新建,应当避免被其他中断所打断
OSTaskCreate(start_task, NULL, &Start_STK[Start_STK_NUM - 1], Start_PRIO);
OSStart(); /* Start multitasking (i.e. give control to uC/OS-II) */
while (DEF_ON) { /* Should Never Get Here. */
;
}
}
void start_task(void* p_arg) {
//禁止任务被打断(主要是被中断打断)
OS_ENTER_CRITICAL(); //进入临界状态,换言之,就是关闭了所有中断
//可以在这里面进行其他任务开启
OSTaskCreate(task1_task, NULL, &Task1_STK[Task1_STK_NUM - 1], Task1_PRIO);
OSTaskCreate(task2_task, NULL, &Task2_STK[Task2_STK_NUM - 1], Task2_PRIO);
OS_EXIT_CRITICAL(); //推出中断
}
void task1_task(void* p_arg) {
while (1) {
printf("Task1 Run...\r\n"); //输出一句话
//一般是延迟一下
OSTimeDlyHMSM(0, 0, 1, 0); //延迟1s,还有一个非常特殊的功能,就是可以发起任务调度
}
}
void task2_task(void* p_arg) {
while (1) {
printf("Task2 Run...\r\n"); //输出一句话
//一般是延迟一下
OSTimeDlyHMSM(0, 0, 1, 0); //延迟1s,还有一个非常特殊的功能,就是可以发起任务调度
}
}
修改任务线程一般最常常使用到的无非就是挂起和恢复这两种。任务一旦被挂起,如果之后没有被恢复,则该任务将来永远不会被使用。
其中挂起任务状态函数名称为OSTaskSuspend()。关于该函数内部代码如下:
INT8U OSTaskSuspend (INT8U prio)
其中入口参数为该任务函数所处的优先级。如果挂起的任务为当前任务,入口参数可以替换为OS_PRIO_SELF。
其中恢复任务状态函数名称为OSTaskResume()。关于该函数内部代码如下:
INT8U OSTaskResume (INT8U prio)
与挂起任务函数同理,这里不再赘述。
那具体是怎么使用呢?为了最终效果明显,本示例需要在原来示例程序基础上再新建一个任务用来挂起。本示例新建任务3。具体代码如下:
OSTaskCreate(task3_task, NULL, &Task3_STK[Task3_STK_NUM - 1], Task3_PRIO);
并且我们还需要知道一点,如果任务3进行了挂起操作,那么如果我们想要恢复任务3的话,那么恢复任务函数必须在其他任务中执行。例如本示例采用在任务1中执行恢复任务3操作。
任务3:
void task3_task(void* p_arg) {
while (1) {
OSTaskSuspend(OS_PRIO_SELF); //一开始运行,就挂起自己
printf("Task3 Run...\r\n"); //输出一句话
//一般是延迟一下
OSTimeDlyHMSM(0, 0, 1, 0); //延迟1s,还有一个非常特殊的功能,就是可以发起任务调度
}
}
任务1:
void task1_task(void* p_arg) {
while (1) {
printf("Task1 Run...\r\n"); //输出一句话
//Task1输出完毕之后,就恢复Task3
OSTaskResume(Task3_PRIO);
//一般是延迟一下
OSTimeDlyHMSM(0, 0, 1, 0); //延迟1s,还有一个非常特殊的功能,就是可以发起任务调度
}
}
并且为了最终效果明显,将任务2中的延时时间改为2s。
任务2:
void task2_task(void* p_arg) {
while (1) {
printf("Task2 Run...\r\n"); //输出一句话
//一般是延迟一下
OSTimeDlyHMSM(0, 0, 2, 0); //延迟1s,还有一个非常特殊的功能,就是可以发起任务调度
}
}
最终效果为Task1 Run、Task2 Run-------Task1 Run、Task3 Run-------Task1 Run、Task2 Run------Task1 Run、Task3 Run之后依次按顺序显示。
接下来为了使得读者能够更加清楚的了解其挂起和恢复对于任务线程的影响,我做了一张时序图供大家观看:
对上图解释如下:
成功创建任务之后,由于任务1的优先级最高,所以优先运行,即正常输出字符串,然后恢复任务3,此时任务3是可以正常运行的,任务1运行完之后,开始运行任务2,也是正常输出字符串,紧接着运行任务3,但是任务3刚开始运行就对任务3进行了挂起操作。即任务3不能正常输出字符串。所以第一秒时间内输出结果为Task1 Run、Task2 Run。然后下一秒,任务1还是能够正常输出,同时将任务3的状态从挂起状态恢复。但是本来应该是任务2优先于任务3运行的,但是任务2的延时时间为2s,即每2s才能触发任务2运行条件。任务1和任务3都是1s。所以此时任务3能够正常输出,而任务2不能正常输出。所以第2s内输出结果为Task1 Run、Task3 Run。然后由于时间已经达到触发任务2运行的条件,所以任务2相对于任务3优先输出,又回到了程序刚开始运行时的状态,之后依次执行。
那么此时如果我们修改任务2的延时时间为1s,最终结果会有什么变化呢?
如果该任务没有在线程中删掉的话,那么在之后不论该任务处在什么状态,都有可能被唤醒。删除任务的函数名称为OSTaskDel()。关于该函数内部代码如下:
INT8U OSTaskDel (INT8U prio)
与前面的函数同理,这里不再赘述。同样,如果删除的为该当前任务函数,则入口参数可替代为OS_PRIO_SELF。这种删除函数的方式比较暴力,因为只要调用该函数之后,该函数之前所储存的所有数据都会遗失。所以一般不建议使用,我们可以使用挂起任务的方式来代替。
UCOS的每个任务都有一个属性需要存储,UCOS将这些属性集合在一起用一个结构体来表示。查询任务状态函数为OSTaskQuery()。关于该函数内部代码如下:
INT8U OSTaskQuery (INT8U prio,
OS_TCB *p_task_data)
第一个参数为当前任务的优先级大小。
第二个入口参数OC_TCB为任务控制块,对其进行查找底层定义,程序定位到如下:
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; /* Pointer to current top of stack */
#if OS_TASK_CREATE_EXT_EN > 0u
void *OSTCBExtPtr; /* Pointer to user definable data for TCB extension */
OS_STK *OSTCBStkBottom; /* Pointer to bottom of stack */
INT32U OSTCBStkSize; /* Size of task stack (in number of stack elements) */
INT16U OSTCBOpt; /* Task options as passed by OSTaskCreateExt() */
INT16U OSTCBId; /* Task ID (0..65535) */
#endif
struct os_tcb *OSTCBNext; /* Pointer to next TCB in the TCB list */
struct os_tcb *OSTCBPrev; /* Pointer to previous TCB in the TCB list */
#if OS_TASK_CREATE_EXT_EN > 0u
#if defined(OS_TLS_TBL_SIZE) && (OS_TLS_TBL_SIZE > 0u)
OS_TLS OSTCBTLSTbl[OS_TLS_TBL_SIZE];
#endif
#endif
#if (OS_EVENT_EN)
OS_EVENT *OSTCBEventPtr; /* Pointer to event control block */
#endif
#if (OS_EVENT_EN) && (OS_EVENT_MULTI_EN > 0u)
OS_EVENT **OSTCBEventMultiPtr; /* Pointer to multiple event control blocks */
#endif
#if ((OS_Q_EN > 0u) && (OS_MAX_QS > 0u)) || (OS_MBOX_EN > 0u)
void *OSTCBMsg; /* Message received from OSMboxPost() or OSQPost() */
#endif
#if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u)
#if OS_TASK_DEL_EN > 0u
OS_FLAG_NODE *OSTCBFlagNode; /* Pointer to event flag node */
#endif
OS_FLAGS OSTCBFlagsRdy; /* Event flags that made task ready to run */
#endif
INT32U OSTCBDly; /* Nbr ticks to delay task or, timeout waiting for event */
INT8U OSTCBStat; /* Task status */
INT8U OSTCBStatPend; /* Task PEND status */
INT8U OSTCBPrio; /* Task priority (0 == highest) */
INT8U OSTCBX; /* Bit position in group corresponding to task priority */
INT8U OSTCBY; /* Index into ready table corresponding to task priority */
OS_PRIO OSTCBBitX; /* Bit mask to access bit position in ready table */
OS_PRIO OSTCBBitY; /* Bit mask to access bit position in ready group */
#if OS_TASK_DEL_EN > 0u
INT8U OSTCBDelReq; /* Indicates whether a task needs to delete itself */
#endif
#if OS_TASK_PROFILE_EN > 0u
INT32U OSTCBCtxSwCtr; /* Number of time the task was switched in */
INT32U OSTCBCyclesTot; /* Total number of clock cycles the task has been running */
INT32U OSTCBCyclesStart; /* Snapshot of cycle counter at start of task resumption */
OS_STK *OSTCBStkBase; /* Pointer to the beginning of the task stack */
INT32U OSTCBStkUsed; /* Number of bytes used from the stack */
#endif
#if OS_TASK_NAME_EN > 0u
INT8U *OSTCBTaskName;
#endif
#if OS_TASK_REG_TBL_SIZE > 0u
INT32U OSTCBRegTbl[OS_TASK_REG_TBL_SIZE];
#endif
} OS_TCB;
该结构体中每个定义之后都有注释,由于该查询功能不经常使用,所以这里不做详细解释。如果大家感兴趣,可以自行研究。具体实现代码如下:
void start_task(void* p_arg) {
OS_TCB osTCB = { 0 };
//禁止任务被打断(主要是被中断打断)
OS_ENTER_CRITICAL(); //进入临界状态,换言之,就是关闭了所有中断
//可以在这里面进行其他任务开启
OSTaskCreate(task1_task, NULL, &Task1_STK[Task1_STK_NUM - 1], Task1_PRIO);
OSTaskCreate(task2_task, NULL, &Task2_STK[Task2_STK_NUM - 1], Task2_PRIO);
OSTaskCreate(task3_task, NULL, &Task3_STK[Task3_STK_NUM - 1], Task3_PRIO);
OS_EXIT_CRITICAL(); //推出中断
//可以监测系统的状态,
while (1) {
OSTaskQuery(Task1_PRIO, &osTCB); //查询
printf("OSTCBStatPend=%d\r\n", osTCB.OSTCBStatPend);
OSTimeDlyHMSM(0, 0, 1, 0);
}
}
#include
#include
#include
#include "app_cfg.h"
#define Start_PRIO 1 //这个是任务优先级,任务优先级的编号,1-63,且不能重复,它代表了任务的唯一性
#define Start_STK_NUM 128 //指任务的OS_STK的数量
OS_STK Start_STK[Start_STK_NUM]; //有Task1_STK_NUM 个4个字节的空间,128*4
void start_task(void* p_arg); //任务函数
#define Task1_PRIO 2 //这个是任务优先级,任务优先级的编号,1-63,且不能重复,它代表了任务的唯一性
#define Task1_STK_NUM 128 //指任务的OS_STK的数量
OS_STK Task1_STK[Task1_STK_NUM]; //有Task1_STK_NUM 个4个字节的空间,128*4
void task1_task(void* p_arg); //任务函数
#define Task2_PRIO 3 //这个是任务优先级,任务优先级的编号,1-63,且不能重复,它代表了任务的唯一性
#define Task2_STK_NUM 128 //指任务的OS_STK的数量
OS_STK Task2_STK[Task2_STK_NUM]; //有Task1_STK_NUM 个4个字节的空间,128*4
void task2_task(void* p_arg); //任务函数
#define Task3_PRIO 4 //这个是任务优先级,任务优先级的编号,1-63,且不能重复,它代表了任务的唯一性
#define Task3_STK_NUM 128 //指任务的OS_STK的数量
OS_STK Task3_STK[Task3_STK_NUM]; //有Task1_STK_NUM 个4个字节的空间,128*4
void task3_task(void* p_arg); //任务函数
int main (void)
{
#if OS_TASK_NAME_EN > 0u
CPU_INT08U os_err;
#endif
//与RTOS无关,用于初始化Win32系统-->
//就有可能会触发各种中断
CPU_IntInit();
Mem_Init(); /* Initialize Memory Managment Module */
CPU_IntDis(); /* Disable all Interrupts */
CPU_Init(); /* Initialize the uC/CPU services */
//<--与RTOS无关,用于初始化Win32系统
OSInit(); /* Initialize uC/OS-II */
//创建各种任务
//增 删 改 查
//1.新建,应当避免被其他中断所打断
OSTaskCreate(start_task, NULL, &Start_STK[Start_STK_NUM - 1], Start_PRIO);
//2.删除线程
//3.修改任务线程,1)挂起任务;2)恢复任务
//4.查询任务状态,1)单个任务的任务数据块;
OSStart(); /* Start multitasking (i.e. give control to uC/OS-II) */
while (DEF_ON) { /* Should Never Get Here. */
;
}
}
void start_task(void* p_arg) {
OS_TCB osTCB = { 0 };
//禁止任务被打断(主要是被中断打断)
OS_ENTER_CRITICAL(); //进入临界状态,换言之,就是关闭了所有中断
//可以在这里面进行其他任务开启
OSTaskCreate(task1_task, NULL, &Task1_STK[Task1_STK_NUM - 1], Task1_PRIO);
OSTaskCreate(task2_task, NULL, &Task2_STK[Task2_STK_NUM - 1], Task2_PRIO);
OSTaskCreate(task3_task, NULL, &Task3_STK[Task3_STK_NUM - 1], Task3_PRIO);
OS_EXIT_CRITICAL(); //推出中断
//可以使用挂起任务
//OSTaskSuspend(OS_PRIO_SELF); //参数传入的是优先级,但是当挂起自己的时候,可以使用OS_PRIO_SELF
//OSTaskDel(OS_PRIO_SELF); //该方式,比较暴力,
//可以监测系统的状态,
while (1) {
OSTaskQuery(Task1_PRIO, &osTCB); //查询
printf("OSTCBStatPend=%d\r\n", osTCB.OSTCBStatPend);
OSTimeDlyHMSM(0, 0, 1, 0);
}
}
void task1_task(void* p_arg) {
while (1) {
printf("Task1 Run...\r\n"); //输出一句话
//Task1输出完毕之后,就 恢复Task3
OSTaskResume(Task3_PRIO);
//一般是延迟一下
OSTimeDlyHMSM(0, 0, 1, 0); //延迟1s,还有一个非常特殊的功能,就是可以发起任务调度
}
}
void task2_task(void* p_arg) {
while (1) {
printf("Task2 Run...\r\n"); //输出一句话
//一般是延迟一下
OSTimeDlyHMSM(0, 0, 2, 0); //延迟1s,还有一个非常特殊的功能,就是可以发起任务调度
}
}
void task3_task(void* p_arg) {
while (1) {
OSTaskSuspend(OS_PRIO_SELF); //一开始运行,就挂起自己
printf("Task3 Run...\r\n"); //输出一句话
//一般是延迟一下
OSTimeDlyHMSM(0, 0, 1, 0); //延迟1s,还有一个非常特殊的功能,就是可以发起任务调度
}
}
个人认为大家如果细心看完这篇文章,我相信大家会彻底掌握UCOSII的任务部分!!!如果觉得这篇文章还不错的话,记得点赞 ,支持下!!!
以后我会继续推出关于嵌入式(UCOS系统)方面的讲解,下一讲会推出UCOSII任务的同步与通信概念的文章!敬请期待!!!
**我先休息去了~~╭(╯^╰)╮