大疆嵌入式软件一面二面:DMA 缓冲交接与 D-Cache 一致性追问
- 轮次
- 一面+二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一面(约 70 分钟)
- 自我介绍,然后完整讲一遍项目:数据从哪来、经过哪些模块、最后发到哪?
- 这部分是谁做的?你自己写了哪些?
- 为什么选这颗 MCU?主频、RAM、Flash 大概多少,项目用掉多少?
- SPI 读的是什么传感器?速率怎么选,CPOL 和 CPHA 怎么配?
- UART 轮询、中断、DMA 接收有什么差别,你为什么选 DMA?
- DMA 启动时要配置哪些信息?源地址、目的地址、传输长度分别是什么?
- static 修饰局部变量、全局变量和函数有什么区别?
- volatile 解决什么问题,为什么不能拿它代替同步措施?
- 结构体内存对齐怎么算?把收到的字节流直接转成结构体指针,可能有哪些问题?
- 系统分了几个任务?优先级依据是什么,任务一般在什么地方阻塞?
- 中断和任务之间怎么交接事件?哪些工作放中断,哪些留给任务?
- 互斥量和信号量分别用在哪,项目里有没有共享资源?
- 遇到过什么不好复现的问题,怎么确认修好了?
二面(约 80 分钟)
- 循环 DMA 写到缓冲区末尾之后怎么办?软件怎么知道哪些字节还没处理?
- UART 检测到空闲,就一定收到了一整帧协议数据吗?两帧连着发呢?
- 协议解析任务没及时运行,DMA 绕回来覆盖旧数据,能不能发现?
- 两个缓冲区轮换,任务处理时间超过一次轮换周期怎么办?
- 队列里传的是整份数据,还是缓冲区指针?两种方式的代价和风险分别是什么?
- 指针发进队列后,原来的缓冲区什么时候能重新使用?谁负责决定?
- 关 CPU 中断能不能阻止 DMA 继续往内存里写?
- 如果换成带 D-Cache、又没有 DMA 硬件一致性的平台,还要额外考虑什么?
- 中断里通知任务,为什么要用对应的 FromISR 接口?通知失败怎么办?
- 偶发丢帧只在高负载时出现,你会先抓什么证据?
- 日志一开问题就消失,是修好了,还是运行时序被改变了?
《参考解析》
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 翻转配合逻辑分析仪这类非侵入手段。工程结论通常是三件事:把「最坏情况」纳入设计(缓冲区按最坏延迟算大小、给关键数据处理留够优先级与余量),把异常路径做成可观测(覆盖、丢帧、超时都计数上报),以及把「怎么确认修好了」写清——能在压测下长时间跑出零计数缺口,才算真的修好。