A005-軟件結構-從前後臺到調度器

主要內容:
(1). 前後臺
(2). 事件管理
(3). 時間觸發的調度器(分時複用)
(4). 事件觸發的調度器(狀態機)
(5). 中斷的上下半部機制

-------------------------------------------------------------------------------------------------------------------------------------
開發環境:AVR Studio 4.19 + avr-toolchain-installer-3.4.1.1195-win32.win32.x86
芯片型號:ATmega16
芯片主頻:8MHz


-------------------------------------------------------------------------------------------------------------------------------------

本文將一步步地、將軟件的結構、從簡單前後臺過渡到調度器


-------------------------------------------------------------------------------------------------------------------------------------

1、 概述:


      簡單的前後臺結構如上圖所示。
      前臺中斷爲中心,後臺CPU爲中心。
      這裏顯示着程序涉及到的3個資源:中斷RAMCPU,並隱含第4個資源:時間(CPU消耗多少比例的時間在某個任務上)。

      這種結構下、每個任務產生的數據,都直接作爲全局變量放在RAM裏面、所有任務都可以直接使用。
      其中:
1、1ms定時任務:每隔1ms更新一次時刻計數
2、紅外接收任務:任意時刻(隨機)收到紅外碼、就更新紅外接收數據的數值
3、數值運算01任務:計數完畢後、更新計算結果01的數值
4、數碼管刷新任務:需要讀取計算結果01的數值
5、紅外發送任務:需要讀取按鍵碼的數值、如果是按鍵1按下、就啓動1次紅外發送
6、按鍵掃描任務:任意時刻(隨機)按下按鍵、就更新按鍵碼的數值

      這樣的結構容易出現以下問題:
1、任務數量如果較多、就會有很多任務函數排隊在後臺CPU的主循環中等待被順序執行、顯得比較擁擠。
      我們不能確定地知道某個任務到底是在哪個時刻被執行的,這使得我們只能粗略的估計出一個任務會在間隔多久後被執行。
      而按鍵掃描數碼管刷新等任務最好在穩定的間隔時刻被週期性地執行,才能保證最終的效果。
2、任務之間可以直接調用其他任務的子函數、這會導致代碼結構不夠清晰,功能越複雜、互相調用越多,維護代碼就越麻煩。
      而任務之間使用全局變量來傳遞數據和信息的情況、將會加大這種維護的難度。
3、某些數據可能會同時被多個任務使用,這是可能出現衝突:任務1正在使用數據A、此時中斷中的任務2打斷進來、修改了數據A
      等到程序返回任務1後、被修改的數據A可能導致本次的任務1出錯。

     對此、我們可以做如下改進、以應對這些問題:
1、使用某種任務調度方式:分時調度、事件觸發調度
2、引入事件管理,將部分共享的數據納入事件隊列統一存儲管理
3、對數據訪問引入加鎖
4、儘量減少可以中斷其他任務的搶佔式任務的數量
5、中斷中只收發數據,具體的數據處理放入後臺任務,比如使用中斷上下半部方式

-------------------------------------------------------------------------------------------------------------------------------------

2、後臺CPU分時調度任務

      這一步將使用分時調度的方式對後臺CPU處理的任務隊列進行改進,具體結構如下:
      CPU每隔1ms或調度1個任務,直到所有任務都被調用一遍。
      大體結構如下:

      這裏設置一個長度爲6的任務隊列,CPU每隔1ms就去任務隊列中調度1個任務,直到完全遍歷任務隊列的全部6個元素。
      調度週期是6ms,也就是說、每個任務都是每隔6ms被調度1次,或者說是每個時刻調度1個任務,調度週期是6個時刻。
      如果任務數量少於6個,也並不減小任務隊列的長度、因爲我們需要保持每個任務的調度週期都是固定的。
      這種實現方式相當簡潔,任務在何時被調度是很清晰的。

      CPU也可以讓每個任務有自己的週期:
      (1). 每隔10個時刻調度1次紅外發送任務(通常延遲10ms再啓動數據發送並不會有什麼副作用)
      (2). 每隔10個時刻調度1次按鍵掃描任務
      (3). 每隔  2個時刻調度1次數碼管刷新任務
      (4). 每隔  1個時刻調度1次數值計算01任務
      這將使用一個時間觸發的調度器來實現、大體結構如下(1個任務的調度週期一到、就認爲該任務已就緒):


-------------------------------------------------------------------------------------------------------------------------------------

3、前臺分時調度任務

      既然是利用1ms定時任務產生的時刻值去調度任務,那麼也可以直接在前臺裏面、每當產生新的時刻值、就去調度1個任務:

      CPU在這裏什麼都不用做、任務結束後去休眠即可。
      每次中斷到來時、CPU被喚醒,將中斷函數執行完畢並返回之後,CPU再次進入休眠
      很多應用中、這也是一個很好的方式,整個系統完全由中斷事件驅動CPU平時處於靜默。

      而對於任務較多、功能較爲繁重的情形,一般使用由CPU調度任務的方式、以保持中斷輕巧簡潔
      以應對較多的中斷事件,尤其是隨機的中斷事件

-------------------------------------------------------------------------------------------------------------------------------------

4、事件/消息管理

(1). 概述

      上面是任務調度上的組織,下面進行RAM數據上的組織,使得任務之間互相隔離、不再互相使用對方的子函數全局變量

事件1ms定時任務 每隔1ms產生一個時刻值,我們可以視爲是每隔1ms產生一個事件:1ms時刻到事件、或時刻(時基)更新事件。
           按鍵掃描任務在按鍵按下後產生按鍵碼,我們也將其視爲是發生了1個事件:按鍵按下事件、帶一個參數(按鍵號和按鍵類型)
