嵌入式軟體開發之程式架構設計-任務調度

来源:https://www.cnblogs.com/const-zpc/archive/2022/06/22/16364444.html
-Advertisement-
Play Games

1 前言 在嵌入式MCU軟體開發過程中,程式任務調度架構的搭建尤為重要,直接關係到該程式能支持多少功能(隨著功能越多系統響應能力越弱,好的任務調度架構能夠在保持相同的系統響應能力前提下支持更多的功能),下麵介紹三種常用的程式任務調度框架設計方案: 前後臺順序執行法 時間片論法 操作系統 2 程式框架 ...


1 前言

在嵌入式MCU軟體開發過程中,程式任務調度架構的搭建尤為重要,直接關係到該程式能支持多少功能(隨著功能越多系統響應能力越弱,好的任務調度架構能夠在保持相同的系統響應能力前提下支持更多的功能),下麵介紹三種常用的程式任務調度框架設計方案:

  • 前後臺順序執行法
  • 時間片論法
  • 操作系統

2 程式框架設計

2.1 前後臺順序執行法

這是初學者們常用的程式框架設計方案,不用考慮太多東西,代碼簡單,或者對系統的整體實時性和併發性要求不高;初始化後通過 while(1){ } 或 for(;;){ } 迴圈不斷調用自己編寫完成的函數,也基本不考慮每個函數執行所需要的時間,大部分情況下函數中或多或少都存在毫秒級別的延時等待。

  • 優點:對於初學者來說,這是最容易也是最直觀的程式架構,邏輯簡單明瞭,適用於邏輯簡單,複雜度比較低的軟體開發。
  • 缺點:實時性低,由於每個函數或多或少存在毫秒級別的延時,即使是1ms,也會造成其他函數間隔執行時間的不同,雖然可通過定時器中斷的方式,但是前提是中斷執行函數花的時間必須短。當程式邏輯複雜度提升時,會導致後來維護人員的大腦混亂,很難理清楚該程式的運行狀態。

以下是我在學校期間做的寢室防盜系統的主函數代碼(當時也存在部分BUG,沒有解決。現在再看,其實很多問題,而且比較嚴重,比如中斷服務函數內竟然有3000ms延時,這太可怕了,還有串口發送等等;由於實時性要求不算太高,因此主函數中的毫秒級別延時對系統運行沒有多大影響,當然除BUG外;若是後期需要維護,那就是一個大工程,還不如推翻重寫):

int main(void)
 {    
    u8 temperature;          
    u8 humidity;   
    int a;
    delay_init();
    uart2_Init(9600);     
    TIM3_Int_Init(4999,7199);
    ds1302_init();
    while(DHT11_Init());    //DHT11初始化    
    a1602_init();        
    lcd12864_INIT();
    LcdInit();

    while(1)
    {
        for(a=0;a<11;a++)
        {
            num[a+3]=At24c02Read(a+2)-208;
            delay_us(10);                    
        }
        for(a=0;a<6;a++)
        {
            shuru[a]=At24c02Read(a+13)-208;
            delay_us(10);                
        }        
        delay_ms(10);  
        RED_Scan();
        Ds1302ReadTime();                    //讀取ds1302的日期時間
        shi=At24c02Read(0);                //讀取鬧鐘保存的數據
        delay_ms(10);
        fen=At24c02Read(1);                //讀取鬧鐘保存的數據            
        usart2_scan();                        //藍牙數據掃描
        usart2_bian();                        //藍牙處理數據
        nao_scan();
        k++;
        if(k<20)
        {
            if(k==1)
                LcdWriteCom(0x01);  //清屏
            LcdDisplay();                            //顯示日期時間
        }
        if(RED==0)
            RED_Scan();

        if(k>=20&&k<30)
        {
            if(k==20)
                LcdWriteCom(0x01);  //清屏
            Lcddisplay();                            //顯示溫濕度
            LcdWriteCom(0x80+6);    
            DHT11_Read_Data(&temperature,&humidity);    //讀取溫濕度值    
            Temp=temperature;Humi=humidity;
            LcdWriteData('0'+temperature/10);
            LcdWriteData('0'+temperature%10);
            LcdWriteCom(0x80+0X40+6);    
            LcdWriteData('0'+humidity/10);
            LcdWriteData('0'+humidity%10);
        }
        if(k==30)
            k=0;
        lcd12864();                                //顯示防盜鬧鐘狀態

    }        
}


