STM32 简单认识一下FreeRTOS(纯理论)
本节先来简单了解一下FreeRTOS:
RTOS全称Real-Time Operating System(实时操作系统),是专门给单片机/嵌入式芯片用的轻量操作系统,核心就两件事:保证任务按时响应(实时性)+自动管理多个任务并行运行。
和裸机开发的本质区别:
1、**裸机开发:**只有一个主while(1)循环,所有功能按顺序排队执行,就像一个人同时炒菜、接电话、洗碗、一件事卡住,其他全部延误(就像阻塞式按键,如果一直按住按键不动,整个程序就会卡住);不过你也可以靠中断来解决这个问题,但是主程序还是会被中断影响,如果安排在中断的程序运行很久,那主程序照样运行不了,那我把所有任务都放在中断呢?那这两个任务还是会相互影响,假如第一个任务卡很久,那还是会影响第二个任务的运行,所以主程序只能靠中断处理紧急任务,复杂业务很容易出时讯混乱。
2、**RTOS开发:**内核就像一个“调度主管”,把功能拆成多个独立的“任务”,每个任务有自己的优先级和独立栈空间。主管会按优先级自动切换任务,高级优先任务可以立刻抢占低优先级任务,保证紧急事件第一时间响应。
严格来说:FreeRTOS 并不是实时操作系统,因为它是分时复用的。 系统将时间分割成很多时间片,然后轮流执行各个任务。 每个任务都是独立运行的,互不影响,由于切换的频率很快,就感觉像是同时运行的一样。
堆和栈的概念:
**堆:**就是一块空闲的内存,只要我在这块内存实现分配和释放,那这块内存就是堆


heap_buf就是堆,(void*通用指针,可以接收任意类型的内存地址,malloc是c标准库动态内存申请函数,size就是你想分出多少地址大小),
在这里的代码逻辑就是调用my_malloc把堆从heap_buf[0]到heap_buf[99]分配出来,将这块内存的首地址分配给buf
栈:就是保存函数的返回地址和局部变量
在main函数中,调用函数a_fun,执行完a_fun函数后返回到return 0,在a_fun函数里面调用函数b_fun,b_fun执行完后返回到c_fun函数,同理
c_fun又返回到return a,在语言中我们这样看当然知道它们的返回地址在哪,但是它们的返回地址保存在哪呢?如下:

main函数在调用a_fun函数时,会先将return 0的地址(即返回地址)保存在LR寄存器中,然后才会去调用a_fun函数,同理a_fun函数在调用b_fun函数之前也会先将c_fun的地址保存在LR寄存器中,然后才会去调用b_fun函数,那LR寄存器不是被覆盖了吗?这怎么处理呢?如下:

在a_fun函数内部中,它会把return 0的地址(即返回地址)保存到栈中,同理在b_fun函数内部中,它也会把c_fun的地址保存到栈中,这样LR寄存器就不会被覆盖了,所以返回地址会保存在栈中,那栈是什么?栈就是一块空闲的空间,不过这块空间很小,所以不要定义一块很大的数组在函数中!

SP就是栈寄存器,让它指向一块空闲的内存

如图,BL指令会调用main函数,main函数会调用a_fun函数,a_fun函数又会调用b_fun函数。
调用main函数会发生什么呢?它会划分出一块空间,对应图中的就是N字节的内存,这N字节的内存就是main函数的栈,这块空间就会放LR寄存器的值(即返回地址)和局部变量,同理调用a_fun函数,它也会划分一块M字节的内存,这块空间也会放LR寄存器的值(即返回地址)和局部变量,下面来讲讲它是如何执行的

**调用a_fun函数时,执行到a=8时,它让M空间中的局部变量这块内存值等于8,下面同理,执行完a_fun函数后,它会从栈中取出返回地址到LR寄存器中,然后跳到这个返回地址中(也就是return 0地址)**所以栈非常重要,是程序运行背后的英雄
对于RTOS,栈非常重要,例如有两个任务task_A和task_B,这两个任务都有几个函数,这些函数的返回地址和局部变量都会保存在自己任务的栈里面,这两个栈互相独立,互不影响
创建FreeRTOS任务:
经典单片机程序和RTOS程序的对比:


创建FeedRTOS任务函数:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, //① 任务函数地址
const char * pcName, //② 任务名字,仅调试用
uint16_t usStackDepth, //③ 栈大小,**单位是字(words)**,STM32 1字=4字节
void *pvParameters, //④ 传给任务的参数地址 void*
UBaseType_t uxPriority, //⑤ ⭐任务优先级
TaskHandle_t *pxCreatedTask //⑥ ⭐输出:任务句柄的指针(用来保存输出结果,可以通过该句柄操控这个任务)
);
流程如下:
TaskHandle_t xHandleTask1; //定义句柄变量
//第6参数填变量地址,创建成功后,xHandleTask1就拿到任务TCB地址
xTaskCreate(vTask1,"task1",128,NULL, 2, &xHandleTask1)
// TaskHandle_t 的原始定义
typedef void * TaskHandle_t;
TaskHandle_t xHandleTask1;
// xHandleTask1:这是一个指针变量,将来用来存TCB结构体的内存地址
这里的流程理解是:我先定义一个句柄变量,这个变量本身就是 TCB 结构体指针;把这个变量的地址(二级指针)传给 xTaskCreate。内核创建出 TCB 结构体,拿到结构体的地址,写入到我的句柄变量。之后我拿着句柄就能操作这个任务。
TCB结构介绍:
TCB 全称 Task Control Block(任务控制块),是 RTOS 内核用来管理每个任务的核心数据结构。你可以把它理解成每个任务的「状态档案 + 上下文储物柜」:每创建一个任务,内核就会在内存中生成一份专属的 TCB 结构体,任务的所有运行信息都存在这里,系统调度全靠读写它来完成。
它的核心作用
多任务系统的核心是 “任务切换”:CPU 从任务 A 切去跑任务 B 之前,必须把任务 A 当前的运行现场完整保存下来;等下次切回任务 A 时再原样恢复,任务才能从暂停的位置继续执行,就像从没被打断过。
TCB 就是保存这份 “运行现场” 的载体。
接着就是创建一个任务函数,参数1函数指针,用来保存这个任务的地址,而参数4就是保存输入该任务函数参数的地址,这个跟我们制作简易万能年系统那个回调+函数指针本质是一样的,说白了就是:参数一保存内核上层函数地址,同时参数四保存内核数据地址;当需要执行上层函数时,将数据地址作为实参传入上层函数的形参,以此完成参数传递。函数本身不会被修改
源码结构与编程规范:

在FreeRTOS定义了两种数据类型,基于效率这两种类型可以是32位的也可以是16位的,这要看你的处理器是多少位的,然后选择对应的就行了


可以看到*p表示指针,x代表这个变量的类型是BaseType_t的,你也可以认为是复杂的结构体、task handle等,后面就是函数的含义

可以看到参数一,p就是指针,x表示一个结构体;参数四,p表示指针,v表示void类型。
所以变量有两部分组成,一部分是它的前缀:表示它的类型;后一部分表示它的函数
对于函数呢?在xTaskCreate函数中,x表示类型,Task表示文件,表示在哪一个文件里面实现,后面加上这个函数的含义了,所以这个函数是x类型的结构体,在Task.c文件中实现的

动态分配和静态分配:
对于任务我们需要构造一个结构体,如图每个任务我们用一个TCB结构体来描述它
对于每个Task任务,都有一个TCB_T结构体,_t就表示结构体,对于这个结构体,我们可以使用动态分配内存,也可以使用静态分配内存

可以看到对于这个函数,内部就使用了malloc进行动态分配,而分配出来的地址就是栈,可以看到参数3就是栈的深度,每个任务的栈都不一样,不然就乱套了,。
参数五就是一个任务控制块(句柄),指向了这个TCB结构体,就是这个TCB的指针,所以如上面操作一样,你要定义一个该类型的二次指针来接收它返回的TCB结构体的地址(这样就可以得到这个结构体的句柄)

这是个静态分配的任务函数,如图静态分配就要你事先分配好栈的大小和TCB任务
实验:

上图是创建任务函数的参数,可以看到优先级参数中,数值越小,优先级越低,这跟我们配置NVIC中断优先级时,数值越小,优先级越大不一样的
下面我们来进行四个实验来帮我们理解创建任务函数的参数:
优先级实验:

如图我们设置三个任务标志位,在每个任务中让它的标志位置1,然后可以观察到下面结果:
可以看到这三个任务优先级一样的话,它们是交叉执行的,那我将任务一的优先级设置为2呢?
可以看到任务一的标志位恒为1,其它任务恒为0
所以在默认的任务调度机制下面,高优先级的任务先执行,如果高优先级的任务没有主动放弃执行的话,其它优先级的任务根本没法执行,注意这只是一种调度机制,还有其它的调度机制
删除任务实验:
如何要调用一个任务,我们这么引用它呢,我们需要一个xHandTask(句柄),我们就可以通过这个句柄来引用这个任务,所以我们如果要删除一个任务必须要用到这个句柄。