消息:本文將事件(event)所帶的參數稱爲消息(message),以區分事件本身和事件的參數
          事件/消息管理簡稱事件管理

      在簡單的前後臺結構裏面、事件消息都是作爲數據、直接使用全局變量來存放的。
      下面要將這些數據統一放在一個事件隊列裏面進行管理,不再分散到每個任務單獨管理。
      每個任務產生的事件、都統一交給事件隊列存儲管理,它們不再是任務私有的數據

      事件管理下的後臺CPU分時調度任務方式

      比如、數值計算01任務在執行後將向事件隊列發出2個事件計算結果01事件(參數=計算結果),數碼管刷新事件(參數=計算結果)。
      雖然這些數據需要顯示在數碼管上,但產生這些數據數值計算01任務不去調用數碼管顯示函數
      它並不負責這個、也不關心這些數據是否被數碼管正確地使用。
      數碼管刷新任務自己會到事件隊列裏面去查詢數碼管刷新事件的是否有效。
      如果該事件的有效,它就將數碼管刷新事件對應的參數取出來、送到數碼管顯示,至於這個數據由哪個任務產生,它並不關心。
      也就是說、任務之間相互獨立。

      關於事件管理在前後臺中的應用、可以參考這篇文章《消息機制在軟件設計中的應用》

(2). 基本結構

事件隊列的結構如下:

      圖中事件隊列的結構中包含了事件的三個信息:事件類型(告訴我們這是什麼事件)、事件的參數、事件的鎖定狀態。
      事件(event)放入type部分,事件的消息(message)放入data部分。
      對應如下結構:
// 事件隊列的結構(type[7bit],lock[1bit],data[32bit])
typedef struct 
{
    uint8_t  type :7 ;  // 事件類型、如數碼管數據有更新:EVENT_SEG_UPDATE
    uint8_t  lock :1 ;  // 加鎖標誌
    uint32_t data;      // 事件參數、如數碼管的數據:1265214
}T_EVENT_LIST, *pT_EVENT_LIST;
      至於消息是否需要加鎖
      1、如果約定所有中斷中都不訪問事件隊列,就不需要加鎖
           此時,中斷做得比較小巧、只接收或發送數據,數據處理都在後臺的某個任務中完成。
           在某個任務訪問事件隊列期間,打斷它的中斷都不會訪問事件隊列,因爲不用擔心數據會被修改。
      2、如果允許中斷訪問事件隊列,就需要加鎖
      3、如果後臺CPU調度方式裏面、包含軟件中斷,那麼可能需要加鎖

(3). 事件隊列的代碼實現

sys_event.h
// ==========================================================================================================
// Copyright (c) 2016 Manon.C <[email protected]>
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
// associated documentation files (the "Software"), to deal in the Software without restriction, including 
// without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 
// sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject 
// to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// 
// ---------------------------
// 本文定義了事件管理模塊
// 
// 說明:
// (1).本文將事件(event)所帶的參數稱爲消息(message),以區分事件本身和事件的參數
// 
// ==========================================================================================================
#ifndef __SYS_EVENT_H__
#define __SYS_EVENT_H__



#include <avr/interrupt.h>
#include "sys_timer.h"
#include "config.h"


// 事件(事件的類型tpye,爲8bit)(事件的參數data,爲32bit)
typedef enum 
{
    EVENT_SYS,
    EVENT_KEY,
    EVENT_IR_RECIEVE,
    EVENT_IR_SEND,
    EVENT_RTC,
    EVENT_DIGITAL_FORMAT,// 數據進制格式、範圍:[2,16]進制
    EVENT_SEG_UPDATE,    // 參數爲32bit的事件(必須至少有一個、避免數組sys_event_int32[]的元素個數爲0)
    EVENT_MAX
}EVENT;


// 事件隊列的結構(type[7bit],lock[1bit],data[32bit])
typedef struct 
{
    uint8_t  type :7 ;  // 事件類型、如數碼管數據有更新:EVENT_SEG_UPDATE
    uint8_t  lock :1 ;  // 加鎖標誌
    uint32_t data;      // 事件參數、如數碼管的數據:1265214
}T_EVENT_LIST, *pT_EVENT_LIST;

// 事件管理器的結構(任務獨佔的事件緩存應是這種結構:T_EVENT_INT32 task_event_buffer[])
typedef struct 
{
          uint8_t number;   // 緩存中的事件數量
    pT_EVENT_LIST pBuffer;  // 事件緩存的地址
}T_TASK_EVENT_BOX;


void sys_event_lock(uint8_t type);
void sys_event_unlock(uint8_t type);
void sys_event_unlock_all(void);
uint8_t sys_event_any_lock(void);

void sys_event_init(void);
void sys_event_buffer_set(const p_void_funtion_void task, const pT_EVENT_LIST buffer);
void sys_event_buffer_post(const p_void_funtion_void task, const uint8_t event_number);
bool sys_event_push(void);
bool sys_event_post(uint8_t type, uint32_t data);

bool sys_event_get(pT_EVENT_LIST event);
bool sys_event_peek(uint8_t type, uint32_t data);
bool sys_event_data(uint8_t type, uint32_t *data);



#endif	// #ifndef __SYS_EVENT_H__
sys_event.c
#include "sys_event.h"


// 事件隊列
static T_EVENT_LIST sys_event_list[EVENT_MAX];

// 事件管理器
// (保存着每個任務獨佔的事件緩存的首地址,數組下標和任務隊列的下標保持一致)
T_TASK_EVENT_BOX task_event_box[SYS_TASK_MAX];


// ==========================================================================================================
//      鎖定事件隊列中的元素
// 
// ==========================================================================================================
void sys_event_lock(uint8_t type)
{
    if(type < EVENT_MAX)
    {
        sys_event_list[type].lock = LOCKED;
    }
}

// ==========================================================================================================
//      解鎖事件隊列中的元素
// 
// ==========================================================================================================
void sys_event_unlock(uint8_t type)
{
    if(type < EVENT_MAX)
    {
        sys_event_list[type].lock = UNLOCKED;
    }
}

// ==========================================================================================================
//      檢查是否有事件被鎖定
// 
// 返回值:index  被鎖定事件的事件號
// 
// 說明:
// (1). 從頭開始查找,直到找到第一個被鎖定的事件爲止
// (2). 調度器在每次調度新任務前,都會檢查所有事件,確保沒有任何鎖的存在
//      因爲,每個任務退出後、必須解鎖所有的鎖
// 
// ==========================================================================================================
uint8_t sys_event_any_lock(void)
{
    uint8_t index;

    for(index = 0; index < EVENT_MAX; index++)
    {
        if(LOCKED == sys_event_list[index].lock)
        {
            break;
        }
    }
    return index;
}

