首頁>技術>

前言

前不久,我有位做測試的朋友轉去做開發的工作,面試遇到了一個問題,他沒明白,打電話問了我。題目大概就是:

在微控制器裸機開發時,微控制器要處理多個任務,此時你的程式框架是怎樣的呢?

這其實是個經典面試問題,我以前面試也被問過。

答案一:輪詢系統

程式碼結構如:

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微控制器。

面對文首提到的面試問題,若是可以提到使用軟體定時器來處理,進一步能清楚地表達出來,再進一步能寫出一些虛擬碼,那這場面試多半是穩了。

19
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 遞迴問題怎麼破?