FreeRTOS学习笔记

发布时间:2024年01月03日

前言

??本笔记基于B站正点原子的视频讲解,和个人的理解应用情况。应该适合用来回忆复习FreeRTOS的基本内容,避免在应用时突然忘了某个知识点要查很久。还有就是B站正点原子的讲解视频主要是对FreeRTOS的移植应用讲解,而我重点只听应用部分,因为一般都是基于STM32CubeMX直接生成FreeRTOS,也用每次都移植了,所以下面笔记内容关于移植部分的是没有的。有时间会在继续在实践中,补充补充笔记,给每个知识点在加上基本案例之类的。

进度跳跃

P7快速的跳过了,因为是配置他家的FreeRTOS配置 。

下载流程

  1. 官网下载源码

    https://freertos.org/zh-cn-cmn-s/index.html

  2. 下载得到包解压得到的目录及意思

    名称描述
    FreeRTOSFreeRTOS内核
    FreeRTOS-PlusFreeRTOS组件
    tools工具
    GitHub-FreeRTOS-HomeFreeRTOS的GitHub仓库链接
    Quick_Start_Guide快速入门指南官方文档链接
    Upgrading-toFreeRTOS-XXX升级到指定FreeRTOS版本官方文档链接
    History.txtFreeRTOS历史更新记录
    其他其他
  3. 打开FreeRTOS文件夹

    名称描述
    DemoFreeRTOS演示例程
    LicenseFreeRTOS相关许可
    SourceFreeRTOS源码
    Test公用以及移植层测试代码

移植流程

略。。。

FreeRTOS+STM32基本使用流程

  • 创建首任务,在里面中开启临界区创建各分任务。(这里可能讲的是先开启调度器在创建任务的情况,不过一般都是先创建任务才开始调度吧)

  • FreeRTOS的基本使用

    //任务函数
    void task0(void *pvParameters)
    {
        for(;;)
     {
      //...
     }
    }
    // 任务函数,任务名称,堆栈大小,传递参数,优先级,任务句柄。
    xTaskCreate(vTask1,"LED1",128,NULL,1,NULL);
    xTaskCreate(vTask2,"LED2",128,NULL,1,NULL);
    //启动任务调度器
    vTaskStartScheduler();
    

    功能函数

    vTaskDelay(500); //500个任务节拍 
    vTaskDelay(pdMS_TO_TICKS(500)); // 将500毫秒转换为时钟节拍并进行延迟
    
    知识
    • 只有使用FreeRTOS提供的延时才能有效的切换任务。
    • 任务没执行的话,可以打印一下创建任务是否成功。
    • 函数几乎分成两大类,普通程序中执行的,中断中执行的。
    • 任务创建成功,没有执行的话,可能有空的运行任务占用了线程。

任务创建与删除

xTaskCreate();			//动态方式创建任务
xTaskCreateStatic();	//静态方式创建任务
vTaskDelete();			//删除任务

//案例
/*
xTaskCreate(Task0_RunLed,"Task0",128,NULL,1,NULL);
*/
  • 任务句柄:动态任务的句柄是入口函数,静态任务是创建任务时的返回值。
  • 删除任务函数,传入任务句柄,删除指定任务,当传入NULL,删除当前正在运行的任务。 (任务在空闲时,才执行删除。静态需要先释放内存)

任务挂起与恢复

vTaskSuspend();			//挂起任务
vTaskResume();			//恢复被挂起任务
xTaskResumeFromISR();	//在中断中恢复被挂起的任务
  • 挂起任务函数,传入任务句柄,挂起指定任务,当传入NULL,挂起 当前正在运行的任务。
//中断恢复要点
//定义结构
BaseType_t xYieldRequired;
xYieldRequired = xTaskResumeFromISR();
if(xYieldRequired = pdTRUE)
{
	portYIELD_FROM_ISR(xYieldRequired);
}
  • 中断管理的抢断优先级数不能超过FreeRTOS所管理的,否则会报错。

中断管理

临界段保护

临界保护区段的代码内,关闭所有FreeRTOS所控制的中断