// ==========================================================================================================
//      解鎖所有事件
// 
// ==========================================================================================================
void sys_event_unlock_all(void)
{
    uint8_t index;

    for(index = 0; index < EVENT_MAX; index++)
    {
        sys_event_list[index].lock = UNLOCKED;
    }
}

// ==========================================================================================================
//      事件隊列初始化、事件緩存管理器初始化
// 
// ==========================================================================================================
void sys_event_init(void)
{
    uint8_t index;

    for(index = 0; index < EVENT_MAX; index++)
    {
        sys_event_list[index].type = EVENT_MAX;
        sys_event_list[index].lock = UNLOCKED;
        sys_event_list[index].data = 0;
    }
    for(index = 0; index < SYS_TASK_MAX; index++)
    {
        task_event_box[index].number  = 0;
        task_event_box[index].pBuffer = NULL;
    }
}

// ==========================================================================================================
//      直接將每個任務產生的事件寫入到事件隊列
// 
// 返回值:Fin  TURE  = 寫入成功
//              FALSE = 寫入失敗(事件被鎖定、或事件是無效的事件)
// 
// ==========================================================================================================
bool sys_event_post(uint8_t type, uint32_t data)
{
    bool Fin = FALSE;

    if(type < EVENT_MAX)
    {
        if(UNLOCKED == sys_event_list[type].lock)
        {
            sys_event_list[type].lock = LOCKED;
            sys_event_list[type].type = type;
            sys_event_list[type].data = data;
            sys_event_list[type].lock = UNLOCKED;
            Fin = TRUE;
        }
    }
    return Fin;
}
寫入事件
使用上面這幾個函數來將事件寫入事件隊列,並設置事件的鎖定狀態。
比如、任務A可以使用函數bool sys_event_post(uint8_t type, uint32_t data);將1個事件及其參數直接寫入事件隊列。

但是,可能會有事件寫入失敗,比如:
1、任務B正在訪問這個事件、並已經將其鎖定。
2、接着任務A打斷任務B,並且要去更新(寫入)這個事件,這個寫入操作會因爲該事件被鎖定而導致寫入失敗。
     如果寫入失敗,任務A就只能將這個事件保存起來,以便下次再次嘗試寫入。

鑑於任務A會寫入失敗,我們給出了另一種方法:
將事件寫入操作獨立出來,讓事件管理模塊自己負責去寫入這個事件。
任務A不負責寫入操作,只是將事件保存起來,並通知事件管理模塊、這裏有個事件需要寫入。

爲了讓事件管理模塊自己去負責事件的寫入,需要:
1、任務A需要建立一個緩存來保存自己產生的所有事件、並且任務A獨佔這個緩存,其他任務不會訪問這個緩存。
2、建立一個事件管理器task_event_box[SYS_TASK_MAX],將任務A及其他所有任務獨佔的緩存們都註冊到裏面。
3、任務管理模塊將遍歷檢查事件管理器,如果發現任務A有事件需要寫入,就將其寫入事件隊列。
     如果寫入失敗,可能是任務B正在訪問這個事件並將其鎖定了,但這個事件仍然保存在任務A的緩存中。
     在任務B退出後,任務管理模塊再次遍歷事件管理器時,就可以將該事件正常地寫入了。

事件管理器和任務獨佔的緩存之間的關係如下:

具體代碼:
// ==========================================================================================================
//      將任務獨佔的事件緩存註冊到事件管理器
// 
// 參數:task       任務函數
//       buffer     該任務獨佔的事件緩存的首地址(每個任務獨佔1個buffer,不和其他任務共享、無訪問衝突)
// 
// 說明:
// (1). 一般在任務初始化函數裏面、去註冊該任務獨佔的事件緩存
//      也就是說、這個函數一般在任務的初始化函數裏面被調用
// 
// ==========================================================================================================
void sys_event_buffer_set(const p_void_funtion_void task, const pT_EVENT_LIST buffer)
{
    uint8_t task_index;

    task_index = sys_task_index(task);
    if(task_index < SYS_TASK_MAX)
    {
        task_event_box[task_index].pBuffer = buffer;
    }
}

// ==========================================================================================================
//      設置任務需要發送的事件的數量
// 
// 參數:task       任務函數
//       number     該任務獨佔的事件緩存裏面需要發送的事件的數量
// 
// 說明:
// (1). 每個任務根據需要建立事件緩存,並在任務退出時將緩存地址發給事件管理模塊即可
//      如果事件管理模塊發現該任務需要發送的事件數量>0,它就會將這些事件送到事件隊列,任務本身不需要自己去發送事件
// (2). 被delete的任務的緩存還在,所以依然可以在被delete後得以發送
// (3). 假如任務A的事件緩存(.buffer)有4個元素,但本次只發送2個事件、並設置.number = 2
//      那麼就需要將這2個事件放在.buffer[0]和.buffer[1],否則不能保證事件可以被髮送
//      因爲我們在sys_event_push()裏面只查詢.buffer的前2個元素(前.number個元素),後面的不再查詢
//      所以一般將.number設爲_countof(event_buffer)、如果這個值不是很大的話
//      當然、如果已經確認將這2個事件放在了buffer[0]和buffer[1],那麼只需.number = 2即可避免額外的任務消耗
// 
// ==========================================================================================================
void sys_event_buffer_post(const p_void_funtion_void task, const uint8_t event_number)
{
    uint8_t task_index;

    task_index = sys_task_index(task);
    if(task_index < SYS_TASK_MAX)
    {
        task_event_box[task_index].number = event_number;
    }
}

