嵌入式八股文总结(FreeRTOS篇)

        最近在准备秋招,在此每天总结一些面试常问的问题,希望可以和大家一起进步。(持续更新中……)

目录

1. 优先级反转

2.如何合理地设定任务栈地大小

3. 前后台程序与实时操作系统的区别是什么?

4. 剥夺型内核和不可剥夺型内核的区别

5. FreeRTOS中的IPC通信都用过哪些

6. 请说说FreeRTOS的启动流程

7. FreeRTOS是如何进行任务切换的

8. 空闲任务有什么作用

9. Tickless低功耗模式

10. 什么是任务控制块(TCB),在FreeRTOS中有什么作用

11. 什么是临界区和临界资源,原理是什么

12. 什么是任务死锁,如何处理

13. 介绍下FreeRTOS内核的基本组成

14. 介绍下FreeRTOS的调试技巧


1. 优先级反转

        在使用二值信号量和计数型信号量的时候会遇到优先级反转的问题,比如现在有三个任务:H、M、L,H任务优先级最高,L任务优先级最低。L先运行,H、M处于阻塞态,此时L要访问共享资源,所以获取到信号量。此时H开始运行,抢占L任务,H要访问共享资源,但信号量无资源了,H阻塞等待,L继续运行。此时M任务开始运行,抢占任务L。M运行完毕,L继续运行,L释放信号量,H任务唤醒,运行任务H。可见,H任务的任务优先级最高,但却最后执行,图示如下:

e2ddd05e2ad44cd1bcda4c13af09276e.png

解决方法:

        优先级继承,低优先级获取到信号量时,若高优先级任务尝试获取信号量,则临时将低优先级任务的等级提升至 和高优先级任务相等,高优先级获取到信号量后恢复地任务优先级等级。优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响。

补充:互斥信号量其实就是一个拥有优先级继承的二值信号量,注意互斥信号量不能用于中断服务函数中,原因如下:

        (1) 互斥信号量有任务优先级继承的机制,但是中断不是任务,没有任务优先级,所以互斥信号量只能用与任务中,不能用于中断服务函数。

        (2) 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

2.如何合理地设定任务栈地大小

        可以先给任务设置比较大的任务栈,确保不会出现栈溢出的情况,然后让系统运行一段比较长的时间,同时尽量触发各种可能的情况,可通过uxTaskGetStackHighWaterMark() 函数来查看任务最多使用了多少栈空间,一般将最终的栈大小设置为该值的1.5~2倍是比较合适的。

3. 前后台程序与实时操作系统的区别是什么?

        前后台系统中所有任务都是平级的,都放在一个大循环中,运行一个任务前必须等待前面的任务执行完成,实时性比较差。单片机为了处理紧急事件,设置了中断机制,我们把中断机制成为前台。执行中断时要花费时间去处理中断点的信息,所以频繁使用中断的话会占用较多的CPU资源。

        而实时操作系统采用时间片轮转的方式来执行任务,并会为每个任务分配不同的优先级,高优先级的任务先执行,极大地提高了CPU的利用率,并且提供系统的实时性。   

4. 剥夺型内核和不可剥夺型内核的区别

        不可剥夺型内核是指在执行任务过程中,一个任务可以一直运行,只有当前运行的任务主动放弃CPU控制权才会进行任务切换。可剥夺型内核是指系统可以强制中断正在执行的任务,并将控制权交给其他高优先级的任务。比如FreeRTOS就是剥夺型内核,通过时间片轮转来实现任务切换。

5. FreeRTOS中的IPC通信都用过哪些

 1) 消息队列:消息队列主要用于任务与任务间、消息与任务间通信。队列的本质是一个环形缓冲区,可以存储数量有限、大小固定的多个数据,创建队列的时候就要指定队列的长度和队列项的大小。队列的特点是先进先出,任何任务或中断都可以项队列写入数据或从队列读出数据,当任务向队列写数据时队列已满或读数据时队列为空,则会进入阻塞状态并加入到等待链表中,可设置阻塞时间。当队列可用时,优先级最高的任务获取队列,若任务优先级相等,等待时间最长的任务获取到队列。基于队列,FreeRTOS 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、递归互斥信号量

