FreeRTOS学习 韦东山

本文详细介绍了FreeRTOS实时操作系统的基础知识和高级特性,包括任务管理、中断处理、信号量与互斥量的使用、队列与事件组的原理及应用、软件定时器的配置与优化等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1月21:

学习的文档链接:

百问网《FreeRTOS入门与工程实践-基于STM32F103》教程-基于DShanMCU-103(STM32F103) | 百问网

一、对于单片机,他被称为SOC(System On Chip)

207f84a252b24ab9864005fe02ac9fd0.png

cpu:用来运行程序                   Flash:用来保存程序                     内存:ram

cpu在运行程序时要访问内存

所有的计算都在cpu中执行的

stm32内存资源_stm32 数据存储-优快云博客

 fcd70428562a4b2aa040044197b5492d.png

所以stm32的flash本质时rom,在系统初始化时会把flash中的一些值放入ram中

ram就充当内存,再配合flash和cpu完成程序运行 

923613bd200c43e6a06065be3b35ad9f.png

447d0e0bc0d34a0295c8b8bb19623335.png

stm32中RAM地址是从0x2000 0000开始的,Flash地址是从0x8000 0000开始的

数据传输的三大要素:源,目的,长度

memcpy(源,目的,长度)

局部变量都保存在栈中,栈在ram中

1月22:

单片机执行逻辑:

程序是被存放在FLASH中的,他会一步一步的在FLASH中按逻辑执行(从代码起始地址一直执行到代码结束地址为止),其中会涉及到寄存器的操作指令(在CPU中执行),会有对内存(缓存区)的读写(比如读地址,读变量,写地址,写变量)

1、堆和栈

堆:是一块空闲的内存,你可以从这块内存中取出一部分,用完后再释放回去

栈:也是一块内存,cpu的sp寄存器指向它,它可以用于函数调用、局部变量、多任务系统里保护现场(是freertos基础)。每个任务都会有自己的栈。

2、

b2a9436191354229bfd129846d6ef6cb.png

LR(存返回地址)

PC(存即将跳转的函数的地址)

7e7ebfa40818461a9458b42dfec888ac.png

在进行函数的嵌套调用时的三个问题(使用汇编码解读):

①LR被覆盖了怎么办:①使用堆栈的方式来实现保护LR不被破坏

②局部变量在栈中分配,如何分配

③为何每个rtos任务都有自己的栈

1月27:

②局部变量用栈保存,但有时编译器会给你优化,使它保存在了寄存器里面,不过你加上了volatile后就会避免优化,使它保存在栈里面了。

当你局部变量越来越多了的时候,它必定会在栈里给你分配空间。

2月16号:

③因为每个任务都有自己的调用关系,都有自己的局部变量,现场 

2月27:

 Heap_1到Heap_5一共有五种,只能选择使用其中的一种

大部分是使用Heap_4和Heap_5

P6:创建自己的freertos

选择TIM4做基准时钟

P16

这个是Freertos创建的默认任务,且为默认优先级

P17 5-1-2创建任务_估算栈大小:

栈的估算方式:

一个任务的栈由三个部分组成

1、返回地址LR和其他的寄存器(每个寄存器是4字节),调用深度越深,保存的LR和寄存器越多

公式为size=(1+n)*m*4字节

2、局部变量的大小,可能在任务中创建了一个很大的数组,那么它也会占用很大的空间

3、任务如果被切换的时候会创建一个固定的现场保护大小size=16*4=64字节

P18 5-2_创建任务_使用任务参数:

(互斥问题)没有加延时函数时同时访问一个函数大概率会出现只有一个任务可以运行(普通方法),因为没有延时函数时任务在g_LCDCanuse为0时被切换出去的概率是最大的

Second pass:

创建结构体:

初始化结构体

定义函数时引入结构体指针

创建任务时输入变量

task1,task2,task3函数的输入变量为结构体指针

P19:

