FreeRTOS

FreeRTOS

使用静态创建任务函数的话,返回值就是句柄,因此,要想对静态函数创建出来的任务进行操作的话需要创建一个临时变量以记录其返回值
使用vTaskDelet函数要注意不要在自己的进程内部自杀,这样会导致空闲任务无法清理内存

2023-12-24_09-51

2023-12-19_15-36
TCB结构体内部包含函数指针,SP位置,优先级,函数名字,函数参数等,但并不包括函数的指针(用来执行本函数),也不包括函数参数,CPU通过保存所有寄存器的值来保存这两项,保存的位置在该函数栈的顶部,其中R0,R1…保存了函数的参数(约定俗成),R15(PC)保存了函数的指针,来进行函数的跳转操作
优先级最大值只能取到最大值-1,高于这个值的会自动变为最大值-1
空闲任务(xIdleTask)是启动调度器时就自动创建的,空闲任务可以使用自带的钩子函数创建出来,也可以用来处理下列任务

  1. 执行一些低优先级的,后台的,需要连续执行的函数
  2. 进入省电模式
  3. 测量系统空闲时间

空闲任务的限制: 不能让空闲任务进入阻塞状态或者暂停状态
取消支持时间片流转会使优先级相同的其中一个任务在未被高优先级打断前始终霸占CPU
调度策略配置项

  1. 是否抢占(高优先级任务是否可以立即打断低优先级的执行)
  2. 允许抢占时,是否允许时间片流转(同一优先级任务执行时,是否允许相互打断执行)
  3. 允许抢占并且允许时间片流转时,是否允许空闲任务让步(空闲任务和用户任务在同一优先级时,空闲任务是否让出时间片)
    博客园

2023-12-16_10-16
采用这种方式运行并发程序会导致Task3 Task4在将锁置为1之前就都已经进入if语句内部了,这就会导致概率性并发Bug
写队列函数xQueueSend与xQueueSendToBack是一样的,都是默认在队列尾部写入数据,而xQueueSendToFront则是在头部写入数据(头部就是pcReadFrom指针所指的地址,并不是队列的最右侧),此时还需要将pcReadFrom指针减去一个ItemSize的地址
使用队列集并在队列里面写入数据时会把任务的数据写在队列里,并把任务的Handle写在队列集(Queue Set)里,读一次Queue Set后返回的Queue只能读一次,不能读多次,普通的Queue也一样因为读队列后系统自动删除数据
二值信号量的Give函数会检测信号量是否为1,若为1则会返回失败,而Take函数则会检查信号量是否为0,并且可以设置阻塞时间
信号量不能是负值
创建二值型信号量时默认初始值是0,需要Give一下,互斥量则不需要
信号量:Semaphore 互斥量:Mutex
只有申请方和接收方有锁的情况下才会产生优先级反转,如果其中一方无锁则正常调度就不会产生优先级反转,解决优先级反转的办法是优先级继承
互斥量有优先级继承的功能,而信号量则没有
死锁的原因有两种

  1. A函数内部二次上锁,A将自己阻塞了但其他任务并不能解锁造成死锁: A获得了锁,A调用一个库函数,这个库函数需要获得上面那个锁,死锁发生
  2. A依赖B的锁,B又依赖A的锁,此时会死锁
    互斥量解决了第1点,办法是使用递归锁(Recursive Mutex),递归锁解决了第2点,采用了谁持有谁释放的方法