taskENTER_CRITICAL();		//任务级进入临界段
taskEXIT_CRITICAL();		//任务级退出临界段
taskENTER_CRITICAL_FROM_ISR();	//中断级进入临界段
taskEXIT_CRITICAL_FROM_ISR();	//中断级退出临界段

//任务级临界格式:
/*
taskENTER_CRITICAL();
内容
taskEXIT_CRITICAL();
*/
//中断级临界格式:
/*
uint32_t save_status;
save_status = taskENTER_CRITICAL_FROM_ISR();
内容
taskEXIT_CRITICAL_FROM_ISR(save_status);
*/
  • 退出和进入是成对出现,支持多层嵌套。
  • 任务级指的是在任务函数中可调用该函数,中断级是则是在中断函数中使用

任务调度器的挂起和恢复

任务调度器的挂起,只是给任务的切换带来停止,中断还会照常。

vTaskSuspendAll();		//挂起任务调度器
xTaskResumeAll();		//恢复任务调度器

//使用格式示例:
/*
vTaskSuspendAll();
内容
xTaskResumeAll();
*/

列表操作

vListInitialise();		//初始化列表
vListInitialiseltem();	//初始化列表项
vListInsertEnd();		//列表末尾插入列表项
vListInsert();			//列表插入列表项
uxListRemove();			//列表移除列表项
  • FreeRTOS中的列表类似于C语言的链表的概念,是一种双向链表。

延时函数

vTaskDelay(1000); 		//延时1000个节拍
vTaskDelayUntil(1000);	//延时1000给节拍
vTaskDelay(pdMS_TO_TICKS(1000));	//延时1000ms

队列操作

使用事项

  • 注意导入队列库

    #include "queue.h"
    
  • 可以利用队列的等读取待函数,从就绪态变成阻塞态,卡取任务状态,如等待按钮按下之类的。

创建队列

xQueueCreate();			//动态方式创建队列
xQueueCreateStatic();	//静态方式创建队列
  • 动态创建成功返回句柄,失败则返回NULL。

队列写入

 			//往队列的尾部写入消息
xQueueSendToBack();		//同 xQueueSend()
xQueueSendToFront();	//往队列的头部写入消息
xQueueOverwrite();		//覆写队列消息(只用于队列长度为1的情况)
xQueueSendFromISR();		//在中断中往队列的尾部写入消息
xQueueSendToBackFromISR();	//同xQueueSendFromISR()
xQueueSendToFrontFromISR();	//在中断中往队列的头部写入消息
xQueueOverwriteFromISR();	//在中断中覆写队列消息(只用于队列长度为1的情况)

队列读取

xQueueReceive();		//从队列头部读取消息,并删除消息
xQueuePeek();			//从队列头部读取消息
xQueueReceiveFromISR();	//在中断中从队列头部读取消息,并删除消息
xQueuePeekFromISR();	//在中断中从队列头部读取消息

应用案例

// 定义队列变量  
QueueHandle_t xQueue;  


信号量

简介:信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问。

特点:

  • 假如最大值限定为1,那么只有0和1两种状态,那就称为二值信号量。如果最大值不为1,那它就是计数型信号量
  • 相当于长度为1 的队列,消耗资源比队列小,对比与队列的话,信号量,写入时不存在堵塞,只会返回失败。读取时存在堵塞。

使用流程:创建信号量 --> 获取信号量 --> 释放信号量

??二值型信号量通常用于任务同步或互斥访问,与互斥信号量比较类似,但是二值信号量有可能会导致优先级翻转的问题,所以二值信号量更适合用于同步!

??计数型信号量通常用于事件计数或资源管理,事件计数的数值一般从0开始释放获取,资源管理一般从设定的最大值开始释放获取。

??互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中!

使用事项:

  • 注意导入信号量库

    #include "semphr.h"
    
  • 当有两个不同优先级的任务获取二信号量堵塞时,一个中级任务在运行,在一定条件下,就可能引发优先级翻转(高优先级任务,反而是最慢运行的),会有不可控问题。在这种互斥问题中引入互斥信号量,能缓解优化这种问题。

  • 互斥信号量不能用于中断服务中,因为中断服务函数不能因为互斥信号的堵塞事件进入堵塞态。而且互斥信号量有任务优先级继承的机制,但是中断不是任务。

  • 注意:创建互斥信号量时,会主动释放一次信号量