//定時器3中斷服務程式
void TIM3_IRQHandler(void)   //TIM3中斷
{
    int i;
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //檢查TIM3更新中斷發生與否
    {
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中斷標誌 
        if(key1==1&&FEN-fen==0&&SHI-shi==0)                //時間一到鬧鐘響起
        {
            f=1;                        
        }
        else
        {
            f=0;
        }    
        if(USART_RX_BUF[0]=='R'&&USART_RX_BUF[1]=='I'&&USART_RX_BUF[2]=='N'&&USART_RX_BUF[3]=='G')
        {
            key0=1;
            for(i=0;i<17;i++)
            {
                USART_SendData(USART1, num[i]);//向串口1發送數據
                while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待發送結束
                USART_RX_STA=0;    
            }    
            delay_ms(3000);
            for(i=0;i<3;i++)
            {
                USART_SendData(USART1, num1[i]);//向串口1發送數據
                while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待發送結束
                USART_RX_STA=0;    
            }            
        }
    }
}

2.2 時間片論法

介於“前後臺順序執行法”和“操作系統”之間的一種程式架構設計方案。該設計方案需能幫助嵌入式軟體開發者更上一層樓,在嵌入式軟體開發過程中,若遇到以下幾點,那麼該設計方案可以說是最優選擇,適用於程式較複雜的嵌入式系統;

  1. 目前的需求設計需要完全沒有必要上操作系統
  2. 任務函數無需時刻執行,存在間隔時間(比如按鍵,一般情況下,都需要軟體防抖,初學者的做法通常是延時10ms左右再去判斷,但10ms極大浪費了CPU的資源,在這段時間內CPU完全可以處理很多其他事情)
  3. 實時性有一定的要求

該設計方案需要使用一個定時器,一般情況下定時1ms即可(定時時間可隨意定,但中斷過於頻繁效率就低,中斷太長,實時性差),因此需要考慮到每個任務函數的執行時間,建議不能超過1ms(能通過程式優化縮短執行時間則最好優化,如果不能優化的,則必須保證該任務的執行周期必須遠大於任務所執行的耗時時間),同時要求主迴圈或任務函數中不能存在毫秒級別的延時。

如何確定每個函數的任務周期呢?根據任務的耗時和效果決定、如按鍵掃描任務周期為 10ms(為了提高響應),指示燈控制任務周期為 100ms(通常情況下最高100ms的閃爍頻率正好,特殊需求除外),LCD/OLED 顯示周期為 100ms(通過這種通過SPI/IIC等介面的方式耗時大約在 1~10ms,甚至更長,所以任務周期必須遠大於耗時,同時為了滿足人眼所能接受的刷屏效果,也不能太長,100ms 的任務周期比較合適)等

以下介紹兩種不同的實現方案,分別針對無函數指針概念的朋友和想進一步學習的朋友。

2.2.1 無函數指針的設計方式

/**
  * @brief      主函數.
  * @param      None.
  * @return     None.
  */
int main(void)
{
    System_Init();
    
    while (1)
    {
        
        if (TIM_1msFlag)                            // 1ms
        {
            CAN_CommTask();                         // CAN發送/接收通信任務
            
            TIM_1msFlag = 0;
        }
        
        if (TIM_10msFlag)                           // 10ms
        {
            KEY_ScanTask();                         // 按鍵掃描處理任務
            
            TIM_10msFlag = 0;
        }
        
        if (TIM_20msFlag)                           // 20ms
        {
            LOGIC_HandleTask();                     // 邏輯處理任務

            TIM_20msFlag = 0;
        }
        
        if (TIM_100msFlag)                          // 100ms
        { 
            LED_CtrlTask();                         // 指示燈控制任務
            
            TIM_100msFlag = 0;
        }    
        
        if (TIM_500msFlag)                          // 500ms
        {
            TIM_500msFlag = 0;
        }    
        
        if (TIM_1secFlag)                           // 1s
        {
            WDog_Task();                            // 喂狗任務
            
            TIM_1secFlag = 0;
        }  
    }
}

