低功耗模式都是指STM32的停机模式,在该模式下PLL停止工作,仅LSI和HSI继续运行(进休眠前已经打开的话),所有IO状态、内部RAM数据保持不变,所有外部IO中断、内部RTC定时唤醒、LPUART、LPTIM可以将MCU唤醒,唤醒后程序从进入低功耗的地方继续运行,默认刚唤醒后会使用HSI作为系统时钟,因此唤醒后需要立即配置时钟。
一、低功耗串口(LPUART)
STM32L4系列有LPUART,可以在停止模式下直接唤醒MCU。LPUART中断可以设置为接收到1个bit位唤醒或接受到1个byte唤醒,具体可以参考STM32L4参考手册或官方的相关文档,以下是下载链接:
https://www.stmcu.com.cn/Designresource/design_resource_download/file_id/513641/file/DM00355687_ENV1.pdf/token/90db775645fe1ad7c3004bd795aae6d9
STM32系列LPUART资源
LPUART时钟源使用LSE或HSI时从低功耗唤醒
使用32.768KHz的LSE作为LPUART的时钟时,波特率最大只能是9600;使用16MHz的HSI作为LPUART的时钟时,波特率可以比较高,实测波特率为115200时低功耗唤醒是没有问题的,波特率为256000时已经出现首字节丢失的情况。推荐使用HSI作为LPUART的时钟,因为从停止模式唤醒时,MCU默认使用的就是HSI时钟。注意:进入低功耗前需要使能低功耗串口的唤醒功能,不要关闭串口。有篇帖子专门计算了STM32的LPUART的波特率计算:http://www.elecfans.com/lab/MCU/20171031573087.html。
LPUART唤醒的三种方式
从上图可以看出,将MCU从低功耗唤醒有3种匹配方式:
地址匹配唤醒,我也没搞懂这个地址是指什么地址,猜测可能就是接收的第一个字节,只有匹配上了,才会产生WUF中断,否则将重新进入低功耗。
起始位唤醒,显然就是指接收到串口的起始位就立即唤醒,这种方式应该是这三种里唤醒最快的。
字节唤醒,类似于RXNE,收到一个字节后产生WUF唤醒。
注意:进入低功耗前需要使能低功耗串口的唤醒功能,不要关闭串口。仅在使用LSE或HSI作为LPUART时钟源时允许LPUART从STOP模式唤醒,其他时钟源是不可以的。
HAL库提供了几个函数供我们使用:
//1、用于配置LPUART唤醒的方式:地址匹配、起始位唤醒或字节唤醒,需要在进低功耗前配置好 HAL_StatusTypeDef HAL_UARTEx_StopModeWakeUpSourceConfig(UART_HandleTypeDef *huart, UART_WakeUpTypeDef WakeUpSelection); //2、用于开启LPUART的低功耗唤醒功能,需要在进低功耗前开启 HAL_StatusTypeDef HAL_UARTEx_EnableStopMode(UART_HandleTypeDef *huart); //3、用于关闭LPUART的低功耗唤醒功能 HAL_StatusTypeDef HAL_UARTEx_DisableStopMode(UART_HandleTypeDef *huart); //4、LPUART低功耗唤醒中断回调函数,程序运行到这里说明WUF中断标记被置位,MCU被LPUART唤醒 void HAL_UARTEx_WakeupCallback(UART_HandleTypeDef *huart);
优点:无需额外的GPIO,发送端无需关心如何唤醒对端(对端是指有LPUART唤醒的一端)。
缺点:STM32的LPUART资源较少,一个芯片可能只有一个LPUAT外设,有些芯片甚至没有LPUART外设。
二、串口RX作为GPIO中断
此种唤醒方式需要连接通信两端的TX、RX、GND。当通信两端都处于空闲状态时,接收方的RX设为GPIO上拉输入,并检测下降沿中断。当发送方需要发送数据时,先发一些无用的数据,接收方检测到下降沿中断后,将RX引脚重新初始化为串口RX开始接收数据。需要注意的是,这种情况下,接收方一开始接收的可能不是真正想要的数据,需要靠协议解析器去过滤(一般都是接收然后先放入环形buffer,然后在主循环或某一线程中从该buffer中取数据并解析)。
注意:一般接收方(STM32)被唤醒并完成串口初始化所需的时间为1ms左右(RX下降沿中断里直接初始化串口),因此发送方在发送真正的数据前,可以先发1字节0x00,延时一小段时间再发真正的数据,我一般延时2ms,实际等待1ms对方也能正常被唤醒并接收完整的数据,设为2ms比较保险。当然也可以直接在要发送的有效数据前加发几个字节的0x00来唤醒,例如10个0x00,这取决于波特率,这种情况可以使用中断方式发送或者更高效的DMA发送。被唤醒方最好加个标记位来指示当前串口是否已经处于打开状态,避免重复初始化。
优点:不要求接收方的串口是低功耗串口,而且节省IO。
缺点:需要改变发送方的发送逻辑。
三、使用额外的GPIO唤醒
这种方式实际上与串口RX作为GPIO中断唤醒是差不多的,只不过使用了额外的GPIO作为唤醒IO。建议串口空闲时,两边的唤醒IO都设为上拉输入检测下降沿中断,当发送方需要发送数据时,将该GPIO设为推挽输出,反复拉高拉低几次唤醒对端,发送完后将该引脚重新设为上拉输入检测下降沿中断。如果引脚足够多,也可以使用两组唤醒IO,每一组指定了一个唤醒方向,即引脚1用于A唤醒B,引脚2用于B唤醒A。
建议使用一个标志位来记录串口的使能状态,当需要发送数据时,初始化一下串口(如果标志位已经置位,则无需重新初始化,下同)。同理,当收到唤醒中断时,建议直接在中断里根据标志位初始化一下串口,因为在裸机的情况下,如果使用中断标志来通知串口需要重新初始化,可能会由于主循环里某个函数有阻塞导致串口没有及时初始化而丢包;如果使用RTOS的信号量来通知,则等待该信号量的线程的优先级应该尽量高。
优点:使用起来方便,无需修改串口引脚的设置。
缺点:使用了额外的GPIO,发送方需要修改发送逻辑,发送时需要延时。
四、唤醒方案优化
对于非低功耗串口(非LPUART,即普通的UART),发送方每次都要通过拉低电平或发送无用来唤醒接收端,这里的延时对于整个系统来说,无疑是浪费。这里提供一些方法来规避这个问题,但一定程度上增加了通信双方的耦合性,只能作为参考。
发送方维护一个SendTick变量,接收方维护一个RecvTick变量。当发送方每次发送完数据后,都更新一下SendTick(例如用HAL_GetTick函数),接收方收到一帧完整的数据后,也更新一下RecvTick。接收方如果发现PastTick(RecvTick)大于100ms,就关闭串口,PastTick函数即当前时刻和入参时间的差值;发送方如果发现PastTick(SendTick)小于80ms,就不再进行唤醒操作而是直接发送串口数据。这样做的好处:如果发送方以较快频率连续发送数据包,那么只用唤醒一次,减少了频繁唤醒所需的时间。
以上例子只是A发B收的情况,实际上A和B都可能收发,那么双方都要维护两个变量SendTick、RecvTick,并约定好自动休眠的时间。
五、代码案例
我项目中用到的是STM32L4系列的MCU,以标准库为例,配置URAT3可以低功耗唤醒:
HAL_UARTEx_EnableStopMode函数的说明:只要UART时钟为HSI或LSE,UART就能将MCU从停止1模式唤醒,所以第一部需要先配置串口。
//1.在串口初始化函数中增加以下代码 UART_WakeUpTypeDef WakeUpSelection; WakeUpSelection.WakeUpEvent = UART_WAKEUP_ON_STARTBIT;//起始位唤醒 HAL_UARTEx_StopModeWakeUpSourceConfig(&LPUart3Handle WakeUpSelection); __HAL_UART_ENABLE_IT(&LUart3Handle UART_IT_WUF); //唤醒中断 LPUart3Handle.Instance->CR3 |= 0x0800000; //打开stop时的时钟
//2.在HAL_UART_MspInit函数中配置串口时钟源 /* Enable GPIO TX/RX clock */ __HAL_RCC_GPIOC_CLK_ENABLE(); /*##-1- Enable the HSI clock #*/ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; if (HAL_RCC_OscConfig(&RCC_OscInitStruct)!= HAL_OK){ /* Error */ while(1); } /*##-2- Configure SYSCLK as USART clock source #*/ __HAL_RCC_USART3_CONFIG(RCC_USART3CLKSOURCE_HSI);//此处配置时钟源为HSI,需在上方使能HSI时钟 /* Enable USARTx clock */ __HAL_RCC_USART3_CLK_ENABLE(); /* Enable DMA clock */ __HAL_RCC_DMA1_CLK_ENABLE();
//3.单片机进入低功耗模式可以自定义3个函数: void SYSCLKConfig_WAKEUP_From_STOP1(void){//唤醒后需立即配置时钟 RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_OscInitTypeDef RCC_OscInitStruct = {0}; uint32_t pFLatency = 0; /* Enable Power Control clock */ __HAL_RCC_PWR_CLK_ENABLE(); /* Get the Oscillators configuration according to the internal RCC registers */ HAL_RCC_GetOscConfig(&RCC_OscInitStruct); /* Enable PLL */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_NONE; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /* Get the Clocks configuration according to the internal RCC registers */ HAL_RCC_GetClockConfig(&RCC_ClkInitStruct, &pFLatency); /* Select PLL as system clock source and keep HCLK, PCLK1 and PCLK2 clocks dividers as before */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, pFLatency) != HAL_OK) { Error_Handler(); } } void Enter_STOP_Init(void){//初始化 /* Enable Power Clock */ __HAL_RCC_PWR_CLK_ENABLE(); /* Ensure that MSI is wake-up system clock */ __HAL_RCC_WAKEUPSTOP_CLK_CONFIG(RCC_STOP_WAKEUPCLOCK_MSI); /* Disable all used wakeup source */ HAL_RTCEx_DeactivateWakeUpTimer(&RtcHandle); /* Re-enable wakeup source */ /* ## Setting the Wake up time ############################################*/ /* RTC Wakeup Interrupt Generation: the wake-up counter is set to its maximum value to yield the longuest stop time to let the current reach its lowest operating point. The maximum value is 0xFFFF, corresponding to about 33 sec. when RTC_WAKEUPCLOCK_RTCCLK_DIV = RTCCLK_Div16 = 16 Wakeup Time Base = (RTC_WAKEUPCLOCK_RTCCLK_DIV /(LSI)) Wakeup Time = Wakeup Time Base * WakeUpCounter = (RTC_WAKEUPCLOCK_RTCCLK_DIV /(LSI)) * WakeUpCounter ==> WakeUpCounter = Wakeup Time / Wakeup Time Base To configure the wake up timer to 60s the WakeUpCounter is set to 0xFFFF: Wakeup Time Base = 16 /(~32.000KHz) = ~0.5 ms Wakeup Time = 0.5 ms * WakeUpCounter Therefore, with wake-up counter = 0xFFFF = 65,535 Wakeup Time = 0,5 ms * 65,535 = 32,7675 ms =~ 33 sec. */ HAL_RTCEx_SetWakeUpTimer_IT(&RtcHandle, 2047, RTC_WAKEUPCLOCK_RTCCLK_DIV16); //1s中断唤醒一次。 } void EnterSTOP1Mode(void){ HAL_UARTEx_EnableStopMode(&Uart3Handle);//启用UART停止模式 HAL_PWREx_EnterSTOP1Mode(PWR_STOPENTRY_WFI);//MCU进入停止模式1 SYSCLKConfig_WAKEUP_From_STOP1();//退出低功耗模式后需要手动配置时钟 }
参考文档:
发表吐槽
你肿么看?
既然没有吐槽,那就赶紧抢沙发吧!