本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wuyazhe/archive/2010/05/27/5627253.aspx

我们的串口程序,除了通用的,进行串口监听收发的简单工具,大多都和下位机有关,这就需要关心我们的通讯协议如何缓存,分析,以及通知界面。

我们先说一下通讯协议。通讯协议就是通讯双方共同遵循的一套规则,定义协议的原则是尽可能的简单以提高传输率,尽可能的具有安全性保证数据传输完整正确。基于这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 plaincopy
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Data;
  5. using System.Drawing;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Windows.Forms;
  9. using System.IO.Ports;
  10. using System.Text.RegularExpressions;
  11. namespace SerialportSample
  12. {
  13. public partial class SerialportSampleForm : Form
  14. {
  15. private SerialPort comm = new SerialPort();
  16. private StringBuilder builder = new StringBuilder();//避免在事件处理方法中反复的创建,定义到外面。
  17. private long received_count = 0;//接收计数
  18. private long send_count = 0;//发送计数
  19. private bool Listening = false;//是否没有执行完invoke相关操作
  20. private bool Closing = false;//是否正在关闭串口,执行Application.DoEvents,并阻止再次invoke
  21. private List<byte> buffer = new List<byte>(4096);//默认分配1页内存,并始终限制不允许超过
  22. private byte[] binary_data_1 = new byte[9];//AA 44 05 01 02 03 04 05 EA
  23. public SerialportSampleForm()
  24. {
  25. InitializeComponent();
  26. }
  27. //窗体初始化
  28. private void Form1_Load(object sender, EventArgs e)
  29. {
  30. //初始化下拉串口名称列表框
  31. string[] ports = SerialPort.GetPortNames();
  32. Array.Sort(ports);
  33. comboPortName.Items.AddRange(ports);
  34. comboPortName.SelectedIndex = comboPortName.Items.Count > 0 ? 0 : -1;
  35. comboBaudrate.SelectedIndex = comboBaudrate.Items.IndexOf("19200");
  36. //初始化SerialPort对象
  37. comm.NewLine = "/r/n";
  38. comm.RtsEnable = true;//根据实际情况吧。
  39. //添加事件注册
  40. comm.DataReceived += comm_DataReceived;
  41. }
  42. void comm_DataReceived(object sender, SerialDataReceivedEventArgs e)
  43. {
  44. if (Closing) return;//如果正在关闭,忽略操作,直接返回,尽快的完成串口监听线程的一次循环
  45. try
  46. {
  47. Listening = true;//设置标记,说明我已经开始处理数据,一会儿要使用系统UI的。
  48. int n = comm.BytesToRead;//先记录下来,避免某种原因,人为的原因,操作几次之间时间长,缓存不一致
  49. byte[] buf = new byte[n];//声明一个临时数组存储当前来的串口数据
  50. received_count += n;//增加接收计数
  51. comm.Read(buf, 0, n);//读取缓冲数据
  52. /
  53. //<协议解析>
  54. bool data_1_catched = false;//缓存记录数据是否捕获到
  55. //1.缓存数据
  56. buffer.AddRange(buf);
  57. //2.完整性判断
  58. while (buffer.Count >= 4)//至少要包含头(2字节)+长度(1字节)+校验(1字节)
  59. {
  60. //请不要担心使用>=,因为>=已经和>,<,=一样,是独立操作符,并不是解析成>和=2个符号
  61. //2.1 查找数据头
  62. if (buffer[0] == 0xAA && buffer[1] == 0x44)
  63. {
  64. //2.2 探测缓存数据是否有一条数据的字节,如果不够,就不用费劲的做其他验证了
  65. //前面已经限定了剩余长度>=4,那我们这里一定能访问到buffer[2]这个长度
  66. int len = buffer[2];//数据长度
  67. //数据完整判断第一步,长度是否足够
  68. //len是数据段长度,4个字节是while行注释的3部分长度
  69. if (buffer.Count < len + 4) break;//数据不够的时候什么都不做
  70. //这里确保数据长度足够,数据头标志找到,我们开始计算校验
  71. //2.3 校验数据,确认数据正确
  72. //异或校验,逐个字节异或得到校验码
  73. byte checksum = 0;
  74. for (int i = 0; i < len + 3; i++)//len+3表示校验之前的位置
  75. {
  76. checksum ^= buffer[i];
  77. }
  78. if (checksum != buffer[len + 3]) //如果数据校验失败,丢弃这一包数据
  79. {
  80. buffer.RemoveRange(0, len + 4);//从缓存中删除错误数据
  81. continue;//继续下一次循环
  82. }
  83. //至此,已经被找到了一条完整数据。我们将数据直接分析,或是缓存起来一起分析
  84. //我们这里采用的办法是缓存一次,好处就是如果你某种原因,数据堆积在缓存buffer中
  85. //已经很多了,那你需要循环的找到最后一组,只分析最新数据,过往数据你已经处理不及时
  86. //了,就不要浪费更多时间了,这也是考虑到系统负载能够降低。
  87. buffer.CopyTo(0, binary_data_1, 0, len + 4);//复制一条完整数据到具体的数据缓存
  88. data_1_catched = true;
  89. buffer.RemoveRange(0, len + 4);//正确分析一条数据,从缓存中移除数据。
  90. }
  91. else
  92. {
  93. //这里是很重要的,如果数据开始不是头,则删除数据
  94. buffer.RemoveAt(0);
  95. }
  96. }
  97. //分析数据
  98. if (data_1_catched)
  99. {
  100. //我们的数据都是定好格式的,所以当我们找到分析出的数据1,就知道固定位置一定是这些数据,我们只要显示就可以了
  101. string data = binary_data_1[3].ToString("X2") + " " + binary_data_1[4].ToString("X2") + " " +
  102. binary_data_1[5].ToString("X2") + " " + binary_data_1[6].ToString("X2") + " " +
  103. binary_data_1[7].ToString("X2");
  104. //更新界面
  105. this.Invoke((EventHandler)(delegate { txData.Text = data; }));
  106. }
  107. //如果需要别的协议,只要扩展这个data_n_catched就可以了。往往我们协议多的情况下,还会包含数据编号,给来的数据进行
  108. //编号,协议优化后就是: 头+编号+长度+数据+校验
  109. //</协议解析>
  110. /
  111. builder.Clear();//清除字符串构造器的内容
  112. //因为要访问ui资源,所以需要使用invoke方式同步ui。
  113. this.Invoke((EventHandler)(delegate
  114. {
  115. //判断是否是显示为16禁止
  116. if (checkBoxHexView.Checked)
  117. {
  118. //依次的拼接出16进制字符串
  119. foreach (byte b in buf)
  120. {
  121. builder.Append(b.ToString("X2") + " ");
  122. }
  123. }
  124. else
  125. {
  126. //直接按ASCII规则转换成字符串
  127. builder.Append(Encoding.ASCII.GetString(buf));
  128. }
  129. //追加的形式添加到文本框末端,并滚动到最后。
  130. this.txGet.AppendText(builder.ToString());
  131. //修改接收计数
  132. labelGetCount.Text = "Get:" + received_count.ToString();
  133. }));
  134. }
  135. finally
  136. {
  137. Listening = false;//我用完了,ui可以关闭串口了。
  138. }
  139. }
  140. private void buttonOpenClose_Click(object sender, EventArgs e)
  141. {
  142. //根据当前串口对象,来判断操作
  143. if (comm.IsOpen)
  144. {
  145. Closing = true;
  146. while (Listening) Application.DoEvents();
  147. //打开时点击,则关闭串口
  148. comm.Close();
  149. }
  150. else
  151. {
  152. //关闭时点击,则设置好端口,波特率后打开
  153. comm.PortName = comboPortName.Text;
  154. comm.BaudRate = int.Parse(comboBaudrate.Text);
  155. try
  156. {
  157. comm.Open();
  158. }
  159. catch(Exception ex)
  160. {
  161. //捕获到异常信息,创建一个新的comm对象,之前的不能用了。
  162. comm = new SerialPort();
  163. //现实异常信息给客户。
  164. MessageBox.Show(ex.Message);
  165. }
  166. }
  167. //设置按钮的状态
  168. buttonOpenClose.Text = comm.IsOpen ? "Close" : "Open";
  169. buttonSend.Enabled = comm.IsOpen;
  170. }
  171. //动态的修改获取文本框是否支持自动换行。
  172. private void checkBoxNewlineGet_CheckedChanged(object sender, EventArgs e)
  173. {
  174. txGet.WordWrap = checkBoxNewlineGet.Checked;
  175. }
  176. private void buttonSend_Click(object sender, EventArgs e)
  177. {
  178. //定义一个变量,记录发送了几个字节
  179. int n = 0;
  180. //16进制发送
  181. if (checkBoxHexSend.Checked)
  182. {
  183. //我们不管规则了。如果写错了一些,我们允许的,只用正则得到有效的十六进制数
  184. MatchCollection mc = Regex.Matches(txSend.Text, @"(?i)[/da-f]{2}");
  185. List<byte> buf = new List<byte>();//填充到这个临时列表中
  186. //依次添加到列表中
  187. foreach (Match m in mc)
  188. {
  189. buf.Add(byte.Parse(m.Value, System.Globalization.NumberStyles.HexNumber));
  190. }
  191. //转换列表为数组后发送
  192. comm.Write(buf.ToArray(), 0, buf.Count);
  193. //记录发送的字节数
  194. n = buf.Count;
  195. }
  196. else//ascii编码直接发送
  197. {
  198. //包含换行符
  199. if (checkBoxNewlineSend.Checked)
  200. {
  201. comm.WriteLine(txSend.Text);
  202. n = txSend.Text.Length + 2;
  203. }
  204. else//不包含换行符
  205. {
  206. comm.Write(txSend.Text);
  207. n = txSend.Text.Length;
  208. }
  209. }
  210. send_count += n;//累加发送字节数
  211. labelSendCount.Text = "Send:" + send_count.ToString();//更新界面
  212. }
  213. private void buttonReset_Click(object sender, EventArgs e)
  214. {
  215. //复位接受和发送的字节数计数器并更新界面。
  216. send_count = received_count = 0;
  217. labelGetCount.Text = "Get:0";
  218. labelSendCount.Text = "Send:0";
  219. }
  220. }
  221. }