/**
  * @brief      定時器3中斷服務函數.
  * @param      None.
  * @return     None.
  */
void TIM3_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET) // 溢出中斷
    {
        sg_1msTic++;
        
        sg_1msTic % 1 == 0 ? TIM_1msFlag = 1 : 0;
        
        sg_1msTic % 10 == 0 ? TIM_10msFlag = 1 : 0;
        
        sg_1msTic % 20 == 0 ? TIM_20msFlag = 1 : 0;
        
        sg_1msTic % 100 == 0 ? TIM_100msFlag = 1 : 0;
        
        sg_1msTic % 500 == 0 ? TIM_500msFlag = 1 : 0;
        
        sg_1msTic % 1000 == 0 ? (TIM_1secFlag  = 1, sg_1msTic = 0) : 0;
    }
    
    TIM_ClearITPendingBit(TIM3,TIM_IT_Update);  // 清除中斷標誌位
}

2.2.2 含函數指針的設計方式

/**
  * @brief 任務函數相關信息結構體定義.
  */
typedef struct{
    uint8 m_runFlag;                 /*!< 程式運行標記:0-不運行,1運行 */
    uint16 m_timer;                  /*!< 計時器 */
    uint16 m_itvTime;                /*!< 任務運行間隔時間 */
    void (*m_pTaskHook)(void);       /*!< 要運行的任務函數 */
} TASK_InfoType; 

#define TASKS_MAX     5              // 定義任務數目

/** 任務函數相關信息 */
static TASK_InfoType sg_tTaskInfo[TASKS_MAX] = {
    {0, 1, 1, CAN_CommTask},         // CAN通信任務
    {0, 10, 10, KEY_ScanTask},       // 按鍵掃描任務
    {0, 20, 20, LOGIC_HandleTask},   // 邏輯處理任務
    {0, 100, 100, LED_CtrlTask},     // 指示燈控制任務
    {0, 1000, 1000, WDog_Task},      // 喂狗任務
};


/**
  * @brief      任務函數運行標誌處理.
  * @note       該函數由1ms定時器中斷調用
  * @param      None.
  * @return     None.
  */
void TASK_Remarks(void)
{
    uint8 i;

    for (i = 0; i < TASKS_MAX; i++)
    {
         if (sg_tTaskInfo[i].m_timer)
        {
            sg_tTaskInfo[i].m_timer--;

            if (0 == sg_tTaskInfo[i].m_timer)
            {
                 sg_tTaskInfo[i].m_timer = sg_tTaskInfo[i].m_itvTime;
                 sg_tTaskInfo[i].m_runFlag = 1;
            }
        }
   }
}

/**
  * @brief      任務函數運行處理.
  * @note       該函數由主迴圈調用
  * @param      None.
  * @return     None.
  */
void TASK_Process(void)
{
    uint8 i;

    for (i = 0; i < TASKS_MAX; i++)
    {
        if (sg_tTaskInfo[i].m_runFlag)
        {
             sg_tTaskInfo[i].m_pTaskHook();         // 運行任務
             sg_tTaskInfo[i].m_runFlag = 0;         // 標誌清0
        }
    }   
}

/**
  * @brief      主函數.
  * @param      None.
  * @return     None.
  */
int main(void)
{
    System_Init();
    
    while (1)
    {
        TASK_Process();
    }
}

/**
  * @brief      定時器3中斷服務函數.
  * @param      None.
  * @return     None.
  */
void TIM3_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET) // 溢出中斷
    {
        TASK_Remarks();
    }
    
    TIM_ClearITPendingBit(TIM3,TIM_IT_Update);     // 清除中斷標誌位
}

