UART接收总线Telegram时DateTime.Now时间戳倒退的原因与解决方法
UART Telegram时间戳回退问题的原因及解决方案
问题描述
通过UART从总线接收Telegram并记录,每条Telegram使用DateTime.Now添加时间戳,但偶尔出现下一条Telegram的时间戳比前一条早100ms的异常情况。
相关代码
ComPort接收与处理逻辑
private SerialPort port; public void RegisterReceive() { ... port.DiscardOutBuffer(); port.DiscardInBuffer(); port.DataReceived += OnReceive; .... } public static Mutex mutexReceivingCom = new Mutex(); void OnReceive(object sender, SerialDataReceivedEventArgs args) { SerialPort port = sender as SerialPort; mutexReceivingCom.WaitOne(); // 等待进入安全区域 if (data.AppIsRunning) { Byte by; try { while (port.BytesToRead > 0) { by = (Byte)port.ReadByte(); mainWindow.Dispatcher.Invoke(() => { General_HandleTelegram.Add_HandleTeleData_ByteToUartInBuffer(by); }); } } catch (Exception ex) { System.Windows.MessageBox.Show("SerialPort OnReceive Exception: " + ex.ToString()); // 其他错误处理逻辑 } } mutexReceivingCom.ReleaseMutex(); // 释放Mutex ThreadPool.QueueUserWorkItem(handleReceivedBytes); } private void handleReceivedBytes(object state) { mutexReceivingCom.WaitOne(); // 等待进入安全区域 if (data.AppIsRunning) { try { BusAnswerOrCyclic_Handling tempPack = null; if (port.IsOpen) { tempPack = ReceiveTelegram(LastRecTelegramm, this); while (tempPack != null) { mainWindow.Dispatcher.Invoke(() => { if (tempPack != null) tempPack.ProcessBusTelegram(); }); if (tempPack != null) { LastRecTelegramm = tempPack; } if (port.IsOpen) { tempPack = ReceiveTelegram(LastRecTelegramm, this); } else break; } } CheckIfBufferCanBeReset(); } catch (Exception ex) { System.Windows.MessageBox.Show("Error ComPort.cs handleReceivedBytes: " + ex.Message); } } mutexReceivingCom.ReleaseMutex(); // 释放Mutex }
Telegram解析与时间戳记录
public BusAnswerOrCyclic_Handling ReceiveTelegram(BusAnswerOrCyclic_Handling parLastSendBusTelegram, ComPort comPort) { ... pack = new BusAnswerOrCyclic_Handling(recState, CmdToAssignToReceivedTele, LastSendBusTelegram, fetchedByteList.ToArray(), Generic_HandleTelegrams.mainWindow, comPort, -1, ucTelTyp == enTelTyp.SynLine, true); .... return pack; } public BusAnswerOrCyclic_Handling(enRecStates recState, BusTelegram_Command command, BusAnswerOrCyclic_Handling LastBusTelegram, byte[] bytes, MainWindow mainWindow, ComPort comPort, short iMasterByteCountWithCRC, bool isSynLine, bool isFullyReceived) { DateTime dt = DateTime.Now; ... } public void ProcessBusTelegram() { // 将Telegram添加到数据日志 }
问题原因分析
DateTime.Now的局限性- Windows系统中
DateTime.Now的精度仅为10-15ms,无法满足高精度时间记录需求。 - 若系统运行时触发时钟同步(如NTP服务)或硬件时钟调整,会导致当前时间被回调,直接引发时间戳回退。
- Windows系统中
线程调度的不确定性
OnReceive是串口回调线程,每次处理完字节后通过ThreadPool.QueueUserWorkItem将解析任务排入线程池。线程池的任务调度顺序不保证与排队顺序一致,可能出现后排队的解析任务先执行的情况。- 若后一条Telegram的解析任务先执行,其构造函数中获取的
DateTime.Now可能早于先排队但晚执行的前一条Telegram的时间戳。
时间戳记录时机不合理
- 当前时间戳在Telegram对象的构造函数中获取,而构造时机是在解析阶段,而非实际接收完成的时刻。解析阶段的延迟会导致时间戳不能准确反映Telegram的实际接收时间。
解决方案
1. 改用高精度且不受系统时钟影响的计时方式
使用Stopwatch替代DateTime.Now,结合程序启动时的基准时间生成绝对时间:
// 全局变量 private static DateTime _startTime = DateTime.Now; private static Stopwatch _stopwatch = Stopwatch.StartNew(); // 获取精确时间戳的方法 private DateTime GetPreciseTimestamp() { return _startTime.Add(_stopwatch.Elapsed); }
在Telegram构造时调用此方法获取时间戳,既保证微秒级精度,又不受系统时钟调整的影响。
2. 确保任务处理顺序与接收顺序一致
放弃线程池的无序调度,改用单线程队列处理解析任务,保证任务按接收顺序执行:
// 全局任务队列 private ConcurrentQueue<Action> _parseQueue = new ConcurrentQueue<Action>(); private bool _isProcessing = false; // 在OnReceive中替换QueueUserWorkItem lock (_parseQueue) { _parseQueue.Enqueue(() => handleReceivedBytes(null)); if (!_isProcessing) { _isProcessing = true; Task.Run(ProcessQueue); } } // 队列处理方法 private void ProcessQueue() { while (_parseQueue.TryDequeue(out Action action)) { action.Invoke(); } _isProcessing = false; }
3. 调整时间戳记录时机
将时间戳记录从构造函数前移至Telegram完整接收完成的时刻,例如在ReceiveTelegram成功提取完整字节序列时立即记录:
public BusAnswerOrCyclic_Handling ReceiveTelegram(BusAnswerOrCyclic_Handling parLastSendBusTelegram, ComPort comPort) { ... DateTime timestamp = GetPreciseTimestamp(); // 使用高精度方法 pack = new BusAnswerOrCyclic_Handling(recState, CmdToAssignToReceivedTele, LastSendBusTelegram, fetchedByteList.ToArray(), Generic_HandleTelegrams.mainWindow, comPort, -1, ucTelTyp == enTelTyp.SynLine, true, timestamp); .... return pack; } // 修改构造函数接收时间戳参数 public BusAnswerOrCyclic_Handling(enRecStates recState, BusTelegram_Command command, BusAnswerOrCyclic_Handling LastBusTelegram, byte[] bytes, MainWindow mainWindow, ComPort comPort, short iMasterByteCountWithCRC, bool isSynLine, bool isFullyReceived, DateTime timestamp) { DateTime dt = timestamp; ... }
内容的提问来源于stack exchange,提问作者user24852915
相关产品推荐
相关产品推荐

