引言

在电池供电的物联网设备中,低功耗设计是核心需求。STM32 提供了多种低功耗模式(Sleep、Stop、Standby),而 DMA(直接内存访问)则能在外设与内存间高效搬运数据,减少 CPU 干预。然而,当两者结合时,开发者常遇到 DMA 传输在低功耗模式下失效的问题,导致数据丢失或系统挂起。本文将深入分析失效原因,并提供切实可行的解决策略。

低功耗模式与 DMA 的冲突根源

1. 时钟与电源域的变化

  • Stop 模式:关闭大部分外设时钟,仅保留低功耗定时器(LPTIM)和部分唤醒源。DMA 依赖的系统时钟(如 AHB/APB)被关闭,导致 DMA 控制器停止工作。
  • Standby 模式:几乎全部电路断电,仅保留备份域和唤醒引脚。DMA 完全失效。
  • Sleep 模式:CPU 停止,但外设时钟仍运行,DMA 可正常工作,但功耗降低有限。

2. DMA 传输的触发源

DMA 传输通常由外设事件触发(如 UART 接收、ADC 转换完成)。在低功耗模式下,这些外设可能被关闭或时钟停止,无法产生触发信号,导致 DMA 无法启动。

3. 缓冲区与内存的电源域

在 Stop 模式下,SRAM 内容保留,但若 DMA 配置的缓冲区位于被断电的域(如备份 SRAM),则访问失败。

解决策略

策略一:选择合适的低功耗模式

  • 若需 DMA 持续工作,应使用 Sleep 模式低功耗运行模式(Low-power Run),此时系统时钟仍运行。
  • 若必须使用 Stop 模式,则需在进入低功耗前完成必要的 DMA 传输,或使用支持低功耗的外设(如 LPUART)配合 DMA,但需注意时钟源。

策略二:使用低功耗定时器(LPTIM)触发 DMA

LPTIM 在 Stop 模式下仍可运行(使用 LSI 时钟),可配置为周期性触发 DMA。但需确保 DMA 控制器在 Stop 模式下保持时钟(部分系列支持,如 STM32L4 的 DMA 可配置为保持时钟)。

策略三:进入低功耗前完成 DMA 传输并等待完成

最稳妥的方法:在进入低功耗前,确保所有 DMA 传输已完成,并关闭 DMA 中断,避免在低功耗模式下产生中断唤醒。

策略四:利用事件唤醒后重新初始化 DMA

在唤醒后重新配置 DMA,确保其状态正确。

完整代码示例(基于 STM32L4 + HAL 库)

以下示例演示如何安全地使用 DMA 进行 UART 接收,并在进入 Stop 模式前完成传输。

// 全局变量
UART_HandleTypeDef huart2;
DMA_HandleTypeDef hdma_usart2_rx;
uint8_t rx_buffer[128];
volatile uint8_t rx_complete = 0;

// UART DMA 接收初始化
void MX_DMA_Init(void)
{
    __HAL_RCC_DMA1_CLK_ENABLE();
    hdma_usart2_rx.Instance = DMA1_Channel5;
    hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart2_rx.Init.Mode = DMA_NORMAL;
    hdma_usart2_rx.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_usart2_rx);
    __HAL_LINKDMA(&huart2, hdmarx, hdma_usart2_rx);
}

// 启动 DMA 接收
void Start_DMA_Receive(void)
{
    rx_complete = 0;
    HAL_UART_Receive_DMA(&huart2, rx_buffer, sizeof(rx_buffer));
}

// DMA 传输完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART2) {
        rx_complete = 1;
    }
}

// 进入低功耗前调用此函数
void Enter_StopMode_Safely(void)
{
    // 停止 DMA 接收,防止在低功耗模式下触发
    HAL_UART_DMAStop(&huart2);
    
    // 等待当前传输完成(若正在传输)
    while (HAL_DMA_GetState(&hdma_usart2_rx) != HAL_DMA_STATE_READY) {
        // 超时保护
    }
    
    // 关闭 UART 时钟(可选)
    __HAL_UART_DISABLE(&huart2);
    
    // 进入 Stop 模式
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    
    // 唤醒后重新配置系统时钟(需调用 SystemClock_Config())
    SystemClock_Config();
    
    // 重新初始化 UART 和 DMA
    MX_UART2_Init();
    MX_DMA_Init();
    
    // 重新启动 DMA 接收
    Start_DMA_Receive();
}

注意事项

  • 时钟配置:进入 Stop 模式前,确保系统时钟配置正确,唤醒后必须重新配置时钟,否则外设工作异常。
  • DMA 中断:在低功耗模式下,DMA 中断可能无法唤醒 CPU,因此不要在低功耗期间依赖 DMA 中断。
  • 缓冲区位置:若使用备份 SRAM,需确认其电源域在低功耗模式下是否保留。
  • 外设时钟门控:在 Stop 模式下,外设时钟被关闭,但部分外设(如 LPUART)有独立时钟源,可配合 DMA 使用,但需查阅参考手册确认。
  • 调试建议:使用调试器观察 DMA 状态寄存器,确认传输是否启动。

总结

STM32 低功耗模式与 DMA 的冲突主要源于时钟和电源域的变化。解决思路是:要么选择允许 DMA 工作的低功耗模式(如 Sleep),要么在进入低功耗前完成传输并妥善处理唤醒后的重新初始化。通过合理配置和代码设计,可以兼顾低功耗与高效数据传输,确保系统稳定可靠。