FreeRTOS學習記錄(轉)

原文地址:FreeRTOS學習記錄-ESP32 - 能跑就行_NPJX - 博客園 (cnblogs.com)

筆記的目的是,借此學習一下ESP32,且快速回顧一下FreeRTOS,在需要時,可以快速找到對應的概念和API接口。

ESP32使用FreeRTOS與原生FreeRTOS應用程序入口有所不同,

在 ESP-IDF 中使用 FreeRTOS 的用戶 永遠不應調用 vTaskStartScheduler()vTaskEndScheduler()。相反,ESP-IDF 會自動啟動 FreeRTOS。用戶必須定義一個 void app_main(void) 函數作為用戶應用程序的入口點,并在 ESP-IDF 啟動時被自動調用。

  • 通常,用戶會從 app_main 中啟動應用程序的其他任務。
  • app_main 函數可以在任何時候返回(應用終止前)。
  • app_main 函數由 main 任務調用。

以下FreeRTOS筆記無特殊說明外,默認為單核

任務

創(chuàng)建任務

  • 使用xTaskCreate()創(chuàng)建任務時,任務內存動態(tài)分配。
  • 使用xTaskCreateStatic()創(chuàng)建任務時,任務內存靜態(tài)分配,即由用戶提供。

執(zhí)行任務

  • 只能處于以下任一狀態(tài):運行中、就緒、阻塞或掛起。
  • 任務函數通常為無限循環(huán)。
  • 任務函數不應返回。

刪除任務

  • 使用vTaskDelete()刪除任務,若任務句柄為NULL,則會刪除當前正在運行的任務
  • 注意事項:
    • 請保證刪除任務時,任務是處于已知的狀態(tài)
      • 比如任務內部,運行完成,且釋放了任務內分配的資源,再進行刪除,不然會造成內存泄露
      • 刪除持有互斥鎖的任務,會導致別的任務永久鎖死

打印任務信息

  • 使用vTaskList()羅列出所有任務的當前狀態(tài),以及堆棧信息
  • 使用uxTaskGetStackHighWaterMark()獲取任務棧剩余空間,越接近0越代表接近溢出,可以通過這個值,監(jiān)測任務函數??臻g是否充足

空閑任務

  • ESP-IDF會隱式創(chuàng)建一個優(yōu)先級為 0 的空閑任務。當沒有其他任務準備運行時,空閑任務運行并有以下作用:

    • 釋放已刪除任務的內存
    • 執(zhí)行應用程序的空閑函數

任務看門狗定時器 (TWDT)

  • 任務看門狗定時器 (TWDT) 用于監(jiān)視特定任務,確保任務在配置的超時時間內執(zhí)行。

  • TWDT 主要監(jiān)視每個 CPU 的空閑任務

  • TWDT 是基于定時器組 0 中的硬件看門狗定時器構建的。超時發(fā)生時會觸發(fā)中斷。

  • 可以在用戶代碼中定義函數 esp_task_wdt_isr_user_handler 來接收超時事件,并擴展默認行為。

  • 調用以下函數,用 TWDT 監(jiān)視任務:

    • esp_task_wdt_init()初始化 TWDT 并訂閱空閑任務。
    • esp_task_wdt_add()為其他任務訂閱 TWDT。
    • 訂閱后,應從任務中調用esp_task_wdt_reset()來喂 TWDT。
    • esp_task_wdt_delete()可以取消之前訂閱的任務。
    • esp_task_wdt_deinit取消訂閱空閑任務并反初始化 TWDT。
  • 注意事項:

    • 擦除較大的 flash 區(qū)域可能會非常耗時,并可能導致任務連續(xù)運行,觸發(fā) TWDT 超時。以下兩種方法可以避免這種情況:

      • 延長看門狗超時時間。
      • 在擦除 flash 區(qū)域前,調用esp_task_wdt_init(),增加看門狗超時時間。

消息隊列

消息隊列就是通過 RTOS 內核提供的服務,任務或中斷服務子程序可以將一個消息(注意,FreeRTOS 消息隊列傳遞的是實際數據,并不是數據地址,RTX,uCOS-II 和 uCOS-III 是傳遞的地址)放入到隊列。

同樣,一個或者多個任務可以通過 RTOS 內核服務從隊列中得到消息。通常,先進入消息隊列的消息先傳 給任務,也就是說,任務先得到的是最先進入到消息隊列的消息,即先進先出的原則(FIFO),FreeRTOS 的消息隊列支持 FIFO 和 LIFO 兩種數據存取方式。

  • 消息隊列和全局變量相比,在FreeRTOS里更具以下優(yōu)勢:
*   使用消息隊列可以讓 RTOS 更有效管理任務,而全局變量是無法做到
    
    
    1.  比如,任務的超時等待機制,用全局變量則需要用戶自己去實現。
*   消息隊列支持FIFO,更有利于數據處理
    
    
*   使用全局數組,還需要處理多任務的訪問沖突,而消息隊列就處理好了這個問題
    
    
*   消息隊列可以有效解決中斷與任務之間通信問題
  • 使用消息隊列傳輸數據時有兩種方法:
1.  拷貝:把數據、把變量的值復制進隊列里
2.  引用:把數據、把變量的地址復制進隊列里

消息隊列--任務之間通信

image.png

消息隊列--中斷與任務之間通信

