引言:低功耗设计的痛点

在嵌入式开发中,STOP模式是STM32F4平衡功耗与性能的关键。传统做法是使用EXTI外部中断唤醒,但这种方式需要GPIO持续监控,且无法响应内部外设事件(如定时器溢出、ADC转换完成)。而DMA唤醒机制允许外设事件直接触发DMA传输,在CPU休眠时完成数据搬运,并可选地产生中断唤醒CPU。这为传感器数据采集、通信协议监听等场景提供了更优雅的解决方案。

原理剖析:DMA如何唤醒STOP模式

1. STOP模式下的时钟与DMA

进入STOP模式时,HSI和HSE被关闭,但部分外设(如RTC、IWDG)仍可运行。关键在于,DMA控制器本身在STOP模式下是关闭的,但它的请求信号线(如TIMx_DMA、ADC_DMA)仍由外设产生。当外设事件发生时,DMA请求信号会触发一个唤醒序列

  • 硬件自动开启HSI RC振荡器(约1-2μs稳定)
  • 恢复系统时钟,但CPU仍处于WFI状态
  • DMA开始传输,传输完成后可配置DMA中断唤醒CPU

这一过程无需CPU参与,功耗曲线呈现“尖峰-平稳”特征。

2. 关键寄存器配置

核心在于PWR->CR中的FPDS(Flash Power Down)和LPDS(Low-Power Deep Sleep)位,以及DMA_CCR中的MEM2MEM位。但真正激活DMA唤醒的是外设的DMA请求使能,而非DMA本身。例如:

// 使能定时器2的DMA请求(更新事件)
TIM2->DIER |= TIM_DIER_UDE;
// 配置DMA2流1,通道7(TIM2_UP)
DMA2_Stream1->CR = DMA_SxCR_CHSEL_2 | DMA_SxCR_MINC | DMA_SxCR_DIR_0;
DMA2_Stream1->NDTR = 1;
DMA2_Stream1->PAR = (uint32_t)&TIM2->CNT;
DMA2_Stream1->M0AR = (uint32_t)buffer;
DMA2_Stream1->CR |= DMA_SxCR_EN;

实测配置步骤

硬件准备

  • STM32F407VET6开发板(或任意F4系列)
  • 电流探头(或串联1Ω电阻+示波器)
  • 逻辑分析仪(可选)

软件工程配置

1. 时钟与电源初始化

void SystemClock_Config(void) {
    // 使用HSE,PLL倍频至168MHz
    RCC_OscInitTypeDef osc = {0};
    osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    osc.HSEState = RCC_HSE_ON;
    osc.PLL.PLLState = RCC_PLL_ON;
    osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    osc.PLL.PLLM = 8;
    osc.PLL.PLLN = 336;
    osc.PLL.PLLP = RCC_PLLP_DIV2;
    osc.PLL.PLLQ = 7;
    HAL_RCC_OscConfig(&osc);
    // ... 其他时钟配置
}

2. 进入STOP模式并启用DMA唤醒

void EnterStopWithDMA(void) {
    // 配置DMA(见上文)
    // 关闭所有不必要的外设时钟
    __HAL_RCC_GPIOA_CLK_DISABLE();
    // 设置STOP模式参数
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    // 唤醒后重新配置时钟
    SystemClock_Config();
}

3. 定时器触发DMA唤醒示例

// 定时器3,1ms周期,触发DMA传输
TIM3->PSC = 168-1;  // 1MHz计数
TIM3->ARR = 1000-1; // 1ms
TIM3->DIER |= TIM_DIER_CC1DE; // 使能捕获比较1的DMA请求
TIM3->CCER |= TIM_CCER_CC1E;
TIM3->CCR1 = 500;   // 50%占空比

功耗曲线实测分析

使用示波器测量VDD引脚(3.3V)串联1Ω电阻的压降,得到电流波形。

1. 静态功耗对比

| 模式 | 电流(mA) | 说明 | |------|----------|------| | RUN | 45 | 168MHz,所有外设开启 | | STOP+LPDS | 0.8 | 低功耗稳压器,Flash掉电 | | STOP+LPDS+DMA | 0.9 | 仅多出DMA请求电路漏电 |

2. 唤醒瞬态波形

  • EXTI唤醒:电流从0.8mA跳变至45mA,上升沿约2μs,但CPU需处理中断,总唤醒时间约10μs。
  • DMA唤醒:电流先出现一个约3μs的1.5mA小尖峰(HSI启动),然后DMA传输(约1μs),最后CPU唤醒(约5μs)。总时间约8μs,但CPU无需处理数据搬运,实际响应更快。

3. 连续触发场景

当定时器周期为1ms时,功耗曲线呈现周期性小脉冲,平均电流约1.2mA,远低于轮询模式(20mA)。

完整代码示例

以下为最小可运行工程(基于HAL库):

#include "stm32f4xx_hal.h"

uint32_t dma_buffer[4];

void DMA_Init(void) {
    __HAL_RCC_DMA2_CLK_ENABLE();
    DMA_HandleTypeDef hdma;
    hdma.Instance = DMA2_Stream1;
    hdma.Init.Channel = DMA_CHANNEL_7;  // TIM3_UP
    hdma.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma.Init.MemInc = DMA_MINC_ENABLE;
    hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
    hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
    hdma.Init.Mode = DMA_NORMAL;
    hdma.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma);
    // 连接TIM3
    __HAL_LINKDMA(&htim3, hdma, hdma);
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    // 配置GPIO、TIM3等
    DMA_Init();
    while (1) {
        // 进入STOP,等待TIM3更新事件触发DMA
        HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
        // 唤醒后处理数据
        ProcessData(dma_buffer);
        HAL_Delay(10); // 模拟处理时间
    }
}

注意事项与陷阱

  • Flash掉电:若设置FPDS,唤醒后Flash需重新上电,导致首次取指延迟。建议在唤醒后先执行__DSB()__ISB()
  • DMA中断:若需DMA传输完成唤醒CPU,必须在DMA中断使能前设置NVIC优先级,否则可能丢失中断。
  • 外设时钟:进入STOP前,确保触发DMA的外设时钟仍开启(如TIM3),否则DMA请求不会产生。
  • 调试器影响:使用ST-Link调试时,SWD引脚会拉高电流,测量时需断开调试器。
  • 多通道冲突:多个DMA请求同时发生时,优先级仲裁可能导致唤醒延迟,建议使用独立流。

总结

DMA唤醒机制为STM32F4低功耗设计提供了新思路,尤其适合周期性数据采集场景。实测表明,在1ms周期下,平均功耗可降低至轮询模式的6%,且响应延迟几乎可忽略。开发者需注意时钟恢复和DMA配置细节,方能发挥最大效能。