// 二值型
xSemaphoreCreateBinary();		//使用动态方式创建二值信号量
xSemaphoreCreateBinaryStatic();	//使用静态方式创建二值信号量
// 计数型
xSemaphoreCreateCounting();			//使用动态方法创建计数型信号量
xSemaphoreCreateCountingStatic();	//使用静态方法创建计数型信号量
uxSemaphoreGetCount();				//获取信号量的计数值
// 互斥量
xSemaphoreCreateMutex();			//使用动态方法创建互斥信号量。
xSemaphoreCreateMutexStatic();		//使用静态方法创建互斥信号量。
// 通用(互斥量不能用于中断服务) 
xSemaphoreGive();				//释放信号量
xSemaphoreGiveFromISR();		//在中断中释放信号量
xSemaphoreTake();				//获取信号量
xSemaphoreTakeFromISR();		//在中断中获取信号量

事件标志组

简介:事件标志位,用一个位,来表示事件是否发生。事件标志组是一组事件标志位的集合,可以简单理解事件标志组,就是一个整数。

特点:

  • 它的每一个位表示一个事件,高8位不算。因为高八位还要用于存储控制信息。
  • 每一位事件的含义,由用户自己决定。如:按钮按下改变某一位。
  • 任意任务或中断都可以读写这些位
  • 可以等待某一位成立,或者等待多位同时成立

事件标志组与队列、信号量的区别?

功能唤醒对象事件清除
队列、信号量事件发生时,只会唤醒一个任务是消耗型的资源,队列的数据被读走就没了;信号量被获取后就减少了
事件标志组事件发生时,会唤醒所有符合条件的任务,可以理解为“广播”的作用被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件

使用事项:

  • 注意导入信号量库

    #include "event_groups.h"
    
  • 可以根据宏定义configUSE_16_BIT_TICKS来配置,事件标志组的16位或32位无符号的数据类型。(STM32中这个变量类型为32位的)

xEventGroupCreate();			//使用动态方式创建事件标志组
xEventGroupCreateStstic();		//使用静态方式创建事件标志组
xEventGroupClearBits();			//清零事件标志位
xEventGroupClearBitsFromISR();	//在中断中清零事件标志位
xEventGroupWaitBits(); 			//设置事件标志位
xEventGroupSetBitsFromISR();	//在中断中设置事件标志位
xEventGroupWaitBits();			//等待事件标志位
xEventGroupSync();				//设置事件标志位,并等待事件标志位

任务通知

简介:任务通知,用来通知任务,任务控制块中的结构体成员变量 ulNottifiedValue就是这个通知值。

特点:

  • 不消耗额外内存,可以模拟队列,信号量, 事件标志组的使用方式。

  • 使用队列,信号量, 事件标志组时都需要另外创建一个结构体,通过中间的结构体进行间接通信。使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的“通知”。

  • 发送通知API函数可以用于任务和中断服务函数中;接收通知API函数只能用在任务中。

任务通知值的更新方式:

  • 不覆盖接受任务的通知值
  • 覆盖接受任务的通知值
  • 更新接受任务通知值的一个或多个bit
  • 增加接受任务的通知值

优势:

  • 效率更高,使用任务通知向任务发送事件或数据比使用队列、事件标志组或信号量快得多
  • 使用内存更小,使用其他方法时都要先创建对应的结构体,)使用任务通知时无需额外创建结构体

劣势:

  • 无法发送数据给ISR,ISR没有任务结构体,所以无法给ISR发送数据。但是ISR可以使用任务通知的功自发数据给任务。
  • 无法广播给多个任务,任务通知只能是被指定的一个任务接收并处理。
  • 无法缓存多个数据,任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数据。
  • 发送受阻不支持阻塞,发送方无法进入阻塞状态等待。
// 通知方式
xTaskNotify();			//发送通知,带有通知值
xTaskNotifyAndQuery();	//发送通知,带有通知值,并且保留接收任务的原通知值
xTaskNotifyGive();		//发送通知,不带通知值
xTaskNotifyFromISR();			//在中断中发送任务通知
xTaskNotifyAndQueryFromISR();	//在中断中发送任务通知
vTaskNotifyGiveFromISR();		//在中断中发送任务通知

