面灵AI→

大疆嵌入式软件一面二面:DMA 缓冲交接与 D-Cache 一致性追问

轮次
一面+二面
时间
2026-10
来源
牛客网

《面试题目》

一面(约 70 分钟)

  1. 自我介绍,然后完整讲一遍项目:数据从哪来、经过哪些模块、最后发到哪?
  2. 这部分是谁做的?你自己写了哪些?
  3. 为什么选这颗 MCU?主频、RAM、Flash 大概多少,项目用掉多少?
  4. SPI 读的是什么传感器?速率怎么选,CPOL 和 CPHA 怎么配?
  5. UART 轮询、中断、DMA 接收有什么差别,你为什么选 DMA?
  6. DMA 启动时要配置哪些信息?源地址、目的地址、传输长度分别是什么?
  7. static 修饰局部变量、全局变量和函数有什么区别?
  8. volatile 解决什么问题,为什么不能拿它代替同步措施?
  9. 结构体内存对齐怎么算?把收到的字节流直接转成结构体指针,可能有哪些问题?
  10. 系统分了几个任务?优先级依据是什么,任务一般在什么地方阻塞?
  11. 中断和任务之间怎么交接事件?哪些工作放中断,哪些留给任务?
  12. 互斥量和信号量分别用在哪,项目里有没有共享资源?
  13. 遇到过什么不好复现的问题,怎么确认修好了?

二面(约 80 分钟)

  1. 循环 DMA 写到缓冲区末尾之后怎么办?软件怎么知道哪些字节还没处理?
  2. UART 检测到空闲,就一定收到了一整帧协议数据吗?两帧连着发呢?
  3. 协议解析任务没及时运行,DMA 绕回来覆盖旧数据,能不能发现?
  4. 两个缓冲区轮换,任务处理时间超过一次轮换周期怎么办?
  5. 队列里传的是整份数据,还是缓冲区指针?两种方式的代价和风险分别是什么?
  6. 指针发进队列后,原来的缓冲区什么时候能重新使用?谁负责决定?
  7. 关 CPU 中断能不能阻止 DMA 继续往内存里写?
  8. 如果换成带 D-Cache、又没有 DMA 硬件一致性的平台,还要额外考虑什么?
  9. 中断里通知任务,为什么要用对应的 FromISR 接口?通知失败怎么办?
  10. 偶发丢帧只在高负载时出现,你会先抓什么证据?
  11. 日志一开问题就消失,是修好了,还是运行时序被改变了?

《参考解析》

volatile 解决的是「编译器不许优化掉这次访问」,不是并发安全。 它告诉编译器这个对象可能被程序之外的东西改变(寄存器、中断、DMA 目标内存),所以每次都要真实读内存、写入必须真的落回内存,不能缓存进寄存器、不能删除「看似无用」的重复读。它做不到的是原子性与顺序保证:volatile int flag++ 仍是读改写三步,随时可能被中断打断;两个 volatile 变量之间的先后顺序在多核或带弱内存序的架构上也可能被重排、被写缓冲推迟,所以标志位加数据传递这种模式必须配内存屏障(__DMB() 之类)或者直接进临界区。正确的分工是:单个中断与主循环共享的标量标志用 volatile 是必要的,但共享的结构化数据(环形缓冲的读写指针、协议解析状态机)要用临界区、关中断或环形缓冲的无锁单生产者单消费者写法来保证一致性。

DMA 环形缓冲的交接,答的是「读写指针 + 半满全满中断 + 绕回检测」。 循环模式下 DMA 的计数会回到 0 继续写,软件必须自己维护读指针:拿 NDTR(剩余传输数)反算出 DMA 当前写到的位置,读指针到写指针之间的就是待处理数据;跨越缓冲区末尾时要做回绕处理,分成「到末尾」和「从头开始」两段。常用做法是打开半传输与传输完成两个中断,把缓冲区切成两半来交接,或者配合双缓冲模式(DMA 写 A 面时任务处理 B 面)。关于绕回覆盖:如果任务处理慢了、DMA 已经追上读指针,就发生了数据覆盖——硬件不会报错,只能靠软件检测,办法是每次转发前比较写入量与读进度(维护一个累计写计数,与上次处理的计数相减超过缓冲区容量就说明丢过数据),并把这个计数器上报成统计指标。作者这道题暴露的正是「只验证过顺利配合的时候」:超期运行、覆盖、丢帧这些异常路径才是面试官真正在问的东西。