事件组要么等待事件中的某一个发生,要么等待事件中的所有都发生,不能等待事件中的指定某几个发生
2023-12-17_19-34
2023-12-17_19-38
2023-12-17_19-34_1
2023-12-17_19-35
2023-12-17_19-35_1
切换任务后是直接执行在任务中断处执行,一般为while(1)循环内继续执行,因此对于在循环外部的初始化变量一定小心,例如在while(1)外部定义了i,while(1)循环内部定义了for循环,使用的变量是前面的i,这样,当第一次while(1)循环结束后i的是值为跳出循环的值,从本次循环开始,就不会再进入for内部了
任务通知是多对一的关系
对于任务通知来说,发送方发送的结果只有两种,要么成功要么失败,并不会等待,而对于接收方则有三种情况
2023-12-18_17-35
xQueueReceive()与xEventGroupWaitBits()中的WaitForAllBits数量有关,如果WaitForAllBits是pdFalse时,则需要一个xQueueReceive()就够了,否则会造成从未赋值的中间变量中取数据的Bug,造成的现象是同时多个数据被写入队列时会产生数据被读取多次,次数取决于并发的程序数量,不同时写入数据则会只覆盖队列中第一个数据,第二个数据始终被保留。而WaitForAllBits是pdTure时,则需要多个xQueueReceive(),这样才会有足够多的中间变量存储数据,而后被一次读走(具体实验可以看24_freertos_example_tasknotify_event_group)
使用任务通知构造轻量化事件组无法等待指定的某个任务,但是可以通过判断xTaskNotifyWait()中的pulNotificationValue值来进行等待某个指定任务
定时器函数在守护任务中被执行,每当产生Tick时定时器函数会比较当前Tick与预设Tick,并根据结果来执行回调函数(CallBackFun),想要设置其定时器函数的具体参数需要经过队列通信来完成
2023-12-19_17-09
调用如上函数时,如果不加static关键字会导致每次调用函数都会产生新的栈和新的int,这样int的值就不是理想的,如果初始化cnt为0,则每次都会打印0,也不符合逻辑,解决办法是使用static关键字来存储每次产生的cnt
FreeRTOS对于中断会使用不同的ISR(Interrupt Service Routines)函数,这是为了保证ISR执行的时候不能处于阻塞状态,这样才能保证实时性
临界资源是指能够被多个进程共享,但是同一时间只能由一个进程访问的资源,因此是互斥的
用到syscall的中断可以通过调用系统函数被屏蔽掉,这样即使是优先级比较低的中断也可以安心访问临界资源,CotexM3/M4调用syscall的中断位于中断向量表的191255位,0255位是可编程异常,-30位分别是复位、NMI、硬件错误,屏蔽中断本质上是调用191号中断,从而将191255中断屏蔽
在中断中调用中断恢复函数会将中断恢复至原来的状态,可能是中断开启也可能是中断屏蔽,而在任务中调用中断恢复宏定义则会将中断打开,因此,中断恢复函数有返回值
任务切换一定要给延时!尽量不要在定时器回调函数内部进行延时操作,定时器守护任务优先级及其栈深度在Config文件内被定义,默认为4,要想抢占定时器回调函数注意修改配置
任务自杀后就永远不会被执行,除非被再次创建
编程时,一般的逻辑错误都容易解决。难以处理的是内存越界、栈溢出等。
并没有很好的方法检测内存越界,但是可以提供一些回调函数:

  • 使用pvPortMalloc失败时,如果在FreeRTOSConfig.h里配置configUSE_MALLOC_FAILED_HOOK为1,会调用:
1
void vApplicationMallocFailedHook( void );

在切换任务(vTaskSwitchContext)时调用taskCHECK_FOR_STACK_OVERFLOW来检测栈是否溢出,如果溢出会调用:

1
void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName );

怎么判断栈溢出?有两种方法:

  • 方法1:
    • 当前任务被切换出去之前,它的整个运行现场都被保存在栈里,这时很可能就是它对栈的使用到达了峰值。
    • 这方法很高效,但是并不精确
    • 比如:任务在运行过程中调用了函数A大量地使用了栈,调用完函数A后才被调度。
  • 方法2:
    • 创建任务时,它的栈被填入固定的值,比如:0xa5
    • 检测栈里最后16字节的数据,如果不是0xa5的话表示栈即将、或者已经被用完了
    • 没有方法1快速,但是也足够快
    • 能捕获几乎所有的栈溢出
    • 为什么是几乎所有?可能有些函数使用栈时,非常凑巧地把栈设置为0xa5:几乎不可能

简而言之,使用上面两个Hook函数需要配置Config.h,并要求自己实现Hook函数的功能(内存回收以防止nalloc失败等)

FreeRTOS的内部机制

Arm架构汇编指令:
读指令 LDR (Load Register) LDR R0, [addr]
写指令 STR (Store Register) STR R0, [addr]
加指令 ADD ADD R0,R1,R2 //R0=R1+R2
PUSH指令本质是写指令, Push {R3,LR}把LR,R3 Push到内存中的栈,把LR放在高地址,R3放在低地址
CPU中重要的寄存器: SP(R13) 栈顶指针寄存器 LR(R14) 返回地址寄存器 PC(R15) 当前指令寄存器 CPSR(A64架构) 状态寄存器,LR是一种特殊的PC
POP指令本质是读指令 POP {R3,LR}
栈可以用来保存现场,任务可以理解为函数+函数的栈
保存现场的三种情况

  • 切换任务时,保存所有的寄存器
  • 调用其他函数时,但由于前几个寄存器(R0,R1,R2…)被用来传参,因此不需要保存
  • 硬件中断时,硬件会自动保存一部分寄存器,但是还需要软件保存一部分