// 接受方式
ulTaskNotifyTake();				//获取任务通知,可以设置在退出此函数的时候将任务通知值清零戴者减一。当任务通知用作二值信号量或者计数信号量的时候,使用此函数来获取信号量。
 ();				//获取任务通知,比 ulTaskNotifyTak()更为复杂,可获取通知值和清除通知值的指定位。当任务通知用作于事件标志组或队列时,使用此函数来获取。

软件定时器

简介:

  • 定时器:从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可自定义定时器的周期。
  • 硬件定时器:芯片本身自带的定时器模块,硬件定时器的精度一般很高,每次在定时时间到达之后就会自动触发一个中断,用户在中断服务函数中处理信息。
  • 软件定时器:是指具有定时功能的软件,可设置定时周期,当指定时间到达后要调用回调函数)也称超时函数) ,用户在回调函数中处理信息

软件定时器优缺点?

优点:

  • 硬件定时器数量有限,而软件定时器理论上只需有足够内存,就可以创建多个;

  • 使用简单、成本

缺点:

  • 软件定时器相对硬件定时器来说,精度没有那么高因为它以系统时钟为基准,系统时钟中断优先级又是最低,容易被打断)。 对于需要高精度要求的场合,不建议使用软件定时器。

FreeRTOS软件定时器特点

  • 可裁剪,软件定时器是可裁剪可配置的功能,如果要使能软件定时器,需将configUSE_TIMERS配置项配置成1
  • 单次和周期,软件定时器支持设置成:单次定时器或周期定时器
  • 注意:软件定时器的超时回调函数是由软件定时器服务任务调用的,软件定时器的超时回调函数本身不是任务,因此不能在该回调函数中使用可能会导致任务阻塞的API函数。
  • 软件定时器服务任务:在调用函数vTaskStartScheduler()开启任务调度器的时候,会创建一个用于管理软件定时器的任务,这个任务就叫做软件定时器服务任务

软件定时器服务任务作用

  1. 负责软件定时器超时的逻辑判断

  2. 调用超时软件定时器的超时回调函数

  3. 处理软件定时器命令队列

软件定时器的命令队列

??FreeRTOS提供了许多软件定时器相关的API函数,这些API函数大多都是往定时器的队列中写入消息(发送命令),这个队列叫做软件定时器命令队列,是提供给FreeRTOS中的软件定时器使用的,用户是不能直接访问的。

软件定时器的相关配置

  • 当FreeRTOS的配置项configUSE_TIMERS设置为1,在启动任务调度器时,会自动创建软件定时器的服务/守护任务prvTimerTask();

  • 软件定时器服务任务的优先级为 configTIMER_TASK_PRIORITY = 31;

  • 注意:软件定时器的超时回调函数是在软件定时器服务任务中被调用的,服务任务不是专为某个定时器服务的,它还要处理其他定时器。

    所以,定时器的回调函数不要影响其他"人” :

    1、回调函数要尽快实行,不能进入阻塞状态,即不能调用那些会阻塞任务的API函数,如: vTaskDelay();

    2、访问队列或者信号量的非零阻塞时间的API函数也不能调用。

软件定时器共有两种状态:

  • 休眠态:软件定时器可以通过其句柄被引用,但因为没有运行,所以其定时超时回调函数不会被执行

  • 运行态:运行态的定时器,当指定时间到达之后,它的超时回调函数会被调用

    注意:新创建的软件定时器处于休眠状态,也就是未运行的!

    状态的转化依靠发送命令队列。

FreeRTOS提供了两种软件定时器:

  • 单次定时器:单次定时器的一旦定时超时,只会执行一次其软件定时器超时回调函数,不会自动重新开启定时,不过可以被手动重新开启。
  • 周期定时器:周期定时器的一旦启动以后就会在执行完回调函数以后自动的重新启动,从而周期地执行其软件定时器回调函数。

函数大致分成五类:创建,开启,停止,复位,更改超时时间。

