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 两个引脚的电平变化,具体如下图所示

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

5.2、待改进

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

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

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

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

FreeRTOS简单内核实现5 阻塞延时的更多相关文章

  1. 一种Cortex-M内核中的精确延时方法

    本文介绍一种Cortex-M内核中的精确延时方法 前言 为什么要学习这种延时的方法? 很多时候我们跑操作系统,就一般会占用一个硬件定时器--SysTick,而我们一般操作系统的时钟节拍一般是设置100 ...

  2. 基于mykernel完成时间片轮询多道进程的简单内核

    基于mykernel完成时间片轮询多道进程的简单内核 原创作品转载请注明出处+中科大孟宁老师的linux操作系统分析:https://github.com/mengning/linuxkernel/ ...

  3. RHCA学习笔记:RH442-Unit9内核定时与进程延时

      Unit 9 Kernel Timing and Process Latency 内核定时与进程延时 学习目标: A.了解CPU 是怎样追踪时间的 B.调整CPU的访问次数 C.调整调度延时 D. ...

  4. [arm驱动]Linux内核开发之阻塞非阻塞IO----轮询操作【转】

    本文转载自:http://7071976.blog.51cto.com/7061976/1392082 <[arm驱动]Linux内核开发之阻塞非阻塞IO----轮询操作>涉及内核驱动函数 ...

  5. 源码级别gdb远程调试(实现OS简单内核)

    最近在学着编写一个操作系统的简单内核,需要debug工具,我们这里使用gdb来进行调试,由于虚拟机运行和本机是两个部分,所以使用 gdb 的远程调试技术,这里对 gdb 常见调试以及远程调试方式做一个 ...

  6. 服务器端IO模型的简单介绍及实现 阻塞 / 非阻塞 VS 同步 / 异步 内核实现的拷贝效率

    小结: 1.在多线程的基础上,可以考虑使用"线程池"或"连接池","线程池"旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲 ...

  7. 芯灵思Sinlinx A64开发板 Linux内核等待队列poll ---阻塞与非阻塞

    开发平台 芯灵思Sinlinx A64 内存: 1GB 存储: 4GB 开发板详细参数 https://m.tb.cn/h.3wMaSKm 开发板交流群 641395230 阻塞:阻塞调用是指调用结果 ...

  8. 基于mykernel完成多进程的简单内核

    学号351 原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ mykernel简介 mykernel是由孟宁老师建立的一个用于开发您自己的操 ...

  9. HAL无阻塞延时

    //实现间隔time_interval时间点亮红灯(此时间间隔并不是绝对的,是大于等于的关系)//用于系统要求无延时且延时时间粗略的场合,比如间隔一段时间采样数据,间隔一段时间点亮状态灯等//HAL_ ...

  10. linux简单内核链表排序

    #include <stdio.h> #include <stdlib.h> #define container_of(ptr, type, mem)(type *)((uns ...

随机推荐

  1. HTML中元素分类与对应的CSS样式特点

    元素就是标签,布局中常用的有三种标签,块元素.内联元素.内联块元素,了解这三种元素的特性,才能熟练的进行页面布局. 块元素 块元素,也可以称为行元素,布局中常用的标签如:div.p.ul.li.h1~ ...

  2. STM32【HAL库】使用外部SRAM程序

    #include <board.h> #ifdef BSP_USING_SRAM #include <drv_common.h> #include <rtthread.h ...

  3. 探索 DTD 在 XML 中的作用及解析:深入理解文档类型定义

    DTD 是文档类型定义(Document Type Definition)的缩写.DTD 定义了 XML 文档的结构以及合法的元素和属性. 为什么使用 DTD 通过使用 DTD,独立的团体可以就数据交 ...

  4. 『手撕Vue-CLI』添加自定义指令

    前言 经上篇『手撕Vue-CLI』添加帮助和版本号的介绍之后,已经可以在控制台中输入 nue --help 来查看帮助信息了,但是在帮助信息中只有 --version,--help 这两个指令,而 v ...

  5. 【保姆级Python入门教程】马哥手把手带你安装Python、安装Pycharm、环境配置教程

    您好,我是 @马哥python说 ,一枚10年程序猿. 我的社群中小白越来越多,咨询讨论的问题很多集中在python安装上,故输出此文,希望对大家起步有帮助. 下面开始,先安装Python,再安装py ...

  6. nginx与location规则

    ========================================================================= 2018年3月28日 记录: location = ...

  7. 在Linux下想要删除一个目录需要怎样的权限

    场景一 在Home目录下创建一个目录dirtest,然后使用chmod 333 dirtest修改目录权限.这时候dirtest的权限为d-wx-wx-wx,如果执行rm -r dirtest可以进行 ...

  8. 能碳双控| AIRIOT智慧能碳管理解决方案

    在当前全球气候变化和可持续发展的背景下,建设能碳管理平台成为组织迎接挑战.提升可持续性的重要一环,有助于组织实现可持续发展目标,提高社会责任形象,同时适应未来碳排放管理的挑战.能碳管理是一个涉及跟踪. ...

  9. Django自定义模板标签与过滤器

    title: Django自定义模板标签与过滤器 date: 2024/5/17 18:00:02 updated: 2024/5/17 18:00:02 categories: 后端开发 tags: ...

  10. 单体项目使用Spring Security实现登陆认证授权

    前端可以根据权限信息控制菜单和页面展示,操作按钮的显示.但这并不够,如果有人拿到了接口,绕过了页面直接操作数据,这是很危险的.所以我们需要在后端也加入权限控制,只有拥有操作权限,该接口才能被授权访问. ...