嵌入式面试中DMA、看门狗、低功耗是必考三大件。本文从原理到代码到面试话术一网打尽,附追问预判和回答模板。
面试官:"你用过DMA吗?说说它和中断传输有什么区别?"
你:"呃...DMA就是...不用CPU?"
面试官:(微笑,低头写了个C)
别笑,这是真实发生过的。 DMA、看门狗、低功耗这三样东西,面试问到的概率极高------因为它们代表了嵌入式开发的三个核心能力:效率、可靠性、功耗管理。
这篇文章帮你把这三个知识点讲透,附带面试追问预判和回答模板。
💬 你面试被问过这三个问题吗?评论区说说你被问懵的瞬间!
1. DMA(Direct Memory Access)
问题:什么是DMA?和中断传输有什么区别?
回答
DMA是直接内存访问 ,它允许外设和内存之间直接搬运数据,不需要CPU参与每一个字节的传输。
对比三种数据传输方式:
┌─────────────────────────────────────────────────────────┐
│ 方式1:轮询(Polling) │
│ CPU一直在等,干不了别的事 │
│ while(!flag); ← CPU在这空转 │
│ 适合:数据量小、对实时性要求不高 │
├─────────────────────────────────────────────────────────┤
│ 方式2:中断(Interrupt) │
│ 每传完一个字节/一组数据,中断通知CPU │
│ CPU被打断 → 保存现场 → 处理 → 恢复现场 │
│ 适合:数据量中等、需要CPU及时响应 │
├─────────────────────────────────────────────────────────┤
│ 方式3:DMA │
│ DMA控制器直接搬运,CPU完全不参与 │
│ 搬完了才通知CPU(一次中断) │
│ 适合:大数据量、高速传输(ADC连续采集、串口大批量、SPI刷屏) │
└─────────────────────────────────────────────────────────┘
一句话总结:轮询是CPU一直盯着,中断是搬一个叫一次CPU,DMA是全搬完了才叫CPU。
面试官可能的追问
Q:DMA传输的具体流程是什么?
- CPU配置DMA源地址、目标地址、传输长度、传输方向
- CPU启动DMA传输,然后去干别的事
- DMA控制器逐字节/半字/字搬运数据
- 传输完成后,DMA产生一个中断通知CPU
- CPU在中断回调中处理数据
Q:DMA有什么限制?
- DMA只能在内存和外设之间 、或内存和内存之间搬运数据,不能做计算
- DMA需要占用总线,频繁使用会和CPU争抢总线带宽
- 源地址和目标地址必须在同一地址空间内
Q:你在项目中怎么用DMA的?
"我在做ADC连续采集时用了DMA。ADC每采完一轮(比如8个通道),DMA自动把结果搬到内存数组,不用CPU逐个读取。配合定时器触发ADC,实现了精确的周期性采样,CPU空出来做数据滤波和通信。"
代码示例:ADC+DMA连续采集(CubeMX配置+HAL库)
c
/* CubeMX配置:
- ADC1: 连续扫描模式,扫描3个通道
- DMA: ADC1 → Memory, Circular模式, Half-Word
*/
#define ADC_CHANNELS 3
uint16_t adc_buffer[ADC_CHANNELS]; /* DMA搬运目标 */
/* 启动ADC+DMA采集 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, ADC_CHANNELS);
/* DMA传输完成回调------一轮采集完成 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
if (hadc->Instance == ADC1) {
/* 此时 adc_buffer[0]~[2] 已经是最新数据 */
float voltage_ch0 = adc_buffer[0] * 3.3f / 4096;
float voltage_ch1 = adc_buffer[1] * 3.3f / 4096;
float voltage_ch2 = adc_buffer[2] * 3.3f / 4096;
printf("ADC: %.2fV | %.2fV | %.2fV\r\n",
voltage_ch0, voltage_ch1, voltage_ch2);
}
}
2. 看门狗(Watchdog)
问题:什么是看门狗?为什么需要它?
回答
看门狗本质是一个定时器 ,如果你不在规定时间内"喂狗"(重置计数器),它就会强制复位整个系统。
正常运行:
CPU ──▶ 正常工作 ──▶ 喂狗 ──▶ 正常工作 ──▶ 喂狗 ──▶ ...
看门狗: 计数→0 重新计数 计数→0 重新计数
✅ 系统不死
程序跑飞:
CPU ──▶ 死循环/卡死 ──▶ 没人喂狗 ──▶ 计数到0 ──▶ 复位!
看门狗: 计数→0 🔥 强制重启
为什么需要看门狗? 嵌入式系统可能跑在无人值守的环境(工业控制、户外设备),程序死机了没人按重启按钮。看门狗就是自动重启机制,保证系统在异常时能恢复。
STM32的两种看门狗
| 对比项 | IWDG(独立看门狗) | WWDG(窗口看门狗) |
|---|---|---|
| 时钟源 | LSI(~40kHz,内部RC) | APB1时钟 |
| 精度 | 较低(LSI有±30%偏差) | 较高 |
| 喂狗限制 | 任何时候都可以喂 | 必须在"窗口"内喂,太早太晚都复位 |
| 适用场景 | 防死机、基本保护 | 对时序有严格要求的场合 |
| 典型超时 | 100ms ~ 26s | 几ms ~ 几十ms |
面试官可能的追问
Q:IWDG和WWDG的核心区别是什么?
IWDG只要在超时前喂狗就行,喂早了没事 。WWDG有"窗口"概念------必须在计数器降到某个值之前、又不能太早喂,喂早了也复位。WWDG能检测到程序"跑得太快"(不正常地频繁喂狗)。
Q:在FreeRTOS中怎么用看门狗?
"我会创建一个专门的看门狗任务,优先级设最高。这个任务通过任务通知等待其他关键任务的'心跳'信号。只有当所有关键任务都正常运行并发送了心跳,才喂狗。任何一个任务卡死,看门狗就不会被喂,系统自动复位。"
代码示例:IWDG + FreeRTOS多任务看门狗
c
/* CubeMX配置:
- IWDG: 预分频=64, 重载值=625 → 超时 ≈ 1s
*/
/* FreeRTOS任务心跳标志 */
#define TASK_COUNT 3
volatile uint8_t task_alive[TASK_COUNT] = {0};
/* 各任务中定期发送心跳 */
void sensor_task(void *param) {
while (1) {
/* 执行传感器读取... */
task_alive[0] = 1; /* 标记"我还活着" */
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void comm_task(void *param) {
while (1) {
/* 执行通信处理... */
task_alive[1] = 1;
vTaskDelay(pdMS_TO_TICKS(300));
}
}
void display_task(void *param) {
while (1) {
/* 执行显示刷新... */
task_alive[2] = 1;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
/* 看门狗任务------优先级最高 */
void watchdog_task(void *param) {
while (1) {
vTaskDelay(pdMS_TO_TICKS(800)); /* 在超时前检查 */
uint8_t all_alive = 1;
for (int i = 0; i < TASK_COUNT; i++) {
if (!task_alive[i]) {
printf("[WDG] 任务 %d 卡死!准备复位...\r\n", i);
all_alive = 0;
break; /* 不喂狗,等复位 */
}
task_alive[i] = 0; /* 清标志,下一轮重新检测 */
}
if (all_alive) {
HAL_IWDG_Refresh(&hiwdg); /* 喂狗 */
}
}
}
3. 低功耗模式
问题:STM32有哪些低功耗模式?怎么选?
回答
STM32有三种低功耗模式,功耗从低到高:
┌───────────────────────────────────────────────────────────┐
│ Sleep(睡眠) │
│ • CPU停止,外设继续运行 │
│ • 任意中断唤醒 │
│ • 功耗:~mA级 │
│ • 适合:等数据的时候让CPU歇会儿 │
├───────────────────────────────────────────────────────────┤
│ Stop(停止) │
│ • CPU停止,大部分时钟关闭 │
│ • 只有EXTI(外部中断)能唤醒 │
│ • 功耗:~几十μA │
│ • 适合:电池供电设备,长时间等待外部事件 │
├───────────────────────────────────────────────────────────┤
│ Standby(待机) │
│ • 几乎全关,只保留唤醒电路 │
│ • WKUP引脚 / RTC闹钟 / 看门狗唤醒 │
│ • 功耗:~几μA(最低) │
│ • 适合:极端省电,类似"关机但能定时醒来" │
└───────────────────────────────────────────────────────────┘
面试官可能的追问
Q:Stop模式和Standby模式的核心区别?
Stop模式保留了SRAM和寄存器的内容,唤醒后从断点继续执行 。Standby模式会清空大部分寄存器,唤醒后相当于系统复位,从main函数重新开始。所以Stop模式适合需要保持状态的场景,Standby适合对功耗极致敏感的场景。
Q:FreeRTOS中怎么实现低功耗?
"FreeRTOS提供了Tickless Idle模式。当所有任务都阻塞时(都在等vTaskDelay或信号量),调度器会自动进入Sleep/Stop模式,并在下一个任务唤醒时间点之前醒来。这样CPU空闲的时间段就被低功耗模式覆盖了,而不是空转。"
进阶补充:在FreeRTOSConfig.h中开启:
c#define configUSE_TICKLESS_IDLE 1还可以重写
portSUPPRESS_TICKS_AND_SLEEP()函数,自定义进入哪种低功耗模式。
Q:低功耗设计中有哪些常见坑?
- 唤醒源没配对:进Stop模式前要确保EXTI中断配置正确,否则醒不过来
- 外设时钟被关了:Stop模式会关HSI/HSE,唤醒后需要重新配置时钟
- 串口还在发数据 :进低功耗前要等串口发送完成(
__HAL_UART_FLUSH_DRREGISTER)- DMA还在跑:进低功耗前停止DMA传输,否则数据会丢
代码示例:Stop模式 + RTC唤醒
c
/* CubeMX配置:
- RTC: 唤醒定时器,内部时钟
- 按键: PA0 → EXTI0,下降沿触发(唤醒源之一)
*/
/* 进入Stop模式,RTC 10秒后唤醒 */
void enter_stop_mode(void) {
/* 1. 确保串口发送完成 */
while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET);
/* 2. 配置RTC唤醒定时器(10秒) */
HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 10, RTC_WAKEUPCLOCK_CK_SPRE_16BITS);
/* 3. 进入Stop模式 */
printf("进入Stop模式,10秒后或按键唤醒...\r\n");
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
/* 4. 被唤醒后从这里继续执行 */
SystemClock_Config(); /* 重新配置时钟 */
printf("已唤醒!\r\n");
}
/* 唤醒后会进入RTC Wakeup中断或EXTI中断 */
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) {
/* RTC唤醒,什么都不做,程序会从HAL_PWR_EnterSTOPMode之后继续 */
}
总结:三个知识点怎么串起来理解?
┌───────────┐ ┌───────────┐ ┌───────────┐
│ DMA │ │ 看门狗 │ │ 低功耗 │
│ 效率 │ │ 可靠性 │ │ 功耗 │
│ │ │ │ │ │
│ CPU不搬砖 │ │ 程序死了 │ │ CPU闲着 │
│ 让DMA干 │ │ 自动重启 │ │ 就去睡觉 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────┼───────────────┘
▼
一个优秀的嵌入式系统:
高效(DMA)+ 可靠(看门狗)+ 省电(低功耗)
面试回答模板:先说是什么 → 再说为什么需要 → 最后说你在哪里用过。有项目经验加分巨大。
📌 下期预告:嵌入式面试高频题第6弹------SPI vs I2C对比、Flash读写、Bootloader原理
👉 关注我不迷路,持续更新嵌入式面试题 + FreeRTOS笔记
💬 评论区来聊聊:
- DMA、看门狗、低功耗这三个,你最熟的是哪个?
- 面试中有没有被问到过"低功耗怎么设计"这种开放题?
- 你用过看门狗吗?遇到过什么坑?