2.3 操作系統

       嵌入式操作系統EOS(Embedded OperatingSystem)是一種用途廣泛的系統軟體,過去它主要應用於工業控制和國防系統領域,而對於單片機來說,比較常用的有 UCOS、FreeRTOS、RT-Thread Nano 和 RTX 等多種搶占式操作系統(其他如 Linux 等操作系統不適用於單片機)

操作系統和“時間片論法”,在任務執行方面來說,操作系統對每個任務的耗時沒有過多的要求,需要通過設置每個任務的優先順序,在高優先順序的任務就緒時,會搶占低優先順序的任務;操作系統相對複雜,因此這裡沒有詳細介紹了。

關於如何選擇合適的操作系統([RTOS]uCOS、FreeRTOS、RTThread、RTX等RTOS的對比之特點):

  • UCOS:網上資料豐富,非常適合學習,但是在產品上使用則需要收費
  • FreeRTOS:使用免費,因此很多產品都在用
  • RT-Thread:國產物聯網操作系統,有著十分豐富的組件,也免費,資料:RT-Thread 文檔中心
  • RTX:為ARM和Cortex-M設備設計的免版稅,確定性的實時操作系統

借網上一張對比圖:


總結

從上述的對比中可以看出,時間片輪詢法的優勢還是比較大的,它既有前後臺順序執行法的優點,也有操作系統的優點。結構清晰,簡單,非常容易理解,所以這種是比較常用的單片機設計框架。

下篇:嵌入式軟體開發之程式分層(二)

本文來自博客園,作者:大橙子瘋,轉載請註明原文鏈接:https://www.cnblogs.com/const-zpc/p/16364444.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • watch是監聽某個變數或者屬性的變化,並執行相應的回調函數,通常是一個變數的變化決定多個變數的變化,watch可以進行非同步操作,但不支持緩存。一個偵聽器對應一條數據,當偵聽的數據發生變化是,watch才會執行對應的方法(一對一) computed計算屬性是依賴已有的變數來計算一個目標變數,大多數情 ...
  • 1、所謂數據響應式就是能夠使數據變化可以被檢測並對這種變化做出響應的機制。 2、MVVM框架要解決的一個核心問題是連接數據層和視圖層,通過數據驅動應用,數據變化,視圖更新,要做到這點的就需要對數據做響應式處理,這樣一旦數據發生變化就可以立即做出更新處理。 3、以vue為例說明,通過數據響應式加上虛擬 ...
  • 路由懶載入:有效拆分App尺寸,訪問時才非同步載入 keep-alive組件緩存:避免重覆創建組件實例,且能保留緩存組件狀態 使用v-show復用DOM:避免重覆創建組件 v-for遍歷避免同時使用v-if 長列表性能優化:如果是大數據長列表,可採用虛擬滾動(不會將所有內容渲染,只是把可是區域渲染), ...
  • 泛型 指在定義函數、介面或類的時候,不預先指定具體的類型,而在使用的時候再指定具體類型的一種特性。 引入 下麵創建一個函數, 實現功能: 根據指定的數量 count 和數據 value , 創建一個包含 count 個 value 的數組 不用泛型的話,這個函數可能是下麵這樣: function c ...
  • 前言 : 組件系統是Vue.js其中一個重要的概念,它提供了一種抽象,讓我們可以使用獨立可復用的小組件來構建大型應用,任意類型的應用界面都可以抽象為一個組件樹。 現在基於vue的UI組件庫有很多,比如iview,element-ui等。但有時候這些組件庫滿足不了我們的開發需求,這時候我們就需要自己寫 ...
  • Vue 3.x基礎 模版 <template> // html </template> <script setup> // setup API // ... </script> <style> // css </style> setup API 變數(響應式) // 基本數據類型 let refVa ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 1. ref的使用 ref 接受一個原始值,返回一個具有響應式的對象,對象有一個value屬性,其值就是所傳遞的原始值。 ref是做的一個拷貝關係,修改對象msg的值,不會影響對象obj,視圖會發生變化。 import { ref } f ...
  • 引言 本篇文章主要介紹的是關於CSS Sandbox的一些事情,為什麼要介紹這個呢?在我們日常的開發中,樣式問題其實一直是一個比較耗時的事情,一方面我們根據 UI 稿不斷的去調整,另一方面隨著項目越來越大可能哪一次開發就發現——誒,我的樣式怎麼不起作用了,亦或是怎麼被另一個樣式所覆蓋了。原因可能有很 ...