可以看到,我们实现了在任务二中,“杀死了”任务一,并且也“自杀”了
调用的函数:
//v无返回值,Task在Task.c文件,Delete删除任务
//参数任务句柄
vTaskDelete(TsakHandle_t xTaskToDelete)
使用这个函数我们可以去删除xTaskCreate()函数和xTaskCreateStatic函数创建的任务,删除这些任务,我们都必须得到一个句柄,那句柄怎么得呢?
可以看到xTaskCreateStatic()函数内部会返回一个TaskHandle_t(句柄),所以如果我们要删除这个函数创建的任务的话,我们必须用一个指针去得到这个TaskHandle_t(TCB结构体的地址),注意这里与xTaskCreate()不同,xTaskCreate()不会返回TaskHand,它返回什么呢?后面我们在了解
一个函数创建多个任务实验:
我们先创建一个通用函数:
然后调用两个创建任务函数:
运行结果如下:
可以看到,程序能正常打印4,5
所以我们使用了同一个函数创建了两个不同的任务,为什么能实现呢?因为xTaskCreate()函数创建的任务,它们的栈是不一样的,每个任务都有自己的栈,相互独立的
栈大小实验:
xTaskCreate()函数如何分配栈:
如图可以看到我们分配一块空间给buf,那我们怎么释放这块空间呢?free()你传入的是buf,这块内存的首地址,但我不知道它长度啊,所以在buf前面还有一个结构体,这个结构体(头部)存放有buf的长度信息,所以在我们用free()释放buf的时,我们往这个地址往前推,我们就可以知道这个buf内存长度有多少

我们假设用Task1Funtion这个函数,使用大量的局部变量,大量的栈,去破坏这个头部,去破坏TCB,看看会发生什么
可以看到我们定义了一个数值变量为500字节,已经超过分配给这个任务的栈(400字节)了,运行结果如下:
可以看到程序奔溃了,所以我们在创建任务时,要仔细考虑分配多少栈给这个任务
任务管理:
在RTOS里面,多个任务运行在我们人类眼里的同时运行,其实它们是交叉运行,还记得上节的我们的优先级实验吗?实验数据图中,那三个任务不会同时置1,而且交替置1.
那在RTOS里面,它们是怎么实现多任务交替实行的呢?关键在于在RTOS里面有个tick中断(周期性的定时器中断)
可以看到任务三先运行1ms左右到任务二运行1ms左右到任务一运行1ms左右,为什么它们都只运行1ms?因为在系统里面有个定时器,这个定时器每隔1ms产出一次中断

如图可以看到每个时间断为1ms(即1个tick),tick count(就是tick总数),在系统中有一个Tick函数,每产生一次中断就判断一次执行哪个任务,当然你也可以设置tick的时间,如下图:
可以看到当前设置的tick周期时间为1000Hz(即1ms),而且你还能设置每个任务的tick时间,比如我可以设置任务1执行5个tick时间,任务二执行10个tick时间这是各种操作系统的一些细微差别
任务状态:
如上图在FeedRTOS里面一个任务有四种状态:运行状态、就绪状态(意思我可以随时运行,但还轮到我)、阻塞状态(即等待该任务执行,比如:一个母亲喂她小孩吃饭,你不能一直喂吧,你总要等他吃完上一口,才能喂下一口,等待他吃完,这种等待某事发生就是阻塞)、暂停状态(分为主动和被动,你可以主动去休息,也可以被别人叫去休息)
状态转换图:
如上图可以看到,假设有三个任务ABC,A处于Running状态,如果任务A想要去休息,那它可以调用vTaskSuspend()函数,参数可以是NULL/A的Handle(句柄)来选择主动休息还是被动休息,当然你也可以在当前运行的任务里调用vTaskSuspend()函数来控制其它任务去休息,比如当前A的任务里我调用vTaskSuspend()函数参数我输入B的Handle(句柄),这样B就会从Ready状态进入到Suspended状态,阻塞状态呢?如果任务A在等待某件事发生,它就会进入阻塞状态,怎么从阻塞状态回到Ready状态呢?就是Event(事件)的发生,这事件可以是(中断,也可以其它任务)
那处于各个任务的状态,它们是怎么被管理起来的呢?最简单的就是链表

如上图,是在xTaskCreate()任务内部的代码,可以看到有一个链表函数(将新任务添加到链表里面)


从上面两个图可以看到,当发生tick中断时,系统就会到链表里面找下一个执行的任务
任务状态实验:
接下来我们在任务一调用vTaskSuspend()函数让任务三在任务一执行10ms后进入暂停状态10ms,让任务二进入阻塞状态10ms

实验数据如下:
通过实验数据可以看到任务二每10ms才置一次1,任务三在任务一执行10ms后,也进入了暂停状态
介绍两个延时函数:


可以看到如果调用的是vTaskDelay()函数,如果任务运行时间不一样,那任务的开始间隔时间是不一样的?那如何周期性地执行任务呢?如下图:

可以看到,如果你调用的是vTskDelayuntil()函数(参数一是起始时间,参数二是设置绝对间隔时间)
下面我们通过程序验证一下:
我们先把任务一的优先级设置最高,这样只有任务一进入暂停状态或者阻塞状态,那么其它任务才能执行