// ==========================================================================================================
//      將所有任務的事件緩存中的事件保存到事件隊列
// 
// 返回值:Fin  FALSE = 至少有1個事件還未寫入事件隊列
//              TRUE  = 所有事件都已成功地寫入事件隊列
// 
// 說明:
// (1). 每個任務獨佔1個buffer,不和其他任務共享、無訪問衝突
// (2). 假如有>=2個事件必須同時成功地寫入事件隊列、才能保證用戶任務執行正常
//      那麼就必須在用戶任務裏面判斷這>=2個事件同時有效
// 
// ==========================================================================================================
bool sys_event_push(void)
{
    bool Fin = TRUE;

    uint8_t post;       // 某個事件發送是否成功
    uint8_t task_index; // 任務號
    uint8_t msg_number; // 事件數量
    uint8_t msg_index;  // 事件序號
    T_EVENT_LIST event; // 事件及其參數

    for(task_index = 0; task_index < SYS_TASK_MAX; task_index++)
    {
        msg_number = task_event_box[task_index].number;
        if(msg_number > 0)
        {
            // --------
            // 發送事件
            for(msg_index = 0; msg_index < msg_number; msg_index++)
            {
                event.type = (task_event_box[task_index].pBuffer)[msg_index].type;
                if(EVENT_MAX != event.type)
                {
                    event.data = (task_event_box[task_index].pBuffer)[msg_index].data;
                    post = sys_event_post(event.type, event.data);
                    if(TRUE == post)  // 事件發送成功,則清除該事件
                    {
                        (task_event_box[task_index].pBuffer)[msg_index].type = EVENT_MAX;
                    }
                }
            }
            // ----------------------------------------------------------------------------------
            // 檢查是否將全部事件都發送完畢,沒發完就將未發送的部分移到前面,等待下次進來再次發送
            post = TRUE;
            for(msg_index = 0; msg_index < msg_number; msg_index++)
            {
                if(EVENT_MAX != (task_event_box[task_index].pBuffer)[msg_index].type)
                {
                    post = FALSE;
                    Fin  = FALSE;  // 至少有1個事件還未寫入事件隊列
                    break;
                }
            }
            if(TRUE == post)
            {
                task_event_box[task_index].number = 0;
            }
        }
    }
    return Fin;
}
任務管理模塊使用函數bool sys_event_push(void)去遍歷查詢任務管理器,並將其中的事件寫入事件隊列。
其中使用到的函數uint8_t sys_task_index(const p_void_funtion_void task);在下面的調度器部分會給出,它用來讀取一個任務的任務號。

讀取事件
有兩類讀取方式:
1、遍歷事件隊列,看看有沒有事件,這是無目的讀取。
2、精確地讀取某個事件,看看該事件是否存在,如果存在、可以讀出它的參數。

代碼:
// ==========================================================================================================
//      查詢事件隊列
// 
// 參數:  event    用於讀出事件的type和data
// 
// 返回值:Fin      TURE  = 讀到1個事件及其消息參數
//                  FALSE = 沒有任何事件存在
// 
// 說明:
// (1). 由於總是從頭開始查找,直到找到第一個有效的事件爲止
//      所以在typedef enum { }EVENT中越靠前的事件、越會被優先查詢到
// 
// ==========================================================================================================
bool sys_event_get(pT_EVENT_LIST event)
{
    bool Fin = FALSE;
    uint8_t index;

    event->type = EVENT_MAX;
    event->data = 0;

    for(index = 0; index < EVENT_MAX; index++)
    {
        if(UNLOCKED == sys_event_list[index].lock)
        {
            sys_event_list[index].lock = LOCKED;
            if(EVENT_MAX != sys_event_list[index].type)
            {
                Fin = TRUE;
                event->type = sys_event_list[index].type;
                event->data = sys_event_list[index].data;
                sys_event_list[index].type = EVENT_MAX;
                sys_event_list[index].data = 0;
            }
            sys_event_list[index].lock = UNLOCKED;
        }
        if(TRUE == Fin)
        {
            break;
        }
    }
    return Fin;
}

// ==========================================================================================================
//      查看某個事件是否已經存在、要求參數也對應
// 
// 參數:  type     事件
//         data     事件的參數
// 
// 返回值:FALSE    該事件沒有發生、或被鎖定
//         TRUE     該事件已經發生,返回後將該事件從事件隊列中清除
// 
// ==========================================================================================================
bool sys_event_peek(uint8_t type, uint32_t data)
{
    bool Fin = FALSE;

    if(type < EVENT_MAX)
    {
        if(UNLOCKED == sys_event_list[type].lock)
        {
            sys_event_list[type].lock = LOCKED;
            if(sys_event_list[type].type == type)
            {
                if(sys_event_list[type].data == data)
                {
                    Fin = TRUE;
                    sys_event_list[type].type = EVENT_MAX;
                    sys_event_list[type].data = 0;
                }
            }
            sys_event_list[type].lock = UNLOCKED;
        }
    }
    return Fin;
}

// ==========================================================================================================
//      查看某個事件是否已經存在、並取出事件的參數
// 
// 參數:  type     事件號
//        *data     取出該事件的參數
// 
// 返回值:FALSE    該事件沒有發生、或被鎖定
//         TRUE     該事件已經發生,返回後將該事件從事件隊列中清除
// 
// ==========================================================================================================
bool sys_event_data(uint8_t type, uint32_t *data)
{
    bool Fin = FALSE;

    if(type < EVENT_MAX)
    {
        if(UNLOCKED == sys_event_list[type].lock)
        {
            sys_event_list[type].lock = LOCKED;
            if(sys_event_list[type].type == type)
            {
                Fin = TRUE;
                *data = sys_event_list[type].data;
                sys_event_list[type].type = EVENT_MAX;
                sys_event_list[type].data = 0;
            }
            sys_event_list[type].lock = UNLOCKED;
        }
    }
    return Fin;
}

(4). 事件管理下的任務函數

有了事件管理,一個任務將包含以下幾個特徵:
1、獨佔1個事件緩存
2、任務函數需要查詢事件
3、任務函數需要將事件寫入事件緩存、並通知事件管理模塊