xTimerCreate();					//动态方式创建软件定时器
xTimerCreateStatic();			//静态方式创建软件定时器
xTimerStart();					//开启软件定时器定时
xTimerStartFromISR();			//在中断中开启软件定时器定时
xTimerStop();					//停止软件定时器定时
xTimerStopFromISR();			//在中断中停止软件定时器定时
xTimerReset();					//复位软件定时器定时
xTimerResetFromISR();			//在中断中复位软件定时器定时
xTimerChangePeriod();			//更改软件定时器的定时超时时间
xTimerChangePeriodFromISR();	//在中断中更改定时超时时间

STM32cubeMX中需要在软件中先配置,否正编译报错

Tickless低功耗模式

介绍:很多应用场合对于功耗的要求很严格,比如可穿戴低功耗产品,物联网低功耗产品。且mcu一般也有对应的低功耗模式,而Tickless则是FreeRTOS中,进入低功耗模式的存在。

  • Tickless低功耗模式的本质是通过调用指令 WFI 实现睡眠模式!

  • Tickless模式的设计思想, 在一个任务运行时间统计实验中,可以看出,在整个系统的运行过程中,其实大部分时间是在执行空闲任务(是在系统中的所有其它任务都阻塞或被挂起时才运行的),所以为了达到降低功耗(可以降低功耗,又不影响系统运行),就可以在本该空闲任务执行的期间,让MCU进入相应的低功耗模式;当其他任务准备运行的时候,MCU退出低功耗模式。

  • 难点:1、进入低功耗之后,多久唤醒?也就是下一个要运行的任务如何被准确唤醒。2、任何中断均可唤醒MCU,若滴答定时器频繁中断则会影响低功耗的效果?将滴答定时器的中断周期修改为低功耗运行时间。退出低功耗后,需补上系统时钟节拍数。

    值得庆幸的是: FreeRTOS的低功耗Tickless模式机制已经处理好了这些难点。

Tickless模式相关配置项(宏定义)

  • configUSE_TICKLESS_IDLE

    此宏用于使能低功耗 Tickless 模式(0:为不打开 1:启动系统配置好的方案 2:用户自定义方案)

  • configEXPECTED_IDLE_TIME_BEFORE_SLEEP

    此宏用于定义系统进入相应低功耗模式的最短时长(一般为2,也就是2ms以上)

  • configPRE_SLEEP_PROCESSING (x)

    此宏用于定义需要在系统进入低功耗模式前执行的事务,如:进入低功耗前关闭外设时钟,以达到降低功耗的目的(宏函数,需要用户编写,可以在FreeRTOSConfig.h中,编写好宏声明)

  • configPOSR_SLEEP_PROCESSING(x)

    此宏用于定义需要在系统退出低功耗模式后执行的事务,如:退出低功耗后开启之前关闭的外设时钟,以使系统能够正常运行(宏函数,需要用户编写,可以在FreeRTOSConfig.h中,编写好宏声明)

STM32cubeMX中需要在软件中先配置,否正编译报错

FreeRTOS内存管理

疑问:为啥不用标准的C库自带的内存管理算法?

因为标准C库的动态内存管理方法有如下几个缺点:

  • 占用大量的代码空间 不适合用在资源紧缺的嵌入式系统中
  • 没有线程安全的相关机制
  • 运行有不确定性,每次调用这些函数时花费的时间可能都不相同
  • 内存碎片化
  • …(还有很多)

FreeRTOS的5种动态内存管理 算法的优缺点

算法优点缺点
heap_1分配简单,时间确定只允许申请内存,不允许释放内存
heap_2允许申请和释放内存不能合并相邻的空闲内存块会产生碎片、时间不定
heap_3直接调用C库函数malloc()和free() ,简单速度慢、时间不定
heap_4相邻空闲内存可合并,减少内存碎片的产生时间不定
heap_5能够管理多个非连续内存区域的heap_4时间不定

内存管理相关函数

void*pvPortMalloc( size_t xWantedSize);	//申请内存
void vPortFree(void*pv);				//释放内存
size_t xPortGetFreeHeapSize(void);		//获取当前空闲内存的大小
文章来源:https://blog.csdn.net/m0_60313295/article/details/135368942
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。