任务一程序代码如上,实验数据如下:
可以看到使用vTaskDelay()函数,我们只能保证休眠的时间是一样的(20ms),但不能保证任务while()循环的时间是一样的
那我们调用第二个函数看看:
我们先了解一下这个函数参数,可以看到,第一个参数是一个x结构体类型的指针,它保留了之前的唤醒时间,第二个参数是一个增量,什么意思呢?如下图:
可以看到*pxpre是参数一,t是参数二,代表了从pxpre时间开始(即任务开始时间),经过t它才会延迟结束,参数一是会更新的,它会把上段延迟结束的时间端放到下次调用vTaskDelayUntil()的参数一,这也是为什么它是指针,下面我们通过实验现象来了解一下:
这代码什么意思呢?就是参数一是任务1开始的时间,只有经过20ms后才会停下来,下面这张图很好的解释了,说白了这个任务的启动时刻是周期性的
实验现象如下:
我们可以通过对比看到,下图中任务起始时间间隔是一样的(都是20ms),简单来说第一个延迟函数,是在前面代码执行完后轮到它才开始算延迟时间,第二个延迟函数,是从你选择的任务时间开始算延迟时间(参数一)
空闲任务和其钩子函数:


上图是xTaskCreat()函数的返回值,所以我们在main函数里创建任务一,然后在任务一里创建任务二,然后在任务二执行完后,让任务一删掉任务二,观察一下会发生什么


可以看到任务二的优先级是高于任务一的(这样是为了保证创建任务二后马上执行任务二),所以我们在让任务二进入阻塞状态,才能执行任务一,然后杀了它


可以看到空闲函数的优先级最低,所以空闲函数(函数里面会执行清理工作)不会被执行
实验现象:
可以看到程序并没有崩溃,按理来说,程序不断执行创建函数,删除函数,没有调用空闲函数的话,会很快将堆占满,造成奔溃啊,可是这里并没有,这是为什么呢?老师并没有说,我问AI,AI说程序会奔溃,只是堆还没被占满,但我看教学,老师表达的意思应该不是这个,这个问题的答案等我后面更深入了解先把
实验二:
假设我让任务二自杀呢?

可以看到程序奔溃了,所以堆被占满了,没有得到释放。
这个图说明任务自己删除是没有办法清理空间的
我们空闲任务还可以做很多事情,怎么做到呢?可以靠钩子函数
上图说明如果我们要调用钩子函数,需要定义一个宏,然后空闲函数就会帮我们调用钩子函数
下面是钩子函数的限制:
这图说明,钩子函数只能处于进行状态和就绪状态,而且执行的越快越好,因为空闲任务还要执行清理工作
调用钩子函数操作:
先定义一个宏
然后设置钩子函数:
注意:要将其它任务优先级改成0,不然空闲函数调用不了,钩子函数也调用不了
实验现象:
可以看到任务一先被执行,然后创建任务二,任务二优先级最高先执行,然后自杀,然后因为空闲函数与任务一同级,所以会执行空闲函数,然后就会调用钩子函数
任务调度策略:


从上面知识我们知道,默认的调度策略:任务优先级高的能一直占着CPU,除非进入阻塞状态或者暂停状态 ,那还有没有其它的调度策略呢?有的
调度策略:
如上图,在默认的调度策略中,因为任务三的优先级最高,如果任务三没有调用vTaskDelay ()函数,那其它任务将一直得不到执行

可以看到我们还能设置不能抢占,只要把这个宏定义设置成0即可
下面我们来观察现象:
可以看到任务三在放弃任务后,任务一小人得志,一直占用CPU资源,所以如果我们使用这种调度策略,我们要在每个任务执行完后,调用一下xTaskDelay()函数,让它们去休眠,这样才能执行其它任务
还可以设置一个调度策略,同优先级任务不轮流执行,把该宏定义设置为0
实验现象:
可以看到任务三优先级最高,任务三先执行,等任务三进入阻塞状态时,轮到任务一执行,但任务一一直执行,没有与任务二交替执行,直到任务三退出阻塞状态,等任务三执行完后,才轮到任务二执行。
最后一个配置项:
该配置项是空闲任务应该礼让别人,什么意思呢?
可以看到在main函数中,任务一和任务二优先级都是0(跟空闲任务一个级别),那我们设置空闲任务礼让会怎么样呢?
可以看到正常的话,在空闲任务中它会执行很多次while()循环里面的代码,但如果我们设置空闲任务礼让的话,那它只会执行一次代码,然后就会礼让其它任务,让出CPU资源
那我们先设置空闲函数礼让
下面是实验现象:
可以看到空闲任务的波形很小,那我们不设置礼让呢?如下图:
可以看到空闲任务的波形是跟任务一和二是差不多的
本次纯理论完结,接下来我们会开始实操教学
更多推荐



所有评论(0)