【秋招】嵌入式面试八股文- FreeRTOS篇
本文为 第三章 RTOS 部分,具体整篇目录可以看前言!
第一部分(纯八股)
- 由于FreeRTOS一些概念比较重要,所以在本篇文章中对FreeRTOS的一些基础核心概念进行了总结
1. RTOS基本概念
1.1 简介
- FreeRTOS是一个开源、轻量级的实时操作系统,适用于微控制器和小型嵌入式系统设计
- 核心代码仅包含四个C文件:list.c、tasks.c、queue.c、continue.c
- 系统主要的配置文件:FreeRTOSConfig.h
- 支持多种架构:ARM Cortex-M、PIC、AVR等
1.2 任务的概念
- 任务是FreeRTOS中的独立执行部分,与Linux中的线程概念类似。多任务和多线程相似。
- 每个任务有自己的栈空间和上下文(不理解上下文概念可以去搜一下)
- 任务通常包含一个无限循环,不应该退出(但是每个任务要有休眠)
- 任务函数原型(简化):
void TaskFunction(void *pvParameters);
2. 任务创建与管理
2.1 任务创建(FreeRTOS源码)
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode, // 任务函数指针 const char * const pcName, // 任务名称 uint16_t usStackDepth, // 栈深度(字) void *pvParameters, // 传递给任务的参数 UBaseType_t uxPriority, // 任务优先级 TaskHandle_t *pxCreatedTask // 任务句柄 );
实际应用案例(以LED灯为例):
void vLED_Change_Task(void *pvParameters) { while(1){ // 改变灯的状态 GPIO_ToggleBits(GPIOC, GPIO_Pin_13); // 延时1000ms vTaskDelay(pdMS_TO_TICKS(1000)); //休眠 } } // 进行任务创建 TaskHandle_t xLEDTaskHandle = NULL; xTaskCreate( vLED_Change_Task, // 任务函数 "LED_Change_Task", // 任务名称 configMINIMAL_STACK_SIZE, // 栈大小 NULL, // 参数 tskIDLE_PRIORITY + 1, // 优先级 &xLEDTaskHandle // 任务句柄 );
2.2 任务状态及转换
(1)FreeRTOS任务状态:
- 运行态(Running):当前正在执行的任务
- 就绪态(Ready):准备好执行但未获得CPU,即准备就绪,等待起跑
- 阻塞态(Blocked):等待事件或超时(调用阻塞API函数)
- 挂起态(Suspended):通过API显式挂起
(2)状态转换图:
2.3 任务优先级管理(参考FreeRTOS源码)
// 设置任务优先级 BaseType_t xTaskPrioritySet( TaskHandle_t xTask, // 任务句柄 UBaseType_t uxNewPriority // 新优先级 ); // 获取任务优先级 UBaseType_t uxTaskPriorityGet(TaskHandle_t xTask);
优先级抢占示例:
void vHighPriorityTask(void *pvParameters) { for(;;) { // 高优先级任务执行 printf("高优先级任务运行\r\n"); vTaskDelay(pdMS_TO_TICKS(1000)); } } void vLowPriorityTask(void *pvParameters) { for(;;) { // 低优先级任务执行 printf("低优先级任务运行\r\n"); // 提高自身优先级 vTaskPrioritySet(NULL, uxTaskPriorityGet(NULL) + 1); printf("低优先级任务提升优先级\r\n"); // 恢复原优先级 vTaskPrioritySet(NULL, uxTaskPriorityGet(NULL) - 1); vTaskDelay(pdMS_TO_TICKS(500)); } }
3. 任务调度与同步
3.1 调度算法
- FreeRTOS默认使用抢占式优先级调度(高优先级任务可以抢占低优先级任务)
- 同优先级任务:使用时间片轮转调度。就是一个时间片执行A任务,这个时间片时间到了,换同优先级的B任务执行。
调度器配置(FreeRTOS源码):
// 启动调度器 vTaskStartScheduler(); // 挂起调度器 vTaskSuspendAll(); // 恢复调度器 xTaskResumeAll();
3.2 任务延时(FreeRTOS源码)
// 绝对延时(阻塞指定时钟节拍数) void vTaskDelay(TickType_t xTicksToDelay); // 相对延时(保证任务间隔固定) void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, // 上次唤醒时间 TickType_t xTimeIncrement // 增量时间 );
精确周期任务实现(参考FreeRTOS源码):
void vPeriodicTask(void *pvParameters) { TickType_t xLastWakeTime; const TickType_t xPeriod = pdMS_TO_TICKS(100); // 100ms周期 // 初始化上次唤醒时间 xLastWakeTime = xTaskGetTickCount(); for(;;) { // 执行周期性任务 DoPeriodicWork(); // 精确延时到下一个周期 vTaskDelayUntil(&xLastWakeTime, xPeriod); } }
3. 3 任务同步与通信
- 信号量(Semaphore):控制资源访问
- 互斥量(Mutex):带优先级继承的信号量
- 消息队列(Queue):任务间数据传递
- 事件组(Event Group):多事件等待
- 任务通知(Task Notification):轻量级通信机制
任务通知示例:
// 发送任务通知 void vSenderTask(void *pvParameters) { TaskHandle_t xReceiverHandle = (TaskHandle_t)pvParameters; while(1) { // 发送通知,值为0x01 xTaskNotify(xReceiverHandle, 0x01, eSetBits); vTaskDelay(pdMS_TO_TICKS(1000)); } } // 接收任务通知 void vReceiverTask(void *pvParameters) { uint32_t ulNotifiedValue; while(1) { // 等待通知,超时时间为portMAX_DELAY(永久等待) if(xTaskNotifyWait(0, 0xFFFFFFFF, &ulNotifiedValue, portMAX_DELAY) == pdTRUE) { if((ulNotifiedValue & 0x01) != 0) { // 处理通知 printf("收到通知: 0x%08lx\r\n", ulNotifiedValue); } } } }
3. 4 空闲任务
- vTaskDelay() 为相对延时函数,可以让任务进入阻塞状态。
- FreeRTOS程序在任意时刻,必须至少有一个任务处于运行状态,为了达到这个要求,FreeRTOS使用了Idle任务:当vTaskStartScheduler调用后,调度器会自动创建Idle任务,这个任务的任务函数就是一个连续性工作的任务,所以他总是可以处于就绪态(在运行态和就绪态之间转换,没有其他状态)。由于Idle任务的优先级是最低的(优先级为0),所以Idle任务不会抢占用户任务的运行。当其他高优先级的任务需要运行时,他们会抢占Idle任务。
- Idle任务主要用于资源回收清理工作,例如当你在程序中删除一个任务后,就需要Idle任务去清理这个任务占用的资源。因此,不要让Idle任务“饿死”,具体而言,不要创建一个优先级比Idle任务优先级高,且连续性工作的任务。如果应用程序也需要一个在背后连续工作的任务,则应该设置其优先级和Idle任务相同。当然这个需求更好的实现方法是通过下面介绍的Idle钩子来完成。
4 内存管理与栈溢出检测
4.1 内存分配方案
- 堆1(heap_1):最简单的分配方案,不支持释放
- 堆2(heap_2):支持释放,但可能产生碎片
- 堆3(heap_3):使用标准库malloc/free
- 堆4(heap_4):支持合并相邻空闲块
- 堆5(heap_5):类似堆4,但支持跨多个内存区域
内存分配函数:
// 分配内存 void *pvPortMalloc(size_t xSize); // 释放内存 void vPortFree(void *pv);
4.2 栈溢出检测
- 栈溢出检测方法: 栈溢出钩子函数栈溢出保护区运行时栈使用量检测
栈溢出钩子函数:
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName ) { // 栈溢出处理 printf("栈溢出: 任务名 = %s\r\n", pcTaskName); // 系统复位或其他处理 NVIC_SystemReset(); }
获取任务栈使用情况:
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);
5 常见面试题与解答
5.1 FreeRTOS中如何创建任务?创建任务时需要注意哪些参数?
回答: 使用xTaskCreate
或xTaskCreateStatic
函数创建任务。关键参数包括:
- 任务函数:必须是无限循环,不应返回
- 栈大小:根据任务复杂度确定,过小会导致栈溢出
- 优先级:影响任务调度顺序,需避免优先级反转
- 任务名称:便于调试,建议使用有意义的名称
5.2 FreeRTOS中任务的生命周期是怎样的?各状态之间如何转换?
回答: FreeRTOS任务有运行态、就绪态、阻塞态、挂起态和删除态。
- 创建后进入就绪态
- 调度器选择最高优先级就绪任务进入运行态
- 运行态任务可通过延时、等待事件等进入阻塞态
- 任务可通过API显式挂起和恢复
- 任务可以被删除,进入删除态等待资源回收
5.3 如何处理FreeRTOS中的任务优先级?如何避免优先级反转?
回答: 任务优先级从0到(configMAX_PRIORITIES-1),数值越大优先级越高。
- 避免优先级反转的方法: 使用互斥量(mutex)代替信号量,互斥量具有优先级继承机制使用优先级上限协议合理设计任务优先级,减少共享资源使用临界区保护短时间的共享资源访问
// 优先级继承示例 SemaphoreHandle_t xMutex; void vHighPriorityTask(void *pvParameters) { for(;;) { // 尝试获取互斥量 if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) { // 访问共享资源 ProcessSharedResource(); // 释放互斥量 xSemaphoreGive(xMutex); } vTaskDelay(pdMS_TO_TICKS(100)); } } void vMediumPriorityTask(void *pvParameters) { for(;;) { // 中优先级任务执行 DoMediumPriorityWork(); vTaskDelay(pdMS_TO_TICKS(100)); } } void vLowPriorityTask(void *pvParameters) { for(;;) { // 获取互斥量 if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) { // 低优先级任务持有互斥量时 // 如果高优先级任务尝试获取互斥量 // 低优先级任务会临时继承高优先级 LongOperation(); // 长时间操作 // 释放互斥量 xSemaphoreGive(xMutex); } vTaskDelay(pdMS_TO_TICKS(100)); } }
5.4 FreeRTOS的调度算法是怎样的?如何实现时间片轮转?
回答: FreeRTOS默认使用抢占式优先级调度,高优先级任务可以抢占低优先级任务。同优先级任务通过时间片轮转方式调度。
时间片轮转通过以下方式实现:
- 配置
configUSE_TIME_SLICING
为1(默认) - 每个系统时钟节拍,调度器检查是否有同优先级的就绪任务
- 如果有,则切换到下一个同优先级任务
- 时间片长度等于系统时钟节拍周期
5.5 如何实现精确的周期性任务?vTaskDelay和vTaskDelayUntil有什么区别?
回答:
vTaskDelay
提供相对延时,任务阻塞指定的时钟节拍数vTaskDelayUntil
提供绝对延时,保证任务以固定周期运行
区别:
vTaskDelay
的实际延时时间会受到其他任务执行时间的影响vTaskDelayUntil
能保证任务的周期性更加精确
实现精确周期任务应使用vTaskDelayUntil
,并考虑任务执行时间。
5.6 FreeRTOS中任务间通信有哪些机制?各有什么特点?
回答: FreeRTOS提供多种任务间通信机制:
- 队列(Queue):支持多个数据项的FIFO传输可用于任务间和中断与任务间通信支持阻塞等待和超时机制
- 信号量(Semaphore):二值信号量:用于同步计数信号量:用于资源管理
- 互斥量(Mutex):带优先级继承的特殊信号量用于解决优先级反转问题
- 事件组(Event Group):支持多事件等待和同步可实现"与"和"或"逻辑等待
- 任务通知(Task Notification):轻量级通信机制,性能最高每个任务有一个32位通知值可替代二值信号量、计数信号量和事件组
- 选择合适的通信机制应考虑性能需求、复杂度和功能需求。
5.7 什么是任务通知?与传统IPC机制相比有什么优势?
回答: 任务通知是FreeRTOS中的轻量级任务间通信机制,每个任务有一个32位通知值和状态。
优势:
- 更快的执行速度(比信号量快约45%)
- 更小的RAM占用
- 无需创建单独的IPC对象
- 灵活的通知值更新方式(覆盖、按位设置、递增等)
限制:
- 每个任务只有一个通知值
- 只能有一个任务等待特定任务的通知
// 任务通知与二值信号量对比 // 使用任务通知 void vTask1(void *pvParameters) { for(;;) { // 等待通知 ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 执行操作 ProcessData(); } } void vTask2(void *pvParameters) { for(;;) { // 准备数据 PrepareData(); // 发送通知 vTaskNotifyGive(xTask1Handle); vTaskDelay(pdMS_TO_TICKS(100)); } } // 使用二值信号量 SemaphoreHandle_t xSemaphore; void vTask1_Sem(void *pvParameters) { for(;;) { // 等待信号量 xSemaphoreTake(xSemaphore, portMAX_DELAY); // 执行操作 ProcessData(); } } void vTask2_Sem(void *pvParameters) { for(;;) { // 准备数据 PrepareData(); // 释放信号量 xSemaphoreGive(xSemaphore); vTaskDelay(pdMS_TO_TICKS(100)); } }
5.8 FreeRTOS提供了哪些内存分配方案?如何选择合适的方案?
回答: FreeRTOS提供5种内存分配方案:
- heap_1:最简单的分配器,不支持释放适用于创建任务后不再动态分配内存的系统
- heap_2:支持释放,使用最佳匹配算法不合并相邻空闲块,可能产生碎片适用于频繁分配释放但大小固定的场景
- heap_3:封装标准库的malloc/free支持所有动态内存操作需要线程安全的标准库支持
- heap_4:支持合并相邻空闲块,减少碎片使用首次适应算法适用于大多数通用场景
- heap_5:类似heap_4,但支持跨多个不连续内存区域适用于内存分散的系统
选择因素:
- 系统复杂度和内存使用模式
- 碎片化敏感度
- 性能需求
- 内存布局
5.9 如何检测和处理FreeRTOS中的栈溢出?
回答: FreeRTOS提供多种栈溢出检测机制:
- 栈溢出钩子函数:配置configCHECK_FOR_STACK_OVERFLOW为1或2实现vApplicationStackOverflowHook函数处理溢出
- 栈使用量监控:使用uxTaskGetStackHighWaterMark获取任务栈使用情况定期检查栈使用量,预警接近溢出的情况
- 栈溢出保护方法:方法1(configCHECK_FOR_STACK_OVERFLOW=1):检查栈指针是否超出栈边界方法2(configCHECK_FOR_STACK_OVERFLOW=2):检查栈边界的模式是否被破坏
处理栈溢出的策略:
- 增加任务栈大小
- 优化任务代码,减少局部变量和递归
- 记录溢出信息并安全重启系统
5.10 如何优化FreeRTOS应用的内存使用?
回答: 优化FreeRTOS内存使用的策略:
- 任务栈优化:合理设置任务栈大小,使用uxTaskGetStackHighWaterMark监控减少任务中的局部变量,特别是大数组避免或限制递归调用深度
- 内存分配优化:尽量在启动时静态分配资源,减少运行时动态分配使用xTaskCreateStatic代替xTaskCreate使用内存池管理小块内存分配
- 数据结构优化:使用合适的数据类型(如uint8_t代替int)合理使用结构体对齐和打包考虑使用位域节省内存
- 配置优化:调整FreeRTOS配置参数,如configMINIMAL_STACK_SIZE禁用不需要的功能,如统计功能、跟踪功能等
// 静态任务创建示例 // 声明任务栈和控制块 static StaticTask_t xTaskBuffer; static StackType_t xStack[configMINIMAL_STACK_SIZE]; // 创建静态任务 TaskHandle_t xHandle = xTaskCreateStatic( vTaskFunction, "StaticTask", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, xStack, &xTaskBuffer );
5.11 如何处理FreeRTOS中的中断和临界区?
回答: FreeRTOS中处理中断和临界区的机制:
- 中断处理:中断服务例程(ISR)应尽量简短使用"FromISR"后缀的API函数在ISR中操作FreeRTOS对象通过返回值pxHigherPriorityTaskWoken判断是否需要任务切换
- 临界区保护:taskENTER_CRITICAL()和taskEXIT_CRITICAL():禁用所有中断taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR():在ISR中使用vTaskSuspendAll()和xTaskResumeAll():挂起调度器,不禁用中断
// 在任务中使用临界区 void vTask(void *pvParameters) { for(;;) { // 进入临界区 taskENTER_CRITICAL(); // 访问共享资源 UpdateSharedResource(); // 退出临界区 taskEXIT_CRITICAL(); vTaskDelay(pdMS_TO_TICKS(100)); } } // 在ISR中使用FromISR函数 void EXTI0_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 清除中断标志 EXTI_ClearITPendingBit(EXTI_Line0); // 发送数据到队列 uint32_t ulValue = GetSensorValue(); xQueueSendFromISR(xQueue, &ulValue, &xHigherPriorityTaskWoken); // 如果需要任务切换,触发PendSV中断 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }
5.12 如何调试FreeRTOS应用?有哪些常用的调试技巧?
回答: FreeRTOS应用调试技巧:
- 使用调试工具:支持RTOS感知的调试器(如IAR、Keil、SEGGER J-Link)FreeRTOS+Trace等跟踪工具逻辑分析仪捕获任务切换和事件
- 日志和断言:实现日志系统记录关键事件使用configASSERT()检查关键条件
- 运行时统计信息:启用configGENERATE_RUN_TIME_STATS使用vTaskGetRunTimeStats()获取CPU使用率使用vTaskList()获取任务状态信息
- 常见问题排查:栈溢出:检查栈高水位线优先级问题:检查任务优先级设置死锁:检查资源获取顺序内存泄漏:监控堆使用情况
// 运行时统计示例 void vDebugTask(void *pvParameters) { char cBuffer[512]; for(;;) { // 获取任务列表 vTaskList(cBuffer); printf("任务状态:\r\n%s\r\n", cBuffer); // 获取CPU使用率 vTaskGetRunTimeStats(cBuffer); printf("CPU使用率:\r\n%s\r\n", cBuffer); vTaskDelay(pdMS_TO_TICKS(5000)); } }
5.13 FreeRTOS的移植与中断管理
- 修改sys.h文件,让它支持OS。
- 修改usart文件,更改中断。在uC/OS的时候,进入和退出中断需要添加OSIntEnter()和OSIntExit()两个函数,然后在FreeRTOS中并没有该机制,所以将这里的代码删除。
- 关于delay函数的修改,FreeRTOS中使用SysTick作为作为操作系统的心跳,所以需要将xPortSysTickHandler()添加,作为系统始终中断。
- delay_init() 用于初始化SysTick,主要修改SysTick的重装载值,修改delay_ms和delay_us函数。
- 修改中断(SysTick中断、SVC中断、PendSV中断)。其中SysTick中断在delay.c文件中已经定义。
6 实战技巧
6.1 任务设计最佳实践
- 任务划分原则:按功能模块划分任务分离时间关键型任务和非关键型任务考虑任务间的数据依赖关系
- 优先级分配策略:实时性要求高的任务优先级高预留优先级,便于后续扩展避免过多任务使用相同优先级
- 栈大小确定方法:从小开始,逐步增加使用uxTaskGetStackHighWaterMark监控考虑任务调用深度和局部变量
6.2 常见陷阱与解决方案
- 优先级反转:使用互斥量代替信号量合理设计任务优先级减少共享资源
- 死锁:按固定顺序获取资源使用超时机制避免在持有资源时等待其他资源
- 栈溢出:增加栈大小减少局部变量避免递归或限制递归深度
- 内存碎片:使用heap_4或heap_5内存分配器减少频繁的小内存分配/释放使用内存池管理小块内存
6.3 性能优化技巧
- 减少上下文切换:合理设置任务优先级使用事件组等待多个事件使用任务通知代替轻量级IPC
- 中断处理优化:保持ISR简短使用延迟处理机制合理设置中断优先级
- 内存访问优化:考虑数据对齐减少动态内存分配使用DMA减少CPU负担
【秋招】嵌入式八股文最全总结 文章被收录于专栏
双非本,211硕。本硕均为机械工程,自学嵌入式,在校招过程中拿到小米、格力、美的、比亚迪、海信、海康、大华、江波龙等offer。八股文本质是需要大家理解,因此里面的内容一定要详细、深刻!这个专栏是我个人的学习笔记总结,是对很多面试问题进行的知识点分析,专栏保证高质量,让大家可以高效率理解与吸收里面的知识点!掌握这里面的知识,面试绝对无障碍!