从零开始构建嵌入式实时操作系统5——设计延时功能

在这里插入图片描述

1.前言

人生只有三天,昨天、今天和明天。昨天已然成为过去,明天尚在未来,拥有的不过是今天。
每一个今天,终将成为昨天,每一个明天,也都会成为今天,如此往复,抓住现在,珍惜未来,才能过好这一生。

这段箴言道出了时间的宝贵,我们需要珍惜时间,高效的利用时间。对于人如此,对于软件设计也同样如此,优良的软件设计往往能高效利用处理器的执行时间,最大程度的减少低效率的操作。
在这里插入图片描述

很久以前做过一个测量温湿度的项目,当时使用的额是AHT21这款芯片,该芯片再触发测量之后,需要等待80ms,AHT21的datasheet如下:
在这里插入图片描述
当时设计的代码大致如下:
在这里插入图片描述
程序每次读AHT21温度数据时都需要延时等待100ms,假设程序设计的采样频率是5HZ,就是一秒钟读5次AHT21温度数据。那么MCU在执行过程中每一秒就有500ms在死等”,MCU的有效使用率直接下降为50%!由此可见设计高效的延时对提高系统的执行效率有多么重要。
在这里插入图片描述

2.设计背景

在enuo v0.03版本中增加了延时表,这个延时表并非真正的延时表,它本质上是一个停止表,需要停止运行的任务就存放到该表中,该表中的任务将一直处于停止状态,直到有其它任务恢复此表中的任务。
这种设计只是实现了任务两个状态切换(就绪态和停止态),工程如下:

在这里插入图片描述

3.设计目标

enuo v0.04的设计目标是增加一个阻塞延时功能,当任务需要延时等待一定时间时,任务调用延时函数,此时操作系统会将该任务从就绪表中移除,同时该任务会插入延时列表,延时表中的任务在延时时间没有到期之前不会被执行。当延时完成时任务自动恢复到就绪状态,之后操作系统会调度执行该任务。为完成设计目的需要完成以下功能:
1、增加一个计时功能,使用一个32位的全局变量作为计时器,在系统节拍中断函数中完成自加。
2、每次系统节拍函数进入中断后,完成延时表中的任务延时检测,检测任务延时是否到期。
3、完成任务延时表功能,任务根据延时时间长短插入延时表,延时短的任务在延时列表的前端。

在这里插入图片描述

4.设计环境

硬件环境是使用STM32F401RE为核心的自制开发板,软件环境是使用的KEIL V5.2 开发工具。
在这里插入图片描述

5.设计过程

5.1阻塞式延时

相信大家去吃一些美味小食时都排队的经历吧,这种漫长的等待经历是不是还历历在目?除了在队伍里傻站着和刷刷手机,啥也干不了。
在这里插入图片描述
但是有些商场就有一种很好的等待机制:无线提示器。客户点餐后就可以在一定范围内自由活动,当你点的餐做好了,无线提示器会提示你去取餐。这种方式可以极大的减轻客户站立排队的低效和痛苦。无线提示器如下图:
在这里插入图片描述
上面展示了两种等待模式:
1、阻塞式等待
2、非阻塞式等待

阻塞式等待和非阻塞式等待关注的是任务在执行等待时的状态,阻塞式等待是指任务在等待期间会被挂起(处于停止状态),等待结束之后任务将会继续运行,记忆方法:当前任务被阻塞(不运行)。非阻塞式等待任务在等待期间不会被挂起,记忆方法:当前任务被不阻塞(一直运行)

回到上面排队的例子,第一种站立式排队模式,当你处在这种等待模式下时你必须一直保存这种排队等待的状态,直到你买到你需要的商品,这种模式就是非阻塞式等待
第二种无线提示器模式,当客户完成商品付款后,就可以在一定范围内自由活动,直到收到无线提示器会提示就可以直接去取商品,这种模式就是阻塞式等待。非常明显在大多数情况下阻塞式等待会更加高效

5.2延时策略

实现等待很多种策略,常见的策略有以下两种:

1、倒计时法
2、闹钟法

