FreeRTOS简单内核实现5 阻塞延时

文章目录

  • 0、思考与回答
    • 0.1、思考一
    • 0.2、思考二
    • 0.3、思考三
  • 1、创建空闲任务
  • 2、实现阻塞延时
  • 3、修改任务调度策略
  • 4、提供延时时基
    • 4.1、SysTick
    • 4.2、xPortSysTickHandler( )
    • 4.3、xTaskIncrementTick( )
  • 5、实验
    • 5.1、测试
    • 5.2、待改进


0、思考与回答

0.1、思考一

为什么 FreeRTOS简单内核实现3 任务管理 文章中实现的 RTOS 内核不能看起来并行运行呢?

Task1 延时 100ms 之后执行 taskYIELD() 切换到 Task2,Task2 延时 500ms 之后执行 taskYIELD() 再次切换 Task1 ,在延时期间两个任务均占用 MCU ,所以只能一个任务执行完再执行另外一任务,可以看出 MCU 处理这两个任务的大部分时间浪费在了无用的延时上

0.2、思考二

有什么方法解决吗?

引入空闲任务,当有任务执行延时操作时产生任务调度,但因为 MCU 时刻在运行程序,不会说中途休息一会儿,所以当所有任务都处于延时状态时,MCU 必须要有一个空闲任务来执行,当有任务从延时阻塞状态恢复时,再次产生任务调度执行从阻塞状态恢复的任务

0.3、思考三

具体怎么实现呢?

  1. 创建空闲任务
  2. 实现阻塞延时
  3. 修改任务调度策略
  4. 提供延时时基

下面我们就来逐点实现以上 4 点内容

1、创建空闲任务

空闲任务和普通任务一样,只不过任务函数为空而已,由于是静态创建任务,所以需要提前定义好任务栈,任务控制块、任务句柄和任务函数,如下所示

/* task.c */
// 空闲任务参数
TCB_t IdleTaskTCB;
#define confgiMINIMAL_STACK_SIZE 128
StackType_t	IdleTasKStack[confgiMINIMAL_STACK_SIZE];

// 空闲任务函数体
void prvIdleTask(void *p_arg)
{
    for(;;){}
}

由于空闲任务始终要被创建,因此一般选择将其放在启动调度器 vTaskStartScheduler() 函数中,如下所示

/* task.c */
// 启动任务调度器
void vTaskStartScheduler(void)
{
	// 创建空闲任务
	TaskHandle_t xIdleTaskHandle = xTaskCreateStatic((TaskFunction_t)prvIdleTask,
										(char *)"IDLE",
										(uint32_t)confgiMINIMAL_STACK_SIZE,
										(void *)NULL,
										(StackType_t *)IdleTasKStack,
										(TCB_t *)&IdleTaskTCB);
	// 将空闲任务插入到就绪链表中
	vListInsertEnd(&(pxReadyTasksLists), 
				   &(((TCB_t *)(&IdleTaskTCB))->xStateListItem));
	
	pxCurrentTCB = &Task1TCB;
	if(xPortStartScheduler() != pdFALSE){}
}

2、实现阻塞延时

首先需要在任务控制块结构体中增加一个变量用于记录任务阻塞延时的时间

/* task.h */
// 任务控制块
typedef struct tskTaskControlBlock
{
	// 省略之前的结构体成员定义
    TickType_t            xTicksToDelay;                        // 用于延时
}tskTCB;

阻塞延时与普通延时的区别就是普通延时会一直占用 MCU ,而阻塞延时执行后会产生任务调度暂时让出 MCU ,让其执行处于运行状态的任务,阻塞延时函数如下所示

/* task.c */
// 阻塞延时函数
void vTaskDelay(const TickType_t xTicksToDelay)
{
    TCB_t *pxTCB = NULL;

    // 获取当前要延时的任务 TCB
    pxTCB = (TCB_t *)pxCurrentTCB;
    // 记录延时时间
    pxTCB->xTicksToDelay = xTicksToDelay;
    // 主动产生任务调度,让出 MCU 
    taskYIELD();
}

/* task.h */
// 函数声明
void vTaskDelay(const TickType_t xTicksToDelay);

3、修改任务调度策略

注意:需要明白的很重要的一点是任务调度策略是寻找合适的 pxCurrentTCB 指针