image.png
  • 注意

    • 在中斷發(fā)送消息需要使用 xQueueSendFromISR,且不支持超時設置,所以發(fā)送前要通過函數 xQueueIsQueueFullFromISR 檢測 消息隊列是否滿
    • 在中斷中處理越快越好,防止低于該優(yōu)先級的異常無法正常響應
    • 最好不要在中斷中處理消息隊列,只發(fā)送
    • 中斷服務程序中一定要調用專用于中斷的消息隊列函數,即以 FromISR 結尾的函數。

創(chuàng)建消息隊列

  • 動態(tài)分配
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,  /* 消息個數 */
                            UBaseType_t uxItemSize );   /* 每個消息大小,單位字節(jié) */
  • 靜態(tài)分配(一般不用這個)
QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength,    /* 消息個數 */
                                  UBaseType_t uxItemSize,       /* 每個消息大小,單位字節(jié) */
                                  uint8_t *pucQueueStorageBuffer, /* 如果uxItemSize非0,pucQueueStorageBuffer必須指向一個uint8_t數組,此數組大小至少為"uxQueueLength * uxItemSize" */
                                  StaticQueue_t *pxQueueBuffer ); /* 必須執(zhí)行一個StaticQueue_t結構體,用來保存隊列的數據結構 */
 

寫消息隊列

/*
 *  等同于xQueueSendToBack,往隊列尾部寫入數據,如果沒有空間,阻塞時間為xTicksToWait
 */
BaseType_t xQueueSend( QueueHandle_t xQueue,        /* 消息隊列句柄 */
                       const void * pvItemToQueue,  /* 要傳遞數據地址 */
                       TickType_t xTicksToWait );   /* 等待消息隊列有空間的最大等待時間 */
 
/* 
 * 往隊列尾部寫入數據,如果沒有空間,阻塞時間為xTicksToWait
 */
BaseType_t xQueueSendToBack(
                                QueueHandle_t    xQueue,
                                const void       *pvItemToQueue,
                                TickType_t       xTicksToWait
                            );
/* 
 * 往隊列尾部寫入數據,此函數可以在中斷函數中使用,不可阻塞
 */
BaseType_t xQueueSendToBackFromISR(
                                      QueueHandle_t xQueue,
                                      const void *pvItemToQueue,
                                      BaseType_t *pxHigherPriorityTaskWoken
                                   );
 
/* 
 * 往隊列頭部寫入數據,如果沒有空間,阻塞時間為xTicksToWait
 */
BaseType_t xQueueSendToFront(
                                QueueHandle_t    xQueue,
                                const void       *pvItemToQueue,
                                TickType_t       xTicksToWait
                            );
 
 
/* 
 * 往隊列頭部寫入數據,此函數可以在中斷函數中使用,不可阻塞
 */
BaseType_t xQueueSendToFrontFromISR(
                                      QueueHandle_t xQueue,
                                      const void *pvItemToQueue,
                                      BaseType_t *pxHigherPriorityTaskWoken
                                   );

讀消息隊列

/* 讀到一個數據后,隊列中該數據會被移除 */
 
BaseType_t xQueueReceive( QueueHandle_t xQueue,     /* 消息隊列句柄 */
                          void * const pvBuffer,    /* bufer指針,隊列的數據會被復制到這個buffer -*+*/
                          TickType_t xTicksToWait );/* 等待消息隊列有空間的最大等待時間 */
 
BaseType_t xQueueReceiveFromISR( QueueHandle_t    xQueue,
                                 void             *pvBuffer,
                                 BaseType_t       *pxTaskWoken );

刪除消息隊列

/* vQueueDelete()只能刪除使用動態(tài)方法創(chuàng)建的隊列,它會釋放內存 */
void vQueueDelete( QueueHandle_t xQueue );

查詢消息隊列

/*
 * 返回隊列中可用數據的個數
 */
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
 
/*
 * 返回隊列中可用空間的個數
 */
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

覆蓋/窺視

  • 覆蓋

    當隊列長度為1時(隊列長度必須為1才可以使用),可以使用xQueueOverwrite()xQueueOverwriteFromISR()來覆蓋數據

    在FreeRTOS中,隊列的發(fā)送和接收操作是原子的,也就是說,在執(zhí)行這些操作期間,不會被中斷或其他任務打斷。這確保了數據的完整性和可靠性。

    而覆蓋操作是一種特殊情況,它允許在隊列滿時替換隊列中最早的消息,并添加新的消息。

    為了保持原子性,只有在隊列長度為1時,才能保證覆蓋操作的一致性。

    如果隊列長度大于1,那么在進行覆蓋操作時,可能會涉及多個元素的移動和替換。 由于隊列操作必須是原子的,這將涉及更復雜的同步和保護機制,以確保數據的一致性。這不僅增加了實現的復雜度,還可能引入競爭條件和死鎖等問題。

/* 覆蓋隊列
 * xQueue: 寫哪個隊列
 * pvItemToQueue: 數據地址
 * 返回值: pdTRUE表示成功, pdFALSE表示失敗
 */
BaseType_t xQueueOverwrite(
                           QueueHandle_t xQueue,
                           const void * pvItemToQueue
                      );
 
BaseType_t xQueueOverwriteFromISR(
                           QueueHandle_t xQueue,
                           const void * pvItemToQueue,
                           BaseType_t *pxHigherPriorityTaskWoken
                      );
  • 窺視

    想讓隊列中的數據供多方讀取,也就是說讀取時不要移除數據,要留給后來人。那么可以使用"窺視",也就是xQueuePeek()xQueuePeekFromISR()。這些函數會從隊列中復制出數據,但是不移除數據。

    這也意味著,如果隊列中沒有數據,那么"偷看"時會導致阻塞;一旦隊列中有數據,以后每次"偷看"都會成功。

