FreeRTOS学习
FreeRTOS学习
在学完初步的stm32后,便开始了解到了FreeRTOS这个实时操作系统,可以很轻松的实现多任务管理,既可以同时运行多个while1的任务,不会有阻塞的情况。
本质上就是切换任务并且保存寄存器的值到栈里面,即保存现场,当切换回来的时候,又从栈里面将寄存器的值恢复回去。
我使用的是STMcubemx库生成的FreeRTOS的方式,简单易用,只需要开启FreeRTOS支持和简单创建几个任务,然后修改任务的运行函数就可以了,通过osDelay来控制运行时间。
首先是基础理论学习:
ARM架构基础知识
硬件架构
ARM芯片属于精简指令集计算机,有CPU,Flash,内存,其中Flash就相当于硬盘,读取写入速度最快,内存则比Flash快不少,最快的是CPU里面的寄存器,读写速度是纳秒级别,可以跟的上CPU的运行速度。
但是CPU的内部寄存器资源很有限,所以程序通常只会放一部分需要交给CPU运算的变量到寄存器里面,就是寄存器读内存的值,其他的基本都是放在内存里面,而Flash则是保存整个程序的。
寄存器一般的寄存器是r0,r1这样的,还有一些特殊的寄存器,其中有SP,LR,PC,SP一般是保存栈指针,LR则是一般用来保存返回地址,就是跳转到其他函数的时候,将需要跳转回去的函数地址写进去,而PC则是程序计数器,表示的是当前指令地址,写入新值就可以跳转。
常用汇编指令
读内存 Load
1 | LDR R0,[R1,#4] //读地址“R1+4”,得到的四个字节存入R0 |
写内存 Store
1 | STR R0,[R1,#4] //把R0的4字节数据写入地址“R1+4” |
加减
1 | ADD R0,R1,R2 //R0 = R1+R2 |
比较
1 | CMP R0,R1 //将比较结果保存在程序状态寄存器PSR |
跳转
1 | B main //直接跳转,PC写入main函数地址 |
在运行函数的时候,会先将所需要的内容,比如局部变量什么的,保存在栈里面,还有将LR也保存进去,然后CPU使用LDRD进行读取,将需要的东西读取到寄存器里面,然后再进行运算,运算完成以后,通过STR修改栈里面的数据,就是将CPU运算出来的结果再返回的栈里面,将栈里面的数据一起POP给CPU寄存器,同时也将之前保存的LR的地址POP给PC,这就完成一个运行一个自定义函数的过程。
堆和栈
堆就是一块空闲的内存,可以使用但是要记得释放。
一般实际使用堆分配内存即进行堆管理的时候,通常会带一个头部和后面的内存,头部通常储存了将会分配多少内存,在有了头部之后,我们接下来就可以很好地完成释放内存的的操作了,因为头部储存了内存大小,我们就知道了需要释放了多少内存,但是问题又来了,这样一直使用,不断分配释放内存,会发现有很多空的内存,我们应该怎么使用它空的内存呢,这里就就需要引入链表了。就是在头部中再加一个nextFree,指向下一个空闲的内存的地址,就是通过链表储存空闲内存,当我们在分配内存的时候,就会修改头部的nextFree所指向的地址,将这个地址指向新的空闲内存。
栈就是程序自己分配的一块区域,每个任务都有自己的一个栈,专门用来储存一个函数运行过程中的一些中间变量,比如储存不同的LR,还有过程中的不同的中间变量,在RTOS中还可以保存现场,将寄存器中的所有的值都存入栈里面。
其中,如果局部变量比较少或者不加volatile的情况下,这些局部变量都会保存在寄存器里面,这是因为编译的时候进行了优化,放到寄存器里面可以提高速度,但是如果局部变量比较多,或者加上了volatile,编译器就不会对它进行优化,就会将它直接保存到栈里面。
FreeRTOS编程规范
FreeRTOS中对函数和变量的命名都很有规范,通常会在一个变量的前面加入一些前缀,来表示这个变量的属性,更加简单易读。
这是对里面函数命名的规范。
这是对里面宏的命名规范。
内存管理
我们通常使用的就是heap4的堆管理方法,heap4可以很好地解决碎片化的问题,相邻空闲内存可以合并。
常见的内存管理里面的函数有
1 | void * pvPortMalloc( size_t xWantedSize ); |
这个就是释放和分配内存的
1 | size_t xPortGetFreeHeapSize( void ); |
这个函数的返回值就是当前堆中可用的剩余内存大小,这是给开发者用来优化内存分配策略用的
1 | void * pvPortMalloc( size_t xWantedSize )vPortDefineHeapRegions |
这个是钩子函数的使用范例,如果当运行分配内存失败了,并且钩子函数的宏定义是1,就会执行vApplicationMallocFailedHook这个函数,这个函数我们需要手动定义并且写内容
任务
创建任务
任务最核心的三要素:函数,栈,优先级。
1 | BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数 |
这是动态分配内存的方式创建任务,按照注释传递参数就可以了。
1 | TaskHandle_t xTaskCreateStatic ( |
这是静态分配内存的方式创建任务,其实使用也不难,只需要提前声明一个合适大小的数组作为栈,另外还需要提供一个TCB结构体,使用StaticTask_t声明一个就可以了,记得使用static,其他的使用跟动态分配没有什么大差别。其中创建静态任务返回的数据是任务句柄,而创建动态任务需要自己传进去任务句柄。
删除任务
1 | void vTaskDelete( TaskHandle_t xTaskToDelete ); |
传入的参数是任务句柄,也可以传入NULL,传入NULL就是自杀。
不过一般都是任务自己停止,而不是调用删除任务来暂停函数的执行。
如果想要优化一些任务,比如说优化音乐播放这种类似的场景,可以适当提高任务优先级,在FreeRTOS中,数字越大的优先级越高,并且使用软延时vTaskDelay而非硬延时。
任务状态
FreeRTOS中有四个任务,分别是Ready,Running,Blocked,Suspended。
这是一个完整的任务状态转换图。
一个任务被创建出来就在ready状态,这个时候随时都可以成为running状态。如果在一个任务里面有一些软延时vTaskDelay类似的函数,这个任务就会被切换成blocked状态,也就是阻塞态,等到触发到某个事件的时候,比如延时结束了,就又会回到ready状态。
暂停状态只能通过调用暂停状态函数才可以进入。
1 | vTaskSuspend(TaskHandle_t xTaskToSuspend); |
任务调度
- 相同优先级的任务轮流运行
- 最高优先级的任务先运行,一旦高优先级的任务就绪,就会马上运行,高优先级的任务没有运行完,不会执行低优先级任务
任务调度方面通过链表来进行管理。
都是ready的任务都是通过数组储存,比如最大的优先级是56,那么就会有一个readyList[56]的数组,里面的每一项都是一个链表,其中这个数组的索引值则代表一个优先级,同一个优先级的所有任务的TCB都是通过双向循环链表连接在一起的,一开始我们创建任务,从task0一直创建到task3,其中有一个全局变量加pxCurrentTCB,它会指向我们正在创建的任务,所以任务创建完成以后,调度开始,默认会从我们最后创建的任务开始执行。
FreeRTOS还会初始化一个tick中断,可以设置每1ms触发一次中断,中断就会发起一次调度,发起调度。
调度,先是从delayList里面遍历一边,看有没有可以被拿出来放入readyList的,如果没有,就从高到低遍历readyList里面的所有优先级,找到第一个非空的链表后,就会开始找pxCurrentTCB的位置,在pxCurrentTCB的位置的下一个TCB开始运行。
当任务在阻塞状态时,就会移出readyList,并且放入delayList,这个时候也会立马触发调度,重新寻找最高优先级的任务,并且从原来执行的任务的下一个TCB开始,继续运行任务。
所有的任务必须是死循环,不能返回出来,一旦返回出来了,就会自动跳转到prvTaskExitError函数,终止所有调度。
如果你打算设置你的任务只执行一会,就打算退出来了,就必须删除任务,删除任务有自杀和他杀,其中自杀就是由空闲任务进行释放TCB和释放栈,空闲任务的优先级是0,一开始调度就会创建这个空闲任务,且永远处于ready或者running状态。但是因为这个空闲任务的优先级是0,有的时候根本没有机会执行空闲任务回收内存,有可能会导致内存不够用。
所以我们应该使用vTaskDelay函数进行延时,让空闲函数有机会回收任务自杀的内存,
两个delay函数
1 | void vTaskDelay( const TickType_t xTicksToDelay ); |
第一个延时函数vTaskDelay是延时固定时间的函数,只能往后延时多少tick,第二个延时函数xTaskDelayUntil则是可以控制每次循环的周期相等。
其中xTaskDelayUntil的两个参数,第一个参数pxPreviousWakeTime,这个是起始时间,每次使用前都需要先获得一个起始时间,第二个参数xTimeIncrement是增加的时间,就是你打算让这个循环总共多少时间。
1 | BaseType_t preTime; |
这样子设置就是循环的周期固定为500tick了。