根据目前实现的 RTOS 内核,发生任务调度有如下两种情况

  1. 手动调用 taskYIELD() 函数
  2. 执行 vTaskDelay() 阻塞延时函数

之前的任务调度策略为 Task1 和 Task2 两个任务轮流执行,现在加入了空闲任务和阻塞延时后需要修改任务调度策略,目前理想的任务调度策略应该如下所示

  1. 如果发生任务调度时运行的任务为 IdleTask,就按顺序始终尝试去执行未阻塞的 Task1 或 Task2
  2. 如果发生任务调度时运行的任务为 Task1,就按顺序尝试执行未阻塞的 Task2 或 Task1
  3. 如果发生任务调度时运行的任务为 Task2,就按顺序尝试执行未阻塞的 Task1 或 Task2
  4. 如果步骤 1 ~ 3 尝试执行的任务都已阻塞,就执行空闲任务

上述步骤 1 ~ 3 中笔者描述的尝试执行的任务是有顺序的,比如步骤 2 会先尝试执行未阻塞的 Task2,不满足才会尝试执行未阻塞的 Task1,这样在手动调用 taskYIELD() 函数发生任务调度时才会切换任务,否则达不到任务切换的目的

具体的任务调度函数如下所示

/* task.c */
// 任务调度函数
void vTaskSwitchContext(void)
{
    if(pxCurrentTCB == &IdleTaskTCB)
    {
        if(Task1TCB.xTicksToDelay == 0)
        {
            pxCurrentTCB = &Task1TCB;
        }
        else if(Task2TCB.xTicksToDelay == 0)
        {
            pxCurrentTCB = &Task2TCB;
        }
        else
        {
            return;
        }
    }
    else
    {
        if(pxCurrentTCB == &Task1TCB)
        {
            if(Task2TCB.xTicksToDelay == 0)
            {
                pxCurrentTCB = &Task2TCB;
            }
            else if(pxCurrentTCB->xTicksToDelay != 0)
            {
                pxCurrentTCB = &IdleTaskTCB;
            }
            else
            {
                return;
            }
        }
        else if(pxCurrentTCB == &Task2TCB)
        {
            if(Task1TCB.xTicksToDelay == 0)
            {
                pxCurrentTCB = &Task1TCB;
            }
            else if(pxCurrentTCB->xTicksToDelay != 0)
            {
                pxCurrentTCB = &IdleTaskTCB;
            }
            else
            {
                return;
            }
        }
    }
}

4、提供延时时基

4.1、SysTick

阻塞延时本质是延时函数,涉及到时间就需要提供时间基准,我们在任务控制块结构体中使用了一个名为 xTicksToDelayuint32_t 类型的变量记录每个任务的延时时间,那这个延时时间什么时候递减呢?

通常 MCU 都有一个名为 SysTick 的滴答定时器,其会按照某一固定周期产生中断,一般用来为 MCU 提供时间基准,对于 STM32 HAL 库来说,其滴答定时器只用于 HAL_Delay() 延时函数,我们可以在其中断 SysTick_Handler() 函数中对任务的延时时间进行递减操作,那如何控制滴答定时器产生中断的周期呢?

对于配置好时钟树,然后由 STM32CubeMX 生成的代码中,滴答定时器会自动初始化并启动滴答定时器中断,初始化流程如下所示

  1. HAL_RCC_DeInit( )
  2. -> HAL_InitTick( )
  3. -> HAL_SYSTICK_Config( )
  4. -> SysTick_Config( )

初始化流程中有一个重要的参数用于配置滴答定时器的中断周期(频率),默认为 1KHZ(1ms),读者可按需要对其做相应修改,具体定义如下所示

/* stm32f4xx_hal.c */
HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT;  /* 1KHz */

/* stm32f4xx_hal.h */
typedef enum
{
  HAL_TICK_FREQ_10HZ         = 100U,
  HAL_TICK_FREQ_100HZ        = 10U,
  HAL_TICK_FREQ_1KHZ         = 1U,
  HAL_TICK_FREQ_DEFAULT      = HAL_TICK_FREQ_1KHZ
} HAL_TickFreqTypeDef;

4.2、xPortSysTickHandler( )

xPortSysTickHandler() 本质是滴答定时器中断服务函数,作为 RTOS 的心跳在其中对任务的阻塞延时参数做处理,每次心跳一次就将阻塞延时参数递减,直到减到 0 之后使任务从阻塞状态恢复,具体如下所示

