前言
前不久,我有位做測試的朋友轉去做開發的工作,面試遇到了一個問題,他沒明白,打電話問了我。題目大概就是:
在微控制器裸機開發時,微控制器要處理多個任務,此時你的程式框架是怎樣的呢?
這其實是個經典面試問題,我以前面試也被問過。
答案一:輪詢系統程式碼結構如:
int main(void){ init_something(); while(1) { do_something1(); do_something2(); do_something3(); }}
這種結構大概是我們初學微控制器的時候的程式碼結構。在沒有外部事件驅動時,可以較好使用。
只答出了這種情況,印象分估計會比較低,多半涼涼。
答案二:前後臺系統程式碼結構如(該程式碼來自 《RT-Thread核心實現與應用開發實踐指南》 ):
int flag1 = 0;int flag2 = 0;int flag3 = 0;int main(void){ /* 硬體相關初始化 */ HardWareInit(); /* 無限迴圈 */ for (;;) { if (flag1) { /* 處理事情 1 */ DoSomething1(); } if (flag2) { /* 處理事情 2 */ DoSomethingg2(); } if (flag3) { /* 處理事情 3 */ DoSomethingg3(); } }}void ISR1(void){ /* 置位標誌位 */ flag1 = 1; /* 如果事件處理時間很短,則在中斷裡面處理 如果事件處理時間比較長,在回到後臺處理 */ DoSomething1();}void ISR2(void){ /* 置位標誌位 */ flag2 = 2; /* 如果事件處理時間很短,則在中斷裡面處理 如果事件處理時間比較長,在回到後臺處理 */ DoSomething2();}void ISR3(void){ /* 置位標誌位 */ flag3 = 1; /* 如果事件處理時間很短,則在中斷裡面處理 如果事件處理時間比較長,在回到後臺處理 */ DoSomething3();}
此處,中斷稱為前臺,main中的while迴圈稱為後臺。相比於迴圈系統,這種方式相對可以提高外部事件的實時響應能力。
可以回答出這種情況,印象分大概一半以上,會再細問。
答案三:升級版前後臺系統(軟體定時器法)以前,學C語言時,常常聽到有人說:指標是C語言的靈魂,沒學會指標就是沒學會C語言。。
後來,學微控制器時,又聽到有人說:中斷和定時器是微控制器的靈魂,沒掌握中斷與定時器就沒學會微控制器。。
大佬們都那麼說了,那就拿定時器來搞點事情。定時器渾身都是寶,本篇筆記我們來介紹使用定時器(系統滴答定時器或者其它定時器)來做的裸機框架。軟體定時器法也有另一種說法:時間片輪詢法。
可以回答出這種情況,這場面試多半穩了。
下面以STM32微控制器為例看看這種方法的使用。
站在巨人的肩膀上開源專案—— MultiTimer ,專案倉庫地址:
https://github.com/0x1abin/MultiTimer
1、MultiTimer 簡介MultiTimer 是一個軟體定時器擴充套件模組,可無限擴充套件你所需的定時器任務,取代傳統的標誌位判斷方式, 更優雅更便捷地管理程式的時間觸發時序。
2、MultiTimer 的demo#include "multi_timer.h"struct Timer timer1;struct Timer timer2;void timer1_callback(){ printf("timer1 timeout!\r\n");}void timer2_callback(){ printf("timer2 timeout!\r\n");}int main(){ timer_init(&timer1, timer1_callback, 1000, 1000); //1s loop timer_start(&timer1); timer_init(&timer2, timer2_callback, 50, 0); //50ms delay timer_start(&timer2); while(1) { timer_loop(); }}void HAL_SYSTICK_Callback(void){ timer_ticks(); //1ms ticks}
3、MultiTimer 的移植、剖析想要對MultiTimer 進行深入學習可閱讀專案原始碼及如下這篇文章:MultiTimer,一款可無限擴充套件的軟體定時器
自己動手,豐衣足食1、程式碼模板準備一個定時器,可以是系統滴答定時器,也可以是TIM定時器。使用這個定時器拓展出多個軟體定時器。比如我們系統中有三個任務:LED翻轉、溫度採集、溫度顯示。此時我們可以使用一個硬體定時器拓展出3個軟體定時器,定義如下宏定義:
#define MAX_TIMER 3 // 最大定時器個數EXT volatile unsigned long g_Timer1[MAX_TIMER]; #define LedTimer g_Timer1[0] // LED翻轉定時器#define GetTemperatureTimer g_Timer1[1] // 溫度採集定時器#define SendToLcdTimer g_Timer1[2] // 溫度顯示定時器#define TIMER1_SEC (1) // 秒#define TIMER1_MIN (TIMER1_SEC*60) // 分
在定時器初始化的時候也順便給三個軟體定時器進行初始化操作:
/********************************************************************************************************** 函式: TIM1_Init, 通用定時器1初始化**------------------------------------------------------------------------------------------------------** 引數: arr:自動重灌值 psc:時鐘預分頻數** 說明: 定時器溢位時間計算方法:Tout=((arr+1)*(psc+1))/Ft** 返回: void ********************************************************************************************************/void TIM1_Init(uint16_t arr, uint16_t psc){ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); /* 定時器TIM1初始化 */ TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler =psc; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_RepetitionCounter=0; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); TIM_ClearFlag(TIM1,TIM_FLAG_Update ); /* 中斷使能 */ TIM_ITConfig(TIM1,TIM_IT_Update, ENABLE ); /* 中斷優先順序NVIC設定 */ NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM1, ENABLE); // 全域性定時器初始化 for(int i = 0; i < MAX_TIMER; i++) { g_Timer1[i] = 0; }}
在定時器中斷中對這些軟體定時器進行定時值做遞減操作:
/********************************************************************************************************** 函式: TIM1_IRQHandler, 定時器1中斷服務程式**------------------------------------------------------------------------------------------------------** 引數: 無** 返回: 無 ********************************************************************************************************/void TIM1_UP_IRQHandler(void) //TIM1中斷{ uint8 i; if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) // 檢查TIM1更新中斷髮生與否 { //------------------------------------------------------------------------------- // 各種定時間器計時 for (i = 0; i < MAX_TIMER; i++) // 定時時間遞減 if( g_Timer1[i] ) g_Timer1[i]-- ; TIM_ClearITPendingBit(TIM1, TIM_IT_Update); //清除TIMx更新中斷標誌 }}
我們在各個定時任務中給這些軟體定時器賦予定時值,這些定時值遞減到0則該任務會被觸發執行,比如:
void Task_Led(void){ //---------------------------------------------------------------- // 等待定時時間 if(LedTimer) return; LedTimer = 1 * TIMER1_SEC; //---------------------------------------------------------------- // LED任務主體 LedToggle();}void Task_GetTemperature(void){ //---------------------------------------------------------------- // 等待定時時間 if(GetTemperatureTimer) return; GetTemperatureTimer = 2 * TIMER1_SEC; //---------------------------------------------------------------- // 溫度採集任務主體 GetTemperature();}void Task_SendToLcd(void){ //---------------------------------------------------------------- // 等待定時時間 if(SendToLcdTimer) return; SendToLcdTimer = 2 * TIMER1_SEC; //---------------------------------------------------------------- // 溫度顯示任務主體 LcdDisplay();}
如此一來,每過1、2、4秒則分別觸發LED翻轉任務、溫度採集任務、溫度顯示任務。
這裡配置的最小定時單位為1秒,當然根據實際需要進行配置(定時器初始化),定時器初始化可以放在系統統一初始化函數里:
/********************************************************************************************************** 函式: SysInit, 系統上電初始化**------------------------------------------------------------------------------------------------------** 引數: ** 說明: ** 返回: ********************************************************************************************************/void SysInit(void){ CpuInit(); // 配置系統資訊函式 SysTickInit(); // 系統滴答定時器初始化函式 UsartInit(115200); // 串列埠初始化函式,波特率115200 TIM1_Init(2000-1, 36000-1); // 定時週期1s LedInit(); // Led初始化 TemperatureInit(); // 溫度感測器初始化 LcdInit(); // LCD初始化}
此時我們的main函式就可以設計為:
int main(void){ //----------------------------------------------------------------------------------------------- // 上電初始化函式 SysInit(); //----------------------------------------------------------------------------------------------- // 主程式 while (1) { //----------------------------------------------------------------------------------------------- // 定時任務 Task_Led(); Task_GetTemperature(); Task_SendToLcd(); }}
主函式主要是進行系統上電的一些初始化操作,接著是呼叫各定時任務函式。
本demo使用定時器1來擴展出3個軟體定時器,如果TIM資源不夠用,可以換用系統滴答定時器來做。如:
其中,時間基數可以根據實際需要進行調整。
2、實踐(代入法)套用以上模板,分享我的一個例項:
需要思考及注意的問題是給每個任務的定時值設定多大合適?這也是一些朋友有疑問的,這隻能是自己對自己的任務做考慮,具體情況具體分析,給經驗值、除錯調整。就如同常常有人問定義多大的數組合適?在使用RTOS時每個執行緒的執行緒棧大小設定多大合適、優先順序設定為多少合適?這些都是需要我們自己進行思考的。有模板/輪子套用是好事,但有些問題不能單單依靠模板,否則有可能把自己給套進去。
以上是以STM32為例的,其它微控制器也是可以用這樣子的思想的,包括51微控制器。
面對文首提到的面試問題,若是可以提到使用軟體定時器來處理,進一步能清楚地表達出來,再進一步能寫出一些虛擬碼,那這場面試多半是穩了。