WPF实现串行RS232通信

本文将向展示如何在WPF应用程序中实现串行RS232通信。重点将放在代码实现上,界面设计相对简单。在串行通信到微控制器(如PIC)的过程中,有几个关键点,例如Dispatcher.Invoke方法和ASCII到HEX的转换,这些都是常见的难点,本文将对这些内容进行详细说明。

假设读者已经具备C#和WPF的基础知识。作者作为一名电气工程师,在过去7年中一直在使用C#进行编程。大约5个月前,作者开始转向WPF,这是一个有趣的转变。作者大部分时间都在致力于让计算机与外部世界进行通信。在进行DMX项目时,作者决定使用WPF的XAML界面,而不是传统的、功能有限的C#窗体。

本文的核心目标是让WPF项目能够与PIC微控制器进行通信。在C#中,这是通过使用System.IO.Ports库实现的;在WPF中,实现方式并无不同。但是,从C#背景的用户在转向WPF时会遇到一些独特的问题,比如Invoke方法的缺失。实际上,Invoke方法隐藏在Dispatcher类中。

使用代码

代码结构非常简单,没有过多关注命名规范等细节。这些将在后续版本中改进。首先,从界面开始。界面非常基础,包括一个用于选择COM端口的ComboBox、一个用于选择波特率的ComboBox、一个用于发送数据的TextBox,以及一个用于显示从COM端口接收到的数据的RichTextBox,还有一些用于执行操作的按钮。界面的代码和布局不是本文的重点,可以从源代码中下载。

高级样式

第一个重要的因素是COM端口名称和波特率的数据来源。虽然有从计算机获取这些数据的方法,但作者希望有更多的控制权,因此将所有标题放在了一个名为CommsData.xml的XML文件中。这些数据由“数据提供者”提供,每个数据提供者都放置在Windows.Resource元素中。XPath用于确定作者正在查看的元素,x:Key是作者在代码中引用的键。

<Window.Resources> <XmlDataProvider x:Key="ComPorts" Source="CommsData.xml" XPath="/Comms/Ports" /> <XmlDataProvider x:Key="ComSpeed" Source="CommsData.xml" XPath="/Comms/Baud" /> </Window.Resources>

为了进一步理解,需要检查CommsData.xml的内容。

<Comms> <Ports> COM1 </Ports> <Baud> 921600 </Baud> </Comms>

Comms是根元素,在数据文件中只能有一个。Ports和Baud子元素将文本分开,这样数据提供者就只能看到作者想要它们看到的内容。每个子元素可以有自己的子元素;只需要正确设置数据提供者以显示想要的数据。如果想要格式化数据的显示方式,请搜索数据模板的使用。

数据提供者通过绑定ComboBox的ItemsSource属性来使用。下面ComPorts是作者为数据提供者选择的x:Key:

ItemsSource="{Binding Source={StaticResource ComPorts}}"

设置串行端口

在C#环境中有两种方法:一种是将元素拖到窗体上并编辑其属性,另一种是硬编码。不幸的是,在WPF中,必须硬编码COM端口,但这非常容易做到。

首先,必须引用IO端口库:

using System.IO.Ports;

然后,可以创建一个串行端口。为了方便,作者将其命名为serial,如果使用更多的端口,应该更改命名约定。

SerialPort serial = new SerialPort();

可以编辑串行端口的多个属性。这发生在源代码中,当按下连接按钮时,但只要在打开端口之前完成即可。

serial.PortName = Comm_Port_Names.Text; // COM端口名称 serial.BaudRate = Convert.ToInt32(Baud_Rates.Text); // COM端口波特率 serial.Handshake = System.IO.Ports.Handshake.None; serial.Parity = Parity.None; serial.DataBits = 8; serial.StopBits = StopBits.One; serial.ReadTimeout = 200; serial.WriteTimeout = 50;

有关这些属性的信息,请参见RS-232通信协议。这些设置应该能满足大多数应用程序的需求,但有两个重要属性。ReadTimeout是允许读取串行端口上接收到的数据的时间。如果设置得太低,可能会出现错误,消息可能会被截断。WriteTimeout是允许从串行端口写入数据的时间——如果尝试写入长字符串数据,这可能会导致错误。