2) 信号量:信号量主要用于解决同步问题,实现对共享资源的有序访问。信号量是基于队列实现的,但由于任务同步和互斥访问不需要传递数据,主要就是依靠计数值,所以信号量不需要队列后面的环形缓冲区,只有队列头部,所以比队列节省资源。根据计数值的不同可分为二值信号量和计数型信号量,二值信号量只能取0和1两种值,计数型信号量则没有数值大小的限制。为了解决二值信号量和计数型信号量优先级翻转的问题,引入了互斥信号量和递归互斥信号量。需要注意的是,互斥信号量和递归互斥信号量不能在中断中使用

3) 事件标志:事件标志是用来指示事件是否发生的布尔值,只有0和1两种状态,FreeRTOS将多个事件标志存储在一个 EventBits_t 变量中,这个变量就是事件组,事件组也就是多个事件标志的集合。这个EventBits_t 变量其实是一个 16 位或 32 位无符号的数据类型数据,当发生某个事件时,任务就可以将事件组中某位设置为1,表示发生了指定的事件,其他任务可通过读取事件组查看是否发生了某个事件,进而做相应的处理。

4) 任务通知:每个任务都有两个用于任务通知的数组,分别为任务通知数组和任务通知状态数组,这两个数组为任务控制块中的成员变量,因此任务通知的传输是直接传出到任务中的,不需要像队列那样创建一个通讯对象,示意图如下。任务通知向任务发送事件或数据比队列这些方式快得多,并且使用任务通知可以节省空间,因为每个任务通知只需要在每个任务中占用固定的 5 字节内存。但任务通知也有一些限制:比如不能向中断发送事件或数据,因为中断没有任务控制块,但是中断可以向任务发送事件或数据。其次,任务通知没有缓存能力,本次任务通知会覆盖掉上一次的任务通知,一个任务通知值只能保存一次。

db84e851287d4db9a3d85e94cf72c7c0.png

间接通讯示意图

6fe6726f18b14ab5978d0c78885a4d25.png

直接通讯示意图

6. 请说说FreeRTOS的启动流程

第一步:系统给上电后,首先会调用复位函数Reset_Handle,然后调用__main来初始化堆栈,最后跳转到C中的main函数中。

第二步:main函数中会进行系统初始化(比如初始化系统时钟)和外设始化(比如初始化I2C、SPI等),并创建相应的任务,所有的任务都创建完成后则调用vTaskStartScheduler()函数来启动任务调度器,任务调度器是FreeRTOS的核心。

第三步:开启任务调度函数vTaskStartScheduler()中会执行如下操作:

        ① 创建空闲任务、定时器服务任务、关闭中断;

        ② 设置PendSV和Systick为最低优先级的中断,并且开启Systick中断;

        ③ 初始化一些全局变量,将变量xSchedulerRuning设置为True,表示调度器开始运行;

        ④ 初始化时钟节拍计数器、初始化临界区嵌套计数器,如果ARM内核支持FPU,还会使能FPU;

        ⑤ 最后调用vPortStartFirstTask()函数运行第一个任务(任务优先级最高的任务),这是一个汇编函数,通过SVC中断来启动第一个任务。(整个FreeRTOS中只在这里使用了一次SVC中断)

7. FreeRTOS是如何进行任务切换的

        任务切换的本质其实就是CPU寄存器的切换,主要分为两步:保存当前任务的寄存器值到任务堆栈(保存现场),将要切换的任务的寄存器值恢复到CPU寄存器中(恢复现场),保存现场和恢复现场也就是常说的上下文切换。FreeRTOS是通过PendSV中断来进行任务切换的。PendSV中断可由systick定时器中断触发,也可以由FreeRTOS提供的portYIELD()函数进行触发。这两种触发方式其本质上其实都是向中断控制和状态寄存器ICSR的bit28中写1来触发的。PendSV中断服务函数中执行的操作就是:保存当前任务的CPU寄存器信息到任务栈中,然后找到下一个要执行的任务的任务控制块,更新PSP指针为当前任务的任务堆栈指针,将要切换的任务的任务栈中的数据恢复到CPU寄存器中。(任务中使用PSP指针,中断中使用MSP指针)

注:为什么要在PendSV中进行任务切换呢?

        将任务切换放在PendSV中断中进行,是因为 PendSV具有挂起特性,FreeRTOS将PendSV的中断优先级设置为了系统最低的优先级,在高优先级中断中可以触发PendSV中断,并且当高优先级中断执行完成后才会执行PendSV中断。下面先看下不使用PendSV进行任务切换的情况:

a6708c8813be4179b2a77f2d46a5647c.png

        如上所示,如果在systick中进行任务切换的话,若在Systick中断前发生中断请求,则Systick会抢占中断请求,导致中断被延时处理,这影响了系统的实时性,在实时系统中是不允许出现的。并且当 SysTick 完成任务的上下文切换,准备返回任务中运行时,由于存在中断请求,ARM Cortex-M 不允许返回线程模式,因此,将会产生用法错误异常(Usage Fault)。

094d4f61ddb04860b5bb079ce8d9f84d.png

        如上所示,Systick中只触发PendSV中断,具体的任务切换放在PendSV中进行处理。因为PendSV中断具有挂起特性,也就是说系统会先执行比PendSV优先级高的中断,高优先级的中断执行完成后才会去执行PendSV中断,这就避免了因任务切换而导致的中断处理延时的问题。这里可能会有个疑问,在中断处理完成之后才进行任务切换,会影响FreeRTOS的时钟精度吧。其实没关系的,Systick中断是FreeRTOS的心跳节拍,这个心跳频率只要稳定在一个范围内就可以,满足正常的任务调度就行,快点慢点也不用那么计较。

8. 空闲任务有什么作用

        当调用 vTaskStartScheduler()函数启动任务调度器时,会自动创建一个空闲任务,空闲任务的任务优先级为0,所以不会占用其他就绪态任务的被执行时间。其实,空闲任务的主要功能就是处理待删除的任务列表和进入低功耗,具体介绍如下:

1) 防止处理器空转:空闲任务被看做是一个后备任务,当其他任务没有工作要执行时,就会执行空闲任务,以防止处理器空转。因为RTOS要对外部事件作出快速响应,通过空闲任务就可以保证系统的连续性和响应性。

2) 释放内存:当任务中调用vTaskDelete()函数删除自身任务时,该任务不会立刻被删除,而是先将这个任务添加到待删除列表中,之后由空闲任务来对任务的内存资源进行回收。所以在删除任务后若想要立即分配资源,应该稍微延时一下,给空闲任务一些回收资源的时间,否则可能加重资源碎片的风险。

注意:如果任务中删除的不是自身任务,而是其他任务的话,那么被删除任务的删除工作可以由函数 vTaskDelete()的调用者完成,就不需要空闲任务来释放了。被删除的任务如果是静态创建的,那内存资源就需要用户自己来释放了。

3)执行钩子函数: 可通过配置FreeRTOSConfig.h 文件中的相关宏,来使能空闲任务中相应的钩子函数实现一些功能,比如统计系统信息、进入低功耗模式等,这些功能需要用户自己来实现。如下所示,如果要启用相应的钩子函数,只需将对应的配置项配置为 1 即可,当然也不要忘了编写相应的钩子函数。

注意:不论在任何时候,都要保证系统有任务在被执行,所以不能在钩子函数中调用使空闲任务阻塞或挂起的函数,比如不能调用延时函数vTaskDelay()。在空闲任务中可以进入低功耗模式,也就是每次进入空闲任务时,进入相应的低功耗模式,在每次 SysTick 中断发生的时候就会被唤醒,这是比较常用的低功耗方式,所有RTOS都可以使用此方式实现低功耗。同时FreeRTOS也提供了一种低功耗 Tickless 模式,后面会介绍,相比之下,低功耗 Tickless 模式的低功耗效果更好一点。

9. Tickless低功耗模式

        在智能穿戴、物联网等产品中都会对功耗有严格的要求,这里总结下FreeRTOS的Tickless低功耗模式。Tickless低功耗模式是基于MCU的硬件层面的相应低功耗模式来实现的,先回顾一下STM32的三种低功耗模式:睡眠、停机、待机三种模式。

        由上表可知,进入不同的低功耗模式后会关闭不同的电源区域,STM32的电源主要分为三部分:模拟部分供电、数字部分供电和后备供电,图示如下。

1) 睡眠模式:STM32进入睡眠模式时,程序暂停运行,但所有的I/O引脚都保持运行时的状态;当发生任一中断或唤醒事件后,程序从暂停的地方继续运行;睡眠模式下只是关闭了CPU的时钟,也就是只有CPU停止运行,但寄存器和存储器中的内容不会丢失