一周排行
    -Advertisement-
    Play Games
  • 用例演示 - 創建實體 本節將演示一些示例用例並討論可選場景。 創建實體 從實體/聚合根類創建對象是實體生命周期的第一步。聚合/聚合根規則和最佳實踐部分 建議為Entity類創建一個主構造函數,以保證創建一個有效的實體。因此,無論何時我們需要創建實體的實例,我們都應該使用那個構造函數 參見下麵的問題 ...
  • 領域邏輯 & 應用邏輯 如前所述,領域驅動設計中的業務邏輯分為兩部分(層):領域邏輯和應用邏輯: 領域邏輯由系統的核心領域規則組成,應用邏輯實現應用特定的用例 雖然定義很明確,但實現起來可能並不容易。您可能無法決定哪些代碼應該位於應用程式層,哪些代碼應該位於領域層。本節試圖解釋其中的差異 多個應用程 ...
  • 表弟大學快畢業了,學了一個學期Python居然還不會寫學生管理系統,真的給我丟臉啊,教他又不肯學,還讓我直接給他寫,我真想兩巴掌上去,最終還是寫了給他,誰讓他是我表弟呢,關鍵時候還是得幫他一把! 寫完了放在那也是放著,所以今天分享給大家吧! 話不多說,咱們直接開始吧! 代碼解析 一、登錄頁面 1、定 ...
  • Zookeeper3.7源碼剖析 能力目標 掌握Zookeeper中Session的管理機制 能基於Client進行Debug測試Session創建/刷新操作 能搭建Zookeeper集群源碼配置 掌握集群環境下Leader選舉啟動過程 能說出Zookeeper選舉過程中的概念 能說出Zookeep ...
  • 前言 今天給大家分享一下我自己寫的筆記,純純的都是乾貨,關於字好像也能看。這是我學python整理出來的一些資料,希望對大家 有用。想要更多的資料那就的給一個關註了… python學習交流Q群:903971231### #導入Counter from collections import Count ...
  • Hi,大家好,我是Mic 一個工作5年的粉絲找到我。 他說: “Mic老師,你要是能回答出這個問題,我就佩服你” 我當場就懵了,現在打賭都這麼隨意了嗎? 我問他問題是什麼,他說“Kafka如何避免重覆消費的問題!” 下麵看看普通人和高手的回答! 普通人: Kafka怎麼避免重覆消費就是我們可以通過 ...
  • 前言 Steam是由美國電子游戲商Valve於2003年9月12日推出的數字發行平臺,被認為是電腦游戲界最大的數位發行平臺之一,Steam平臺是全球最大的綜合性數字發行平臺之一。玩家可以在該平臺購買、下載、討論、上傳和分享游戲和軟體。 而每周的steam會開啟了一輪特惠,可以讓游戲打折,而玩家就會 ...
  • 本篇內容將在上一篇已有的內容基礎上,進一步的聊一下項目中使用JPA的一些高階複雜場景的實踐指導,覆蓋了主要核心的JPA使用場景,可以讓你在需求開發的時候對JPA的使用更加的游刃有餘。 ...
  • 1.路徑處理 1.找模塊:sys.path import sys print(sys.path) - 1.理解 - 1.是python去查找包或模塊 - 2.項目開始根目錄,python內置的目錄 - 3.雖然說python的安裝目錄下也可以存放我們寫的模塊,但是不建議(太多了,不大好找) - 4. ...
  • Go 語言入門練手項目系列 01 基於命令行的圖書的增刪查改 02 文件管理 持續更新中... > 本文來自博客園,作者:Arway,轉載請註明原文鏈接:https://www.cnblogs.com/cenjw/p/gobeginner-proj-bookstore-cli.html 介紹 這是一 ...