栈的大小取决于局部变量的大小以及函数调用的深度,函数调用需要Push LR
TCB结构体内部包含函数指针,SP位置,优先级,函数名字,函数参数等,但并不包括函数的指针(用来执行本函数),也不包括函数参数,CPU通过保存所有寄存器的值来保存这两项,保存的位置在该函数栈的顶部,其中R0,R1…保存了函数的参数(约定俗成),R15(PC)保存了函数的指针,来进行函数的跳转操作
2023-12-30_10-48
申请堆栈的指令名为SPACE,这是在初始化文件里就执行的
2023-12-24_21-33_1
main函数的栈是在STM32汇编文件文件中设置的,与芯片厂商相关,步骤如下

  1. 在内存中申请一段空间(首地址为__initial_sp,也是msp(Main_Stack_Pointer,也是sp),同时会给中断函数的栈使用
  2. 然后转入向量表,找到__initial_sp
  3. 执行Reset_Handle,在Reset_Handle内部跳转到了main函数

任务调度原理: 任务存在三个大类链表中,分别是ReadyList链表数组,DelayList,PendingList。其中ReadyList链表数组按照优先级排列(0-4)共有五个链表,FreeRTOS会先从ReadyList中最高优先级的一个开始寻找任务并执行,当高优先级任务发生阻塞时会将其从ReadyList移到DelayList,这时就不会执行了,而是去找低优先级的任务去执行。当创建任务时,会有指针指向当前创建的优先级最高的任务,因此,最后创建的任务总会先执行,然后再按前文所说按优先级高低依次执行其他任务。
选要注意的是,当所有任务优先级为0时,最后创建的是IdleTask。因此指针指向的是空闲任务。最终程序首先执行的IdleTask并没有现象,程序“看起来”好像是从最初的Task开始运行的
2023-12-25_09-14
在整个任务调度的过程中利用的是任务的TCB结构体
为防止写队列过程中发生调度导致失败,所以在写队列中需要互斥,这是通过关闭中断来实现的(任务调度本质上是定时器产生中断)
一函数写队列时,会唤醒其他等待队列的函数,当其他等待队列的函数条件符合时会准备进行切换任务,条件不符合时会将写队列的函数进行休眠以节省CPU资源。当使用队列且函数休眠时不仅把自己从ReadyList移到DelayList,还会在等待队列中写入自身,等待其他任务写队列时来唤醒它
在使用队列读写数据时,首先关中断,之后读写数据,在读写之后还需要进行链表操作来调度和通知任务,最后再开中断
因此使用队列有两点好处

  • 实现互斥
  • 节省CPU资源,提高程序运行效率
    队列要点
  • 关中断实现互斥
  • 环形缓冲区保存数据
  • 链表实现休眠唤醒

内存中队列的构成: Queue_t(队列头)+Buffer(数据缓冲区),队列头由队列结构体构成,内部包含xTaskWaitingToSend链表以及xTaskWaitingToReceive链表及其对应的指针,数据缓冲区大小 = ItemSize * Length
xTaskWaitingToSend以及xTaskWaitingToReceive仅仅是为了记录哪个任务需要读写数据,真正的调度还需要去执行ReadyList与DelayList之间的操作,这样是为了防止在中断内部阻塞
critical: 临界区
读队列内核操作
2023-12-25_11-40
信号量是一种特殊的队列,它不传递数据,因此它只有Queue_t(队列头),在Queue_t内部还有个链表用来记录Take失败的任务,以便之后唤醒
互斥量优先级继承是通过高优先级任务的优先级赋值给低优先级任务完成的,在这之前还需要记录低优先级任务的优先级,这样才能保证Give互斥量之后回到原优先级
事件组某些位被设置后会唤醒事件组里所有DelayList的任务将其移到ReadyList,然后每个任务依次检查是否符合执行条件,符合的执行,不符合的再次进入DelayList
使用队列和信号量时为了防止其他任务干扰需要关调度器,同时为了防止其他中断任干扰也要关中断,但是关中断后再关调度器也就没有意义了,因此在其内部源码中只是关闭中断而已。而事件组只是关闭调度器即可,这是因为FreeRTOS不会在中断中使用事件组,本质原因是事件组的FromISR函数并没有实际切换任务,而是写一个链表,等待事件组的FromISR函数执行完毕后才会进行实际的任务切换,这样一旦对事件组的某些位设置就不会在中断中唤醒所有任务了,否则会在中断内消耗大量时间。
任务通知能且只能点对点的唤醒任务,具体过程和前几种方法类似,都是先关中断,然后发通知的一方设置eaction的值,等通知的一方需要接受值,判断执行什么指令。之后由系统进行链表操作及状态转移,最后开中断
对于定时器任务, FreeRTOS的处理与其他操作系统不同。其他操作系统由硬件定时器(systick)出发形成软件定时器,在软件定时器内部直接调用待处理任务。而FreeRTOS认为,软件定时器也是中断,为了防止在中断内部直接处理任务会导致阻塞,因此采用了队列的方式处理任务,这与xTaskWaitingToSend以及xTaskWaitingToReceive在关中断内记录要更改的链表操作类似
定时器队列中,写队列一侧是systick,读队列一侧是守护任务。有趣的是创建Timer任务和启动Timer也是写队列,所以在函数xTimerStart(xTimer,xTicksToWait)中有两个参数,第二个参数给的就是当定时器队列满时是否接受等待
中断的优先级: systick服务于任务调度,pendsv服务于任务切换,其他的类似GPIO的中断用于业务,为了能够让业务正常执行,GPIO中断优先级最高,systick和pendsv优先级最低
2023-12-27_10-19
pendsv本质上是保存现场和任务切换的汇编代码

1
2
3
4
5
6
7
taskENTER_CRITICAL();
/*访问临界资源,防止其他任务,ISR破坏*/
taskEXIT_CRITICAL();
----------------------------
vTaskSuspendAll();
/*关闭调度器,防止其他任务破坏性访问*/
xTaskResumeAll();

2023-12-30_09-55
可以通过116行将宏消除(因为宏未定义,编译器会消除宏)
2023-12-30_10-22
FreeRTOS的链表构成如上图,xList负责产生头xMINI_List_Item,并记录链表中信息(如元素个数,当前指针的位置),xMINI_List_Item本身作为链表头,xList_Item产生节点。要注意xList_Item的pvOwner才是学习链表时的”container”(容器),container内部有下一个Node,Node里面也有下一个的container,而对应Node节点的就是FreeRTOS的Item。此时的pxContainer是指向新加入元素的那个List的指针
内存管理:
有些芯片支持对指针取指赋值,有些芯片则根本不支持,考虑的原因是防止内存不对齐而引发错误,在使用malloc函数中需要注意
生成堆的本质就是申请一块全局数组
heap1到heap5内存回收机制逐渐增强,一般常用heap4
heap_1.c里,只实现了pvPortMalloc函数,vPortFree函数并未实现。并在malloc函数内实现了互斥,以防止其他任务再次进行malloc

使用heap1进行内存申请时,首先heap1内部会定义两个指针,pucAlignedHeap与xWantedSize,pucAlignedHeap主要负责内存对齐,每次pucAlignedHeap移动到下一个内存对齐的地址(最大移动8个地址)。xWantedSize负责给出申请空间的尾地址,这样将xWantedSize赋值给pucAlignedHeap就可以知道下一次该在何处对齐内存。例如,想在0x4000 0001处申请99字节的空间,首先pucAlignedHeap会指向0x4000 0004处(32位机内存块大小为4*8字节,所以pucAlignedHeap最大偏移量为4),而后计算xWantedSize大小,考虑99不能被4整除,因此向上取整为100,所以新申请的内存地址为0x4000 0004至0x4000 0104,而0x4000 0001到0x4000 0003处内存被丢弃
heap2实现了内存回收,但没有解决内存碎片问题



heap2实现的内存分配的方法是,首先生成一个链表,让链表头指向一个未分配的很大的heap(大小17k),每当申请空间时首先会在已经空闲的block中找出是否能拆分出需求大小的block,如果不能,则从未划分block的堆中划分出一个新block,划分出的block内部包含申请空间的大小以及下一个空闲内存的指针。如果找到大于申请内存大小的block时,首先将该block裁剪为申请内存的大小,并在新申请的block记录该block大小,之后会将链表内的指针指向下一个未被占用的block。
heap2实现的内存回收的方法是,当需要free时,链表头就会free指定的地址,并将当前指针向前移动(pxNextFreeBlock+xBlockSize)个地址,从而找到block起始位置,但是这种方法并没有实现将这些已经free的内存重新归还到整个未被使用的内存中,因此这种free的空间在整个内存上来看是被各种pxNextFreeBlock+xBlockSize结构体分割出来的,这就是 内存的碎片化
需要注意的是,使用heap2进行malloc操作的时候,链表申请是从小到大排序的,这样方便查找需要的block
heap能够进行内存碎片回收,链表按地址排序
2023-12-31_15-26
为了增加程序的健壮性,heap4加入了block使用的标志位(1<<31)
2024-01-01_12-36
2024-01-01_12-29
heap5与heap4的不同点在于heap5支持多块内存,在使用heap5时需要手动调用vPortDefineHeapRegions()函数,并以数组方式传入想要划分为堆的地址,这个数组以NULL结尾来确定数组结束的标志