郭 宇,陳年生,方曉平
(湖北師范學院 計算機科學與技術學院, 湖北 黃石 435002)
自動化技術的進步使得實時控制系統得到越來越廣泛的應用,實時采集數據是系統中重要的部分,這個過程直接影響控制質量和生產效率,因此對定時器精度和實時性要求相當高,工業控制軟件定時精度需達到毫秒級。
在以往DOS操作系統中,用戶可以容易且精確地控制采樣精度,這得益于對硬件的直接操作。現在主流的Windows操作系統,提供了豐富的圖形界面,人機交互友好,使用方便,用戶卻難以對底層硬件進行直接操作。另外,Windows是一個基于消息機制的操作系統,各個應用程序都有自己的消息隊列[1],這樣可能會帶來諸如低優先級消息不能得到及時響應等問題。PLC廣泛應用于工業自動化控制,現擬對某PLC中D寄存器數據進行實時采樣。在Windows環境使用Visual Studio開發工具編寫PLC數據采集的上位機軟件,通過視窗和操作系統來完成定時任務,這樣對實時性就提出了挑戰,以下將著重介紹軟件中使用的兩種定時器。
WM_TIMER定時器是Windows操作系統自帶的一種定時器,使用起來方便簡單,因此也經常被使用。
其實現原理是先由SetTimer()函數在內存中創建一個定時器,并在參數中設置好定時間隔,每當達到設定時間,系統就會發出一個WM_TIMER消息,并由相應的響應函數(回調函數)去做處理。定時器使用完后由KillTimer()函數釋放。
相關函數如下:
1)SetTimer() 創建定時器;
2)OnTimer() 消息響應函數,由用戶編寫處理事件的代碼;
3)KillTimer() 中止定時器,釋放定時器資源。
對SetTimer()函數作如下說明:
UINT_PTR SetTimer(
UINT_PTR nIDEvent.
//nIDEvent是定時器編號ID,倘若有多個定時器,可通過ID識別
UINT nElapse,
//用戶設定的時間間隔,以ms為單位
TIMERPROC lpTimerFunc
//回調函數
);
其中最后一個參數lpTimerFunc是處理定時器消息的回調函數,在這個函數里面由程序設計人員添加處理事件的代碼。這個參數也可以設為NULL,此時將使用系統默認的響應函數OnTimer()。
在Visual Studio 2010環境中,使用類向導生成OnTimer()函數:在類視圖中右擊需要定時器的類,選擇“類向導”,在彈出來的對話框中的“消息”選項卡下選擇“WM_TIMER”,點擊“添加處理程序”,這樣類中就會自動生成OnTimer()函數。程序人員在該函數中添加實現功能的代碼,每隔設定的時間都會執行一次該函數,本例就是在OnTimer()函數中添加采集PLC內D寄存器數據的代碼,其流程如圖1所示。