至此,你只要按这个协议格式发送数据到软件打开的串口。就能在数据的data标签显示出你的数据内容,我们现在是直接显示为:

01 02 03 04 05

也就是数据段内容。

运行截图:

发送模拟数据的界面,使用通用工具SSCOMM32.exe

我们在回顾一下,一般二进制格式数据就是这样分析,分析数据长度是否足够,找到数据头,数据长度,校验,然后分析。

分析方式很多。结合各自实际情况操作,可以使用序列化方式,但是wince不支持,也可以用BitConvert方式将连续的字节读取为某个类型的变量。

希望看到这里,能给你带来帮助,欢迎大家和我讨论,希望经验丰富的朋友不吝赐教。上一篇中,有朋友说用BeginInvoke可以避免死锁问题,我暂时没有线,没有测试成功,改天测试后再公布结果。

DataReceived事件中,最高效的做法是指缓存数据,然后异步的去分析数据。但是,这样较复杂,在效率要求不是很高的情况下(大多数情况),可以在DataReceived事件中缓存数据后,立刻进行数据完整性检查,有效性检查,分析数据,以及更新界面。但这么做有一个隐患,底层串口操作的效率依赖于数据分析和界面更新,任何一个环节频繁耗时过长,都会造成数据的堆积。文章只假设都不拖时间的情况。