2) 停机模式(停止模式):STM32进入停机模式时,程序停止运行,但所有的I/O引脚都保持运行时的状态;当发生任一外部中断或外部中断对应的唤醒事件后,程序从暂停的地方继续运行;停机模式下,除了关闭CPU的时钟外,还关闭了1.8V区域的时钟,也就是关闭了外设的时钟,所以CPU和外设都会停止运行,但电压调节器没有关闭,所以寄存器和存储器中的内容不会丢失

注意:当进入停止模式时,PLL、HSI和HSE都会被关闭,退出停止模式后,会选择HSI的8MHz直接作为系统的主频,所以我们通常在退出停止模式后立刻启动HSE,重新配置系统主频。

3) 待机模式:STM32进入待机模式,程序停止运行,所有的I/O引脚变为高阻态(浮空输入);当发生指定的事件时(见前面的表格)才会退出待机模式,唤醒后程序会从头开始运行;待机模式下,会关闭CPU时钟、关闭1.8V区域时钟、关闭电压调节器,所以CPU和外设都会停止运行,并且寄存器和存储器中的内容会丢失,但是后备供电区域还是可以正常运行的。

        上面介绍了三种低功耗模式的操作和效果,那么这三种模式到底有多省电呢。通过查看对应的数据手册,可看到相应的电流值如下:

1) 运行模式下的最大电流消耗典型电流消耗(STM32F1系列)

2)睡眠模式下的电流消耗

3) 停止和待机模式下的电流消耗

        上面介绍了裸机实现低功耗的三种模式,下面正式介绍FreeRTOS的Tickless低功耗模式。Tickless的本质其实就是通过调用指令WFI进入睡眠模式来实现低功耗。在系统的运行过程中,绝大部分时间都在运行空闲任务,所以可以在空闲任务中进入低功耗模式,当其他任务准备运行时再唤醒CPU,退出低功耗模式。这里需要考虑如下两个问题:

1) 进入低功耗模式后多久被唤醒,也就是要确保下一个任务被准确唤醒

2) 进入睡眠模式后,任何中断都会唤醒CPYU,那么滴答定时器频繁进入中断就会频繁唤醒CPU,这会影响低功耗的效果

        这两个问题已经由FreeRTOS的Tickless模式替我们处理好了,我们只需要使用它就行了。解决方式为:调用prvGetExpectedIdleTime()函数获取下一个任务的解锁时间,也就是进入低功耗的事件,然后将滴答定时器的中断周期修改为低功耗的运行时间,当产生滴答定时器中断或其他中断后退出低功耗模式,并且补上低功耗事件漏掉的系统时钟节拍数。Tickless模式的使用步骤介绍如下:

① 配置宏configUSE_TICKLESS_IDLE为1,使能Tickless低功耗模式;

② 配置宏configEXPECTED_IDLE_TIME_BEFORE_SLEEP,设置进入低功耗模式的最短时长,最低为2,也就是两个滴答定时器周期;

③ 配置宏configPRE_SLEEP_PROCESSING(x),定义一些需要在系统进入相应低功耗模式前执行的事务,例如可以在进入低功耗模式前关闭一些 MCU 片上外设的时钟,以达到降低功耗的目的;

④ 配置宏configPOSR_SLEEP_PROCESSING(x),定义一些需要在系统退出相应低功耗模式后执行的事务,例如开启在系统在进入相应低功耗模式前关闭的 MCU 片上外设的时钟,确保系统能够正常运行。

10. 什么是任务控制块(TCB),在FreeRTOS中有什么作用

        任务控制块其实就是一个结构体变量,用于保存任务的属性信息,比如执行任务堆栈的指针、任务的优先级、任务栈的起始地址、任务名等信息。任务控制块中的成员信息可通过配置FreeRTOSConfig.h 配置文件中相关的宏来进行裁剪。FreeRTOS 中的每一个已创建任务都有一个自己的任务控制块,之后系统就可以通过任务句柄(指向任务控制块的指针)来对任务进行操作了。

11. 什么是临界区和临界资源,原理是什么

        临界区是指那些必须完整运行的代码区域,在临界区中的代码必须完整运行,不能被打断。临界资源就是临界区中的共享资源,比如消息队列、共享内存等。FreeRTOS在进出临界区时通过关闭和打开受FreeRTOS管理的中断来保护临界区中的代码,注意,FreeRTOS的临界区是可以嵌套的,也就是程序可以重复进入临界区,后续也需要重复退出相同次数的临界区。