/* 偷看隊列
* xQueue: 偷看哪個隊列
* pvItemToQueue: 數據地址, 用來保存復制出來的數據
* xTicksToWait: 沒有數據的話阻塞一會
* 返回值: pdTRUE表示成功, pdFALSE表示失敗
*/
BaseType_t xQueuePeek(
                        QueueHandle_t xQueue,
                        void * const pvBuffer,
                        TickType_t xTicksToWait
                    );

BaseType_t xQueuePeekFromISR(
                               QueueHandle_t xQueue,
                               void *pvBuffer,
                           );

隊列集合

知道有這個即可,實際很少用到!

隊列郵箱

郵箱的概念,其實就是,長度為1的消息隊列,使用覆蓋函數,之所以是覆蓋函數,就是不管隊列有沒有值,都能及時更新,不會陷入阻塞的情況,發(fā)送任務發(fā)送數據后,隊列就成了郵箱,其他任務都在”訂閱“郵箱,使用窺視函數去,只獲取值,不刪除數據,達到一個發(fā)送,多個接收。

這個也是知道概念即可,具體用到的函數也是上述幾個。

具體流程如下


image.png

使用注意

  1. 分辨數據源
    1. 當有多個發(fā)送任務,通過同一個隊列發(fā)出數據,接收任務如何分辨數據來源?數據本身帶有"來源"信息,比如寫入隊列的數據是一個結構體,結構體中的lDataSouceID用來表示數據來源:
typedef struct {
    ID_t eDataID;
    int32_t lDataValue;
}Data_t;
/* 不同的發(fā)送任務,先構造好結構體,填入自己的eDataID,再寫隊列;接收任務讀出數據后,根據eDataID就可以知道數據來源了 */
  1. 傳輸大塊數據
    1. 因為FreeRTOS的隊列使用拷貝傳輸,如果是用uint32_t類型,那就拷貝4字節(jié),如果用uint8_t類型,那就拷貝1字節(jié),但是如果要拷貝很大的數據,比如uint8_t data[1000],寫隊列的時候直接拷貝1000個字節(jié)嗎?讀隊列連續(xù)讀1000個字節(jié)??那樣效率未免也太低了吧。更為合適的方法是 使用地址來間接傳數據
/* 比如 send_task 里面 malloc(1000)字節(jié),把地址通過隊列寫進去
    在 recive_task 里面 free() 釋放空間,使用時要注意,成對出現
 
    不要未使用就釋放內存,導致野指針
    也不要忘記釋放內存,導致內存泄漏
 */