大家一定都看过火箭发射的场面,那清晰洪亮的倒计时声仿佛就在耳边回响:

10…9…8…7…6…5…4…3…2…1…0

火箭发射的就是使用的倒计时法,这种策略只需要关注剩余时间。

大部分人早上起床的时候肯定是被设定的闹钟叫醒,这种策略使用起来十分方便,只用关注设定的时间即可。

倒计时法的算法逻辑是:每一次判断剩余时间是否为0,如果剩余时间不为0,就将剩余时间减一,如果剩余时间为0则完成延时等待。

闹钟法的算法逻辑是:比较当前时间是否等于设定时间,不等于设定时间则继续等待,等于设定时间则完成延时等待。

在这里插入图片描述

5.3两种策略对比

倒计时法需要完成一次时间判断操作和一次时间减法操作,闹钟法只需要完成一次时间判断操作。

假设现在有100个任务,使用倒计时法需要完成100次时间判断操作和100次时间减法操作。而闹钟法只要完成100次时间判断操作

如果我们将延时任务按照顺序排列,延时短的任务放在延时队列的前端,只就意味着最快完成延时的任务肯定是延时队列的第一个任务,因此我们只用判断延时队列的第一个任务是否完成延时操作即可

假设现在有100个任务,使用倒计时法需要完成100次时间减法操作和1次判断延时队列首个任务操作。而闹钟法只要完成1次判断延时队列首个任务操作。 由于闹钟法执行步骤较少,enuo使用闹钟法机制。

闹钟法机制虽然执行效率高,但是同时存在一个时间归零问题,我们知道当前时间是23点,如果需要等待2小时,闹钟时间就变成了1点,因此闹钟法需要额外处理时间归零问题

在这里插入图片描述

5.4系统节拍

操作系统需要一个周期性的时钟源,这个时钟源称之为时钟节拍。为了生成时周期性钟节拍,通常需要使用硬件定时器,配置硬件定时器产生一个固定频率的中断(通常为10~1000Hz之间),当中断发生时,中断服务程序调用操作系统中的一个特殊程序:系统时钟节拍服务。
请添加图片描述

系统时钟节拍可以配置为10Hz,也可以配置为1000Hz。时钟节拍值越大说明硬件定时器产生的中断越频繁,系统时钟节拍服务就越频繁的执行。

高频率时钟节拍的优点:

1、提高操作系统时间管理精度
2、提高任务抢占准确度

例如10Hz的时钟节拍,意味着的操作系统执行时间粒度为100ms,系统中的周期性事件最快为100ms一次,无法有更高的精度。例如1000Hz的时钟节拍,此时的执行时间粒度就提高了100倍,此时系统中的周期性事件最快为1ms一次,时间精度可以达到1ms。

enuo系统定义一个全局变量heartbeat ,记录了系统节拍时间。系统中断服务程序中执行heartbeat 自加工作,更新系统节拍时间。代码如下:

volatile uint32_t  heartbeat ;
/*********************************************************************************************************
* @名称	: SysTick_Handler
* @描述	: 系统中断服务程序
**********************************************************************************************************/
void SysTick_Handler(void)
{	
	/* 系统节拍心跳计数  */	
	heartbeat++;
	/* 延时处理  */	
	delay_handle();
	/* 开始任务切换调度 */
	scheduler_task();
}

5.5加入延时队列

需要延时等待的任务执行延时函数delay,延时函数会将指定的任务从原有的链表中移除,并将任务插入延时列表,延时列表中的任务将不会被执行。
任务延时的结束时间是当前的系统节拍时间加上延时时间,延时列表中的任务按照结束时间从小到大进行排列,延时列表的第一个任务就是最快被唤醒的任务。
延时函数中末尾有一次主动任务调度请求,此时操作系统将立即执行下一个就绪的任务。代码如下:

/*********************************************************************************************************
* @名称	: 延时
* @描述	: 延时单位为一个系统心跳节拍
**********************************************************************************************************/
void delay( uint32_t delay ,task_tcb_t *task)
{
	/* 保存滑动指针位置 */
	list_node_t * const new_node = &task->link;
	task->link.sort_value = heartbeat + delay;
	/* 移除原有链表关系 */
	list_remove(new_node);
	/* 插入滑动指针末尾 */	
	list_sort_insert( &delay_list , new_node);
	/* 调度任务 */
	scheduler_task();
}