谢谢观赏,通讯协议分析系列,未完待续……

2010示例代码

2008示例代码

C# 串口操作系列(3) -- 协议篇,二进制协议数据解析相关推荐

  1. C# 串口操作系列(1) -- 入门篇,一个标准的,简陋的串口例子。

    我假设读者已经了解了c#的语法,本文是针对刚打算解除串口编程的朋友阅读的,作为串口编程的入门范例,也是我这个系列的基础. 我们的开发环境假定为vs2005(虽然我在用vs2010,但避免有些网友用20 ...

  2. C# 串口操作系列(1) -- 入门篇,一个标准的,简陋的串口例子

    本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wuyazhe/archive/2010/05/17/5598945.aspx 我假设读者已经了解了c#的语法,本文是针 ...

  3. C# 串口操作系列(1) -- 入门篇,一个标准的,简陋的串口例子。 ——兔子党逍遥原创,转来分享学习

    我假设读者已经了解了c#的语法,本文是针对刚打算解除串口编程的朋友阅读的,作为串口编程的入门范例,也是我这个系列的基础. 我们的开发环境假定为vs2005(虽然我在用vs2010,但避免有些网友用20 ...

  4. C# 串口操作系列(2) -- 入门篇,为什么我的串口程序在关闭串口时候会死锁 ?

    本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wuyazhe/archive/2010/05/19/5606276.aspx 第一篇文章我相信很多人不看都能做的出来, ...

  5. (一)JAVA基于OPENXML的word文档插入、合并、替换操作系列之基础篇

    (一)JAVA基于OPENXML的word文档插入.合并.替换操作系列之基础篇 前言 什么是Open Xml? Open XML SDK 这系列笔记要做点什么? 涉及技术点 关于word.openxm ...

  6. APP与后台通信数据格式的演进:从文本协议到二进制协议

    转载 http://www.52im.net/thread-1536-1-1.html 1.前言 相信活跃在即时通讯网社区的开发者们都很清楚,即时通讯应用(包括IM聊天应用.实时消息推送应用等)的流量 ...

  7. 主程的晋升攻略(5):HTTP协议和二进制协议的对比

    在上一篇< 主程的晋升攻略(4):TCP.消息分包和协议设计>中谈了协议设计的一些话题,这里补充聊聊HTTP协议和二进制协议的对比. HTTP协议是一种文本协议,也是一种Name-Base ...

  8. 文本协议与二进制协议

    感想随笔: 1.文本协议与二进制协议 个人理解: 1)文本协议是根据文本中出现某些字符来表达信息,如出现\n.<.{."等等,json xml等就是文本协议: 2)二进制协议是按照字节 ...

  9. C# 串口操作系列(5)--通讯库雏形

    串口是很简单的,编写基于串口的程序也很容易.新手们除了要面对一堆的生僻概念,以及跨线程访问的细节,还有一个需要跨越的难题,就是协议解析,上一篇已经说明了: 一个二进制格式的协议一般包含: 协议头 + ...

最新文章

  1. python爬虫项目教程_Python 爬虫速成教程,还有35个实战项目送给你
  2. spring中的BeanFactoryPostProcessor
  3. ubuntu16.04引导修复不能开机、grub丢失等——这是一剂良药
  4. SAP Spartacus里的injector
  5. 为革命,保护视力——为Eclipse更换暗黑皮肤及编辑页面的字体颜色主题
  6. android远程桌面软件毕设_向日葵远程管理软件
  7. 数据结构之交换排序:快速排序
  8. Flutter MaterialButton组件详细概述
  9. php把字符串变成多组,php把字符串变成数组(有分隔符)
  10. redis、mysql、和php原生array数组效率对比
  11. c语言实现猜数字游戏
  12. Android多点触控最佳实践
  13. vbs或vbe如何修改图标
  14. 一篇文章看明白 Activity 与 Window 与 View 之间的关系
  15. matlab 等高线密度,CASTEP获得电荷密度等高线的Matlab作图法
  16. 想要搭建自己的云主机可以怎么做
  17. Linux route命令
  18. 关于圆周卷积和fft求卷积的一些看法
  19. OpenGL显示文字--显示英文
  20. java 简单的调用类_java 调用 C# 类库搞定,三步即可,可以调用任何类及方法,很简单,非常爽啊...

热门文章

  1. 诸神战纪死神之谜java_诸神战纪2死神之谜攻略
  2. 超级计算机想象作文700字,科幻宇宙-想象作文700字
  3. 3.3. Foreign Keys
  4. C++实现的基于NSM的简易数据库
  5. 北大研究团队面向新冠疫情的数据可视化分析与模拟预测
  6. 基于智能手机的光谱仪设计
  7. vue手机端的调试神器
  8. android studio gcm,在android studio中使用GCM
  9. Oracle11gR2集群心跳单网卡改bond实施方案
  10. STM32MP157学习笔记(三) ---- A7开发环境搭建