void send_task(void *arg)
{
    QueueHandle_t QueueHandle1 = (QueueHandle_t)arg;
    BaseType_t status;
 
    while(1) {
        char *pStrSend = malloc(1000);  //分配內存,并拷貝數據(我忽略了這步)
        status = xQueueSend(QueueHandle1, &pStrSend , 5000);
        if (status == pdPASS) {
            printf("send success\n");
        } else {
            printf("send fail\n");
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}
 
void recive_task(void *arg)
{
    QueueHandle_t QueueHandle1 = (QueueHandle_t)arg;
    BaseType_t status;
    char *pStrRecive;
 
    while(1) {
        status = xQueueReceive(QueueHandle1, &pStrRecive, 0);
        if (status == pdPASS) {
            // 在這里處理數據,隨后并釋放空間
            free(pStrRecive);
            printf("pStrRecive:%s\n\n", pStrRecive);
        } else {
            printf("recive fail\n");
        }
        
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

信號量

  • 信號:起通知作用
  • 量:還可以用來表示資源的數量
    • 當"量"沒有限制時,它就是"計數型信號量"(Counting Semaphores)
    • 當"量"只有0、1兩個取值時,它就是"二進制信號量"(Binary Semaphores)
  • 支持的動作都一樣,give--給出資源,take--拿走資源,計數值減1

二進制信號量跟計數型的唯一差別,就是計數值的最大值被限定為1。

信號量的"give"、"take"雙方并不需要相同,可以用于生產者-消費者場合:即多個任務產生信號量,多個任務消費信號量

計數型信號量

/* 創(chuàng)建一個計數型信號量,返回它的句柄。
 * 此函數內部會分配信號量結構體 
 * uxMaxCount: 最大計數值
 * uxInitialCount: 初始計數值
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
 
/* 創(chuàng)建一個計數型信號量,返回它的句柄。
 * 此函數無需動態(tài)分配內存,所以需要先有一個StaticSemaphore_t結構體,并傳入它的指針
 * uxMaxCount: 最大計數值
 * uxInitialCount: 初始計數值
 * pxSemaphoreBuffer: StaticSemaphore_t結構體指針
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, 
                                                 UBaseType_t uxInitialCount, 
                                                 StaticSemaphore_t *pxSemaphoreBuffer );

二進制信號量

信號量在創(chuàng)建后是空的狀態(tài),在調用take獲取之前,需要先give釋放一個資源出來

/* 創(chuàng)建一個二進制信號量,返回它的句柄。
 * 此函數內部會分配信號量結構體 
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateBinary( void );
 
/* 創(chuàng)建一個二進制信號量,返回它的句柄。
 * 此函數無需動態(tài)分配內存,所以需要先有一個StaticSemaphore_t結構體,并傳入它的指針
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );

give/take 信號量

  • 關于give的函數
/* 在任務中使用,釋放信號量
 * xSemaphore:信號量句柄
 * 返回值: pdTRUE表示成功,
 *          如果二進制信號量的計數值已經是1,再次調用此函數則返回失?。? *          如果計數型信號量的計數值已經是最大值,再次調用此函數則返回失敗
 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
 
/* 在ISR中使用,釋放信號量
 * xSemaphore:信號量句柄
 * pxHigherPriorityTaskWoken:如果釋放信號量導致更高優(yōu)先級的任務變?yōu)榱司途w態(tài),
 *                            則*pxHigherPriorityTaskWoken = pdTRUE
 * 返回值: pdTRUE表示成功,
 *          如果二進制信號量的計數值已經是1,再次調用此函數則返回失??;
 *          如果計數型信號量的計數值已經是最大值,再次調用此函數則返回失敗
 */
BaseType_t xSemaphoreGiveFromISR(
                        SemaphoreHandle_t xSemaphore,
                        BaseType_t *pxHigherPriorityTaskWoken
                    );
 
  • 關于take的函數
/* 在任務中使用,獲取信號量
 * xSemaphore:信號量句柄
 * xTicksToWait:阻塞時間,0:不阻塞,馬上返回, portMAX_DELAY: 一直阻塞直到成功,
 *               其他值: 阻塞的Tick個數,可以使用pdMS_TO_TICKS()來指定阻塞時間為若干ms
 * 返回值: pdTRUE表示成功
 */
BaseType_t xSemaphoreTake(
                   SemaphoreHandle_t xSemaphore,
                   TickType_t xTicksToWait
               );
 
 
/* 在ISR中使用,獲取信號量
 * xSemaphore:信號量句柄
 * pxHigherPriorityTaskWoken:如果獲取信號量導致更高優(yōu)先級的任務變?yōu)榱司途w態(tài),
 *                            則*pxHigherPriorityTaskWoken = pdTRUE
 * 返回值: pdTRUE表示成功,
 */
BaseType_t xSemaphoreTakeFromISR(
                        SemaphoreHandle_t xSemaphore,
                        BaseType_t *pxHigherPriorityTaskWoken
                    );

獲取信號量數量

/* * xSemaphore: 信號量句柄 * 返回值:   信號量個數 */uxSemaphoreGetCount( xSemaphore );

刪除信號量

對于動態(tài)創(chuàng)建的信號量,不再需要它們時,可以刪除它們以回收內存。
vSemaphoreDelete可以用來刪除二進制信號量、計數型信號量。

/*
 * xSemaphore: 信號量句柄,你要刪除哪個信號量
 */
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

互斥量

互斥量的用途:用來實現互斥訪問

值只有 0 和 1

資源互斥的核心在于:誰上鎖,就只能由誰開鎖 (代碼上又并沒有實現這點,只是約定成俗,誰上鎖誰開鎖)

提及兩個概念:

  • 對變量的非原子化訪問

    修改變量、設置結構體、在16位的機器上寫32位的變量,這些操作都是非原子的。也就是它們的操作過程都可能被打斷,如果被打斷的過程有其他任務來操作這些變量,就可能導致沖突。

  • 函數重入

    “可重入的函數"是指:多個任務同時調用它、任務和中斷同時調用它,函數的運行也是安全的。可重入的函數也被稱為"線程安全”(thread safe)。 每個任務都維持自己的棧、自己的CPU寄存器,如果一個函數只使用局部變量,那么它就是線程安全的。 函數中一旦使用了全局變量、靜態(tài)變量、其他外設,它就不是"可重入的",如果改函數正在被調用,就必須阻止其他任務、中斷再次調用它。

    任務A訪問這些全局變量、函數代碼時,獨占它,就是上個鎖。這些全局變量、函數代碼必須被獨占地使用,它們被稱為臨界資源。

互斥量有以下特點需要注意:

  • 剛創(chuàng)建的互斥量可以被成功"take"
  • “take"互斥量成功的任務,被稱為"holder”,只能由它"give"互斥量;別的任務"give"不成功
  • 在ISR中不能使用互斥量
  • 互斥量會去“繼承”,企圖獲取互斥量的任務的優(yōu)先級

創(chuàng)建互斥量

互斥量初始值為1

/* 創(chuàng)建一個互斥量,返回它的句柄。
 * 此函數內部會分配互斥量結構體 
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateMutex( void );
 
/* 創(chuàng)建一個互斥量,返回它的句柄。
 * 此函數無需動態(tài)分配內存,所以需要先有一個StaticSemaphore_t結構體,并傳入它的指針
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );

刪除互斥量

/*
 * xSemaphore: 信號量句柄,你要刪除哪個信號量, 互斥量也是一種信號量
 */
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

獲取、釋放互斥量

/* 釋放 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
 
/* 釋放(ISR版本) */
BaseType_t xSemaphoreGiveFromISR(
                       SemaphoreHandle_t xSemaphore,
                       BaseType_t *pxHigherPriorityTaskWoken
                   );
 
/* 獲得 */
BaseType_t xSemaphoreTake(
                   SemaphoreHandle_t xSemaphore,
                   TickType_t xTicksToWait
               );
/* 獲得(ISR版本) */
xSemaphoreGiveFromISR(
                       SemaphoreHandle_t xSemaphore,
                       BaseType_t *pxHigherPriorityTaskWoken
                   );

死鎖

死鎖又分為兩種情況:

  • 互斥死鎖

    • 假設有2個互斥量M1、M2,2個任務A、B:

      • A獲得了互斥量M1
      • B獲得了互斥量M2
      • A還要獲得互斥量M2才能運行,結果A阻塞
      • B還要獲得互斥量M1才能運行,結果B阻塞
      • A、B都阻塞,再無法釋放它們持有的互斥量
      • 死鎖發(fā)生!
  • 自我死鎖

    • 任務A獲得了互斥鎖M
    • 它調用一個庫函數
    • 庫函數要去獲取同一個互斥鎖M,于是它阻塞:任務A休眠,等待任務A來釋放互斥鎖!
    • 死鎖發(fā)生!

為了解決上訴死鎖問題,又衍生出一種 遞歸互斥量

遞歸互斥量

遞歸互斥量實現了:誰上鎖就由誰解鎖。

遞歸鎖 一般互斥量x
創(chuàng)建 xSemaphoreCreateRecursiveMutex xSemaphoreCreateMutex
獲得 xSemaphoreTakeRecursive xSemaphoreTake
釋放 xSemaphoreGiveRecursive xSemaphoreGive

假設任務1 需要去對資源A訪問,并且資源A需要對資源B進行訪問,如果每次訪問都加一個普通互斥量,那這對代碼維護也十分麻煩,就有了遞歸互斥量


image.png

事件標志組

事件標志組是實現多任務同步的有效機制之一。

事件標志組和全局變量相比,在FreeRTOS里更具以下優(yōu)勢:

  1. 使用事件標志組可以讓 RTOS 內核有效地管理任務,而全局變量是無法做到
    1. 比如,任務的超時等機制, 用全局變量則需要用戶自己去實現。
  2. 使用了全局變量就要防止多任務的訪問沖突,而使用事件標志組則處理好了這個問題。
  1. 使用事件標志組可以有效地解決中斷服務程序和任務之間的同步問題。
    TickType_t 數據類型可以是 16 位數或者 32 位數

創(chuàng)建事件標志組

/* 創(chuàng)建一個事件組,返回它的句柄。
 * 此函數內部會分配事件組結構體 
 * 返回值: 返回句柄,非NULL表示成功
 */
EventGroupHandle_t xEventGroupCreate( void );
 
/* 創(chuàng)建一個事件組,返回它的句柄。
 * 此函數無需動態(tài)分配內存,所以需要先有一個StaticEventGroup_t結構體,并傳入它的指針
 * 返回值: 返回句柄,非NULL表示成功
 */
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t * pxEventGroupBuffer );

刪除事件組

/*
 * xEventGroup: 事件組句柄,你要刪除哪個事件組
 */
void vEventGroupDelete( EventGroupHandle_t xEventGroup )

等待事件標志位

EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
                                 const EventBits_t uxBitsToWaitFor,
                                 const BaseType_t xClearOnExit,
                                 const BaseType_t xWaitForAllBits,
                                 TickType_t xTicksToWait );

設置事件標志位

  • 使用xEventGroupSetBits(),不可以在中斷服務程序中調用此函數

    • 返回當前的事件標志組數值

    • 用戶通過函數設置的標志位,并不一定會保留到此函數的返回值中,下面舉兩種情況:

      1. 調用此函數的過程中,其它高優(yōu)先級的任務就緒了,并且也修改了事件標志,此函數返回的事件 標志位會發(fā)生變化。
      2. 調用此函數的任務是一個低優(yōu)先級任務,通過此函數設置了事件標志后,讓一個等待此事件標志的高優(yōu)先級任務就緒了,會立即切換到高優(yōu)先級任務去執(zhí)行,相應的事件標志位會被函數 xEventGroupWaitBits 清除掉,等從高優(yōu)先級任務返回到低優(yōu)先級任務后,函數 xEventGroupSetBits 的返回值已經被修改。
/* 設置事件組中的位
 * xEventGroup: 哪個事件組
 * uxBitsToSet: 設置哪些位? 
 *              如果uxBitsToSet的bitX, bitY為1, 那么事件組中的bitX, bitY被設置為1
 *               可以用來設置多個位,比如 0x15 就表示設置bit4, bit2, bit0
 * 返回值: 返回原來的事件值(沒什么意義, 因為很可能已經被其他任務修改了)
 */
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
                                    const EventBits_t uxBitsToSet );
 
 
/* 設置事件組中的位
 * xEventGroup: 哪個事件組
 * uxBitsToSet: 設置哪些位? 
 *              如果uxBitsToSet的bitX, bitY為1, 那么事件組中的bitX, bitY被設置為1
 *               可以用來設置多個位,比如 0x15 就表示設置bit4, bit2, bit0
 * pxHigherPriorityTaskWoken: 有沒有導致更高優(yōu)先級的任務進入就緒態(tài)? pdTRUE-有, pdFALSE-沒有
 * 返回值: pdPASS-成功, pdFALSE-失敗
 */
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
                                      const EventBits_t uxBitsToSet,
                                      BaseType_t * pxHigherPriorityTaskWoken );

