本文将向展示如何在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);
}