频繁的创建、删除不好,会产生内存的碎片,后续可能就没法再创建任务了

删除任务后还需要进行一些任务的清除工作

P20:

两个步骤:1,提高优先级。2,把mdelay()替换为使用vTaskDelay()。

P21:

Ready/Running(就绪)(运行)

Blocked((阻塞状态)等待某些event)(调用阻塞函数进入阻塞状态如vTaskDelay)

Suspended(暂停)(要么自己调用暂停函数进入暂停状态(如xTaskSuspend),要么别人调用暂停函数(如xTaskSuspend)使它进入暂停状态)(恢复函数xTaskResume)

P22:

句柄到底是什么?TCB又是什么?C代码实例讲解_c 句柄-优快云博客很重要!

/****************************************核心:****************************************/

/****************************************核心:****************************************/

任务优先级,数值越大优先级越高

每次Tick中断都会从上到下遍历一遍链表找出一个任务运行(因为是从上到下遍历链表,而越上面的链表的值越大,所以一旦出现了高优先级的任务就会先执行高优先级的任务)

任务调用vTaskDelay()(延时)后会被放入DelayTaskList()

任务调用vTaskSuspend()(暂停)后会被放入

任务调用(恢复)后任务会被放入就绪链表

所以在调度(遍历就绪链表找到该执行的任务并执行,后把指针向下一级)之前会先判断DelayTaskList()中的任务是否可恢复,如果可恢复则会将其放回其原优先级链表

Second pass:

因为这是抢占式优先级,可以打断前面正在执行的任务,非抢占式的就必须等待上一个任务执行完。

所以突然创建了更高优先级的任务后,系统会马上去执行这个更高优先级的任务。

高优先级的任务需要使用vTaskDelay()(延时)来释放时间资源,来使其他比他优先级低的任务可以执行

P23 5-5-3:空闲任务

如果任务给的是有限循环,则任务会跳到错误处理函数,系统会死机。

因为任务不能使用有限循环,所以想结束任务(任务退出)循环只能删除

任务退出方法

例:

自杀(任务清除工作)

良好的编程习惯

1,事件驱动(比如我按下了某个按键之后,它才会做某些事情,没有得到按键他就会阻塞)

2,把mdelay()换成vTaskDelay(),避免系统进入死循环,让出cpu资源,使空闲任务可以执行,用于释放被删除任务的内存

Second pass

如果某个任务要调用函数 vTaskDelete()删除自身,那么这个任务的任务控制块 TCB 和任务堆栈等这些由 FreeRTOS 系统自动分配的内存需要在空闲任务中释放掉,如果删除的是别的任务那么相应的内存就会被直接释放掉,不需要在空闲任务中释放。因此,一定要给空闲任务执行的机会!

P24:

xTaskDelayUntil()函数:

使用xTaskDelayUntil()函数,你要在前面获得他的起始时间

P25:

为什么g_calc_end改变了但是还是会一直卡在while()呢?

对于g_calc_end这个变量,因为编译器把它优化了,所以它一直读的是它存在寄存器的值(旧的值),我们要一直读它新的值(因为这个变量在其他函数里一直被修改),需要一直去读内存,所以为了避免优化,需要加上volatile

我们在处理同步任务时应该考虑的问题:让等待的任务阻塞,不要让他们参与cpu的调度

P26:

对于同时调用共享资源时

如果用这种全局变量来保护公共资源还是可能会出现问题

高效的运行方式

P27:

任务之间的通信:

怎么保证任务通信的结果是正确的

P29:

数据传输方法

P30:

队列的本质就是加了互斥措施和唤醒机制的环形缓冲区

阻塞理解为vTaskDealy

P32:

static TickType_t   time=100;
if(xQueueReceive(xQueue,pvBuffer,time)==pdPASS)//可以模拟空闲中断
{//在xTicksToWait时间内收到数据则可以将数据存入数组

}else{//处理数据内的数据
    datadeal();
    time=portMAX_DELAY;
}

             中断中写队列不可阻塞(还有好多写函数,具体去看文档)

