65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的串行通信入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (152投票s)

2013年11月4日

CPOL

20分钟阅读

viewsIcon

1025308

downloadIcon

45249

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 端口到组合框(我称之为**端口**),这些更改现在会得到以下结果。

波特率是每秒可能发生的事件数量。它通常显示为每秒位数,可能使用的数字是 300、600、1200、2400、9600、14400、19200、38400、57600 和 115200(这些来自 UAR 8250 芯片,如果是 16650 则有额外的 230400、460800 和 921600)。

这些都受**串行端口**类支持,并源自旧式的电传打字机。非标准波特率是不可取的,但是,如果您能找到 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 型连接器以实现全双工通信。

如果 `Handshake` 属性设置为 `None`,则 DTR 和 RTS 引脚将释放用于常见的电源用途。我正在输入的 PC 在 DTR 引脚上输出 +10.99 伏特,RTS 引脚设置为 `true` 时也输出 +10.99 伏特。如果设置为 `false`,则 DTR 输出 -9.95 伏特,RTS 输出 -9.94 伏特。这些值介于 +3 至 +25 伏特和 -3 至 -25 伏特之间,这提供了一个死区以允许噪声抗扰度。

这种切换可以通过以下方式实现

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` 的文本框,则窗体如下。

这个例子使用中断来处理数据,与写入是分开的。对于某些应用程序,我发现这种方法并不是真正有效的。我发现有一种方法,可以调用一个函数来发送数据,然后数据保留在函数中等待响应,这在调试硬件时可能很有用(用来解决 Windows 或硬件的问题!)。
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 添加了一些关于应用程序为何会崩溃的详细信息
© . All rights reserved.