You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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添加到数据日志
}

问题原因分析

  1. DateTime.Now的局限性

    • Windows系统中DateTime.Now的精度仅为10-15ms,无法满足高精度时间记录需求。
    • 若系统运行时触发时钟同步(如NTP服务)或硬件时钟调整,会导致当前时间被回调,直接引发时间戳回退。
  2. 线程调度的不确定性

    • OnReceive是串口回调线程,每次处理完字节后通过ThreadPool.QueueUserWorkItem将解析任务排入线程池。线程池的任务调度顺序不保证与排队顺序一致,可能出现后排队的解析任务先执行的情况。
    • 若后一条Telegram的解析任务先执行,其构造函数中获取的DateTime.Now可能早于先排队但晚执行的前一条Telegram的时间戳。
  3. 时间戳记录时机不合理

    • 当前时间戳在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 04:27:04