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;// 获取当前要延时的任务 TCBpxTCB = (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)) {// 获取每个链表项的任务控制块 TCBpxTCB = (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. 不支持时间片轮询

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

hbuilderx如何创建html模板

需求:想要将34.html文件的内容作为一个模板,以便后续直接能创建类似内容的html文件 1 首先ctrlc复制模板文件 2 在顶部菜单栏点击 文件 -> 新建 或者使用快捷键 CtrlN 3 在弹出的对话框中选择【自定义模板】 4 将第一步复制的模板文件复制到弹出的文件…

Android Studio项目升级报错:Namespace not specified

原项目升级AGP到8.0时报错: Namespace not specified. Specify a namespace in the modules build file: C:\Users\Administrator\Desktop\MyJetpack\app\build.gradle. See https://d.android.com/r/tools/upgrade-assistant/set-namespace for information about…

IT人的拖延——这个任务太复杂,太难了怎么办?

随着科技的发展,IT人需要不断地运用新技术来解决更多传统方式难以解决的问题,有些问题真的不是不想解决,而是真的太复杂,太难了,根本不知道从何开始,也没有什么前辈的经验可以借鉴。我们这些对事情难度的认…

软链接和硬链接的详解 (Linux系统下)

文章目录 硬链接的引入软链接和硬链接的形成软链接硬链接 软硬链接区别的探究硬链接数结语 硬链接的引入 当我们在命令行中输入ll时会出现很多行信息,详情请看下面的图 ~~~~εεε( ̄▽ ̄) 我在之前的几篇Linux的文章也讲过哦 (o゚v…

ARM32开发--电源管理单元

知不足而奋进 望远山而前行 目录 文章目录 前言 学习目标 学习内容 PMU 电源域 VDD/VDDA域 备份域 1.2V域 省电模式 睡眠模式 深度睡眠模式 待机模式 几种模式总结 WFI和WFE指令 案例需求 模式初始化 源码 总结 前言 在嵌入式系统中,有效的电池管…

buuctf-findKey

exe文件 运行发现这个窗口,没有任何消息 32位 进入字符串就发现了flag{ 左边红色代表没有F5成功 我们再编译一下(选中红色的全部按p) LRESULT __stdcall sub_401640(HWND hWndParent, UINT Msg, WPARAM wParam, LPARAM lParam) {int v5; // eaxsize_t v6; // eaxDWORD v7; /…

【python基础语法1】注释,变量与运算符

这里写自定义目录标题 一、注释分类注意 二、变量变量的声明变量的命名注意 变量的交换常量 三、数据类型分类六大标准数据类型Number 数字类型 分类:2个内置方法 type 和 id自动类型转换强制类型转换容器类型分类:五个字符串类型 str列表类型 list内置函…

【Numpy】一文向您详细介绍 np.round()

【Numpy】一文向您详细介绍 np.round() 下滑即可查看博客内容 🌈 欢迎莅临我的个人主页 👈这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地!🎇 🎓 博主简介:985高校的普通本硕,…

Vue2+Element-ui实现el-table表格自适应高度

效果图 新建指令 Vue.directive(height, {inserted(el, _binding, vnode) {const paginationRef vnode.context.$refs.paginationRefconst calculateHeight () > {const windowHeight window.innerHeightconst topOffset el.getBoundingClientRect().topconst otherEle…

debug调试高级功能 断点、布局 及Android Studio常用快捷按键使用详情

文章目录 debug断点篇:打临时断点(只用一次):alt断点条件断点:在断点上,点击右键,在Condition那里,设置我们需要的值,循环就会自动停到我们设置的那个值那里依赖断点&…

Jmeter多个请求按照比例并发压测的几种方式

🍅 视频学习:文末有免费的配套视频可观看 🍅 点击文末小卡片 ,免费获取软件测试全套资料,资料在手,涨薪更快 一、需求 在压测的过程中,为了能够压测整个链路,通常需要多个接口进行并…

Markdown如何分页操作

Markdown导出分页操作 在平时的文档导出过程中Markdown过程中会出现因为不能分页导致的排版问题。 排版问题在将Markdown文档导出为PDF或其他格式时尤为明显。当文档内容超过一页时,无法自动调整页面布局,导致内容不连续,甚至导致图片或表格…

【每日刷题】Day66

【每日刷题】Day66 🥕个人主页:开敲🍉 🔥所属专栏:每日刷题🍍 🌼文章目录🌼 1. 小乐乐改数字_牛客题霸_牛客网 (nowcoder.com) 2. 牛牛的递增之旅_牛客题霸_牛客网 (nowcoder.com)…

预编译、函数变量提升

函数声明会覆盖变量的声明,也就是会提升到最前面。 形参传进来相当于变量声明,所以当有函数声明时,会被覆盖。

计算机组成原理之定点加法与减法运算

文章目录 补码的加减法算法的流程与逻辑实现溢出判断溢出原因单符号位判断双符号位(变形补码) 基本的加法/减法器舍入方法习题 补码的加减法 数用补码表示,符号位参与运算 考虑几个问题? 1.实际操作能否只取决于操作码&#xff1f…

python-求分数序列和

[题目描述]: 输入: 输入一行一个正整数n(n≤30)。输出: 输出一行一个浮点数,表示分数序列前n 项的和,精确到小数点后4位。样例输入1 2 样例输出1 3.5000 来源/分类(难度系数:一星)…

和鲸科技执行总裁殷自强:面向空间数据协同分析场景的模型生命周期管理方法

导读: 由 ACM SIGSPATIAL 中国分会主办的第五届空间数据智能学术会议(SpatialDI 2024)于 2024 年 4 月 25 日- 27 日在南京圆满召开,主题为“ AGI 时代下的空间数据智能”,旨在深入推动空间数据智能研究的理论进步与应…

模型实战(23)之 yolov10 使用总结及训练自己的数据集

yolov10 使用总结及训练自己的数据集 0. yolov10 原理分析 此处参考:https://blog.csdn.net/CVHub/article/details/139204248论文:https://arxiv.org/pdf/2405.14458源码:https://github.com/THU-MIG/yolov10 论文原理分析: 创新: 双标签分配策略 众所周知,标签分配策略…

【嵌入式DIY实例】-Nokia 5110显示DS3231 RTC数据

Nokia 5110显示DS3231 RTC数据 文章目录 Nokia 5110显示DS3231 RTC数据1、硬件准备与接线2、代码实现本文将介绍如何使用 ESP8266 NodeMCU 板和 DS3231 RTC 模块制作一个简单的数字实时时钟,其中可以使用连接到 NodeMCU 的两个按钮设置时间和日期,并将它们打印在诺基亚 5110 …

STM32学习 BKP

BKP就是备份寄存器的意思,听名字就知道它的作用就是用来备份数据的。下面是手册当中的描述。备份寄存器是42个16位的寄存器,可用来存储84个字节的用户应用程序数据。他们处在备份域 里,当VDD电源被切断,他们仍然由VBAT维持供电。当…