串口通信工具准备
(1)sscom5.13.1.exe: 串口调试工具
(2)VSPD: 是一种虚拟串口驱动程序,用于模拟和创建多个虚拟串口,以便在计算机间进行串口通信
VSPD 串口介绍
Sent: 0 Bytes
表示从该串口发送出去的数据字节数为 0,即目前还没有通过这个串口向外发送任何数据。
Received: 0 Bytes
表示通过该串口接收到的数据字节数为 0,即目前还没有从这个串口接收到任何数据。
Baudrate emulation: Enabled
“波特率仿真已启用”,说明当前串口的波特率仿真功能处于开启状态。波特率仿真可以模拟不同波特率下的通信场景,用于调试或测试串口通信在不同速率下的表现。
Pinout: Standard
“引脚排列:标准”,表示该串口采用的是标准的引脚定义。串口(如 RS - 232 等)有规范的引脚功能定义(如 TXD 用于发送数据、RXD 用于接收数据等),“Standard” 说明遵循了通用的标准引脚配置。
波特率
bit 与 byte
- bit 就是位,也叫比特位,是计算机中最小的单位;
- byte 是字节,也就是 B;
- 1 字节(byte)=8 位(bit)既
- 位只有两种形式 0 和 1,只能表示 2 种状态,而字节是有 8 个位组成的。可以表示 256 个状态。
- 1byte = 8 bit, 1KB= 1024 byte,1MB = 1024 KB,1G = 1024 MB,1T = 1024 G。
波特率:表示每秒传输 BIT 的位数
校验位
- 无校验(no parity)
- 奇校验(odd parity):如果字符数据位中 "1" 的数目是偶数,校验位为 "1",如果 "1" 的数目是奇数,校验位应为 "0"。(校验位调整个数)
- 偶校验(even parity):如果字符数据位中 "1" 的数目是偶数,则校验位应为 "0",如果是奇数则为 "1"。(校验位调整个数)
- mark parity:校验位始终为 1
- space parity:校验位始终为 0
数据位
- 数据位:紧跟在起始位之后,是通信中的真正有效信息。数据位的位数可以由通信双方共同约定,一般可以是 5 位、7 位或 8 位,标准的 ASCII 码是 0~127(7 位),扩展的 ASCII 码是 0~255(8 位)。传输数据时先传送字符的低位,后传送字符的高位。
停止位
- 表示单个包的最后一位。典型的值为 1,1.5 和 2 位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
private void initSerialPort()
{_serialPort = new SerialPort();_serialPort.PortName = this.cbb_comList.Text;_serialPort.BaudRate = int.Parse(this.cbb_baudRate.Text);/*** 奇偶无标志空格* */switch (this.cbb_parityBit.Text){case "奇":_serialPort.Parity = Parity.Odd;break;case "偶":_serialPort.Parity = Parity.Even;break;case "无":_serialPort.Parity = Parity.None;break;default:break;}/** 45678*/_serialPort.DataBits = int.Parse(this.cbb_dataBit.Text);// 1, 1.5, 2switch (this.cbb_stopBit.Text){case "1":_serialPort.StopBits = StopBits.One;break;case "1.5":_serialPort.StopBits = StopBits.OnePointFive;break;case "2":_serialPort.StopBits = StopBits.Two;break;default:break;}_serialPort.DataReceived += _serialPort_DataReceived;}
获取设备的串口
/// <summary>
/// 获取设备的串口
/// </summary>
private void SerialLoad()
{// 打开Windows注册表中的串口设备映射位置RegistryKey keyCon = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DEVICEMAP\SERIALCOMM");// 获取所有串口设备的值名称数组string[] serialComms = keyCon.GetValueNames();// 遍历每个串口设备foreach (var item in serialComms){// 从注册表中获取具体的串口名称(如COM1, COM2等)string name = (string)keyCon.GetValue(item);// 将串口名称添加到_comList中,并设置isOpen属性为false_comList.Add(new SerialPortList() { portName = name, isOpen = false });}
}
SerialPort 类
串口数据的接收
public int Read(byte[] buffer, int offset, int count);
public int Read(char[] buffer, int offset, int count);
public int ReadByte();
public int ReadChar();
public string ReadExisting();
public string ReadLine();
public string ReadTo(string value);
public event SerialDataReceivedEventHandler DataReceived;
串口数据的发送
public void Write(byte[] buffer, int offset, int count);
public void Write(string text);
public void Write(char[] buffer, int offset, int count);
public void WriteLine(string text);
串口数据:接收字符的处理
-
将数据接收并缓存到数据缓存区(
List<byte>
优于byte[]
) -
字符的编码格式:“GB2312”,“UTF8” 等等...
EncodingInfo[] encodingInfos = Encoding.GetEncodings();
资料:https://www.cnblogs.com/yank/p/3529395.html
-
使用 GB2312 处理接收的数据
ribreceive.AppendText(Encoding.GetEncoding("gb2312").GetString(data).Replace("\0", "\\0"));
异步线程:更新 UI
this.Invoke((EventHandler)delegate{});
执行一个异步线程来处理跨线程的数据。
DataReceived
是在辅助线程执行,数据要更新到 UI 的主线程时,这个操作就跨线程了,可以通过异步线程来执行更新。
发送数据
private void SendMessage()
{_serialPort.Write(sendBuffer.ToArray(), 0, sendBuffer.Count);sendCount += sendBuffer.Count;this.tsslab_sendCount.Text = sendCount.ToString();
}
接收数据
private void _serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{try{byte[] dataTemp = new byte[_serialPort.BytesToRead];_serialPort.Read(dataTemp, 0, dataTemp.Length);receiveBuffer.AddRange(dataTemp);receiveCount += dataTemp.Length;this.Invoke(new Action(() =>{string str = string.Empty;if (this.chb_receiveConfig_hexadecimal.Checked){// 转换为十六进制字符串显示,结果示例:"00-01"string hexDisplay = BitConverter.ToString(dataTemp);str = hexDisplay.Replace("-", " ");}else{//直接获取文本str = Encoding.GetEncoding("gb2312").GetString(dataTemp);str = str.Replace("\0", "\\0"); //处理0}this.rtb_receiveTxt.AppendText(str);}));}catch (Exception ex){// 异常处理Console.WriteLine("Error in _serialPort_DataReceived: " + ex.Message);}
}
编码问题 gb2312:
System.ArgumentException:“'gb2312' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method. (Parameter 'name')”
public Form1()
{InitializeComponent();// 注册编码提供程序以支持GB2312等编码Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);InitForm();
}
解析数据
数据大小端
大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。
小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。*/
示例说明以一个 16 位(2 字节)的整数 0x1234 为例,在内存中的存储情况如下(假设内存地址从低到高增长):
大端模式:
内存低地址处存放高位字节 0x12,内存高地址处存放低位字节 0x34。
用表格表示:
| 内存地址 | 存储内容 |
| ---- | ---- |
| 低地址 | 0x12 |
| 高地址 | 0x34 |
小端模式:
内存低地址处存放低位字节 0x34,内存高地址处存放高位字节 0x12。
用表格表示:
| 内存地址 | 存储内容 |
| ---- | ---- |
| 低地址 | 0x34 |
| 高地址 | 0x12 |对于 32 位(4 字节)整数 0x12345678,存储情况如下:
大端模式:
| 内存地址 | 存储内容 |
| ---- | ---- |
| 低地址 | 0x12 |
| | 0x34 |
| | 0x56 |
| 高地址 | 0x78 |小端模式:
| 内存地址 | 存储内容 |
| ---- | ---- |
| 低地址 | 0x78 |
| | 0x56 |
| | 0x34 |
| 高地址 | 0x12 |
不同平台的使用情况
- 大端模式:常见于一些网络设备和部分处理器架构,如 PowerPC 架构。在网络通信中,遵循大端模式的网络字节序(Network Byte Order)是标准约定,这样可以保证不同设备之间通信时数据字节顺序的一致性,例如在 TCP/IP 协议中,传输层头部中的端口号、序号等多字节数据都采用大端模式存储和传输。
- 小端模式:被 x86 架构的处理器广泛采用,像常见的个人计算机(PC)大多基于 x86 架构,使用小端模式进行数据存储 。此外,ARM 架构在默认情况下也采用小端模式。
解析数据处理
- 关键:queue 队列的先进先出逻辑
- 关键:控制协议,决定了数据解析逻辑,不同数据解析方式不同
- 案例:帧头 (0x7F)+ 数据长度 + 数据 + CRC
- 数据样本:7f+04+31323334+DE10
发送数据
接收数据
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace 串口demo1
{public static class ParsingHelper{/// <summary>/// * 解析数据/// 字节位置 | 十六进制值 | 含义/// 0 | 7F | 帧头(起始标志)/// 1 | 04 | 数据长度(有效数据字节数)/// 2-5 | 31 32 33 34 | 有效数据(负载)/// 6-7 | DE 10 | 校验码(如 CRC16)////// @param dataTemp 待解析数据/// @param bufferQueue 缓存队列/// @param frameHeader 帧头/// @return 解析后的数据/// </summary>/// <param name="dataTemp"></param>/// <param name="bufferQueue"></param>/// <param name="frameHeader"></param>/// <returns></returns>public static byte[] ParsingData(byte[] dataTemp, Queue<byte> bufferQueue, byte[] frameHeader){if (frameHeader == null){frameHeader = new byte[] { 0x7F };}bool isHeadRecive = false;int frameLength = 0;// 解析数据 queueforeach (byte item in dataTemp){// 入列bufferQueue.Enqueue(item);}// 1.解析获取帧头if (isHeadRecive == false){while (bufferQueue.Count >= frameHeader.Length){// 检查是否匹配帧头bool headerMatch = true;var queueArray = bufferQueue.ToArray();for (int i = 0; i < frameHeader.Length; i++){if (queueArray[i] != frameHeader[i]){headerMatch = false;break;}}if (headerMatch){isHeadRecive = true;Debug.WriteLine($"{BitConverter.ToString(frameHeader)} is received !!");break;}else{// 不匹配则移除第一个字节继续查找bufferQueue.Dequeue();Debug.WriteLine($"Header mismatch, Dequeue !!");}}}if (isHeadRecive){// 计算需要的最小长度: 帧头长度 + 长度字段(1字节) int minLength = frameHeader.Length + 1;if (bufferQueue.Count >= minLength){var queueArray = bufferQueue.ToArray();// 获取数据长度字段(在帧头之后)frameLength = queueArray[frameHeader.Length];Debug.WriteLine(DateTime.Now.ToLongTimeString());Debug.WriteLine($"show the data in bufferQueue{HexHelper.BytesToHexString(queueArray)}");Debug.WriteLine($"frame length ={String.Format("{0:X2}", frameLength)}");// 计算完整帧长度: 帧头 + 长度字段 + 数据 + 校验(2字节)int fullFrameLength = frameHeader.Length + 1 + frameLength + 2;if (bufferQueue.Count >= fullFrameLength){byte[] frameBuffer = new byte[fullFrameLength];Array.Copy(queueArray, 0, frameBuffer, 0, fullFrameLength);if (crc_check(frameBuffer)){Debug.WriteLine("frame is check ok, pick it");// 移除已处理的数据for (int i = 0; i < fullFrameLength; i++){bufferQueue.Dequeue();}return frameBuffer;}else{// CRC校验失败,移除帧头继续查找Debug.WriteLine("bad frame, drop it");bufferQueue.Dequeue(); // 只移除第一个字节继续查找}}}}return new byte[0];}private static bool crc_check(byte[] frameBuffer){/*大端模式: 是指数据的高字节保存在内存的低地址中,* 而数据的低字节保存在内存的高地址中,这样的存储* 模式有点儿类似于把数据当作字符串顺序处理:地址* 由小向大增加,而数据从高位往低位放;这和我们的* 阅读习惯一致。* * 小端模式: 是指数据的高字节保存在内存的高地址中,* 而数据的低字节保存在内存的低地址中,这种存储模* 式将地址的高低和数据位权有效地结合起来,高地址* 部分权值高,低地址部分权值低。*/bool ret = false;byte[] temp = new byte[frameBuffer.Length - 2];Array.Copy(frameBuffer, 0, temp, 0, temp.Length);byte[] crcdata = DataCheck.DataCrc16_Ccitt(temp, DataCheck.BigOrLittle.BigEndian);if (crcdata[0] == frameBuffer[frameBuffer.Length - 2] &&crcdata[1] == frameBuffer[frameBuffer.Length - 1]){// check okret = true;}return ret;}}
}
form.cs 接受数据
/// <summary>
/// 接收数据
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{try{byte[] dataTemp = new byte[_serialPort.BytesToRead];_serialPort.Read(dataTemp, 0, dataTemp.Length);receiveBuffer.AddRange(dataTemp);receiveCount += dataTemp.Length;this.tsslab_receiveCount.Text = receiveCount.ToString();if (!chb_parsing_start.Checked){this.Invoke(new Action(() =>{string str = string.Empty;if (this.chb_receiveConfig_hexadecimal.Checked){// 转换为十六进制字符串显示,结果示例:"00-01"string hexDisplay = BitConverter.ToString(dataTemp);str = hexDisplay.Replace("-", " ");}else{//直接获取文本str = Encoding.GetEncoding("gb2312").GetString(dataTemp);str = str.Replace("\0", "\\0"); //处理0}this.rtb_receiveTxt.AppendText(str);}));}else{//解析数据byte[] frameBuffer = ParsingHelper.ParsingData(dataTemp, bufferQueue, new byte[] { 0x7f });if (frameBuffer.Length > 0){this.Invoke(new Action(() =>{this.txt_Parsing_dataAll.Text = HexHelper.BytesToHexString(frameBuffer);this.txt_Parsing_data1.Text = String.Format("{0:X2}", frameBuffer[2]);this.txt_Parsing_data2.Text = String.Format("{0:X2}", frameBuffer[3]);this.txt_Parsing_data3.Text = String.Format("{0:X2}", frameBuffer[4]);this.txt_Parsing_data4.Text = String.Format("{0:X2}", frameBuffer[5]);}));}}}catch (Exception ex){// 异常处理Console.WriteLine("Error in _serialPort_DataReceived: " + ex.Message);}
}