用Visual C++制作微秒級精度定時器
在工業生產控制系統中,有許多需要定時完成的操作,如:定時顯示當前時間,定時刷新屏幕上的進度條,上位機定時向下位機發送命令和傳送數據等。特別是在對控制 性能 要求較高的控制系統和數據采集系統中,就更需要精確定時操作。眾所周知, Windows 是基于消
在工業生產控制系統中,有許多需要定時完成的操作,如:定時顯示當前時間,定時刷新屏幕上的進度條,上位機定時向下位機發送命令和傳送數據等。特別是在對控制
性能要求較高的控制系統和數據采集系統中,就更需要精確定時操作。眾所周知,
Windows是基于消息機制的系統,任何事件的執行都是通過發送和接收消息來完成的。這樣就帶來了一些問題,如一旦計算機的CPU被某個進程占用,或系統資源緊張時,發送到消息隊列中的消息就暫時被掛起,得不到實時處理。因此,不能簡單地通過
Windows消息引發一個對定時要求嚴格的事件。另外,由于在
Windows中已經封裝了計算機底層硬件的訪問,所以要想通過直接利用訪問硬件來完成精確定時,也比較困難。在實際應用時,應針對具體定時精度的要求,采取與之相適應的定時方法。
本實例實現了一中微秒級的精確定時,程序的界面提供了兩個"Edit"編輯框,其中一個編輯框輸入用戶理想的定時長度,另外一個編輯框返回實際的時間長度,經過大量的實驗
測試,一般情況下誤差不超過5個微秒。程序的運行界面如圖一所示:
 圖一、實現微秒級的精確定時器 |