圖1 WM_TIMER定時器采集數據的程序框圖
VS2010作為開發工具,在對話框類中實現定時器功能:
1)在啟動按鈕中創建定時器
void CDDataDlg::OnBnClickedStart()
{
// TODO: 在此添加控件通知處理程序代碼
...
SetTimer(1,200,NULL);
}
2)數據采集,使用類向導自動生成
void CDDataDlg::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息處理程序代碼/或調用默認值
...
實現數據采集的代碼
...
}
3)在結束按鈕中關閉定時器,釋放資源
void CDDataDlg::OnBnClickedEnd()
{
// TODO: 在此添加控件通知處理程序代碼
KillTimer(1);
}
微軟公司在其多媒體Windows中提供了精確定時器的底層API(應用程序編程接口)支持,相對于WM_TIMER定時器,多媒體定時器的設計要稍顯復雜,但是精度更高。
使用多媒體定時器需要包含頭文件Mmsystem.h,其接口函數位于mmsystem.dll的動態鏈接庫中,多媒體計時器的設計包括以下函數:
1)timeGetDevCaps() 獲取定時器所支持的最大和最小精度,不同的PC機由于硬件配置的不同獲取的精度范圍可能有所差異;
2)timeBeginPeriod() 設置定時器的定時精度,其參數可由timeGetDevCaps()函數獲取;
3)timeEndPeriod() 清除定時器精度,釋放定時器資源;
4)timeKillEvent() 刪除定時器事件。
5)MMRESULT timeSetEvent(
UINT uDelay,
//以毫秒設定定時周期
UINT wAccuracy,
//以毫秒指定延時的精度,默認值為1ms
LPTIMECALLBACK lpTimeProc,
//回調函數,用戶自定義,定時調用
DWORD dwUser,
//存放回調數據
UINT fuEvent
//指定定時器事件類型:
//TIME_ONESHOT:uDelay毫秒后只產生一次事件
//TIME_PERIODIC:每隔uDelay毫秒周期性地產生事件
);
該函數設置一個定時回調事件,此事件可以是一個一次性事件或者是周期性事件。事件一旦被激活,即調用指定的回調函數,成功則返回事件的標識符代碼,否則返回NULL.
6)static VOID CALLBACK TimeProc() 回調函數是由開發者自己編寫的周期性執行的函數。回調函數可以定義為相應的類成員函數或者全局函數。C++的類成員都有一個自動隱藏的私有成員this指針,如果回調函數定義為類的普通成員函數,函數類型與CALLBACK函數預設類型不相同。這時要定義為靜態類型,屏蔽this指針,然而靜態成員函數只能訪問靜態成員,不便操作,所以,回調函數一般定義為全局函數[2]。
在使用多媒體定時器時,將需要周期性循環執行的任務定義在 lpTimeProc回調函數中(定時采樣、控制等),通過調用timeSetEvent()函數觸發回調函數,完成所需處理的任務。定時器使用完畢后,應及時調用timeKillEvent()釋放。如圖2所示,簡要概括了使用多媒體定時器的基本流程。

