线程安全的.NET平台串口通信程序设计研究
2012-10-13彭伟
彭 伟
(武汉城市职业学院电子信息工程学院,湖北武汉430064)
RS-232串行端口通信技术大量应用于工业控制系统,Microsoft在其.NET Framework加入了串行端口类Serial Port,以支持 Windo ws软件界面与外部设备之间的串口通信,实现数据采集及管理控制.然而,当.NET程序开发人员在串口组件数据接收事件处理程序中将读取的数据直接显示到可视化UI组件时,系统却抛出线程异常,提示当前线程与可视化组件不在同一线程上运行.通过查阅MSDN技术手册可知,.NET串口通信组件与可视化显示组件虽然处于同一窗体,但串口数据接收事件却是在辅助线程上引发,它并非与窗体中其他可视化组件同处于主线程[1].
本文将分析研究.NET Framewor k所提供的Serial Port、delegate、Thread及 Background Worker类相关技术,研究解决.NET开发环境下串口通信程序中的线程安全问题,并与以PIC微控制器为核心的8通道A/D转换模块之间进行串口数据通信与图文显示测试.
1 .NET串行端口类相关技术
1.1 串行端口类简介
.NET的System.IO.Ports命名空间包含用于控制串行端口的类,最重要的类Serial Port为同步和事件驱动I/O提供框架,提供对串口针脚和中断状态的访问以及对串行驱动程序属性的访问.
1.2 串行端口类数据接收事件处理
,由于其处理程序在辅助线程上引发,故而在事件处理程序中无法直接访问UI组件.为刷新窗体中相关组件的数据显示,根据 MSDN技术手册可知,在Data Recived事件的辅助线程内必须用Invoke激发委托来访问UI组件,它将会在恰当的线程上执行这些操作,实现线程安全调用.
2 .NET的委托类型
2.1 委托类型简介
.NET Fra mewor k定义了称为委托(delegate)的特殊的类型,该类型提供函数指针的功能,是具有相同函数属性(签名)的抽象,是一种类型安全的方法引用,它可以看成是一个类型安全的C函数指针,但与C函数指针不同的是,.NET的委托是面向对象和类型安全的,通用语言运行时(CLR)会确保一个委托指向一个有效的方法[2-3].
委托可用于封装对“命名”或“匿名”方法的引用,使用委托有三个步骤,即:声明、实例化和激发.委托类型声明的格式为:
修饰符 关键字delegate返回类型 委托类型名称(参数列表);
委托类型构造函数的参数必须是对方法(“命名”或“匿名”方法)或lambda表达式的引用.
2.2 用数据接收事件与委托实现线程安全访问
基于Serial Port类数据接收事件处理程序,借助委托实现UI组件访问,可有如下委托类型声明:public delegate void ShowCall Back(string s);
假设主线程中刷新显示的命名方法为:private void Set Text(string s){
/*访问UI刷新组件显示的代码写在这里*/}
通过委托访问Set Text,需要先实例化委托[4]:Show Call Back d= new Show Call Back(Set Text);
当Data Received事件在辅助线程上引发以后,其事件处理程序通过委托实例d即可在辅助线程中访问主线程中的Set Text方法.
.NET并不保证对接收到的每个字节引发Data Received事件,在该事件发生时,可根据Bytes To-Read属性确定缓冲区中尚未读取的数据字节数,通过循环读取数据直到Bytes To Read返回0,如此即可读取缓冲内的所有数据.
对于所选择的以PIC微控制器为核心的A/D转换模块所发送的8通道A/D值,本文约定通过串口读取的数据为形如“xxx xxx xxx xxx xxx xxx xxx xxx\r\n”的字符串,它以“\r\n”为结束标识.Data Received事件处理程序通过串口读取8通道A/D转换数据时有:String s=serial Port1.Read-Line();调用Read Line时要注意设置串口组件的New Line属性.读取数据字符串s以后,即可同步或异步激发委托实例d访问Set Text.以同步方法Invoke为例,有:
this.Invoke(d,s);
省略变量s的定义,可以写成:
this.Invoke(d,new object[]{serial Port1.Read Line()});
更为简洁的写法是:
this.Invoke(new Show Call Back(Set Text),new object[]{serial Port1.Read Line()});
图1所示的在辅助线程中通过委托访问UI的示意图描绘了上述委托定义及在辅助线程内实例化委托,并通过Invoke/BeginInvoke激发对命名方法Set Text调用的整个过程.
如果省略委托声明,还可以使用预定义委托Event Handler调用主线程中的方法,参照图1,有this.Invoke(new Event Handler(UIShow));其中UIShow可定义为
private void UIShow(object sender,Event Args e){
/*读取串口数据,访问UI刷新数据显示*/}
图1 在辅助线程中通过委托访问UI组件
如果要求辅助线程提供事件数据而不是在UIShow方法内部读取待显示串口数据,可选择使用Event Handler<TEvent Ar gs>泛型委托.
.NET还为UI组件提供了一个特殊的属性:Contr ol.Invoke Required,它既可以在主线程也可以在辅助线程内访问,根据该属性值可知调用方在对控件进行方法调用时是否必须使用Invoke方法,因为调用方可能位于创建控件所在的线程以外的线程中.借助这一属性,可以按图2所示流程设计主线程与辅助线程共用的访问UI刷新显示的方法.当辅助线程调用该方法时,代码执行形如“递归调用”,当然它与递归是毫无关系的.
图2 主/辅线程共用的UI访问方法设计
2.3 Invoke/BeginInvoke的工作原理
Invoke与BeginInvoke方法都需要以委托对象作为参数,委托类似于回调函数的地址,因此调用者通过Invoke或BeginInvoke方法可以把需要调用的函数地址封送给UI线程.使用同步方法Invoke完成一个委托方法的封送类似于使用Windows API的Send Message函数给界面线程发送消息,消息将进入消息队列(Message Queue)并等待处理.作为一个异步方法,BeginInvoke则类似于使用Post-Message函数进行通信,封送完毕后将不等待委托方法执行结束即返回,调用者线程不会被阻塞.不过调用者也可以使用EndInvoke方法或者其它类似Wait Handle的机制等待异步操作完成.
通过Reflector.exe反编译软件可知,在内部实现时,Invoke和BeginInvoke都使用了 Windows API的Post Message函数,其中前者的同步阻塞通过Wait For Wait Handle方法完成.此外,它们还引用通过循环向上回溯查找顶级父控件的Find Marshaling Control方法;使消息进入消息队列的Enqueue方法、定义一个新的视窗消息的API Register Window Message,以便通过 Send Message或Post Message发送(这里仅使用了后者);获取与指定窗口关联的进程和线程标识符的API Get Window Thread ProcessId及获取当前线程唯一线程标识符的Get Current ThreadId.图3描绘二者通过Windows的消息系统实现线程安全的UI访问过程.
图3 通过消息系统实现线程安全的UI访问
3 .NET的Thread类
3.1 Thread类简介
.NET的System.Threading命名空间提供进行多线程编程的类和接口,命名空间中的Thread类可创建并控制线程[6],线程执行的代码通过Thread Start委托或Parameterized Thread Start(带参数的Thread Start)委托指定,使用后者可以向线程过程传递数据.显然,借助Thread类可以不使用串口组件的内置Data Received事件,直接通过线程类与委托实现同样的功能.
3.2 用Thread类及委托实现线程安全访问
仍使用此前设计的显示委托,下面给出Para meterized Thread Start委托及线程类Thread实现线程安全访问的代码.以串口数据接收与UI访问刷新显示为例,其中Recv Data方法将在form加载时调用,有:
异步接收及显示数据的命名方法如下:
由于“匿名”方法也可以实例化委托,使用匿名方法时无须创建独立的方法,可减少实例化委托所需的系统开销.例如:
对于函数Recv Data,可进一步作如下改写:
4 .NET的Background worker类
4.1 Backgr ound worker类简介
System.Component Model命名空间提供用于实现组件和控件运行时及设计时行为的类,其中的Background Worker类用于在单独的线程上执行操作.图4给出了该类的Do_Wor k事件处理流程.
图4 Background Worker类事件处理流程
主线程通过Run worker Async引发Backgr ound worker对象在辅助线程上处理Do_Wor k事件以后,在辅助线程中,通过Report Progress方法可以将数据消息封送给主线程中的Progress-Changed事件处理程序;当完成所有事务处理时,也会将消息发给主线程,激发Run Worker completed事件处理程序.辅助线程上引发的Do_Wor k事件处理程序内有两条路径可以实现对主线程中UI组件的安全访问.
4.2 使用Background worker实现线程安全访问
仍以串口数据接收及主窗体UI访问为例,主窗体加载时,首先启动后台工作者:
Run worker Async使后台工作者开启辅助线程执行Do_Work,系统退出时注意在formClosing事件中停止backgr ound worker1对象的后台工作:
当后台工作者对象发生“进度变化”事件时,由事件处理流程图可知事件处理程序将在主线程上执行,故而可以直接访问UI组件:
要持续读取串口数据并直接访问UI,辅助线程需不断通过报告进度方法(Report Progress)触发“进度变化”事件.下面的代码中,辅助线程上执行的
Do_Wor k事件处理程序将每隔10 ms执行一次“进度报告”:
5 PIC微控制器A/D转换及串口通信设计
5.1 8通道A/D转换及串口数据传输电路
为测试C#程序对串口数据的接收及显示效果,图5给出了所选择的以PIC18F452微控制器为核心的通信测试模块,其中8个10位精度的A/D转换通道(AN0~AN7)分别连接了不同类型的模拟信号,图中同时给出了虚拟仿真串口组件(COMPI M)及物理串口(MAX232)[5-6].
基于HI-TECH PICC18编译器设计的C程序将循环执行8通道A/D转换,并将结果转换为约定的字符串格式,通过串口发送给上位主机程序接收并刷新图文显示.
图5 8通道A/D转换及串口数据传输电路
5.2 PIC微控制器串口配置及数据传输设计
HI-TECH PICC18提供了 USART专用库函数,例如:Open USART用于按指定配置(例如波特率设置等)打开指定串口;puts USART与gets USART分别用于发送与接收字符串.假设上位机控制PIC微控制器A/D启停的语句为:
PIC微控制器可使用相应函数读取主机C#程序发送的控制命令,选择其当前工作状态;对于已转换后的当前8通道A/D数据,则可通过相应函数向上位主机发送[7-8].
5.3 A/D通道数据采集函数设计
函数ADC_Convert()将对PIC18F452的AN0~AN7通道分别进行 A/D转换.由于 HI-TECH PICC18提供了A/D转换专用库函数,ADC_Convert()的具体实现将大为简化:
执行PIC微控制器8通道A/D转换的函数:
由于PIC18F452的A/D模块为10位精度,最大值为0B0000001111111111(即0x03FF),故保存一个通道的转换结果最多只需要3位十六进制数(即xxx).PIC微控制器主程序将循环执行A/D转换,并将ADC_Buff中的8组数据转换为形如:“xxx xxx xxx xxx xxx xxx xxxx xxx\r\n”的字符串,再通过puts USART库函数输出.
6 主机接收及图文刷新显示程序设计
实现线程安全的串口数据接收及UI访问可以有多种方法,以下选择通过Data Received事件及委托实现.下面的C#程序将在UI组件中实现8通道转换结果对应的电压值的图文显示:
辅助线程上执行的串口组件数据接收事件Data Received的处理程序内简化编写的唯一语句为:this.Invoke(new Show Call Back (Show_ADC),new object[]{serial Port1.Read Line()});8通道模拟电压数据在基于C#.NET设计的上位机UI中的图文刷新显示效果见图6.
图6 C#程序接收并刷新8通道数据显示
7 结束语
通过对 Serial Port及 delegate、Thread、Background worker类相关技术的研究,给出了.NET开发环境下线程安全的串口通信程序,并通过了与PIC18F452微控制器8通道A/D转换及串口通信模块的数据传输与图文刷新显示测试,验证了本文提出的程序设计方法的正确性与可靠性,为.NET平台的串口通信管理控制与数据传输程序设计提供了参考.
[1]Microsoft Cor poration.Serial Port类[EB/OL].[2012-01-03]http://msdn.microsoft.co m/zh-cn/library.
[2]蔡昭权.C#和C++数据传递的研究与实现[J].计算机应用与软件,2009(3):145-147.
[3]曹 文.C#程序设计语言中的委托和事件 [J].现代计算机,2008(2):72-75,81.
[4]韩志强.对C#委托内部机制的探析 [J].赤峰学院学报(自然科学版),2010(10):37-38.
[5]谢振华.PLC与上位机串口通讯的实现及应用 [J].仪器仪表用户,2011(6):59-61.
[6]杨旭东,蔡敬坤,李 娟.一种通用串口线程在C++Builder中的实现 [J].计算机测量与控制,2011(7):1 687-1 689.
[7]徐蕾璐,俞子荣.C#.NET环境下基于Serial Port实现SR23与PC机的通信 [J].计算机与现代化,2011(5):107-108.
[8]熊才高.基于PIC单片机低功耗数据采集系统的设计[J].湖北工业大学学报,2008(2):20-22.