中斷中設置事件標志位

  • 使用xEventGroupSetBitsFromISR(),在中斷中使用的是這個
  • 設置事件組時,有可能導致多個任務被喚醒,這會帶來很大的不確定性。所以xEventGroupSetBitsFromISR函數不是直接去設置事件組,而是給一個FreeRTOS后臺任務(daemon task)發(fā)送隊列數據,由這個任務來設置事件組。

事件組同步

有一個事情需要多個任務協(xié)同,使用xEventGroupSync()函數可以同步多個任務:

  • 可以設置某位、某些位,表示自己做了什么事
  • 可以等待某位、某些位,表示要等等其他任務
  • 期望的時間發(fā)生后,xEventGroupSync()才會成功返回。
  • xEventGroupSync成功返回后,會清除事件
EventBits_t xEventGroupSync(    EventGroupHandle_t xEventGroup,
                                const EventBits_t uxBitsToSet,
                                const EventBits_t uxBitsToWaitFor,
                                TickType_t xTicksToWait );

任務通知

任務通知,簡單概括,就是具體通知到哪個任務去運行

我們使用隊列、信號量、事件組等等方法時,并不知道對方是誰。使用任務通知時,可以明確指定:通知哪個任務。

使用隊列、信號量、事件組時,我們都要事先創(chuàng)建對應的結構體,雙方通過中間的結構體通信:


image.png

使用任務通知時,任務結構體TCB中就包含了內部對象,可以直接接收別人發(fā)過來的"通知":


image.png

任務通知的特性

  • 優(yōu)勢
    • 效率更高:使用任務通知來發(fā)送事件、數據給某個任務時,效率更高。比隊列、信號量、事件組都有大的優(yōu)勢。
    • 更節(jié)省內存:使用其他方法時都要先創(chuàng)建對應的結構體,使用任務通知時無需額外創(chuàng)建結構體。
  • 限制
    • 不能發(fā)送數據給ISR: ISR并沒有任務結構體,所以無法使用任務通知的功能給ISR發(fā)送數據。但是ISR可以使用任務通知的功能,發(fā)數據給任務。
    • 數據只能給該任務獨享
      使用隊列、信號量、事件組時,數據保存在這些結構體中,其他任務、ISR都可以訪問這些數據。使用任務通知時,數據存放入目標任務中,只有它可以訪問這些數據。 在日常工作中,這個限制影響不大。因為很多場合是從多個數據源把數據發(fā)給某個任務,而不是把一個數據源的數據發(fā)給多個任務。
    • 無法緩沖數據
      使用隊列時,假設隊列深度為N,那么它可以保持N個數據。 使用任務通知時,任務結構體中只有一個任務通知值,只能保持一個數據。 無法廣播給多個任務 使用事件組可以同時給多個任務發(fā)送事件。 使用任務通知,只能發(fā)個一個任務。 如果發(fā)送受阻,發(fā)送方無法進入阻塞狀態(tài)等待 假設隊列已經滿了,使用xQueueSendToBack()給隊列發(fā)送數據時,任務可以進入阻塞狀態(tài)等待發(fā)送完成。 使用任務通知時,即使對方無法接收數據,發(fā)送方也無法阻塞等待,只能即刻返回錯誤。

通知狀態(tài)和通知值

每個任務都有一個結構體:TCB(Task Control Block),里面有2個成員:

  • 一個是uint8_t類型,用來表示通知狀態(tài)
  • 一個是uint32_t類型,用來表示通知值
typedef struct tskTaskControlBlock
{
    ......
    /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */
    volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    ......
} tskTCB;

通知狀態(tài)有3種取值

/* 任務沒有在等待通知,也是初始狀態(tài) */
#define taskNOT_WAITING_NOTIFICATION              ( ( uint8_t ) 0 )
/* 任務在等待通知 */
#define taskWAITING_NOTIFICATION                  ( ( uint8_t ) 1 )
/* 任務接收到了通知,也被稱為pending(有數據了,待處理) */
#define taskNOTIFICATION_RECEIVED                 ( ( uint8_t ) 2 )

任務通知的使用

使用任務通知,可以實現輕量級的隊列(長度為1)、郵箱(覆蓋的隊列)、計數型信號量、二進制信號量、事件組。

任務通知有2套函數,簡化版、專業(yè)版,列表如下:

簡化版函數的使用比較簡單,它實際上也是使用專業(yè)版函數實現的
專業(yè)版函數支持很多參數,可以實現很多功能

簡化版 專業(yè)版
發(fā)出通知 xTaskNotifyGive vTaskNotifyGiveFromISR xTaskNotify xTaskNotifyFromISR
取出通知 ulTaskNotifyTake xTaskNotifyWait

具體這塊內容,使用時再找demo看即可,大概了解使用就好,一般較少使用

軟件定時器

  • 軟件定時器分為兩種狀態(tài):
    • 運行(Running、Active):運行態(tài)的定時器,當指定時間到達之后,它的回調函數會被調用
    • 冬眠(Dormant):冬眠態(tài)的定時器還可以通過句柄來訪問它,但是它不再運行,它的回調函數不會被調用
  • 軟件定時器工作原理:
    • 首先,FreeRTOS有個Tick中斷,軟件定時器是基于Tick運行,按照非操作系統(tǒng)的理解,我們是在Tick中斷里計數,達到值就調用定時器回調,但是在RTOS里,它不允許在內核、在中斷中執(zhí)行不確定的代碼:如果定時器函數很耗時,會影響整個系統(tǒng)。

      所以,FreeRTOS中,不在Tick中斷中執(zhí)行定時器函數。

    • 在FreeRTOS中,有個RTOS守護任務(RTOS Daemon Task),該任務跟普通任務基本一樣,不過守護任務的流程只有

      • 處理命令:從命令隊列里取出命令、處理
      • 執(zhí)行定時器的回調函數
        <mark style="margin: 0px; padding: 0px;">定時器的回調函數是在守護任務中被調用的,守護任務不是專為某個定時器服務的,它還要處理其他定時器</mark>。注意如下:
      • 回調函數要盡快實行,不能進入阻塞狀態(tài)
      • 不要調用會導致阻塞的API函數,比如vTaskDelay()
      • 可以調用xQueueReceive()之類的函數,但是超時時間要設為0:即刻返回,不可阻塞