例如,數碼管刷新任務的任務函數,它只需要查詢消息:
// ==========================================================================================================
// LED數碼管刷新任務
// 
// 查詢消息:EVENT_SEG_UPDATE
//           EVENT_DIGITAL_FORMAT
// 消息參數:32位數值
// 發送消息:無
// 
// 說明:
// (1). 在系統定時器或任務調度器中定時刷新(被作爲1個任務去調度)
// 
// ==========================================================================================================
void task_Mod_LED_display(void)
{
    uint32_t temp = 0;

    // ------------------------------------------------
    // 查詢事件
    if(TRUE == sys_event_data(EVENT_SEG_UPDATE, &temp))
    {
        p_LED_display_ctrl->set_data   = TRUE;  // 如果得到更新的數據、就啓動數據拆分
        p_LED_display_ctrl->data_index = 0;     // 如果正在拆分過程中、又一次需要拆分,就需要重新設置.data_index爲0
        p_LED_display_ctrl->data       = temp;
        p_LED_display_ctrl->data_copy  = temp;
    }
    if(TRUE == sys_event_data(EVENT_DIGITAL_FORMAT, &temp))
    {
        p_LED_display_ctrl->set_format = TRUE;
        p_LED_display_ctrl->format     = temp;
    }

    // -------------------------------------
    // 任務正文
    if(TRUE == p_LED_display_ctrl->set_data)
    {
        p_LED_display_ctrl->set_data = Mod_LED_display_set_data(p_LED_display_ctrl->format);
    }
    if(TRUE == p_LED_display_ctrl->set_format)
    {
        p_LED_display_ctrl->set_format = FALSE;
        p_LED_display_ctrl->set_data   = TRUE;
        p_LED_display_ctrl->data_index = 0;
        p_LED_display_ctrl->data = p_LED_display_ctrl->data_copy;
    }

    // --------
    // 刷新顯示
    Mod_LED_display_update();

    // ----------------------
    // 發送事件
}

數碼管模塊的完整代碼詳見《B001-Atmega16-數碼管》的最後一步。

計時任務需要建立事件緩存,並通知事件管理模塊:
volatile uint32_t cout = 0;
volatile uint32_t second = 10000000;  // 測試用變量

// 事件緩存
T_EVENT_LIST event_buffer_task_count_time[2];
// ==========================================================================================================
// 任務事件緩存初始化
// 
// ==========================================================================================================
void task_count_time_event_buffer_init(void)
{
    uint8_t index;
    for(index = 0; index < _countof(event_buffer_task_count_time); index++)
    {
        event_buffer_task_count_time[index].lock = UNLOCKED;
        event_buffer_task_count_time[index].type = EVENT_MAX;
        event_buffer_task_count_time[index].data = 0;
    }
    // 將自己的事件緩存註冊到事件管理器
    sys_event_buffer_set(task_count_time, event_buffer_task_count_time);
}

void task_count_time_init(void)
{
    // ----------
    // 硬件初始化
    Drv_IO_mode_bit(DDRD, DDD0, IO_OUTPUT);
    Drv_IO_clr_bit(PORTD, PD0);

    // --------------
    // 事件緩存初始化
    task_count_time_event_buffer_init();
}

void task_count_time(void)
{
    // 運行時刻標記、用來標記任務何時被調度
    Drv_IO_toggle_bit(PORTD, PD0);

    // ------------------------------------------
    // 消息查詢

    // ------------------------------------------
    // 任務正文
    if(++cout >= 250)
    {
        cout = 0;
        second++;
        // --------------------------------------
        // 組織事件、並通知事件管理模塊
        event_buffer_task_count_time[0].type = EVENT_IR_SEND;
        event_buffer_task_count_time[0].data = second;
        event_buffer_task_count_time[1].type = EVENT_SEG_UPDATE;
        event_buffer_task_count_time[1].data = second;
        sys_event_buffer_post(task_count_time, _countof(event_buffer_task_count_time));
    }
}

-------------------------------------------------------------------------------------------------------------------------------------

(5). 組織事件的參數(消息)

事件隊列的結構:

1、在這個結構中,消息(message)是一個32bit的數,但事件(event)只有1個,這會帶來一個問題。
     比如按鍵事件,事件是EVENT_KEY,那如何區分按鍵的狀態呢(按下、鬆手、短按、長按、... )。
     顯然這需要在32bit的消息裏面去進一步組織。
     也就是說、 這些詳細劃分消息的工作都需要產生這些事件的任務自己去完成。
     因爲每個任務如何劃分消息、消息的意義,都需要任務自己定義。

2、下面以EVENT_SYS爲例來進行組織。
EVENT_SYS的參數(消息)如下:
typedef enum 
{
    MSG_SYS_TASK_DELAYED, // 有任務被延遲
    MSG_SYS_EVENT_LOCKED, // 有事件被鎖定

    MSG_SYS_SLEEP,
    MSG_SYS_WAKEUP,
    MSG_SYS_IDLE,
    MSG_SYS_START
}MSG_EVENT_SYS;
使用如下結構來進一步組織消息(message)、以區分出EVENT_SYS的衆多參數(消息):
// MSG_EVENT_SYS的結構(32bit)
typedef struct 
{                               // 數據放在一個或半個字節裏面、調試的時候以十六進制格式查看會更方便
    uint8_t  event_index  : 8;  // bit[07:00]被鎖定的事件號
    uint8_t  task_index   : 8;  // bit[15:08]被延遲的任務號
    uint8_t  event_locked : 1;  // bit[  :16]有事件被鎖定
    uint8_t  task_delayed : 1;  // bit[  :17]有任務被延遲

    uint16_t reserved     : 10; // bit[27:18]用於將來擴展消息

    uint8_t  sys_sleep    : 1;  // bit[  :28]系統休眠
    uint8_t  sys_wakeup   : 1;  // bit[  :29]系統喚醒
    uint8_t  sys_ldle     : 1;  // bit[  :30]系統空閒
    uint8_t  sys_start    : 1;  // bit[  :31]系統開機
}T_MSG_EVENT_SYS, *pT_MSG_EVENT_SYS;

// MSG_EVENT_SYS的聯合體結構、更適合在函數中進行操作
typedef union
{
    T_MSG_EVENT_SYS msg;
    uint32_t data;
}U_MSG_EVENT_SYS;
這裏使用位域將32bit的消息拆分成很多段,每一段對應MSG_EVENT_SYS裏面的一個消息。