在这里插入图片描述
系统开始运行时task0,task1,task2都处于就绪状态,当task1执行延时操作后,系统将task1从就绪列表中移除,同时将task1插入延时列表中,此时系统任务的总体状态如下:
在这里插入图片描述

5.6延时处理

系统中断服务程序中执行heartbeat自加工作,更新系统节拍时间后执行延时处理函数delay_handle,进入延时处理函数后先判断延时列表中的任务数量;如果延时列表中的任务数量不为0,就会判断延时列表第一个任务是否完成延时;如果延时列表第一个任务延时时间到期,就会将任务从延时列表中移除,同时将该任务加入就绪列表末尾。代码如下:

/*********************************************************************************************************
* @名称	: delay_handle
* @描述	: 延时处理  	
**********************************************************************************************************/
void delay_handle(void)
{
	/* 判断延时列表是否有任务  */	
	if( delay_list.node_value != 0 )
	{
		/* 判断延时列表第一个任务 是否完成延时 */
		if(delay_list.head.next->sort_value <= heartbeat)
		{
			list_node_t * delay_node;
			delay_node = delay_list.head.next;
			/* 移除原有链表关系 */
			list_remove(delay_node);
			/* 插入滑动指针末尾 */	
			list_insert_sliding_pointer_end( &ready_list , delay_node);			
		}
	}
}
/*********************************************************************************************************
* @名称	: SysTick_Handler
* @描述	: 系统中断服务程序
**********************************************************************************************************/
void SysTick_Handler(void)
{	
	/* 系统节拍心跳计数  */	
	heartbeat++;
	/* 延时处理  */	
	delay_handle();
	/* 开始任务切换调度 */
	scheduler_task();
}

假设task1,task2在延时列表中,此时系统任务的状态如下:
在这里插入图片描述
当系统节拍时间更新后,task1延时时间到期,系统将task1从延时列表中移除,将task1插入就绪列表中,此时系统任务的总体状态如下:
在这里插入图片描述

6.运行结果

测试任务代码如下:

/*********************************************************************************************************
* @名称	:task0 
* @描述	:任务0
**********************************************************************************************************/
void task0(void)
{
	while(1)
	{
		/* 测试跟踪 */
		task_debug_num0++;	
		test_function();
		/* 延时 */
		delay(5,&my_task0);			
	}
}           
/*********************************************************************************************************
* @名称	: task1
* @描述	: 任务1
**********************************************************************************************************/  
void task1(void)
{	
	while(1)
	{
		task_debug_num1++;	/* 测试跟踪 */
		test_function();
		/* 延时 */		
		delay(50,&my_task1);
	}
}
/*********************************************************************************************************
* @名称	: task2
* @描述	: 任务2
**********************************************************************************************************/ 
void task2(void)
{	
	while(1)
	{
		task_debug_num2++;	/* 测试跟踪 */
		test_function();
		/* 延时 */
		delay(100,&my_task2);
	}
}

工程中有3个任务task0,task1,task2,其中task0每执行一次就延时5个节拍,task1每执行一次就延时50个节拍,task2每执行一次就延时100个节拍。

因此运行一段时间后task0,task1,task2各种的task_debug_num之比约为20:2:1

仿真结果如下:
在这里插入图片描述
task_debug_num之比约为20:2:1 ,实现延时功能的设计目标。

总结:本文讲解了延时的不同模式和策略,描述了enuo延时功能的设计过程

希望获取源码的朋友们在评论区里留言。

未完待续…
实时操作系统系列将持续更新
创作不易希望朋友们点赞,转发,评论,关注。
您的点赞,转发,评论,关注将是我持续更新的动力
作者:李巍
Github:liyinuoman2017
CSDN:liyinuo2017
今日头条:程序猿李巍

在这里插入图片描述

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liyinuo2017

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值