文章目录
- 9. 时间管理
- 9.1 `OSTimeDly()`
- 9.2 `OSTimeDlyHMSM()`
- 9.3 `OSTimeDlyResume()`
- 9.4 延时函数实验
- 10. 消息队列
- 10.1 创建消息队列函数`OSQCreate()`
- 10.2 发送消息到消息队列函数(写入队列)`OSQPost()`
- 10.3 获取消息队列中的消息函数(读出队列)`OSQPend()`
- 10.4 消息队列操作实验
- 11. 信号量
- 11.1 二值信号量
- 11.1.1 创建信号量函数`OSSemCreate()`
- 11.1.2 释放信号量函数`OSSemPost()`
- 11.1.3 获取信号量函数`OSSemPend ()`
- 11.1.4 二值信号量实验
- 11.2 计数型信号量
- 11.2.1 计数型信号量实验
- 11.3 优先级翻转问题
- 11.3.1 优先级翻转实验
- 11.4 互斥信号量
- 11.4.1 创建互斥信号量`OSMutexCreate()`
- 11.4.2 获取互斥信号量`OSMutexPend()`
- 11.4.3 释放互斥信号量`OSMutexPost()`
- 11.4.4 互斥信号量实验
- 12. 任务内嵌信号量/队列
- 12.1 任务内嵌信号量
- 12.1.1 获取任务信号量函数`OSTaskSemPend()`
- 12.1.2 释放信号量函数`OSTaskSemPost()`
- 12.1.3 强制设置指定的任务信号量为指定值函数`OSTaskSemSet()`
- 12.1.4 任务内嵌信号量实验
- 12.2 任务内嵌消息队列
- 12.2.1 获取任务消息队列函数(读出队列)`OSTaskQPend()`
- 12.2.2 发送任务消息队列函数(写入队列)`OSTaskQPost()`
- 12.2.3 任务消息队列实验
- 13. 事件标志
- 13.1 创建事件标志组函数`OSFlagCreate()`
- 13.2 设置事件标注组的事件函数`OSFlagPost()`
- 13.3 等待事件标注组中的事件函数`OSFlagPend()`
- 13.4 事件标注组实验
- 14. 软件定时器
9. 时间管理
时间管理是一种建立在时钟节拍上,对任务运行时间管理的一种系统内核机制;
时间管理相关API 函数:
函数 | 描述 |
---|---|
OSTimeDly() | 以系统时钟节拍为单位进行任务延时 |
OSTimeDlyHMSM() | 以时、分、秒、毫秒为单位进行任务延时 |
OSTimeDlyResume() | 恢复被延时的任务 |
实际延时时间取决于系统时钟节拍的频率:OS_CFG_TICK_RATE_HZ
,它默认为1000;
如:task1 被延时1分钟,在1分钟的延时等待过程中,可调用
OSTimeDlyResume()
来及时恢复被延时的task1;
9.1 OSTimeDly()
函数原型输入形参:
void OSTimeDly (OS_TICK dly,
OS_OPT opt,
OS_ERR *p_err)
- dly:任务延时的系统时钟节拍数;
- opt:延时选项;
opt | 描述 |
---|---|
OS_OPT_TIME_DLY | 任务延时的结束时刻为 OSTickCtr = OSTickCtr +dly,相对延时,OSTickCtr 每进入一次滴答定时器中断加一次1 |
OS_OPT_TIME_TIMEOUT | 任务延时的结束时刻为 OSTickCtr = OSTickCtr +dly,功能与上面一样 |
OS_OPT_TIME_MATCH | 任务延时的结束时刻为 OSTickCtr = dly,绝对时间,一般不用 |
OS_OPT_TIME_PERIODIC | 任务延时的结束时刻为 OSTickCtr = OSTCBCurPtr -> TickCtrPrev + dly |
- OS_OPT_TIME_DLY :如dly 设置为1,
OSTimeDly
在OSTickCtr 等于12时被调用,则任务延时的结束时刻为OSTickCtr 等于13时;- OS_OPT_TIME_MATCH :如dly 设置为100,
OSTimeDly
在OSTickCtr 等于0时被调用,则任务延时的结束时刻为OSTickCtr 等于100时,即总延时时刻为100;若OSTimeDly
在OSTickCtr 等于101时被调用,则任务延时的结束时刻同样为OSTickCtr 等于100时,即总延时时刻为4,294,967,296+100=4,294,967,396(系统时钟节拍为32位,即2^32=4,294,967,296,OSTickCtr 在等于4,294,967,296时,溢出回零),在延时期间,该任务进入阻塞态;- OS_OPT_TIME_PERIODIC :控制当前任务运行的总时间为dly;注意任务运行时间不能超过dly;
- p_err:指向接收错误代码变量的指针;
9.2 OSTimeDlyHMSM()
使用该函数前,先把宏OS_CFG_TIME_DLY_HMSM_EN
置1;
函数原型输入形参:
void OSTimeDlyHMSM (CPU_INT16U hours,
CPU_INT16U minutes,
CPU_INT16U seconds,
CPU_INT32U milli,
OS_OPT opt,
OS_ERR *p_err)
- hours:任务延时的小时数;
- minutes:任务延时的分钟数;
- seconds:任务延时的秒数;
- milli:任务延时的毫秒数;
- opt:延时选项;
9.3 OSTimeDlyResume()
使用该函数前,先把宏OS_CFG_TIME_DLY_RESUME_EN
置1;
函数原型输入形参:
void OSTimeDlyResume (OS_TCB *p_tcb,
OS_ERR *p_err)
9.4 延时函数实验
- 实验目的:完成以下3个任务:
- start_task:初始化CPU,配置Systick 中断及优先级,创建其他2个任务
- task1:展示延时函数
OSTimeDly()
的使用; - task2:展示延时函数
OSTimeDlyHMSM()
的使用;
- 实验过程:
- 拿一个实验例程,修改task1、task2 的任务函数:
/* task1 展示延时函数`OSTimeDly()` 的使用 */
void task1(void *p_arg)
{
OS_ERR err;
while(1)
{
LED0_TOGGLE();
OSTimeDly(500,OS_OPT_TIME_DLY,&err); // 延时500ms,相对延时
}
}
/* task2 展示延时函数`OSTimeDlyHMSM()` 的使用 */
void task2(void *p_arg)
{
OS_ERR err;
while(1)
{
LED1_TOGGLE();
OSTimeDlyHMSM (0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT|OS_OPT_TIME_DLY,&err); // 延时500ms,相对延时
}
}
代码运行结果:LED0、LED1每0.5秒翻转一次;
- 修改task1的延时选项为OS_OPT_TIME_MATCH:
/* task1 展示延时函数`OSTimeDly()` 的使用 */
void task1(void *p_arg)
{
OS_ERR err;
while(1)
{
LED0_TOGGLE();
OSTimeDly(500,OS_OPT_TIME_MATCH,&err); // 延时500ms,绝对延时
}
}
/* task2 展示延时函数`OSTimeDlyHMSM()` 的使用 */
void task2(void *p_arg)
{
OS_ERR err;
while(1)
{
LED1_TOGGLE();
OSTimeDlyHMSM (0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT|OS_OPT_TIME_DLY,&err);
}
}
代码运行结果:LED0 翻转的时间间隔非常长;
- 修改task1的延时选项为OS_OPT_TIME_PERIODIC:
/* task1 展示延时函数`OSTimeDly()` 的使用,任务总延时时间为1s */
void task1(void *p_arg)
{
OS_ERR err;
while(1)
{
LED0_TOGGLE();
delay_ms(200); // 模拟任务运行时间
OSTimeDly(1000,OS_OPT_TIME_PERIODIC,&err); // 整个任务的总延时为1s
}
}
/* task2 展示延时函数`OSTimeDlyHMSM()` 的使用,总延时时间为700ms */
void task2(void *p_arg)
{
OS_ERR err;
while(1)
{
LED1_TOGGLE();
delay_ms(200); // 模拟任务运行时间
OSTimeDlyHMSM (0,0,0,500,OS_OPT_TIME_HMSM_NON_STRICT|OS_OPT_TIME_DLY,&err);
}
}
代码运行结果:
代码备份位置:百度网盘 - 知识资源 - 电子工程 - 正点原子stm32f103精英开发板V2资料 - 资料备份 - UCOSIII 时间管理实验.rar
10. 消息队列
队列:任务到任务、中断到任务的一种数据交流机制;
队列与全局变量类似都用于数据传递,但在实时系统中使用全局变量进行数据交流,会导致数据不安全,当多个任务同时对同一变量操作时,数据易受损,故在操作系统中,数据交流一般使用队列的方式;
在读写队列函数中,会先进入临界区,从而屏蔽中断与任务打断,完成数据交流后,再退出临界区;
在中断中,只可写入消息队列,不可读出消息;
UCOS-III 队列特点:
- 数据入队出队方式:一般采用FIFO(先进先出)的数据存储缓冲机制,即先入的数据先从队列中被读取;也可以通过配置,配置成LIFO(后进先出)方式;
- 数据传输方式:UCOS-III 的队列数据是一种“万能指针”,可以指向任何数据,甚至是函数;
- 多任务访问:任何任务、中断都可以向队列发送消息,但中断不能从队列中读消息;
- 出队阻塞:但任务向一个队列读取消息时,可指定一个阻塞时间,用于读取等待;注意,入队(写入消息队列)不会阻塞;
使用队列的流程:创建队列 --> 写入队列(发送消息到队列) --> 读出队列(从队列中督促消息)
队列相关API 函数介绍:
更多关于队列相关API 函数介绍,详见《UCOS-III开发指南_V1.5》第十三章;
10.1 创建消息队列函数OSQCreate()
该函数用于创建一个消息队列;
函数原型输入形参:
void OSQCreate (OS_Q *p_q,
CPU_CHAR *p_name,
OS_MSG_QTY max_qty,
OS_ERR *p_err)
p_q
:指向消息队列结构体的指针,类似于一个句柄;p_name
:指向作为消息队列名的ASCII 字符串的指针;max_qty
:消息队列的大小,即该消息队列可包含多少个消息成员;p_err
:指向接收错误代码变量的指针;
10.2 发送消息到消息队列函数(写入队列)OSQPost()
函数原型输入形参:
void OSQPost (OS_Q *p_q,
void *p_void,
OS_MSG_SIZE msg_size,
OS_OPT opt,
OS_ERR *p_err)
p_q
:指向消息队列结构体的指针,类似于一个句柄;p_void
:指向消息的指针;msg_size
:消息的大小,单位:字节;opt
:函数操作选项,可组合使用;
opt | 描述 |
---|---|
OS_OPT_POST_FIFO | 将发送的消息保存在队列的末尾 |
OS_OPT_POST_LIFO | 将发送的消息保存在队列的开头 |
OS_OPT_POST_ALL | 将消息发送给所有等待消息的任务 |
OS_OPT_POST_NO_SCHED | 禁止在本函数内执行任务调度 |
p_err
:指向接收错误代码变量的指针;
10.3 获取消息队列中的消息函数(读出队列)OSQPend()
函数原型输入形参:
void *OSQPend (OS_Q *p_q,
OS_TICK timeout,
OS_OPT opt,
OS_MSG_SIZE *p_msg_size,
CPU_TS *p_ts,
OS_ERR *p_err)
p_q
:指向消息队列结构体的指针,类似于一个句柄;timeout
:任务挂起等待消息队列的最大允许时间,为0时,表示一直等待,知道接收到消息;opt
:函数操作选项,该参数配合timeout
使用;
opt | 描述 |
---|---|
OS_OPT_PEND_BLOCKING | 若没有任何消息存在于消息队列,则阻塞任务 |
OS_OPT_PEND_NON_BLOCKING | 若没有任何消息存在于消息队列,则直接返回 |
p_msg_size
:指向一个变量,用于表示接收到的消息长度(字节数);p_ts
:指向接收消息队列接收时时间戳的变量的指针,为NULL
则表示没有使用时间戳;p_err
:指向接收错误代码变量的指针;
函数的返回值:从上述函数原型可知,该函数的返回值类型为void *
,即“万能指针”(任意类型数据都能指向),故消息的读与写必须保持数据类型相同,否则会导致消息读写错乱;
10.4 消息队列操作实验
- 实验目的:完成以下4个任务:
- start_task:创建2个队列,并创建另外3个任务;
- task1:当按键key0或key1按下,将键值的地址写入到队列key_queue中(入队);当按键key_up 按下,将传输大数据,将大数据的地址写入队列big_date_queue 中(入队);
- task2:读取队列key_queue 中的消息(出队),打印出接收到的键值;
- task3:从队列big_date_queue 读取大数据地址,打印出接收到的数据;
- 实验过程:
- 定义消息队列句柄:
OS_Q key_queue;
OS_Q big_date_queue;
- 创建2个消息队列:
OSQCreate (&key_queue,"key_queue",1,&err); // 创建消息队列
OSQCreate (&big_date_queue,"big_date_queue",1,&err); // 创建消息队列
- 定义大数据数组:
char buf[] = {"我是一个大数据 abc 123456789"};
- 编写3个任务函数
/* task1 写入队列 */
void task1(void *p_arg)
{
OS_ERR err;
uint8_t key=0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES || key == KEY1_PRES)
{
printf("发送键值!\r\n");
OSQPost (&key_queue,&key,sizeof(key),OS_OPT_POST_FIFO,&err); // 写入消息队列
}
else if(key == WKUP_PRES)
{
printf("发送大数据!\r\n");
OSQPost (&big_date_queue,&buf[0],sizeof(buf),OS_OPT_POST_FIFO,&err); // 写入消息队列
}
OSTimeDly(10,OS_OPT_TIME_DLY,&err);
}
}
/* task2 读出队列key_queue */
void task2(void *p_arg)
{
OS_ERR err;
uint8_t *key; // 读出的消息队列要跟写入的消息队列的类型相同
OS_MSG_SIZE size = 0;
while(1)
{
key = OSQPend (&key_queue,0,OS_OPT_PEND_BLOCKING,&size,NULL,&err);
printf("接收到的键值为:%d\r\n",*key);
printf("接收到的数据长度为:%d字节\r\n",size);
}
}
/* task3 读出队列big_date_queue */
void task3(void *p_arg)
{
OS_ERR err;
char *buf2; // 读出的消息队列要跟写入的消息队列的类型相同
OS_MSG_SIZE size = 0;
while(1)
{
buf2 = OSQPend (&big_date_queue,0,OS_OPT_PEND_BLOCKING,&size,NULL,&err);
printf("接收到的大数据为:%s\r\n",buf2);
printf("接收到的数据长度为:%d字节\r\n",size);
}
}
代码运行结果:
按下key0:
按下key1:
按下key up:
代码备份位置:百度网盘 - 知识资源 - 电子工程 - 正点原子stm32f103精英开发板V2资料 - 资料备份 - UCOSIII 消息队列操作实验.rar
11. 信号量
上节的消息队列用于传递数据,而本节的信号量用于传递状态;若该信号只有2种状态,则该信号又叫做二值信号量;若该信号有多种状态,则该信号又叫做计数型信号量;
信号量是一种解决任务间同步问题的机制,可实现对共享资源的有序访问,即在多任务访问统一资源时的资源管理;
11.1 二值信号量
二值信号量只有空和非空两种状态;通常用于互斥访问或信号同步,尤其是后者;
二值信号量的使用过程:创建二值信号量 -> 释放二值信号量 -> 获取二值信号量
二值信号量相关API 函数:
更多关于二值信号量相关API 函数的介绍,详见《UCOSIII开发指南》第十一章
11.1.1 创建信号量函数OSSemCreate()
函数原型输入形参:
void OSSemCreate( OS_SEM *p_sem,
CPU_CHAR *p_name,
OS_SEM_CTR cnt,
OS_ERR *p_err)
- p_sem:指向信号量结构体的指针;
- p_name:指向作为信号量名的 ASCII 字符串的指针;
- cnt:信号量资源数的初始值,
0
或1
,0
表示二值信号量初始无资源,反之同理; - p_err:指向接收错误代码变量的指针;
11.1.2 释放信号量函数OSSemPost()
函数原型输入形参:
OS_SEM_CTR OSSemPost (OS_SEM *p_sem,
OS_OPT opt,
OS_ERR *p_err)
- p_sem:指向信号量结构体的指针;
- opt:函数操作选项;
OS_OPT_POST_1
:只给最高优先级的那一个任务发信号;即该信号量释放后,优先分配给到最高优先级的那一个任务,其他同样等待该信号量的任务继续保持阻塞状态;OS_OPT_POST_ALL
:给所有等待该信号量的任务发信号量;即该信号量释放后,所有等待该信号量的任务都解除阻塞状态;OS_OPT_POST_NO_SCHED
:禁止在本函数内执行任务调度;该宏可与上述两个宏组合使用
- p_err:指向接收错误代码变量的指针;
返回值:信号量资源数更新后的值;
11.1.3 获取信号量函数OSSemPend ()
函数原型输入形参:
OS_SEM_CTR OSSemPend (OS_SEM *p_sem,
OS_TICK timeout,
OS_OPT opt,
CPU_TS *p_ts,
OS_ERR *p_err)
- p_sem:指向信号量结构体的指针;
- timeout:任务挂起等待信号量的最大允许时间,当为0时,表示一直等待,直到接收到信号;
- opt:函数操作选项;
OS_OPT_PEND_BLOCKING
:若信号量没有资源,则阻塞任务,阻塞时间却决于参数timeout
;OS_OPT_PEND_NON_BLOCKING
:若信号量没有资源,则任务直接返回;
- p_ts:指向接收信号量接收时的时间戳的变量的指针,没有则写0;
- p_err:指向接收错误代码变量的指针;
返回值:信号量资源数更新后的值;
11.1.4 二值信号量实验
- 实验目的:完成以下2个任务:
- start_task:初始化CPU,配置Systick 中断及优先级,创建1个二值信号量,创建task1任务;
- task1:获取二值信号量,当获取成功后打印提示信息;
- 实验过程:
- 定义二值信号量句柄:
OS_SEM binnary_sem;
- 创建二值信号量:
OSSemCreate (&binnary_sem,"binnary_sem",1,&err);
- 编写task1 函数:
/* task1 二值信号量测试 */
void task1(void *p_arg)
{
OS_ERR err;
OS_SEM_CTR ctr=0;
while(1)
{
/* 获取二值信号量 */
ctr = OSSemPend (&binnary_sem,0,OS_OPT_PEND_BLOCKING,0,&err);
printf("task1获取信号量成功!\r\n");
printf("ctr 获取信号量成功后的资源数的计数值:%d\r\n",ctr);
/* 释放二值信号量 */
ctr = OSSemPost (&binnary_sem,OS_OPT_POST_1,&err);
printf("task1释放信号量成功!\r\n");
printf("ctr 释放信号量成功后的资源数的计数值:%d\r\n",ctr);
}
}
11.2 计数型信号量
计数型信号量与二值信号量类似,但二值信号量的资源数只能是0和1,而计数型信号量只要有任务释放信号量,信号量的资源数就会被加1,直到溢出,只要有任务获取信号量,那么信号量的资源数就会被减 1,直到 0;
计数型信号量适用场合:
- 事件计数:当事件发生,在事件处理函数中释放计数型信号量(计数值+1),其他任务会获取计数型信号量(计数值-1),这种场合一般在创建时将初始计数值设置为0;
- 资源管理:信号量表示有效的资源数目;任务必须先获取信号量(计数值-1)才能获取资源控制权,当计数值减到为0时,表示没有资源;当任务使用完资源后,必须释放信号量(计数值+1);信号量创建时计数值应等于最大资源数;
计数型信号量与二值信号量的API 函数是公用的;
11.2.1 计数型信号量实验
- 实验目的:完成以下3个任务:
- start_task:初始化CPU,配置Systick 中断及优先级,创建一个计数型信号量,创建task1、task2任务;
- task1:用于按键扫描,当检测到按键KEY0被按下时,释放计数型信号量;
- task2:每过1秒获取一次计数型信号量,当成功获取后打印信号量计数值;
- 实验过程:
- 定义计数型信号量句柄:
OS_SEM count_sem; // 计数型信号量句柄
- 编写taks1、task2函数:
/* task1 计数型信号量释放 */
void task1(void *p_arg)
{
OS_ERR err;
uint8_t key = 0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES)
{
printf("释放计数型信号量!\r\n");
OSSemPost (&count_sem,OS_OPT_POST_1,&err);
}
OSTimeDly(10,OS_OPT_TIME_DLY,&err);
}
}
/* task2 计数型信号量获取 */
void task2(void *p_arg)
{
OS_ERR err;
OS_SEM_CTR cnt = 0;
while(1)
{
cnt = OSSemPend (&count_sem,0,OS_OPT_PEND_BLOCKING,0,&err);
printf("获取计数型信号量成功,cnt 的值为%d\r\n",cnt);
OSTimeDly(1000,OS_OPT_TIME_DLY,&err);
}
}
代码运行结果:
按下Key0:
连续按下时,信号量释放,信号量增加,不按后信号量依次获取,信号量减少:
11.3 优先级翻转问题
优先级翻转即让高优先级的任务慢执行,低优先级的任务反而优先执行;
- 背景:
优先级翻转在抢占式内核中非常常见,但在实时操作系统中不允许使用,因为优先级翻转会破坏任务的预期顺序,可能导致未知后果;
在使用二值信号量时,经常会遇到优先级翻转问题;
如:任务 H 为优先级最高的任务,任务 L 为优先级最低的任务,任务 M 为优先级介于任务 H 与任务 L 之间的任务;假设任务 H与任务L 都获取同一个二值信号量;
- 开始时任务 H 和任务 M 为挂起状态,等待某一事件发生,而任务 L 正常运行;
- 任务 L 要访问共享资源,因此需要获取信号量;
- 任务 L 成功获取信号量,并且此时二值信号量已无资源,任务 L 开始访问共享资源;
- 此时任务 H 就绪,抢占任务 L 运行;
- 任务 H 开始运行;
- 此时任务 H 要访问共享资源,因此需要获取二值信号量,但二值信号量已无资源,因此任务 H挂起等待信号量资源;
- 任务 L 继续运行;
- 此时任务 M 就绪,抢占任务 L 运行;
- 任务 M 正在运行;
- 任务 M 运行完毕,继续挂起;
- 任务 L 继续运行;
- 此时任务 L 对共享资源的访问操作完成,释放信号量,虽有任务 H 因成功获取二值信号量,解除挂起状态并抢占任务 L 运行。
- 任务 H 得以运行;
从上面优先级翻转的示例中,可知,任务 H 为最高优先级的任务,因此任务 H 执行的操作需要有较高的实时性,但是由于优先级翻转的问题,导致了任务 H 需要等到任务 L 释放信号量才能够运行,并且,任务 L 还会被其他介于任务 H 与任务 L 任务优先级之间的任务 M 抢占,因此任务 H 还需等待任务 M 运行完毕,这显然不符合任务 H 需要的高实时性要求;
11.3.1 优先级翻转实验
- 实验目的:完成以下4个任务:
- start_task:创建一个二值信号量,并创建其他任务;
- high_task:高优先级任务,获取二值信号量,获取成功后打印提示信息,处理完后释放信号量;
- middle_task:中优先级任务,简单的引用任务;
- low_task:低优先级任务,与高优先级任务一样的操作,但占用信号量的时间更久一点;
- 实验过程:
以11.1.4 二值信号量实验代码为基础:
- 修改任务1函数名为
low_task
:
/* 低优先级任务 配置
* 包括: 任务优先级 任务栈大小 任务控制块 任务栈 任务函数
*/
#define TASK1_PRIO 4
#define TASK1_STACK_SIZE 256
CPU_STK* task1_stack; // 申请一个内存给task1 的任务堆栈
OS_TCB task1_tcb;
void low_task(void *p_arg);
- 创建middle_task 和high_task:
/* 中优先级任务 配置
* 包括: 任务优先级 任务栈大小 任务控制块 任务栈 任务函数
*/
#define TASK2_PRIO 3
#define TASK2_STACK_SIZE 256
CPU_STK* task2_stack; // 申请一个内存给task1 的任务堆栈
OS_TCB task2_tcb;
void middle_task(void *p_arg);
/* 高优先级任务 配置
* 包括: 任务优先级 任务栈大小 任务控制块 任务栈 任务函数
*/
#define TASK3_PRIO 2
#define TASK3_STACK_SIZE 256
CPU_STK* task3_stack; // 申请一个内存给task1 的任务堆栈
OS_TCB task3_tcb;
void high_task(void *p_arg);
/* 创建低优先级任务 */
task1_stack = (CPU_STK *)mymalloc(SRAMIN,TASK1_STACK_SIZE*sizeof(CPU_STK)); // 向系统申请内存
OSTaskCreate ((OS_TCB*) &task1_tcb,
(CPU_CHAR*) "low_task",
(OS_TASK_PTR) low_task,
(void*) 0,
(OS_PRIO) TASK1_PRIO,
(CPU_STK*) task1_stack,// 以申请内存方式提供任务堆栈
(CPU_STK_SIZE) TASK1_STACK_SIZE/10,
(CPU_STK_SIZE) TASK1_STACK_SIZE,
(OS_MSG_QTY) 0,
(OS_TICK) 0, // 若设置为0,则使用前面所设置的时间片长度
(void*) 0,
(OS_OPT) (OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR),
(OS_ERR*) &err);
/* 创建中优先级任务 */
task1_stack = (CPU_STK *)mymalloc(SRAMIN,TASK2_STACK_SIZE*sizeof(CPU_STK)); // 向系统申请内存
OSTaskCreate ((OS_TCB*) &task2_tcb,
(CPU_CHAR*) "middle_task",
(OS_TASK_PTR) middle_task,
(void*) 0,
(OS_PRIO) TASK2_PRIO,
(CPU_STK*) task2_stack,// 以申请内存方式提供任务堆栈
(CPU_STK_SIZE) TASK2_STACK_SIZE/10,
(CPU_STK_SIZE) TASK2_STACK_SIZE,
(OS_MSG_QTY) 0,
(OS_TICK) 0, // 若设置为0,则使用前面所设置的时间片长度
(void*) 0,
(OS_OPT) (OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR),
(OS_ERR*) &err);
/* 创建高优先级任务 */
task1_stack = (CPU_STK *)mymalloc(SRAMIN,TASK3_STACK_SIZE*sizeof(CPU_STK)); // 向系统申请内存
OSTaskCreate ((OS_TCB*) &task3_tcb,
(CPU_CHAR*) "high_task",
(OS_TASK_PTR) high_task,
(void*) 0,
(OS_PRIO) TASK3_PRIO,
(CPU_STK*) task3_stack,// 以申请内存方式提供任务堆栈
(CPU_STK_SIZE) TASK3_STACK_SIZE/10,
(CPU_STK_SIZE) TASK3_STACK_SIZE,
(OS_MSG_QTY) 0,
(OS_TICK) 0, // 若设置为0,则使用前面所设置的时间片长度
(void*) 0,
(OS_OPT) (OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR),
(OS_ERR*) &err);
- 编写任务函数:
/* 低优先级任务函数 */
void low_task(void *p_arg)
{
OS_ERR err;
while(1)
{
printf("low_task 获取二值信号量!");
OSSemPend (&binnary_sem,0,OS_OPT_PEND_BLOCKING,0,&err);
printf("low_task 正在运行!");
delay_ms(3000); // 模拟任务运行时间
printf("low_task 释放二值信号量!");
OSSemPost (&binnary_sem,OS_OPT_POST_1,&err);
OSTimeDly(1000,OS_OPT_TIME_DLY,&err);
}
}
/* 中优先级任务函数 */
void middle_task(void *p_arg)
{
OS_ERR err;
while(1)
{
printf("middle_task 正在运行!");
OSTimeDly(1000,OS_OPT_TIME_DLY,&err);
}
}
/* 高优先级任务函数 */
void high_task(void *p_arg)
{
OS_ERR err;
while(1)
{
printf("high_task 获取二值信号量!");
OSSemPend (&binnary_sem,0,OS_OPT_PEND_BLOCKING,0,&err);
printf("high_task 正在运行!");
delay_ms(1000); // 模拟任务运行时间
printf("high_task 释放二值信号量!");
OSSemPost (&binnary_sem,OS_OPT_POST_1,&err);
OSTimeDly(1000,OS_OPT_TIME_DLY,&err);
}
}
代码运行结果:代码开始运行 - 高优先级任务先运行(获取&释放二值信号量) - 高优先级任务运行结束后中优先级任务运行 - 低优先级任务运行(获取&释放二值信号量) - 在低优先级任务释放二值信号量之前,中优先级任务抢占运行,高优先级任务抢占运行,但由于二值信号量未被释放,故高优先级任务阻塞,直到二值信号量被释放;
11.4 互斥信号量
互斥信号量又叫互斥锁,是一种特殊的二值信号量,互斥信号量拥有优先级继承的机制使得互斥信号量能够在一定的程度上解决优先级翻转的问题;
优先级继承:当一个互斥信号量正被一个低优先级的任务持有时,此时有一个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会因为获取不到互斥信号量而被挂起,不过接下来,高优先级的任务会将该低优先级任务的任务优先级提到与高优先级任务的任务优先级相同的任务优先级,那么高与低优先级任务之间的任务,就不能被执行,直至低优先级任务被执行完毕;此时高优先级任务的阻塞时间仅仅是低优先级任务的执行时间,将优先级翻转带来的影响降到最低;
注:互斥信号量不能用于中断服务函数中,原因如下:
- 互斥信号量有任务优先级继承的机制,但中断不是任务,没有任务优先级;
- 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态;
在同步的应用中适合使用二值信号量,而那些需要互斥访问的应用中使用互斥信号量;
互斥信号量的API 函数:
11.4.1 创建互斥信号量OSMutexCreate()
创建互斥信号量时,默认信号量有效,即可被获取;
函数原型输入形参:
void OSMutexCreate (OS_MUTEX *p_mutex,
CPU_CHAR *p_name,
OS_ERR *p_err)
- p_mutex:指向互斥信号量结构体的指针;
- p_name:指向作为信号量名的 ASCII 字符串的指针;
- p_err:指向接收错误代码变量的指针;
11.4.2 获取互斥信号量OSMutexPend()
函数原型输入形参:
void OSMutexPend (OS_MUTEX *p_mutex,
OS_TICK timeout,
OS_OPT opt,
CPU_TS *p_ts,
OS_ERR *p_err)
- p_mutex:指向互斥信号量结构体的指针;
- timeout:任务挂起等待信号量的最大允许时间,当为0时,表示一直等待,直到接收到信号;
- opt:函数操作选项;
OS_OPT_PEND_BLOCKING
:若信号量没有资源,则阻塞任务,阻塞时间却决于参数timeout
;OS_OPT_PEND_NON_BLOCKING
:若信号量没有资源,则任务直接返回;
- p_ts:指向接收信号量接收时的时间戳的变量的指针,没有则写0;
- p_err:指向接收错误代码变量的指针;
11.4.3 释放互斥信号量OSMutexPost()
函数原型输入形参:
void OSMutexPost (OS_MUTEX *p_mutex,
OS_OPT opt,
OS_ERR *p_err)
- p_mutex:指向互斥信号量结构体的指针;
- timeout:任务挂起等待信号量的最大允许时间,当为0时,表示一直等待,直到接收到信号;
- opt:函数操作选项;
OS_OPT_POST_NONE
:不指定特定的选项;OS_OPT_POST_NO_SCHED
:禁止在本函数内执行任务调度;
- p_err:指向接收错误代码变量的指针;
11.4.4 互斥信号量实验
- 实验过程:在优先级翻转实验的基础上,将二值信号量更改为互斥信号量:
OS_MUTEX mutex_sem; // 互斥信号量句柄
/* 创建互斥信号量 */
OSMutexCreate(&mutex_sem,"mu_sem",&err);
OSMutexPend (&mutex_sem,0,OS_OPT_PEND_BLOCKING,0,&err);
OSMutexPost(&mutex_sem,OS_OPT_POST_NONE,&err);
代码运行结果:
12. 任务内嵌信号量/队列
12.1 任务内嵌信号量
任务内嵌信号量(下称任务信号量)本质上就是一个信号量,与前面11. 信号量中的信号量是类似的,但任务信号量是分配于每一个任务的任务控制块结构体中的,因此每一个任务都有独自的任务信号量,任务信号量只能被该任务获取,但可以由其他任务或者中断释放;
任务信号量的优点是使用内存更小,效率更高;缺点是无法共享给多个任务;
任务信号量的API 函数:
12.1.1 获取任务信号量函数OSTaskSemPend()
获取任务信号量函数只能被当前任务调用;
函数原型输入形参:
OS_SEM_CTR OSTaskSemPend (OS_TICK timeout,
OS_OPT opt,
CPU_TS *p_ts,
OS_ERR *p_err)
- timeout:任务挂起等待信号量的最大允许时间,当为0时,表示一直等待,直到接收到信号;
- opt:函数操作选项;
OS_OPT_PEND_BLOCKING
:若信号量没有资源,则阻塞任务,阻塞时间却决于参数timeout
;OS_OPT_PEND_NON_BLOCKING
:若信号量没有资源,则任务直接返回;
- p_ts:指向接收信号量接收时的时间戳的变量的指针,没有则写0;
- p_err:指向接收错误代码变量的指针;
返回值:任务信号量资源数更新后的值;
12.1.2 释放信号量函数OSTaskSemPost()
函数原型输入形参:
OS_SEM_CTR OSTaskSemPost (OS_TCB *p_tcb,
OS_OPT opt,
OS_ERR *p_err)
- p_tcb:指向任务控制块的指针;
- opt:函数操作选项;
OS_OPT_POST_NONE
:不指定特定的选项;OS_OPT_POST_NO_SCHED
:禁止在本函数内执行任务调度;
- p_err:指向接收错误代码变量的指针;
返回值:任务信号量资源数更新后的值;
12.1.3 强制设置指定的任务信号量为指定值函数OSTaskSemSet()
函数原型输入形参:
OS_SEM_CTR OSTaskSemSet (OS_TCB *p_tcb,
OS_SEM_CTR cnt,
OS_ERR *p_err)
- p_tcb:指向任务控制块的指针;
- cnt:指定的信号量资源数;
- p_err:指向接收错误代码变量的指针;
返回值:任务信号量设置前的资源数;
12.1.4 任务内嵌信号量实验
- 实验目的:完成以下3个任务:
- start_task:创建task1、task2任务;
- task1:按键扫描,当检测到按键KEY0 被按下,释放task2任务内嵌信号量;
- task2:获取任务信号量,并打印相关提示信息;
- 实验过程:以11.2.1 计数型信号量实验为基础:
- 删除计数型信号量相关代码;
- 编写task1、task2任务函数:
/* task1 往task2释放任务内嵌信号 */
void task1(void *p_arg)
{
OS_ERR err;
uint8_t key = 0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES)
{
printf("释放任务信号量!\r\n");
OSTaskSemPost(&task2_tcb,OS_OPT_POST_NONE,&err);
}
OSTimeDly(10,OS_OPT_TIME_DLY,&err);
}
}
/* task2 获取任务信号量,并打印相关提示信息 */
void task2(void *p_arg)
{
OS_ERR err;
OS_SEM_CTR cnt = 0;
while(1)
{
cnt = OSTaskSemPend (0,OS_OPT_PEND_BLOCKING,0,&err);
printf("获取任务信号量成功,cnt 的值为%d\r\n",cnt);
OSTimeDly(1000,OS_OPT_TIME_DLY,&err);
}
}
代码运行结果:
-
按下key0,释放task2任务内嵌信号量,task2获取任务信号量:
-
连续按下key0,连续释放task2任务内嵌信号量,task2依次获取任务信号量:
-
在上述基础上,强制设置task2任务信号量为指定值:
OSTaskSemSet (&task2_tcb,1,&err); // 强制设置task2的任务信号量为指定值1
代码运行结果:上电后,不需要按下key0释放task2任务信号量,因为其资源数指定为1;
12.2 任务内嵌消息队列
任务内嵌消息队列(下称任务消息队列)本质上就是一个消息队列,与10. 消息队列 中介绍的消息队列是类似的,任务内嵌消息队列是分配于每一个任务的任务控制块结构体中的,因此每一个任务都有独自的任务内嵌消息队列,任务内嵌消息队列只能被该任务获取(读出),但是可以由其他任务或中断释放(写入),如下图所示:
任务消息队列的API 函数:
12.2.1 获取任务消息队列函数(读出队列)OSTaskQPend()
函数原型输入形参:
void *OSTaskQPend (OS_TICK timeout,
OS_OPT opt,
OS_MSG_SIZE *p_msg_size,
CPU_TS *p_ts,
OS_ERR *p_err)
timeout
:任务挂起等待消息队列的最大允许时间,为0时,表示一直等待,知道接收到消息;opt
:函数操作选项,该参数配合timeout
使用;
opt | 描述 |
---|---|
OS_OPT_PEND_BLOCKING | 若没有任何消息存在于消息队列,则阻塞任务 |
OS_OPT_PEND_NON_BLOCKING | 若没有任何消息存在于消息队列,则直接返回 |
p_msg_size
:指向一个变量,用于表示接收到的消息长度(字节数);p_ts
:指向接收消息队列接收时时间戳的变量的指针,为NULL
则表示没有使用时间戳;p_err
:指向接收错误代码变量的指针;
函数的返回值:从上述函数原型可知,该函数的返回值类型为void *
,即“万能指针”(任意类型数据都能指向),故消息的读与写必须保持数据类型相同,否则会导致消息读写错乱;
12.2.2 发送任务消息队列函数(写入队列)OSTaskQPost()
函数原型输入形参:
void OSTaskQPost (OS_TCB *p_tcb,
void *p_void,
OS_MSG_SIZE msg_size,
OS_OPT opt,
OS_ERR *p_err)
p_tcb
:指向任务控制块的指针,类似于一个句柄;p_void
:指向消息的指针;msg_size
:消息的大小,单位:字节;opt
:函数操作选项,可组合使用;
opt | 描述 |
---|---|
OS_OPT_POST_FIFO | 将发送的消息保存在队列的末尾 |
OS_OPT_POST_LIFO | 将发送的消息保存在队列的开头 |
OS_OPT_POST_NO_SCHED | 禁止在本函数内执行任务调度 |
p_err
:指向接收错误代码变量的指针;
12.2.3 任务消息队列实验
- 实验目的:完成以下3个任务:
- start_task:创建task1、task2任务;
- task1:按键扫描,将键值发送到task2任务消息队列;
- task2:获取任务消息队列,并打印相关提示信息;
- 实验过程:以10.4 消息队列操作实验 为基础:
- 删除task3与消息队列相关代码;
- 编写task1、task2任务函数:
/* task1 写入任务消息队列 */
void task1(void *p_arg)
{
OS_ERR err;
uint8_t key=0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES || key == KEY1_PRES|| key == WKUP_PRES)
{
printf("发送键值!\r\n");
OSTaskQPost (&task2_tcb,&key,sizeof(key),OS_OPT_POST_FIFO,&err);
}
OSTimeDly(10,OS_OPT_TIME_DLY,&err);
}
}
/* task2 读出任务消息队列key_queue */
void task2(void *p_arg)
{
OS_ERR err;
uint8_t *key; // 读出的消息队列要跟写入的消息队列的类型相同
OS_MSG_SIZE size = 0;
while(1)
{
OSTaskQPend (0,OS_OPT_PEND_BLOCKING,&size,NULL,&err);
printf("接收到的数据长度为:%d字节\r\n",size);
}
}
代码运行结果:
按下key0,task1发送键值1,task2接收到键值1;按下key1,task1发送键值2,task2接收到键值2;
13. 事件标志
事件标志与信号量一样属于任务间同步的一种机制,但是信号量一般用于任务间的单事件同步;
事件标志是一个用于指示事件是否发生的比特位,µC/OS-III 用 1表示事件发生,用 0 表示事件未发生;
事件标志组即是多个事件标志的集合;
事件标志组的特点:
- 其每个位表示一个事件,最多可表示32个事件标志;
- 每一位事件的含义,由用户自己决定;
- 任意任务或中断都可以写这些位,但读这些位只能由任务读;
- 可以等待某一位成立,或等待多位同时成立;(或操作、与操作)
- 支持读取阻塞;
事件标志组的逻辑关系图:
事件标志组的API 函数:更多详见《UCOS-III 开发指南》第十六章
使用事件标志组的流程:创建事件标志组 --> 设置事件标志 --> 获取事件标志;
13.1 创建事件标志组函数OSFlagCreate()
函数原型输入形参:
void OSFlagCreate (OS_FLAG_GRP *p_grp,
CPU_CHAR *p_name,
OS_FLAGS flags,
OS_ERR *p_err)
p_grp
:指向事件标注组结构体的指针;p_name
:指向作为事件标志组名的ASCII 字符串的指针;flags
:事件标志组的初始值,一般设置为0,表示初始无任务发生;p_err
:指向接收错误代码变量的指针;
13.2 设置事件标注组的事件函数OSFlagPost()
函数原型输入形参:
OS_FLAGS OSFlagPost (OS_FLAG_GRP *p_grp,
OS_FLAGS flags,
OS_OPT opt,
OS_ERR *p_err)
p_grp
:指向事件标注组结构体的指针;flags
:等待的事件标志;opt
:OS_OPT_POST_FLAG_SET
置1,OS_OPT_POST_FLAG_CLR
清0;p_err
:指向接收错误代码变量的指针;
函数的返回值:事件标志组更新后的事件标志值;
13.3 等待事件标注组中的事件函数OSFlagPend()
函数原型输入形参:
OS_FLAGS OSFlagPend (OS_FLAG_GRP *p_grp,
OS_FLAGS flags,
OS_TICK timeout,
OS_OPT opt,
CPU_TS *p_ts,
OS_ERR *p_err)
p_grp
:指向事件标注组结构体的指针;flags
:等待的事件标志;timeout
:任务挂起等待事件标志组的最大允许时间当为0时,表示一直等待,直到接收到信号;opt
:函数操作选项;
opt | 描述 |
---|---|
OS_OPT_PEND_FLAG_CLR_ALL | 等待“flags” 中的所有指定位被清0 |
OS_OPT_PEND_FLAG_CLR_ANY | 等待“flags” 中的任意指定位被清0 |
OS_OPT_PEND_FLAG_SET_ALL | 等待“flags” 中的所有指定位被置1 |
OS_OPT_PEND_FLAG_SET_ANY | 等待“flags” 中的任意指定位被置1 |
调用上述四个选项时,还可以搭配下面三个选项 | |
OS_OPT_PEND_FLAG_CONSUME | 当等待到指定位后,清0对应位 |
OS_OPT_PEND_BLOCKING | 标志组不满足条件时挂起任务 |
OS_OPT_PEND_NON_BLOCKING | 标志组不满足条件时不挂起任务 |
p_ts
:指向接收等待到事件时的时间戳的变量的指针,一般设置为NULL
;p_err
:指向接收错误代码变量的指针;
函数的返回值:任务实际等待到的事件标志,如果没有则返回0;
13.4 事件标注组实验
- 实验目的:完成以下3个任务:
- start_task:创建task1、task2任务,创建事件标志组;
- task1:按键扫描,根据不同键值将事件标志组相应事件位置1,模拟事件发生;
- task2:同时等待事件标志组中的多个事件位,当这些事件位都置1,就打印提示信息;
- 实验过程:以10.4 消息队列操作实验 为基础:
- 删除task3与消息队列相关代码;
- 定义定义事件标志组指针与事件标志组bit0、bit1
OS_FLAG_GRP flag; // 定义事件标志组指针
#define FLAG_BIT0 (1<<0) // 定义事件标志组bit0,0x01
#define FLAG_BIT1 (1<<1) // 定义事件标志组bit1,0x02
- 创建一个事件标志组:
OSFlagCreate (&flag,"flag",0,&err);/* 创建一个事件标志组 */
- 编写task1、task2任务函数:
/* task1 设置事件标志 */
void task1(void *p_arg)
{
OS_ERR err;
uint8_t key=0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES)
{
printf("KEY0按下,bit0置1!\r\n");
OSFlagPost (&flag,FLAG_BIT0,OS_OPT_POST_FLAG_SET,&err); // bit0置1
}
else if(key == KEY1_PRES)
{
printf("KEY1按下,bit1置1!\r\n");
OSFlagPost (&flag,FLAG_BIT1,OS_OPT_POST_FLAG_SET,&err);// bit1置1
}
OSTimeDly(10,OS_OPT_TIME_DLY,&err);
}
}
/* task2 等待事件标志 */
void task2(void *p_arg)
{
OS_ERR err;
while(1)
{
OSFlagPend (&flag,
FLAG_BIT0|FLAG_BIT1,
0,
OS_OPT_PEND_FLAG_SET_ALL|OS_OPT_PEND_FLAG_CONSUME|OS_OPT_PEND_BLOCKING,
NULL,
&err);
printf("等待到指定事件成立!\r\n");
}
}
代码运行结果:
- 按下key0和key1:
- 若把task2 任务函数中的
OS_OPT_PEND_FLAG_SET_ALL|OS_OPT_PEND_FLAG_CONSUME|OS_OPT_PEND_BLOCKING,
改为S_OPT_PEND_FLAG_SET_ALL|OS_OPT_PEND_BLOCKING,
,即去掉“当等待到指定位后,清0对应位”功能,则在按下key0和key1后,由于对应位一直为1,故一直不阻塞task2:
- 若把task2 任务函数中的
OS_OPT_PEND_FLAG_SET_ALL|OS_OPT_PEND_FLAG_CONSUME|OS_OPT_PEND_BLOCKING,
改为OS_OPT_PEND_FLAG_SET_ANY|OS_OPT_PEND_FLAG_CONSUME|OS_OPT_PEND_BLOCKING,
,即将“等待“flags” 中的所有指定位被置1”更改为“等待“flags” 中的任意指定位被置1”,则在按下key0或key1任意一个后,即可触发“等待到指定事件”:
14. 软件定时器
定时器:从指定时刻开始,经过一个指定时间,然后触发一个超时事件,用户可自定义定时器的周期;
硬件定时器:芯片本身自带的定时器模块,硬件定时器的精度一般很高,每次在定时时间到达后就会自动触发一个定时器中断,用户在中断服务函数中处理信息;
软件定时器:指具有定时功能的软件,可设置定时周期,当指定时间到达后调用回调函数(又叫超时函数),用户在回调函数中处理信息;
软件定时器的优缺点:
- 硬件定时器数量有限,而软件定时器理论上只需有足够多的内存,就可创建多个;
- 使用简单,成本低;
- 软件定时器相对硬件定时器,精度不高,对于需要高精度场合不适用;
单次定时器:一旦定时超时,只会执行一次其软件定时器回调函数,并且不会自动重新开启定时,只能手动重新开始;
周期定时器:一旦启动软件定时器后,就会在执行完回调函数后自动重新开始定时,从而周期地执行其软件定时器的回调函数;
软件定时器的状态:
- 未使用态:软件定时器被定义但未被创建或软件定时器被删除时;
- 停止态:软件定时器被创建但未开启定时器或被停止时;新创建的软件定时器默认处于停止状态;
- 运行态:运行态的定时器,当指定时间到达后,其回调函数会被调用;
- 完成态:当单次定时器定时超时后,软件定时器处于完成态;
软件定时器的特点:
- 支持裁剪:如需使能软件定时器,需将
OS_CFG_TMR_EN
配置项置1;
注:软件定时器的超时回调函数由 软件定时器服务任务 调用,软件定时器的超时回调函数本身不是任务,因此不能在该回调函数中使用可能会导致任务阻塞的API 函数(如系统延时、等待信号量、读取队列等);
- 软件定时器的频率由宏
OS_CFG_TMR_TASK_RATE_HZ
(默认为10)决定,要注意软件定时器的定时器频率并不等于系统时钟节拍的频率,但软件定时器是依靠系统节拍实现定时的,故需要进行换算;
软件定时器时间分辨率 : OSTmrToTicksMult = OSCfg_TickRate_Hz / OSCfg_TmrTaskRate_Hz;
;
*
如:在本教程的实验例程中将宏OSCfg_TmrTaskRate_Hz
配置为 10,将宏OSCfg_TickRate_Hz
配置为 1000,这代表着软件定时器的定时器频率为 10Hz,那么当开启了一个定时器时长为 10 的软件定时器后,软件定时器将会在大约 1000 毫秒后超时(因为频率为 10Hz,所以周期为 100ms,那么总的定时器周期就为 10*100ms=1000ms);
*
详见《UCOS-III开发指南》17.1.6 软件定时器定时频率