C#实现高精度定时器

时间:2021-08-29 23:27:17

这两天正在准备做一个实时控制的东西,想用C#。可是昨天日本人展示了一个在LINUX平台下使用C语言控制的单*度机械臂,我问他们为什么不用WINDOWS,他们说用WINDOWS编程实时性很差,定时很不准,所以用了LINUX,为了兼容性好,服务器也用的是LINUX平台,用于网络控制。可是如果网络也用C或C++的话编程肯定比较慢,还是想用C#编程,于是今天就研究了一下C#中定时器的问题。

在.Net Framework中在三个地方有定时器类分别是System.Timers.Timer, System.Threading.Timer和System.Windows.Form.Timer。与了个程序分别对这三个类进行了测试,精度的确差得不能用,三个定时精度都差不多,最好可以到15ms左右,但很不稳定,定时间隔误差比较大,五般在15个ms左右,甚至更多,经过MATLAB画出的曲线看,误差还是有规律的,下面是定时间隔为40ms时,定时器中断服务函数相临两次响应之间的时间间隔采样数据连线图。

C#实现高精度定时器

由图中可以看出,在很多时候实际响应时间要比设定时间晚10毫秒左右,但采样时间都已经40ms了,如果把采样时间再调小一些误差会更大,而且经过测试,最小间隔只能到15ms,即使把间隔调成1ms,实际响应时间都在15ms以上,这种精度在实时控制中几乎不能接受,故而需要使用其它方法提高定时器的精度。

因为WINDOWS是多任务的操作系统,定时器的延时可能是由于其它任务占用CPU资源,没能及时响应定时器事件,如果能够改变定时器线程的优先级应该也可以改善定时器的精度,但查了一些资料也没有看到能够控制那些定时器类封装的纯种的优先级的资料,因为这些类都没有提供控制定时器所在线程的优先级的接口。故而想自己构造一个高精度的定时器,使得精度尽量能达到1ms。

在查阅资料中发现两篇文章很有用,一个是介绍VC使用WIN32API函数

///


/// Pointer to a variable that receives the current performance-counter value, in counts.
///
///
/// If the function succeeds, the return value is nonzero.
///
[DllImport("Kernel32.dll")]
private static extern bool QueryPerformanceFrequency(out long lpPerformanceCount);

 

///


/// Pointer to a variable that receives the current performance-counter frequency,
/// in counts per second.
/// If the installed hardware does not support a high-resolution performance counter,
/// this parameter can be zero.
///
///
/// If the installed hardware supports a high-resolution performance counter,
/// the return value is nonzero.
///
[DllImport("Kernel32.dll")]
private static extern bool QueryPerformanceFrequency(out  long lpFrequency);

 

另一篇就是介绍在C#中使用这两个函数的方法了,也就是上面的代码。QueryPerformanceFrequency 函数是读取系统所支持的硬件定时器计数频率,也就是每秒的计数值,QueryPerformanceFrequency应该是从开机时的计数值。这样通过读取两次计数值之差,再除以计数频率也就是两次读取时钟间隔的秒数。然后通过查询两次的间隔来确定是否响应服务函数,封装了如下类:

    /// 
    /// ManualTimer
    /// A simulated timer by loop 
    /// It creates a new thread in Thread Pool using ThreadPool class
    /// Nocky Tian @ 2008-3-16
    /// 
    /// The timer starts a new thread using  object,
    /// and the value of the property Priority is set to 
    /// so that the accuray could be kept 1ms around.
    /// 
    /// 
    /// 
    /// 
    public class UltraHighAccurateTimer
    {
        public event ManualTimerEventHandler tick;
        private object threadLock = new object();       // for thread safe
        private long clockFrequency;            // result of QueryPerformanceFrequency() 
        bool running = true;
        Thread thread ;

        private int intervalMs;                     // interval in mimliseccond;

        /// 
        /// Timer inteval in milisecond
        /// 
        public int Interval
        {
            get { return intervalMs; }
            set
            {
                intervalMs = value;
                intevalTicks = (long)((double)value * (double)clockFrequency / (double)1000);
            }
        }
        private long intevalTicks;
        private long nextTriggerTime;               // the time when next task will be executed

        /// 
        /// Pointer to a variable that receives the current performance-counter value, in counts. 
        /// 
        /// 
        /// If the function succeeds, the return value is nonzero.
        /// 
        [DllImport("Kernel32.dll")]
        private static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

        /// 
        /// Pointer to a variable that receives the current performance-counter frequency, 
        /// in counts per second. 
        /// If the installed hardware does not support a high-resolution performance counter, 
        /// this parameter can be zero. 
        /// 
        /// 
        /// If the installed hardware supports a high-resolution performance counter, 
        /// the return value is nonzero.
        /// 
        [DllImport("Kernel32.dll")]
        private static extern bool QueryPerformanceFrequency(out  long lpFrequency);


        protected void OnTick()
        {
            if (tick != null) {
                tick();
            }
        }

        public UltraHighAccurateTimer()
        {
            if (QueryPerformanceFrequency(out clockFrequency) == false) {
                // Frequency not supported
                throw new Win32Exception("QueryPerformanceFrequency() function is not supported");
            }

            thread = new Thread(new ThreadStart(ThreadProc));
            thread.Name = "HighAccuracyTimer";
            thread.Priority = ThreadPriority.Highest;
        }

        /// 
        /// 进程主程序
        /// 
        /// 
        private void ThreadProc()
        {
            long currTime;
            GetTick(out currTime);
            nextTriggerTime = currTime + intevalTicks;
            while (running) {
                while (currTime < nextTriggerTime) {
                    GetTick(out currTime);
                }   // wailt an interval
                nextTriggerTime = currTime + intevalTicks;
                //Console.WriteLine(DateTime.Now.ToString("ss.ffff"));
                if (tick != null) {
                    tick();
                }
            }
        }

        public bool GetTick(out long currentTickCount)
        {
            if (QueryPerformanceCounter(out currentTickCount) == false)
                throw new Win32Exception("QueryPerformanceCounter() failed!");
            else
                return true;
        }

        public void Start()
        {
            thread.Start();
        }
        public void Stop()
        {
            running = false;
        }

        ~UltraHighAccurateTimer()
        {
            running = false;
            thread.Abort();
        }
    }

  本来上面的类是用线程池创建的定时器线程,使用的ThreadPool类,但是精度不高,只能到5ms,或4ms,不是很满意,改用Thread类,将其优秀级提到最高,精度可以达到1ms. 可能是操作系统调整优先级顺序问题,采样200次,发现到四十几次以后精度才能到1ms,一开始采样间隔有2~3ms,尝试一开始先运行一段空程序,使线程进入正常以后才开始定时器的工作,可基本没有什么改善,也有可能等待的时间不够。

上面的程序在VISTA上测试过,可以达到1ms的精度,但需要一个过渡期。可能是由于任务太多造成的,因为在测试程序中把时间显示到终端屏幕了,而且里面有一些字符串格式化操作,会比较占用资源,而且使用了delegate作为事件代理,可能性能有所损失,以后再研究一下delegate对性能损失的程度。

如果系统对实时性要求比较高,可以不对定时器进行封装,将定时器的主进程当作控制器采样的主进程,减少函数调用造成的CPU资源浪费,精度会更高。