圖2 多媒體定時器采集數據的程序框圖
Visual Studio 2010為開發工具,使用多媒體定時器對PLC內D寄存器數據進行實時采集,主要步驟和代碼如下:
1)包含頭文件,定義全局變量
#include 'Mmsystem"
#pragma comment(lib, "Winmm.lib")
HWND hWnd;//窗口句柄
UINT uTimerID;//定時器句柄
UINT uResolution;//定時器分辨率
在BOOL CDDataAquisitionDlg::OnInitDialog()中添加代碼:
hWnd=this->m_hWnd;
2)定義回調函數,用于處理需要周期性執行的任務,即采集數據
static VOID CALLBACK ProFunc(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2)
{
...
采集數據
...
}
3)啟動定時器,觸發2)中回調函數
BOOL TimerStart(UINT &wAccuracy,UINT uDelay,UINT &TimerID,LPVOID fptc,DWORD dwUser)
{
TIMECAPS tc;
if(timeGetDevCaps(&tc,sizeof(TIMECAPS))==TIMERR_NOERROR)
{
//分辨率的值不能超出系統的取值范圍
wAccuracy = min(max(tc.wPeriodMin,1),tc.wPeriodMax);
//設置定時器的分辨率
timeBeginPeriod(wAccuracy);
}
else
return FALSE;
//啟動定時器
TimerID=timeSetEvent(uDelay,wAccuracy,(LPTIMECALLBACK)fptc,dwUser,TIME_PERIODIC);
if(TimerID==0) return FALSE;
return TRUE;
}
4)關閉定時器,釋放定時器資源
void TimerEnd(UINT uTimerID,UINT &uResolution)
{
//刪除定時事件
timeKillEvent(uTimerID);
if(lpuPeriod != NULL)
//清除定時器分辨率
timeEndPeriod(uResolution);
}
支持WM_TIMER定時器的底層硬件中斷頻率是18.2Hz,即每隔至少54.945ms中斷一次。我們知道,Windows是一個基于消息機制的搶占式操作系統,操作系統給每個應用程序都建立一個消息隊列,而且消息隊列中常有大量的消息等待處理[3],一旦系統資源緊張或者CPU被占用,隊列中的消息將被掛起,不能得到實時響應。另外,WM_TIMER消息的優先級很低,排在其他消息(WM_PAINT除外)之后響應,一個消息隊列中只允許有一個WM_TIMER消息,倘若遇到系統繁忙處理一個消息超時,后面產生的多個相同ID的消息會合并成一個,所以該定時器對消息的處理在數目上可能出現錯誤,時間上甚至出現“丟秒”的現象。顯然WM_TIMER定時器不適用于精度要求較高的控制系統中,但是WM_TIMER定時器使用起來很方便,而且相當安全,可以在定時函數中進行幾乎任何操作也不用擔心死機的問題。即使出現處理超時的情況,也不會引起系統崩潰。
多媒體定時器的工作原理和WM_TIMER定時器不同,多媒體定時器并不依賴消息機制,而是提供硬件中斷服務,并且Windows提供了高精度定時器的底層API支持。timeSetEvent()函數產生獨立的線程,直接調用回調函數,不存在消息隊列和優先級的問題,是一種理想的高精度定時器,最小精度可以達到1ms。在使用多媒體定時器的過程中要注意:定時器線程結束前不可再次啟動定時器,這樣容易迅速出現死機現象[4]。任務處理的時間不能大于周期間隔時間,不然會引起程序崩潰。定時器是系統資源,使用完畢之后應該釋放,分辨率也要清除,以免降低系統響應的速度。
最后,通過在Visual Studio中設計實驗做形象對比,建立單文檔的MFC工程,在View類中作圖,使條帶長度從0開始隨時間而動態增加,可在任意時刻執行暫停,同時生成條帶圖形并讀取長度。在程序中添加多個WM_TIMER定時器和多媒體定時器。實驗中,每個定時器都設置一個時間間隔(可更改),擬使每個時間間隔內條帶的長度都增加1個單位長度。當時間間隔設置的很小,只有十幾毫秒甚至一毫秒時,從圖3可以看出,WM_TIMER定時器對時間間隔的差異并不敏感,可見這些時間間隔不在其定時精度范圍,而多媒體定時器卻十分精準,精確到1ms.當時間間隔達到幾百毫秒或者秒級時,如圖4所示,兩種定時器幾乎沒有差異。此實驗在不同的機型上測試過,如Intel奔騰雙核,主頻2.5GHz的長城電腦和Intel酷睿i5,主頻為3.1GHz的啟天M4350等,都有相同的實驗結果。
綜上所述,WM_TIMER定時器實現過程較為簡單、安全,主要用于對定時要求不高,采樣周期較長的系統中;而多媒體定時器可以用于精度要求較高,采樣周期較短的控制系統中。

圖3 定時間隔只有幾毫秒時兩種定時器對比

圖4 定時間隔幾百毫秒時兩種定時器對比
編寫此上位機軟件的目的是對PLC內D寄存器的數據進行實時采集,在開發過程中,先是選擇了WM_TIMER定時器,使用很方便,用于采樣周期較長(幾百毫秒)的任務時,還是有著良好的效果。若是遇到精度要求較高,采樣周期只有幾毫秒的任務,就必須使用多媒體定時器完成任務。本文兩種定時器的設計方法均在當前流行的Visual Studio 2010中調試通過,并且在具體應用中切實可行。
參考文獻:
[1]王光輝. 深入分析Windows消息機制[J]. 電腦與電信,2011,02:69~70+73.
[2]王 偉,徐國華. 多媒體定時器在工業控制中的應用[J]. 微型機與應用,2001,12:8~10.
[3]卓紅艷,趙 平. 基于VC++的實時數據采集系統中定時器的使用與比較[J]. 現代電子技術,2007,18:80~82.
[4]黃兆祥,郭麥成,沈利香. Win9x下實時數據采集的實現[J]. 微計算機信息,2003,11:34~35.