项目是自己敲的,面试却像抄的:嵌入式项目追问怎么答
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
原帖是一篇项目复盘,不是按轮次记的题单,正文里明确出现的面试问题只有下面三处,其余内容是作者对「跟着教程做完了、却说不清为什么」的剖析:
- 这个数据为什么放在中断里处理?
- 接收缓冲区为什么定这么大?
- 如果上位机连续发两条指令,你怎么把它们分开?
《参考解析》
中断里做多少事,取决于什么必须即时:中断服务程序的原则是「短」——只做必须在中断上下文完成的事,比如读走硬件寄存器避免下次数据覆盖、清标志位、把字节塞进缓冲区、置一个事件让任务去处理;解析指令、计算、写外部存储、打印日志这类耗时操作都应该搬到主循环或任务里。原因是中断执行期间同级与低优先级中断被屏蔽,占得越久越容易丢中断、丢数据,系统的时间行为也会变得不可预测。追问「为什么这个数据放中断里处理」时,面试官想听的是你能不能区分「必须即时」和「可以延后」:数据寄存器的内容可能下一个字节就被覆盖,属于必须即时;而把这些字节拼成一条完整指令,属于可以延后。配套设施也要一起说:中断与主循环共享的变量要加 volatile,多字节变量的读写要用临界区或原子访问保护;中断里不能动态分配内存、不能做阻塞等待、也尽量不打印。常见的追问是「数据来得比处理快怎么办」——答案是靠环形缓冲加溢出策略(丢弃并计数、或者覆盖旧数据),而不是让中断等着。
接收缓冲区的大小是算出来的,不是拍出来的:容量由最坏情况决定:单帧最长多少字节、上位机可能连续发几条、主循环最久多久才回来取一次数据、接口速率是多少。粗糙的下界可以先估:至少覆盖「最大帧长 × 可能连发的条数」,也要覆盖「按接口速率在最长处理间隔内能到达的字节数」,两者取大。取 2 的幂有个实际好处——环形缓冲的索引可以用位掩码取模,省掉除法。缓冲区定完之后必须定义满了怎么办:是拒绝新数据还是覆盖最旧的,有没有溢出计数和告警,溢出时的数据是丢掉整帧还是丢弃部分字节。能把「为什么是这个值、满了会怎样、怎么知道溢出了」讲清楚,这道题就从背参数变成了设计题。反过来说,很多人写 buf[256] 只是因为例程里写的是 256,一旦被追问最快和最慢情况就答不上来了。
一条指令的边界,靠协议设计加状态机认出来:串口是字节流,本身不提供消息边界,一次中断收到的可能是半条指令,也可能是两条连在一起。三种常见做法:固定长度最简单但不灵活;分隔符(例如换行)实现简单,但正文里出现分隔符时需要转义,并且要有超时兜底;帧头加长度字段加校验(CRC)最可靠,代价是每帧多几个字节。推荐按状态机实现:等待帧头、读取长度、收取载荷、校验、交付,任何一步超时或校验失败就回到等待帧头重新同步——关键是失败时不要死等,否则一次干扰就会把整个接收卡住。缓冲区要能容纳「半条包」跨多次中断慢慢拼起来,所以环形缓冲加状态机是这类场景的标配。追问方向通常有三条:载荷里恰好出现帧头字节怎么办(按长度字段收,不要去搜索帧头);校验失败是丢包还是重传(由上层可靠性需求决定,重传要有次数上限);连续两条指令间隔很短时怎么保证不丢(中断只入队、处理在任务里,队列深度够)。
从「跟着教程做完了」到「讲得清为什么」,有三条可操作的路:一是合上教程把自己讲一遍——从功能入口讲到输出,哪里只能用「反正这样能跑」解释,哪里就是没弄明白的地方,回去查清楚再讲第二遍。二是换一条线索讲项目:别再按代码顺序「先初始化时钟、再初始化串口、最后进主循环」,改成顺着一条数据讲——数据从哪个外设进来、先放在哪里、谁判断它是否完整、处理完交给谁、中间来不及处理怎么办;这么讲一遍,很多原先没注意的地方(比如「每次测试都按同样顺序点按钮、发的都是同样长度的数据」)会自己浮出来。三是给自己加一点需求,加到需要动脑的程度:把采集频率提高而刷新频率不变、把定长指令改成变长参数、把传感器中途拔掉再插上要求程序能恢复。改的过程中会发现原先挤在一起的代码必须拆开、没检查的返回值要补上、原来「一直等」的地方要加超时,这些具体的取舍就是面试时最有分量的素材。
调过的 bug 别只记住结论,要记住怎么查的:面试前准备「项目里最难的问题」时,很多人想不出什么大问题,只能说环境配置麻烦;但真正折腾过的细节其实不少——串口偶尔丢数据、按键按一次触发好几次、模块单独跑正常合起来就不对。当时为了让它赶紧好,换根线、重新初始化、加个延时,问题消失了就接着往下做,至于哪一步起了作用、为什么有效,往往没再确认。到了面试里就只剩「我调了很久,最后解决了」,再被问怎么定位就说不下去。可用的做法是遇到问题随手记四件事:什么现象、什么条件下出现、先怀疑哪里、做了什么检查、最后改了哪里;如果只是暂时绕过去了,也把「还没查明原因」写清楚。讲的时候按「现象—怀疑—验证—结论」展开,并主动交代哪些怀疑被排除了、依据是什么。能讲出「原来这样写会卡在哪里,所以后来改成了这样」,比强调自己敲了多少行代码、熬了多少夜有说服力得多。
项目普通,就更要把「自己的那部分」切开:觉得项目太简单、想去换个看起来更大的项目,是很常见的念头,但如果连现有项目里哪些是库函数完成的、哪些是模块自带的、哪些是自己写的逻辑都分不清,换一个复杂项目通常只是多了一批解释不清的代码。更稳的做法是在介绍时就划清边界:底层驱动沿用了什么、自己改了哪几处、自己新增了哪些处理(比如指令解析、数据缓存、异常提示),并说清这几块的输入输出。这样讲未必显得厉害,但后续追问有了明确范围,不会出现「整个系统都是我做的」之后连一个关键接口怎么用都答不上来的情况。有限的时间里,把手里做过的东西讲明白,比急着再堆一个新名字踏实。