stm32f10x基於freeRTOS的低功耗實現
- 2019 年 10 月 3 日
- 筆記
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引入的比例誤差,還有一個每次喚醒後切換時鐘、操作暫存器的消耗的額外時間,這部分是固定的誤差,可以把線性誤差用KB校準的思路補償回去。
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官網就有。