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函数即可,简单一点就是直接修改宏定义即可。
1 /* #define SYSCLK_FREQ_HSE HSE_VALUE */
2 #define SYSCLK_FREQ_24MHz 24000000
3 #else
4 /* #define SYSCLK_FREQ_HSE HSE_VALUE */
5 /* #define SYSCLK_FREQ_24MHz 24000000 */
6 #define SYSCLK_FREQ_36MHz 36000000
7 /* #define SYSCLK_FREQ_48MHz 48000000 */
8 /* #define SYSCLK_FREQ_56MHz 56000000 */
9 // #define SYSCLK_FREQ_72MHz 72000000
10 #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机制。
- 设计低功耗模式下的唤醒源,并确保定时精度,注意唤醒前、后的处理。
- 适当降低系统主频,有利于降低系统正常工作时的功耗。
实际的工作功耗如下:
- 基准功耗: 72M + systick 1ms,65mA.
- 降频功耗:36M + systick 1ms , 45mA.
- 增加IDLE HOOK,36M + systick 1ms, 37mA.
- 增加tickless mode, 36M + systick 1ms, 28mA.
- 增加tick less mode , 36M + RTC 1ms, 最低值5.5mA,最大值30mA,平均约10mA,满足项目需求 < 17mA。
显然,基于systick的tickless mode 无法降低MCU消耗的功耗,而采用stop模式后功耗降低非常明显。
6. 参考资料
(1) 适用于stm32的freertos版本,官网可以下载到。
(2) stm32数据手册中文版,ST官网就有。