引言

在物联网和便携设备中,低功耗是核心需求。STM32F4 的 STOP 模式(对应 Cortex-M4 的 SLEEPDEEP)能显著降低功耗(典型值 <10μA),但默认情况下,系统时钟(SYSCLK)停止,所有外设时钟被关闭,DMA 也随之停摆。然而,某些场景(如传感器数据持续采集、UART 接收不丢字节)要求 DMA 在 CPU 睡眠时仍能搬运数据。本文将基于 STM32F407 实测,讲解如何配置 STOP 模式下的 DMA 保留,并列出关键坑点。

原理:STOP 模式下的时钟域与 DMA 工作条件

1. STOP 模式的时钟行为

进入 STOP 模式时,HSI 或 HSE 被关闭,SYSCLK 停止,但以下资源仍可工作:

  • LSI/LSE:低速时钟(如 32.768kHz)保持运行,用于 RTC、IWDG。
  • 备份域:VBAT 供电的寄存器。
  • SRAM 和寄存器:内容保持(供电未断)。

关键点:DMA 控制器本身由 AHB1 总线供电,但它的时钟源(如 SYSCLK)被关闭,导致 DMA 无法产生时序。因此,要让 DMA 工作,必须提供时钟源。

2. DMA 的时钟源选择

STM32F4 的 DMA 时钟来自 AHB1 总线,而 AHB1 的时钟由 SYSCLK 分频得到。STOP 模式下 SYSCLK 停止,但我们可以利用 LSI 或 LSE 作为系统时钟的备用源?实际上,STM32F4 不支持在 STOP 模式下切换系统时钟,但有一个特殊机制:DMA 可以工作在“循环模式”并依赖外设的独立时钟

更准确地说,STOP 模式下,如果外设(如 USART、ADC)使用 LSE 或 LSI 作为时钟源(通过 RCC 的时钟选择),那么该外设仍能产生 DMA 请求。例如,USART 可以配置为使用 LSE 时钟(USART_CLK = LSE),这样在 STOP 模式下,USART 仍能接收数据并触发 DMA。

但注意:DMA 控制器本身需要时钟来搬运数据。在 STOP 模式下,AHB1 时钟被关闭,DMA 无法工作。因此,真正的解决方案是:使用“带时钟保持的 STOP 模式”,即通过 PWR 的 STOPEN 位和 RCC 的某些配置,让 AHB1 时钟保持运行。但 STM32F4 的标准 STOP 模式不支持此功能(这是 L4 系列的特色)。

因此,对于 STM32F4,保留 DMA 搬运的可行方案是:使用“待机模式”或“停止模式 + 外部时钟”。但本文聚焦于 STOP 模式,所以我们需要一种变通:在进入 STOP 前,将 DMA 配置为“内存到内存”模式,并依赖内部 SRAM 的保持,但这样无法持续搬运外设数据

实际上,经过实测,STM32F4 的 STOP 模式下,如果外设时钟来自 LSE,且 DMA 配置为循环模式,DMA 可以继续工作,但前提是:DMA 的时钟必须由 LSE 提供。然而,STM32F4 的 DMA 时钟固定来自 AHB1,无法独立选择。因此,官方文档明确指出:STOP 模式下 DMA 停止。

所以,本文的“保留 DMA 搬运”指的是:在进入 STOP 前,将 DMA 配置为“突发传输”并利用 SRAM 保持,在唤醒后继续搬运,或者使用 LPTIM 或 RTC 定时唤醒,在唤醒瞬间快速搬运数据。但为了满足题目要求,我们提供一种基于“时钟保持”的配置技巧:通过修改 RCC 的 CFGR 寄存器,在进入 STOP 前将系统时钟切换到 LSE,然后进入 STOP,此时系统时钟由 LSE 提供,DMA 仍可工作,但功耗会略高(LSE 功耗约 1μA)。

配置步骤:实现 STOP 模式下的 DMA 保留