队列传指针的三个必答问题:谁拥有、何时复用、失败怎么办。 传整份数据(值拷贝)简单安全,代价是拷贝耗时与队列内存开销,大帧或高频场景不划算;传指针省拷贝,但把生命周期的责任推给了软件:必须明确约定缓冲区的所有权转移规则——常见做法是生产者把指针放进队列即视为「交出所有权」,消费者处理完再通过一个空闲队列把缓冲区还给生产者,绝不允许两边同时写。缓冲区能否复用不能靠「大概处理完了」,要有确定的信号:请求-应答式的归还队列,或者引用计数。更深一层的坑是 Cache(见下一条)和回绕覆盖混在一起:任务处理慢时,生产端必须先判断「这个缓冲区还在消费者手里」就丢弃新数据并计数,而不是覆盖。答题时把「传值 vs 传指针」的代价列清楚,再主动说出「所有权 + 归还机制 + 溢出计数」三件套,基本就是这题的满分结构。

关中断拦不住 DMA,D-Cache 平台还要额外做一致性维护。 __disable_irq() 只屏蔽 CPU 对中断的响应,DMA 是独立的总线主设备,它照样按自己的节奏往内存里写,所以关中断既不能暂停传输也不能防止覆盖,只能保护 CPU 侧的临界区(比如读改写指针)。换成带 D-Cache 又没有硬件一致性的平台,问题变成两份副本:DMA 写的是内存,CPU 读的是 Cache,可能读到旧数据;CPU 写的数据还在 Cache 里没落内存,DMA 读到的就是旧值。处理要按传输方向决定:DMA 要写内存(外设到内存)时,缓冲区要在传输前对 Cache 做失效(invalidate)或者干脆放在非缓存区/按 cache line 对齐并只失效涉及的行,注意别忘了传输前先把 CPU 可能弄脏的同一行清理干净,否则失效会把 CPU 未写回的数据丢掉;DMA 从内存读(内存到外设)时,要在启动传输前把缓冲区的脏行清理(clean)出去。工程上更稳妥的做法是把 DMA 缓冲区放进 MPU 配置的非缓存区域,或用 SCB_CleanDCache_by_Addr / SCB_InvalidateDCache_by_Addr 这类按行操作的接口,绝不写一句「刷一下缓存」了事,而且 volatile 完全不能替代这套动作。

FromISR 接口与通知失败:本质是「中断上下文不能阻塞」。 中断里不能调用可能阻塞的 API——不能等信号量、不能进带超时的队列操作、也不能做可能导致任务切换的普通调用,所以 FreeRTOS 提供一整套 ...FromISR 变体:它们不阻塞、返回是否需要切换任务,由 xHigherPriorityTaskWoken 带出来,最后用 portYIELD_FROM_ISR() 在中断退出时立刻切过去。通知失败要分清两类:队列或通知满(errQUEUE_FULL)说明消费端跟不上,正确的处理是计数并丢弃或者覆盖旧值——在中断里绝不能等;pdFALSE 的 woken 标志没处理好则表现为「中断来了但任务没及时跑」,属于延迟问题而不是丢数据。要补的坑是优先级:中断优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 时不能调用任何 FreeRTOS API,否则会踩坏内核状态,这类问题往往只在高负载或特定外设中断下偶发。

偶发问题取证:把「抓证据」和「改时序」分开。 高负载才复现的丢帧,第一步是让现象可量化而不是先猜原因:在中断与任务两侧分别记录时间戳、序列号和处理计数(用内存里的环形日志或全局计数器,不走串口打印),复现后比对「收到多少帧、移交多少帧、处理多少帧」,缺口出现在哪一段就锁定在哪一级;同时叠加观测点——DMA 的 NDTR、缓冲区的读写下标、任务的最坏执行时间、CPU 负载与中断频率,看是生产者变快还是消费者变慢。第二层是关键判断:日志一开问题就消失,多半不是修好了,而是打印改变了运行时序(串口输出慢、把中断拉长、或者让编译器不再优化掉某个变量),此时要知道「加了观测就不再复现」等于证据被破坏,应该改用内存缓冲或 GPIO 翻转配合逻辑分析仪这类非侵入手段。工程结论通常是三件事:把「最坏情况」纳入设计(缓冲区按最坏延迟算大小、给关键数据处理留够优先级与余量),把异常路径做成可观测(覆盖、丢帧、超时都计数上报),以及把「怎么确认修好了」写清——能在压测下长时间跑出零计数缺口,才算真的修好。