参考教程:【正点原子】手把手教你学UCOS-III实时操作系统_哔哩哔哩_bilibili
一、任务信号量
1、任务信号量简介
(1)任务内嵌信号量本质上就是一个信号量,任务信号量是分配于每一个任务的任务控制块结构体中的,因此每一个任务都有独自的任务内嵌信号量。
(2)任务内嵌信号量只能被该任务获取,这也意味着任务内嵌信号量无法广播给多个任务,但是可以由其它任务或者中断释放。
2、任务信号量相关API函数介绍
(1)任务信号量相关API函数概览:
函数 | 描述 |
OSTaskSemPend | 获取任务信号量 |
OSTaskSemPendAbort | 终止任务挂起等待任务信号量 |
OSTaskSemPost | 释放指定任务的任务信号量 |
OSTaskSemSet | 强制设置指定的任务信号量为指定值 |
①释放任务信号量API函数可以用于任务和中断服务函数中。
②接收任务信号量API函数只能用在任务中。
(2)OSTaskSemPend函数:
OS_SEM_CTR OSTaskSemPend //返回任务内嵌信号量更新后的资源数
(
OS_TICK timeout,
OS_OPT opt,
CPU_TS* p_ts,
OS_ERR* p_err
)
形参 | 描述 |
timeout | 任务挂起等待任务内嵌信号量的最大允许时间 |
opt | OS_OPT_PEND_BLOCKING:如果信号量没有资源的话就阻塞任务 OS_OPT_PEND_NON_BLOCKING:如果信号量没有资源任务就直接返回 |
p_ts | 指向接收任务内嵌信号量接收时的时间戳的变量的指针 |
p_err | 指向接收错误代码变量的指针 |
(3)OSTaskSemPost函数:
OS_SEM_CTR OSTaskSemPost //返回任务内嵌信号量更新后的资源数
(
OS_TCB* p_tcb,
OS_OPT opt,
OS_ERR* p_err
)
形参 | 描述 |
p_tcb | 指向任务控制块的指针 |
opt | OS_OPT_POST_NONE:不指定特定的选项 |
p_err | 指向接收错误代码变量的指针 |
(4)OSTaskSemSet函数:
OS_SEM_CTR OSTaskSemSet //返回任务内嵌信号量设置前的资源数
(
OS_TCB* p_tcb,
OS_SEM_CTR cnt,
OS_ERR* p_err
)
形参 | 描述 |
p_tcb | 指向任务控制块的指针 |
cnt | 指定的信号量资源数 |
p_err | 指向接收错误代码变量的指针 |
二、任务信号量实验
1、原理图与实验目标
(1)原理图:
(2)实验目标:
①设计4个任务——start_task、task1、task2、task3:
[1]start_task:用于创建task1、task2和task3任务。
[2]task1:当获取到LED1的硬件资源后,控制LED1约每500ms完成亮暗翻转的状态切换,每完成一次即将资源释放。
[3]task2:当获取到LED2的硬件资源后,控制LED2约每1000ms完成亮暗翻转的状态切换,每完成一次即将资源释放。
[4]task3:按下按键1,获取(或者说霸占)LED1和LED2的硬件资源;按下按键2,释放LED1和LED2的硬件资源。
②预期实验现象:
[1]程序下载到板子上后,两个LED灯闪烁。
[2]按下按键1,LED1和LED2停止闪烁(允许有1秒左右的延迟,这是因为task3一直在释放硬件资源,当task3停止释放时,task1和task2会收到最后一次任务通知,但它们此时可能处在自我阻塞引起的阻塞态,当从阻塞态中解脱时,它们会消耗这最后一次任务通知进行一次LED状态翻转操作)。
[3]按下按键2,LED1和LED2恢复闪烁。
2、实验步骤
(1)将“二值信号量实验”的工程文件夹复制一份,在拷贝版中进行实验。
(2)将UCOS_experiment.c文件中关于信号量的代码全部移除,并更改task1、task2和task3函数的实现。
①task1函数的思路:需要使用LED的硬件资源时调用OSTaskSemPend函数等待任务通知,并且必须等待到通知(同时将通知值清零)才可进行下一步操作——翻转LED1的状态。
②task2函数的思路:需要使用LED的硬件资源时调用OSTaskSemPend函数等待任务通知,并且必须等待到通知(同时将通知值清零)才可进行下一步操作——翻转LED2的状态。
③task3函数的思路:
[1]task1和task2需要task3的通知才能使用LED硬件资源,与二值信号量实验不同,二值信号量实验有单独的两个“队列”分别管理两个LED硬件资源,而本实验则是把task3当作了二值信号量队列管理员,两个二值信号量分别由task1和task2的TCB的任务通知相关成员代替。
[2]task1和task2不断获取和释放(获取实际上指的是接收通知,而释放在程序中并没有体现,可认为发生在下一次调用接收通知函数的前一时刻)硬件资源,那么task3也要不断处理task1和task2的申请,要不断地管理资源的分配,而不是等待按键事件到来的一刻才做一次资源分配操作,于是原本的算法需要做变更。
[3]在未按下任何按键时,task3可以一直做释放LED硬件资源给task1和task2的操作,即使手上没有LED资源也不会陷入阻塞,而是直接执行下一条语句(当然,在实际项目中通常不建议这么做,此处仅仅是为了功能演示),task1和task2哪个先结束阻塞,哪个就先调用OSTaskSemPend函数等待任务通知,或者已经调用OSTaskSemPend函数但之前未等待到通知而进入无限阻塞,但task3一旦分配资源以后就能立刻被唤醒。
[4]在按下按键1之后,task3不做资源分配操作,task1和task2只能无限等待任务通知,无法执行后续的任何操作。
[5]在按下按键2之后(以下执行流程图的情形为按下按键1后再按下按键2),task3可以继续一直做释放LED硬件资源给task1和task2的操作。
void task1(void){
OS_ERR err;
while(1){
OSTaskSemPend(0, OS_OPT_PEND_BLOCKING, NULL, &err);
LED1_Turn(); //LED1状态翻转
OSTimeDly(500, OS_OPT_TIME_DLY, &err);
}
}
void task2(void){
OS_ERR err;
while(1){
OSTaskSemPend(0, OS_OPT_PEND_BLOCKING, NULL, &err);
LED2_Turn(); //LED2状态翻转
OSTimeDly(1000, OS_OPT_TIME_DLY, &err);
}
}
void task3(void){
OS_ERR err;
uint8_t Key_memory = 0, key = 0;
//OSTaskSemPost释放的是计数型信号量,没有上限,需注意资源释放频率
uint16_t task1_blocktime = 0, task2_blocktime = 0;
while(1){
key = Key_GetNum(); //读取按键键值
if(key != 0) Key_memory = key;
if(Key_memory == 1) ;
if(Key_memory == 2 || Key_memory == 0)
{
task1_blocktime++; task2_blocktime++;
if(task1_blocktime >= 50)
{
OSTaskSemPost(&task1_tcb, OS_OPT_POST_NONE, &err);
task1_blocktime = 0;
}
if(task2_blocktime >= 100)
{
OSTaskSemPost(&task2_tcb, OS_OPT_POST_NONE, &err);
task2_blocktime = 0;
}
}
OSTimeDly(10, OS_OPT_TIME_DLY, &err); //自我阻塞10ms
}
}
(3)程序完善好后点击“编译”,然后将程序下载到开发板上,根据程序注释进行调试。
三、任务队列
1、任务队列简介
(1)任务内嵌消息队列本质上就是一个消息队列,任务队列是分配于每一个任务的任务控制块结构体中的,因此每一个任务都有独自的任务内嵌消息队列。
(2)任务内嵌队列只能被该任务获取消息,这也意味着任务内嵌队列中的消息无法广播给多个任务,但是其中的消息可以由其它任务或者中断写入。
2、任务队列相关API函数介绍
(1)任务队列相关API函数概览:
函数 | 描述 |
OSTaskQFlush | 清空任务内嵌消息队列中的所有消息 |
OSTaskQPend | 获取任务内嵌消息队列中的消息 |
OSTaskQPendAbort | 终止任务挂起等待任务内嵌消息队列 |
OSTaskQPost | 发送消息到任务内嵌消息队列 |
①发送任务消息队列API函数可以用于任务和中断服务函数中。
②接收任务消息队列API函数只能用在任务中。
(2)OSTaskQPend函数:
void *OSTaskQPend //指向获取到的消息的万能指针
(
OS_TICK timeout,
OS_OPT opt,
OS_MSG_SIZE* p_msg_size,
CPU_TS* p_ts,
OS_ERR* p_err
)
形参 | 描述 |
timeout | 任务挂起等待任务内嵌消息队列的最大允许时间 |
opt | OS_OPT_PEND_BLOCKING:如果任务队列中没有消息的话就阻塞任务 OS_OPT_PEND_NON_BLOCKING:如果任务队列中没有消息的话就直接返回 |
p_msg_size | 指向用于接收消息大小变量的指针 |
p_ts | 指向接收消息队列接收时的时间戳的变量的指针 |
p_err | 指向接收错误代码变量的指针 |
(3)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 | OS_OPT_POST_FIFO :将发送的消息保存在任务队列的末尾 OS_OPT_POST_NO_SCHED :禁止在本函数内执行任务调度(以上的几种类型可以与该宏进行组合使用) |
p_err | 指向接收错误代码变量的指针 |
四、任务队列实验
1、原理图与实验目标
(1)原理图(按键未画出,接法与任务信号量实验相同):
(2)实验目标:
①设计3个任务——start_task、task1、task2:
[1]start_task:用于创建task1和task2任务。
[2]task1:用于按键扫描,并将键值发送到task2任务消息队列。
[3]task2:用于接收任务消息队列,并打印相关提示信息。
2、实验步骤
(1)将“任务创建与删除实验”的工程文件夹复制一份,在拷贝版中进行实验。
(2)在stm32教程中找到“串口发送(单片机通过串口向电脑发送数据)”实验,复制其中的Serial.c文件和Serial.h文件,将其添加到本实验的项目中,如下图所示,并在主函数中将串口初始化。
int main(void)
{
/*模块初始化*/
LED_Init(); //LED初始化
Key_Init(); //按键初始化
Serial_Init(); //串口初始化
UCOS_Test();
while (1)
{
}
}
(3)修改UCOS_experiment.c文件,删除task3任务的相关内容,并修改task1和task2的实现。
void task1(void)
{
OS_ERR err;
uint8_t key = 0;
while(1)
{
key = Key_GetNum();
if(key == 1 || key == 2) //如果按下按键1或按键2
{
Serial_Printf("发送键值!\r\n");
OSTaskQPost(&task2_tcb, &key, sizeof(key), OS_OPT_POST_FIFO, &err);
}
OSTimeDly(10,OS_OPT_TIME_DLY,&err);
}
}
void task2(void)
{
OS_ERR err;
uint8_t * key;
OS_MSG_SIZE size = 0;
while(1)
{
key = OSTaskQPend(0, OS_OPT_PEND_BLOCKING, &size, NULL, &err);
Serial_Printf("接收到的键值为:%d\r\n", *key);
Serial_Printf("接收到数据长度为:%d字节\r\n", size);
}
}
(4)程序完善好后点击“编译”,然后将程序下载到开发板上,根据程序注释进行调试。