/* port.c */
// SysTick 中断服务函数
void xPortSysTickHandler(void)
{
    // 关中断
    vPortRaiseBASEPRI();
    // 更新任务延时参数
    xTaskIncrementTick();
    // 开中断
    vPortSetBASEPRI(0);
}
/* portMacro.h */
#define xPortSysTickHandler         SysTick_Handler

注意:由于我们重新实现了 SysTick 中断服务函数,因此在 stm32f4xx_it.c 中自动生成的 SysTick_Handler 函数需要注释或者直接删除

4.3、xTaskIncrementTick( )

该函数为具体的处理函数,其遍历链表中每个链表项(任务),如果链表项的延时参数不为 0 就将其递减,直到减少到 0 表示该任务延时阻塞到期,然后产生任务调度,具体如下所示

/* task.c */
// 滴答定时器计数值
static volatile TickType_t xTickCount = (TickType_t)0U;
// 更新任务延时参数
void xTaskIncrementTick(void)
{
	TCB_t *pxTCB = NULL;
	ListItem_t *pxListItem = NULL;
	List_t *pxList = &pxReadyTasksLists;
	uint8_t xSwitchRequired = pdFALSE;
	
	// 更新 xTickCount 系统时基计数器
	const TickType_t xConstTickCount = xTickCount + 1;
	xTickCount = xConstTickCount;
	
	// 检查就绪链表是否为空
	if(listLIST_IS_EMPTY(pxList) == pdFALSE) 
	{
		// 不为空获取链表头链表项
		pxListItem = listGET_HEAD_ENTRY(pxList);

		// 迭代就绪链表所有链表项
		while(pxListItem != (ListItem_t *)&(pxList->xListEnd)) 
		{
			// 获取每个链表项的任务控制块 TCB
			pxTCB = (TCB_t *)listGET_LIST_ITEM_OWNER(pxListItem);
			
			// 延时参数递减
			if(pxTCB->xTicksToDelay > 0){
				pxTCB->xTicksToDelay--;
			}
			else{
				xSwitchRequired = pdTRUE;
			}
			// 移动到下一个链表项
			pxListItem = listGET_NEXT(pxListItem);
		}
	}
	// 如果就绪链表中有任务从阻塞状态恢复就产生任务调度
	if(xSwitchRequired == pdTRUE){
		// 产生任务调度
		taskYIELD();
	}
}

/* task.h */
// 函数声明
void xTaskIncrementTick(void);

5、实验

5.1、测试

测试程序与 FreeRTOS简单内核实现3 任务管理 几乎一致,主要是将任务函数体内的延时由 HAL_Delay() 修改为本文创建的阻塞延时 vTaskDelay() 函数,然后删除掉 taskYIELD() 函数即可,具体如下所示

/* main.c */
/* USER CODE BEGIN Includes */
#include "FreeRTOS.h"
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
extern List_t pxReadyTasksLists;

TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE                    128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;

TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE                    128
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;

// 任务 1 入口函数
void Task1_Entry(void *parg)
{
	for(;;)
	{
		HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
		vTaskDelay(100);
	}
}
// 任务 2 入口函数
void Task2_Entry(void *parg)
{
	for(;;)
	{
		HAL_GPIO_TogglePin(ORANGE_LED_GPIO_Port, ORANGE_LED_Pin);
		vTaskDelay(500);
	}
}
/* USER CODE END PV */

/* USER CODE BEGIN 2 */
// 使用链表前手动初始化
prvInitialiseTaskLists();
// 创建任务 1 和 2
Task1_Handle = xTaskCreateStatic((TaskFunction_t)Task1_Entry,
								 (char *)"Task1",
								 (uint32_t)TASK1_STACK_SIZE,
								 (void *)NULL,
								 (StackType_t *)Task1Stack,
								 (TCB_t *)&Task1TCB);
														
Task2_Handle = xTaskCreateStatic((TaskFunction_t)Task2_Entry,
								 (char *)"Task2",
								 (uint32_t)TASK2_STACK_SIZE,
								 (void *) NULL,
								 (StackType_t *)Task2Stack,
								 (TCB_t *)&Task2TCB );
// 将两个任务插入到就绪链表中
vListInsertEnd(&(pxReadyTasksLists),&(((TCB_t *)(&Task1TCB))->xStateListItem));
vListInsertEnd(&(pxReadyTasksLists),&(((TCB_t *)(&Task2TCB))->xStateListItem));
// 启动任务调度器,永不返回
vTaskStartScheduler();
/* USER CODE END 2 */

