# 基于HAL库建立自己的低功耗模式配置库(STM32L4系列低功耗所有配置汇总) **Repository Path**: Mike_Zhou_Admin/STM32L4_HAL_User_LOW_POWER_Lib ## Basic Information - **Project Name**: 基于HAL库建立自己的低功耗模式配置库(STM32L4系列低功耗所有配置汇总) - **Description**: 【STM32】基于HAL库建立自己的低功耗模式配置库(低功耗所有配置汇总) - **Primary Language**: C - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 14 - **Forks**: 2 - **Created**: 2023-11-02 - **Last Updated**: 2025-08-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: stm32, 嵌入式, 单片机, mcu, 低功耗 ## README 【STM32】基于HAL库建立自己的低功耗模式配置库(STM32L4系列低功耗所有配置汇总) @[toc] 更新以gitee为准 此文章是讨论将先前所有的低功耗配置功能整合起来的一个库(适用于STM32L4系列) 目前除了普通唤醒方式外 加入了UART唤醒和RTC唤醒配置 如果后续有更多唤醒加入(如I2C等 将直接在后续的文章中进行讨论) 本文所建的库将不再更新其他唤醒方式 但会对某些BUG进行修复 相关函数调用和配置 可以从我之前的文章里找到 [【STM32】基于HAL库建立自己的低功耗模式配置库(STM32L4系列低功耗所有配置汇总)](https://blog.csdn.net/weixin_53403301/article/details/134182195) # 低功耗模式(此章节可直接跳过) [【STM32笔记】低功耗模式配置及避坑汇总](https://blog.csdn.net/weixin_53403301/article/details/129119798) ## 低功耗模式简介 系统提供了多个低功耗模式,可在 CPU 不需要运行时(例如等待外部事件时)节省功耗。由用户根据应用选择具体的低功耗模式,以在低功耗、短启动时间和可用唤醒源之间寻求最佳平衡。 睡眠模式、停止模式及待机模式中,若备份域电源正常供电,备份域内的 RTC 都可以正常运行,备份域内的寄存器的数据会被保存,不受功耗模式影响。 从表中可以看到,这三种低功耗模式层层递进,运行的时钟或芯片功能越来越少,因而功耗越来越低。 | 模式 |说明 |进入方式 |唤醒 |对1.8V区域时钟的影响 |对VDD区域时钟的影响 | 调压器| |--|--|--|--|--|--|--| | 睡眠模式|内核停止,所有外设包括M3核心的外设,如NVIC、系统时钟(SysTick)等仍在运行|WFI、WFE命令(HAL库直接调用)|任意中断/事件 |内核时钟关,对其他时钟和ADC时钟无影响| 无|开 | |--|--|--|--|--|--| | 停止模式|所有的时钟都已停止|配置PWR_CR寄存器的PDDS+LPDS位+SLEEPDEEP位+WFI或WFE命令|任意外部中断EXTI(在外部中断寄存器中设置)|关闭所有1.8V区域的时钟| HSI和HSE的振荡器关闭|开启或处于低功耗模式(依据电源控制寄存器的设定) | |--|--|--|--|--|--| | 待机模式|1.8V电源关闭|配置PWR_CR寄存器的PDDS+SLEEPDEEP位+WFI或WFE命令|WKUP、引脚的RTC闹钟事件、NRST引脚上的外部复位、IWDG复位 |关闭所有1.8V区域的时钟| HSI和HSE的振荡器关闭|关 | |--|--|--|--|--|--| L4及L4+的通用模式状态表可见手册 ![在这里插入图片描述](https://img-blog.csdnimg.cn/e1d0240c04f144cb9ad4e18f95eedd1c.jpeg) ![在这里插入图片描述](https://img-blog.csdnimg.cn/3f246ea281ab4c27b2850f17fb91758d.png) [【STM32笔记】低功耗模式下的RTC唤醒(非闹钟唤醒,而是采用RTC_WAKEUPTIMER)](http://blog.csdn.net/weixin_53403301/article/details/129031935) [【STM32笔记】低功耗模式下GPIO省电配置避坑实验(闲置引脚配置为模拟输入其实更耗电)](http://blog.csdn.net/weixin_53403301/article/details/129055530) [【STM32笔记】低功耗模式下GPIO、外设省电配置避坑](http://blog.csdn.net/weixin_53403301/article/details/129060093) ### 睡眠模式 在睡眠模式中,仅关闭了内核时钟,内核停止运行,但其片上外设,CM3 核心的外设全都还照常运行。有两种方式进入睡眠模式,它的进入方式决定了从睡眠唤醒的方式,分别是 WFI(wait for interrupt) 和 WFE(wait for event),即由等待“中断”唤醒和由“事件”唤醒。 ```c 特性和说明: 立即睡眠: 在执行 WFI 或 WFE 指令时立即进入睡眠模式。 退出时睡眠: 在退出优先级最低的中断服务程序后才进入睡眠模式。 进入方式: 内核寄存器的 SLEEPDEEP=0 ,然后调用 WFI 或 WFE 指令即可进入睡眠模式;SLEEPONEXIT=1 时,进入“退出时睡眠”模式。 唤醒方式: 如果是使用 WFI 指令睡眠的,则可使用任意中断唤醒;如果是使用 WFE 指令睡眠的,则由事件唤醒。 睡眠时: 关闭内核时钟,内核停止,而外设正常运行,在软件上表现为不再执行新的代码。这个状态会保留睡眠前的内核寄存器、内存的数据。 唤醒延迟: 无延迟。 唤醒后: 若由中断唤醒,先进入中断,退出中断服务程序后,接着执行 WFI 指令后的程序;若由事件唤醒,直接接着执行 WFE 后的程序。 ``` 唤醒后即可开始行动 继续程序 无需配置任何寄存器 睡眠模式和低功耗睡眠模式是两个模式 由`PWR_MAINREGULATOR_ON`和`PWR_LOWPOWERREGULATOR_ON`两个变量确定 要进入低功耗睡眠模式 首先得进入低功耗运行模式 ```c HAL_PWREx_EableLowPowerRunMode() ``` 且工作频率降低到2MHz以下 唤醒时 睡眠模式直接唤醒 而低功耗睡眠模式唤醒后 会进入到低功耗运行模式 若想正常工作 需用`HAL_PWREx_DisableLowPowerRunMode()`退出 ### 停止模式 在停止模式中,进一步关闭了其它所有的时钟,于是所有的外设都停止了工作,但由于其 1.8V 区域的部分电源没有关闭,还保留了内核的寄存器、内存的信息,所以从停止模式唤醒,并重新开启时钟后,还可以从上次停止处继续执行代码。停止模式可以由任意一个外部中断(EXTI)唤醒,在停止模式中可以选择电压调节器为开模式或低功耗模式。 ```c 特性和说明: 调压器低功耗模式: 在停止模式下调压器可工作在正常模式或低功耗模式,可进一步降低功耗。 进入方式: 内核寄存器的 SLEEPDEEP=1,PWR_CR 寄存器中的 PDDS=0,然后调用 WFI 或 WFE 指令即可进入停止模式;PWR_CR 寄存器的 LPDS=0 时,调压器工作在正常模式,LPDS=1 时工作在低功耗模式。 唤醒方式: 如果是使用 WFI 指令睡眠的,可使用任意 EXTI 线的中断唤醒;如果是使用 WFE 指令睡眠的,可使用任意配置为事件模式的 EXTI 线事件唤醒。 停止时: 内核停止,片上外设也停止。这个状态会保留停止前的内核寄存器、内存的数据。 唤醒延迟: 基础延迟为 HSI 振荡器的启动时间,若调压器工作在低功耗模式,还需要加上调压器从低功耗切换至正常模式下的时间。 唤醒后: 若由中断唤醒,先进入中断,退出中断服务程序后,接着执行 WFI 指令后的程序;若由事件唤醒,直接接着执行 WFE 后的程序。唤醒后,STM32 会使用 HSI 作为系统时钟。 ``` 只能由外部中断唤醒 唤醒后需要重新使能时钟(`SystemClock_Config();`) 建议将一条外部中断线专门作为唤醒中断,执行中断后进入回调进行时钟使能 停止模式0和1由`PWR_MAINREGULATOR_ON`和`PWR_LOWPOWERREGULATOR_ON`两个变量确定 停止模式0和1可以被串口 I2C等设备唤醒(具体看手册) 停止模式2则在pwr_ex.c中进入 停止模式2 只能被特定器件(如LPUART等在内部与EXTI有链接的器件)唤醒 详情见后续关于STOP模式串口唤醒的文章 [【STM32笔记】HAL库低功耗STOP停止模式的串口唤醒(解决串口唤醒和回调无法一起使用的问题)](https://blog.csdn.net/weixin_53403301/article/details/129014963) ### 待机模式 翻译成shutdown更为合适 待机模式,它除了关闭所有的时钟,还把 1.8V 区域的电源也完全关闭了,也就是说,从待机模式唤醒后,由于没有之前代码的运行记录,只能对芯片复位,重新检测 boot 条件,从头开始执行程序。它有四种唤醒方式,分别是 WKUP(PA0)引脚的上升沿,RTC 闹钟事件,NRST 引脚的复位和 IWDG(独立看门狗)复位。 ```c 特性和说明: 进入方式: 内核寄存器的 SLEEPDEEP=1,PWR_CR 寄存器中的 PDDS=1,PWR_CR 寄存器中的唤醒状态位 WUF=0,然后调用 WFI 或 WFE 指令即可进入待机模式。 唤醒方式: 通过 WKUP ,RTC 闹钟、唤醒、入侵、时间戳事件或 NRST 引脚外部复位及 IWDG 复位唤醒。 待机时: 内核停止,片上外设也停止;内核寄存器、内存的数据会丢失;除复位引脚、RTC_AF1 引脚及 WKUP 引脚,其它 I/O 口均工作在高阻态。 唤醒延迟: 芯片复位的时间。 唤醒后: 相当于芯片复位,在程序表现为从头开始执行代码。 ``` # 建立自己的低功耗模式配置库 首先 在先前的文章中 我们通过`Enter_Low_PWR`来进入低功耗模式 同时 在进入低功耗之前 需要调用唤醒配置函数 退出低功耗后 也要初始化时钟等等 而建立的这个库 就是把所有配置整合到一起 从而使其能直接用一个通用函数代替 文中用于传参的配置结构体是一个全局变量 另外用到了结构体嵌套 回调函数等等 该写法也是TI的SDK常用写法 ## 通过结构体的方式来进行传参 最主要的就是`mode_flag`参数 该参数决定了四种低功耗模式 其中 停止模式默认是停止1 ```c typedef struct { uint8_t mode_flag; // 0/大于4 不进入任何模式,1 进入睡眠,2 进入停止1,3 进入待机,4 关机 LOW_POWER_SLEEPEntry_Cfg SLEEPEntry_Cfg; //进入睡眠模式的方式 LOW_POWER_STOPEntry_Cfg STOPEntry_Cfg; //进入停止模式的方式 LOW_POWER_WakeUpPin_Cfg WakeUpPin_Cfg ; //待机模式的唤醒引脚配置 LOW_POWER_RTC_Cfg RTC_Cfg; //RTC唤醒配置 LOW_POWER_Device_Cfg Device_Cfg; SystemClock_Config_Callback SystemClock_Config_Fxn; // 用于传入退出相关低功耗模式后 需要进行配置的系统时钟配置函数 }LOW_POWER_Entry_Cfg; ``` 次级结构体包括进入低功耗的方式 待机模式唤醒引脚配置以及外设唤醒模式和唤醒后的时钟配置回调. 其中 外设唤醒分为外设和RTC 之所以要把RTC单独列出来 是因为RTC有且仅有一个 而其他外设唤醒 可能会有UART 也能有I2C SPI等等 所以我建立的这些函数都是`__weak`声明 可以结合不同的工程来覆写 ```c typedef void (*SystemClock_Config_Callback)(void); typedef struct { uint8_t SLEEPEntry; //SLEEPEntry: 一般是 PWR_SLEEPENTRY_WFI 等待中断 也可以是 PWR_SLEEPENTRY_WFE }LOW_POWER_SLEEPEntry_Cfg; typedef struct { uint8_t STOPEntry; //STOPEntry: 一般是 PWR_STOPENTRY_WFI 等待中断 也可以是 PWR_STOPENTRY_WFE }LOW_POWER_STOPEntry_Cfg; typedef struct { uint32_t WakeUpPinPolarity; //WakeUpPinPolarity: 待机模式下WKUP唤醒引脚极性配置,其他模式无用 有的只能配置一个引脚 所以要看数据手册 /* * PWR_WAKEUP_PIN1_HIGH or PWR_WAKEUP_PIN1_LOW * PWR_WAKEUP_PIN2_HIGH or PWR_WAKEUP_PIN2_LOW * PWR_WAKEUP_PIN3_HIGH or PWR_WAKEUP_PIN3_LOW * PWR_WAKEUP_PIN4_HIGH or PWR_WAKEUP_PIN4_LOW * PWR_WAKEUP_PIN5_HIGH or PWR_WAKEUP_PIN5_LOW */ }LOW_POWER_WakeUpPin_Cfg; typedef struct { bool EnableNotDisable; RTC_HandleTypeDef *rtc_handle; uint32_t counter; //RTC计数值 由于进入低功耗模式会有约10ms消抖 所以建议减去这段时间 uint32_t clock; //RTC时钟源 一般是 RTC_WAKEUPCLOCK_RTCCLK_DIV16 }LOW_POWER_RTC_Cfg; typedef struct { bool EnableNotDisable; UART_HandleTypeDef *uart_handle; UART_WakeUpTypeDef UART_WakeUpStruct; //UART唤醒的结构体配置 UART_WakeUpStruct.WakeUpEvent = UART_WAKEUP_ON_READDATA_NONEMPTY 就是接收数据不为空时唤醒 }LOW_POWER_UART_Cfg; typedef struct { LOW_POWER_UART_Cfg UART_Cfg[5]; //串口唤醒配置 有五个串口 所以最大buf长度为5 }LOW_POWER_Device_Cfg; typedef struct { uint8_t mode_flag; // 0/大于4 不进入任何模式,1 进入睡眠,2 进入停止1,3 进入待机,4 关机 LOW_POWER_SLEEPEntry_Cfg SLEEPEntry_Cfg; //进入睡眠模式的方式 LOW_POWER_STOPEntry_Cfg STOPEntry_Cfg; //进入停止模式的方式 LOW_POWER_WakeUpPin_Cfg WakeUpPin_Cfg ; //待机模式的唤醒引脚配置 LOW_POWER_RTC_Cfg RTC_Cfg; //RTC唤醒配置 LOW_POWER_Device_Cfg Device_Cfg; SystemClock_Config_Callback SystemClock_Config_Fxn; // 用于传入退出相关低功耗模式后 需要进行配置的系统时钟配置函数 }LOW_POWER_Entry_Cfg; ``` ```c /*! * @brief 进入低功耗模式 * * @return None */ __weak void Enter_Low_PWR(void) { __HAL_RCC_PWR_CLK_ENABLE(); switch(LP_Entry_Cfg.mode_flag) { case 0: { printf("[INFO] 不进入低功耗模式\n"); break; } case 1: { printf("[INFO] 进入睡眠模式\n"); delay_ms(10); //消抖 PWR_Device_Init(false); __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); Ctrl_RTC_WakeUp(&LP_Entry_Cfg.RTC_Cfg,LP_Entry_Cfg.RTC_Cfg.EnableNotDisable); HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON,LP_Entry_Cfg.SLEEPEntry_Cfg.SLEEPEntry); __HAL_RCC_PWR_CLK_ENABLE(); Ctrl_RTC_WakeUp(NULL,false); PWR_Device_Init(true); break; } case 2: { printf("[INFO] 进入停止模式\n"); delay_ms(10); //消抖 PWR_Device_Init(false); __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); Ctrl_Stop_Mode_WakeUp_Device(true); Ctrl_RTC_WakeUp(&LP_Entry_Cfg.RTC_Cfg,LP_Entry_Cfg.RTC_Cfg.EnableNotDisable); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON,LP_Entry_Cfg.STOPEntry_Cfg.STOPEntry); __HAL_RCC_PWR_CLK_ENABLE(); LP_Entry_Cfg.SystemClock_Config_Fxn(); Ctrl_Stop_Mode_WakeUp_Device(false); Ctrl_RTC_WakeUp(NULL,false); PWR_Device_Init(true); break; } case 3: { printf("[INFO] 三秒后进入待机模式\n"); delay_ms(3000); printf("[INFO] 进入待机模式\n"); HAL_PWR_EnableWakeUpPin(LP_Entry_Cfg.WakeUpPin_Cfg.WakeUpPinPolarity); delay_ms(10); //消抖 __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); Ctrl_RTC_WakeUp(&LP_Entry_Cfg.RTC_Cfg,LP_Entry_Cfg.RTC_Cfg.EnableNotDisable); HAL_PWR_EnterSTANDBYMode(); break; } case 4: { printf("[INFO] 三秒后进入关机模式\n"); delay_ms(3000); printf("[INFO] 进入关机模式\n"); HAL_PWR_EnableWakeUpPin(LP_Entry_Cfg.WakeUpPin_Cfg.WakeUpPinPolarity); delay_ms(10); //消抖 __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); Ctrl_RTC_WakeUp(&LP_Entry_Cfg.RTC_Cfg,LP_Entry_Cfg.RTC_Cfg.EnableNotDisable); HAL_PWREx_EnterSHUTDOWNMode(); break; } default: { printf("[INFO] 不进入低功耗模式\n"); break; } } } ``` ### RTC配置 ```c typedef struct { bool EnableNotDisable; RTC_HandleTypeDef *rtc_handle; uint32_t counter; //RTC计数值 由于进入低功耗模式会有约10ms消抖 所以建议减去这段时间 uint32_t clock; //RTC时钟源 一般是 RTC_WAKEUPCLOCK_RTCCLK_DIV16 }LOW_POWER_RTC_Cfg; ``` 这里的时钟源和计数值就是`HAL_RTCEx_SetWakeUpTimer_IT`中的传参 ```c /*! * @brief 配置RTC在低功耗模式下的唤醒 * * @param [in] RTC_Cfg: RTC配置 * * @return None */ __weak void Ctrl_RTC_WakeUp(LOW_POWER_RTC_Cfg *RTC_Cfg,bool EnableNotDisable) { if(EnableNotDisable) { HAL_RTCEx_SetWakeUpTimer_IT(RTC_Cfg->rtc_handle,RTC_Cfg->counter,RTC_Cfg->clock); } else { __HAL_RTC_WAKEUPTIMER_EXTI_DISABLE_IT(); } } ``` [【STM32笔记】低功耗模式下的RTC唤醒(非闹钟唤醒,而是采用RTC_WAKEUPTIMER)](http://blog.csdn.net/weixin_53403301/article/details/129031935) ### UART配置 [【STM32笔记】HAL库低功耗STOP停止模式的串口唤醒(解决串口唤醒和回调无法一起使用的问题)](https://blog.csdn.net/weixin_53403301/article/details/129014963) ```c typedef struct { bool EnableNotDisable; UART_HandleTypeDef *uart_handle; UART_WakeUpTypeDef UART_WakeUpStruct; //UART唤醒的结构体配置 UART_WakeUpStruct.WakeUpEvent = UART_WAKEUP_ON_READDATA_NONEMPTY 就是接收数据不为空时唤醒 }LOW_POWER_UART_Cfg; ``` `UART_WakeUpStruct`结构体中 一般把`WakeUpEvent=UART_WAKEUP_ON_READDATA_NONEMPTY` 表示接收数据不为空时唤醒 串口最多有五个 所以在结构体中定义的是一个长度为5的数组 然后在配置函数中做判断 为NULL就跳过 ```c /*! * @brief 配置串口在停止模式下的唤醒 * * @param [in] UART_Cfg: UART配置 * * @return None */ __weak uint8_t Ctrl_UART_StopMode_WakeUp(LOW_POWER_UART_Cfg *UART_Cfg,bool EnableNotDisable) { if (!UART_Cfg->uart_handle) { return 0; } if(EnableNotDisable) { __HAL_RCC_WAKEUPSTOP_CLK_CONFIG(RCC_STOP_WAKEUPCLOCK_HSI); //保留唤醒用的HSI线 串口初始化时钟也必须要配置为HSI HAL_UARTEx_StopModeWakeUpSourceConfig(UART_Cfg->uart_handle,UART_Cfg->UART_WakeUpStruct); __HAL_UART_ENABLE_IT(UART_Cfg->uart_handle,UART_IT_WUF); //开启唤醒中断 HAL_UARTEx_EnableStopMode(UART_Cfg->uart_handle); //开启模式 } else { __HAL_UART_DISABLE_IT(UART_Cfg->uart_handle,UART_IT_WUF); //关闭唤醒中断 HAL_UARTEx_DisableStopMode(UART_Cfg->uart_handle); //关闭模式 } return 1; } ``` ## 通过回调函数来配置时钟 ```c typedef void (*SystemClock_Config_Callback)(void); ``` 声明了一个函数指针类型 在调用时需要传入函数指针 一般是系统时钟配置 也就是`SystemClock_Config` ## 通过虚假的回调来初始化低功耗外设 在低功耗进入前和退出以后 都可以通过把已经打开的外设关掉来降低功耗 关闭: ```c PWR_Device_Init(false); ``` 打开: ```c PWR_Device_Init(true); ``` 同时 在此函数中 也包含GPIO的配置 这两个函数用的虚假回调方式来编写 在调用时 需要自己补全代码 [【STM32笔记】低功耗模式下GPIO省电配置避坑实验(闲置引脚配置为模拟输入其实更耗电)](http://blog.csdn.net/weixin_53403301/article/details/129055530) [【STM32笔记】低功耗模式下GPIO、外设省电配置避坑](http://blog.csdn.net/weixin_53403301/article/details/129060093) ## 初始化函数 这里传参是系统时钟配置函数 当然 你也可以自己写一个 然后就是各个变量的赋值 这里对几个常用变量进行了赋值 ```c void Init_Enter_Low_PWR(SystemClock_Config_Callback SystemClock_Config_Fxn) { uint8_t i=0; memset(&LP_Entry_Cfg,0,sizeof(LP_Entry_Cfg)); LP_Entry_Cfg.SystemClock_Config_Fxn=SystemClock_Config_Fxn; LP_Entry_Cfg.SLEEPEntry_Cfg.SLEEPEntry=PWR_SLEEPENTRY_WFI; LP_Entry_Cfg.STOPEntry_Cfg.STOPEntry=PWR_STOPENTRY_WFI; LP_Entry_Cfg.RTC_Cfg.counter=RTC_WAKEUPCLOCK_RTCCLK_DIV16; for(i=0;i #include #include #include typedef void (*SystemClock_Config_Callback)(void); typedef struct { uint8_t SLEEPEntry; //SLEEPEntry: 一般是 PWR_SLEEPENTRY_WFI 等待中断 也可以是 PWR_SLEEPENTRY_WFE }LOW_POWER_SLEEPEntry_Cfg; typedef struct { uint8_t STOPEntry; //STOPEntry: 一般是 PWR_STOPENTRY_WFI 等待中断 也可以是 PWR_STOPENTRY_WFE }LOW_POWER_STOPEntry_Cfg; typedef struct { uint32_t WakeUpPinPolarity; //WakeUpPinPolarity: 待机模式下WKUP唤醒引脚极性配置,其他模式无用 有的只能配置一个引脚 所以要看数据手册 /* * PWR_WAKEUP_PIN1_HIGH or PWR_WAKEUP_PIN1_LOW * PWR_WAKEUP_PIN2_HIGH or PWR_WAKEUP_PIN2_LOW * PWR_WAKEUP_PIN3_HIGH or PWR_WAKEUP_PIN3_LOW * PWR_WAKEUP_PIN4_HIGH or PWR_WAKEUP_PIN4_LOW * PWR_WAKEUP_PIN5_HIGH or PWR_WAKEUP_PIN5_LOW */ }LOW_POWER_WakeUpPin_Cfg; typedef struct { bool EnableNotDisable; RTC_HandleTypeDef *rtc_handle; uint32_t counter; //RTC计数值 由于进入低功耗模式会有约10ms消抖 所以建议减去这段时间 uint32_t clock; //RTC时钟源 一般是 RTC_WAKEUPCLOCK_RTCCLK_DIV16 }LOW_POWER_RTC_Cfg; typedef struct { bool EnableNotDisable; UART_HandleTypeDef *uart_handle; UART_WakeUpTypeDef UART_WakeUpStruct; //UART唤醒的结构体配置 UART_WakeUpStruct.WakeUpEvent = UART_WAKEUP_ON_READDATA_NONEMPTY 就是接收数据不为空时唤醒 }LOW_POWER_UART_Cfg; typedef struct { LOW_POWER_UART_Cfg UART_Cfg[5]; //串口唤醒配置 有五个串口 所以最大buf长度为5 }LOW_POWER_Device_Cfg; typedef struct { uint8_t mode_flag; // 0/大于4 不进入任何模式,1 进入睡眠,2 进入停止1,3 进入待机,4 关机 LOW_POWER_SLEEPEntry_Cfg SLEEPEntry_Cfg; //进入睡眠模式的方式 LOW_POWER_STOPEntry_Cfg STOPEntry_Cfg; //进入停止模式的方式 LOW_POWER_WakeUpPin_Cfg WakeUpPin_Cfg ; //待机模式的唤醒引脚配置 LOW_POWER_RTC_Cfg RTC_Cfg; //RTC唤醒配置 LOW_POWER_Device_Cfg Device_Cfg; SystemClock_Config_Callback SystemClock_Config_Fxn; // 用于传入退出相关低功耗模式后 需要进行配置的系统时钟配置函数 }LOW_POWER_Entry_Cfg; extern LOW_POWER_Entry_Cfg LP_Entry_Cfg; void GPIO_Reset_Init(bool EnableNotDisable); void PWR_Device_Init(bool EnableNotDisable); uint8_t Ctrl_UART_StopMode_WakeUp(LOW_POWER_UART_Cfg *UART_Cfg,bool EnableNotDisable); void Ctrl_RTC_WakeUp(LOW_POWER_RTC_Cfg *RTC_Cfg,bool EnableNotDisable); void Ctrl_Stop_Mode_WakeUp_Device(bool EnableNotDisable); void Enter_Low_PWR(void); void Init_Enter_Low_PWR(SystemClock_Config_Callback SystemClock_Config_Fxn); #endif ``` ```c #include "stm32l4xx_hal.h" #include "LOW_POWER.h" LOW_POWER_Entry_Cfg LP_Entry_Cfg={0}; /*! * @brief 重置GPIO(都会进行),或再将除外部高低速晶振复用、SWCLK、SWDIO复用的所有GPIO配置为模拟输入(false) * 注意:用于串口唤醒等的引脚,不可配置为模拟输入,也不可关闭 * 在进行GPIO初始化前,先将GPIO_DeInit,但是不做也不影响,不过还是建议跑一下 * 以优先级顺序来看: * 如果这一组GPIO都没用到过 那么直接不开启时钟就最省电 * 如果这一组GPIO有引脚用过了 时钟不能关 那么就将用过的引脚配置为模拟输入 * 切记!!!: * 不要将没用过的引脚配置为模拟输入 耗电量其实会稍微增加一点! * 不要将没用过的GPIO时钟打开以后再配置为模拟输入 耗电量会增加很多 就算配置后再关时钟也没用! * 尽量不要勾选CubeMX中的配置闲置引脚为模拟输入的选项 没用到的时钟还开启了会增加很多耗电 * 低功耗模式配置: * 在进入STOP模式时 GPIO会保留原本的状态 所以把开启后不需要再保留的GPIO配置为模拟输入确实省电 时钟的话不用的肯定关 其他的反正都会关(除了保留的时钟) * 在进入SLEEP模式时 时钟并不会关闭 所以时钟应手动关闭 且将开启后的GPIO配置为模拟输入 * 待机模式和关机模式就更不用在意GPIO口耗电了 * https://blog.csdn.net/weixin_53403301/article/details/129055530 * * @param [in] EnableNotDisable: 使所有GPIO变成模拟输入或不进行模拟配置 * * @return None */ __weak void GPIO_Reset_Init(bool EnableNotDisable) { // HAL_GPIO_DeInit(GPIOA,GPIO_PIN_2|GPIO_PIN_3); //用于串口唤醒的引脚 不可变动 /* HAL_GPIO_DeInit(GPIOA,GPIO_PIN_0|GPIO_PIN_1 |GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7 |GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11 |GPIO_PIN_12|GPIO_PIN_15); HAL_GPIO_DeInit(GPIOB,GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_10 |GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14 |GPIO_PIN_15|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5 |GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9); HAL_GPIO_DeInit(GPIOC,GPIO_PIN_13|GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2 |GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6 |GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10 |GPIO_PIN_11|GPIO_PIN_12); HAL_GPIO_DeInit(GPIOD,GPIO_PIN_2); HAL_GPIO_DeInit(GPIOH,GPIO_PIN_3); */ if(EnableNotDisable) { /* GPIO_InitTypeDef GPIO_InitStruct = {0}; */ /* GPIO Ports Clock Enable */ /* __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOH_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); */ /*Configure GPIO pins : PC13 PC0 PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9 PC10 PC11 PC12 */ /* GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2 |GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6 |GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10 |GPIO_PIN_11|GPIO_PIN_12; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); */ /*Configure GPIO pins : PA0 PA1 PA2 PA3 PA4 PA5 PA6 PA7 PA8 PA9 PA10 PA11 PA12 PA15 */ /* GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1 |GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7 |GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11 |GPIO_PIN_12|GPIO_PIN_15; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); */ // //用于串口唤醒的 不可变动 // GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_3; // GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; // GPIO_InitStruct.Pull = GPIO_NOPULL; // HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /*Configure GPIO pins : PB0 PB1 PB2 PB10 PB11 PB12 PB13 PB14 PB15 PB3 PB4 PB5 PB6 PB7 PB8 PB9 */ /* GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_10 |GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14 |GPIO_PIN_15|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5 |GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); */ /*Configure GPIO pin : PD2 */ /* GPIO_InitStruct.Pin = GPIO_PIN_2; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); */ /*Configure GPIO pin : PH3 */ /* GPIO_InitStruct.Pin = GPIO_PIN_3; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOH, &GPIO_InitStruct); */ } } /*! * @brief 所有外设初始化配置,根据使用需求来写 * * @param [in] EnableNotDisable: 使能或者关闭 * true: 进行初始化外设(不包含时钟初始化) * false: 或者关闭所有外设,所有GPIO配置为无上拉下拉且模拟输入,仅保留系统时钟和系统所需的GPIO口复用 * 该函数在进入低功耗前调用(false) * 建议在进入该函数前(false)先配置用于唤醒的外设 如指定UART或RTC作为唤醒使用 然后再调用该函数 且不能关闭有唤醒功能的外设 * 若用于唤醒后的初始化,则建议先初始化时钟,再执行该函数的初始化(true) * 在休眠期间使用的外设,不要关闭,也不要关闭GPIO等;相反,外设和GPIO等建议同时关闭(避免出现bug,并且也省电) * 未关闭,但唤醒时重复初始化外设并不受影响 * 若未关闭的外设在运行中改变了初始化值,则建议不在唤醒时运行该初始化(前提是外设的GPIO等也没有作改动) * 若需要在初始化后更改初始化值,则建议要么不进行初始化且不关闭(也包括GPIO等),或重新设置新值 * * @return None */ __weak void PWR_Device_Init(bool EnableNotDisable) { if(EnableNotDisable) { //这里是系统最初的初始化值 GPIO_Reset_Init(false); //重置GPIO /* MX_GPIO_Init(); MX_USART2_UART_Init(); MX_UART4_Init(); MX_ADC1_Init(); MX_ADC2_Init(); MX_TIM6_Init(); MX_RTC_Init(); MX_ADC3_Init(); */ //这里放初始化后还要更改的配置,若要重新初始化,建议先运行外设DeInit // HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8,GPIO_PIN_SET); // HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4,GPIO_PIN_SET); } else { /* // HAL_ADC_DeInit(&hadc1); // HAL_ADC_DeInit(&hadc2); // HAL_ADC_DeInit(&hadc3); // HAL_UART_DeInit(&huart2); //唤醒用的串口 最好不要关闭:若不用于唤醒 则可以关闭 GPIO等同步关闭;若用于唤醒 则不能关闭 GPIO等也不能关闭 // HAL_UART_DeInit(&huart4); // HAL_TIM_Base_DeInit(&htim6); // HAL_RTC_DeInit(&hrtc); //唤醒用的RTC 最好不要关闭 */ GPIO_Reset_Init(true); //GPIO配置为复用 } } /*! * @brief 配置串口在停止模式下的唤醒 * * @param [in] UART_Cfg: UART配置 * * @return None */ __weak uint8_t Ctrl_UART_StopMode_WakeUp(LOW_POWER_UART_Cfg *UART_Cfg,bool EnableNotDisable) { if (!UART_Cfg->uart_handle) { return 0; } if(EnableNotDisable) { __HAL_RCC_WAKEUPSTOP_CLK_CONFIG(RCC_STOP_WAKEUPCLOCK_HSI); //保留唤醒用的HSI线 串口初始化时钟也必须要配置为HSI HAL_UARTEx_StopModeWakeUpSourceConfig(UART_Cfg->uart_handle,UART_Cfg->UART_WakeUpStruct); __HAL_UART_ENABLE_IT(UART_Cfg->uart_handle,UART_IT_WUF); //开启唤醒中断 HAL_UARTEx_EnableStopMode(UART_Cfg->uart_handle); //开启模式 } else { __HAL_UART_DISABLE_IT(UART_Cfg->uart_handle,UART_IT_WUF); //关闭唤醒中断 HAL_UARTEx_DisableStopMode(UART_Cfg->uart_handle); //关闭模式 } return 1; } /*! * @brief 配置停止模式下的外设唤醒函数 true为开启 false为关闭 (不包含RTC唤醒) * * @return None */ __weak void Ctrl_Stop_Mode_WakeUp_Device(bool EnableNotDisable) { uint8_t i=0; if(EnableNotDisable) { for(i=0;irtc_handle,RTC_Cfg->counter,RTC_Cfg->clock); } else { __HAL_RTC_WAKEUPTIMER_EXTI_DISABLE_IT(); } } /*! * @brief 进入低功耗模式 * * @return None */ __weak void Enter_Low_PWR(void) { __HAL_RCC_PWR_CLK_ENABLE(); switch(LP_Entry_Cfg.mode_flag) { case 0: { printf("[INFO] 不进入低功耗模式\n"); break; } case 1: { printf("[INFO] 进入睡眠模式\n"); delay_ms(10); //消抖 PWR_Device_Init(false); __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); Ctrl_RTC_WakeUp(&LP_Entry_Cfg.RTC_Cfg,LP_Entry_Cfg.RTC_Cfg.EnableNotDisable); HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON,LP_Entry_Cfg.SLEEPEntry_Cfg.SLEEPEntry); __HAL_RCC_PWR_CLK_ENABLE(); Ctrl_RTC_WakeUp(NULL,false); PWR_Device_Init(true); break; } case 2: { printf("[INFO] 进入停止模式\n"); delay_ms(10); //消抖 PWR_Device_Init(false); __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); Ctrl_Stop_Mode_WakeUp_Device(true); Ctrl_RTC_WakeUp(&LP_Entry_Cfg.RTC_Cfg,LP_Entry_Cfg.RTC_Cfg.EnableNotDisable); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON,LP_Entry_Cfg.STOPEntry_Cfg.STOPEntry); __HAL_RCC_PWR_CLK_ENABLE(); LP_Entry_Cfg.SystemClock_Config_Fxn(); Ctrl_Stop_Mode_WakeUp_Device(false); Ctrl_RTC_WakeUp(NULL,false); PWR_Device_Init(true); break; } case 3: { printf("[INFO] 三秒后进入待机模式\n"); delay_ms(3000); printf("[INFO] 进入待机模式\n"); HAL_PWR_EnableWakeUpPin(LP_Entry_Cfg.WakeUpPin_Cfg.WakeUpPinPolarity); delay_ms(10); //消抖 __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); Ctrl_RTC_WakeUp(&LP_Entry_Cfg.RTC_Cfg,LP_Entry_Cfg.RTC_Cfg.EnableNotDisable); HAL_PWR_EnterSTANDBYMode(); break; } case 4: { printf("[INFO] 三秒后进入关机模式\n"); delay_ms(3000); printf("[INFO] 进入关机模式\n"); HAL_PWR_EnableWakeUpPin(LP_Entry_Cfg.WakeUpPin_Cfg.WakeUpPinPolarity); delay_ms(10); //消抖 __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); Ctrl_RTC_WakeUp(&LP_Entry_Cfg.RTC_Cfg,LP_Entry_Cfg.RTC_Cfg.EnableNotDisable); HAL_PWREx_EnterSHUTDOWNMode(); break; } default: { printf("[INFO] 不进入低功耗模式\n"); break; } } } void Init_Enter_Low_PWR(SystemClock_Config_Callback SystemClock_Config_Fxn) { uint8_t i=0; memset(&LP_Entry_Cfg,0,sizeof(LP_Entry_Cfg)); LP_Entry_Cfg.SystemClock_Config_Fxn=SystemClock_Config_Fxn; LP_Entry_Cfg.SLEEPEntry_Cfg.SLEEPEntry=PWR_SLEEPENTRY_WFI; LP_Entry_Cfg.STOPEntry_Cfg.STOPEntry=PWR_STOPENTRY_WFI; LP_Entry_Cfg.RTC_Cfg.counter=RTC_WAKEUPCLOCK_RTCCLK_DIV16; for(i=0;iLOAD中的值为计数值 计算方法为工作频率值/分频值 比如工作频率/1000 则周期为1ms 以ADuCM4050为例: ```c #include "ADuCM4050.h" void delay_ms(unsigned int ms) { SysTick->LOAD = 26000000/1000-1; // Count from 255 to 0 (256 cycles) 载入计数值 定时器从这个值开始计数 SysTick->VAL = 0; // Clear current value as well as count flag 清空计数值到达0后的标记 SysTick->CTRL = 5; // Enable SysTick timer with processor clock 使能52MHz的系统定时器 while(ms--) { while ((SysTick->CTRL & 0x00010000)==0);// Wait until count flag is set 等待 } SysTick->CTRL = 0; // Disable SysTick 关闭系统定时器 } void delay_us(unsigned int us) { SysTick->LOAD = 26000000/1000/1000-1; // Count from 255 to 0 (256 cycles) 载入计数值 定时器从这个值开始计数 SysTick->VAL = 0; // Clear current value as well as count flag 清空计数值到达0后的标记 SysTick->CTRL = 5; // Enable SysTick timer with processor clock 使能52MHz的系统定时器 while(us--) { while ((SysTick->CTRL & 0x00010000)==0);// Wait until count flag is set 等待 } SysTick->CTRL = 0; // Disable SysTick 关闭系统定时器 } ``` 其中的52000000表示芯片的系统定时器频率 32系列一般为外部定时器频率的两倍 Cortex-M架构SysTick系统定时器阻塞和非阻塞延时 #### 阻塞延时 首先是最常用的阻塞延时 ```c void delay_ms(unsigned int ms) { SysTick->LOAD = 50000000/1000-1; // Count from 255 to 0 (256 cycles) 载入计数值 定时器从这个值开始计数 SysTick->VAL = 0; // Clear current value as well as count flag 清空计数值到达0后的标记 SysTick->CTRL = 5; // Enable SysTick timer with processor clock 使能26MHz的系统定时器 while(ms--) { while ((SysTick->CTRL & 0x00010000)==0);// Wait until count flag is set 等待 } SysTick->CTRL = 0; // Disable SysTick 关闭系统定时器 } void delay_us(unsigned int us) { SysTick->LOAD = 50000000/1000/1000-1; // Count from 255 to 0 (256 cycles) 载入计数值 定时器从这个值开始计数 SysTick->VAL = 0; // Clear current value as well as count flag 清空计数值到达0后的标记 SysTick->CTRL = 5; // Enable SysTick timer with processor clock 使能26MHz的系统定时器 while(us--) { while ((SysTick->CTRL & 0x00010000)==0);// Wait until count flag is set 等待 } SysTick->CTRL = 0; // Disable SysTick 关闭系统定时器 } ``` 50000000表示工作频率 分频后即可得到不同的延时时间 以此类推 那么 不用两个嵌套while循环 也可以写成: ```c void delay_ms(unsigned int ms) { SysTick->LOAD = 50000000/1000*ms-1; // Count from 255 to 0 (256 cycles) 载入计数值 定时器从这个值开始计数 SysTick->VAL = 0; // Clear current value as well as count flag 清空计数值到达0后的标记 SysTick->CTRL = 5; // Enable SysTick timer with processor clock 使能26MHz的系统定时器 while ((SysTick->CTRL & 0x00010000)==0);// Wait until count flag is set 等待 SysTick->CTRL = 0; // Disable SysTick 关闭系统定时器 } void delay_us(unsigned int us) { SysTick->LOAD = 50000000/1000/1000*us-1; // Count from 255 to 0 (256 cycles) 载入计数值 定时器从这个值开始计数 SysTick->VAL = 0; // Clear current value as well as count flag 清空计数值到达0后的标记 SysTick->CTRL = 5; // Enable SysTick timer with processor clock 使能26MHz的系统定时器 while ((SysTick->CTRL & 0x00010000)==0);// Wait until count flag is set 等待 SysTick->CTRL = 0; // Disable SysTick 关闭系统定时器 } ``` 但是这种写法有个弊端 那就是输入ms后,最大定时不得超过计数值,也就是不能超过LOAD的最大值,否则溢出以后,则无法正常工作 而LOAD如果最大是32位 也就是4294967295 晶振为50M的话 50M的计数值为1s 4294967295计数值约为85s 固最大定时时间为85s 但用嵌套while的话 最大可以支持定时4294967295*85s #### 非阻塞延时 如果采用非阻塞的话 直接改写第二种方法就好了: ```c void delay_ms(unsigned int ms) { SysTick->LOAD = 50000000/1000*ms-1; // Count from 255 to 0 (256 cycles) 载入计数值 定时器从这个值开始计数 SysTick->VAL = 0; // Clear current value as well as count flag 清空计数值到达0后的标记 SysTick->CTRL = 5; // Enable SysTick timer with processor clock 使能26MHz的系统定时器 //while ((SysTick->CTRL & 0x00010000)==0);// Wait until count flag is set 等待 //SysTick->CTRL = 0; // Disable SysTick 关闭系统定时器 } void delay_us(unsigned int us) { SysTick->LOAD = 50000000/1000/1000*us-1; // Count from 255 to 0 (256 cycles) 载入计数值 定时器从这个值开始计数 SysTick->VAL = 0; // Clear current value as well as count flag 清空计数值到达0后的标记 SysTick->CTRL = 5; // Enable SysTick timer with processor clock 使能26MHz的系统定时器 //while ((SysTick->CTRL & 0x00010000)==0);// Wait until count flag is set 等待 //SysTick->CTRL = 0; // Disable SysTick 关闭系统定时器 } ``` 将等待和关闭定时器语句去掉 在使用时加上判断即可变为阻塞: ```c delay_ms(500); while ((SysTick->CTRL & 0x00010000)==0); SysTick->CTRL = 0; ``` 在非阻塞状态下 可以提交定时器后 去做别的事情 然后再来等待 不过这样又有一个弊端 那就是定时器会自动重载 可能做别的事情以后 定时器跑过了 然后就要等85s才能停下 故可以通过内部定时器来进行非阻塞延时函数的编写 基本上每个mcu的内部定时器都可以配置自动重载等功能 网上资料很多 这里就不再阐述了