面灵AI→

嵌入式面试的追问链:从发一个字节到线上电平,中间经过哪些环节

时间
2026-10
来源
牛客网

《面试题目》

  1. 你做过 UART 吗?从发一个字节到线上电平变化,中间经过了哪些环节?
  2. 工程能配起来,但说不清复位之后到 main 之间发生了什么,这块怎么讲?
  3. 用过队列和信号量,但讲不清阻塞、唤醒和超时该怎么选。
  4. 写过 UART/SPI/I2C,一问时序、错误处理和 DMA 就含糊。
  5. 项目里写了「实时」,最坏路径和优先级设计是怎么定的?
  6. 中断上下文为什么不能阻塞?临界区到底应该包住哪几行?
  7. DMA 的缓冲什么时候可以复用?什么时候不能动?
  8. 一个指针为什么不能在函数里返回它指向的局部变量?

《参考解析》

UART 发一个字节的完整链路。 这是最典型的「从接口往下钻」问法,答的时候要按层走,不能只说「写进寄存器就发出去了」。

应用层调用 write 或驱动封装的 uart_send,字节先进入驱动的发送环形缓冲;如果发送器空闲,就把第一个字节写进发送数据寄存器(THR/TDR)。硬件把这个字节搬到移位寄存器,然后按波特率时钟逐位往 TX 引脚上推:先拉低一个位时间作为起始位,接着按低位在前(LSB first)输出 5~9 个数据位,需要的话再加 1 位校验位,最后拉高 1、1.5 或 2 个位时间作为停止位。引脚上的电平再经过外部收发器(TTL 直连、RS-232 电平转换、或 RS-485 差分收发器)才真正变成线上的信号。

中间有两个容易漏的点。一是波特率从哪来:它由外设时钟(PCLK)分频得到,写的是 BRR 之类的分频寄存器,分频误差会累积到采样点偏移上,所以晶振选型和波特率误差要一起算。二是驱动怎么知道可以送下一个字节:THR 空时置 TXE 标志并触发中断,ISR 从环形缓冲取下一个字节写入;最后一个字节移出后置 TC(发送完成),半双工总线(如 RS-485)就是靠 TC 来按时切换收发方向的。如果这里用了 DMA,ISR 就不做搬运,只处理「传输完成」和错误标志。

复位到 main 之间发生了什么。 上电后 CPU 先从向量表的头两个字取初始 MSP 和复位入口地址,跳进 Reset_Handler;这里通常先做最小时钟配置(SystemInit:PLL、Flash 等待周期、总线分频),再把已初始化数据段 .data 从 Flash 拷到 RAM、把 .bss 清零,然后跑 C 运行时的初始化(C++ 还要跑全局对象的构造),最后才调 main。答不出这段,本质是没把「链接脚本 → 段布局 → 启动代码」串起来:.data 的加载地址和执行地址为什么不同、.bss 为什么只占 RAM 不占 Flash,都是同一道题的下半段。

阻塞、唤醒、超时怎么选。 以 FreeRTOS 队列为例:任务调用带超时参数的接收接口,队列空时任务被挂到这个队列的等待列表上、进入阻塞态,让出 CPU;有数据写入时,发送方把等待列表里的任务移到就绪列表(唤醒);如果等满 xTicksToWait 个 tick 还没有数据,内核把它按超时唤醒并返回「空」。所以「阻塞多久」是一个显式的设计参数:等硬件事件可以给较长超时甚至永久等待,等用户输入或做超时保护就必须给有限值,否则一个环节挂死会拖住整条任务链。

选型上分四类:队列传数据(按值拷贝,自带阻塞语义);二值信号量做「事件发生了」的同步(中断通知任务最典型);计数信号量管「有 N 份资源」;互斥量保护共享资源,并且带优先级继承,防止低优先级任务持锁时被中优先级任务抢占、把高优先级任务拖死(优先级翻转)。中断上下文里只能用 FromISR 版本,且不能阻塞——中断没有任务控制块,让它睡眠没有任何机制能把它唤醒回来。

DMA 缓冲什么时候能复用。 判据只有一个:这次传输真的结束了。也就是传输完成标志或完成中断(TC)已经置位,而不是「我把数据填进去、启动 DMA 了」就当作发完了。多缓冲场景用 ping-pong:两块缓冲交替,A 块在传输时 CPU 填 B 块,靠半传输/传输完成中断切换。还有两个常被追问的坑:一是 cache 一致性——带 D-cache 的核(如 Cortex-M7)上,DMA 读的是内存,CPU 读的可能是 cache,发送前要 clean、接收后要 invalidate,或者干脆把这块内存配成非缓存/非共享属性;二是对齐和长度,很多 DMA 要求源地址按字对齐、传输长度是数据宽度的整数倍,否则行为依芯片而异。

「实时」要能给出最坏路径。 面试官问这句不是要你背定义,而是要你说出:哪个任务的最坏响应时间是多少、它被谁阻塞、阻塞多久。可用的框架是最坏响应时间 ≈ 自身执行时间 + 被高优先级任务抢占的时间 + 被低优先级任务持锁阻塞的时间 + 中断关中断的时间。所以设计上要控制三件事:临界区只包真正共享的那几行(不是整个函数)、关中断的时间要能报出上限、栈要留高水位余量。能报数字的候选人,和只会说「我们用了 RTOS 所以是实时的」的候选人,差距就在这。

指针为什么不能返回局部变量。 局部变量在栈上,函数返回时栈帧回收,返回的地址指向一块随时会被别的函数覆写的内存,行为未定义——可能「看起来是对的」,一加打印或换优化等级就崩。正确做法三选一:调用方传入缓冲区(最常用)、返回 static 变量(不可重入,多任务下要加锁)、堆上分配(谁分配谁释放,要写清所有权)。同一条追问链还会往下走到数组传参退化、结构体对齐和 volatile:数组作参数退化成指针所以 sizeof 不对、结构体按最大成员对齐所以跨平台传二进制要显式打包、volatile 只保证每次都真读内存,不保证原子性也不保证顺序。