C# 中的串行通信入门






4.93/5 (152投票s)
C# 中的串行通信。
引言
PC 之间的串行通信总是被视为起点。我编写的第一段串行通信代码是大学作业,目的是让两台 PC,然后三台,然后……您就明白了,使用 Borland 的 Turbo C++ for DOS 在(我现在知道是)RS422 上通过一对线缆进行通信。时间流逝,我毕业了(不知怎么的!),并在“真实”世界成为了一名设计工程师。看起来不够忙碌让我被分配了著名的“Glenn,你没那么忙,看看这个”。“这个”是接手了一个半途而废的项目,然后那个罪魁祸首跑路了。为了让它按预期工作,我不得不拿出软件(VB6),然后我所有的社交生活就消失了。
背景
RS-232 是 PC 通信中最著名的方法,RS232 的特性是逻辑 1(真)的电压范围可以从 -3v 到 -25v,逻辑 0(假)的电压范围是从 +3v 到 +25v。-3v 到 0 到 +3v 的范围被视为无效,以允许线路上的噪声和干扰。如果端口空闲,端口处于高电平(或 -12v),这就是为什么如果您查看外围设备的电路图,总是有很多反相器。RS-485 在文末详细介绍,RS-422 & RS-449 以及后续的允许非常长的线缆运行和高速度都遵循相同的基本路径。
VB6 使用了 *MSComm32.ocx*;这是 *MSComm16.ocx* 的 32 位版本,这两个组件都工作得相当好。我说相当好是因为控件的**中断处理程序** `CommEvents` 从未像我预期的那样快速或良好地工作。我发现唯一可靠的方法是通过循环或计时器(可能还会锁定系统,唉 `DoEvents`!)来轮询它。另一个值得注意的点是最多只能支持 16 个串行端口(我在使用 MSComm 的测试平台上使用了 9 个,并开始担心是否会有另一组设备!)。谢天谢地,Net Serial Class 将这个限制提升到了 255。随着 .NET1 的发布,这一切都改变了,串行通信被忽视了(原因尚不清楚,我推测与安全性有关)。这提供了两种解决方法。这提供了使用 DOS 创建并打印到(虚拟)设备或导入 OCX 的选项(当时正是 .NET-COM 战争的高峰期)。我确实写了一个程序,使用 MScomm 在 Borland C++ Builder 中与高清视频屏幕通信(我最终让它工作了,Cliff!哈!),这导致了一个很大的可执行文件,不适合大多数应用程序。.NET 2.0 发布后,我松了一口气,因为它附带了一个框架原生的串行端口类。我开始同时使用 C# 和 VB,然后完全放弃了 VB6。
串行端口现在已经采用(并在某些情况下放弃了)9 针 D 型连接器,尽管我遇到过旧设备(特别是三星的高精度比色计摄像头)使用旧的 25 针 D 型连接器。
Pin Number (9 way) Pin Number (25way) Function
1 8 Carrier Detect
2 3 Receive
3 2 Transmit
4 20 Data Terminal Ready
5 7 Signal Ground
6 6 Data Set Ready
7 4 Request To Send
8 5 Clear To Send
9 21 Ring Indicator
(1,9-19,21, 23-25 are unused for RS232)
一个常见的测试线缆或端口是否正常的方法是使用终端模拟器,将引脚 2 和 3 短接,看看是否会收到键盘按键对应的字符。
Using the Code
设置
我主要是硬件人(我身上有很多烧伤和疤痕,每次都给美国移民局带来麻烦!),我学会了 C 和汇编。我直到进入现实世界才开始接触 Windows 的那些花哨玩意,所以请原谅我可能犯的任何错误。一个重要的步骤是打开设备管理器。您可以在 Windows 10 的 Windows 管理工具中找到这个非常有用的工具,选择计算机管理,这会弹出下面的计算机管理窗口。
选择下面的设备管理器选项
这将打开下面的设备管理器
如这张图片所示,我正在编写此更新的 PC 只有一个虚拟 COM 端口。一如既往,红叉或黄三角表示硬件安装有问题。
在使用下面的软件方法之前,最好先安装一个通信端口。我曾考虑过用 `try catch` 更新软件,以防止它在没有通信端口的系统上运行时崩溃。这虽然可以防止软件虚拟爆炸成 小碎片,但可能会导致混淆(谢谢你,Dell!)。所以我的两分钱的建议是,这比一个一直存在但什么都不做的软件要好。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.IO.Ports;
using System.Windows.Forms;
我总是像对待 C 程序的 `#include` 部分一样对待 `using` 部分。
`System.IO.Ports` 是要使用的类,无需诉诸底层黑客技术。它涵盖了出现在机器上的所有串行端口。
SerialPort ComPort = new SerialPort;
这将创建一个名为 `ComPort` 的对象。这将创建一个串行端口对象,其默认参数为 9600bps,无校验,一个停止位,无流控制。
下面是窗体
我通过**文件**菜单创建了一个标准的 Windows 窗体应用程序。在此基础上,我添加了一个按钮(名称为**端口**)和一个富文本框。
我将按钮命名为 `btnGetSerialPorts`,将富文本框命名为 `rtbIncomingData`(稍后会明白这个名称的含义)。
我倾向于使用富文本框,因为它比普通文本框更灵活。它在排序和对齐文本方面的用途比普通文本框多得多。
我在按钮的点击例程中添加了以下代码
private void btnGetSerialPorts_Click(object sender, EventArgs e)
{
string[] ArrayComPortsNames = null;
int index = -1;
string ComPortName = null;
ArrayComPortsNames = SerialPort.GetPortNames();
do
{
index += 1;
rtbIncoming.Text += ArrayComPortsNames[index]+"\n";
}
while (!((ArrayComPortsNames[index] == ComPortName) ||
(index == ArrayComPortsNames.GetUpperBound(0))));
}
这会显示所有显示为 COM 端口的设备。一个常见的错误是认为插入 USB 的设备会显示为 COM 端口。例如,Com5 和 Com6 在我的手机里,下面是设备管理器屏幕,其中 COM 和 LPT 端口选项已展开。
设备将在这里显示它们的制造商(RIM、Intel 等)。
COM1 如果您的机器碰巧有的话。如今,在上网本、笔记本电脑和其他便携式机器中,9 针 D 型公头 COM1 越来越少见。曾经有一个 COM 1 甚至有时是 COM 2(我的第一台奔腾电脑,Windows 95 有两个!)。可以看到,端口不是按顺序排列的,这些可以排序。我倾向于使用组合框将数据加载到其中(我认为它看起来更专业,用户出错的可能性也更小),并按如下方式排序。
ArrayComPortsNames = SerialPort.GetPortNames();
do
{
index += 1;
cboPorts.Items.Add(ArrayComPortsNames[index]);
}
while (!((ArrayComPortsNames[index] == ComPortName)
|| (index == ArrayComPortsNames.GetUpperBound(0))));
Array.Sort(ArrayComPortsNames);
//want to get first out
if (index == ArrayComPortsNames.GetUpperBound(0))
{
ComPortName = ArrayComPortsNames[0];
}
cboPorts.Text = ArrayComPortsNames[0];
如果您注意到,对点击例程所做的更改是,一个标准的组合框现在接受数组而不是富文本框,还有 `Array.Sort(ArrayComPortsNames)`。
排序以获取第一个 COM 端口到组合框(我称之为**端口**),这些更改现在会得到以下结果。
这些都受**串行端口**类支持,并源自旧式的电传打字机。非标准波特率是不可取的,但是,如果您能找到 Elltech 公司提供的 *MHComm32.ocx*,它提供了一种创建“自定义”波特率的方法。
我曾在一个应用程序中使用它来获得 9550 的速率,当时第三方板卡在出现一些错误的情况下无法接受 9600。标准波特率是 9600(串行端口类的默认值),较低的波特率 600 和 300 用于连接嵌入式处理器和微控制器。有时为了限制板卡的复杂性、尺寸(以及当然还有成本!),设备必须充当 UART(通用异步收发器)。UART 芯片处理大部分串行通信,例如,PC 中使用的 16550 有八个数据引脚,以及各种状态和控制引脚,而普通的嵌入式系统没有足够的可用引脚来控制此芯片。因此,为了绕过这个事实,处理器被编程为模拟 UART。由于处理器可能忙于其他任务,而串行通信是低优先级任务,因此使用了较慢的波特率。下面是应用程序的波特率 `cboBaudRate` 的代码。
cboBaudRate.Items.Add(300);
cboBaudRate.Items.Add(600);
cboBaudRate.Items.Add(1200);
cboBaudRate.Items.Add(2400);
cboBaudRate.Items.Add(9600);
cboBaudRate.Items.Add(14400);
cboBaudRate.Items.Add(19200);
cboBaudRate.Items.Add(38400);
cboBaudRate.Items.Add(57600);
cboBaudRate.Items.Add(115200);
cboBaudRate.Items.ToString();
//get first item print in text
cboBaudRate.Text = cboBaudRate.Items[0].ToString();
点击按钮后,它会在组合框中显示所有波特率,其中 300 是最低的。
下一个框是数据位数,它们代表数据传输(或 Tx 线)的总转换次数。8 是标准(8 对于读取某些嵌入式应用程序很有用,因为它提供了两个半字节(4 位序列)。
7 位通常用于某些 RS485,其中额外的位用于全局消息。通过上述修改,窗体显示如下。
接下来几个命令在我看来需要更多的解释。它们是停止位、校验位和握手。
`Handshaking` 属性用于使用一套完整的连接时(例如我桌上的灰色 9 针 D 型连接器)。它最初用于确保双方都对齐并且数据被正确发送和接收。发送方和接收方之间需要一个共同的握手。下面是组合框的代码。
cboHandShaking.Items.Add("None");
cboHandShaking.Items.Add("XOnXOff");
cboHandShaking.Items.Add("RequestToSend");
cboHandShaking.Items.Add("RequestToSendXOnXOff");
我认为现在是时候详细说明每个属性的作用及其工作原理了。RS-232 旨在成为一种标准,但正如俗话所说,“规则是给愚者遵守,智者指导”。数据传输需要将 `Handshaking` 属性设置为以上设置之一。为了使其正常工作,CTS(准备发送,9 针的引脚 8)和 RTS(请求发送,9 针的引脚 7)是直接由软件控制的,DTR(数据终端就绪,9 针的引脚 4)同样由软件直接控制,而 DSR(数据设备就绪)类似于 CTS 引脚,但由连接的另一端激活。
握手可以设置为无(这是如今最常见的)。握手最初用于速度较低且重发数据成本较高时,也用于发送大量数据以确保正确接收。上面详细介绍了串行端口类提供的可用设置。下面的图显示了如何连接两个 9 针 D 型连接器以实现全双工通信。
这种切换可以通过以下方式实现
ComPort.RtsEnable = true;
ComPort.DtrEnable = true;
如果将 `true` 属性替换为 `false`,可以看到电压的切换。
这些值是在用于编写本文的 PC 上使用设置为伏特模式的万用表测得的,并以 9 针连接器的第 5 针作为地线。我发现在学习软件通信时,花了太多时间解释理论,而实际操作太少。下面的程序显示了我正在开发的程序的代码修改,四个标签,名为:`lblBreakStatus`、`lblCTSStatus`、`lblDTRStatus` 和 `lblRIStatus`。
`lblCTSStatus` 和 `lblDTRStatus` 显示 CTS 线(在上面的图表中连接到 RTS 线)和 DTR 线(在全双工通信中连接到 DTR 线)的状态。
创建了一个名为 `btnTest` 的按钮,其点击例程如下(此代码包含在列表和示例项目中,但被注释掉了,请取消注释以使用!)。
private void btnTest_Click(object sender, EventArgs e)
{
SerialPinChangedEventHandler1 = new SerialPinChangedEventHandler(PinChanged);
ComPort.PinChanged += SerialPinChangedEventHandler1;
ComPort.Open();
ComPort.RtsEnable = true;
ComPort.DtrEnable = true;
btnTest.Enabled = false;
}
我总是习惯性地阻止用户再次点击打开按钮,这会导致异常(通常,我更改文本并检查它是打开/关闭……)。RS232 连接有一根线从 DTR 或 RTS 引脚引出,并接触到引脚(将它们短接),这会导致标签背景像这样改变。
这提供了一个快速演示引脚如何工作的。下面是代码。
internal void PinChanged(object sender, SerialPinChangedEventArgs e)
{
SerialPinChange SerialPinChange1 = 0;
bool signalState = false;
SerialPinChange1 = e.EventType;
lblCTSStatus.BackColor = Color.Green;
lblDSRStatus.BackColor = Color.Green;
lblRIStatus.BackColor = Color.Green;
lblBreakStatus.BackColor = Color.Green;
switch (SerialPinChange1)
{
case SerialPinChange.Break:
lblBreakStatus.BackColor = Color.Red;
//MessageBox.Show("Break is Set");
break;
case SerialPinChange.CDChanged:
signalState = ComPort.CtsHolding;
//MessageBox.Show("CD = " + signalState.ToString());
break;
case SerialPinChange.CtsChanged:
signalState = ComPort.CDHolding;
lblCTSStatus.BackColor = Color.Red;
//MessageBox.Show("CTS = " + signalState.ToString());
break;
case SerialPinChange.DsrChanged:
signalState = ComPort.DsrHolding;
lblDSRStatus.BackColor = Color.Red;
// MessageBox.Show("DSR = " + signalState.ToString());
break;
case SerialPinChange.Ring:
lblRIStatus.BackColor = Color.Red;
//MessageBox.Show("Ring Detected");
break;
}
}
要实现此功能,应将以下行添加到 `SerialPort` 声明下方。
internal delegate void SerialPinChangedEventHandlerDelegate
(object sender, SerialPinChangedEventArgs e);
private SerialPinChangedEventHandler SerialPinChangedEventHandler1;
这将创建所需的对象。下面的行应添加到窗体声明的 `InitializeComponents();` 下方。
SerialPinChangedEventHandler1 = new SerialPinChangedEventHandler(PinChanged);
这声明了委托 `PinChanged`。在按钮点击事件中:
Add
SerialPinChangedEventHandler1 = new SerialPinChangedEventHandler(PinChanged);
并按如下方式附加委托。
ComPort.PinChanged += SerialPinChangedEventHandler1;
现在,当 DTR 或 RTS 连接到引脚时,程序将更改标签的背景颜色。我发现最简单的方法是将此程序与 9 针电缆末端的母插座一起运行。然后可以用一根电线接触焊杯(导致标签颜色从绿色变为红色再变回),这将让你感觉到你在取得一些进展。此时应注意,PC 将尝试打开默认的串行端口,默认设置为 9600 波特、无校验、一个停止位和无流控制,端口号由最低的可用串行端口决定,即 Com1(如果已安装)。
要实现两个单元之间最简单的通信方式,使用以下方法:
这只是将一个 RS-232 端口的引脚 2 连接到另一个端口的引脚 3,并将另一个端口的引脚 2 连接到另一个端口的引脚 3。虽然这已经足够了,但建议连接屏蔽地线以防止错误。
最简单的连接如下。
引脚 2 和 3 直接连接(**提示**:标准 9 针 D 型连接器的引脚间距刚好可以容纳一个来自旧 CD-ROM 背面的跳线!)
这种连接类型常用于查看程序是否接收和发送数据。
要实现此目的的演示代码,请在窗体上添加一个名为 `PortState` 的按钮,并添加以下代码。
private void btnPortState_Click(object sender, EventArgs e)
{
if (btnPortState.Text == "Closed")
{
btnPortState.Text = "Open";
ComPort.PortName = Convert.ToString(cboPorts.Text);
ComPort.BaudRate = Convert.ToInt32(cboBaudRate.Text);
ComPort.DataBits = Convert.ToInt16(cboDataBits.Text);
ComPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cboStopBits.Text);
ComPort.Handshake = (Handshake)Enum.Parse(typeof(Handshake), cboHandShaking.Text);
ComPort.Parity = (Parity)Enum.Parse(typeof(Parity), cboParity.Text);
ComPort.Open();
}
else if (btnPortState.Text == "Open")
{
btnPortState.Text = "Closed";
ComPort.Close();
}
代码一旦点击按钮,就会将按钮的文本更改为“**打开**”,然后单独设置 COM 端口的各种属性。我以这种方式设置我所有的 COM 端口,因为这样可以节省盯着代码试图弄清楚发生了什么。
一个很好的习惯是使用 `IsOpen` 属性来检查端口是否可以打开。我发现它最主要的用途是 USB COM 端口,这些端口可以随时添加和删除(并且不总是放回同一个插槽),示例如下。
if(!(ComPort.IsOpen))
{
}
这个 `if` 语句将检查 COM 端口是否已打开,`!` 运算符用于检查是否已关闭。另一个有用的技巧是使用 `try`...`catch`,如下所示。
try
{
ComPort.Open();
}
catch(UnauthorizedAccessException ex)
{
MessageBox.Show(ex.Message);
}
这将导致软件在发现故障时产生一条消息,而不是直接崩溃!另外,串行端口类还提供 `Timeout` 属性,以防止软件在循环中等待,您可以这样做:
ComPort.ReadTimeout(4000);
ComPort.WriteTimeout(6000);
上面的代码为串行端口的操作设置了时间限制。默认情况下,这些被设置为无限,但可以进行设置。我曾使用读取超时功能与一些无线电板通信,以检查它们是否仍在范围内(如果获取主板 X 序列号的命令超时,X 则不在范围内)。
有两种方法可以接收数据:使用 `DataRecieved` 事件的中断,以及使用计时器进行轮询(不推荐),但可以使用工具箱中的秒表计时器,并将间隔属性设置为 1000 或 1 秒(代码见下文)……
private void tmrPollForRecivedData_Tick(object sender, EventArgs e)
{
String RecievedData;
RecievedData = ComPort.ReadExisting();
if (!(RecievedData == ""))
{
rtbIncoming.Text += RecievedData;
}
}
要启动计时器,请使用以下行:
tmrPollForRecivedData.Enable = true;
要停止它,请使用:
tmrPollForRecivedData.Enable = false;
通常,这种方法对于单个串行端口且数据变化不快时是有效的,但我曾因此而吃过亏。有人(我认为我知道是谁!!)更改了软件中的间隔值以使其运行更快,结果非常糟糕(谁为此承担了责任!),因为 Windows 可能会覆盖和忽略计时器组件。
这两种方法都是有效的,但是 `DataReceived` 事件是基于中断的,使用委托。要使此功能工作,请添加以下代码。
delegate void SetTextCallback(string text);
string InputData = String.Empty;
代码创建了一个委托 `SetTextCallback` 和一个名为 `InputData` 的空字符串。
接下来,事件处理程序被添加到代码中。
private void port_DataReceived_1(object sender, SerialDataReceivedEventArgs e)
{
InputData = ComPort.ReadExisting();
if (InputData != String.Empty)
{
this.BeginInvoke(new SetTextCallback(SetText), new object[] { InputData });
}
}
`SetText` 由 `SetTextCallback` 调用。
private void SetText(string text)
{
this.rtbIncoming.Text += text;
}
窗体上创建了一个名为**Hello**的按钮,其点击例程如下。
private void btnHello_Click(object sender, EventArgs e)
{
ComPort.Write("Hello World!");
}
有了上面的按钮,我们可以发送著名的(或臭名昭著的!)消息 `Hello World`。
这是通过使用 `.Write()` 函数实现的。它可以写入字节数组、字符数组或子数组或字符串。与此相关的有 `WriteLine()`,它发送一个字符串和一个新行(对 C 程序员来说是 `\n`),以及 `WriteByte`,它输出一个字节。这些方法是“阻塞”的,也就是说,在发送数据时,它们无法接收数据。
读取方法是 `ReadExisting()` 方法。它会读取接收缓冲区中的文本,直到缓冲区为空。此方法在获得数据或超时到期之前不会阻塞端口。
如上所示,相关命令有 `Read()`、`ReadByte()`、`ReadChar()`、`ReadLine()` 和 `ReadTo()`。我经常发现设备倾向于用文本回复(有些设备的 `\r\n` 或回车符 & 换行符设置可以更改)。在所有这些方法中,我发现 `ReadExisting()` 和 `ReadLine()` 是最常用的。`ReadTo()` 可用于检查传入数据中的特定字符,例如“`\n`”。问题是,如果数据很大,有时字符可能会被意外包含,例如“`\n`”。我使用 `ReadExisting()` 方法与那些我知道只会发送固定数量数据的设备通信,而使用 `ReadLine()` 用于数据量相当大的情况,因为它会读取直到遇到新行(丢弃新行),如下所示。
private void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
ComPort.ReadLine();
}
放置在端口的中断处理程序中。
下面是应用程序的当前状态。
这是通过点击**端口**按钮填充组合框,然后点击**打开**按钮以打开所选端口来实现的。300 的速度足够慢,可以观察到消息在端口的读取周期之间被捕获。
在尝试完以上内容后,接下来可以尝试添加另一个名为 `rtbOutgoing` 的富文本框,并将以下代码添加到项目中。
private void rtbOutgoing_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)13) // enter key
{
ComPort.Write("\r\n");
rtbOutgoing.Text = "";
}
else if (e.KeyChar < 32 || e.KeyChar > 126)
{
e.Handled = true; // ignores anything else outside printable ASCII range
}
else
{
ComPort.Write(e.KeyChar.ToString());
}
}
添加富文本框和上述代码后,必须将 `KeyPress` 事件“连接”到它。方法是点击事件选择器(看起来像一个闪电),转到 `KeyPress` 事件,点击向下箭头并选择 `rtbOutgoing_KeyPress`,这将附加事件。
这样就可以在输出文本框中输入内容,并通过前面提到的引脚 2 和 3 之间的短接来接收。这将允许像 Hyperterminal 那样将文本作为按键发送(Hyperterminal 从 Vista 开始就不再自带,我猜微软觉得它不再需要了……)。多年来,Hyperterminal 一直被视为大多数串行通信的基础,许多设备都期望 Hyperterminal 风格的命令。我不能说这是正确的方法,但是,它看起来有点丑陋,但确实有效。对于这个例子,文本被加载到一个名为 `txtCommand` 的文本框中。
string Command1 = txtCommand.Text;
string CommandSent;
int Length, j = 0;
Length = Command1.Length;
for (int i = 0; i < Length; i++)
{
CommandSent = Command1.Substring(j, 1);
myComPort.Write(CommandSent);
j++;
}
实际上,这只是获取 `txtCommand` 中的文本(例如,“RZ04”),设置一个名为 `Length` 的整数为字符串的长度,然后一个 `for` 循环从 0 计数到 `Length`,每次 `CommandSent` 等于 `Command1` 在 `j` 位置的一个字符的子字符串。
如果将此代码作为按钮添加到应用程序,并添加一个名为 `txtCommand` 的文本框,则窗体如下。
enum REPLY : int { NO_REPLY, YES_REPLY, TIMEOUT_REPLY }
System.Timers.Timer NoDataAtPort = new System.Timers.Timer(10000);
通过使用枚举 `Reply`,`Reply` 可以有三个可能的值:`No`、`Yes` 和 `Timeout`。`NO_REPLY` 用于等待回复。计时器将停止无限期等待。`YES_REPLY` 表示设备已收到数据并已确认。`TIMEOUT_REPLY` 是计时器到期但未收到回复的情况。
下面是我编写并用于某些自动测试设备(因此使用了 ATE!)的写入/读取例程。
private string Write_ATE(string Data_To_ATE)
{
string Data_From_ATE = "";
Reply_Status = (int)REPLY.NO_REPLY;
ATEComPort.Write(Data_To_ATE);
while (Reply_Status == (int)REPLY.NO_REPLY)
{
NoDataAtPort.Enabled = true;
}
NoDataAtPort.Enabled = false;
if (Reply_Status == (int)REPLY.TIMEOUT_REPLY)
{
Reply_Status = (int)REPLY.NO_REPLY;
ATEComPort.Write(Data_To_ATE);
while (Reply_Status == (int)REPLY.NO_REPLY)
{
NoDataAtPort.Enabled = true;
}
NoDataAtPort.Enabled = false;
if (Reply_Status == (int)REPLY.TIMEOUT_REPLY)
{
Data_From_ATE = "TIMEOUT";
return (Data_From_ATE);
}
else if (Reply_Status == (int)REPLY.YES_REPLY)
{
Data_From_ATE = ATEComPort.ReadTo("\r\n");
if ((Data_From_ATE.Substring(0, 1)) == (Data_To_ATE.Substring(1, 1)))
{
return (Data_From_ATE);
}
}
else
{
Data_From_ATE = "SERIOUS_ERROR";
return (Data_From_ATE);
}
}
else if (Reply_Status == (int)REPLY.YES_REPLY)
{
Data_From_ATE = ATEComPort.ReadTo("\r\n");
if ((Data_From_ATE.Substring(0, 1)) == (Data_To_ATE.Substring(1, 1)))
{
return (Data_From_ATE);
}
//add hardware replies to this section as below
/*else if ((Data_From_ATE.Substring(0, 1)) == "E")
{
Reply_Status = (int)REPLY.NO_REPLY;
ATEComPort.Write(Data_To_ATE);
while (Reply_Status == (int)REPLY.NO_REPLY)
{
NoDataAtPort.Enabled = true;
}
NoDataAtPort.Enabled = false;
if (Reply_Status == (int)REPLY.TIMEOUT_REPLY)
{
Data_From_ATE = "TIMEOUT";
return (Data_From_ATE);
}
else if (Reply_Status == (int)REPLY.YES_REPLY)
{
Data_From_ATE = ATEComPort.ReadTo("\r\n");
if ((Data_From_ATE.Substring(0, 1)) == (Data_To_ATE.Substring(1, 1)))
{
return (Data_From_ATE);
}
else if ((Data_From_ATE.Substring(0, 1)) == "E")
{
return (Data_From_ATE);
}
}
else
{
Data_From_ATE = "SERIOUS_ERROR";
return (Data_From_ATE);
}
}*/
}
else
{
Data_From_ATE = "SERIOUS_ERROR";
return (Data_From_ATE);
}
return (Data_From_ATE);
}
这也是一个关于何时以及如何使用 `ReadTo()` 的例子。在该例程中,我使用 `ReadTo(“\r\n”);` 读取到回车符、换行符。该设备过去会因为某个原因(我一时想不起来了!)输出“`\n`”,而获取可靠消息的唯一方法是使用“`\r\n`”。时间和运输原因导致错误部分未使用,因为设备会返回 Exx,xx 为 00 到 99,例如 E99 表示“命令未识别”。还有其他代码,但这些都不需要。
计时器代码如下。
private void OnTimeOut(object sender, ElapsedEventArgs e)
{
Reply_Status = (int)REPLY.TIMEOUT_REPLY;
}
接收数据处理程序如下。
private void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
Reply_Status = (int)REPLY.YES_REPLY;
}
这种方法只应在单元命令以某个固定字符串(如本单元的“`\r\n`”)结尾,并且程序不需要执行任何其他操作,或者某些预期数据丢失时使用,例如“`Hello World!!`”变成“`llo World!`”。
此方法应使用以下方式调用:
private void btnTest_Click(object sender, EventArgs e)
{
//to send command use below routine
string ATE_Reply = null;
Reply_Status = (int)REPLY.NO_REPLY;
ATE_Reply = Write_ATE("Hello World!\r\n");
rtbIncoming.Text += ATE_Reply + "\r\n";
}
这将从 Com 1 发送“`Hello World!`”。此方法最初是为一款难以使用的自动测试设备开发的(用委婉的说法!),其中一些设备以高速率发送数据,而另一些设备以低速率发送数据,并且可能会在发送命令完成之前调用中断方法并可能阻塞端口(因此使用了 `Enum` 数组,并且中断处理程序中没有读取操作)。
我在示例中专注于 RS-232;然而,这应该同样适用于其他串行网络,如 RS-422 和 RS-485。
RS-485 不是 PC 的标准配置,需要第三方设备,如 Amplicon (http://www.amplicon.com/) 公司的产品。设置由总线上的设备决定。PC 通过连接到通信端口的设备进行监听。要打开端口,请遵循产品数据手册并打开端口。命令及其格式应在文档中详细说明。
下面是完整的代码版本。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.IO.Ports;
using System.Windows.Forms;
//CodeProjectSerialComms program
//23/04/2013 16:29
namespace CodeProjectSerialComms
{
public partial class Form1 : Form
{
SerialPort ComPort = new SerialPort();
internal delegate void SerialDataReceivedEventHandlerDelegate(
object sender, SerialDataReceivedEventArgs e);
internal delegate void SerialPinChangedEventHandlerDelegate(
object sender, SerialPinChangedEventArgs e);
private SerialPinChangedEventHandler SerialPinChangedEventHandler1;
delegate void SetTextCallback(string text);
string InputData = String.Empty;
public Form1()
{
InitializeComponent();
SerialPinChangedEventHandler1 = new SerialPinChangedEventHandler(PinChanged);
ComPort.DataReceived +=
new System.IO.Ports.SerialDataReceivedEventHandler(port_DataReceived_1);
}
private void btnGetSerialPorts_Click(object sender, EventArgs e)
{
string[] ArrayComPortsNames = null;
int index = -1;
string ComPortName = null;
//Com Ports
ArrayComPortsNames = SerialPort.GetPortNames();
do
{
index += 1;
cboPorts.Items.Add(ArrayComPortsNames[index]);
} while (!((ArrayComPortsNames[index] == ComPortName) ||
(index == ArrayComPortsNames.GetUpperBound(0))));
Array.Sort(ArrayComPortsNames);
if (index == ArrayComPortsNames.GetUpperBound(0))
{
ComPortName = ArrayComPortsNames[0];
}
//get first item print in text
cboPorts.Text = ArrayComPortsNames[0];
//Baud Rate
cboBaudRate.Items.Add(300);
cboBaudRate.Items.Add(600);
cboBaudRate.Items.Add(1200);
cboBaudRate.Items.Add(2400);
cboBaudRate.Items.Add(9600);
cboBaudRate.Items.Add(14400);
cboBaudRate.Items.Add(19200);
cboBaudRate.Items.Add(38400);
cboBaudRate.Items.Add(57600);
cboBaudRate.Items.Add(115200);
cboBaudRate.Items.ToString();
//get first item print in text
cboBaudRate.Text = cboBaudRate.Items[0].ToString();
//Data Bits
cboDataBits.Items.Add(7);
cboDataBits.Items.Add(8);
//get the first item print it in the text
cboDataBits.Text = cboDataBits.Items[0].ToString();
//Stop Bits
cboStopBits.Items.Add("One");
cboStopBits.Items.Add("OnePointFive");
cboStopBits.Items.Add("Two");
//get the first item print in the text
cboStopBits.Text = cboStopBits.Items[0].ToString();
//Parity
cboParity.Items.Add("None");
cboParity.Items.Add("Even");
cboParity.Items.Add("Mark");
cboParity.Items.Add("Odd");
cboParity.Items.Add("Space");
//get the first item print in the text
cboParity.Text = cboParity.Items[0].ToString();
//Handshake
cboHandShaking.Items.Add("None");
cboHandShaking.Items.Add("XOnXOff");
cboHandShaking.Items.Add("RequestToSend");
cboHandShaking.Items.Add("RequestToSendXOnXOff");
//get the first item print it in the text
cboHandShaking.Text = cboHandShaking.Items[0].ToString();
}
private void port_DataReceived_1(object sender, SerialDataReceivedEventArgs e)
{
InputData = ComPort.ReadExisting();
if (InputData != String.Empty)
{
this.BeginInvoke(new SetTextCallback(SetText), new object[] { InputData });
}
}
private void SetText(string text)
{
this.rtbIncoming.Text += text;
}
internal void PinChanged(object sender, SerialPinChangedEventArgs e)
{
SerialPinChange SerialPinChange1 = 0;
bool signalState = false;
SerialPinChange1 = e.EventType;
lblCTSStatus.BackColor = Color.Green;
lblDSRStatus.BackColor = Color.Green;
lblRIStatus.BackColor = Color.Green;
lblBreakStatus.BackColor = Color.Green;
switch (SerialPinChange1)
{
case SerialPinChange.Break:
lblBreakStatus.BackColor = Color.Red;
//MessageBox.Show("Break is Set");
break;
case SerialPinChange.CDChanged:
signalState = ComPort.CtsHolding;
// MessageBox.Show("CD = " + signalState.ToString());
break;
case SerialPinChange.CtsChanged:
signalState = ComPort.CDHolding;
lblCTSStatus.BackColor = Color.Red;
//MessageBox.Show("CTS = " + signalState.ToString());
break;
case SerialPinChange.DsrChanged:
signalState = ComPort.DsrHolding;
lblDSRStatus.BackColor = Color.Red;
// MessageBox.Show("DSR = " + signalState.ToString());
break;
case SerialPinChange.Ring:
lblRIStatus.BackColor = Color.Red;
//MessageBox.Show("Ring Detected");
break;
}
}
private void btnTest_Click(object sender, EventArgs e)
{
//SerialPinChangedEventHandler1 = new SerialPinChangedEventHandler(PinChanged);
//ComPort.PinChanged += SerialPinChangedEventHandler1;
//ComPort.Open();
//ComPort.RtsEnable = true;
//ComPort.DtrEnable = true;
//btnTest.Enabled = false;
}
private void btnPortState_Click(object sender, EventArgs e)
{
if (btnPortState.Text == "Closed")
{
btnPortState.Text = "Open";
ComPort.PortName = Convert.ToString(cboPorts.Text);
ComPort.BaudRate = Convert.ToInt32(cboBaudRate.Text);
ComPort.DataBits = Convert.ToInt16(cboDataBits.Text);
ComPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cboStopBits.Text);
ComPort.Handshake =
(Handshake)Enum.Parse(typeof(Handshake), cboHandShaking.Text);
ComPort.Parity = (Parity)Enum.Parse(typeof(Parity), cboParity.Text);
ComPort.Open();
}
else if (btnPortState.Text == "Open")
{
btnPortState.Text = "Closed";
ComPort.Close();
}
}
private void rtbOutgoing_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)13) // enter key
{
ComPort.Write("\r\n");
rtbOutgoing.Text = "";
}
else if (e.KeyChar < 32 || e.KeyChar > 126)
{
e.Handled = true; // ignores anything else outside printable ASCII range
}
else
{
ComPort.Write(e.KeyChar.ToString());
}
}
private void btnHello_Click(object sender, EventArgs e)
{
ComPort.Write("Hello World!");
}
private void btnHyperTerm_Click(object sender, EventArgs e)
{
string Command1 = txtCommand.Text;
string CommandSent;
int Length, j = 0;
Length = Command1.Length;
for (int i = 0; i < Length; i++)
{
CommandSent = Command1.Substring(j, 1);
ComPort.Write(CommandSent);
j++;
}
}
}
}
关注点
我确实发现关于模拟 HyperTerminal 的信息很少,所以我自己做了一个版本,它似乎可以工作,正如我所说的,它不怎么漂亮,但是……为了进一步澄清这里提到的任何内容,可以参考 Jan Axelson (LVR) 的著作《Serial Port Complete (Second Edition)》。另外,这是我第一次向 CodeProject 提交,请温柔点!
历史
- 0.1 首次基本上传以供提交
- 1.0 添加了一些关于应用程序为何会崩溃的详细信息