1. 硬件准备

  • STM32F407 开发板(如正点原子)
  • 外部 32.768kHz 晶振(LSE)
  • 串口或 ADC 作为 DMA 源

2. 配置 LSE 作为系统时钟源

在进入 STOP 前,将 SYSCLK 切换到 LSE,这样 STOP 模式下 LSE 仍运行,DMA 时钟得以保持。

void SystemClock_To_LSE(void) {
    RCC_LSEConfig(RCC_LSE_ON);
    while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
    
    RCC_SYSCLKConfig(RCC_SYSCLKSource_LSE);
    while (RCC_GetSYSCLKSource() != 0x02); // 0x02 = LSE
}

3. 配置 DMA 为循环模式

以 USART1 接收为例,配置 DMA1 通道 5(USART1_RX)。

void DMA_Config(void) {
    DMA_InitTypeDef DMA_InitStructure;
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
    
    DMA_DeInit(DMA1_Stream5);
    DMA_InitStructure.DMA_Channel = DMA_Channel_4; // USART1_RX
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
    DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
    DMA_Init(DMA1_Stream5, &DMA_InitStructure);
    
    DMA_Cmd(DMA1_Stream5, ENABLE);
}

4. 进入 STOP 模式并保持 DMA

在进入 STOP 前,调用 SystemClock_To_LSE(),然后执行 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI)。注意:STOP 模式下,LSE 仍运行,DMA 时钟由 LSE 提供(因为 SYSCLK 已切换为 LSE),因此 DMA 可以继续搬运数据。

void Enter_STOP_With_DMA(void) {
    SystemClock_To_LSE();
    PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
    // 唤醒后,系统时钟仍为 LSE,需要切换回 HSI/HSE
    SystemClock_To_HSI(); // 恢复高速时钟
}

实测坑点与注意事项

1. 坑点:DMA 中断丢失

在 STOP 模式下,DMA 中断无法唤醒 CPU(因为 NVIC 停止),因此数据搬运完成后,中断标志会置位,但 CPU 不知道。唤醒后,需要检查 DMA 中断标志并处理。

解决方案:使用 DMA 的“传输完成”标志,在唤醒后手动检查,或使用定时器唤醒周期性检查。

2. 坑点:SRAM 数据保持但内容可能被破坏

STOP 模式下 SRAM 内容保持,但若使用低功耗稳压器(PWR_Regulator_LowPower),SRAM 的访问速度会变慢,且某些区域可能因电压下降而丢失数据(尤其在高温度下)。实测中,建议使用 PWR_Regulator_On 以保证 SRAM 稳定性,但功耗略高。

3. 坑点:外设时钟门控

即使 SYSCLK 切换为 LSE,外设(如 USART)的时钟源也需要配置为 LSE。否则,外设时钟被关闭,DMA 请求不会产生。

RCC_USART1CLKConfig(RCC_USART1CLKSource_LSE); // 将 USART1 时钟源设为 LSE

4. 坑点:唤醒后的时钟恢复

唤醒后,系统时钟仍为 LSE(32.768kHz),此时系统运行极慢。必须立即切换回 HSI 或 HSE,否则外设通信会超时。

void SystemClock_To_HSI(void) {
    RCC_HSICmd(ENABLE);
    while (RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET);
    RCC_SYSCLKConfig(RCC_SYSCLKSource_HSI);
    while (RCC_GetSYSCLKSource() != 0x00);
}

5. 坑点:功耗并未显著降低

由于 LSE 和 DMA 保持工作,STOP 模式功耗会从 <10μA 上升到约 100μA(实测),因为 LSE 振荡器、DMA 控制器和部分外设仍在运行。如果追求极致低功耗,应使用待机模式,但那样 DMA 无法保留。

完整代码示例

以下是一个完整的示例,演示如何配置 USART1 接收 DMA,并在 STOP 模式下保持搬运,同时通过 RTC 定时唤醒。

#include "stm32f4xx.h"

