C#\面向对象程序设计\运行错误的原因!麻烦详细解答,谢谢!

C#中的面向对象编程
C#中的面向对象是指3个基本特征:封装、继承、多态。
封装是指把类的内部数据隐藏起来,不让对象直接进行操作。C#中可用属性来对类内部的状态进行操作,使用public、private、protected、internal等关键词来实现。
1.为何要封装
当类的内部数据没有被封装时,若把字段定义为公共字段,则外部对象可以对内部数据进行任意的操作,很可能导致不当的操作结果。例如:
public class Person
class Program
static void Main(string[] args)
Person person = new Person();
person.name = &张三&;
person.age = -2;
Console.Read();
以上代码中,虽然程序可以正常运行,但是逻辑上有问题,因为人的年龄不可能为负。
这就是外部对象对类内部数据进行任意操作的产生的不当结果。为了避免这种情况,可以将类进行封装。
2.如何封装
C#提供了属性机制来对私有字段数据进行间接的操作,并且可以在属性中加入判断机制来避免逻辑错误。因此,在面向对象编程中,应更多的定义私有字段。例如:
public class Person
//定义私有字段
//定义公有属性
public int Age
//加入逻辑判定
if(value & 0 || value & 100)
throw(new ArgumentOutOfRangeException(&AgeIntPropery&, value, &年龄必须在0到100之间&));
public string Name
set{name =}
class Program
static void Main(string[] args)
Person person = new Person();
person.Name = &张三&;
person.Age = 20; //设置了一个合理的值
Console.WriteLine(person.Name);
Console.WriteLine(person.Age);
以上代码中,person.Age设置为一个不合理的数(小于0,大于100)时,执行时程序会中断,并抛出ArgumentOutOfRangeException。
在C#中,一个类可以继承另一个已有类(密封的除外),被继承的类称为基类(或父类),继承的类称为派生类(或子类),子类将获得基类除构造函数和析构函数以外的所有成员。此外,静态类是密封类,不能被继承。
1.什么是继承
简单来说,一个类定义了一些方法、属性等,若另一个类也要定义同样的方法、属性等,可以直接通过继承的方式来获得基类的所有方法、属性等,避免了代码重复。并且同时,子类可以定义另外一些自有的方法、属性等。这就好比男人女人都先拥有人的属性,然后还可以定义自有的属性。
2.如何继承
在C#中,子类仅支持派生于一个基类,也就是一个子类仅可有一个父类。但一个父类可以有多个子类。这些子类都继承父类相同的成员。C#中继承的方法为:
//定义一个基类
public class Father
//基类成员定义
//子类继承父类
public class ChildA : Father
//子类自有成员定义
public class ChildB : Father
//子类自有成员定义
需要注意的是,子类并不能对父类的私有成员进行直接访问,它只可对保护成员和公有成员进行访问。但是子类会继承基类的私有成员,子类可以通过调用公有或保护方法间接的对自有成员进行访问。
上面说到,一个类可以继承另一个除密封类以外的类。所以,若不想被继承,则可以使用sealed关键字将其定义为密封类。方法为:
public sealed class SealedClass
//密封类成员定义
若有类要继承该密封类时,编译器会报错。
子类的初始化顺序:
上面说到,子类不会继承基类的构造函数和析构函数。但是当子类进行初始化时,任然会调用基类的构造函数。
初始化顺序为:
①初始化子类的实例字段;
②调用基类的构造函数,若没有指明基类,则调用System.Object的构造函数;
③调用子类的构造函数;
class Program
static void Main(string[] args)
//初始化子类实例
Child child = new Child();
//调用子类方法
child.Print();
Console.Read();
//创建一个父类Father
public class Father
//初始化它的实例字段;
private string name = &张三&;
//定义一个方法输出实例字段
public void Print()
Console.WriteLine(name);
//调用基类构造函数
public Father()
Console.WriteLine(&基类构造函数被调用了&);
//创建一个子类Child
public class Child : Father
//初始化它的实例字段
private int Age = 3;
//定义一个方法输出实例字段
public void Print()
Console.WriteLine(Age);
//调用子类构造函数
public Child()
Console.WriteLine(&子类构造函数被调用了&);
运行以上代码,结果为:
需要注意,运行结果显示,初始化子类实例时,先执行了基类的构造函数,再执行了子类的构造函数,最后执行了子类的方法。并未执行基类的方法,且子类执行顺序并不是按代码顺序先执行方法再执行构造函数。因此可见总是要并先要执行父类的构造函数,第二再执行子类的构造函数。
简单来说,如果一个子类继承了一个基类,则获得了基类中的某些行为(如方法、属性等),但是如果子类想改变继承的方法,就需要覆写基类的方法,这种技术就称为多态。
1.用virtual和override关键字实现
基类成员声明为virtual(称为虚方法)时,子类可以重写,如果想改变虚方法的实现行为,要使用override关键字。例如:
class Program
static void Main(string[] args)
//初始化子类实例
Son son = new Son();
Daughter daughter = new Daughter();
//调用子类的方法
son.Age();
daughter.Age();
Console.Read();
public class Father
//定义输出年龄的方法
public virtual void Age()
Console.WriteLine(&父亲的年龄为50岁&);
public class Son : Father
//覆写输出年龄的方法
public override void Age()
//调用基类的方法
base.Age();
Console.WriteLine(&儿子的年龄为20岁&);
public class Daughter : Father
public override void Age()
//base.Age();不调用基类的方法
Console.WriteLine(&女儿的年龄为18岁&);
运行以上代码,结果为:
由此可见,子类继承了基类虚方法后对虚方法进行了覆写,在儿子类中覆写时调用了基类方法,因此运行结果仍然有基类方法。女儿类中覆写时未调用基类方法,运行结果则无基类方法。
可见,多态的精髓是相同类型的对象调用相同的方法却表现了不同的行为。
2.用abstract关键字防止创建类的实例
用1实现多态时会存在一个问题,即基类可以通过new操作符创建基类的实例。可有的情况下,基类是一个抽象概念(如:人、动物、天气),我们希望避免创建这种抽象的实例。此时可用abstract关键字来防止在代码种直接创建这样的实例,然后仍然可用1实现多态。例如:
public abstract class Wether
public virtual void Print()
Console.WriteLine(&天气状况为:&);
public class Rain : Wether
public override void Print()
base.Print();
Console.WriteLine(&雨&);
public class Sun : Wether
public override void Print()
base.Print();
Console.WriteLine(&晴&);
class Program
static void Main(string[] args)
Rain rain = new Rain();
Sun sun = new Sun();
//无法创建基类的实例
//Wether wether = new Wether();
rain.Print();
sun.Print();
Console.Read();
运行以上代码,结果为:
若将Main中的创建基类实例的注释取消,则会报错:
3.用sealed关键字防止子类被覆写
前面讲到可以用sealed关键字来密封类,同理,可以用sealed关键字来密封子类方法防止子类被重写。例如:
public abstract class Parent
public virtual void Marry()
Console.WriteLine(&父母已经结婚25年&);
public class Son : Parent
//用sealed关键字防止Son类被覆写
public sealed override void Marry()
base.Marry();
Console.WriteLine(&儿子已经结婚2年&);
以上代码的类Son无法被覆写,若添加以下代码:
public class GrandSon : Son
public override void Marry()
base.Marry();
Console.WriteLine(&孙子还小,不能结婚&);
则会显示错误信息:无法对密封成员进行复写。
4.用new关键字隐藏基类成员
有的情况下,子类需要定义与基类相同名字的成员,此时可以用new关键字将基类成员隐藏起来。若不使用new关键字,则会报错。例如:
public class Father
public void Name()
Console.WriteLine(&爸爸叫大明&);
public class Son : Father
//子类中仍然想定义一个叫Name的方法,使用new关键字
public new void Name()
Console.WriteLine(&儿子叫小明&);
如果此时仍然想要访问基类的成员,可使用强制类型转换,把子类强制转换成基类类型,从而访问隐藏的基类成员。例如:
class Program
static void Main(string[] args)
Son son = new Son();
son.Name();
//调用基类的Name方法
((Father)son).Name();
Console.Read();
以上代码,结果为:
所有类的父类:System.Object
C#中,所有的类都派生自System.Object类。如果类没有指定任何的基类,则编译器就自动吧Object当作它的基类。
详细信息请参考:Object类(System)当前位置: >>
c#学习资料
using S using System.Collections.G using System.ComponentM using System.D using System.D using System.L using System.T using System.Windows.F using System.T namespace WindowsFormsApplication4 { public partial class Form1 : Form { private delegate void FlushClient(); //代理 Thread thread = int counter = 0; public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { this.listBox1.Items.Clear(); button1.Enabled = thread = new Thread(CrossThreadFlush); thread.IsBackground = thread.Start(); } private void button2_Click(object sender, EventArgs e) { thread.Suspend(); button1.Enabled = } private void CrossThreadFlush() { while (true) { //将 sleep 和无限循环放在等待异步的外面 Thread.Sleep(1000); ThreadFunction(); } } private void ThreadFunction() { if (this.listBox1.InvokeRequired)//等待异步 { FlushClient fc = new FlushClient(ThreadFunction); this.Invoke(fc); //通过代理调用刷新方法 } else { counter += 1; this.label1.Text = counter.ToString(); this.listBox1.Items.Add(System.DateTime.Now.ToString()); } } } }由于项目的需要,最近几天一直在做串口和数据库。由于 C#使用的时间不 长, 所以在编写代码和调试的过程中总是遇到意想不到的问题,比如在使用串口 接收数据的时候,在接收数据事件中想把接收的数据放入一个 textbox 作显示, 但是明明非常简单的代码,在编译的时候总是提示有错误。后来查看网上资料, 才知道 C#还有委托,匿名等等之类的新东西。下面我就把我这几天的经验和大 家分享一下。 这次就主要说说委托和匿名方法, 以后在说说串口使用方面的经验。 先说一下委托的基本概念,委托是一种引用型的数据类型,其实它的概念和 C 语言的函数指针几乎是一样的。 回忆一下 C 语言的函数指针, 定义一个函数指 针, 需要指定形参的类型和返回值的类型,只要有函数的形参类型和返回类型和 这个函数指针一致,那么该函数指针就可以指向这个函数。C 语言学习中一定会 提高一个四则运算的例子,就是利用函数指针。 C#中委托的申明如下 【访问修饰符】 delegate 返回值类型 委托名(【参数列表】); 委托虽然是一种数据格式,但是却需要像类一下去实例化。所以委托的实例 化如下 委托类型 委托变量名 = new 委托型构造函数(委托要引用的方法名) 实例化过后就可以使用命名方法和匿名方法两种方法,来来指定这个委托的 函数引用。 废话少说,还是来一个实例吧。比如说,在主线程之外开辟一个线程,这个 线程使用主线程的一个 label 控件,显示当前的时间。由于新开的线程要使用其 他线程开的控件,所以需要使用到 control 类型的 Invoke 方法,而这个方法传入 正是 delegate 类型。 先来看一个“想当然”的代码。private void showDateTimeMethod() { while (true) { //显示当前时间 label1.Text = &当前时间 & + DateTime.Now.ToString(); //线程暂停 Thread.Sleep(1000); } } private void Form1_Load(object sender, EventArgs e) { //新建一个线程 Thread showDateTimethread = new Thread(new ThreadStart(showDateTimeMethod)); //该线程为后台线程 showDateTimethread.IsBackground = //线程启动 showDateTimethread.Start(); }用我的大腿想想,上面的代码应该是对的,但是由于 net 苛刻的安全机制, showDateTimeMethod 中的代码是有错误的。为了解决这个错误可以声明一个委 托类型,实例化一个委托变量(这话听起来很拗口),并给这个委托变量指定函 数引用。方法一,使用命名方法//声明一个委托类型,该委托类型无输入参数和输出参数 public delegate void ProcessDelegate(); //函数引用,label 控件显示当前时间,输入参数无,输出参数无,和声明的委托 类型形式一致 public void LabelShow() { label1.Text = &当前时间 & + DateTime.Now.ToString(); } 然后在线程中实例化一个委托变量,指向这个函数引用。 while (true) { //使用命名方法 ProcessDelegate showProcess = new ProcessDelegate(LabelShow); //调用 label 的 invoke 方法 label1.Invoke(showProcess); //线程暂停 Thread.Sleep(1000); } 这样的话就可以在窗体中看到当前的时间了。方法二,使用匿名方法 刚刚的函数引用只有区区一行,这一行完全可以使用匿名方法实现,如果使用匿名 方法的话就可以免去编写一个函数,代码显得非常简洁。 匿名方法使用方法也很简单。 委托类型 委托变量 = delegate (【参数列表】){代码块}; while (true) { //实例化一个委托变量,使用匿名方法构造 ProcessDelegate showProcess = delegate() { label1.Text = &当前时间 & + DateTime.Now.ToString(); }; label1.Invoke(showProcess); //线程暂停 Thread.Sleep(1000); }方法三,使用 MethodInvoker再观察一下上面的代码,这个委托类型,输入参数无输出参数也无,就可以 使用 C#的 MethodInvoker 直接构造,关于 MethodInvoker,MSDN 给出这样一个 解释“表示一个委托,该委托可执行托管代码中声明为 void 且不接受任何参数的任何方法”//使用匿名方法 2 while (true) { label1.Invoke ( //委托,托管无参数的任何方法 new MethodInvoker ( delegate { label1.Text = &当前时间 & + DateTime.Now.ToString(); } ) ); //线程暂停 Thread.Sleep(1000); } 如果熟悉了 C#的委托和匿名方法之后,那么在使用串口控件的时候就方便了很多。 但是解决了旧的问题,新的问题也会到来。如果串口获得的数据很多,直接在接收的时 候处理的话很难保证实时性,代码也变得很臃肿不堪。通过查阅网上的资料,想尝试使 用异步的方法处理问题,即串口接收事件只负责接收数据,并把数据放入队列中一个线 程每隔一段时间处理线程的内容,然后把整理好的数据放入数据库中。不过这要下次再 说了啊。1. 属性列表: SelectionMode 组件中条目的选择类型: None- 根本不允许任何选择; One- 默认值,只选择单个选项; MultiSimple- 简 单 的 多 项 选 择 , 单 击 一 次 鼠 标 就 选 中 或 取 消 选 中 列 表 中 的 一 项 ; MultiExtended-扩展的多项选择,类似 windows 中的选择操作. SelectedItem 在单选的列表框里, SelectedItem 返回的是一个对象,它的文本由 Text 属性表示.作用是 获得列表框中被选择的条目.如果控件允许多项选择,被选中的条目就以 SelectedItems 属 性表示,它是 Item 对象的一个集合. Count 列表框中条目的总数 SelectedIndex /SelectedIndices/SelectedItem/SelectedItems ListBox.SelectedIndex 属 性 获 取 单 项 选 择 ListBox 中 当 前 选 定 项 的 位 置 ; ListBox.SelectedIndices 属性获取一个集合,该集合包含 ListBox 中所有当前选定项的从零 开始的索引; ListBox.SelectedItem 属性获取 ListBox 中当前选定的项; ListBox.SelectedItems 属性获取多重选择 ListBox 中所有选定的项,它是一集合。 Public ReadOnly Property SelectedIndices As ListBox.SelectedIndexCollection ListBox.SelectedIndexCollection,包含控件中当前选定项的索引。如果当前没有选定的项, 则返回空 ListBox.SelectedIndexCollection 泛指列表框中的所有项 2. 取列表框中被选中的值 ListBox.SelectedValue 3. 动态的添加列表框中的项: ListBox.Items.Add(&所要添加的项&); 以下代码实现通过输入框向列表框中添加内容: Private Sub bttnAdd1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bttnAdd1.Click Dim ListItem As String ListItem = InputBox(&Enter new item's name&) If ListItem.Trim && && Then sourceList.Items.Add(ListItem) End If End Sub ListBox.Items.Insert(index,item) item 是要添加到列表的对象,index 是这个新项的索引。 4. 移出指定项: //首先判断列表框中的项是否大于 0 If(ListBox.Items.Count & 0 ) { //移出选择的项 ListBox.Items.Remove(ListBox.SelectedItem); } 以下代码实现从单项选择的列表框中删除被选中的条目: Private Sub bttnRemoveSelDest_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bttnRemoveSelDest.Click ListBox.Items.Remove(ListBox.SelectedItem) End Sub 以下代码实现从多项选择列表框中删除多个条目: Private Sub bttnRemoveSelSrc_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bttnRemoveSelSrc.Click Dim i As Integer For i = 0 To ListBox.SelectedIndices.Count - 1 LisBoxt.Items.RemoveAt(ListBox.SelectedIndices(0)) Next End Sub 备注:利用 ListBox.Items.Remove 方法,以要删除的对象作为参数,从列表中删除条目。 而利用 RemoveAt 方法可以删除 指定位置(索引)的列 表项,它以索引作为参 数: ListBox.Items.RemoveAt(index) 5. 清空所有项: //首先判断列表框中的项是否大于 0 If(ListBox.Items.Count & 0 ) { //清空所有项 ListBox.Items.Clear(); } 6. 列表框可以一次选择多项: 只需设置列表框的属性 SelectionMode=&Multiple&,按 Ctrl 可以多选 7. 多列表框中搜索字符串: FindString 和 FindStringExact 方法可以迅速地找到条目 (search word) 在列表里的位置 (wordIndex)。它们都接收字符串作为弟一个参数,第二个参数可选,用于指定搜索开始的 位置。其中 FindString 找到与指定字符部分匹配的条目,而 FindStringExact 找到时完全匹 配的。 wordIndex=ListBox.FindStringExact(&search word&) wordIndex=ListBox.FindString(&search word&) 8. Contains 方法 利用它可以避免在列表中插入相同的对象。此方法接收一个对象作为参数,返回 Ture/False 来表示 Items 集合中是否包含这个对象。比如,要实现以下功能:先检查插入的字符串是否 已经存在于列表,只有当列表中还没有包含这个字符串时才插入它。其代码如下(VB.Net): Dim itm As String=&Remote Computing& If Not ListBox.Items.Contains(itm) then ListBox1.Item.Add(itm) End If 9. 两个列表框联动,即两级联动菜单 //判断第一个列表框中被选中的值 switch(ListBox1.SelectValue) { //如果是&A&,第二个列表框中就添加这些: case &A& ListBox2.Items.Clear(); ListBox2.Items.Add(&A1&); ListBox2.Items.Add(&A2&); ListBox2.Items.Add(&A3&); //如果是&B&,第二个列表框中就添加这些: case &B& ListBox2.Items.Clear(); ListBox2.Items.Add(&B1&); ListBox2.Items.Add(&B2&); ListBox2.Items.Add(&B3&); } 10. 实现列表框中项的移位 即:向上移位、向下移位 具体的思路为:创建一个 ListBox 对象,并把要移位的项先暂放在这个对象中。 如果是向上移位,就是把当前选定项的的上一项的值赋给当前选定的项,然后 把刚才新加入的对象的值,再附给当前选定项的前一项。 具体代码为: //定义一个变量,作移位用 index = -1; //将当前条目的文本以及值都保存到一个临时变量里面 ListItem lt=new ListItem (ListBox.SelectedItem.Text,ListBox.SelectedValue); //被选中的项的值等于上一条或下一条的值 ListBox.Items[ListBox.SelectedIndex].Text=ListBox.Items[ListBox.SelectedIndex index].T //被选中的项的值等于上一条或下一条的值 ListBox.Items[ListBox.SelectedIndex].Value=ListBox.Items[ListBox.SelectedIndex index].V //把被选中项的前一条或下一条的值用临时变量中的取代 ListBox.Items[ListBox.SelectedIndex].Test=lt.T //把被选中项的前一条或下一条的值用临时变量中的取代 ListBox.Items[ListBox.SelectedIndex].Value=lt.V //把鼠标指针放到移动后的那项上 ListBox.Items[ListBox.SelectedIndex].Value=lt.V 11. 移动指针到指定位置: (1).移至首条 //将被选中项的索引设置为 0 就 OK 了 ListBox.SelectIndex=0; (2).移至尾条 //将被选中项的索引设置为 ListBox.Items.Count-1 就 OK 了 ListBox.SelectIndex=ListBox.Items.Count-1; (3).上一条 //用当前被选中的索引去减 1 ListBox.SelectIndex=ListBox.SelectIndex - 1; (4).下一条 //用当前被选中的索引去加 1 ListBox.SelectIndex=ListBox.SelectIndex + 1;++在 C#中使用 SerialPort 类实现串口通信 遇到多线程问题4推 在 C#中使用 SerialPort 类实现串口通信。 2009 年 11 月 01 日 星期日 10:03 在.NET work 2.0 中提供了 SerialPort 类,该类主要实现串口数据通信等。本文章将本人在学习过程中 从网络上搜集到的相关信息写出来供大家参考。 下面主要介绍该类的主要属性(表 1)和方法(表.2)。 如果需要了解更多的信息请登录 http://msdn.microsoft.com/zh-cn/library/system.io.ports.serialp ort(VS.80).aspx 查看。 荐 相关文章 《使用 System.IO.Ports 读取 COM 口数据》 http://www.devasp.net/net/articles/display/727.html 使用 SerialPort 类的方法: 方法一: 首先要添加 using System.IO; using System.IO.P 1...在类的内部定义 SerialP 2...打开串口 com = new SerialPort(); com.BaudRate = 115200; com.PortName = &COM1&; com.DataBits = 8; com.Open();//打开串口 3...发送数据 Byte[] TxData ={1,2,3,4,5,6,7,8 }; com.Write(TxData, 0, 8); 4...接收数据 4.1 使用事件接收 this.com.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler(this.OnDat aReceived); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) 4.2 使用线程接收 接收数据启动一个线程,使其接收。 在类的内部定义 Thread _readT bool _keepR 打开串口后启动线程 _keepReading = _readThread = new Thread(ReadPort); _readThread.Start(); 线程函数 view plaincopy to clipboardprint? private void ReadPort() { while (_keepReading) { if (com.IsOpen) { byte[] readBuffer = new byte[com.ReadBufferSize + 1]; try { // If there are bytes available on the serial port, // Read returns up to &count& bytes, but will not block (wait) // for the remaining bytes. If there are no bytes available // on the serial port, Read will block until at least one byte // is available on the port, up until the ReadTimeout milliseconds // have elapsed, at which time a TimeoutException will be thrown. int count = com.Read(readBuffer, 0, com.ReadBufferSize); String SerialIn = System.Text.Encoding.ASCII.GetString(readBuffer, 0, coun t); if (count != 0) //byteToHexStr(readBuffer); Thread(byteToHexStr(readBuffer,count)); } catch (TimeoutException) { } } else { TimeSpan waitTime = new TimeSpan(0, 0, 0, 0, 50); Thread.Sleep(waitTime); } } } private void ReadPort() { while (_keepReading) { if (com.IsOpen) { byte[] readBuffer = new byte[com.ReadBufferSize + 1]; try { // If there are bytes available on the serial port, // Read returns up to &count& bytes, but will not block (wait) // for the remaining bytes. If there are no bytes available // on the serial port, Read will block until at least one byte // is available on the port, up until the ReadTimeout milliseconds // have elapsed, at which time a TimeoutException will be thrown. int count = com.Read(readBuffer, 0, com.ReadBufferSize); String SerialIn = System.Text.Encoding.ASCII.GetString(readBuffer, 0, count); if (count != 0) //byteToHexStr(readBuffer); Thread(byteToHexStr(readBuffer,count)); } catch (TimeoutException) { } } else { TimeSpan waitTime = new TimeSpan(0, 0, 0, 0, 50); Thread.Sleep(waitTime); } } } 方法二:使用 C#自带的 SerialPor 控件。 1...在“工具箱”的“组件”中选择 SerialPor 控件添加。 2...设置串口并打开 serialPort1.PortName = &COM1&; serialPort1.BaudRate = 9600; serialPort1.Open(); 3...写入数据可以使用 Write 或者下面的函数 serialPort1.WriteLine(str); 4...添加数据接收的事件 private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e) 使用中的一些常见问题 C#中 SerialPort 类中 DataReceived 事件 GUI 实时处理方法(来自 wanglei_.cn 的看法) MSDN:从 SerialPort 对象接收数据时,将在辅助线程上引发 DataReceived 事件。由于此事件在辅助线 程而非主线程上引发,因此尝试修改主线程中的一些元素(如 UI 元素)时会引发线程异常。如果有必要 修改主 Form 或 Control 中的元素,必须使用 Invoke 回发更改请求,这将在正确的线程上执行.进而要 想将辅助线程中所读到的数据显示到主线程的 Form 控件上时,只有通过 Invoke 方法来实现 下面是代码实例: private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e) { int SDateTemp = this.serialPort1.ReadByte(); //读取串口中一个字节的数据 this.tB_ReceiveDate.Invoke( //在拥有此控件的基础窗口句柄的线程上执行委托 Invoke(Delegate) //即在 textBox_ReceiveDate 控件的父窗口 form 中执行委托. new MethodInvoker( /*表示一个委托,该委托可执行托管代码中声明为 void 且不接受任何参数的任何方法。 在对控件的 I nvoke 方法进行调用时或需要一个简单委托又不想自己定义时可以使用该委托。*/ delegate{ /*匿名方法,C#2.0 的新功能,这是一种允许程序员将一段完整代码区块当成参数传递的程序代码编 写技术,通过此种方法可 以直接使用委托来设计事件响应程序以下就是你要在主线程上实现的功能但是 有一点要注意,这里不适宜处理过多的方法,因为 C#消息机制是消息流水线响应机制,如果这里在主线 程上处理语句的时间过长会导致主 UI 线程阻塞,停止响应或响应不顺畅,这时你的主 form 界面会延迟或 卡死 */ this.tB_ReceiveDate.AppendText(SDateTemp.ToString());// 输出到主窗口文本控件 this.tB_ReceiveDate.Text += & &;} ) ); } 如何知道当前电脑有哪个串口 在窗体上添加一个 comboBox 控件。 然后使用 comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames()); 或者 string[] portList = System.IO.Ports.SerialPort.GetPortNames(); for (int i = 0; i & portList.L ++i) { string name = portList[i]; comboBox1.Items.Add(name); } [简介] 本文将介绍如何在.NET 平台下使用 C#创建串口通信程序,.NET 2.0 提供了串口通信的功能,其命名 空间是 System.IO.Ports。这个新的框架不但可以访问计算机上的串口,还可以和串口设备进行通信。 我们将使用标准的 RS 232 C 在 PC 间通信。它工作在全双工模式下,而且我们不打算使用任何的握手 或流控制器,而是使用无 modem 连接。 命名空间 System.IO.Ports 命名空间中最重用的是 SerialPort 类。 创建 SerialPort 对象 通过创建 SerialPort 对象,我们可以在程序中控制串口通信的全过程。 我们将要用到的 SerialPort 类的方法: ReadLine():从输入缓冲区读一新行的值,如果没有,会返回 NULL WriteLine(string):写入输出缓冲 Open():打开一个新的串口连接 Close():关闭 Code: //create a Serial Port object SerialPort sp = new SerialPort ();默认情况下,DataBits 值是 8,StopBits 是 1,通信端口是 COM1。这些都可以在下面的属性中重新设置 : BaudRate:串口的波特率 StopBits:每个字节的停止位数量 ReadTimeout:当读操作没有完成时的停止时间。单位,毫秒 还有不少其它公共属性,自己查阅 MSDN。 串口的硬件知识 在数据传输的时候,每个字节的数据通过单个的电缆线传输。包包括开始位,数据,结束为。一旦 开始位传出,后面就会传数据,可能是 5,6,7 或 8 位,就看你的设定了。发送和接收必须设定同样 的波特率和数据位数。 无猫模式 没有 Modem 模式的电缆只是简单地交叉传送和接收线。同样 DTR & DSR, 和 RTS & CTS 也需要交叉。 RS232 针图这里,我们三条线。互连 2 和 3(一段的 2pin 连接 3pin) ,连接两端的 5pin。 [示例程序] 主程序 如果想使用默认属性,按“Save Status”按钮,如果想改变属性按“Property” 。它会弹出下图:设定好之后,可以通信了。 主窗口的代码 Code: #region Using directives using S using System.Collections.G using System.ComponentM using System.D using System.D using System.Windows.F using System.IO.P #endregion namespace Serialexpample { partial class Form1 : Form { //create instance of property page //property page is used to set values for stop bits and //baud rate PropertyPage pp = new PropertyPage(); //create an Serial Port object SerialPort sp = new SerialPort(); public Form1() { InitializeComponent(); } private void propertyButton_Click(object sender, EventArgs e) { //show property dialog pp.ShowDialog(); propertyButton.Hide(); } private void sendButton_Click(object sender, EventArgs e) { try { //write line to serial port sp.WriteLine(textBox.Text); //clear the text box textBox.Text = &&; } catch (System.Exception ex) { baudRatelLabel.Text = ex.M } } private void ReadButton_Click(object sender, EventArgs e) { try { //clear the text box textBox.Text = &&; //read serial port and displayed the data in text box textBox.Text = sp.ReadLine(); } catch(System.Exception ex) { baudRatelLabel.Text = ex.M } } private void Form1_Load(object sender, EventArgs e) { } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { MessageBox.Show(&Do u want to Close the App&); sp.Close(); } private void startCommButton_Click(object sender, EventArgs e) { startCommButton.Hide(); sendButton.Show(); readButton.Show(); textBox.Show(); } //when we want to save the status(value) private void saveStatusButton_Click_1(object sender, EventArgs e) { //display values //if no property is set the default values if (pp.bRate == && && pp.sBits == &&) { dataBitLabel.Text = &BaudRate = & + sp.BaudRate.ToString(); readTimeOutLabel.Text = &StopBits = & + sp.StopBits.ToString(); } else { dataBitLabel.Text = &BaudRate = & + pp.bR readTimeOutLabel.Text = &StopBits = & + pp.sB } parityLabel.Text = &DataBits = & + sp.DataBits.ToString(); stopBitLabel.Text = &Parity = & + sp.Parity.ToString(); readTimeOutLabel.Text = &ReadTimeout = & + sp.ReadTimeout.ToString(); if (propertyButton.Visible == true) propertyButton.Hide(); saveStatusButton.Hide(); startCommButton.Show(); try { //open serial port sp.Open(); //set read time out to 500 ms sp.ReadTimeout = 500; } catch (System.Exception ex) { baudRatelLabel.Text = ex.M } } } }属性设置对话框代码: Code: #region Using directives using S using System.Collections.G using System.ComponentM using System.D using System.D using System.T using System.Windows.F #endregion namespace Serialexpample { partial class PropertyPage : Form { //variables for storing values of baud rate and stop bits private string baudR=&&; private string stopB=&&; //property for setting and getting baud rate and stop bits public string bRate { get { return baudR; } set { baudR = } } public string sBits { get { return stopB; } set { stopB = } } public PropertyPage() { InitializeComponent(); } private void cancelButton_Click(object sender, EventArgs e) { this.bRate = &&; this.sBits = &&; //close form this.Close(); } private void okButton_Click_1(object sender, EventArgs e) { //here we set the value for stop bits and baud rate. this.bRate = BaudRateComboBox.T this.sBits = stopBitComboBox.T // this.Close(); } } }一.概述在 Visual Studio 6.0 中编写串口通讯程序,一般都使用 Microsoft Communication Control (简称 MSComm) 的通讯控件, 只要通 过对此控件的属性和事件进行相应编程操作, 就可以轻松地实现串口通讯。但在 Microsoft.Net 技术广泛应用的今天,Visual Studio.Net 没 有将此控件加入控件库, 所以人们采用了许多方法在 Visual Studio.Net 来编写串口通讯程序: 第一种方法是通过采用 Visual Studio 6.0 中原来的 MSComm 控件这是最简单的,最方便的 方法,但需要注册;第二种方法是采用微软在 .NET 推出了一个串口控件,基于 .NET 的 P/Invoke 调用方法实现;第三种方法是自己用 API 写串口通信,虽然难度高,但可以方便实 现自己想要的各种功能。 现在微软推出了最新版本的 Visual Studio 2005 开发工具,可以不再采用第三方控件 的方法来设计串口通讯程序。 NET Framework 2.0 类库包含了 SerialPort 类,方便地实现了所 需要串口通讯的多种功能,为了使 MSComm 编程方法快速转换到以 SerialPort 类为核心的 串口通讯的设计方法, 本文着重讨论了 Visual Studio 6.0 的 MSComm 控件和 SerialPort 类设 计方法的异同点。 二.SerialPort 常用属性、方法和事件 1.命名空间 System.IO.Ports 命名空间包含了控制串口重要的 SerialPort 类,该类提供了同步 I/O 和事件驱动的 I/O、对管脚和中断状态的访问以及对串行驱动程序属性的访问,所以在程序 代码起始位置需加入 Using System.IO.Ports。 2.串口的通讯参数 串口通讯最常用的参数就是通讯端口号及通讯格式(波特率、数据位、停止位和校验 位),在 MSComm 中相关的属性是 CommPort 和 Settings。SerialPort 类与 MSComm 有一些 区别: a.通讯端口号 [PortName]属性获取或设置通信端口,包括但不限于所有可用的 COM 端口,请注意 该属性返回类型为 String,不是 Mscomm.CommPort 的 short 类型。通常情况下,PortName 正常返回的值为 COM1、 COM2……,SerialPort 类最大支持的端口数突破了 CommPort 控件 中 CommPort 属性不能超过 16 的限止,大大方便了用户串口设备的配置。 b. 通讯格式 SerialPort 类对分别用[BaudRate]、[Parity]、[DataBits]、[StopBits]属性设置通讯格式中 的波特率、数据位、停止位和校验位,其中 [Parity] 和 [StopBits] 分别是枚举类型 Parity、 StopBits, Parity 类型中枚举了 Odd(奇)、Even(偶)、 Mark、None、 Space, Parity 枚举了 None、 One、OnePointFive、Two。 SerialPort 类提供了七个重载的构造函数,既可以对已经实例化的 SerialPort 对象设置 上述相关属性的值, 也可以使用指定的端口名称、 波特率和奇偶校验位数据位和停止位直接 初始化 SerialPort 类的新实例。 3.串口的打开和关闭 SerialPort 类没有采用 MSComm.PortOpen=True/False 设置属性值打开关闭串口,相应 的是调用类的 Open()和 Close()方法。 4. 数据的发送和读取 Serial 类调用重载的 Write 和 WriteLine 方法发送数据, 其中 WriteLine 可发送字符串并 在字符串末尾加入换行符, 读取串口缓冲区的方法有许多, 其中除了 ReadExisting 和 ReadTo, 其余的方法都是同步调用,线程被阻塞直到缓冲区有相应的数据或大于 ReadTimeOut 属性 设定的时间值后,引发 ReadExisting 异常。 5.DataReceived 事件 该事件类似于 MSComm 控件中的 OnComm 事件, DataReceived 事件在接收到了 [ReceivedBytesThreshold] 设置的字符个数或接收到了文件结束字符并将其放入了输入缓冲 区时被触发。其中[ReceivedBytesThreshold]相当于 MSComm 控件的[Rthreshold]属性,该事 件的用法与 MsComm 控件的 OnComm 事件在 CommEvent 为 comEvSend 和 comEvEof 时是 一致的。 三.SerialPort 的使用 对于熟悉 MSComm 控件的程序设计者,SerialPort 类是相当容易上手的。在进行串口 通讯时,一般的流程是设置通讯端口号及波特率、数据位、停止位和校验位,再打开端口连 接,发送数据,接收数据,最后关闭端口连接这样几个步骤。 数据接收的设计方法在这里比较重要, 采用轮询的方法比较浪费时间, 在 Visual Basic 中的延时方法中一般会调用 API 并用 DOEvents 方法来处理,但程序不易控制,建议采用 DataReceived 事件触发的方法,合理的设置 ReceivedBytesThreshold 的值,若接收的是定长 的数据,则将 ReceivedBytesThreshold 设为接收数据的长度,若接收数据的结尾是固定的字 符或字符串则可采用 ReadTo 的方法或在 DataReceived 事件中判断接收的字符是否满足条 件。 SerialPort 类读取数据的许多方法是同步阻塞调用,尽量避免在主线程中调用,可以 使用异步处理或线程间处理调用这些读取数据的方法。 由于 DataReceived 事件在辅线程被引发, 当收到完整的一条数据, 返回主线程处理或 在窗体上显示时,请注意跨线程的处理,C#可采用控件异步委托的方法 Control.BeginInvoke 及同步委托的方法 Invoke。 四.结束语 在.NET 平台下熟练使用 SerialPort 类,可以很好地开发出串口通讯类程序,对于过 去使用 MSComm 控件设计了一些通讯程序,也可以将 MSComm 控件替换为 SerialPort 类, 当然为了避免对以前的项目做大的改动,可以使用 SerialPort 类设计一些与 MSComm 控件 具有相同接口的类,在今后工业控制中,SerialPort 类将广泛地应用于串口通讯程序的设计 中,发挥着与 MSComm 控件一样的作用。2.以类的方式 VB: 1) 定义 SerialPort 类实例 Dim SpCom As New System.IO.Ports.SerialPort() 2)设置通讯端口号及波特率、数据位、停止位和校验位。 SpCom.PortName = &COM1& SpCom.BaudRate = 9600 SpCom.Parity = IO.Ports.Parity.None SpCom.DataBits = 8 SpCom.StopBits = IO.Ports.StopBits.One 或是定义时直接初始化 Dim SpCom As New System.IO.Ports.SerialPort(&COM1&, 9600, IO.Ports.Parity.Even, 8, IO.Ports.StopBits.None)3) 4)发送数据 SpCom.Write(TextSendData.Text) 添加接受事件 a) 在运行时将事件与事件处理程序相关联 AddHandler SpCom.DataReceived, AddressOf EventReceiveData 说明: AddressOf 创建引用特定过程的过程委托实例 AddressOf 运算符可以用作委托构造函数的操作数, 或可以用在编译器能够确定委 型的上下文中。 b) 添加事件处理程序(签名一定要一致) Sub EventReceiveData(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) 托类5)读取数据 Dim strT As String strT = SpCom.ReadExisting()C#: 1) 2) 3) 添加引用 using System.IO.P 定义 SerialPort 类实例 private SerialPort SpCom2 = new SpCom (&COM2&, 9600,Parity.None, 8, StopBits.One); 设置通讯端口号及波特率、数据位、停止位和校验位。 SpCom.PortName = &COM1&; SpCom.BaudRate = 9600; SpCom.Parity = IO.Ports.Parity.N SpCom.DataBits = 8; SpCom.StopBits = IO.Ports.StopBits.O 或是定义时直接初始化 private SerialPort SpCom2 = new SpCom (&COM2&, 9600,Parity.None, 8, StopBits.One); 4) 5) 发送数据 SpCom.Write(TextSendData.Text); 添加接受事件 a) 在运行时将事件与事件处理程序相关联(通过委托实现) SpCom.DataReceived += new SerialDataReceivedEventHandler(SpCom2_DataReceived); 说明: SerialDataReceivedEventHandler 委托 表示将处理 SerialPort 对象的 DataReceived事件的方法b) 添加事件处理程序(签名一定要一致) private void SpCom_DataReceived(object sender, SerialDataReceivedEventArgs e) 6) 读取数据 string data = SpCom .ReadExisting();我假设读者已经了解了 c#的语法,本文是针对刚打算解除串口编程的朋友阅读的,作为串 口编程的入门范例,也是我这个系列的基础。 我们的开发环境假定为 vs2005(虽然我在用 vs2010,但避免有些网友用 2005,不支持 lambda,避免不兼容,就用 2005 来做例子)一个基本的串口程序,既然是个程序了。我们就先从功能说起,包含 串口选择 波特率选择 打开 关闭 接受数据显示 发送数据输入 发送数据 数据量提示以及归零 好吧,有了这些功能,我们就先画出界面。例如: 这里,波特率就定死几种好了。直接界面上添加 00,, comboPortName 这里,为了我们的软件能通用所有电脑避免每次查询的效率损失,我们使 用微软提供的枚举方式,代码如下:[c-sharp] view plaincopyprint? 1. string[] ports = SerialPort.GetPortNames(); 2. Array.Sort(ports); 3. comboPortName.Items.AddRange(ports); string[] ports = SerialPort.GetPortNames(); Array.Sort(ports); comboPortName.Items.AddRange(ports);显然,我们需要定义一个 SerialPort 对象。添加 DataReceived 事件响应收到数据,还有一 个重点,我们需要记得设置 NewLine 属性哦。好想有的版本不设置的时候,WriteLine 和 Write 效果一样。所以,我们需要初始化 SerialPort 对象,例如: [c-sharp] view plaincopyprint? 1. //初始化 SerialPort 对象 2. comm.NewLine = &/r/n&; 3. comm.RtsEnable =//根据实际情况吧。 4. //添加事件注册 5. comm.DataReceived += comm_DataR//初始化SerialPort对象 comm.NewLine = &/r/n&; comm.RtsEnable =//根据实际情况吧。 //添加事件注册初始化好串口,简单的编写打开,关闭方法,编写界面响应的是否自动换行,如何复位计 数器,发送方法。以及数据处理。因为我已经写了完整注视,我就直接贴代码了。[c-sharp] view plaincopyprint? 1. using S 2. using System.Collections.G 3. using System.ComponentM 4. using System.D 5. using System.D 6. using System.L 7. using System.T 8. using System.Windows.F 9. using System.IO.P 10. using System.Text.RegularE 11. namespace SerialportSample 12. { 13. 14. 15. 16. public partial class SerialportSampleForm : Form { private SerialPort comm = new SerialPort(); private StringBuilder builder = new StringBuilder();//避免在事件处理方法中反 复的创建,定义到外面。 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. -1; 31. comboBaudrate.SelectedIndex = comboBaudrate.Items.IndexOf(&9600&); } //窗体初始化 private void Form1_Load(object sender, EventArgs e) { //初始化下拉串口名称列表框 string[] ports = SerialPort.GetPortNames(); Array.Sort(ports); comboPortName.Items.AddRange(ports); comboPortName.SelectedIndex = comboPortName.Items.Count & 0 ? 0 : private long received_count = 0;//接收计数 private long send_count = 0;//发送计数 public SerialportSampleForm() { InitializeComponent();32.//初始化 SerialPort 对象 33. 34. 35. 36. 37. 38. 39. 40. }comm.NewLine = &/r/n&; comm.RtsEnable =//根据实际情况吧。 //添加事件注册 comm.DataReceived += comm_DataRvoid comm_DataReceived(object sender, SerialDataReceivedEventArgs e) { int n = comm.BytesToR//先记录下来,避免某种原因,人为的原因,操 作几次之间时间长,缓存不一致41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66.byte[] buf = new byte[n];//声明一个临时数组存储当前来的串口数据 received_count +=//增加接收计数 comm.Read(buf, 0, n);//读取缓冲数据 builder.Clear();//清除字符串构造器的内容 //因为要访问 ui 资源,所以需要使用 invoke 方式同步 ui。 this.Invoke((EventHandler)(delegate { //判断是否是显示为 16 禁止 if (checkBoxHexView.Checked) { //依次的拼接出 16 进制字符串 foreach (byte b in buf) { builder.Append(b.ToString(&X2&) + & &); } } else { //直接按 ASCII 规则转换成字符串 builder.Append(Encoding.ASCII.GetString(buf)); } //追加的形式添加到文本框末端,并滚动到最后。 this.txGet.AppendText(builder.ToString()); //修改接收计数 labelGetCount.Text = &Get:& + received_count.ToString(); })); 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91. 92. 93. 94. 95. 96. 97. 98.} private void buttonOpenClose_Click(object sender, EventArgs e) { //根据当前串口对象,来判断操作 if (comm.IsOpen) { //打开时点击,则关闭串口 comm.Close(); } else { //关闭时点击,则设置好端口,波特率后打开 comm.PortName = comboPortName.T comm.BaudRate = int.Parse(comboBaudrate.Text); try { comm.Open(); } catch(Exception ex) { //捕获到异常信息,创建一个新的 comm 对象,之前的不能用了。 comm = new SerialPort(); //现实异常信息给客户。 MessageBox.Show(ex.Message); } } //设置按钮的状态 buttonOpenClose.Text = comm.IsOpen ? &Close& : &Open&; buttonSend.Enabled = comm.IsO } //动态的修改获取文本框是否支持自动换行。 private void checkBoxNewlineGet_CheckedChanged(object sender, EventAr gs e)99. 100.{ txGet.WordWrap = checkBoxNewlineGet.C 101. 102. 103. 104. 105. 106. 107. 108. 109.} private void buttonSend_Click(object sender, EventArgs e) { //定义一个变量,记录发送了几个字节 int n = 0; //16 进制发送 if (checkBoxHexSend.Checked) { //我们不管规则了。如果写错了一些,我们允许的,只用正则得到有效的十六进制数 110. 111. 112. 113. 114. 115. 116. 117. 118. 119. 120. 121. 122. 123. 124. 125. 126. 127. 128. 129. 130. 131. 132. 133. 134. } } else//不包含换行符 { comm.Write(txSend.Text); n = txSend.Text.L } else//ascii 编码直接发送 { //包含换行符 if (checkBoxNewlineSend.Checked) { comm.WriteLine(txSend.Text); n = txSend.Text.Length + 2; } //转换列表为数组后发送 comm.Write(buf.ToArray(), 0, buf.Count); //记录发送的字节数 n = buf.C MatchCollection mc = Regex.Matches(txSend.Text, @&(?i)[/da-f]{2}&); List&byte& buf = new List&byte&();//填充到这个临时列表中 //依次添加到列表中 foreach (Match m in mc) { buf.Add(byte.Parse(m.Value)); 135. 136. 137. 138. 139. 140. 141. 142. 143. 144. 145. 146. 147. } } } }} send_count +=//累加发送字节数 labelSendCount.Text = &Send:& + send_count.ToString();//更新界面private void buttonReset_Click(object sender, EventArgs e) { //复位接受和发送的字节数计数器并更新界面。 send_count = received_count = 0; labelGetCount.Text = &Get:0&; labelSendCount.Text = &Send:0&;using S using System.Collections.G using System.ComponentM using System.D using System.D using System.L using System.T using System.Windows.F using System.IO.P using System.Text.RegularE namespace SerialportSample { public partial class SerialportSampleForm : Form { private SerialPort comm = new SerialPort(); private StringBuilder builder = new S t r i n g B u i l d e r ( ) ; / / 避免在事件处理方法中反复的 private long received_count = 0 ; / / 接收计数 private long send_count = 0 ; / / 发送计数 public SerialportSampleForm() { InitializeComponent(); } / / 窗体初始化 private void Form1_Load(object sender, EventArgs e) { / / 初始化下拉串口名称列表框 string[] ports = SerialPort.GetPortNames(); Array.Sort(ports); comboPortName.Items.AddRange(ports); comboPortName.SelectedIndex = comboPortName.Items.Count & 0 ? 0 : -1; comboBaudrate.SelectedIndex = comboBaudrate.Items.IndexOf(&9600&); / / 初始化S e r i a l P o r t 对象 comm.NewLine = &/r/n&; comm.RtsEnable = / / 根据实际情况吧。 / / 添加事件注册 comm.DataReceived += comm_DataR } void comm_DataReceived(object sender, SerialDataReceivedEventArgs e) { int n = c o m m . B y t e s T o R / / 先记录下来,避免某种原因,人为的原因,操作几次之间 byte[] buf = new b y t e [ n ] ; / / 声明一个临时数组存储当前来的串口数据 received_count += / / 增加接收计数 comm.Read(buf, 0, n ) ; / / 读取缓冲数据 b u i l d e r . C l e a r ( ) ; / / 清除字符串构造器的内容 / / 因为要访问u i 资源,所以需要使用i n v o k e 方式同步u i 。 this.Invoke((EventHandler)(delegate { / / 判断是否是显示为1 6 禁止 if (checkBoxHexView.Checked) { / / 依次的拼接出1 6 进制字符串 foreach (byte b in buf) { builder.Append(b.ToString(&X2&) + & &); } } else { / / 直接按A S C I I 规则转换成字符串 builder.Append(Encoding.ASCII.GetString(buf)); } / / 追加的形式添加到文本框末端,并滚动到最后。 this.txGet.AppendText(builder.ToString()); / / 修改接收计数 labelGetCount.Text = &Get:& + received_count.ToString(); })); } private void buttonOpenClose_Click(object sender, EventArgs e) { / / 根据当前串口对象,来判断操作 if (comm.IsOpen) { / / 打开时点击,则关闭串口 comm.Close(); } else { / / 关闭时点击,则设置好端口,波特率后打开 comm.PortName = comboPortName.T comm.BaudRate = int.Parse(comboBaudrate.Text); try { comm.Open(); } catch(Exception ex) { / / 捕获到异常信息,创建一个新的c o m m 对象,之前的不能用了。 comm = new SerialPort(); / / 现实异常信息给客户。 MessageBox.Show(ex.Message); } } / / 设置按钮的状态 buttonOpenClose.Text = comm.IsOpen ? &Close& : &Open&; buttonSend.Enabled = comm.IsO } / / 动态的修改获取文本框是否支持自动换行。 private void checkBoxNewlineGet_CheckedChanged(object sender, EventArgs e) { txGet.WordWrap = checkBoxNewlineGet.C } private void buttonSend_Click(object sender, EventArgs e) { / / 定义一个变量,记录发送了几个字节 int n = 0; / / 1 6 进制发送 if (checkBoxHexSend.Checked) { / / 我们不管规则了。如果写错了一些,我们允许的,只用正则得到有效的十六进制数 MatchCollection mc = Regex.Matches(txSend.Text, @&(?i)[/da-f]{2}&); List&byte& buf = new L i s t & b y t e & ( ) ; / / 填充到这个临时列表中 / / 依次添加到列表中 foreach (Match m in mc) { buf.Add(byte.Parse(m.Value)); } / / 转换列表为数组后发送 comm.Write(buf.ToArray(), 0, buf.Count); / / 记录发送的字节数 n = buf.C } e l s e / / a s c i i 编码直接发送 { / / 包含换行符 if (checkBoxNewlineSend.Checked) { comm.WriteLine(txSend.Text); n = txSend.Text.Length + 2; } e l s e / / 不包含换行符 { comm.Write(txSend.Text); n = txSend.Text.L } } send_count += / / 累加发送字节数 labelSendCount.Text = &Send:& + s e n d _ c o u n t . T o S t r i n g ( ) ; / / 更新界面 } private void buttonReset_Click(object sender, EventArgs e) { / / 复位接受和发送的字节数计数器并更新界面。 send_count = received_count = 0; labelGetCount.Text = &Get:0&; labelSendCount.Text = &Send:0&; } } }至此,一个标准的串口调试助手就完成了。留下一个思考题,如果接收数据后,更新界面的 时候,尚未操作完成,此时并发了关闭串口的操作。程序会如何呢?敬请阅读《C# 串口操 作系列(2) -- 如何避免关闭串口偶尔软件死锁》。VS2010 项目范例下载 VS2008 项目范例下载 //append by wuyazhe @ 上面有一点疏漏,源自第一篇,结果到这里还是没修改,源码中有一行,需要修改一下。 //发送按钮中 buf.Add(byte.Parse(m.Value)); 要修改为 buf.Add(byte.Parse(m.Value,System.Globalization.NumberStyles.HexNumber)); 第一篇文章我相信很多人不看都能做的出来,但是,用过微软 SerialPort 类的人,都遇到过 这个尴尬,关闭串口的时候会让软件死锁。天哪,我可不是武断,算了。不要太绝对了。 99.9%的人吧,都遇到过这个问题。我想只有一半的人真的解决了。另外一半的人就睁只眼 闭只眼阿弥佗佛希望不要在客户那里出现这问题了。你看到我的文章,就放心吧,这问题有救了。我们先回顾一下上一篇中的代码[c-sharp] view plaincopyprint? 1. void comm_DataReceived(object sender, SerialDataReceivedEventArgs e) 2. { 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. //先记录下来,避免某种原因,人为的原因,操作几次之间时间长,缓存不一致 int n = comm.BytesToR //声明一个临时数组存储当前来的串口数据 byte[] buf = new byte[n]; //增加接收计数 received_count += //读取缓冲数据 comm.Read(buf, 0, n); //清除字符串构造器的内容 builder.Clear(); //因为要访问 ui 资源,所以需要使用 invoke 方式同步 ui。 14. 15. } 16.this.Invoke((EventHandler)(delegate{...界面更新,略}));17. private void buttonOpenClose_Click(object sender, EventArgs e) 18. { 19. 20. 21. 22. 23. 24. 25. 26. 27. }void comm_DataReceived(object sender, SerialDataReceivedEventArgs e) { //先记录下来,避免某种原因,人为的原因,操作几次之间时间长,缓存不一致 int n = comm.BytesToR //声明一个临时数组存储当前来的串口数据 byte[] buf = new byte[n]; //增加接收计数 received_count += //读取缓冲数据 comm.Read(buf, 0, n); //清除字符串构造器的内容 builder.Clear(); //因为要访问ui资源,所以需要使用invoke方式同步ui。 this.Invoke((EventHandler)(delegate{...界面更新,略})); } private void buttonOpenClose_Click(object sender, EventArgs e) { //根据当前串口对象,来判断操作 if (comm.IsOpen) { //打开时点击,则关闭串口 comm.Close();//这里就是可能导致软件死掉的地方 } else {...} }//根据当前串口对象,来判断操作 if (comm.IsOpen) { //打开时点击,则关闭串口 comm.Close();//这里就是可能导致软件死掉的地方 } else {...}为什么会死锁呢,并发冲突。 我们要了解一下 SerialPort 的实现和串口通讯机制,在你打开串口的时候,SerialPort 会创建一个监听线程 ListenThread,在这个线程中,等待注册的串口中断,当收到中断后, 会调用 DataReceived 事件。调用完成后,继续进入循环等待,直到串口被关闭退出线程。 我们的 UI 主线程如何做的呢,首先创建一个窗体,然后执行了 Application.Run(窗体实 例)。 是这样把, 这里的 Application.Run 就是创建了一个消息循环, 循环的处理相关的消息。 这里我们就有了 2 个线程,UI 主线程、串口监听线程。那么你在 DataReceived 处理数 据的时候,就需要线程同步,避免并发冲突,什么是并发冲突?并发冲突就是 2 个或多个 并行(至少看上去像)的线程运行的时候,多个线程共同的操作某一线程的资源,在时序上同 时或没有按我们的预计顺序操作,这样就可能导致数据混乱无序或是彼此等待完成死锁软 件。 而串口程序大多是后者。为什么呢,看看我们的例子中 DataReceived 做了什么?首先 读取数据,然后就是调用 this.Invoke 方法更新 UI 了。这里 Invoke 的时候,监听线程将等 待 UI 线程的标志,等到后,开始操作 UI 的资源,当操作完成之前,监听线程也就停在 DataReceived 方法的调用这里,如果这个时候。并发了关闭串口的操作会如何呢? SerialPort 的 Close 方法,会首先尝试等待和监听线程一样的一个互斥体、临界区、或是事 件(不确定.net 用的哪种)。那这个同步对象什么时候释放呢?每次循环结束就释放,哦。 循环为什么不结束呢?因为这一次的循环操作执行到 DataReceived 之后,执行了 Invoke 去更新界面了,那 Invoke 怎么又没有执行完成呢?看上去很简单的几行代码。虽然我没仔 细研读过.net 的 Invoke 原理,但我猜测是通过消息的方式来同步的,这也是为什么这么多 的类,只有控件(窗体也是控件的一种,.net 在概念上,颠覆了微软自己的概念,传统的 win32 编程,是说所有的控件都是个 window,只是父窗体不同,表现形式不同,但都是基 于系统消息队列的,.net 出于更高的抽象,正好反过来了。呵呵)才有 Invoke 方法了。(委 托自己的 Invoke 和这个不同) 我猜测控件/窗体的 Invoke 是 SendMessage 方式实现的, 那么发送消息后就会等待消息 循环来处理消息了。如果你直接去关闭串口了。你点击按钮本身也会被转换成消息 WM_CLICK,消息循环在处理按钮的 WM_CLICK 时候,调用你按钮的 OnClick 方法,进 而触发调用你的 ButtonClose_Click 事件,这都是同步调用的,你的主线程,处理消息的过 程,停在了这个 Click 事件,而你的 Click 事件又去调用了 SerialPort 的 Close 方法,Close 方法又因为和串口监听线程的同步信号量关联在一起需要等待一次的 while 结束,而这个 while 循环中调用了 DataReceived 方法,这个方法中调用了 Invoke,也就是发送了消息到 消息队列等待结果,但消息循环正在处理你的关闭按钮事件等待退出。 实在太复杂了, 这个情况下, 你想要真的关闭串口成功, 就需要 while 中的 DataReceived 方法调用结束释放同步信号,就需要执行完 Invoke,就需要执行消息循环,幸运的是,我 们真的有办法执行消息循环来打破僵局。Application.DoEvents()。还好,不幸中的万幸。 可是问题又来了,你能让 Invoke 结束,但你无法确定是否在你调用消息循环后,你的某一 时刻不会再次并发,可能由于单 cpu 的串行操作模拟并行中,又把时间片先分给了优先级 高的串口监听线程呢?是有可能的。所以,我们就需要一点方法来避免再次 invoke 窗体。 优化后不会司机的例子如下,我们修改 DataReceived 方法,关闭方法,并定义 2 个标记 Listening 和 Closing。[c-sharp] view plaincopyprint? 1. using S 2. using System.Collections.G 3. using System.ComponentM 4. using System.D 5. using System.D 6. using System.L 7. using System.T 8. using System.Windows.F 9. using System.IO.P 10. using System.Text.RegularE 11. namespace SerialportSample 12. { 13. 14. 15. 16. public partial class SerialportSampleForm : Form { private SerialPort comm = new SerialPort(); private StringBuilder builder = new StringBuilder();//避免在事件处理方法中反 复的创建,定义到外面。 17. 18. 19. 20. private long received_count = 0;//接收计数 private long send_count = 0;//发送计数 private bool Listening =//是否没有执行完 invoke 相关操作 private bool Closing =//是否正在关闭串口, 执行 Application.DoEvents, 并阻止再次 invoke 21. public SerialportSampleForm() 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. -1; 33.{ InitializeComponent(); } //窗体初始化 private void Form1_Load(object sender, EventArgs e) { //初始化下拉串口名称列表框 string[] ports = SerialPort.GetPortNames(); Array.Sort(ports); comboPortName.Items.AddRange(ports); comboPortName.SelectedIndex = comboPortName.Items.Count & 0 ? 0 :comboBaudrate.SelectedIndex = comboBaudrate.Items.IndexOf(&9600&);34. 35. 36. 37. 38. 39. 40. 41. 42. }//初始化 SerialPort 对象 comm.NewLine = &/r/n&; comm.RtsEnable =//根据实际情况吧。 //添加事件注册 comm.DataReceived += comm_DataRvoid comm_DataReceived(object sender, SerialDataReceivedEventArgs e) { if (Closing)//如果正在关闭,忽略操作,直接返回,尽快的完成串口 监听线程的一次循环43. 44. 45.try { Listening =//设置标记,说明我已经开始处理数据,一会儿要使用系 统 UI 的。46.int n = comm.BytesToR//先记录下来,避免某种原因,人为的原因, 操作几次之间时间长,缓存不一致47. 48. 49. 50. 51.byte[] buf = new byte[n];//声明一个临时数组存储当前来的串口数据 received_count +=//增加接收计数 comm.Read(buf, 0, n);//读取缓冲数据 builder.Clear();//清除字符串构造器的内容 //因为要访问 ui 资源,所以需要使用 invoke 方式同步 ui。 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. } } }this.Invoke((EventHandler)(delegate { //判断是否是显示为 16 禁止 if (checkBoxHexView.Checked) { //依次的拼接出 16 进制字符串 foreach (byte b in buf) { builder.Append(b.ToString(&X2&) + & &); } } else { //直接按 ASCII 规则转换成字符串 builder.Append(Encoding.ASCII.GetString(buf)); } //追加的形式添加到文本框末端,并滚动到最后。 this.txGet.AppendText(builder.ToString()); //修改接收计数 labelGetCount.Text = &Get:& + received_count.ToString(); }));finally { Listening =//我用完了,ui 可以关闭串口了。private void buttonOpenClose_Click(object sender, EventArgs e) { //根据当前串口对象,来判断操作 if (comm.IsOpen) { Closing = while (Listening) Application.DoEvents(); //打开时点击,则关闭串口 87. 88. 89. 90. 91. 92. 93. 94. 95. 96. 97. 98. 99. 100. 101. 102. 103. 104. 105. 106. 107. 108. 109. 110. 111. 112. } } }comm.Close(); Closing =else { //关闭时点击,则设置好端口,波特率后打开 comm.PortName = comboPortName.T comm.BaudRate = int.Parse(comboBaudrate.Text); try { comm.Open(); } catch(Exception ex) { //捕获到异常信息,创建一个新的 comm 对象,之前的不能用了。 comm = new SerialPort(); //现实异常信息给客户。 MessageBox.Show(ex.Message); }//设置按钮的状态 buttonOpenClose.Text = comm.IsOpen ? &Close& : &Open&; buttonSend.Enabled = comm.IsO//动态的修改获取文本框是否支持自动换行。 private void checkBoxNewlineGet_CheckedChanged(object sender, EventArgs e) 113. 114. 115. 116. 117. 118. 119. 120. } private void buttonSend_Click(object sender, EventArgs e) { //定义一个变量,记录发送了几个字节 int n = 0; //16 进制发送 { txGet.WordWrap = checkBoxNewlineGet.C 121. 122. 123.if (checkBoxHexSend.Checked) { //我们不管规则了。如果写错了一些,我们允许的,只用正则得到有效的十六进制数 124. 125. 126. 127. 128. 129. 130. 131. 132. 133. 134. 135. 136. 137. 138. 139. 140. 141. 142. 143. 144. 145. 146. 147. 148. 149. 150. 151. 152. 153. 154. } private void buttonReset_Click(object sender, EventArgs e) { } send_count +=//累加发送字节数 labelSendCount.Text = &Send:& + send_count.ToString();//更新界面 } } else//不包含换行符 { comm.Write(txSend.Text); n = txSend.Text.L } else//ascii 编码直接发送 { //包含换行符 if (checkBoxNewlineSend.Checked) { comm.WriteLine(txSend.Text); n = txSend.Text.Length + 2; } //转换列表为数组后发送 comm.Write(buf.ToArray(), 0, buf.Count); //记录发送的字节数 n = buf.C MatchCollection mc = Regex.Matches(txSend.Text, @&(?i)[/da-f]{2}&); List&byte& buf = new List&byte&();//填充到这个临时列表中 //依次添加到列表中 foreach (Match m in mc) { buf.Add(byte.Parse(m.Value)); 155. 156. 157. 158. 159. 160. 161. }u s i n g S y s t u s i n g S y s t u s i n g S y s t u s i n g S y s t u s i n g S y s t u s i n g S y s t u s i n g S y s t u s i n g S y s t u s i n g S y s t u s i n g S y s t n a m e s p a c e { p u b l i c { p r p r p r p r p r p r p u {//复位接受和发送的字节数计数器并更新界面。 send_count = received_count = 0; labelGetCount.Text = &Get:0&; labelSendCount.Text = &Send:0&; } } e m . C o l l e c t i o n s . G e m . C o m p o n e n t M e m . D e m . D e m . L e m . T e m . W i n d o w s . F e m . I O . P e m . T e x t . R e g u l a r E
S e r i a l p o r t S a m p l e p a r t i a l c l a s s S e r i a l p o r t S a m p l e F o r m : F o r mi v a t e S e r i a l P o r t c o m m = n e w S e r i a l P o r t ( ) ; i v a t e S t r i n g B u i l d e r b u i l d e r = n e w S t r i n g B u i l d e r ( ) ; / / 避免在事件处理方法中反复的创 i v a t e l o n g r e c e i v e d _ c o u n t = 0 ; / / 接收计数 i v a t e l o n g s e n d _ c o u n t = 0 ; / / 发送计数 i v a t e b o o l L i s t e n i n g = / / 是否没有执行完i n v o k e 相关操作 i v a t e b o o l C l o s i n g = / / 是否正在关闭串口,执行A p p l i c a t i o n . D o E v e n t s ,并阻止 b l i c S e r i a l p o r t S a m p l e F o r m ( )}}I n i t i a l i z e C o m p o n e n t ( ) ; } / / 窗体初始化 p r i v a t e v o i d F o r m 1 _ L o a d ( o b j e c t s e n d e r , E v e n t A r g s e ) { / / 初始化下拉串口名称列表框 s t r i n g [ ] p o r t s = S e r i a l P o r t . G e t P o r t N a m e s ( ) ; A r r a y . S o r t ( p o r t s ) ; c o m b o P o r t N a m e . I t e m s . A d d R a n g e ( p o r t s ) ; c o m b o P o r t N a m e . S e l e c t e d I n d e x = c o m b o P o r t N a m e . I t e m s . C o u n t & 0 ? 0 : - 1 ; c o m b o B a u d r a t e . S e l e c t e d I n d e x = c o m b o B a u d r a t e . I t e m s . I n d e x O f ( & 9 6 0 0 & ) ; / / 初始化S e r i a l P o r t 对象 c o m m . N e w L i n e = & / r / n & ; c o m m . R t s E n a b l e = / / 根据实际情况吧。 / / 添加事件注册 c o m m . D a t a R e c e i v e d + = c o m m _ D a t a R } v o i d c o m m _ D a t a R e c e i v e d ( o b j e c t s e n d e r , S e r i a l D a t a R e c e i v e d E v e n t A r g s e ) { i f ( C l o s i n g ) / / 如果正在关闭,忽略操作,直接返回,尽快的完成串口监听线程的 t r y { L i s t e n i n g = / / 设置标记,说明我已经开始处理数据,一会儿要使用系统U I 的。 i n t n = c o m m . B y t e s T o R / / 先记录下来,避免某种原因,人为的原因,操作几次之 b y t e [ ] b u f = n e w b y t e [ n ] ; / / 声明一个临时数组存储当前来的串口数据 r e c e i v e d _ c o u n t + = / / 增加接收计数 c o m m . R e a d ( b u f , 0 , n ) ; / / 读取缓冲数据 b u i l d e r . C l e a r ( ) ; / / 清除字符串构造器的内容 / / 因为要访问u i 资源,所以需要使用i n v o k e 方式同步u i 。 t h i s . I n v o k e ( ( E v e n t H a n d l e r ) ( d e l e g a t e { / / 判断是否是显示为1 6 禁止 i f ( c h e c k B o x H e x V i e w . C h e c k e d ) { / / 依次的拼接出1 6 进制字符串 f o r e a c h ( b y t e b i n b u f ) { b u i l d e r . A p p e n d ( b . T o S t r i n g ( & X 2 & ) + & & ) ; } } e l s e { / / 直接按A S C I I 规则转换成字符串 b u i l d e r . A p p e n d ( E n c o d i n g . A S C I I . G e t S t r i n g ( b u f ) ) ; } / / 追加的形式添加到文本框末端,并滚动到最后。 t h i s . t x G e t . A p p e n d T e x t ( b u i l d e r . T o S t r i n g ( ) ) ; / / 修改接收计数 l a b e l G e t C o u n t . T e x t = & G e t : & + r e c e i v e d _ c o u n t . T o S t r i n g ( ) ; } ) ) ; } f i n a l l y { L i s t e n i n g = / / 我用完了,u i 可以关闭串口了。 } } p r i v a t e v o i d b u t t o n O p e n C l o s e _ C l i c k ( o b j e c t s e n d e r , E v e n t A r g s e ) { / / 根据当前串口对象,来判断操作 i f ( c o m m . I s O p e n ) { C l o s i n g = w h i l e ( L i s t e n i n g ) A p p l i c a t i o n . D o E v e n t s ( ) ; / / 打开时点击,则关闭串口 c o m m . C l o s e ( ) ; C l o s i n g = } e l s e { / / 关闭时点击,则设置好端口,波特率后打开 c o m m . P o r t N a m e = c o m b o P o r t N a m e . T c o m m . B a u d R a t e = i n t . P a r s e ( c o m b o B a u d r a t e . T e x t ) ; t r y { c o m m . O p e n ( ) ; } c a t c h ( E x c e p t i o n e x ) { / / 捕获到异常信息,创建一个新的c o m m 对象,之前的不能用了。 c o m m = n e w S e r i a l P o r t ( ) ; / / 现实异常信息给客户。 M e s s a g e B o x . S h o w ( e x . M e s s a g e ) ; } } / / 设置按钮的状态 b u t t o n O p e n C l o s e . T e x t = c o m m . I s O p e n ? & C l o s e & : & O p e n & ; b u t t o n S e n d . E n a b l e d = c o m m . I s O } / / 动态的修改获取文本框是否支持自动换行。 p r i v a t e v o i d c h e c k B o x N e w l i n e G e t _ C h e c k e d C h a n g e d ( o b j e c t s e n d e r , E v e n t A r g s e ) { t x G e t . W o r d W r a p = c h e c k B o x N e w l i n e G e t . C } p r i v a t e v o i d b u t t o n S e n d _ C l i c k ( o b j e c t s e n d e r , E v e n t A r g s e ) { / / 定义一个变量,记录发送了几个字节 i n t n = 0 ; / / 1 6 进制发送 i f ( c h e c k B o x H e x S e n d . C h e c k e d ) { / / 我们不管规则了。如果写错了一些,我们允许的,只用正则得到有效的十六进制数 M a t c h C o l l e c t i o n m c = R e g e x . M a t c h e s ( t x S e n d . T e x t , @ & ( ? i ) [ / d a - f ] { 2 } & ) ; L i s t & b y t e & b u f = n e w L i s t & b y t e & ( ) ; / / 填充到这个临时列表中 / / 依次添加到列表中 f o r e a c h ( M a t c h m i n m c ) { b u f . A d d ( b y t e . P a r s e ( m . V a l u e ) ) ; } / / 转换列表为数组后发送 c o m m . W r i t e ( b u f . T o A r r a y ( ) , 0 , b u f . C o u n t ) ; / / 记录发送的字节数 n = b u f . C } e l s e / / a s c i i 编码直接发送 { / / 包含换行符 i f ( c h e c k B o x N e w l i n e S e n d . C h e c k e d ) { c o m m . W r i t e L i n e ( t x S e n d . T e x t ) ; n = t x S e n d . T e x t . L e n g t h + 2 ; } e l s e / / 不包含换行符 { c o m m . W r i t e ( t x S e n d . T e x t ) ; n = t x S e n d . T e x t . L } } s e n d _ c o u n t + = / / 累加发送字节数 l a b e l S e n d C o u n t . T e x t = & S e n d : & + s e n d _ c o u n t . T o S t r i n g ( ) ; / / 更新界面 } p r i v a t e v o i d b u t t o n R e s e t _ C l i c k ( o b j e c t s e n d e r , E v e n t A r g s e ) { / / 复位接受和发送的字节数计数器并更新界面。 s e n d _ c o u n t = r e c e i v e d _ c o u n t = 0 ; l a b e l G e t C o u n t . T e x t = & G e t : 0 & ; l a b e l S e n d C o u n t . T e x t = & S e n d : 0 & ; }至此,不会再出现关闭死锁问题了。希望这篇文章能解你的燃眉之急, 非常高兴能与读者分享我层遇到, 大多数人都遇到的这个 问题。如果说的不明白,欢迎讨论。 后续的有关通讯程序底层设计的文章会讲述一个具有丰富扩展性, 但有设计简介的万能通讯 库,支持网络、蓝牙、串口通讯、并口通讯。但不要指望我都实现出来了,我只是设计出这 个框架。示例代码//append by wuyazhe @ 上面有一点疏漏,源自第一篇,结果到这里还是没修改,源码中有一行,需要修改一下。 //发送按钮中 buf.Add(byte.Parse(m.Value)); 要修改为 buf.Add(byte.Parse(m.Value,System.Globalization.NumberStyles.HexNumber)); 我们的串口程序,除了通用的,进行串口监听收发的简单工具,大多都和下位机有关,这就 需要关心我们的通讯协议如何缓存,分析,以及通知界面。 我们先说一下通讯协议。通讯协议就是通讯双方共同遵循的一套规则,定义协议的原则 是尽可能的简单以提高传输率,尽可能的具有安全性保证数据传输完整正确。基于这 2 点 规则,我们一个通讯协议应该是这样的:头+数据长度+数据正文+校验 例如:AA 44 05 01 02 03 04 05 EA 这里我假设的一条数据,协议如下: 数据头: AA 44数据长度: 05 数据正文: 01 02 03 04 05 校验: EA一般数据的校验,都会采用常用的方式,CRC16,CRC32,Xor。 有的数据安全要求高的,不允许丢包的,可能还要加入重发机制或是加入数据恢复算法, 在校验后根据前面数据添加恢复字节流以恢复数据。 我这里采用的是简单的异或校验, 包含 数据头的所有字节,依次异或得到的。 协议很简单,我也认为分析协议是很简单的事情,下面我们就如何分析协议来实际的结 合 c#看一下。 er…再等等,在我们实际开始编码之前,还有一个规则需要了解,我们有了通讯协议, 如何结合串口的协议来分析,需要关心什么呢?哦。一般就是 4 个问题:缓存收到的所有 数据,找到一条完整数据,分析数据,界面通知。 如果分的更详细一点,缓存收到的所有数据,我们想到最高效的办法就是顺序表,也就 是数组,但数组的操作比较复杂,当你使用完一条数据后,用过的需要移除;新数据如果过 多的时候,缓存过大需要清理;数据搬移等等,很有可能一个不小心就会丢数据导致软件出 些莫名其妙的小问题。个人建议,使用 List&byte&,内部是数组方式实现,每次数据不足够 的时候会扩容 1 倍,数据的增删改都已经做的很完善了。不会出现什么小问题。 找到一条完整数据,如何找到完整数据呢?就我们例子的这个协议,首先在缓存的数据 中找 AA 44,当我们找到后,探测后面的字节,发现是 05,然后看缓存剩下的数据是否足 够,不足够就不用判断,减少时间消耗,如果剩余数据&=6 个(包含 1 个字节的校验),我 们就算一个校验,看和最后的校验是否一致。 分析数据:鉴于网络的开放性,我无法确定读者对 c#的了解程度,介绍一下,常用的方 式就是 BitConvert.ToInt32 这一系列的方法,把连续的字节(和变量长度一样)读取并转换 为对应的变量。c++下使用 memcpy,或直接类型转换后进行值拷贝,vb6 下使用 CopyMemory 这个 api。 校验:前面说过了。完整性判断的时候需要和校验对比,大多系统都不太严格,不支持 重发,所以数据错误就直接丢弃。导致数据错误的原因很多,比如电磁干扰导致数据不完整 或错误、硬件驱动效率不够导致数据丢失、我们的软件缓存出错等。这些软件因素数据系统 错误,需要修改,但是电磁干扰么,有这个可能的。虽然很少。 其实我知道,就算是我,看别人的博客也是,喜欢看图片,看代码,文字性的东西,一 看就头大。那我接下来贴出基于上一篇文章的改进版本,支持协议分析(协议不能配置,可 配置的协议不是我们讨论的范畴。可以看看有 DFA(确定性有限状态机)) 我们修改一下界面,以便能显示收到后分析的数据 代码如下:[c-sharp] view plaincopyprint? 1. using S 2. using System.Collections.G 3. using System.ComponentM 4. using System.D 5. using System.D 6. using System.L 7. using System.T 8. using System.Windows.F 9. using System.IO.P 10. using System.Text.RegularE 11. namespace SerialportSample 12. { 13. 14. 15. 16. public partial class SerialportSampleForm : Form { private SerialPort comm = new SerialPort(); private StringBuilder builder = new StringBuilder();//避免在事件处理方法中反 复的创建,定义到外面。 17. 18. 19. 20. private long received_count = 0;//接收计数 private long send_count = 0;//发送计数 private bool Listening =//是否没有执行完 invoke 相关操作 private bool Closing =//是否正在关闭串口, 执行 Application.DoEvents, 并阻止再次 invoke 21. private List&byte& buffer = new List&byte&(4096);//默认分配 1 页内存, 并始终 限制不允许超过 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. -1; 35. ; 36. 37. 38. //初始化 SerialPort 对象 comm.NewLine = &/r/n&; comm.RtsEnable =//根据实际情况吧。 comboBaudrate.SelectedIndex = comboBaudrate.Items.IndexOf(&19200&) } //窗体初始化 private void Form1_Load(object sender, EventArgs e) { //初始化下拉串口名称列表框 string[] ports = SerialPort.GetPortNames(); Array.Sort(ports); comboPortName.Items.AddRange(ports); comboPortName.SelectedIndex = comboPortName.Items.Count & 0 ? 0 : private byte[] binary_data_1 = new byte[9];//AA 44 05 01 02 03 04 05 EA public SerialportSampleForm() { InitializeComponent(); 39. 40. 41. 42. 43. 44. }//添加事件注册 comm.DataReceived += comm_DataRvoid comm_DataReceived(object sender, SerialDataReceivedEventArgs e) { if (Closing)//如果正在关闭,忽略操作,直接返回,尽快的完成串口 监听线程的一次循环45. 46. 47.try { Listening =//设置标记,说明我已经开始处理数据,一会儿要使用系 统 UI 的。48.int n = comm.BytesToR//先记录下来,避免某种原因,人为的原因, 操作几次之间时间长,缓存不一致49. 50. 51. 52. 53. 54. 55. 56. 57. 58.byte[] buf = new byte[n];//声明一个临时数组存储当前来的串口数据 received_count +=//增加接收计数 comm.Read(buf, 0, n);//读取缓冲数据 ///////////////////////////////////////////////////////////////////////////////////////////////////////////// //&协议解析& bool data_1_catched =//缓存记录数据是否捕获到 //1.缓存数据 buffer.AddRange(buf); //2.完整性判断 while (buffer.Count &= 4)//至少要包含头(2 字节)+长度(1 字节)+校 验(1 字节)59. 60.{ //请不要担心使用&=,因为&=已经和&,&,=一样,是独立操作符,并不 是解析成&和=2 个符号61. 62. 63. 64.//2.1 查找数据头 if (buffer[0] == 0xAA && buffer[1] == 0x44) { //2.2 探测缓存数据是否有一条数据的字节,如果不够,就不用费劲 的做其他验证了65. 这个长度 66.//前面已经限定了剩余长度&=4,那我们这里一定能访问到 buffer[2]int len = buffer[2];//数据长度 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 据 79. 80. 81. 82. 83.//数据完整判断第一步,长度是否足够 //len 是数据段长度,4 个字节是 while 行注释的 3 部分长度 if (buffer.Count & len + 4)//数据不够的时候什么都不做 //这里确保数据长度足够,数据头标志找到,我们开始计算校验 //2.3 校验数据,确认数据正确 //异或校验,逐个字节异或得到校验码 byte checksum = 0; for (int i = 0; i & len + 3; i++)//len+3 表示校验之前的位置 { checksum ^= buffer[i]; } if (checksum != buffer[len + 3]) //如果数据校验失败,丢弃这一包数{ buffer.RemoveRange(0, len + 4);//从缓存中删除错误数据//继续下一次循环 } //至此,已经被找到了一条完整数据。我们将数据直接分析,或是缓 存起来一起分析84.//我们这里采用的办法是缓存一次,好处就是如果你某种原因,数据 堆积在缓存 buffer 中85.//已经很多了,那你需要循环的找到最后一组,只分析最新数据,过 往数据你已经处理不及时86. 87.//了,就不要浪费更多时间了,这也是考虑到系统负载能够降低。 buffer.CopyTo(0, binary_data_1, 0, len + 4);//复制一条完整数据到 具体的数据缓存88. 89. 除数据。 90. 91. 92. 93. 94. 95. } }data_1_catched = buffer.RemoveRange(0, len + 4);//正确分析一条数据,从缓存中移else { //这里是很重要的,如果数据开始不是头,则删除数据 buffer.RemoveAt(0); 96. 97. 98. 99. 100.} //分析数据 if (data_1_catched) { //我们的数据都是定好格式的,所以当我们找到分析出的数据 1,就知道固定位置一定是这些数据,}

我要回帖

更多关于 面向对象程序设计 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信