error:

1,在包含"include queue.h"之前必须先包含"include FreeRTOS.h" 

2,右击文件弹出copy full path 可以获得文件的路径,然后在魔法棒中添加文件路径,这样可以解决路径问题。

Second pass

结构体大小的计算(结构体内存对齐)-优快云博客

两种唤醒模式,他们是由两个链表配合共同完成的

而类似的如果把SenderList替换ReceiverList,就是写队列去唤醒读队列

在中断里是不允许等待的

在中断里写队列和读队列必须使用带有IAR的函数

疑问:

如果在写入队列时发现队列满了,这个包含写入队列的任务会进入阻塞态(因为设有等待的阻塞时间),那当读队列

时发现队列空出位置了后,这个包含了写队列的任务会直接进入到就绪链表中后,是和同优先级的任务按先后顺序执行吗,还是优先执行?

解答:

是按先后顺序依次执行。

FreeRTOS基础(六):中断管理_rtos中的中断-优快云博客

在freertos中:

PendSV和SysTick设置中断优先级中的最低优先级,设置最低:保证系统任务切换不会阻塞系统其他中断的响应,为什么?中断是非常紧急的事情,也就是说中断可以打断任意的任务,而任务不可以打断中断,PendSV就是用来处理任务切换的内核中断,因此把PendSV任务切换中断的优先级配置为最低优先级,这样当中断事件发生的时候,它会打断PendSV,也就是中断一旦发生,就不会在切换任务执行,但是中断发生的时候,PendSV是不会打断中断的,因为它的优先级最低。

P33:

P35:

要使用队列集的时候一定要把configUSE_QUEUE_SETS置1(默认为0)

但是如果直接更改configUSE_QUEUE_SETS下次再使用CubeMX后又会恢复为默认值了

所以我们可以在FreeRTOSConfig.h这个头文件的CubeMX代码保护区加上#define   configUSE_QUEUE_SETS  1 宏定义来覆盖初始化宏定义使我们只用一次且不影响以后CubeMX的使用

而且我们发现FreeRTOS.h在FreeRTOSConfig.h的上方,说明FreeRTOS.h应该是先配置的文件,所以我们可以覆盖宏定义。

队列集优势:

①使应用层和底层解耦,后续可以更方便的增加业务

②因为队列集可以只使用一个任务就能替代多个任务来读取队列的数据

所以节省了堆栈的大小。

队列集也是队列,只不过队列集里面放的是那些元素是队列的句柄。

如果你使用多个任务分别管理多个资源(底层驱动数据读取)会造成资源浪费

使用队列集来把多个队列联系在一起就可以使用一个任务,可以节省资源

整体框架:

①创建队列A、B

②创建队列集S

③A、B加入S

④InputTask()

{

        while(1)

        {

                句柄=Read队列集S,得到句柄

                Read句柄,得到数据

        }

}

注:

因为队列集里只存了多个队列的句柄,所以还得再读一次队列

重点:

在写队列A的时候,A会先判断是否属于某个队列集S,如果属于,则顺便在写队列A的时候就把A的句柄也写入队列集S(B同理)

技巧:

在处理数据的时候因为是在得到响应的句柄后读的队列所以是肯定能得到数据的,所以等待时间给0;

问题:没有挡求版了

因为一个任务使用的队列变多了,而且还多了一个队列集,导致任务创建失败,所以要把堆改大,比如改为8000

良好的编程思维:

当你定义了一个static的全局变量时,如果你要在其他文件使用这个静态变量(根据static的规则是不能在其他文件引用这个全局变量)时你可以使用一个函数来封装它。

P38:

这里因为图二传入的参数不一样所以能够显示不一样的任务,而每个任务内部的栈不一样,局部变量的变化也不一样

图一:

图二:

注意:在读到消息队列的值之后队列里会把这个值从队列里清除,idata为读到后存入的值