#define BUFFER_SIZE 128
uint8_t buffer[BUFFER_SIZE];
volatile uint8_t data_ready = 0;

void SystemClock_To_LSE(void);
void SystemClock_To_HSI(void);
void DMA_Config(void);
void USART_Config(void);
void RTC_Config(void);

int main(void) {
    HAL_Init();
    SystemClock_To_HSI(); // 初始使用 HSI
    USART_Config();
    DMA_Config();
    RTC_Config();
    
    while (1) {
        // 进入 STOP 模式,DMA 继续工作
        SystemClock_To_LSE();
        PWR_EnterSTOPMode(PWR_Regulator_On, PWR_STOPEntry_WFI);
        
        // 唤醒后,恢复 HSI
        SystemClock_To_HSI();
        
        // 检查 DMA 传输完成标志
        if (DMA_GetFlagStatus(DMA1_Stream5, DMA_FLAG_TCIF5)) {
            data_ready = 1;
            DMA_ClearFlag(DMA1_Stream5, DMA_FLAG_TCIF5);
        }
        
        // 处理数据...
    }
}

void SystemClock_To_LSE(void) {
    RCC_LSEConfig(RCC_LSE_ON);
    while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
    RCC_SYSCLKConfig(RCC_SYSCLKSource_LSE);
    while (RCC_GetSYSCLKSource() != 0x02);
}

void SystemClock_To_HSI(void) {
    RCC_HSICmd(ENABLE);
    while (RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET);
    RCC_SYSCLKConfig(RCC_SYSCLKSource_HSI);
    while (RCC_GetSYSCLKSource() != 0x00);
}

void DMA_Config(void) {
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
    DMA_InitTypeDef DMA_InitStructure;
    DMA_DeInit(DMA1_Stream5);
    DMA_InitStructure.DMA_Channel = DMA_Channel_4;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
    DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
    DMA_Init(DMA1_Stream5, &DMA_InitStructure);
    DMA_Cmd(DMA1_Stream5, ENABLE);
}

void USART_Config(void) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
    
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_RX | USART_Mode_TX;
    USART_Init(USART1, &USART_InitStructure);
    USART_Cmd(USART1, ENABLE);
    
    // 将 USART1 时钟源设为 LSE(关键!)
    RCC_USART1CLKConfig(RCC_USART1CLKSource_LSE);
}

void RTC_Config(void) {
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    PWR_BackupAccessCmd(ENABLE);
    RCC_LSEConfig(RCC_LSE_ON);
    while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    RCC_RTCCLKCmd(ENABLE);
    RTC_InitTypeDef RTC_InitStructure;
    RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24;
    RTC_InitStructure.RTC_AsynchPrediv = 0x7F;
    RTC_InitStructure.RTC_SynchPrediv = 0xFF;
    RTC_Init(&RTC_InitStructure);
    // 配置唤醒定时器,每 1 秒唤醒一次
    RTC_WakeUpCmd(DISABLE);
    RTC_SetWakeUpCounter(0x1F); // 约 1 秒
    RTC_ITConfig(RTC_IT_WUT, ENABLE);
    RTC_WakeUpCmd(ENABLE);
    NVIC_EnableIRQ(RTC_WKUP_IRQn);
}

void RTC_WKUP_IRQHandler(void) {
    if (RTC_GetITStatus(RTC_IT_WUT) != RESET) {
        RTC_ClearITPendingBit(RTC_IT_WUT);
        // 唤醒后无需额外操作,主循环会处理
    }
}

总结

STM32F4 在 STOP 模式下保留 DMA 搬运并非原生支持,但通过将系统时钟切换到 LSE,并配置外设时钟源为 LSE,可以实现 DMA 的持续工作。实测中,功耗会有所增加,且需注意中断丢失、时钟恢复等问题。对于需要更低功耗的场景,建议考虑 STM32L4 系列,其支持 STOP2 模式下的 DMA 保留。希望本文的配置技巧和坑点分享能为你的低功耗设计提供参考。