更新屏幕和数据采集
使用 .Net SerialPort 类进行数据采集,并同时更新屏幕
引言
数据采集并将其显示在屏幕上是许多应用程序都需要的功能。有多种方式可以采集数据,例如网络套接字、并行端口、串行端口和 USB。本文将介绍如何使用串行端口和 USB 端口在采集数据时同时在用户屏幕上显示数据。首先将讨论多线程架构,然后介绍如何使用串行端口和 USB 端口,最后介绍如何使用 Windows 窗体从单独的线程在屏幕上显示数据。
多线程设计(前台和后台线程)
有多种方法可以设计此类应用程序。其中一种方法是使用多个线程。最简单的方法是将一个线程专门用于数据采集,另一个线程用于数据显示。这样,用户屏幕就不会挂起或无响应,因为有一个专门的线程仅用于显示用户屏幕,而不做其他事情。第二个线程负责所有使数据可用的工作,例如从数据库或端口读取数据。
I/O 任务,例如从端口或外部资源读取数据,可能非常耗时。如果在一个线程中同时执行 I/O 任务和屏幕更新任务,则屏幕将不会频繁更新,也不会接受用户输入。因此,当用户屏幕得不到更新或线程得不到响应时,它将无响应用户输入并变得无响应。因此,将所有长时间运行的任务或 I/O 任务放在单独的线程上,将用户屏幕放在单独的线程上是一个好主意。在本文中,我将重点关注此设计,并提供一些代码示例来展示此设计方法。
.Net 串行端口类
.Net SerialPort 类是 .Net 框架提供的一个组件,用于处理 PC 上的串行端口。可以定义 UART 通信的所有参数,例如波特率、握手模式和 COM 端口地址。此类既可以与硬件串行端口一起使用,也可以与虚拟串行端口一起使用。虚拟串行端口只是一个适配器的名称,该适配器将一个设备的接口转换为另一个设备。 .Net SerialPort 可以与硬件和串行端口无缝协作。
此类支持三个事件:数据已接收、错误已接收和引脚已更改。每当端口上有可用数据时,就会触发“数据已接收”事件。在“数据已接收”事件的处理程序中,可以从端口读取数据。
这里需要注意的一个重要事项是,此事件处理程序是在一个独立于创建 Serial Port 对象的线程的线程上引发的,而该对象在大多数情况下是 Windows 窗体线程。这很好,正如我之前所讨论的,使用不同于用户屏幕线程的线程来采集数据。因此,SerialPort 始终在单独的线程中接收数据。
以下代码显示了在有可用数据时执行的事件处理程序
private void aSerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// receive your data here..
}
可以以多种格式接收数据,例如原始格式,其中接收数据字节。串行端口还提供数据流对象,该对象可以传递给使用数据流对象的类进行进一步处理。
让我们看看读取原始字节的代码
private void aSerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
byte[] data = new byte[Rs232.BytesToRead];
aSerialPort.Read(data, 0, data.Length);
}
在继续之前,有一些 SerialPort 类的属性需要设置才能正确接收数据。这些属性是 ReadBufferSize
和 ReceivedBytesThreshold
。 ReadBufferSize
是包含最大数据量的内部缓冲区的大小。您需要将其设置得足够大,以免丢失任何数据。第二个重要属性是 ReceivedBytesThreshold
。此属性告诉 .Net 框架何时触发“数据已接收”事件。如果将其设置为 400,则一旦缓冲区中有 400 个字节可用,数据接收事件将触发。这不会在 400 字节时触发事件,而是在大约 400 字节时触发。
除了原始数据,还可以接收文本数据。SerialPort 支持 ASCII、Unicode 和 UTF 编码。让我们看看代码
private void aSerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
String message = aSerialPort.ReadLine();
}
在此示例中,串行端口将读取文本并阻塞代码执行,直到找到行尾字符为止。这可能是“\r\n”或“\n”,具体取决于底层系统。
由于如果数据不可用或由于任何其他问题数据停止,代码执行将阻塞。如果未处理这种情况,程序将无限期挂起。要解决此阻塞问题,可以使用 ReadTimeOut
属性,该属性以毫秒为单位指定时间。如果在指定时间内串行端口未收到数据,它将引发异常,该异常可用于摆脱困境并告知用户问题。
FTDI USB 模块
在这里,另一篇文章介绍了如何使用 FTDI USB 模块设计硬件数字数据采集卡。在本文中,我将讨论 FTDI 模块的软件编程。在本文的进一步阅读部分,您可以找到与 FTDI 模块相关的链接。
FTDI USB 模块支持 2 种操作模式。一种选择是使用 VCP 驱动程序和 .Net SerialPort 类。它取决于虚拟串行端口驱动程序。在这种模式下,当将 USB 设备连接到 PC 时,设备将作为串行端口与 PC 通信。任何主机应用程序都可以访问此虚拟串行端口与设备通信。虽然这种方法更容易使用,但与 D2XX 驱动程序相比,数据速率较低。D2XX 驱动程序由 FTDI 提供,并支持更高的数据速率。
访问 USB 模块的第二种方法是使用 D2XX 驱动程序。在这种模式下,您可以借助 FTDI 提供的库直接访问 USB 模块。以下代码显示了如何操作
using FTD2XX_NET;
public void Foo()
{
FTDI ftdiDevice = new FTDI();
FTDI.FT_STATUS ftStatus = FTDI.FT_STATUS.FT_OK;
FTDI.FT_DEVICE_INFO_NODE[] ftdiDeviceList;
// As one can connect one to many FTDI device to a PC
// here I am using the first device that is connected to My PC
uint ftdiDeviceCount = 0;
ftStatus = ftdiDevice.GetNumberOfDevices(ref ftdiDeviceCount);
if (ftdiDeviceCount == 0 && ftStatus == FTDI.FT_STATUS.FT_OK)
{
return;
}
ftdiDeviceList = new FTDI.FT_DEVICE_INFO_NODE[ftdiDeviceCount];
// Populate our device list
ftStatus = ftdiDevice.GetDeviceList(ftdiDeviceList);
ftStatus = ftdiDevice.OpenBySerialNumber(ftdiDeviceList[0].SerialNumber);
}
可以使用多种参数打开设备,例如设备索引、序列号、位置和描述。
这是读取数据的代码
byte[] Read()
{
uint numBytesAvailable = 0;
ftStatus = ftdiDevice.GetRxBytesAvailable(ref numBytesAvailable);
if (numBytesAvailable < 0) return null;
byte[] tempDataBuffer = null;
tempDataBuffer = new byte[numBytesAvailable];
uint numBytesRead = 0;
ftStatus = ftdiDevice.Read(tempDataBuffer, numBytesAvailable, ref numBytesRead);
// here a check is performed to ensure the status of the device
// if device status is not o.K then refresh device.
if (ftStatus != FTDI.FT_STATUS.FT_OK) this.Refresh();
return tempDataBuffer;
}
.Net Serial Port 类与 D2XX 方法之间的一个区别是它不会自行调用设备。另一方面,Serial Port 每次缓冲区中有可用数据时都会引发数据接收事件。解决此问题的一种方法是使用 .net 的计时器类并重复调用 read 方法。让我们看看代码
private void BeginProducingData()
{
AutoResetEvent autoEvent = new AutoResetEvent(false);
TimerCallback continuousTimerCallback = new TimerCallback(acquireData);
device.Refresh();
// usbReadTimeDelay tells the method at what frequency to call the method
threadTimer = new System.Threading.Timer(continuousTimerCallback, autoEvent,0, usbReadTimeDelay);
// Mae sure to call this function from a separate thread because It will be blocked here
autoEvent.WaitOne(-1);
}
完整的数据采集代码可以在这篇文章中找到。需要注意的一点是调用 FTDI 设备数据的频率。如果调用太晚且数据速率足够快,则有丢失数据的风险。同样,如果调用设备过于频繁,则每次调用将没有足够的数据可用,并且会对设备进行无用的调用。因此,在设计优化应用程序时,必须牢记这一现象。
我使用的设备支持高达 8mbps 的数据,我测试过大约 6 mbps。FTDI 设备有一个 64Kbytes 的数据队列。您可以读取的最小数据量约为 4Kbytes。如果您调用设备读取数据,而当前可用数据量小于 4Kbytes,则无法读取数据,调用设备将返回 0 字节。同样,如果您等待较长时间读取数据缓冲区,则缓冲区可能会溢出,因为缓冲区的最大大小为 64Kbytes。
更新用户屏幕
现在,在讨论了串行端口类的数据采集之后,让我们看看如何在后台线程中采集数据时更新屏幕。
正如我们在文章开头讨论的多线程数据采集设计。有一个独立的数据采集线程。在正常情况下,两个线程可以访问彼此的资源,但在使用 win-forms 时情况有所不同。创建 winforms 组件的线程不能从另一个线程访问。因此,如果您尝试从串行端口接收数据事件访问任何 winform 组件,将会发生错误。这是因为数据接收事件处理程序始终在单独的线程中引发。
此限制是因为 .net 使用 Windows 组件,如文本框和窗体。这些组件受到 Windows 操作系统的限制,即每个组件只能由创建它的线程访问。这称为单线程单元模型(STA)。
当在 Visual Studio 中创建窗体时,它始终通过将线程属性设置为 STA 来创建。以下代码显示了这一点
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
为了解决这个问题,winforms 组件提供了一些方法,允许我们传递一个委托,该委托可以由创建 winforms 组件的线程调用。这些方法是 Control.Begininvoke、Control.Invoke 和 Control.EndInvoke,以及一个线程安全的属性 control.IsInvokeRequired。
我在这里推荐使用 BeginInvoke() 方法,因为它是一个异步方法,不会阻塞调用。如果您非常频繁地更新屏幕,例如以 50 Hz 的频率,那么如果您使用同步(Control.Invoke())方法,您的屏幕将会冻结。有关异步方法调用的文章,请参阅进一步阅读部分。
Invoke() 和 BeginInvoke() 需要一个委托和要传递的参数。让我们看看代码
// first define the delegate
public delegate void MessageUpdateCustom(MessageCustom custMsg);
// following code is from the data acquisition thread
// we want to update our screen which is based on windows forms
//MessageCustom is custom object which contains values for display on the screen
internal void UpdateCustomMessage(MessageCustom custMessage)
{
MessageUpdateCustom msg = new MessageUpdateCustom(UpdateAllTxt);
this.BeginInvoke(msg, new object[] { custMessage });
}
// Eventually following method will be execute on the thread where
//winforms components resides
internal void UpdateAllTxt(MessageCustom msg)
{
}
现在从 .net 串行端口类的“数据已接收”事件中调用 UpdateCustomMessage() 方法。或任何属于独立线程的其他方法。这将创建委托并将其传递给 control.BeginInvoke 方法,该方法最终将由创建 Windows 窗体组件的线程调用。这样,在更新屏幕时就不会发生错误,并且用户屏幕将更新而不变得无响应。
结论
在本文中,简要介绍了如何使用 .Net 的串行端口采集数据并同时更新屏幕。还讨论了如何编写访问 FTDI 设备的 Y代码。有关 FTDI 的信息,您可以查看进一步阅读部分中的链接。