創(chuàng)建軟件定時器

/* 使用動態(tài)分配內存的方法創(chuàng)建定時器
 * pcTimerName:定時器名字, 用處不大, 盡在調試時用到
 * xTimerPeriodInTicks: 周期, 以Tick為單位
 * uxAutoReload: 類型, pdTRUE表示自動加載, pdFALSE表示一次性
 * pvTimerID: 回調函數可以使用此參數, 比如分辨是哪個定時器
 * pxCallbackFunction: 回調函數
 * 返回值: 成功則返回TimerHandle_t, 否則返回NULL
 */
TimerHandle_t xTimerCreate( const char * const pcTimerName, 
                            const TickType_t xTimerPeriodInTicks,
                            const UBaseType_t uxAutoReload,
                            void * const pvTimerID,
                            TimerCallbackFunction_t pxCallbackFunction );
 
/* 使用靜態(tài)分配內存的方法創(chuàng)建定時器
 * pcTimerName:定時器名字, 用處不大, 盡在調試時用到
 * xTimerPeriodInTicks: 周期, 以Tick為單位
 * uxAutoReload: 類型, pdTRUE表示自動加載, pdFALSE表示一次性
 * pvTimerID: 回調函數可以使用此參數, 比如分辨是哪個定時器
 * pxCallbackFunction: 回調函數
 * pxTimerBuffer: 傳入一個StaticTimer_t結構體, 將在上面構造定時器
 * 返回值: 成功則返回TimerHandle_t, 否則返回NULL
 */
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
                                 TickType_t xTimerPeriodInTicks,
                                 UBaseType_t uxAutoReload,
                                 void * pvTimerID,
                                 TimerCallbackFunction_t pxCallbackFunction,
                                 StaticTimer_t *pxTimerBuffer );

回調函數類型

void ATimerCallback( TimerHandle_t xTimer );
 
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );

刪除軟件定時器

/* 刪除定時器
 * xTimer: 要刪除哪個定時器
 * xTicksToWait: 超時時間
 * 返回值: pdFAIL表示"刪除命令"在xTicksToWait個Tick內無法寫入隊列
 *        pdPASS表示成功
 */
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );

啟動、暫停、復位

  • 啟動

    啟動定時器就是設置它的狀態(tài)為運行態(tài)(Running、Active)

    這些函數的xTicksToWait表示的是,把命令寫入命令隊列的超時時間。命令隊列可能已經滿了,無法馬上把命令寫入隊列里,可以等待一會。

    xTicksToWait不是定時器本身的超時時間,不是定時器本身的"周期"。

    如果定時器已經被啟動,但是它的函數尚未被執(zhí)行,再次執(zhí)行xTimerStart()函數相當于執(zhí)行xTimerReset(),重新設定它的啟動時間。

/* 啟動定時器
 * xTimer: 哪個定時器
 * xTicksToWait: 超時時間
 * 返回值: pdFAIL表示"啟動命令"在xTicksToWait個Tick內無法寫入隊列
 *        pdPASS表示成功
 */
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
 
/* 啟動定時器(ISR版本)
 * xTimer: 哪個定時器
 * pxHigherPriorityTaskWoken: 向隊列發(fā)出命令使得守護任務被喚醒,
 *                            如果守護任務的優(yōu)先級比當前任務的高,
 *                            則"*pxHigherPriorityTaskWoken = pdTRUE",
 *                            表示需要進行任務調度
 * 返回值: pdFAIL表示"啟動命令"無法寫入隊列
 *        pdPASS表示成功
 */
BaseType_t xTimerStartFromISR(   TimerHandle_t xTimer,
                                 BaseType_t *pxHigherPriorityTaskWoken );
  • 暫停

    停止定時器就是設置它的狀態(tài)為冬眠(Dormant),讓它不能運行

/* 停止定時器
 * xTimer: 哪個定時器
 * xTicksToWait: 超時時間
 * 返回值: pdFAIL表示"停止命令"在xTicksToWait個Tick內無法寫入隊列
 *        pdPASS表示成功
 */
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
 
/* 停止定時器(ISR版本)
 * xTimer: 哪個定時器
 * pxHigherPriorityTaskWoken: 向隊列發(fā)出命令使得守護任務被喚醒,
 *                            如果守護任務的優(yōu)先級比當前任務的高,
 *                            則"*pxHigherPriorityTaskWoken = pdTRUE",
 *                            表示需要進行任務調度
 * 返回值: pdFAIL表示"停止命令"無法寫入隊列
 *        pdPASS表示成功
 */
BaseType_t xTimerStopFromISR(    TimerHandle_t xTimer,
                                 BaseType_t *pxHigherPriorityTaskWoken );
 
  • 復位

    xTimerReset()函數

    • 讓定時器的狀態(tài)從冬眠態(tài)轉換為運行態(tài),相當于使用xTimerStart()函數
    • 如果定時器已經處于運行態(tài),使用xTimerReset()函數就相當于重新確定超時時間。
/* 復位定時器
 * xTimer: 哪個定時器
 * xTicksToWait: 超時時間
 * 返回值: pdFAIL表示"復位命令"在xTicksToWait個Tick內無法寫入隊列
 *        pdPASS表示成功
 */
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
 
