1)實驗平臺:alientek NANO STM32F411 V1開發板
2)摘自《正點原子STM32F4 開發指南(HAL 庫版》關注官方微信號公眾號,獲取更多資料:正點原子
第三十八章 UCOSII 實驗 3-消息隊列、信號量集和軟件定時器
上一章,我們學習了 UCOSII 的信號量和郵箱的使用,本章,我們將學習消息隊列、信號量集和軟件定時器的使用。本章分為如下幾個部分:
38.1 UCOSII 消息隊列、信號量集和軟件定時器簡介
38.2 硬件設計
38.3 軟件設計
38.4 下載驗證
38.1 UCOSII 消息隊列、信號量集和軟件定時器簡介
上一章,我們介紹了信號量和郵箱的使用,本章我們介紹比較複雜消息隊列、信號量集以及軟件定時器的使用。
消息隊列
使用消息隊列可以在任務之間傳遞多條消息。消息隊列由三個部分組成:事件控制塊、消息隊列和消息。當把事件控制塊成員 OSEventType 的值置為 OS_EVENT_TYPE_Q 時,該事件控制塊描述的就是一個消息隊列。消息隊列的數據結構如圖 38.1.1 所示。從圖中可以看到,消息隊列相當於一個共用一個任務等待列表的消息郵箱數組,事件控制塊成員 OSEventPtr 指向了一個叫做隊列控制塊(OS_Q)的結構,該結構管理了一個數組 MsgTbl[],該數組中的元素都是一些指向消息的指針。
隊列控制塊(OS_Q)的結構定義如下:
typedef struct os_q
{
struct os_q *OSQPtr;
void **OSQStart;
void **OSQEnd;
void **OSQIn;
void **OSQOut;
INT16U OSQSize;
INT16U OSQEntries;
} OS_Q;
該結構體中各參數的含義如表 38.1.1 所示:
其中,可以移動的指針為 OSQIn 和 OSQOut,而指針 OSQStart 和 OSQEnd 只是一個標誌(常指針)。當可移動的指針 OSQIn 或 OSQOut 移動到數組末尾,也就是與 OSQEnd 相等時,可移動的指針將會被調整到數組的起始位置 OSQStart。也就是說,從效果上來看,指針 OSQEnd與 OSQStart 等值。於是,這個由消息指針構成的數組就頭尾銜接起來形成了一個如圖 38.1.2所示的循環的隊列。
在UCOSII初始化時,系統將按文件os_cfg.h中的配置常數OS_MAX_QS定義OS_MAX_QS個隊列控制塊,並用隊列控制塊中的指針 OSQPtr 將所有隊列控制塊鏈接為鏈表。由於這時還沒有使用它們,故這個鏈表叫做空隊列控制塊鏈表。接下來我們看看在 UCOSII 中,與消息隊列相關的幾個函數(未全部列出,下同)。
1) 創建消息隊列函數
創建一個消息隊列首先需要定義一指針數組,然後把各個消息數據緩衝區的首地址存入這個數組中,然後再調用函數 OSQCreate 來創建消息隊列。創建消息隊列函數 OSQCreate的原型為:OS_EVENT *OSQCreate(void**start,INT16U size);其中,start 為存放消息緩衝區指針數組的地址,size 為該數組大小。該函數的返回值為消息隊列指針。
2) 請求消息隊列函數
請求消息隊列的目的是為了從消息隊列中獲取消息。任務請求消息隊列需要調用函數
OSQPend,該函數原型為:
void*OSQPend(OS_EVENT*pevent,INT16U timeout,INT8U *err) ;
其中,pevent 為所請求的消息隊列的指針,timeout 為任務等待時限,err 為錯誤信息。
3) 向消息隊列發送消息函數
任務可以通過調用函數 OSQPost 或 OSQPostFront 兩個函數來向消息隊列發送消息。
函數 OSQPost 以 FIFO(先進先出)的方式組織消息隊列,函數 OSQPostFront 以 LIFO(後
進先出)的方式組織消息隊列。這兩個函數的原型分別為:
INT8U OSQPost(OS_EVENT
*pevent,void *msg)和 INT8U OSQPost(OS_EVENT*pevent,void*msg);
其中,pevent 為消息隊列的指針,msg 為待發消息的指針。
消息隊列還有其他一些函數,這裡我們就不介紹了,感興趣的朋友可以參考《嵌入式實時
操作系統 UCOSII 原理及應用》第五章,關於隊列更詳細的介紹,也請參考該書。
信號量集
在實際應用中,任務常常需要與多個事件同步,即要根據多個信號量組合作用的結果來決
定任務的運行方式。UCOSII 為了實現多個信號量組合的功能定義了一種特殊的數據結構——
信號量集。
信號量集所能管理的信號量都是一些二值信號,所有信號量集實質上是一種可以對多個輸
入的邏輯信號進行基本邏輯運算的組合邏輯,其示意圖如圖 38.1.3 所示
不同於信號量、消息郵箱、消息隊列等事件,UCOSII 不使用事件控制塊來描述信號量集,
而使用了一個叫做標誌組的結構 OS_FLAG_GRP 來描述。OS_FLAG_GRP 結構如下:
typedef struct
{
INT8U
OSFlagType;
//識別是否為信號量集的標誌
void *OSFlagWaitList;
//指向等待任務鏈表的指針
OS_FLAGS OSFlagFlags; //所有信號列表
}OS_FLAG_GRP;
成員 OSFlagWaitList 是一個指針,當一個信號量集被創建後,這個指針指向了這個信號量
集的等待任務鏈表。
與其他前面介紹過的事件不同,信號量集用一個雙向鏈表來組織等待任務,每一個等待任
務都是該鏈表中的一個節點(Node)。標誌組 OS_FLAG_GRP 的成員 OSFlagWaitList 就指向了
信號量集的這個等待任務鏈表。等待任務鏈表節點 OS_FLAG_NODE 的結構如下:
typedef struct
{
void
*OSFlagNodeNext;
//指向下一個節點的指針
void
*OSFlagNodePrev;
//指向前一個節點的指針
void *OSFlagNodeTCB;
//指向對應任務控制塊的指針
void *OSFlagNodeFlagGrp;
//反向指向信號量集的指針
OS_FLAGS OSFlagNodeFlags; //信號過濾器
INT8U OSFlagNodeWaitType; //定義邏輯運算關係的數據
} OS_FLAG_NODE;
其中 OSFlagNodeWaitType 是定義邏輯運算關係的一個常數(根據需要設置),其可選值
和對應的邏輯關係如表 38.1.2 所示:
OSFlagFlags、OSFlagNodeFlags、OSFlagNodeWaitType 三者的關係如圖 38.1.4 所示:
圖中為了方便說明,我們將 OSFlagFlags 定義為 8 位,但是 UCOSII 支持 8 位/16 位/32 位
定義,這個通過修改 OS_FLAGS 的類型來確定(UCOSII 默認設置 OS_FLAGS 為 16 位)。
上圖清楚的表達了信號量集各成員的關係:OSFlagFlags 為信號量表,通過發送信號量集的
任務設置;OSFlagNodeFlags 為信號濾波器,由請求信號量集的任務設置,用於選擇性的挑選
OSFlagFlags 中的部分(或全部)位作為有效信號;OSFlagNodeWaitType 定義有效信號的邏輯
運算關係,也是由請求信號量集的任務設置,用於選擇有效信號的組合方式(0/1? 與/或?)。
舉個簡單的例子,假設請求信號量集的任務設置 OSFlagNodeFlags 的值為 0X0F,設置
OSFlagNodeWaitType 的值為 WAIT_SET_ANY,那麼只要 OSFlagFlags 的低四位的任何一位為
1,請求信號量集的任務將得到有效的請求,從而執行相關操作,如果低四位都為 0,那麼請求
信號量集的任務將得到無效的請求。
接下來我們看看在 UCOSII 中,與信號量集相關的幾個函數。
1) 創建信號量集函數
任務可以通過調用函數 OSFlagCreate 來創建一個信號量集。函數 OSFlagCreate 的原
型為:
OS_FLAG_GRP *OSFlagCreate (OS_FLAGS flags,INT8U *err
) ;
其中,flags 為信號量的初始值(即 OSFlagFlags 的值),err 為錯誤信息,返回值為該
信號量集的標誌組的指針,應用程序根據這個指針對信號量集進行相應的操作。
2) 請求信號量集函數
任務可以通過調用函數 OSFlagPend 請求一個信號量集,函數 OSFlagPend 的原型為:
OS_FLAGS OSFlagPend(OS_FLAG_GRP*pgrp, OS_FLAGS flags, INT8U wait_type,
INT16U timeout, INT8U *err);
其中,pgrp 為所請求的信號量集指針,flags 為濾波器(即 OSFlagNodeFlags 的值),
wait_type 為邏輯運算類型(即 OSFlagNodeWaitType 的值),timeout 為等待時限,err 為錯
誤信息。
3) 向信號量集發送信號函數
任務可以通過調用函數 OSFlagPost 向信號量集發信號,函數 OSFlagPost 的原型為:
OS_FLAGS OSFlagPost (OS_FLAG_GRP *pgrp, OS_FLAGS flags, INT8U opt, INT8U *err);
其中,pgrp 為所請求的信號量集指針,flags 為選擇所要發送的信號,opt 為信號有效
選項,err 為錯誤信息。
所謂任務向信號量集發信號,就是對信號量集標誌組中的信號進行置“1”(置位)或
置“0”(復位)的操作。至於對信號量集中的哪些信號進行操作,用函數中的參數 flags
來指定;對指定的信號是置“1”還是置“0”,用函數中的參數 opt 來指定(opt =
OS_FLAG_SET 為置“1”操作;opt = OS_FLAG_CLR 為置“0”操作)。
信號量集就介紹到這,更詳細的介紹,請參考《嵌入式實時操作系統 UCOSII 原理及應用》
第六章。
軟件定時器
UCOSII 從 V2.83 版本以後,加入了軟件定時器,這使得 UCOSII 的功能更加完善,在其上
的應用程序開發與移植也更加方便。在實時操作系統中一個好的軟件定時器實現要求有較高的
精度、較小的處理器開銷,且佔用較少的存儲器資源。
通過前面的學習,我們知道 UCOSII 通過 OSTimTick 函數對時鐘節拍進行加 1 操作,同時
遍歷任務控制塊,以判斷任務延時是否到時。軟件定時器同樣由 OSTimTick 提供時鐘,但是軟
件定時器的時鐘還受 OS_TMR_CFG_TICKS_PER_SEC 設置的控制,也就是在 UCOSII 的時鐘
節拍上面再做了一次“分頻”,軟件定時器的最快時鐘節拍就等於 UCOSII 的系統時鐘節拍。
這也決定了軟件定時器的精度。
軟件定時器定義了一個單獨的計數器 OSTmrTime,用於軟件定時器的計時,UCOSII 並不
在 OSTimTick 中進行軟件定時器的到時判斷與處理,而是創建了一個高於應用程序中所有其他
任務優先級的定時器管理任務 OSTmr_Task,在這個任務中進行定時器的到時判斷和處理。時
鍾節拍函數通過信號量給這個高優先級任務發信號。這種方法縮短了中斷服務程序的執行時間,
但也使得定時器到時處理函數的響應受到中斷退出時恢復現場和任務切換的影響。軟件定時器
功能實現代碼存放在 tmr.c 文件中,移植時需只需在 os_cfg.h 文件中使能定時器和設定定時
器的相關參數。
UCOSII 中軟件定時器的實現方法是,將定時器按定時時間分組,使得每次時鐘節拍到來
時只對部分定時器進行比較操作,縮短了每次處理的時間。但這就需要動態地維護一個定時器
組。定時器組的維護只是在每次定時器到時時才發生,而且定時器從組中移除和再插入操作不
需要排序。這是一種比較高效的算法,減少了維護所需的操作時間。
UCOSII 軟件定時器實現了 3 類鏈表的維護:
OS_EXT OS_TMR OSTmrTbl[OS_TMR_CFG_MAX]; //定時器控制塊數組
OS_EXT OS_TMR *OSTmrFreeList;
//空閒定時器控制塊鏈表指針
OS_EXT OS_TMR_WHEEL OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE];//定時器輪
其中 OS_TMR 為定時器控制塊,定時器控制塊是軟件定時器管理的基本單元,包含軟件定
時器的名稱、定時時間、在鏈表中的位置、使用狀態、使用方式,以及到時回調函數及其參數
等基本信息。
OSTmrTbl[OS_TMR_CFG_MAX];:以數組的形式靜態分配定時器控制塊所需的 RAM 空
間,並存儲所有已建立的定時器控制塊,OS_TMR_CFG_MAX 為最大軟件定時器的個數。
OSTmrFreeLiSt:為空閒定時器控制塊鏈表頭指針。空閒態的定時器控制塊(OS_TMR)中,
OSTmrnext 和 OSTmrPrev 兩個指針分別指向空閒控制塊的前一個和後一個,組織了空閒控制塊
雙向鏈表。建立定時器時,從這個鏈表中搜索空閒定時器控制塊。
OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE]:該數組的每個元素都是已開啟定時器的
一個分組,元素中記錄了指向該分組中第一個定時器控制塊的指針,以及定時器控制塊的個數。
運行態的定時器控制塊(OS_TMR)中,OSTmrnext 和 OSTmrPrev 兩個指針同樣也組織了所在分
組中定時器控制塊的雙向鏈表。軟件定時器管理所需的數據結構示意圖如圖 38.1.5 所示:
OS_TMR_CFG_WHEEL_SIZE 定義了 OSTmrWheelTbl 的大小,同時這個值也是定時器分
組的依據。按照定時器到時值與 OS_TMR_CFG_WHEEL_SIZE 相除的餘數進行分組:不同餘數
的定時器放在不同分組中;相同餘數的定時器處在同一組中,由雙向鏈表連接。這樣,餘數值
為 0~OS_TMR_CFG_WHEEL_SIZE-1 的不同定時器控制塊,正好分別對應了數組元素
OSTmr-WheelTbl[0]~OSTmrWheelTbl[OS_TMR_CFGWHEEL_SIZE-1]的不同分組。每次時鐘節
拍到來時,時鐘數 OSTmrTime 值加 1,然後也進行求餘操作,只有餘數相同的那組定時器才有
可能到時,所以只對該組定時器進行判斷。這種方法比循環判斷所有定時器更高效。隨著時鐘
數的累加,處理的分組也由 0~OS_TMR_CFG_WHE EL_SIZE-1 循環。這裡,我們推薦
OS_TMR_CFG_WHEEL_SIZE 的取值為 2 的 N 次方,以便採用移位操作計算餘數,縮短處理時
間。
信號量喚醒定時器管理任務,計算出當前所要處理的分組後,程序遍歷該分組中的所有控
制塊,將當前 OSTmrTime 值與定時器控制塊中的到時值(OSTmrMatch)相比較。若相等(即到
時),則調用該定時器到時回調函數;若不相等,則判斷該組中下一個定時器控制塊。如此操作,
直到該分組鏈表的結尾。軟件定時器管理任務的流程如圖 38.1.6 所示。
當運行完軟件定時器的到時處理函數之後,需要進行該定時器控制塊在鏈表中的移除和再
插入操作。插入前需要重新計算定時器下次到時時所處的分組。計算公式如下:
定時器下次到時的 OSTmrTime 值(OSTmrMatch)=定時器定時值+當前 OSTmrTime 值
新分組=定時器下次到時的 OSTmrTime 值(OSTmrMatch)%OS_TMR_CFG_WHEEL_SIZE
接下來我們看看在 UCOSII 中,與軟件定時器相關的幾個函數。
1) 創建軟件定時器函數
創建軟件定時器通過函數 OSTmrCreate 實現,該函數原型為:
OS_TMR *OSTmrCreate (INT32U dly, INT32U period, INT8U opt,
OS_TMR_CALLBACK callback,void *callback_arg, INT8U *pname, INT8U *perr);
dly,用於初始化定時時間,對單次定時(ONE-SHOT 模式)的軟件定時器來說,這
就是該定時器的定時時間,而對於週期定時(PERIODIC 模式)的軟件定時器來說,這是
該定時器第一次定時的時間,從第二次開始定時時間變為 period。
period,在週期定時(PERIODIC 模式),該值為軟件定時器的週期溢出時間。
opt,用於設置軟件定時器工作模式。可以設置的值為:OS_TMR_OPT_ONE_SHOT
或 OS_TMR_OPT_PERIODIC,如果設置為前者,說明是一個單次定時器;設置為後者則
表示是週期定時器。
callback,為軟件定時器的回調函數,當軟件定時器的定時時間到達時,會調用該函數。
callback_arg,回調函數的參數。
pname,為軟件定時器的名字。
perr,為錯誤信息。
軟件定時器的回調函數有固定的格式,我們必須按照這個格式編寫,軟件定時器的回
調函數格式為:
void (*OS_TMR_CALLBACK)(void *ptmr, void *parg);
其中,函數名我們可以自己隨意設置,而 ptmr 這個參數,軟件定時器用來傳遞當前定
時器的控制塊指針,所以我們一般設置其類型為 OS_TMR*類型,第二個參數(parg)為回
調函數的參數,這個就可以根據自己需要設置了,你也可以不用,但是必須有這個參數。
2) 開啟軟件定時器函數
任務可以通過調用函數 OSTmrStart 開啟某個軟件定時器,該函數的原型為:
BOOLEAN OSTmrStart (OS_TMR *ptmr, INT8U *perr);
其中 ptmr 為要開啟的軟件定時器指針,perr 為錯誤信息。
3) 停止軟件定時器函數
任務可以通過調用函數 OSTmrStop 停止某個軟件定時器,該函數的原型為:
BOOLEAN OSTmrStop (OS_TMR *ptmr,INT8U opt,void *callback_arg,INT8U *perr);
其中 ptmr 為要停止的軟件定時器指針。
opt 為停止選項,可以設置的值及其對應的意義為:
OS_TMR_OPT_NONE,直接停止,不做任何其他處理
OS_TMR_OPT_CALLBACK,停止,用初始化的參數執行一次回調函數
OS_TMR_OPT_CALLBACK_ARG,停止,用新的參數執行一次回調函數
callback_arg,新的回調函數參數。
perr,錯誤信息。
軟件定時器我們就介紹到這。
38.2 硬件設計
本節實驗功能簡介:本章我們在 UCOSII 裡面創建 7 個任務:開始任務、LED 任務、數碼
管顯示任務、隊列消息顯示任務、信號量集任務、按鍵掃描任務和主任務,開始任務用於創建
郵箱、消息隊列、信號量集以及其他任務,之後掛起;數碼管顯示任務用於更新數碼管的顯示;
隊列消息顯示任務請求消息隊列,在得到消息後顯示收到的消息數據;信號量集任務用於測試
信號量集,採用 OS_FLAG_WAIT_SET_ANY 的方法,KEY0、KEY1、KEY2 按鍵按下,該任
務都會控制蜂鳴器發出“滴”的一聲;按鍵掃描任務用於按鍵掃描,優先級最高,將得到的鍵
值通過消息郵箱發送出去;主任務創建 3 個軟件定時器(定時器 1,500ms 溢出一次,顯示
CPU 和內存使用率;定時器 2,1000ms 溢出一次,數碼管顯示更新;定時 3,,100ms 溢出一
次,用於自動發送消息到消息隊列),並通過查詢消息郵箱獲得鍵值,根據鍵值執行 DS1 控制、
控制軟件定時器 3 的開關、和軟件定時器 2 的開關控制等。
通過按下 KEY0,可以控制軟件定時器 3 的開關,從而控制消息隊列的發送,可以在串口
打印上看到 Q 和 MEM 的值慢慢變大(說明隊列消息在增多,佔用內存也隨著消息增多而增大),
在 QUEUE MSG 區,開始顯示隊列消息,再按一次 KEY0 停止 tmr3,此時可以看到 Q 和 MEM
逐漸減小。當 Q 值變為 0 的時候,QUEUE MSG 也停止顯示(隊列為空)。通過按 KEY1 按
鍵,可以控制軟件定時器 2 的開關,停止數碼管變換顯示。通過按 KEY2 按鍵,可以控制 DS1 的
亮滅。
所要用到的硬件資源如下:
1) 指示燈 DS0 、DS1
2) 3 個機械按鍵(KEY0/KEY1/KEY2)
3) 蜂鳴器
4) 數碼管
5) 串口
這些,我們在前面的學習中都已經介紹過了。
38.3 軟件設計
本章,我們在第九章實驗 (實驗 5)的基礎上修改,首先,是 UCOSII 代碼的添加,具體
方法同第 34 章一模一樣,本章就不再詳細介紹了。由於我們創建了 7 個任務,加上統計任務、
空閒任務和軟件定時器任務,總共 10 個任務,如果你還想添加其他任務,請把 OS_MAX_TASKS
的值適當改大。
另外,我們還需要在 os_cfg.h 裡面修改軟件定時器管理部分的宏定義,修改如下:
#define OS_TMR_EN
1u
//使能軟件定時器功能
#define OS_TMR_CFG_MAX
16u
//最大軟件定時器個數
#define OS_TMR_CFG_NAME_EN
1u
//使能軟件定時器命名
#define OS_TMR_CFG_WHEEL_SIZE
8u
//軟件定時器輪大小
#define OS_TMR_CFG_TICKS_PER_SEC
100u
//軟件定時器的時鐘節拍(10ms)
#define OS_TASK_TMR_PRIO
0u
//軟件定時器的優先級,設置為最高
這樣我們就使能 UCOSII 的軟件定時器功能了,並且設置最大軟件定時器個數為 16,定時
器輪大小為 8,軟件定時器時鐘節拍為 10ms(即定時器的最少溢出時間為 10ms)。
最後,我們只需要修改 main.c 函數了,打開 main.c,輸入如下代碼:
////////////////////////UCOSII 任務設置///////////////////////////////////
//START 任務
//設置任務優先級
#define START_TASK_PRIO
10 //開始任務的優先級設置為最低
//設置任務堆棧大小
#define START_STK_SIZE 64
//任務堆棧
OS_STK START_TASK_STK[START_STK_SIZE];
//任務函數
void start_task(void *pdata);
//LED 任務
//設置任務優先級
#define LED_TASK_PRIO 7
//設置任務堆棧大小
#define LED_STK_SIZE 64
//任務堆棧
OS_STK LED_TASK_STK[LED_STK_SIZE];
//任務函數
void led_task(void *pdata);
//數碼管任務
//設置任務優先級
#define SMG_TASK_PRIO 6
//設置任務堆棧大小
#define SMG_STK_SIZE 64
//任務堆棧
OS_STK SMG_TASK_STK[SMG_STK_SIZE];
//任務函數
void smg_task(void *pdata);
//隊列消息顯示任務
//設置任務優先級
#define QMSGSHOW_TASK_PRIO 5
//設置任務堆棧大小
#define QMSGSHOW_STK_SIZE 64
//任務堆棧
OS_STK QMSGSHOW_TASK_STK[QMSGSHOW_STK_SIZE];
//任務函數
void qmsgshow_task(void *pdata);
//主任務
//設置任務優先級
#define MAIN_TASK_PRIO 4
//設置任務堆棧大小
#define MAIN_STK_SIZE 128
//任務堆棧
OS_STK MAIN_TASK_STK[MAIN_STK_SIZE];
//任務函數
void main_task(void *pdata);
//信號量集任務
//設置任務優先級
#define FLAGS_TASK_PRIO 3
//設置任務堆棧大小
#define FLAGS_STK_SIZE 64
//任務堆棧
OS_STK FLAGS_TASK_STK[FLAGS_STK_SIZE];
//任務函數
void flags_task(void *pdata);
//按鍵掃描任務
//設置任務優先級
#define KEY_TASK_PRIO 2
//設置任務堆棧大小
#define KEY_STK_SIZE 64
//任務堆棧
OS_STK KEY_TASK_STK[KEY_STK_SIZE];
//任務函數
void key_task(void *pdata);
//////////////////////////////////////////////////////////////////////////////
OS_EVENT * msg_key;
//按鍵郵箱事件塊
OS_EVENT * q_msg;
//消息隊列
OS_TMR
* tmr1;
//軟件定時器 1
OS_TMR
* tmr2;
//軟件定時器 2
OS_TMR
* tmr3;
//軟件定時器 3
OS_FLAG_GRP * flags_key;//按鍵信號量集
void * MsgGrp[256];
//消息隊列存儲地址,最大支持 256 個消息
//共陰數字數組
//0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F, .,全滅
u8 smg_num[]={0xfc,0x60,0xda,0xf2,0x66,0xb6,0xbe,0xe0,0xfe,
0xf6,0xee,0x3e,0x9c,0x7a,0x9e,0x8e,0x01,0x00};
u8 smg_duan=0;//數碼管段選
//軟件定時器 1 的回調函數
//每 500ms 執行一次,用於顯示 CPU 使用率和內存使用率
void tmr1_callback(OS_TMR *ptmr,void *p_arg)
{
static u16 cpuusage=0;
static u8 tcnt=0;
if(tcnt==5)
{
printf("CPU:%d%%\r\n",cpuusage/5); //顯示 CPU 使用率
cpuusage=0;
tcnt=0;
}
cpuusage+=OSCPUUsage;
tcnt++;
printf("MEM:%d%%\r\n",mem_perused());//顯示內存使用率
printf("Q:%d\r\n\r\n",((OS_Q*)(q_msg->OSEventPtr))->OSQEntries); //顯示隊列當前的
大小
}
//軟件定時器 2 的回調函數
void tmr2_callback(OS_TMR *ptmr,void *p_arg)
{
smg_duan++;
if(smg_duan>16) smg_duan=0;
}
//軟件定時器 3 的回調函數
void tmr3_callback(OS_TMR *ptmr,void *p_arg)
{
u8* p;
u8 err;
static u8 msg_cnt=0;
//msg 編號
p=mymalloc(13); //申請 13 個字節的內存
if(p)
{
sprintf((char*)p,"ALIENTEK %03d",msg_cnt);
msg_cnt++;
err=OSQPost(q_msg,p); //發送隊列
if(err!=OS_ERR_NONE)
//發送失敗
{
myfree(p);
//釋放內存
OSTmrStop(tmr3,OS_TMR_OPT_NONE,0,&err);
//關閉軟件定時器 3
}
}
}
int main(void)
{
HAL_Init();
//初始化 HAL 庫
Stm32_Clock_Init(96,4,2,4);
//設置時鐘,96Mhz
delay_init(96);
//初始化延時函數
uart_init(115200);
//串口初始化 115200
BEEP_Init();
//蜂鳴器初始化
LED_Init();
//初始化與 LED 連接的硬件接口
KEY_Init();
//按鍵初始化
mem_init();
//初始化內存池
LED_SMG_Init();
//數碼管初始化
printf("NANO STM32\r\n");
printf("UCOSII TEST3\r\n");
printf("KEY0:Q SW KEY1:TMR2 SW KEY2:DS1\r\n\r\n");
OSInit();
OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK
[START_STK_SIZE-1],START_TASK_PRIO );//創建起始任務
OSStart();
}
//開始任務
void start_task(void *pdata)
{
OS_CPU_SR cpu_sr=0;
u8 err;
pdata = pdata;
msg_key=OSMboxCreate((void*)0);
//創建消息郵箱
q_msg=OSQCreate(&MsgGrp[0],256); //創建消息隊列
flags_key=OSFlagCreate(0,&err);
//創建信號量集
OSStatInit();
//初始化統計任務.這裡會延時 1 秒鐘左右
OS_ENTER_CRITICAL();
//進入臨界區(無法被中斷打斷)
OSTaskCreate(led_task,(void *)0,(OS_STK*)&LED_TASK_STK
[LED_STK_SIZE-1],LED_TASK_PRIO);
OSTaskCreate(smg_task,(void *)0,(OS_STK*)&SMG_TASK_STK
[SMG_STK_SIZE-1],SMG_TASK_PRIO);
OSTaskCreate(qmsgshow_task,(void *)0,(OS_STK*)&QMSGSHOW_TASK_STK
[QMSGSHOW_STK_SIZE-1],QMSGSHOW_TASK_PRIO);
OSTaskCreate(main_task,(void *)0,(OS_STK*)&MAIN_TASK_STK
[MAIN_STK_SIZE-1],MAIN_TASK_PRIO);
OSTaskCreate(flags_task,(void *)0,(OS_STK*)&FLAGS_TASK_STK
[FLAGS_STK_SIZE-1],FLAGS_TASK_PRIO);
OSTaskCreate(key_task,(void *)0,(OS_STK*)&KEY_TASK_STK
[KEY_STK_SIZE-1],KEY_TASK_PRIO);
OSTaskSuspend(START_TASK_PRIO);
//掛起起始任務.
OS_EXIT_CRITICAL();
//退出臨界區(可以被中斷打斷)
}
//LED 任務
void led_task(void *pdata)
{
u8 t;
while(1)
{
t++;
delay_ms(10);
if(t==8)LED0=1; //LED0 滅
if(t==100)
//LED0 亮
{
t=0;
LED0=0;
}
}
}
//數碼管顯示任務
void smg_task(void *pdata)
{
while(1)
{
LED_Write_Data(smg_num[smg_duan],7);//數碼管顯示
LED_Refresh();//刷新顯示
delay_ms(10);
}
}
//隊列消息顯示任務
void qmsgshow_task(void *pdata)
{
u8 *p;
u8 err;
while(1)
{
p=OSQPend(q_msg,0,&err);//請求消息隊列
printf("%s\r\n",p);//串口打印消息
myfree(p);
delay_ms(500);
}
}
//主任務
void main_task(void *pdata)
{
u32 key=0;
u8 err;
u8 tmr2sta=1; //軟件定時器 2 開關狀態
u8 tmr3sta=0; //軟件定時器 3 開關狀態
tmr1=OSTmrCreate(10,50,OS_TMR_OPT_PERIODIC,
(OS_TMR_CALLBACK)tmr1_callback,0,"tmr1",&err); //500ms 執行一次
tmr2=OSTmrCreate(10,100,OS_TMR_OPT_PERIODIC,
(OS_TMR_CALLBACK)tmr2_callback,0,"tmr2",&err); //1000ms 執行一次
tmr3=OSTmrCreate(10,10,OS_TMR_OPT_PERIODIC,
(OS_TMR_CALLBACK)tmr3_callback,0,"tmr3",&err); //100ms 執行一次
OSTmrStart(tmr1,&err);//啟動軟件定時器 1
OSTmrStart(tmr2,&err);//啟動軟件定時器 2
while(1)
{
key=(u32)OSMboxPend(msg_key,10,&err);
if((key==KEY0_PRES)||(key==KEY1_PRES)||(key==KEY2_PRES))
OSFlagPost(flags_key,1<
//設置對應的信號量為 1
switch(key)
{
case KEY0_PRES://軟件定時器 3 開關
tmr3sta=!tmr3sta;
if(tmr3sta)
{
printf("TMR3 START\r\n");//提示定時器 3 打開了
OSTmrStart(tmr3,&err);
}
else
{
OSTmrStop(tmr3,OS_TMR_OPT_NONE,0,&err);//關閉軟件定時器 3
printf("TMR3 STOP\r\n");//提示定時器 3 關閉了
}
break;
case KEY1_PRES://軟件定時器 2 開關
tmr2sta=!tmr2sta;
if(tmr2sta)
{
printf("TMR2 START\r\n");//提示定時器 2 打開了
OSTmrStart(tmr2,&err); //開啟軟件定時器 2
}
else
{
OSTmrStop(tmr2,OS_TMR_OPT_NONE,0,&err);//關閉軟件定時器 2
printf("TMR2 STOP\r\n");//提示定時器 2 關閉了
}
break;
case KEY2_PRES://控制 DS1
LED1=!LED1;
break;
}
delay_ms(10);
}
}
//信號量集處理任務
void flags_task(void *pdata)
{
u16 flags;
u8 err;
while(1)
{
flags=OSFlagPend(flags_key,0X001F,OS_FLAG_WAIT_SET_ANY,0,&err);
//等待信號量
if(flags&0X0001) printf("KEY0 DOWN\r\n");
else if(flags&0X0002) printf("KEY1 DOWN\r\n");
else if(flags&0X0004) printf("KEY2 DOWN\r\n");
BEEP=0;
delay_ms(50);
BEEP=1;
OSFlagPost(flags_key,0X0007,OS_FLAG_CLR,&err);//全部信號量清零
}
}
//按鍵掃描任務
void key_task(void *pdata)
{
u8 key;
while(1)
{
key=KEY_Scan(0);
if(key)OSMboxPost(msg_key,(void*)key);//發送消息
delay_ms(10);
}
}
本章 main.c 的代碼有點多,因為我們創建了 7 個任務,3 個軟件定時器及其回調函數,所
以,整個代碼有點多,我們創建的 7 個任務為:start_task、led_task、smg_task、qmsgshow_task 、
main_task、flags_task 和 key_task,優先級分別是 10 和 7~2,堆棧大小除了 main 是 128,其他
都是 64。
我們還創建了 3 個軟件定時器 tmr1、tmr2 和 tmr3,tmr1 用於打印 CPU 使用率和內存使用
率,每 500ms 執行一次;tmr2 用於更新數碼管的顯示,每 1000ms 執行一次;tmr3 用於定時向
隊列發送消息,每 100ms 發送一次。
本章,我們依舊使用消息郵箱 msg_key 在按鍵任務和主任務之間傳遞鍵值數據,我們創建
信號量集 flags_key,在主任務裡面將按鍵鍵值通過信號量集傳遞給信號量集處理任務
flags_task,實現按鍵信息的打印以及發出按鍵提示音。
本章,我們還創建了一個大小為 256 的消息隊列 q_msg,通過軟件定時器 tmr3 的回調函數
向消息隊列發送消息,然後在消息隊列顯示任務 qmsgshow_task 裡面請求消息隊列,並在串口
調試助手打印得到的消息。消息隊列還用到了動態內存管理。
在主任務 main_task 裡面,我們實現了 35.2 節介紹的功能:KEY0 控制軟件定時器 tmr3 的
開關,間接控制隊列信息的發送;KEY1 控制軟件定時器 tmr2 開關,控制數碼管的更新顯示;
KEY2 控制 LED1 亮滅;軟件設計部分就為大家介紹到這裡。
38.4 下載驗證
在代碼編譯成功之後,我們通過下載代碼到 NANO STM32F4 V1 上,打開串口調試助手,
顯示如圖 38.4.1 所示:
從圖中可以看出,默認狀態下,CPU 使用率為 2%左右。這主要是 key_task 裡面增加不停
的更新數碼管顯示(tmr2)操作導致的。
通過按 KEY0 則可以啟動 tmr3 控制消息隊列發送,可以在串口助手上面看到 Q 和 MEM 的
值慢慢變大(說明隊列消息在增多,佔用內存也隨著消息增多而增大),在 QUEUE MSG 區,
開始顯示隊列消息,再按一次 KEY1 停止 tmr3,此時可以看到 Q 和 MEM 逐漸減小。當 Q 值
變為 0 的時候,QUEUE MSG 也停止顯示(隊列為空)。
通過按 KEY1,可以控制數碼管的更新顯示。
通過按 KEY2,可以控制 DS1 的亮滅;
KEY0、KEY1、KEY2 按鍵按下,蜂鳴器都會發出“滴”的一聲,提示按鍵被按下,同時
串口會打印顯示按鍵信息。