0. 写在前面
没有太多时间更新, 可能偶尔有时间就更新一些.
因为突然有项目用到了 stm32f10x 系列并且是电池驱动的, 所以需要对功耗进行优化, 其他 CM3 核心系列应该也同样适用.
1. 背景
Stm32 的低功耗模式, 参考手册中写了有若干种模式, 最方便的是 Sleep 模式 (恢复快),Stop 模式 (省电且数据可以保存).
FreeRtos 的低功耗设计, 可以通过实现 tickless 模式, IDLE hook 实现.
此前网络上关于 FreeRtos 中 tickless 模式, 一般都是基于 systick + sleep 模式处理的, 功耗设计并不是最优. 本文提出的 tickless 方案是 RTC 闹钟中断 +Stop 模式 , 但是需要一定的校准 (stm32 内置了 LSI 校准方案, 但是很少见到相关资料).
2. 设计思路
降低功耗, 首先是有一个既定的目标功耗范围, 状态. 低功耗的实现途径:
(1) 休眠, 也就是没有工作任务时降低系统可以休眠, 有工作任务时通过唤醒机制唤醒 MCU 立刻工作. 对于 freeRtos 来说, 可以通过 IDLE hook ,tickless mode 一起实现.
需要分析清楚, 系统何时可以进行休眠, 休眠的时间范围是什么样子的, 保证性能的前提下最大限制的进行休眠.
避免系统被频繁唤醒, 可以把一些耗时的工作同步下一起完成.
(2) 降低工作时的耗电.
(a). MCU 节电: 降频, 关闭不用的外设端口, 合理设置端口状态.
(b): 外围电路节电: 通过 MCU 控制外设的工作模式或者控制电源实现.
3. 实现方案
3.1 分析
(1)FreeRtos 中 IDLE hook 中采用 sleep 模式进行休眠节能.
(2) 长时间不工作时 (比如 10ms 或者以上), 可以通过 stop 模式唤醒, 该方式依赖于 freeRtos 的 tickless mode, 但是 stop 模式下需要外部中断或者 RTC 的闹钟中断, 对于本项目的系统, 通过 RTC 闹钟中断唤醒实现是最好的, 不需要依赖于外部唤醒触发.
(3) 降低系统主频.(默认 72Mhz, 通过分析 36Mhz 也够用了).
3.2 实现
(1) 降频.
修改 system_stm32f10x.c 中的 SetSysClock 函数即可, 简单一点就是直接修改宏定义即可.
- /* #define SYSCLK_FREQ_HSE HSE_VALUE */
- #define SYSCLK_FREQ_24MHz 24000000
- #else
- /* #define SYSCLK_FREQ_HSE HSE_VALUE */
- /* #define SYSCLK_FREQ_24MHz 24000000 */
- #define SYSCLK_FREQ_36MHz 36000000
- /* #define SYSCLK_FREQ_48MHz 48000000 */
- /* #define SYSCLK_FREQ_56MHz 56000000 */
- // #define SYSCLK_FREQ_72MHz 72000000
- #endif
- (2) 实现 IDLE HOOK.
- IDLE HOOK 主要是系统短时间空闲时使用, 简单来说就是系统有个优先级最低的任务 IDLE, 这个任务中可以调用一个用户定义的函数. 只需要在 FreeRTOSConfig.h 中定义 IDLE hook 相关函数即可.
- 对于本系统来说, FreeRtos 中自带了定时中断, 可以随时唤醒系统, 因此可以使用在 IDLE 中进行休眠, 并且可以随时可以被定时中断唤醒.
- /*FreeRTOSConfig 中增加如下定义 */
- #define configUSE_IDLE_HOOK 1
然后在工程中任意文件中实现 vApplicationIdleHook 函数即可.
- void EnterSleepMode(void)
- {
- SCB->SCR &= ~(SCB_SCR_SLEEPDEEP_Msk);
- __WFI();
- }
- void vApplicationIdleHook(void)
- {
- EnterSleepMode();
- }
(3) 实现 tickless mode.
为了方便处理, 需要把 systick 中断中的内容迁移到 RTC 的秒中断中, 并且通过 RTC 的闹钟中断实现 tickless mode 需要的进入休眠状态.
通过查看手册, stop 模式下 LSI 时钟是可以工作的, 需要用 LSI 时钟作为 RTC 的时钟输入. 但是 LSI 时钟有个问题, 就是不保证精度 (40Khz, 但是实际上大约是 30k ~ 60K 都有可能). 本项目的硬件配备了外部晶振, 因此可以使用外部晶振对 RTC 进行校正 (后面会专门说明).
首先要开启 tickless mode 机制, 还是 freeRTOSConfig.h 中修改 configUSE_TICKLESS_IDLE.
- #define configUSE_TICKLESS_IDLE 1
- extern void PreSleepProcessing(uint32_t ulExpectedIdleTime);
- extern void PostSleepProcessing(uint32_t ulExpectedIdleTime);
- #define configPRE_SLEEP_PROCESSING(x) PreSleepProcessing(x)
- #define configPOST_SLEEP_PROCESSING(x) PostSleepProcessing(x)
systick 中断迁移到 RTC 中断的代码及校准代码网上都有, 这里只说明如何通过闹钟中断实现 tickless mode 的重要函数: vPortSuppressTicksAndSleep ( port.c 中)
- extern void RTC_Disable_Tick_Int(void); // 是自己实现的,
- extern void RTC_Enable_Tick_Int(void);
- extern void RTC_SetCounter(unsigned int ulValue);
- extern unsigned int RTC_GetCounter(void);
- __weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
- {
- uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements;
- TickType_t xModifiableIdleTime;
- // RTC Alarm 的最大毫秒数. 系统的 systick freq = 1000hz, 周期 1ms.
- #define RTC_MAX_ALARM_MS ((uint32_t)(0xFFFFFFFF) / 10)
- if( xExpectedIdleTime> RTC_MAX_ALARM_MS )
- {
- xExpectedIdleTime = RTC_MAX_ALARM_MS;
- }
- RTC_Disable_Tick_Int(); // 禁止秒中断.
- // 注: 原代码中是使用 system tick 中断实现了响应的调度. 这里不需要了.
- ulReloadValue = xExpectedIdleTime - 1 ;
- if( ulReloadValue> ulStoppedTimerCompensation )
- {
- ulReloadValue -= ulStoppedTimerCompensation;
- }
- /* Enter a critical section but don't use the taskENTER_CRITICAL()
- method as that will mask interrupts that should exit sleep mode. */
- __disable_irq();
- __dsb( portSY_FULL_READ_WRITE );
- __isb( portSY_FULL_READ_WRITE );
- /* If a context switch is pending or a task is waiting for the scheduler
- to be unsuspended then abandon the low power entry. */
- if( eTaskConfirmSleepModeStatus() == eAbortSleep )
- {
- /* Restart from whatever is left in the count register to complete
- this tick period. */
- //portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG;
- /* Restart SysTick. */
- //portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
- RTC_Enable_Tick_Int();
- /* Reset the reload register to the value required for normal tick
- periods. */
- // portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
- /* Re-enable interrupts - see comments above __disable_irq() call
- above. */
- __enable_irq();
- }
- else
- {
- /* Set the new reload value. */
- //portNVIC_SYSTICK_LOAD_REG = ulReloadValue;
- /* Clear the SysTick count flag and set the count value back to
- zero. */
- //portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
- /* Restart SysTick. */
- // portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
- xModifiableIdleTime = xExpectedIdleTime;
- // 休眠之前的处理.
- configPRE_SLEEP_PROCESSING( xModifiableIdleTime );
- if( xModifiableIdleTime> 0 )
- {
- __dsb( portSY_FULL_READ_WRITE );
- __wfi();
- __isb( portSY_FULL_READ_WRITE );
- }
- // 休眠之后的处理.
- configPOST_SLEEP_PROCESSING( xExpectedIdleTime );
- //. 启动秒中断.
- RTC_Enable_Tick_Int();
- /* Re-enable interrupts to allow the interrupt that brought the MCU
- out of sleep mode to execute immediately. see comments above
- __disable_interrupt() call above. */
- __enable_irq();
- __dsb( portSY_FULL_READ_WRITE );
- __isb( portSY_FULL_READ_WRITE );
- /* Disable interrupts again because the clock is about to be stopped
- and interrupts that execute while the clock is stopped will increase
- any slippage between the time maintained by the RTOS and calendar
- time. */
- __disable_irq();
- __dsb( portSY_FULL_READ_WRITE );
- __isb( portSY_FULL_READ_WRITE );
- // 检查是否提前返回.
- // 检查方法是查看 RTC Alarm 的 Counter 是否达到了设定值. 处于调试模式时. 仿真器会频繁唤醒 MCU 导致 MCU 提前从休眠中唤醒.
- // 若提前唤醒. 则重新根据剩余休眠时间确认是否有必要再次执行休眠过程.
- // 由于目前软件上只设计了 RTC 唤醒源. 这样做没有问题.
- // !!!!!!!!!!! 如果还有其他外部唤醒源. 则该方法会导致调度周期 / 休眠周期异常.
- /* 重新启动秒中断 */
- // 系统的时钟调整 (对应任务执行调整).
- ulCompleteTickPeriods = RTC_GetCounter();//xExpectedIdleTime - 1UL;
- // 系统的时间向前推进一点 (保证休眠任务的休眠时间被正确处理)
- vTaskStepTick( ulCompleteTickPeriods );
- /* Exit with interrpts enabled. */
- __enable_irq();
- }
- }
其中:
PreSleepProcessing 和 PostSleepProcessing 实现如下:
- void PreSleepProcessing(uint32_t ulExpectedIdleTime)
- {
- // 关闭耗电外设.
- // ADC1_Disable();
- // ADC2_Disable();
- // 清除相关的 RTC 标志位.
- RTC->CRL &= ~(RTC_CRL_ALRF);
- RTC->CRL &= ~(RTC_CRL_SECF);
- PWR->CR |= PWR_CR_CWUF;
- SCB->SCR |= (SCB_SCR_SLEEPDEEP_Msk);
- PWR->CR &= ~PWR_CR_PDDS;
- PWR->CR &= ~PWR_CR_LPDS;
- // SCB->SCR |= (SCB_SCR_SLEEPONEXIT_Msk);
- SetRTCAlarm(ulExpectedIdleTime); // tick = 1ms.
- }
- void PostSleepProcessing(uint32_t ulExpectedIdleTime)
- {
- // 清零.
- SCB->SCR &= ~(SCB_SCR_SLEEPDEEP_Msk);
- StopRtcAlarm(); // 停止 RTC 闹钟.
- //SystemInit(); // 使用 SystemInit 更安全. 会重设系统的时钟配置.(晶振 / PLL / 总线时钟).
- SetSysClock(); // 恢复系统时钟. stop 模式唤醒后默认用的 HSI.
- // 开启关闭的外设.
- // ADC1_Enable();
- // ADC2_Enable();
- }
(4) 合理设计系统的工作时间.
尽量保证系统的任务设计中有机会进入休眠状态, 否则 tickless mode 就没有意义了. 这里跟具体业务有关, 就不说太多了.
简单总结一句: 想办法多调用 vTaskDelay, 越多越好.
4. 校准与补偿
LSI 是不准确的, 需要在系统上电时通过外部晶振 / PLL 进行校, 具体校准方法可以参考官方手册 (TIM5_CH4 + AFIO).
除了 LSI 引入的比例误差, 还有一个每次唤醒后切换时钟, 操作寄存器的消耗的额外时间, 这部分是固定的误差, 可以把线性误差用 K\B 校准的思路补偿回去.
RTC 的相关函数比较杂乱没有整理, 就贴一个校准的函数大致看下思路吧, 其他的辅助函数就不贴了.
- #define LSI_INTERVAL_VALID_CNT 10 // 间隔稳定的情况.(实际情况下 LSI 时钟的前面若干周期不稳定).
- #define MAX_CALIB_TIME_CNT 50 // 总周期.
- u8 g_ucIntCnt = 0;
- u16 g_ausTcnt[MAX_CALIB_TIME_CNT];
- // 校准用. 36M 时钟采集 40k 时钟, 得到的数值应该在 900 附近.
- u32 g_ulLsiTicksHse = 0;
- void StartLsiCalib(void)
- {
- // (1) 使能 LSI 时钟.
- RCC->CSR |= RCC_CSR_LSION; // 启动 LSI.(LSION,bit: 0). 打开 LSI.
- while((RCC->CSR & RCC_CSR_LSIRDY) == 0); // 等待 LSI 时钟稳定.
- // (2) 启动 TIM5-CH4. 通过 CH4 设置 LSI 时钟的输入捕获.
- // 打开 TIM5 的电源.
- RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;
- // 打开 AFIO 的时钟.
- RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
- // 映射 LSI 时钟到 TIM5_CH4. 以完成捕获.
- AFIO->MAPR |= AFIO_MAPR_TIM5CH4_IREMAP;
- // TIM5_CH4 配置为输入捕获.
- TIM5->ARR = 0xFFFF; //
- TIM5->PSC = 0; // 越精确越好. 系统时钟为 36MHz.(参考 startup->system_stm32f10x.c).
- TIM5->CR1 = 0x00; // 设置好时钟.
- TIM5->CCMR2 = (0x01 <<8); //.CC4 映射在 TI4 上. 配置为输入捕获.
- TIM5->CCER |= (0x01 <<12); // 使能输入捕获.
- // 使能 TIM5 的中断 - 代码是 copy 的例子, 没有整理.
- u8 tmppriority = (0x700 - ((SCB->AIRCR) & (uint32_t)0x700))>> 0x08;
- u8 tmppre = (0x4 - tmppriority);
- u8 tmpsub = tmpsub>> tmppriority;
- tmppriority = (uint32_t)0 <<tmppre;
- tmppriority |= 0 & tmpsub;
- tmppriority = tmppriority << 0x04;
- NVIC->IP[TIM5_IRQn] = tmppriority;
- NVIC->ISER[TIM5_IRQn>> 0x05] =
- (uint32_t)0x01 <<(TIM5_IRQn & (uint8_t)0x1F);
- TIM5->CR1 |= 0x01; // 启动 TIM5.
- TIM5->SR = 0;
- TIM5->DIER = 0x10;
- while(g_ucIntCnt <MAX_CALIB_TIME_CNT); // 等待一定的 LSI 时钟周期.
- //. 关闭 TIM5.
- TIM5->CCER &= ~(0x01 <<12); // 禁止输入捕获.
- TIM5->DIER = 0; // 禁止捕获中断.
- TIM5->CR1 = 0; // 禁止 TIM5.
- // 矫正晶振精度数据.
- u32 ulTotalSum = 0;
- for(int i = LSI_INTERVAL_VALID_CNT ;i <MAX_CALIB_TIME_CNT;++i)
- {
- ulTotalSum += g_ausTcnt[i];
- }
- g_ulLsiTicksHse = (ulTotalSum+((MAX_CALIB_TIME_CNT-LSI_INTERVAL_VALID_CNT)/2)) / (MAX_CALIB_TIME_CNT-LSI_INTERVAL_VALID_CNT);
- // 取消映射.
- AFIO->MAPR &= ~AFIO_MAPR_TIM5CH4_IREMAP;
- // 关闭 TIM5 的电源.
- RCC->APB1ENR &= ~RCC_APB1ENR_TIM5EN;
- // 关闭 AFIO 的电源.
- RCC->APB2ENR &= ~RCC_APB2ENR_AFIOEN;
- }
- // TIME5 CH4 中断.
- void TIM5_IRQHandler(void)
- {
- static u8 s_ucFirstEnter = 0;
- static u16 s_usLastTick = 0;
- u16 usCurTicks = 0;
- if (s_ucFirstEnter == 0)
- {
- s_ucFirstEnter = 1;
- s_usLastTick = TIM5->CCR4;
- }
- else if (g_ucIntCnt <MAX_CALIB_TIME_CNT)
- {
- usCurTicks = TIM5->CCR4;
- g_ausTcnt[g_ucIntCnt++] = (usCurTicks> s_usLastTick) ? (usCurTicks - s_usLastTick) : (usCurTicks + 65535 - s_usLastTick);
- s_usLastTick = usCurTicks;
- }
- // 清零中断标记.
- TIM5->SR = ~(0x10);
- }
得到的 g_ulLsiTicksHse 的数值应该在 900 附近 (主频 36M,LSI 是 40K), 实际计算休眠时间的 tick 数, 应该乘以 (900 / g_ulLsiTicksHse) 进行修正.
时间误差测试任务如下:
- void Task_LedCtl(void * pData)
- {
- if(pData){}
- for(;;)
- {
- TestLed(); // 翻转 GPIO.
- vTaskDelay(25);// 延时可以调整得到不同时间下的处理误差. 比如: 25/50/100/250 等.
- }
- }
通过示波器采集一个固定的任务处理 GPIO 的时间, 得到系统实际误差数据如下:
/*
* 测试点 实际测量值 (ms)
* 500 503.6
* 250 253.6
* 100 103
* 50 52.8
*
* 经过分析: 线性误差 K = 0.9982. 误差约 0.18%, 应该是整数计算引入的. 通过修改 tick 计算方式得到了解决.
* B = 2.85/2 = 1.425ms, 属于系统唤醒恢复设置等引入的误差. 需要补偿进去.
* 经过验证. 校准后定时相对误差 < 0.1%. 满足使用需求.
*/
经过以上校准之后, 系统的时钟基本是准确的, 满足使用需求了.
实际休眠时间 Tx = t0 * K + B, 其中 T0 是预期的休眠时间, K/B 通过上面的方法计算得到.
这里的 B 值, 也会影响另外一个重要参数, 这个时间必须必上述的 B 值大很多才有意义.
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 8 // 系统超出空闲时允许休眠.
5. 小结
功耗设计实现, 需要:
合理的设计任务休眠时间, 如果每时每刻都有任务在运行, 系统是没办法进入低功耗模式的.
理解 IDLE Hook 机制跟 Tickless 机制.
设计低功耗模式下的唤醒源, 并确保定时精度, 注意唤醒前, 后的处理.
适当降低系统主频, 有利于降低系统正常工作时的功耗.
6. 参考资料
(1) 适用于 stm32 的 freertos 版本, 官网可以下载到.
(2) stm32 数据手册中文版, ST 官网就有.
来源: https://www.cnblogs.com/handsoul/p/11496322.html