最近涉及到时钟方面的编程,觉得这篇文章总结的不错,收藏了
摘自http://www.vckbase.com/document/viewdoc/?id=1301
在工业生产控制系统中,有许多需要定时完成的操作,如定时显示当前时间,定时刷新屏幕上的进度条,上位
众所周知,Windows 是基于消息机制的系统,任何事件的执行都是通过发送和接收消息来完成的。
VC中提供了很多关于时间操作的函数,利用它们控制程序能够精确地完成定时和计时操作。本文详细介绍了
图一 图像描述
方式一:VC中的WM_TIMER消息映射能进行简单的时间控制。首先调用函数SetTimer()设置定时
方式二:VC中使用sleep()函数实现延时,它的单位是ms,如延时2秒,用sleep(2000)。精度非常
方式三:利用COleDateTime类和COleDateTimeSpan类结合WINDOWS的消息处理过程来实现秒级延时。如示例工程中的Timer3和Timer3_1。以下是实现2秒的延时代码:
COleDateTime start_time = COleDateTime::GetCurrentTime(); COleDateTimeSpan end_time= COleDateTime::GetCurrentTime()-start_time; while(end_time.GetTotalSeconds()< 2) //实现延时2秒 { MSG msg; GetMessage(&msg,NULL,0,0); TranslateMessage(&msg); DispatchMessage(&msg); //以上四行是实现在延时或定时期间能处理其他的消息, //虽然这样可以降低CPU的占有率, //但降低了延时或定时精度,实际应用中可以去掉。 end_time = COleDateTime::GetCurrentTime()-start_time; }//这样在延时的时候我们也能够处理其他的消息。方式四:在精度要求较高的情况下,VC中可以利用GetTickCount()函数,该函数的返回值是
DWORD dwStart = GetTickCount(); DWORD dwEnd = dwStart; do { dwEnd = GetTickCount()-dwStart; }while(dwEnd <50);为使GetTickCount()函数在延时或定时期间能处理其他的消息,可以把代码改为:
DWORD dwStart = GetTickCount(); DWORD dwEnd = dwStart; do { MSG msg; GetMessage(&msg,NULL,0,0); TranslateMessage(&msg); DispatchMessage(&msg); dwEnd = GetTickCount()-dwStart; }while(dwEnd <50);虽然这样可以降低CPU的占有率,并在延时或定时期间也能处理其他的消息,但降低了延时或定时精度。
方式五:与GetTickCount()函数类似的多媒体定时器函数DWORD timeGetTime(void),该函数定时精
方式六:使用多媒体定时器timeSetEvent()函数,该函数定时精度为ms级。利用该函数可以实现周期性的函数调用。如示例工程中的Timer6和Timer6_1。函数的原型如下:
MMRESULT timeSetEvent( UINT uDelay, UINT uResolution, LPTIMECALLBACK lpTimeProc, WORD dwUser, UINT fuEvent )该函数设置一个定时回调事件,此事件可以是一个一次性事件或周期性事件。事件一旦被激活,便调用指定的回调函数,
uDelay:以毫秒指定事件的周期。 Uresolution:以毫秒指定延时的精度,数值越小定时器事件分辨率越高。缺省值为1ms。 LpTimeProc:指向一个回调函数。 DwUser:存放用户提供的回调数据。 FuEvent:指定定时器事件类型: TIME_ONESHOT:uDelay毫秒后只产生一次事件 TIME_PERIODIC :每隔uDelay毫秒周期性地产生事件。具体应用时,可以通过调用timeSetEvent()函数,将需要周期性执行的任务定义在LpTimeProc回调函数
方式七:对于精确度要求更高的定时操作,则应该使用QueryPerformanceFrequency()和
QueryPerformanceFrequency()函数和QueryPerformanceCounter()函数的原型如下:
BOOL QueryPerformanceFrequency(LARGE_INTEGER *lpFrequency); BOOL QueryPerformanceCounter(LARGE_INTEGER *lpCount);数据类型ARGE_INTEGER既可以是一个8字节长的整型数,也可以是两个4字节长的整型数的联合结构,
typedef union _LARGE_INTEGER { struct { DWORD LowPart ;// 4字节整型数 LONG HighPart;// 4字节整型数 }; LONGLONG QuadPart ;// 8字节整型数 }LARGE_INTEGER ;在进行定时之前,先调用QueryPerformanceFrequency()函数获得机器内部定时器的时钟频率,
LARGE_INTEGER litmp; LONGLONG QPart1,QPart2; double dfMinus, dfFreq, dfTim; QueryPerformanceFrequency(&litmp); dfFreq = (double)litmp.QuadPart;// 获得计数器的时钟频率 QueryPerformanceCounter(&litmp); QPart1 = litmp.QuadPart;// 获得初始值 do { QueryPerformanceCounter(&litmp); QPart2 = litmp.QuadPart;//获得中止值 dfMinus = (double)(QPart2-QPart1); dfTim = dfMinus / dfFreq;// 获得对应的时间值,单位为秒 }while(dfTim<0.001);其定时误差不超过1微秒,精度与CPU等机器配置有关。 下面的程序用来测试函数Sleep(100)的精确持续时间:
LARGE_INTEGER litmp; LONGLONG QPart1,QPart2; double dfMinus, dfFreq, dfTim; QueryPerformanceFrequency(&litmp); dfFreq = (double)litmp.QuadPart;// 获得计数器的时钟频率 QueryPerformanceCounter(&litmp); QPart1 = litmp.QuadPart;// 获得初始值 Sleep(100); QueryPerformanceCounter(&litmp); QPart2 = litmp.QuadPart;//获得中止值 dfMinus = (double)(QPart2-QPart1); dfTim = dfMinus / dfFreq;// 获得对应的时间值,单位为秒由于Sleep()函数自身的误差,上述程序每次执行的结果都会有微小误差。下列代码实现1微秒的精确定时:
LARGE_INTEGER litmp; LONGLONG QPart1,QPart2; double dfMinus, dfFreq, dfTim; QueryPerformanceFrequency(&litmp); dfFreq = (double)litmp.QuadPart;// 获得计数器的时钟频率 QueryPerformanceCounter(&litmp); QPart1 = litmp.QuadPart;// 获得初始值 do { QueryPerformanceCounter(&litmp); QPart2 = litmp.QuadPart;//获得中止值 dfMinus = (double)(QPart2-QPart1); dfTim = dfMinus / dfFreq;// 获得对应的时间值,单位为秒 }while(dfTim<0.000001);
其定时误差一般不超过0.5微秒,精度与CPU等机器配置有关。(完)
续----------------------------------------------------------
一般时控函数
VC程序员都会利用Windows的WM_TIMER消息映射来进行简单的时间控制:1.调用函数SetTimer()设置定时间隔,如SetTimer(0,200,NULL)即为设置200毫秒的时间间隔;2.在应用程序中增加定时响应函数OnTimer(),并在该函数中添加响应的处理语句,用来完成时间到时的操作。这种定时方法是非常简单的,但其定时功能如同Sleep()函数的延时功能一样,精度较低,只可以用来实现诸如位图的动态显示等对定时精度要求不高的情况,而在精度要求较高的条件下,这种方法应避免采用。
精度时控函数
在要求误差不大于1毫秒的情况下,可以采用GetTickCount()函数,该函数的返回值是DWORD型,表示以毫秒为单位的计算机启动后经历的时间间隔。使用下面的编程语句,可以实现50毫秒的精确定时,其误差小于1毫秒。
DWORD dwStart, dwStop;
// 起始值和终止值
dwStop = GetTickCount();
while(TRUE)
{
dwStart = dwStop;
// 上一次的终止值变成新的起始值
// 此处添加相应控制语句
do
{
dwStop = GetTickCount();
}while(dwStop - 50 < dwStart);
}
高精度时控函数
对于一般的实时控制,使用GetTickCount()函数就可以满足精度要求,但要进一步提高计时精度,就要采用QueryPerformanceFrequency()函数和QueryPerformanceCounter()函数。这两个函数是VC提供的仅供Windows 9X使用的高精度时间函数,并要求计算机从硬件上支持高精度计时器。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字节整型数
};
LONGLONG QuadPart;
// 8字节整型数
} LARGE_INTEGER;
在进行计时之前,应该先调用QueryPerformanceFrequency()函数获得机器内部计时器的时钟频率。笔者在主频为266、300、333的三种PentiumⅡ机器上使用该函数,得到的时钟频率都是1193180Hz。接着,笔者在需要严格计时的事件发生之前和发生之后分别调用QueryPerformanceCounter()函数,利用两次获得的计数之差和时钟频率,就可以计算出事件经历的精确时间。以下程序是用来测试函数Sleep(100)的精确持续时间。
LARGE—INTEGER litmp;
LONGLONG QPart1,QPart2;
double dfMinus, dfFreq, dfTim;
QueryPerformanceFrequency(&litmp);
// 获得计数器的时钟频率
dfFreq = (double)litmp.QuadPart;
QueryPerformanceCounter(&litmp);
// 获得初始值
QPart1 = litmp.QuadPart;
Sleep(100) ;
QueryPerformanceCounter(&litmp);
// 获得终止值
QPart2 = litmp.QuadPart;
dfMinus = (double)(QPart2 - QPart1);
dfTim = dfMinus / dfFreq;
// 获得对应的时间值
执行上面程序,得到的结果为dfTim=0.097143767076216(秒)。细心的读者会发现,每次执行的结果都不一样,存在一定的差别,这是由于Sleep()自身的误差所致。
续--------------------------------
对关注性能的程序
开发
人员而言,一个好的计时部件既是益友,也是良师。计时器既可以作为程序组件帮助程序员精确的控制程序进程,又是一件有力的调试武器,在有经验的程序员手里可以尽快的确定程序的性能瓶颈,或者对不同的算法作出有说服力的性能比较。
在Windows平台下,常用的计时器有两种,一种是timeGetTime多媒体计时器,它可以提供毫秒级的计时。但这个精度对很多应用场合而言还是太粗糙了。另一种是QueryPerformanceCount计数器,随系统的不同可以提供微秒级的计数。对于实时图形处理、多媒体数据流处理、或者实时系统构造的程序员,善用QueryPerformanceCount/QueryPerformanceFrequency是一项基本功。
本文要介绍的,是另一种直接利用Pentium CPU内部时间戳进行计时的高精度计时手段。以下讨论主要得益于《Windows图形编程》一书,第15页-17页,有兴趣的读者可以直接参考该书。关于RDTSC指令的详细讨论,可以参考Intel产品手册。本文仅仅作抛砖之用。
在Intel Pentium以上级别的CPU中,有一个称为“时间戳(Time Stamp)”的部件,它以64位无符号整型数的格式,
记录
了自CPU上电以来所经过的时钟周期数。由于目前的CPU主频都非常高,因此这个部件可以达到纳秒级的计时精度。这个精确性是上述两种方法所无法比拟的。
在Pentium以上的CPU中,提供了一条机器指令RDTSC(Read Time Stamp Counter)来读取这个时间戳的
数字
,并将其保存在EDX:EAX寄存器对中。由于EDX:EAX寄存器对恰好是Win32平台下C++语言保存函数返回值的寄存器,所以我们可以把这条指令看成是一个普通的函数调用。像这样:
inline unsigned __int64 GetCycleCount()
{
__asm RDTSC
}
但是不行,因为RDTSC不被C++的内嵌汇编器直接支持,所以我们要用_emit伪指令直接嵌入该指令的机器码形式0X0F、0X31,如下:
inline unsigned __int64 GetCycleCount()
{
__asm _emit 0x0F
__asm _emit 0x31
}
以后在需要计数器的场合,可以像使用普通的Win32 API一样,调用两次GetCycleCount函数,比较两个返回值的差,像这样:
unsigned long t;
t = (unsigned long)GetCycleCount();
//Do Something time-intensive ...
t -= (unsigned long)GetCycleCount();
《Windows图形编程》第15页编写了一个类,把这个计数器封装起来。有兴趣的读者可以去参考那个类的代码。作者为了更精确的定时,做了一点小小的改进,把执行RDTSC指令的时间,通过连续两次调用GetCycleCount函数计算出来并保存了起来,以后每次计时结束后,都从实际得到的计数中减掉这一小段时间,以得到更准确的计时数字。但我个人觉得这一点点改进意义不大。在我的机器上实测,这条指令大概花掉了几十到100多个周期,在Celeron 800MHz的机器上,这不过是十分之一微秒的时间。对大多数应用来说,这点时间完全可以忽略不计;而对那些确实要精确到纳秒数量级的应用来说,这个补偿也过于粗糙了。
这个方法的优点是:
1.高精度。可以直接达到纳秒级的计时精度(在1GHz的CPU上每个时钟周期就是一纳秒),这是其他计时方法所难以企及的。
2.成本低。timeGetTime 函数需要链接多媒体库winmm.lib,QueryPerformance* 函数根据MSDN的说明,需要硬件的支持(虽然我还没有见过不支持的机器)和KERNEL库的支持,所以二者都只能在Windows平台下使用(关于DOS平台下的高精度计时问题,可以参考《图形程序开发人员指南》,里面有关于控制定时器8253的详细说明)。但RDTSC指令是一条CPU指令,凡是i386平台下Pentium以上的机器均支持,甚至没有平台的限制(我相信i386版本UNIX和Linux下这个方法同样适用,但没有条件试验),而且函数调用的开销是最小的。
3.具有和CPU主频直接对应的速率关系。一个计数相当于1/(CPU主频Hz数)秒,这样只要知道了CPU的主频,可以直接计算出时间。这和QueryPerformanceCount不同,后者需要通过QueryPerformanceFrequency获取当前计数器每秒的计数次数才能换算成时间。
这个方法的缺点是:
1.现有的C/C++编译器多数不直接支持使用RDTSC指令,需要用直接嵌入机器码的方式编程,比较麻烦。
2.数据抖动比较厉害。其实对任何计量手段而言,精度和稳定性永远是一对矛盾。如果用低精度的timeGetTime来计时,基本上每次计时的结果都是相同的;而RDTSC指令每次结果都不一样,经常有几百甚至上千的差距。这是这种方法高精度本身固有的矛盾。
关于这个方法计时的最大长度,我们可以简单的用下列公式计算:
自CPU上电以来的秒数 = RDTSC读出的周期数 / CPU主频速率(Hz)
64位无符号整数所能表达的最大数字是1.8×10^19,在我的Celeron 800上可以计时大约700年(书中说可以在200MHz的Pentium上计时117年,这个数字不知道是怎么得出来的,与我的计算有出入)。无论如何,我们大可不必关心溢出的问题。
下面是几个小例子,简要比较了三种计时方法的用法与精度
//Timer1.cpp 使用了RDTSC指令的Timer类//KTimer类的定义可以参见《Windows图形编程》P15
//编译行:CL Timer1.cpp /link USER32.lib
#include <stdio.h>
#include "KTimer.h"
main()
{
unsigned t;
KTimer timer;
timer.Start();
Sleep(1000);
t = timer.Stop();
printf("Lasting Time: %d/n",t);
}
//Timer2.cpp 使用了timeGetTime函数
//需包含<mmsys.h>,但由于Windows头文件错综复杂的关系
//简单包含<windows.h>比较偷懒:)
//编译行:CL timer2.cpp /link winmm.lib
#include <windows.h>
#include <stdio.h>
main()
{
DWORD t1, t2;
t1 = timeGetTime();
Sleep(1000);
t2 = timeGetTime();
printf("Begin Time: %u/n", t1);
printf("End Time: %u/n", t2);
printf("Lasting Time: %u/n",(t2-t1));
}
//Timer3.cpp 使用了QueryPerformanceCounter函数
//编译行:CL timer3.cpp /link KERNEl32.lib
#include <windows.h>
#include <stdio.h>
main()
{
LARGE_INTEGER t1, t2, tc;
QueryPerformanceFrequency(&tc);
printf("Frequency: %u/n", tc.QuadPart);
QueryPerformanceCounter(&t1);
Sleep(1000);
QueryPerformanceCounter(&t2);
printf("Begin Time: %u/n", t1.QuadPart);
printf("End Time: %u/n", t2.QuadPart);
printf("Lasting Time: %u/n",( t2.QuadPart- t1.QuadPart));
}
//以上三个示例程序都是测试1秒钟休眠所耗费的时间
file://测/试环境:Celeron 800MHz / 256M SDRAM
// Windows 2000 Professional SP2
// Microsoft Visual C++ 6.0 SP5
以下是Timer1的运行结果,使用的是高精度的RDTSC指令
Lasting Time: 804586872
以下是Timer2的运行结果,使用的是最粗糙的timeGetTime API
Begin Time: 20254254
End Time: 20255255
Lasting Time: 1001
以下是Timer3的运行结果,使用的是QueryPerformanceCount API
Frequency: 3579545
Begin Time: 3804729124
End Time: 3808298836
Lasting Time: 3569712
古人说,触类旁通。从一本介绍图形编程的书上得到一个如此有用的实时处理知识,我感到非常高兴。有美不敢自专,希望大家和我一样喜欢这个轻便有效的计时器。
续-----------------------------------------------------------
以下是多媒体时钟的应用实例,是我比较欣赏的
void CCSDN_DlgDlg::StartTimerCount()
{
m_nIDTimerEvent = ::timeSetEvent (
500,
theApp.m_uTimerPeriod,
TimerProc,
(DWORD)this,
TIME_PERIODIC );
if( m_nIDTimerEvent == 0 )
AfxMessageBox( _T("启动计时器失败") );
}
//
// 函数:TimerProc
// 说明:计时多媒体时钟回调函数
// 参数:
// 返回:无
//
void CALLBACK TimerProc( UINT uTimerID, UINT uTimerMsg, DWORD dwTimerPointer, /
DWORD dwReserved1, DWORD dwReserved2 )
{
CCSDN_DlgDlg *pMainDlg;
pMainDlg = (CCSDN_DlgDlg *)dwTimerPointer;
pMainDlg->SetTimerProc();
return;
}
void CCSDN_DlgDlg::SetTimerProc()
{
CString strSystime;
strSystime = GetCurrentDateTime();
strSystime = _T("系统时钟:") + strSystime;
m_ctrSystime.SetWindowText( strSystime );
}
void CCSDN_DlgDlg::OnDestroy()
{
CDialog::OnDestroy();
// TODO: Add your message handler code here
::timeKillEvent( m_nIDTimerEvent );
}
以下是SetTimer的调用实例
void CALLBACK TimerCallProc( HWND hwnd, // handle to window
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // current system time
)
{
AfxMessageBox( _T("OK") );
}
void CCSDN_DlgDlg::OnSettimer()
{
// TODO: Add your control notification handler code here
::SetTimer( NULL, 1, 100, TimerCallProc );
}