C# - 串口助手

串口通信工具准备

(1)sscom5.13.1.exe: 串口调试工具

(2)VSPD: 是一种虚拟串口驱动程序,用于模拟和创建多个虚拟串口,以便在计算机间进行串口通信

VSPD 串口介绍

image

  1. Sent: 0 Bytes

表示从该串口发送出去的数据字节数为 0,即目前还没有通过这个串口向外发送任何数据。

  1. Received: 0 Bytes

表示通过该串口接收到的数据字节数为 0,即目前还没有从这个串口接收到任何数据。

  1. Baudrate emulation: Enabled

“波特率仿真已启用”,说明当前串口的波特率仿真功能处于开启状态。波特率仿真可以模拟不同波特率下的通信场景,用于调试或测试串口通信在不同速率下的表现。

  1. 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;
    }

    /*
     * 4
        5
        6
        7
        8
     */
    _serialPort.DataBits = int.Parse(this.cbb_dataBit.Text);
    // 1,   1.5,  2
    switch (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

发送数据

image

接收数据

image

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;
            // 解析数据 queue

            foreach (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 ok
                ret = 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);
    }
}
posted @ 2025-10-16 20:55  【唐】三三  阅读(6)  评论(0)    收藏  举报