启动任务调度器后的程序执行流程如下所示

  1. 创建空闲任务并加载到就绪链表中,此时就绪链表中有 Task1、Task2 和 IdleTask 三个任务
  2. 手动指定 pxCurrentTCB = &Task1TCB; ,让 Task1 成为第一个被运行的任务
  3. 然后按照 “3、修改任务调度策略” 小节中描述的理想的任务调度策略从步骤 2 进行任务调度

使用逻辑分析仪捕获 GREEN_LED 和 ORANGE_LED 两个引脚的电平变化,具体如下图所示

![[Kernel_5.1.png]]

从图上可以发现两个任务几乎是并行运行的,和我们期待的 Task1 引脚电平每隔 100 ms 翻转一次,Task2 引脚电平每隔 500ms 翻转一次效果一致

5.2、待改进

当前 RTOS 简单内核已实现的功能有

  1. 静态方式创建任务
  2. 手动切换任务
  3. 临界段保护
  4. 任务阻塞延时

当前 RTOS 简单内核存在的缺点有

  1. 不支持任务优先级
  2. 任务调度策略是基于两个任务的简单调度
  3. 不支持时间片轮询

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/713811.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++ 47 之 函数调用运算符重载

#include <iostream> #include <string> using namespace std;class MyPrint{ public:// 重载小括号() 重载谁operator后就紧跟谁的符号void operator()(string txt){cout << txt << endl;} };class MyAdd{ public:int operator()(int a, int b){retur…

springboot汽车配件管理系统(源码+sql+论文报告)

绪论 1.1 研究意义和背景 随着我国经济的持续发展&#xff0c;汽车已经逐步进入了家庭。汽车行业的发展&#xff0c;也带动了汽车配件行业的快速发展。 汽车配件行业的迅猛发展&#xff0c; 使得汽配行业的竞争越来越激烈。如何在激烈的竞争中取胜&#xff0c;是每家汽车零部…

Java实现异步开发的方式

1&#xff09;、继承 Thread 2&#xff09;、实现 Runnable 接口 3&#xff09;、实现 Callable 接口 FutureTask &#xff08;可以拿到返回结果&#xff0c;可以处理异常&#xff09; 4&#xff09;、使用线程池 区别&#xff1a;1、2&#xff09;不能得到返回值 …

人工智能对零售业的影响

机器人、人工智能相关领域 news/events &#xff08;专栏目录&#xff09; 本文目录 一、人工智能如何改变零售格局二、利用人工智能实现购物体验自动化三、利用人工智能改善库存管理四、通过人工智能解决方案增强客户服务五、利用人工智能分析消费者行为六、利用 AI 打造个性化…

C++封装TCP类,包括客户端和服务器

头文件 XTcp.h #ifndef XTCP_H #define XTCP_H#ifdef WIN32 #ifdef XSOCKET_EXPORTS #define XSOCKET_API __declspec(dllexport) #else #define XSOCKET_API __declspec(dllimport) #endif #else #define XSOCKET_API #endif#include <string> XSOCKET_API std::string…

【git使用四】git分支理解与操作(详解)

目录 &#xff08;1&#xff09;理解git分支 主分支&#xff08;主线&#xff09; 功能分支 主线和分支关系 将分支合并到主分支 快速合并 非快速合并 git代码管理流程 &#xff08;2&#xff09;理解git提交对象 提交对象与commitID Git如何保存数据 示例讲解 &a…

Bio-Info每日一题:Rosalind-07-Mendel‘s First Law(孟德尔第一定律 python实现)

&#x1f389; 进入生物信息学的世界&#xff0c;与Rosalind一起探索吧&#xff01;&#x1f9ec; Rosalind是一个在线平台&#xff0c;专为学习和实践生物信息学而设计。该平台提供了一系列循序渐进的编程挑战&#xff0c;帮助用户从基础到高级掌握生物信息学知识。无论你是初…

C++前期概念(重)

目录 命名空间 命名空间定义 1. 正常的命名空间定义 2. 命名空间可以嵌套 3.头文件中的合并 命名空间使用 命名空间的使用有三种方式&#xff1a; 1:加命名空间名称及作用域限定符&#xff08;::&#xff09; 2:用using将命名空间中某个成员引入 3:使用using namespa…