下面的函數將EVENT_SYS的衆多參數(消息)寫入事件隊列:
// ==========================================================================================================
// 更新系統產生的消息和警告
// 
// 參數:type    需要更新的事件
//       msg     需要更新的消息
//       index   任務號、或事件號
// 
// 說明:
// (1). 使用讀-修改-寫的方式更新事件
// (2). 使用了sys_event_post()來直接寫入,並在讀寫之前強制解鎖該事件
// 
// ==========================================================================================================
void sys_update_event(const uint8_t type, const uint32_t msg, const uint8_t index)
{
    bool peek;
    U_MSG_EVENT_SYS sys;

    sys_event_unlock(type);
    peek = sys_event_data(type, &sys.data);

    if(TRUE == peek)
    {
        switch(msg)
        {
            // 有事件被鎖定 ----------
            case MSG_SYS_EVENT_LOCKED : sys.msg.event_locked = 1;
                                        sys.msg.event_index  = index;
                                        break;
            // 有任務被延遲 ----------
            case MSG_SYS_TASK_DELAYED : sys.msg.task_delayed = 1;
                                        sys.msg.task_index   = index;
                                        break;
            // 系統狀態 --------
            case MSG_SYS_SLEEP  : sys.msg.sys_sleep  = 1;
                                  break;
            case MSG_SYS_WAKEUP : sys.msg.sys_wakeup = 1;
                                  break;
            case MSG_SYS_IDLE   : sys.msg.sys_ldle   = 1;
                                  break;
            case MSG_SYS_START  : sys.msg.sys_start  = 1;
                                  break;
            default : break;
        }
        sys_event_post(type, sys.data);
    }
}
這裏使用sys_event_post(type, sys.data);來將消息直接寫入事件隊列,而沒有建立事件緩存,
是因爲EVENT_SYS不與其他任務共享,不存在訪問衝突。

如果是按鍵事件EVENT_KEY,就需要建立事件緩存,同時使用相同的方法將事件寫入緩存、並通知事件管理模塊。
EVENT_KEY的參數(消息)組織方法類似:
// EVENT_KEY的參數(按鍵狀態)
typedef enum 
{
    MSG_KEY_DOWN,
    MSG_KEY_UP,
    MSG_KEY_SHORT,
    MSG_KEY_LONG,
    MSG_KEY_HOLD,
    MSG_KEY_DOUBLE,
    MSG_KEY_TWO     // 兩個按鍵同時按下、得到一個新的鍵值(同時按下的兩個按鍵的鍵值也許需要清0)
}MSG_EVENT_KEY;

// MSG_EVENT_KEY的結構(32bit)(初值=0)
typedef struct 
{                             // 數據放在一個或半個字節裏面、調試的時候以十六進制格式查看會更方便
    uint32_t key_index : 24;  // bit[23:00]具體鍵值(支持2^24個鍵值)

    uint8_t  reserved   : 1;  // bit[  :24]將來作爲第8種按鍵狀態

    uint8_t  key_two    : 1;  // bit[  :25]兩個按鍵同時按下、得到一個新的鍵值(同時按下的兩個按鍵的鍵值也許需要清0)
    uint8_t  key_double : 1;  // bit[  :26]雙擊
    uint8_t  key_hold   : 1;  // bit[  :27]保持
    uint8_t  key_long   : 1;  // bit[  :28]長按
    uint8_t  key_short  : 1;  // bit[  :29]短按
    uint8_t  key_down   : 1;  // bit[  :30]按下
    uint8_t  key_up     : 1;  // bit[  :31]鬆手
}T_MSG_EVENT_KEY, *pT_MSG_EVENT_KEY;

// MSG_EVENT_KEY的聯合體結構、更適合在函數中進行操作
typedef union
{
    T_MSG_EVENT_SYS msg;
    uint32_t data;
}U_MSG_EVENT_KEY;
使用位域可以避免大量定義MASK_EVENT_KEY_DOWN等常量,更方便編程、容易閱讀。



-------------------------------------------------------------------------------------------------------------------------------------

(6). 事件隊列和任務之間互相獨立


      事件隊列對於任務函數來說、就是一個公共的資源地,或者說是替任務管理數據、管理任務共享出來的那部分數據:



      事件隊列類似一個水池,每個任務都可以從中取得消息,也可以將消息放入其中:


-------------------------------------------------------------------------------------------------------------------------------------

5、時間觸發的任務調度

(1). 基本結構

      這個調度器的結構如下:
 
它和上面給出的後臺CPU分時調度任務方式(編號4)類似,但又會循環遍歷任務隊列。

任務的特點:
1、我們爲每個任務設置獨立的調度週期
      (1). 每隔10個時刻調度1次紅外發送任務(通常延遲10ms再啓動數據發送並不會有什麼副作用)
      (2). 每隔10個時刻調度1次按鍵掃描任務
      (3). 每隔  2個時刻調度1次數碼管刷新任務
      (4). 每隔  1個時刻調度1次數值計算01任務
2、一個任務的調度週期到來、就表示任務處於就緒狀態

調度器:
1、調度器將建立一個任務隊列,將所有任務都註冊進去。
2、調度模塊處於後臺CPU處、它將遍歷任務隊列,並執行隊列中所有處於就緒狀態的任務
3、事件管理模塊將被嵌入到調度模塊
0
任務隊列的結構:
typedef struct 
{
    uint8_t number;         // 任務號:該任務在任務隊列中的位置
    uint8_t co_op;          // 任務類型:1=合作式任務,0=搶佔式任務
    uint8_t run;            // 任務狀態:(0)=準備中、(>0)=就緒、(>1)=任務曾經被延遲
    uint8_t delay;          // 任務延時計數
    uint8_t period;         // 任務運行間隔
    p_void_funtion_void task;  // 任務函數
}T_sys_task;

T_sys_task sys_task_ctrl[SYS_TASK_MAX];  // 任務隊列
1、在1ms定時中斷中、任務延時delay會每隔1ms減1、減到0時表示調度週期到來
2、調度週期到來、就會設置狀態標識run,表示任務處於就緒狀態
3、然後重置delay = period,再次進入每隔1ms減1的循環
4、搶佔式任務的調度週期到來時、將直接在1ms定時中斷中執行、而不去等待調度

(2). 代碼實現