/* 復位定時器(ISR版本)
 * xTimer: 哪個定時器
 * pxHigherPriorityTaskWoken: 向隊列發(fā)出命令使得守護任務被喚醒,
 *                            如果守護任務的優(yōu)先級比當前任務的高,
 *                            則"*pxHigherPriorityTaskWoken = pdTRUE",
 *                            表示需要進行任務調度
 * 返回值: pdFAIL表示"停止命令"無法寫入隊列
 *        pdPASS表示成功
 */
BaseType_t xTimerResetFromISR(   TimerHandle_t xTimer,
                                 BaseType_t *pxHigherPriorityTaskWoken );

修改周期

/* 修改定時器的周期
 * xTimer: 哪個定時器
 * xNewPeriod: 新周期
 * xTicksToWait: 超時時間, 命令寫入隊列的超時時間 
 * 返回值: pdFAIL表示"修改周期命令"在xTicksToWait個Tick內無法寫入隊列
 *        pdPASS表示成功
 */
BaseType_t xTimerChangePeriod(   TimerHandle_t xTimer,
                                 TickType_t xNewPeriod,
                                 TickType_t xTicksToWait );
 
/* 修改定時器的周期
 * xTimer: 哪個定時器
 * xNewPeriod: 新周期
 * pxHigherPriorityTaskWoken: 向隊列發(fā)出命令使得守護任務被喚醒,
 *                            如果守護任務的優(yōu)先級比當前任務的高,
 *                            則"*pxHigherPriorityTaskWoken = pdTRUE",
 *                            表示需要進行任務調度
 * 返回值: pdFAIL表示"修改周期命令"在xTicksToWait個Tick內無法寫入隊列
 *        pdPASS表示成功
 */
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer,
                                      TickType_t xNewPeriod,
                                      BaseType_t *pxHigherPriorityTaskWoken );

定時器ID

  • 用途:
    • 可以用來標記定時器,表示自己是什么定時器
    • 可以用來保存參數,給回調函數使用
  • 接口類型:
    • 更新ID:使用vTimerSetTimerID()函數
    • 查詢ID:查詢pvTimerGetTimerID()函數
      這兩個函數不涉及命令隊列,它們是直接操作定時器結構體
/* 獲得定時器的ID
 * xTimer: 哪個定時器
 * 返回值: 定時器的ID
 */
void *pvTimerGetTimerID( TimerHandle_t xTimer );
 
/* 設置定時器的ID
 * xTimer: 哪個定時器
 * pvNewID: 新ID
 * 返回值: 無
 */
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );

中斷處理

FreeRTOS中很多API函數都有兩套:一套在任務中使用,另一套在ISR中使用。后者的函數名含有"FromISR"后綴。

為什么要引入兩套API函數?(在任務中、在ISR中,這些函數的功能是有差別的)

  • 很多API函數會導致任務計入阻塞狀態(tài):
*   運行這個函數的**任務**進入阻塞狀態(tài)
*   比如寫隊列時,如果隊列已滿,可以進入阻塞狀態(tài)等待一會
  • ISR調用API函數時,ISR不是"任務",ISR不能進入阻塞狀態(tài)

如果使用一套函數的話,則需要在函數內部進行判斷,這樣大量增加復雜代碼,會更難以測試,并且不同平臺內部框架也不一樣,這也大大加大了代碼的復雜度。

中斷的延遲處理

在中斷中處理內容,盡量要快,這是因為:

  • 其他低優(yōu)先級的中斷無法被處理:實時性無法保證。
  • 用戶任務無法被執(zhí)行:系統(tǒng)顯得很卡頓。
  • 如果運行中斷嵌套,這會更復雜,ISR越快執(zhí)行約有助于中斷嵌套。

如果這個硬件中斷的處理,就是非常耗費時間呢?對于這類中斷的處理就要分為2部分:

  • ISR:盡快做些清理、記錄工作,然后觸發(fā)某個任務
  • 任務:更復雜的事情放在任務中處理

資源管理

臨界資源

要獨占式地訪問臨界資源,有3種方法:

  • 公平競爭:比如使用互斥量,誰先獲得互斥量誰就訪問臨界資源,這部分內容前面講過。
  • 誰要跟我搶,我就滅掉誰:
*   中斷要跟我搶?我屏蔽中斷
*   其他任務要跟我搶?我禁止調度器,不運行任務切換

在任務中屏蔽中斷

/* 在任務中,當前時刻中斷是使能的
 * 執(zhí)行這句代碼后,屏蔽中斷
 */
taskENTER_CRITICAL();
 
/* 訪問臨界資源 */
 
/* 重新使能中斷 */
taskEXIT_CRITICAL();

在ISR中屏蔽中斷

void vAnInterruptServiceRoutine( void )
{
    /* 用來記錄當前中斷是否使能 */
    UBaseType_t uxSavedInterruptStatus;
    
    /* 在ISR中,當前時刻中斷可能是使能的,也可能是禁止的
     * 所以要記錄當前狀態(tài), 后面要恢復為原先的狀態(tài)
     * 執(zhí)行這句代碼后,屏蔽中斷
     */
    uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
    
    /* 訪問臨界資源 */
 
    /* 恢復中斷狀態(tài) */
    taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
    /* 現在,當前ISR可以被更高優(yōu)先級的中斷打斷了 */
}

暫停調度器

/* 暫停調度器 */
void vTaskSuspendAll( void );
 
/* 恢復調度器
 * 返回值: pdTRUE表示在暫定期間有更高優(yōu)先級的任務就緒了
 *        可以不理會這個返回值
 */
BaseType_t xTaskResumeAll( void );
 
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容