作者没有找到这些值的完美公式,需要采用试错方法,同时要记住,允许的时间越长,程序的延迟就越大。

接收数据

现在有了串行端口,重要的是为每次串行端口有数据要读取时设置一个函数调用。这比创建一个线程、轮询数据并等待超时异常要高效得多。要做到这一点,只需引入以下代码:

serial.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler(Recieve);

这将在每次接收到数据时调用Recieve函数。在这个函数中,将数据读取到一个名为recieved_data的String中,然后Invoke一个函数将这些数据写入窗体。为了启用Invoke,必须包括:

using System.Windows.Threading; private void Recieve(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { // 收集接收到的字符到'buffer'(字符串)。 recieved_data = serial.ReadExisting(); Dispatcher.Invoke(DispatcherPriority.Send, new UpdateUiTextDelegate(WriteData), recieved_data); }

现在在WPF中,必须使用Dispatcher.Invoke方法。这个方法的目的非常简单,主窗体由它自己的线程控制。当串行端口宣布它已经接收到数据时,会创建一个新线程来读取这些数据。Invoke方法允许将数据字符串从串行数据接收线程传递到窗体线程,当它可能防止任何错误时。

数据可以写入任何能够显示文本的控件。在这个例子中,通过RichTextBox显示它。在WPF中,只能将Flow Document写入RichTextBox,所以必须创建一个段落来写入数据,然后添加这个段落到Flow Document中,然后才能显示它。比C#更复杂,但并非不可能使用。

FlowDocument mcFlowDoc = new FlowDocument(); Paragraph para = new Paragraph(); private void WriteData(string text) { // 将数据值分配给RichTextBox。 para.Inlines.Add(text); mcFlowDoc.Blocks.Add(para); Commdata.Document = mcFlowDoc; }

发送数据

这是在计算机之间通信时可能会发生变化的地方,协议会改变,可以使用标准的serial.Write(string)方法发送数据,就像在C#中一样。然而,如果正在与PIC微控制器通信,必须将文本转换为HEX字节。

SerialData TextBox中包含的文本被发送到SerialCmdSend函数。这个函数在编码数据字符串时需要几个步骤。首先,要检查串行端口是否打开serial.IsOpen。必须这样做,否则会标记错误。在try{} catch{}方法中是编码程序。

using System.Threading; if (serial.IsOpen) { try { // 将二进制数据发送出去 byte[] hexstring = Encoding.ASCII.GetBytes(data); foreach (byte hexval in hexstring) { byte[] _hexval = new byte[] { hexval }; // 需要将byte转换为byte[]才能写入 serial.Write(_hexval, 0, 1); Thread.Sleep(1); } } catch (Exception ex) { para.Inlines.Add("Failed to SEND" + data + "\n" + ex + "\n"); mcFlowDoc.Blocks.Add(para); Commdata.Document = mcFlowDoc; } }

现在,重要的是要理解这里发生了什么以及为什么。第一步是将字符串的十六进制值转换为字节数组。例如,"123"将变为:[1]-49 [2]-50 [3]-51。现在在计算机到计算机的通信中,这可以直接使用:

serial.Write(hexstring, 0, hexstring.Length);

然而,在PIC通信中,当使用更高的波特率时,这种方法可能会出现问题,这是由于计算机的定时问题,计算机会尝试连续发送数据,而PIC会被过载。PIC上的程序将会崩溃并需要重置,在现实世界的应用中不能有这种情况。

秘诀是每次只发送一个字节,确保在字节之间有延迟。延迟只需要1毫秒,但如果不使用,PIC将会崩溃。这是在下面再次显示的循环中完成的。在这个循环中,将每个字节转换为字节数组(byte[])的额外步骤是令人烦恼的,因为使用的serial.Write方法只会发送一个字节数组。

foreach (byte hexval in hexstring) { byte[] _hexval = new byte[] { hexval }; // 需要将byte转换为byte[]才能写入 serial.Write(_hexval, 0, 1); Thread.Sleep(1); }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485