Milvus Cloud 问答机器人 上线!构建企业级的 Chatbot

01. 背景 早些时候我们在社区微信群发出了一份关于Milvus Cloud 自动问答机器人的调研问卷。 调研受到了社区同学的积极响应,很快我们就收到了很多热心用户的回复。 基于这些回复,我们整理出了 Milvus Cloud Chatbot 的形态: 以功能使用和文档查询为核心 提供聊天和搜索双形…

【尚庭公寓SpringBoot + Vue 项目实战】图片上传(十)

【尚庭公寓SpringBoot Vue 项目实战】图片上传&#xff08;十&#xff09; 文章目录 【尚庭公寓SpringBoot Vue 项目实战】图片上传&#xff08;十&#xff09;1、图片上传流程2、图片上传接口查看3、代码开发3.1、配置Minio Client3.2、开发上传图片接口 4、异常处理 1、图片…

【每日刷题】Day66

【每日刷题】Day66 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 小乐乐改数字_牛客题霸_牛客网 (nowcoder.com) 2. 牛牛的递增之旅_牛客题霸_牛客网 (nowcoder.com)…

算法体系-20 第二十节暴力递归到动态规划

前言 动态规划模型从尝试暴力递归到傻缓存到动态规划 四种模型和体系班两种模型一共六种模型 0.1 从左往右模型 0.2 范围讨论模型范围尝试模型 &#xff08;这种模型特别在乎讨论开头如何如何 结尾如何如何&#xff09; 玩家博弈问题&#xff0c;玩家玩纸牌只能那左或者右 0.3 …

Docker Jenkins(改错版本)

Devops:它强调开发(Development)和运维(Operations)团队之间的协作.实现更快,更可靠的软件交付部署. JenKins是一个开源的自动化服务器,广泛用于构建,测试和部署软件项目.它是持续集成(CI)和持续交付/部署(CD)的工具.JenKins是实现DevOps实践的重要工具. 前端项目部署一般流程:…

【javaEE-有关CPU进程和线程实现的并发编程及二者的区别】

&#x1f525;&#x1f525;&#x1f525;有关进程并发编程开发的成本问题 这次之前其实我们所有的写的程序都是使用单核心来运行的&#xff0c;但是一般我们的计算机都有很多核心&#xff0c;如果我们编程的时候&#xff0c;只使用一个核心的话&#xff0c;其实这是一个非常大…

通俗范畴论2 有向图与准范畴

退一步海阔天空&#xff0c;在正式进入范畴论之前&#xff0c;我们可以重新审视一下我们是如何认识世界的&#xff0c;有了这个对人类认识世界过程的底层理解&#xff0c;可以帮助我们更好地理解范畴论。 对于人类认识世界&#xff0c;最神奇的一点就是这个世界居然是可以认识…

【C语言】解决C语言报错:Race Condition

文章目录 简介什么是Race ConditionRace Condition的常见原因如何检测和调试Race Condition解决Race Condition的最佳实践详细实例解析示例1&#xff1a;缺乏适当的同步机制示例2&#xff1a;错误使用条件变量 进一步阅读和参考资料总结 简介 Race Condition&#xff08;竞争条…

element-ui input输入框和多行文字输入框字体不一样

页面中未作样式修改&#xff0c;但是在项目中使用element-ui input输入框和多行文字输入框字体不一样&#xff0c;如下图所示&#xff1a; 这是因为字体不一致引起的&#xff0c;如果想要为Element UI的输入框设置特定的字体&#xff0c;你可以在你的样式表中添加以下CSS代码…

尚品汇-(二)

本地域名解析器&#xff1a;当我们在浏览器输入域名的时候&#xff0c;它首先找的不是远程的DNS&#xff0c;而是去本地的host中去找这个域名有没有对应的&#xff0c;如果有对应的&#xff0c;那么就根据对应的ip进行访问 一&#xff1a;环境安装 1.安装JAVA 运行环境 第一…

MySQL之优化服务器设置(四)

优化服务器设置 InnoDB的IO配置 双写缓冲(Doublewrite Buffer) InnoDB用双写缓冲来避免页没写完整所导致的数据损坏。当一个磁盘写操作不能完整地完成时&#xff0c;不完整的页写入就可能发生&#xff0c;16KB的页可能只有一部分被写到磁盘上。有多种多样的原因(崩溃、Bug&am…