1、這裏使用《時間觸發的嵌入式系統設計模式》裏面提供的調度器來實現、並做部分改動。
sys_timer.h
#ifndef __SYS_TIMER_H__
#define __SYS_TIMER_H__


#include <stdint.h>
#include <avr/interrupt.h>
#include "Drv_IO_Port.h"
#include "Drv_Sys.h"
#include "Drv_Timer.h"
#include "sys_event.h"
#include "sys_warning.h"
#include "config.h"

#define SYS_TASK_MAX         4  // 最多支持10個任務
#define SYS_TASK_RUN_MAX    10  // 1個任務的就緒狀態的最大值

typedef enum 
{
    SYS_TASK_TYPE_PRE_EM = 0,   // 搶佔式任務
    SYS_TASK_TYPE_CO_OP  = 1    // 合作式任務
}SYS_TASK_TYPE;

void sys_task_init(void);
void sys_task_start(void);
void sys_task_dispatch(void);
void sys_task_delete(const uint8_t index);
uint8_t sys_task_add(const uint8_t delay, const uint8_t period, const p_void_funtion_void task, 
                     const uint8_t co_op, const p_void_funtion_void init);
uint8_t sys_task_index(const p_void_funtion_void task);

void delay_ms(const uint16_t count);


#endif // #ifndef __SYS_TIMER_H__

sys_timer.c
#include "sys_timer.h"

typedef struct 
{
    uint8_t number;         // 任務號:該任務在任務隊列中的位置
    uint8_t co_op;          // 任務類型:1=合作式任務,0=搶佔式任務
    uint8_t run;            // 任務狀態:(0)=準備中、(>0)=就緒、(>1)=任務曾經被延遲
    uint8_t delay;          // 任務延時計數
    uint8_t period;         // 任務運行間隔
    p_void_funtion_void task;  // 任務函數
}T_sys_task;

T_sys_task sys_task_ctrl[SYS_TASK_MAX];

// ==========================================================================================================
//      系統任務調度定時器啓動
// 
// (1). 使用Timer0產生1ms的時標
//      定時週期 T = ((1.0/8000000)*1000000)*64*(124+1) = 1000us = 1ms
//      使用較小的OCR0=122、可以從PA1得到更精確的1ms,因爲進入中斷也是需要十幾us
//      這可以讓從中斷產生到進入中斷函數爲止的時間更精確爲1.0ms
// 
// (2). t = ((1.0 / 8000000)) * div * (N + 1)(單位:秒)
//      f = 8000000 / (div * (N + 1))
//      N = 8000000 / freq / div - 1
// 
// ==========================================================================================================
void sys_task_start(void)
{
    uint16_t div;          // 16bit寬度的分頻係數
    uint16_t freq = 1000;  // 16bit寬度的頻率:1000Hz
    uint8_t  ocr0;
    uint8_t  DIV  = T0_CLK_SOURCE_DIV_64;

    // --------
    // 計算參數
    switch(DIV)
    {
        case T0_CLK_SOURCE_DIV_1:    div = 1;    break;
        case T0_CLK_SOURCE_DIV_8:    div = 8;    break;
        case T0_CLK_SOURCE_DIV_64:   div = 64;   break;
        case T0_CLK_SOURCE_DIV_256:  div = 256;  break;
        case T0_CLK_SOURCE_DIV_1024: div = 1024; break;

        default: return;
    }
    ocr0 = SYS_OSC_FREQUENCE / freq / div - 1;  // 運算結果是8位的、但分步的中間結果可能是16位或32位的
    ocr0 = ocr0 - 2;                            // 稍微減少一點OCR0、可以從PA1得到更精確的1ms,因爲進入中斷也需要十幾us

    // --------
    // 設置參數
    Drv_Timer0_init(T0_WGM_CTC, COM_MODE_NONE, DIV);
    Drv_Timer0_set_TCNT0_OCR0(0, ocr0);
    Drv_Timer0_INT_Enable(INT_MODE_OCF, ENABLE);
}

// ==========================================================================================================
// 初始化系統任務調度模塊
// 
// ==========================================================================================================
void sys_task_init(void)
{
    uint8_t index = 0;

    for(index = 0; index < SYS_TASK_MAX; index++)
    {
        sys_task_delete(index);
    }
}

// ==========================================================================================================
//      添加任務到任務隊列
// 
// 參數:
//      delay   任務延時計數
//      period  任務運行間隔
//      task    任務函數名
//      co_op   任務類型:1=合作式任務,0=搶佔式任務-(SYS_TASK_TYPE_CO_OP, SYS_TASK_TYPE_PRE_EM)
//      init    任務的初始化函數-(任務不再放到sys_init()裏面去初始化)
// 
// 返回值:
//      index   任務號,>=SYS_TASK_MAX==SYS_TASK_MAX的任務號是無效的任務號
// 
// ==========================================================================================================
uint8_t sys_task_add(const uint8_t delay, const uint8_t period, const p_void_funtion_void task, 
                     const uint8_t co_op, const p_void_funtion_void init)
{
    uint8_t index = 0;

    for(index = 0; index < SYS_TASK_MAX; index++)
    {
        if(NULL == sys_task_ctrl[index].task) { break; }
    }
    if(index < SYS_TASK_MAX)
    {
        sys_task_ctrl[index].number = index;
        sys_task_ctrl[index].co_op  = co_op;
        sys_task_ctrl[index].run    = (delay == 0) ? 1      : 0;
        sys_task_ctrl[index].delay  = (delay == 0) ? period : delay;
        sys_task_ctrl[index].period = period;
        sys_task_ctrl[index].task   = task;
        // 必須在任務設置完畢後才初始化
        // 因爲初始化裏面需要用到sys_task_index(),這涉及到sys_task_ctrl[index].task、sys_task_ctrl[index].number
        init();
    }
    return index;
}

// ==========================================================================================================
// 刪除任務隊列中的任務
// 
// ==========================================================================================================
void sys_task_delete(const uint8_t index)
{
    if(index < SYS_TASK_MAX)
    {
        if(NULL != sys_task_ctrl[index].task)
        {
            sys_task_ctrl[index].number = SYS_TASK_MAX;
            sys_task_ctrl[index].co_op  = SYS_TASK_TYPE_CO_OP;
            sys_task_ctrl[index].run    = 0;
            sys_task_ctrl[index].delay  = 0;
            sys_task_ctrl[index].period = 0;
            sys_task_ctrl[index].task   = NULL;
        }
    }
}

