使用WPF、RS232和PIC进行串行通信
使用 WPF 从 PC 到 PIC 的 RS232 通信。
引言
本文将介绍如何使用WPF实现RS232串行通信。其整体布局和设计很简单,因为代码是我希望关注的重点。特别是,我解决了与PIC进行串行通信的关键障碍,例如Dispatcher.Invoke
方法和ASCII到HEX的转换。
假设知识
本文期望开发者对C#和WPF有基本的了解。
背景
我花了7年时间愉快地用C#编程。大约5个月前,我决定转向WPF,这真是一段有趣的旅程。我是一名电气工程师,大部分时间都在让我的电脑与外部世界进行通信。我当时正在做一个DMX项目,并决定在WPF中使用XAML界面,而不是那些枯燥且功能有限的C# Forms。
所以,这一切的想法是让一个WPF项目与PIC微控制器进行通信。在C#中,这通过利用System.IO.Ports
库来实现,而在WPF中也没有什么不同。然而,任何C#背景的用户都会遇到一些独特的问题。这就是缺乏Invoke
方法,这稍微有点不准确,因为它隐藏在里面,但已被Dispatcher
类继承。
Using the Code
代码的布局非常简单,几乎没有注意命名约定等。这将在以后解决,但我希望先将基础知识发布出来,以便人们可以开始使用代码。
首先,我们将从界面开始。这非常基础,只是一个用于COM端口和波特率的ComboBox
,一个用于发送数据的Textbox
,以及一个用于显示从COM端口接收数据的Richtextbox
,还有几个用于操作的按钮。我没有展示这部分的代码,因为它可以在源代码中下载,而且布局并不重要。
基本样式

高级样式

XAML数据提供程序
第一个重要因素是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 Port Name
serial.BaudRate = Convert.ToInt32(Baud_Rates.Text); //COM Port Sp
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
,我们必须包含:
using System.Windows.Threading;
private void Recieve(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
// Collecting the characters received to our 'buffer' (string).
recieved_data = serial.ReadExisting();
Dispatcher.Invoke(DispatcherPriority.Send,
new UpdateUiTextDelegate(WriteData), recieved_data);
}
现在在WPF中,我们必须使用Dispatcher.Invoke
方法。它的目的很简单,主窗体由其自己的线程控制。当串行端口宣布已收到数据时,会创建一个新线程来读取这些数据。Invoke
方法允许在可能防止任何错误的情况下,将数据字符串从串行数据接收线程传递到窗体线程。
数据可以写入任何能够显示文本的控件。在这个例子中,我们通过RichTextbox
显示它。在WPF中,你只能将Flow Document写入RichTextbox
,所以你必须创建一个Paragraph来写入数据,然后将这个Paragraph添加到Flow Document中,然后再显示它。比C#复杂,但并非无法处理。
FlowDocument mcFlowDoc = new FlowDocument();
Paragraph para = new Paragraph();
private void WriteData(string text)
{
// Assign the value of the plot to the 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
{
// Send the binary data out the port
byte[] hexstring = Encoding.ASCII.GetBytes(data);
foreach (byte hexval in hexstring)
{
byte[] _hexval = new byte[] { hexval }; // need to convert byte
// to byte[] to write
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个字节,确保字节之间有延迟。延迟只需要1毫秒,但如果不使用它,PIC就会崩溃。这是在下面再次显示的循环中完成的。在这个循环中,将每个字节转换为字节数组(byte[]
)的额外步骤是恼人地必需的,因为所使用的serial.Write
方法只发送字节数组。
foreach (byte hexval in hexstring)
{
byte[] _hexval = new byte[] { hexval }; // need to convert byte to byte[] to write
serial.Write(_hexval, 0, 1);
Thread.Sleep(1);
}
最后,你就拥有了一个工作的WPF串行通信平台,用于RS-232,请稍后查看完整的可用版本以及使用Microsoft Expression Blend重新设计的版本。
历史
- 版本1.0 基本样式和源代码
- 版本1.1 高级样式和源代码