CVTE-技术面面经
🕐 2026.04.30 | CVTE 技术面 | 面试时长 38 分钟 | 岗位: 嵌入式系统软件工程师
这场一共面了 38 分钟,前半段主要是理论知识拷打,后半段是 15 分钟允许 AI 辅助的串口接收代码题。
因为当时没做完整记录,下面主要是凭印象回忆整理。
🙋 自我介绍
这部分直接略过。
📚 理论知识
进程和线程的区别
上来就问操作系统,这个确实有点把我打懵了。当时大概只答了:进程有独立内存空间,进程之间不能直接通信、切换开销更大;线程是进程里的执行单元,共享进程空间,线程之间通信更直接。
参考答案
- 进程:系统进行资源分配的最小单位,拥有独立的地址空间
- 线程:CPU 调度的最小单位,是进程中的一个执行流
- 区别:
- 进程有独立的内存空间,线程共享所属进程的内存空间
- 进程切换开销大,线程切换开销小
- 进程间通信需要 IPC,线程间通信更直接
- 一个进程崩溃通常不影响别的进程,一个线程崩溃可能导致整个进程崩溃
系统资源分配的最小单位是什么?
这个我当时是直接猜的进程。
参考答案
是 进程。
- 进程:资源分配的最小单位
- 线程:CPU 调度的最小单位
进程间通信方式
我答了:有名管道、无名管道、共享内存、消息队列、信号量。
参考答案
常见进程间通信方式有:
- 无名管道
- 有名管道(FIFO)
- 共享内存
- 消息队列
- 信号量
- 信号
- Socket
线程同步
不会。
参考答案
线程同步常见方式有:
- 互斥锁
mutex - 信号量
semaphore - 条件变量
condition variable - 读写锁
rwlock - 自旋锁
spinlock - 原子操作
atomic
线程同步的结构
这个也不会。
参考答案
如果这里问的是线程同步常用的结构 / 原语,一般可以答:
- 互斥锁
- 信号量
- 条件变量
- 读写锁
- 自旋锁
- 屏障(barrier)
如果偏工程实现一点,也可以提:
- 环形缓冲区
- 线程安全队列
- 无锁队列
中断怎么进行的
我不会官方术语,就拿单片机定时器中断举例说明了一下。
参考答案
中断的大致流程:
- 外设产生中断请求
- CPU 响应中断
- 保存现场
- 跳转到中断服务函数 ISR
- 执行中断处理逻辑
- 恢复现场
- 返回原程序继续执行
中断的优点
我只答了增强实时性、可以进行优先级划分、能保证数据处理更稳定。
参考答案
中断的优点主要有:
- 提高实时性
- 避免 CPU 轮询浪费资源
- 可以进行优先级管理
- 提高系统响应效率
- 有利于异步事件处理
串口数据格式
忘了。
参考答案
串口一帧数据通常包括:
- 起始位
- 数据位
- 校验位(可选)
- 停止位
常见配置如 8N1:
- 8 位数据位
- 无校验
- 1 位停止位
IIC 数据传送流程
忘了,当时模模糊糊随便说了一点。
参考答案
IIC 通信基本流程:
- 主机发送起始信号
START - 主机发送从机地址 + 读写位
- 从机应答
ACK - 主机发送或接收数据
- 每发送 1 字节都要有应答位
- 通信结束后主机发送停止信号
STOP
IIC 空闲时 SDA 和 SCL 的状态
忘了,随便答的。
参考答案
IIC 空闲时:
- SDA = 高电平
- SCL = 高电平
因为 IIC 一般是开漏输出,靠上拉电阻拉高。
学过哪些排序
我答了冒泡、归并、插入、希尔。
参考答案
常见排序包括:
- 冒泡排序
- 插入排序
- 选择排序
- 希尔排序
- 归并排序
- 快速排序
- 堆排序
冒泡排序介绍
我答的是双重 for 循环依次比较,时间复杂度是 O(n^2)。居然没继续追问后面那些排序。
参考答案
冒泡排序的核心思想:
- 通过相邻元素比较与交换,把较大或较小的元素逐步“冒”到一端
- 需要双重循环
- 时间复杂度:
- 平均:
O(n^2) - 最坏:
O(n^2)
- 平均:
- 空间复杂度:
O(1) - 属于稳定排序
💻 15 分钟代码手搓
题目原文
🎓 嵌入式底层开发实战考题:UART 通信协议解析与防御性编程
考试说明
允许使用 AI 辅助编程 / 搜索资料,但要求你能够透彻解释数据帧在各种异常边缘情况(Corner Cases)下的流转逻辑。
业务背景:你正在开发一个环境采集设备。主控制器通过串口(UART)连接多个温湿度传感器节点。每个传感器节点每隔 10 秒采集一次数据,并通过串口向主控制器发送一帧数据。
协议描述
- 包头(2 Bytes):
0xAA 0x55 - 长度(1 Byte):
N(有效载荷的字节数) - 有效载荷(N Bytes):数据内容
- 校验和(1 Byte):
CS- 计算规则:长度字节与所有有效载荷字节的累加和,取低 8 位
- 即:
(Len + Payload) & 0xFF
示例数据帧:AA 55 05 01 41 01 2C 02 76
其中 05 为长度,01 41 01 2C 02 为载荷,76 为校验和。
第一部分:代码 Review 与极端场景漏洞分析
以下是一位实习生编写的串口字节解析代码。他采用了经典的“单字节状态机(FSM)”模式,在理想情况下处理正常数据没有问题。
但在真实工业现场(存在电磁干扰)测试中,主控经常会“莫名其妙地丢失整段数据”。经过抓包比对,发现了以下几个会导致代码解析彻底崩溃的测试用例。
问题 1:请仔细阅读以下 C 语言代码,指出至少 3 个致命缺陷,并详细解释为什么在遇到【测试用例 A】和【测试用例 B】时,该状态机会导致有效数据丢失。
|
触发 Bug 的测试用例
- 测试用例 A:连续头部干扰
收到的字节流为:AA AA 55 05 01 41 01 2C 02 76 - 测试用例 B:半包与有效包重叠
收到的字节流为:AA 55 05 01(由于干扰中断)
紧接着又发来一个完整包:AA 55 05 01 41 01 2C 02 76
合并后的字节流为:AA 55 05 01 AA 55 05 01 41 01 2C 02 76
第二部分:防粘包与容错重构
问题 2:请彻底重构数据解析逻辑。
要求
- 抛弃存在隐患的单字节 FSM 逻辑,编写一段极其鲁棒的
uart_rx_byte_handler - 必须完美解决粘包、半包问题
- 在【测试用例 B】的情况下,能准确丢弃前面残缺的 4 个字节,完整救出后面隐藏的有效帧
- (加分项)说明你的设计时间复杂度;如果在低端 MCU 上运行,频繁的内存拷贝是否会成为瓶颈;是否有零拷贝(Zero-Copy)优化方案
我的作答思路
这题我直接塞给 Claude,5 分钟左右就给出了解法;同时我自己也在看代码,总共花了不到 10 分钟。整体来说不算难,我自己找到了 4 个缺陷,AI 帮我补到了 5 个,但面试官不给看解析,只能看题目回答并且还会提问没有修复的情况会导致什么问题,就只回答了四个。
🧩 代码缺陷分析
缺陷 1:STATE_HEAD2 没有处理“重叠头部”
原代码:
case STATE_HEAD2: |
如果当前状态已经看到一个 0xAA,下一字节又是 0xAA,这其实非常关键:
- 第二个
0xAA可能就是新包头的第 1 个字节 - 但实习生代码直接
state = STATE_HEAD1 - 更糟的是,这个字节没有被重新消费
所以它把一个潜在的新起点白白丢掉了。
为什么测试用例 A 会丢包?
字节流如下:
AA AA 55 05 01 41 01 2C 02 76 |
逐字节分析:
- 第 1 个
AA:HEAD1 -> HEAD2 - 第 2 个
AA:不是55,进入else,状态回到HEAD1
问题在于:这个 AA 本来应该被当作新帧头重新识别,但代码直接把它丢了。接下来 55 落到 HEAD1 状态,而 HEAD1 只认 AA,所以 55 也被丢弃。
后面的整帧因此彻底失去同步,完整有效包被整体丢弃。这就是典型的头部重叠失配问题。
这个缺陷我讲解的很详细,没有被提问
缺陷 2:校验失败后强制复位,但没有重同步能力
原代码:
if (cs == byte) { |
无论校验成功还是失败,状态机都只做一件事:回到 STATE_HEAD1。
这意味着:
- 当前字节如果本身可能是下一帧起点,不会被利用
- 已缓存的 payload 中如果埋着
AA 55,不会被重新挖出来 - 遇到半包、错长、错校验后,不能从已有数据窗口内部恢复同步
这在抗干扰串口解析里是致命的。工业现场的噪声不是“丢一个整包然后世界恢复干净”,而是错误字节和正确数据混在一起。
这个缺陷也是,讲的很清楚没有被提问
缺陷 3:长度字段完全没有合法性检查
原代码:
expected_len = byte; |
这里至少有两个问题:
-
Len = 0没有特殊处理
按协议,N = 0时应该直接进入STATE_CS等待校验字节,而不是进入STATE_PAYLOAD。
现在的实现会导致本应是 CS 的字节被当作payload[0],后续流全部错位。 -
没有长度上限约束
虽然payload_buf[256]和uint8_t看起来能装下 0~255,但真实协议通常会有业务最大长度,比如 32 / 64。
如果噪声把长度打成 200,状态机就会盲目等待 200 字节,期间即使出现真正的AA 55头部,也无法恢复同步,导致后续多帧连续丢失。
本质上,错误长度会把状态机拖进长时间失步状态。
提问:如果没有检查会发生什么呢?
会出现数组越界问题,因为缓冲数组payload_buf只有256位,如歌长度超过256位就会导致访问错误 |
缺陷 4:STATE_PAYLOAD 期间不检测包头,无法从半包里抢救后续真包
这是【测试用例 B】崩溃的核心原因。
原代码:
payload_buf[payload_idx++] = byte; |
含义就是:只要进入 PAYLOAD,就死等 expected_len 个字节收满。
如果这期间流里出现了:
AA 55 ... |
也只会被当成旧包 payload 的一部分吞掉。
为什么测试用例 B 会丢包?
合并流:
AA 55 05 01 AA 55 05 01 41 01 2C 02 76 |
拆开看:
- 前 4 字节是残包头:
AA 55-> 进入STATE_LEN05->expected_len = 501-> 收到payload[0]
此时理论上旧包还差 4 个 payload + 1 个 CS,但实际上后面已经来了一个完整新包:
AA 55 05 01 41 01 2C 02 76 |
FSM 的处理方式会是:
- 把
AA当payload[1] - 把
55当payload[2] - 把
05当payload[3] - 把
01当payload[4] - 达到
expected_len = 5后切到STATE_CS - 下一字节
41被当成 CS 做校验
计算出来的校验当然不等于 41,于是状态机复位到 HEAD1。
问题是:
- 真包的
AA 55 05 01已经被当成旧包 payload 吞掉了 - 真包的第 5 个字节
41又被当成旧包 CS 吞掉了 - 后面的
01 2C 02 76已经失去头部上下文
结果就是:后面那个完整有效包也一起被毁掉了。
提问:如果没有校验会发生什么呢?
没有校验机制会导致例如测试用例B的半包情况,会将完整包当作前一个半包的数据写入,从而丢失整个数据包 |
🔧 问题 2:重构解析逻辑
题目要求“抛弃存在隐患的单字节 FSM”,最稳妥的方案就是:
流缓冲区 + 滑动窗口找头 + 长度判定 + 完整帧校验 + 消费已确认字节
它的思路不是“按状态吃字节”,而是:
- 每收到一个字节,先放入接收缓冲区
- 在缓冲区里扫描
AA 55 - 找到后先判断后面是否至少有长度字节
- 再判断整帧是否已经收完整
- 完整则做校验
- 校验成功就处理,并从缓冲区移除整帧
- 校验失败则只丢 1 个字节,再继续重新同步
这种方案能比较稳定地应对:
- 粘包
- 半包
- 错位
- 噪声干扰
- 假头部
🧾 我的代码
|
💭 一点感受
理论知识部分被拷打得挺狠,代码题反而还好。靠 AI 加上自己一起看逻辑,做出来还算顺利。
整体观感就是:八股被拷打,代码题还行。
