【问题标题】:Serial data acquisition program reading from buffer从缓冲区读取串行数据采集程序
【发布时间】:2017-07-26 11:22:17
【问题描述】:

我在 Visual C++ 2008 中开发了一个应用程序,用于从 COM 端口定期(50 毫秒)读取数据。为了定期读取数据,我将读取函数放在OnTimer 函数中,并且因为我不希望 GUI 的其余部分挂起,所以我从线程中调用了这个计时器函数。我已经把代码放在下面了。

应用程序运行良好,但显示以下意外行为:在数据源(硬件设备甚至数据模拟器)停止发送数据后,我的应用程序继续接收数据一段时间,该时间与读取功能已经运行了多长时间(EDIT:此超出时间段与发送数据的时间段相同)。因此,如果我立即启动和停止数据流,这将反映在我的 GUI 上,但如果我启动数据流并在 10 秒后停止它,我的 GUI 将继续显示数据 10 秒以上(已编辑)。

在用尽了所有的调试尝试后,我做了以下观察:

  1. 如上所述,这个超长的运行时间与硬件发送数据的时间成正比。
  2. 传入数据的频率为 50 毫秒,因此要接收 10 秒的数据,我的 GUI 必须再接收大约 200 个数据包。
  3. 我声明的唯一缓冲区是abBuffer,它只是一个固定大小的字节数组。我认为这不会增加大小,因此这些数据存储在某个地方。
  4. 如果我更改数据包中的某些内容,可以理解,此更改会在延迟后显示在 GUI 上(由于上述几点)。但这意味着在 COM 端口接收到的数据存储在某个可变大小的缓冲区中,我的读取函数正在从中读取数据。
  5. 我已经对读取和处理周期进行了计时。后者是瞬时的,而前者很少(1000 次读取中的 3 次(遵循不可识别的模式))需要 16 毫秒。这完全在 GUI 每次读取的 50 毫秒窗口内。

以下是我的线程和定时器代码:

UINT CMyCOMDlg::StartThread(LPVOID param)
{
    THREADSTRUCT *ts = (THREADSTRUCT*)param;

    ts->_this->SetTimer(1,50,0);

    return 0;

}

//Timer function that is called at regular intervals
void CMyCOMDlg::OnTimer(UINT_PTR nIDEvent)
{       

    if(m_bCount==true)
    {
        DWORD NoBytesRead;
        BYTE  abBuffer[45];

        if(ReadFile((m_hComm),&abBuffer,45,&NoBytesRead,0))
        {
            if(NoBytesRead==45)
            {
                if(abBuffer[0]==0x10&&abBuffer[1]==0x10||abBuffer[0]==0x80&&abBuffer[1]==0x80)
                {
                    fnSetData(abBuffer);
                }
                else
                {
                    CString value;                      
                    value.Append("Header match failed");
                    SetDlgItemText(IDC_RXRAW,value);    
                }
            }
            else
            {
                CString value;
                value.Append(LPCTSTR(abBuffer),NoBytesRead);
                value.Append("\r\nInvalid Packet Size");
                SetDlgItemText(IDC_RXRAW,value);
            }
        }
        else
        {
            DWORD dwError2 = GetLastError();
            CString error2;
            error2.Format(_T("%d"),dwError2);
            SetDlgItemText(IDC_RXRAW,error2);
        }

        fnClear();
    }
    else
    {
        KillTimer(1);
    }

    CDialog::OnTimer(nIDEvent);

}

m_bCount 只是我用来终止计时器的标志,ReadFile 函数是标准的 Windows API 调用。 ts 是一个包含指向主对话框类的指针的结构,即this

谁能想到会发生这种情况的原因?我尝试了很多东西,而且我的代码做的很少,我无法弄清楚这种意外行为发生在哪里。

编辑:

我正在添加下面使用的 COM 端口设置和超时:

            dcb.BaudRate = CBR_115200;
            dcb.ByteSize = 8;
            dcb.StopBits = ONESTOPBIT;
            dcb.Parity = NOPARITY;
            SetCommState(m_hComm, &dcb);

            _param->_this=this;

            COMMTIMEOUTS timeouts;
            timeouts.ReadIntervalTimeout=1;
            timeouts.ReadTotalTimeoutMultiplier = 0;
            timeouts.ReadTotalTimeoutConstant = 10;
            timeouts.WriteTotalTimeoutMultiplier = 1;
            timeouts.WriteTotalTimeoutConstant = 1;
            SetCommTimeouts(m_hComm, &timeouts);

