荣耀嵌入式软件二面 面经

二面和一面相比完全不是一个量级,一面还算是考察基础知识,二面更像是在和你讨论技术方案,每个问题都有大量追问,而且他会故意提出反驳意见看你怎么应对。项目聊了将近四十分钟,他把我项目里的每个技术决策都问了一遍为什么,有几次我答不上来被他直接指出设计上的问题。

1. 讲一下 FreeRTOS 的任务状态机,每种状态之间的转换条件是什么,阻塞状态和挂起状态有什么本质区别?

答:FreeRTOS 的任务有五种状态:运行、就绪、阻塞、挂起、删除。

运行状态是任务正在占用 CPU 执行,单核系统里同一时刻只有一个任务处于运行状态。

就绪状态是任务具备运行条件,等待调度器分配 CPU。所有就绪任务按优先级排在就绪列表里,调度器每次选优先级最高的就绪任务运行。

阻塞状态是任务在等待某个事件发生,比如等待信号量、等待队列有数据、等待延时到期。处于阻塞状态的任务不占用 CPU,调度器不会选它运行。阻塞有超时机制,等待时可以指定最长等待时间,超时后任务自动回到就绪状态。

挂起状态是通过 vTaskSuspend 显式挂起的任务,和阻塞状态的本质区别是:阻塞状态有明确的唤醒条件,事件发生或超时后会自动恢复;挂起状态没有唤醒条件,必须由其他任务或中断调用 vTaskResume 才能恢复,不会自动唤醒。挂起状态通常用于调试或者需要暂停某个任务的场景。

删除状态是任务调用 vTaskDelete 后进入的状态,任务的栈和 TCB 等待空闲任务回收,空闲任务会定期清理已删除任务的资源。

状态转换:就绪→运行是调度器选中;运行→就绪是被高优先级任务抢占或时间片用完;运行→阻塞是等待事件;阻塞→就绪是事件发生或超时;运行/就绪→挂起是被显式挂起;挂起→就绪是被显式恢复。

2. 互斥锁的优先级继承是怎么实现的,有没有它解决不了的场景?

答:优先级继承的实现原理:当高优先级任务尝试获取一个已被低优先级任务持有的互斥锁时,FreeRTOS 会临时把低优先级任务的优先级提升到和高优先级任务一样,让它尽快执行完临界区并释放锁。低优先级任务释放锁之后,优先级恢复到原来的值。这个过程由互斥锁的获取和释放函数内部自动处理,不需要用户干预。

在 FreeRTOS 的实现里,互斥锁的 TCB 里记录了持有者,获取失败时检查持有者的优先级,如果低于当前任务就提升它的优先级,同时把当前任务加入等待列表。释放时恢复持有者的原始优先级,唤醒等待列表里优先级最高的任务。

优先级继承解决不了的场景:

第一是链式优先级反转。任务 A 持有锁1,任务 B 持有锁2 等待锁1,任务 C 等待锁2。优先级继承只能处理直接的持有者,链式场景需要递归地传播优先级提升,FreeRTOS 的实现不支持递归传播,这种场景下优先级反转仍然存在。

第二是多锁场景。一个任务同时持有多个锁,另一个高优先级任务等待其中一个,优先级继承会提升持有者的优先级,但如果持有者还在等待另一个锁,情况会变得复杂,可能出现死锁。

第三是优先级继承本身不能防止死锁,只能缓解优先级反转。如果两个任务互相等待对方持有的锁,优先级继承无法解决,还是会死锁。

3. 你了解 tickless idle 模式吗,它是怎么实现低功耗的,有什么代价?

答:正常情况下 FreeRTOS 的 SysTick 每个 tick 都会产生中断,即使系统里所有任务都在阻塞等待,CPU 也会被 SysTick 定期唤醒,无法进入深度睡眠。

tickless idle 模式的思路是:当所有任务都处于阻塞状态时,调度器计算出最近一个任务会在多久后唤醒,然后关闭 SysTick,让 CPU 进入低功耗睡眠模式,同时用一个低功耗定时器(比如 RTC 或者 LPTIM)在那个时间点唤醒 CPU。CPU 醒来后根据实际睡眠时长补偿系统 tick 计数,然后恢复 SysTick,继续正常调度。

实现上,FreeRTOS 提供了 vPortSuppressTicksAndSleep 这个弱函数接口,用户根据具体芯片实现低功耗进入和退出的逻辑,FreeRTOS 在空闲任务里调用它。

代价有几个方面。首先是实现复杂,需要根据具体芯片配置低功耗定时器,处理各种唤醒源,不同芯片差异很大。其次是 tick 补偿有误差,睡眠期间如果有外部中断提前唤醒,补偿的 tick 数可能不精确,对时间精度要求高的场景需要额外处理。另外进入和退出低功耗模式本身有时间开销,如果任务频繁切换,这个开销可能抵消省电效果,tickless 更适合任务间隔较长的场景。

4. 讲一下 ARM Cortex-M 的异常处理机制,HardFault 发生时 CPU 做了什么,怎么从 HardFault 里获取有用的调试信息?

答:异常处理机制:Cortex-M 有一套固定的异常处理流程。异常发生时,如果当前优先级允许响应,CPU 完成当前指令后进入异常入栈流程,自动把 xPSR、PC、LR、R12、R0-R3 这八个寄存器压入当前使用的栈(MSP 或 PSP,取决于当前模式),然后从向量表取出对应的处理函数地址,跳转执行。异常处理函数返回时,CPU 从栈里弹出这八个寄存器,恢复到被打断的状态继续执行。

HardFault 是一个兜底异常,当其他异常(MemManage、BusFault、UsageFault)没有使能或者在处理过程中又发生了错误,都会升级为 HardFault。常见触发原因有访问非法地址、执行非法指令、除零、未对齐访问(在某些配置下)。

从 HardFault 获取调试信息的方法:

HardFault 发生时,CPU 已经把出错时的寄存器压栈了,关键是找到这个栈帧。在 HardFault_Handler 里,通过读取 LR 寄存器的值判断出错时用的是 MSP 还是 PSP,然后读取对应的栈指针,从栈帧里取出 PC 值,这个 PC 就是出错时正在执行的指令地址。

拿到 PC 之后,在 map 文件里查找这个地址对应的函数和行号,就能定位到出错位置。

另外 SCB 里有几个状态寄存器提供更多信息:CFSR(Configurable Fault Status Register)记录了具体的错误类型,BFAR 记录了导致 BusFault 的访问地址,MMFAR 记录了导致 MemManage fault 的地址。把这些信息打印出来对定位问题很有帮助。

如果有调试器,直接在 HardFault_Handler 里打断点,查看 Call Stack 窗口,IDE 通常能自动解析栈帧显示出错位置。

5. 讲一下 Flash 的写入原理,为什么 Flash 只能写 0 不能写 1,擦除和写入的关系是什么?

答:Flash 的存储单元是浮栅晶体管,浮栅是一个被绝缘层包围的导体,通过控制浮栅上的电

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

嵌入式面试八股文全集 文章被收录于专栏

这是一个全面的嵌入式面试专栏。主要内容将包括:操作系统(进程管理、内存管理、文件系统等)、嵌入式系统(启动流程、驱动开发、中断管理等)、网络通信(TCP/IP协议栈、Socket编程等)、开发工具(交叉编译、调试工具等)以及实际项目经验分享。专栏将采用理论结合实践的方式,每个知识点都会附带相关的面试真题和答案解析。

全部评论

相关推荐

03-24 17:57
门头沟学院 Java
yakuso:你这头像哈哈哈
点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务