// ==========================================================================================================
// 獲取一個任務的任務號
// 
// 參數:
//      task    任務函數名
// 
// 返回值:
//      index   任務號,>=SYS_TASK_MAX的任務號是無效的任務號
// 
// ==========================================================================================================
uint8_t sys_task_index(const p_void_funtion_void task)
{
    uint8_t index = SYS_TASK_MAX;

    if(NULL != task)
    {
        for(index = 0; index < SYS_TASK_MAX; index++)
        {
            if(sys_task_ctrl[index].task == task)   // 指針可能需要轉換爲(uint16_t)來進行比較
            {
                index = sys_task_ctrl[index].number;
                break;
            }
        }
    }
    return index;
}

// ==========================================================================================================
//      任務調度函數
// 
// 說明:
// (1). 運行任務隊列中 所有 已經就緒的任務
// (2). .run > 1表示該任務之前有被延遲 >1個調度時刻
// 
// (3). 新時刻到來會立即打斷調度函數:
//      在運行過程中會被刷新任務狀態的ISR(TIMER0_COMP_vect)打斷,但由於任務是在每次時刻到來時被執行
//      所以每次打斷的時刻、都是剛纔任務被執行的時刻結束、而新時刻到來的時刻
//      此時、只要剛纔的任務沒超過或接近1ms,那麼在這個時刻到來前、該任務早就執行完畢了
//      所以、此時打斷並不影響剛纔的任務,也不會出現剛纔的任務執行完畢後、被加載在其後面執行的情況
// 
// (2). 單次任務:
//      將.period初始化爲0,該函數就只會被執行1次、執行完畢後會立即被刪除
//      sys_task_add(0, n, X, Xi)在初始化結束X後,X會立即被執行、或者在下一個時刻被執行:
//      1、如果任務隊列中、當前任務前面還有空餘位置,任務X就會被放在當前任務之前,那麼任務X會在下一個時刻被執行
//      2、如果任務隊列中、當前任務前面沒有空餘位置,任務X就會被放在當前任務之後,那麼任務X會在當前任務所在時刻被執行
//      由於任務X及其初始化函數Xi都會增加執行時間,所以必須考慮新任務不會導致當前任務超時
//      一般:
//      -->只執行一次的任務、儘量放到啓動任務隊列之前去執行,不放到任務隊列中
//      -->只執行一次的任務、儘量做得短小,其初始化一般爲空
//      可以不用支持單次運行的任務,這裏保留
// 
// (3). 每個任務都需要在退出任務前、解鎖自己鎖定的事件等資源
// 
// ==========================================================================================================
void sys_task_dispatch(void)
{
    uint8_t  index = 0;
    uint8_t  lock;

    bool event = FALSE;

    for(index = 0; index < SYS_TASK_MAX; index++)
    {
        // --------
        // 任務調度
        if(sys_task_ctrl[index].run > 0)
        {
            sys_task_ctrl[index].task();
            if(sys_task_ctrl[index].run > 1)
            {
                // 該任務曾經被延遲
                sys_update_event(EVENT_SYS, MSG_SYS_TASK_DELAYED, index);
            }
            sys_task_ctrl[index].run = 0;
            if(0 == sys_task_ctrl[index].period)
            {
                sys_task_delete(index);
            }
        }
        // --------
        // 事件管理
        lock = sys_event_any_lock();
        if(lock < EVENT_MAX)
        {
            // 有事件被鎖定了
            sys_update_event(EVENT_SYS, MSG_SYS_EVENT_LOCKED, lock);
        }
        event = sys_event_push();
    }
    // --------------------------------------------------------
    // 所有任務都執行完畢、且所有事件都已寫入事件隊列後,進入休眠
    if(TRUE == event)
    {
        task_sys_enter_sleep();
    }
}

// ==========================================================================================================
//      系統定時器(Timer0)中斷(中斷週期=1ms)
// 
// (1). 刷新任務狀態
// (2). sys_task_add中將.delay初始化爲0,意味着第1次進入 任務調度函數、就會執行這個任務
// (3). 搶佔式任務將直接在中斷中執行,所以要求儘量短小
// 
// ==========================================================================================================
ISR(TIMER0_COMP_vect)
{
    uint8_t index = 0;

    for(index = 0; index < SYS_TASK_MAX; index++)
    {
        temp2016 = index;
        if(NULL != sys_task_ctrl[index].task)
        {
            if(0 < sys_task_ctrl[index].delay)  // 任務延時計數
            {
                sys_task_ctrl[index].delay--;
            }
            if(0 == sys_task_ctrl[index].delay) // 任務就緒檢查
            {
                if(SYS_TASK_TYPE_CO_OP == sys_task_ctrl[index].co_op)
                {
                    if(sys_task_ctrl[index].run < SYS_TASK_RUN_MAX)
                    {
                        sys_task_ctrl[index].run++;
                    }
                }
                else
                {
                    sys_task_ctrl[index].task();
                    if(0 == sys_task_ctrl[index].period)
                    {
                        sys_task_ctrl[index].co_op = SYS_TASK_TYPE_CO_OP;
                        sys_task_ctrl[index].task  = NULL;
                    }
                }
                // 單次運行的任務將會被刪除、在被刪除前不再重新計數
                if(sys_task_ctrl[index].period > 0)
                {
                    sys_task_ctrl[index].delay = sys_task_ctrl[index].period;
                }
            }
        }
    }
}


-------------------------------------------------------------------------------------------------------------------------------------

6、事件觸發的任務調度

      引入事件管理之後、CPU可以根據事件來調度任務,而不是時間:

      由此可以設計一個事件觸發的調度器
      消息CPU統一查詢,任務不再查詢消息、只發出自己產生的消息
      根據事件來調度任務的方式很適合以輸出接口爲主要內容、或以輸入輸出爲主要內容的應用場合。

7、中斷的上下半部機制










0
發佈了42 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章