一、實現方法 Visual
C++中提供了很多關于時間操作的函數,利用它們控制程序能夠精確地完成定時和計時操作。Visaul
C++中的WM_TIMER消息映射能進行簡單的時間控制。首先調用函數SetTimer()設置定時間隔(退出程序時別忘了調用和SetTimer()配對使用的KillTimer()函數),如SetTimer(0,200,NULL)即為設置200ms的時間間隔。然后在應用程序中增加定時響應函數OnTimer(),并在該函數中添加響應的處理語句,用來完成到達定時時間的操作。這種定時方法非常簡單,但其定時功能如同Sleep()函數的延時功能一樣,精度非常低,只可以用來實現諸如位圖的動態顯示等對定時精度要求不高的情況。
微軟公司在其多媒體Windows中提供了精確定時器的底層API支持。利用多媒體定時器可以很精確地讀出系統的當前時間,并且能在非常精確的時間間隔內完成一個事件、函數或過程的調用。利用多媒體定時器的基本功能,可以通過兩種方法實現精確定時。1)使用timeGetTime()函數,該函數定時精度為ms級,返回從Windows啟動開始所經過的時間。由于使用該函數是通過查詢的方式進行定時控制的,所以,應該建立定時循環來進行定時事件的控制。2)使用timeSetEvent()函數,該函數原型如下:
clearcase/" target="_blank" >cccccc width="90%" align=center bgColor=#e3e3e3 border=1>
MMRESULT timeSetEvent(UINT uDelay,UINT uResolution,LPTIMECALLBACK lpTimeProc, DWORD dwUser,UINT fuEvent); |
該函數的參數說明如下:參數uDelay表示延遲時間;參數uResolution表示時間精度,在Windows中缺省值為1ms;lpTimeProc表示回調函數,為用戶自定義函數,定時調用; 參數dwUser表示用戶提供的回調數據;參數fuEvent為定時器的事件類型,TIME_ONESHOT表示執行一次;TIME_PERIODIC:周期性執行。具體應用時,可以通過調用timeSetEvent()函數,將需要周期性執行的任務定義在lpTimeProc回調函數中(如:定時采樣、控制等),從而完成所需處理的事件。需要注意的是:任務處理的時間不能大于周期間隔時間。另外,在定時器使用完畢后,應及時調用timeKillEvent()將之釋放。下面這段代碼的主要功能是設置兩個時鐘定時器,一個間隔是1ms,一個間隔是2s。每執行一次,把當前系統時鐘值輸入文件"cure.out"中,以比較該定時器的精確度。
# define ONE_MILLI_SECOND 1 //定義1ms和2s時鐘間隔,以ms為單位 ; # define TWO_SECOND 2000 # define TIMER_ACCURACY 1 //定義時鐘分辨率,以ms為單位 UINT wTimerRes_1ms,wTimerRes_2s; //定義時間間隔 UINT wAccuracy; //定義分辨率 UINT TimerID_1ms,TimerID_2s; //定義定時器句柄 /////////////////////////////// CCureApp::CCureApp():fout("cure.out", ios::out) //打開輸出文件"cure.out"; { // 給時間間隔變量賦值 wTimerRes_1ms = ONE_MILLI_SECOND; wTimerRes_2s = TWO_SECOND; TIMECAPS tc; //利用函數timeGetDevCaps取出系統分辨率的取值范圍,如果無錯則繼續; if(timeGetDevCaps(&tc,sizeof(TIMECAPS))==TIMERR_NOERROR) { wAccuracy=min(max(tc.wPeriodMin, //分辨率的值不能超出系統的取值范圍 TIMER_ACCURACY),tc.wPeriodMax); //調用timeBeginPeriod函數設置定時器的分辨率 timeBeginPeriod(wAccuracy); //設置定時器 InitializeTimer(); } } CCureApp:: ~CCureApp() { fout <<"結束時鐘"<< endl; //結束時鐘 timeKillEvent(TimerID_1ms); // 刪除兩個定時器 timeKillEvent(TimerID_2s); // 刪除設置的分辨率 timeEndPeriod(wAccuracy); } void CCureApp::InitializeTimer() { StartOneMilliSecondTimer(); StartTwoSecondTimer(); } //1ms定時器的回調函數,類似于中斷處理程序,一定要聲明為全局PASCAL函數, //否則編譯會有問題 void PASCAL OneMilliSecondProc(UINT wTimerID, UINT msg,DWORD dwUser, DWORD dwl,DWORD dw2) { // 定義計數器 static int ms = 0; CCureApp *app = (CCureApp *)dwUser; // 取得系統時間,以ms為單位 DWORD osBinaryTime = GetTickCount(); //輸出計數器值和當前系統時間 app->fout<<++ms<<":1ms:" } // 加裝1ms定時器 void CCureApp::StartOneMilliSecondTimer() { if((TimerID_1ms = timeSetEvent(wTimerRes_1ms, wAccuracy, (LPTIMECALBACK) OneMil liSecondProc, // 回調函數; (DWORD)this, // 用戶傳送到回調函數的數據; TIME_PERIODIC)) == 0)//周期調用定時處理函數; { AfxMessageBox("不能進行定時!", MB_OK | MB_ICONASTERISK); } else fout << "16ms 計 時:" << endl; //不等于0表明加裝成功,返回此定時器的句柄; } |
在精度要求較高的情況下,如要求定時誤差不大于1ms時,還可以利用GetTickCount()函數返回自計算機啟動后的時間,該函數的返回值是DWORD型,表示以ms為單位的計算機啟動后經歷的時間間隔。通過兩次調用GetTickCount()函數,然后控制它們的差值來取得定時效果.下列的代碼可以實現50ms的精確定時,其誤差是毫秒級的。
// 起始值和中止值 DWORD dwStart, dwStop ; dwStop = GetTickCount(); while(TRUE) { // 上一次的中止值變成新的起始值 dwStart = dwStop ; // 此處添加相應控制語句 do { dwStop = GetTickCount() ; }while(dwStop - 50 < dwStart) ; } |
用上述兩種方式取得的定時效果雖然在許多場合已經滿足實際的要求,但由于它們的精度只有毫秒級的,而且在要求定時時間間隔小時,實際定時誤差大。對于精確度要求更高的定時操作,則應該使用QueryPerformanceFrequency()和QueryPerformanceCounter()函數。這兩個函數是Visual C++提供并且僅供Windows 95及其后續版本使用,其精度與CPU的時鐘頻率有關,它們要求計算機從硬件上支持精確定時器。QueryPerformanceFrequency()函數和QueryPerformanceCounter()函數的原型如下:
BOOL QueryPerformanceFrequency (LARGE_INTEGER *lpFrequency); BOOL QueryPerformanceCounter (LARGE_INTEGER *lpCount); |
上述兩個函數的參數的數據類型LARGE_INTEGER既可以是一個8字節長的整型數,也可以是兩個4字節長的整型數的聯合結構,其具體用法根據編譯器是否支持64位而定。該類型的定義如下:
typedef union _LARGE_INTEGER { struct{ DWORD LowPart ; // 4字節整型數 LONG HighPart ; // 4字節整型數 }; LONG QuadPart ; // 8字節整型數 } LARGE_INTEGER ; |
使用QueryPerformanceFrequency()和QueryPerformanceCounter()函數進行精確定時的步驟如下:
1、首先調用QueryPerformanceFrequency()函數取得高精度運行計數器的頻率f,單位是每秒多少次(n/s),此數一般很大;
2、在需要定時的代碼的兩端分別調用QueryPerformanceCounter()以取得高精度運行計數器的數值n1、n2,兩次數值的差值通過f換算成時間間隔,t=(n2-n1)/f,當t大于或等于定時時間長度時,啟動定時器;