【问题讨论】:

  • 来自 COM 端口的数据由操作系统存储在系统缓冲区中。您的应用程序不直接从硬件读取数据,而只是从中间系统缓冲区中获取数据。 “传入数据的频率是 50 毫秒” -- 频率是一个速率(例如每秒 X 次),但您只是提到了一个时间间隔。
  • @sawdust 我很抱歉没有澄清频率。每秒 20 次(连续数据包之间为 50 毫秒)。我假设ReadFile API 调用将从端口读取。即使不是这种情况,如果在发送数据时立即开始读取,为什么会导致延迟?会不会和定时器功能有关?
  • @sawdust 另外,关于系统缓冲区,如果硬件在我的应用程序开始读取之前发送数据,是否也会读取应用程序正式开始读取之前的所有数据?
  • "如果硬件在之前发送数据..." -- 操作系统通常会在设备打开时分配此系统缓冲区,因此之前接收到的不是"读”。更有可能是数据速率。当您可以简单地使用阻塞读取来等待读取完成时,使用计时器读取是多余的。这假设数据包实际上每 50 毫秒发送一次。但是,由于您看到了额外的数据运行,这意味着您施加的每秒 20 次的读取速率比发送数据的实际速率要慢。因此,程序正在慢慢地产生回流。
  • @sawdust 谢谢你的回复。我检查了系统调用读取函数之间的平均间隔。事实证明,这大约是 60 毫秒。所以 Windows 只是确保间隔大于我指定的时间。有什么办法可以改进这个实现。例如,如果读取功能在第一次读取后会结束,我将如何使用WaitCommEvent 定期读取?

标签: visual-c++ mfc serial-port


【解决方案1】:

您在 OnTimer() 函数中一次处理一条消息。由于定时器间隔为 1 秒,但数据源每 50 毫秒不断发送消息,因此您的应用程序无法及时处理所有消息。

您可以添加 while 循环如下:

while(true)
{
    if(::ReadFile(m_hComm, &abBuffer, sizeof(abBuffer), &NoBytesRead, 0))
    {
        if(NoBytesRead == sizeof(abBuffer))
        {
            ...
        }
        else
        {
            ...
            break;
        }
    }
    else
    {
        ...
        break;
    }
}

但是您的代码中还有另一个问题。如果您的软件在数据源仍在发送消息时检查消息,则 NoBytesRead 可能小于 45。您可能希望将数据存储到消息缓冲区中,例如 CStringstd::queue.

如果消息的末尾不包含 NULL,则将消息传递给 CString 对象是不安全的。

另外,如果第一个字节从 0x80 开始,CString 会将其视为多字节字符串。它可能会导致错误。如果消息不是文字文本字符串,请考虑使用其他数据格式,例如 std::vector

顺便说一句,您不需要在单独的线程中调用 SetTimer()。踢计时器不需要时间。另外,我建议您在 OnTimer() 函数之外的某个地方调用 KillTimer(),这样代码会更直观。

如果数据源不断地发送数据,你可能需要在打开/关闭COMM端口时使用PurgeComm()

【讨论】:

  • 感谢您的回复!我为这个错误道歉,但定时器周期设置为 1000 毫秒只是为了我的调试目的。在正常运行期间,它是 50 毫秒,我在上面的代码中进行了更改。我意识到将数据存储在CString 中的风险,这就是我使用字节数组abBuffer 的原因,因为我明确地将一个参数传递给ReadFile 函数,要求它读取到45 个字节,我没有认为检查将发生在数据包之间。我从线程调用计时器函数的原因是因为我需要 GUI 在读取期间仍能响应,并且 (...contd)
  • (..contd) 通过将所有读取和处理任务推送到工作线程来释放 GUI 的其余部分。这也是我从计时器中调用KillTimer 函数的原因,尽管我可以从我的线程函数中调用它。我觉得的问题是SetTimer 使用的WM_TIMER 消息不准确。这只是确保周期的下限等于设定的时间,实际上这个周期可能在 50ms 到 70ms 之间。我正在尝试实现基于事件的读取。如果您在这方面有任何建议,我将不胜感激!
  • SetTimer() 和 KillTimer() 立即返回。它根本不等待。
猜你喜欢
  • 2018-03-05
  • 1970-01-01
  • 1970-01-01
  • 2015-07-02
  • 1970-01-01
  • 2012-03-15
  • 1970-01-01
  • 2011-09-04
  • 1970-01-01
相关资源
最近更新 更多