12. 什么是任务死锁,如何处理

        任务死锁是FreeRTOS中比较常见的问题,表现为多个任务互相等待对方释放资源,任务死锁的现象如下:

        比如现在有两个任务:任务A和任务B,这两个任务要访问两个共享资源:资源M和资源N。此时,任务A获取到了资源M,任务B获取到了资源N,当任务A接着尝试获取资源N时会进入阻塞,等待任务B释放资源,若此时任务B尝试获取资源M,同样会进入阻塞,等待任务A释放资源,这就造成了任务死锁,两个任务互相等待对方释放资源。

        发生任务死锁需要同时满足以下四个条件:

1) 互斥条件:多个任务不能同时获取同一共享资源;

2) 持有并等待条件:任务会因等待获取资源而进入阻塞等待,并且不会放弃自己已经持有的资源;

3) 不可剥夺条件:任务获取资源后,除非自己释放,否则换路其他任务无法获取;

4) 环路等待条件:死锁发生后,任务间获取资源的顺序形成一个环路,也就是若干任务之间形成一种头尾相接的循环等待资源关系。

        我们就可以根据上面的四个条件来避免死锁,对应的方法如下:

1)破坏互斥条件:也就是允许多个任务同时访问同一共享资源,比如多个任务读取数据文件时可使用这种方式;

2)破坏持有并等待条件:一次性给任务分配所有资源,这样任务就不会因为获取资源而进入阻塞等待了,如果有一个资源得不到分配,那么该任务将不会被分配任何资源;

3) 破坏不可剥夺条件:当任务因获取资源而进入阻塞时,将释放掉目前所持有的所有资源;

4) 破坏环路等待条件:给每类资源分配一个编号,每个任务按照编号的顺序来获取资源,释放资源时也必须按照编号来依次释放。

        除以上四种方法外,还可采用某种算法(如银行家算法)来对资源进行分配,从而来预防死锁,银行家算法可看这篇博文:操作系统——银行家算法(Banker's Algorithm) - 王陸 - 博客园

13. 介绍下FreeRTOS内核的基本组成

        FreeRTOS内核是系统的核心部分,主要由任务控制块(TCB)、任务调度器、上下文切换机制三部分组成,介绍如下:

任务控制块(TCB):每个任务都有一个任务控制块,用于保存任务的状态、优先级、堆栈指针等信息;

任务调度器:调度器根据任务优先级调度算法决定要运行的任务;

上下文切换机制:负责任务间切换上下文,也就是保存当前任务的寄存器状态和恢复下一个任务的寄存器状态。

14. 介绍下FreeRTOS的调试技巧

        在多任务系统中常见的问题有:线程饥饿、抖动、优先级翻转、任务死锁、内存泄漏等,下面简单介绍下:

线程饥饿:当任务的优先级设置的不合理时,可能会导致高优先级的任务使用了太多的CPU时间,使得低优先级的任务没有足够的时间执行,这就是线程饥饿。我们可以将高优先级的任务拆分为多个任务,将关键代码设置为高优先级,将占用CPU时间多的工作交给中或低优先级任务处理。

抖动:周期性执行的任务,随机发生的延迟时间叫做抖动。虽然轻微的抖动很难避免,但是抖动太严重就会导致性能变差,间歇性的数据丢失。例如,每隔5ms调整电机的控制参数,如果控制任务的抖动过大就会使得控制性能就变差。除了线程饥饿会导致抖动之外,RTOS系统配置也会有影响,例如系统节拍定时器节拍频率。理想情况下,两个节拍之间的时间应该比系统中最频繁任务的周期时间短得多。

内存泄漏:通常不建议在嵌入式软件中进行动态内存分配,但有时会出于各种原因需要进行动态内存分配。问题在于,如果使用它,则必须确保一旦内存块不再使用时,释放每个已分配的内存块。如果在某些情况下遗漏了释放操作就会出现内存泄漏,并最终耗尽内存导致严重错误。

任务死锁、优先级翻转:前面介绍了,这里不再赘述。

        下面介绍几个常用的调试手段:使用vTaskList()查看任务状态RTOS跟踪和可视化分析工具,具体可查看这个帖子了解下:https://zhuanlan.zhihu.com/p/405115080。

猜你喜欢

转载自blog.csdn.net/weixin_50591371/article/details/141286653