P39:

信号量的本质是队列,它是特殊的队列

如果有一个任务试图获得一个不可用(已经被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待队列中的那个任务将被唤醒,并获得该信号量。例如:当某个人到了门前(钥匙在门外,进去房间的人持有钥匙),此时房间(临界区)里没有人,于是他就进入房间并关上了门。最大的差异在于当另外一个人想进入房间,但无法进入时,这家伙不是在徘徊,而是把自己的名字写在一个列表中,然后去打盹。当里面的人离开房间(释放钥匙)时,就在门口查看一下列表。如果列表上有名字,他就对第一个名字仔细检查,并叫醒那个人让他进入房间

P40:

二进制信息量类似于互斥量

互斥信号量和二进制信号量的区别_二进制信号量和互斥信号量-优快云博客

当一个想要获得信号量不成功,但它愿意等待,则他会被放入等待链表,获得信号量的优先级参考下图

在执行任务之前先获取信号量(Take),如果获得到了则执行下列任务,如果没有获得则不执行下列任务(进入阻塞)。

当然在执行完任务后(或者当前不需要再执行)需要释放信号量(Give)。

P41:优先级反转问题

关于任务反转的例子:

任务一先运行,任务二1s后运行,任务三2s后运行(任务2是一个不需要获得信号量就可以运行的任务,任务1,3需要)优先级任务一 < 任务二 < 任务三

任务2是一个不需要获得信号量就可以运行的任务,他阻塞了1,1 释放不了信号量,3就一直没法运行。

P42:使用互斥量来解决优先级反转的问题

互斥量(优先级继承,优先级恢复)是信号量的变种

记住:若有多个任务,其中1、2任务为互斥量触发类型,那么这两个任务的优先级将为1、2任务中最高优先级的优先级 ,这就是优先级继承。

解决方法:先提高1的优先级(继承3的优先级)使它的优先级大于2,这时3就可以在1(执行完任务,恢复优先级,释放完互斥量)后能够执行3

解释(如何用互斥量解决的):

使用互斥量把1,3两个任务设为互斥类型,当任务1运行时,1s后任务2运行,2s后任务3因为任务1未完成未释放信号量而不能运行。而任务1因为是和任务3为同一个互斥量触发类型,所以任务1继承了任务3的优先级并能优先与任务2运行,任务1运行完毕后释放互斥量,而任务3因为优先级大于任务二就先于任务二运行,当任务3运行完后任务2最后运行。

P43:事件组

这个事件组里有一个整数,它的高八位不用,之后的每一位都表示一个是事件(Bitn 的n表示事件n ,Bitn的值0为表示事件不触发,1为表示触发)

事件组里还有一个等待链表,等待事件的产生

高八位可以设置等待的事件的触发关系(或 、与)

P44:

P45:10_3事件组实验

P48:软件定时器

这些函数的内部都是去写队列,然后任务去读队列和写队列,通过这种方式来实现函数。

所以我们在用freertos里的定时器的时候,它的优先级一定要足够高

若没办法调整它的优先级的话,其他任务不能够一直运行,必须使用事件驱动的方式,运行完一会马上阻塞,否则会影响到定时器的运行,影响到它处理这些定时器

我们定时器默认的优先级是2,我们可以通过cubemx修改一下

P49:软件定时器的应用

xTimerChangePeriod()它会在这个时刻重新计算你的超时事件(相当于覆盖了上一次的事件)。

因为定时器的这些函数是通过写队列来实现的,所以会有一个超时时间(一般可以不等待,给0)

这里使用一次性定时器实现蜂鸣器的启动(一次响50ms 并且下一次提前到来可以覆盖上一次计时)

这里在调用定时器后,定时器会在time_ms后执行回调函数的内容

P49:任务和中断的两套API函数

中断要尽快处理完

使用xQueueSend()会有三种结果

不带有IAR后缀的函数

带有IAR后缀的函数:它会去唤醒其他任务,但遇到更高优先级的任务不会切换,而是先记录是否有更高优先级的任务需要切换,等中断的任务执行完了以后再去切换

如果在中断里不使用带IAR后缀的则可能会切换为其它高优先级的任务使中断无法执行完毕

P50:FromlSR示例_改进实时性

IAR在中断任务结束时如果  BaseType_t *pxHigherPriorityTaskWoken被设置为NULL(不马上产生调度)则它会在下一个Tick到来时(时间<1个tick)才进行调度

P51:[14-1]_资源管理_互斥操作的本质

P52:[14-2]_资源管理示例_解决DHT11经常出错的问题

P53:[15-1]_优化系统_精细调整栈大小

P54:[15-2]_优化系统_打印所有任务的栈信息

P55:[15-3]_优化系统_统计CPU占比找出有问题的任务

P56:[15-4]_优化系统_改进MPU6050驱动

常见问题:

1、

FreeRTOS/Source/CMSIS_RTOS_V2/freertos_os2.h(31): error: #13: expected a file name 报错-优快云博客

2、这个队列集宏定义一个要放在最上面,不然会报错

这个下面的#include "SEGGER_SYSVIEW_FreeRTOS.h"头文件是systemview文件

3、启动文件中的Heap_Size      EQU     0x600和configTOTAL_HEAP_SIZE 的关系

4、版本问题:

5、快速的粗略估算任务最小栈大小

6、FreeRTOS中 configTOTAL_HEAP_SIZE和任务堆栈的关系

在FreeRTOS中,configTOTAL_HEAP_SIZE 表示内核动态内存池的总大小,而不仅仅是任务堆栈的累计,原因如下:

1. 动态分配对象多样性 FreeRTOS 的堆内存不仅用于任务堆栈,还需分配给以下内核对象: 任务控制块(TCB):每个任务需要一个TCB存储状态、优先级等信息,占用额外内存。 队列、信号量、互斥体:这些通信机制在创建时从堆中分配内存。 软件定时器(若启用):定时器对象及其回调参数占用堆空间。 事件组:事件组的内部结构依赖堆内存。

2. 内存对齐与碎片 对齐开销:内存分配器需满足对齐要求(如8/16字节对齐),可能导致实际分配的块比声明的大。 内存碎片:频繁分配/释放会产生碎片,总堆需预留空间以缓解碎片化影响。 3. 系统任务开销 空闲任务:自动创建的IDLE任务需要堆栈和TCB。 定时器服务任务(若启用):需要独立堆栈和TCB。 中断处理:某些中断服务例程可能临时使用堆内存。

4. 用户代码的动态分配 用户通过pvPortMalloc()等函数直接申请的堆内存,也会占用configTOTAL_HEAP_SIZE。

5. 内存管理算法开销 块头信息:如heap_4算法为每个内存块添加链表节点信息,增加额外开销。 管理数据结构:部分算法需维护全局结构(如空闲块链表)。

示例场景 假设: 任务数:3,每个堆栈1KB。 TCB大小:每个128字节。 创建2个队列,每个占用256字节。 内存对齐导致每个堆栈多占用16字节。 计算: 任务堆栈总和:3 * (1KB + 16) = 3.048KB TCB总和:3 * 128B = 384B 队列总和:2 * 256B = 512B 总计:≈ 3.048KB + 0.896KB = 3.944KB 此时,若configTOTAL_HEAP_SIZE仅设为4KB,可能因碎片或额外分配导致失败。实际需配置更大值(如6KB)以容纳所有潜在开销。

结论 configTOTAL_HEAP_SIZE需覆盖所有动态内存需求,包括任务堆栈、内核对象、对齐/碎片开销及用户分配。设计时应预留余量(通常为计算值的1.2-1.5倍),并通过工具(如FreeRTOS的堆剩余量API)监控实际使用情况,避免内存耗尽。是否正确

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值