深度剖析串口通讯(232/485)

232和485的区别:

使用C#写一套串口通讯工具,是不是232和485都能使用,是因为C#代码层面完全一样 ,发字节的方式没有任何区别,但硬件层完全不同 ,232和485是物理层的两种不同标准。

你的C#程序

┌─────────────────┐

│ 应用层/协议层 │ ← 你写的Modbus/自定义协议(完全一样)

│ (发字节逻辑) │

├─────────────────┤

│ 数据链路层 │ ← 串口参数:波特率、数据位、停止位(完全一样)

│ (UART时序) │

├─────────────────┤

│ 物理层 │ ← 232/485区别在这里!电压/布线/拓扑不同

│ (电气特性) │

└─────────────────┘

物理世界:电线上的电压信号

232和485的区别只在最底层物理层,上层UART时序、数据帧格式完全相同。

过程:

复制代码
我的程序发给COM3 → USB转485模块把字节转成485差分信号 → 485总线传输


你的程序发给COM3 → USB转232模块把字节转成232电平信号 → 232线缆传输 → 232设备接收

这是一篇Modebus的深度剖析网址:Modbus应用层协议的深度剖析-CSDN博客

串口的开启:

cs 复制代码
 lock (obLock)
 {
     if (serialPort.IsOpen)
     {
         //serialPort.DiscardInBuffer();
         //serialPort.DiscardOutBuffer();
         serialPort.Close();
     }

     serialPort.PortName = SerialData.PortName;
     serialPort.BaudRate = SerialData.BauadRade;
     serialPort.DataBits = SerialData.DataBit;
     serialPort.Parity = SerialData.parity;
     serialPort.StopBits = SerialData.stopBit;

   
     //serialPort.WriteTimeout = 3000;
     //serialPort.ReadTimeout = 100;
     serialPort.ReceivedBytesThreshold = 1;
     //serialPort.NewLine = "\r\n";
     WaitTimeNumsReadBytesExitLoop = Math.Max((ushort)3, WaitTimeNumsReadBytesExitLoop);
     serialPort.Open();
     serialPort.DiscardInBuffer();
     IsConnected = true;
     SectionId++;
     tRec = new Thread(() => ReceiveMsg(SectionId));
     tRec.IsBackground = true;
     tRec.Start();

     OnConnectedEvent();
 }

串口和TCP发送和接收的额外知识点补充:

所有通讯都是"字节流"

不存在"一次性发送一条消息"的底层机制 ,无论是串口还是 TCP。

cs 复制代码
串口 UART 底层:逐字节硬件发送
你的应用层调用:serialPort.Write(buffer, 0, 100)  ← 你想发 100 字节
           ↓
UART 硬件寄存器:1 个字节(8 bit)← 实际物理发送单元
           ↓
线路电平变化:bit by bit(起始位+数据位+校验位+停止位)
           ↓
对方 UART:逐字节接收,产生中断/放入 FIFO
           ↓
对方驱动:可能攒多个字节后才通知应用层

TCP 底层:分段(Segmentation)+ 滑动窗口
应用层:socket.Send(largeBuffer)  ← 10KB 数据
    ↓
传输层:拆分为多个 TCP Segment(受 MSS 限制,通常 1460 字节)
    ↓
    Segment 1 ──────┐
    Segment 2 ──────┼──► 网络(可能乱序、延迟、丢包)
    Segment 3 ──────┘
    ↓
对方传输层:重组为有序流,放入 Socket 缓冲区
    ↓
对方应用层:可能一次读到 3KB,可能分 10 次读,完全不确定!
串口的实际案例分析:
cs 复制代码
场景:PLC 发送 256 字节数据块,波特率 9600
理论时间:256 * 10 bit / 9600 ≈ 267ms

实际时间线:
t=0ms     [你的应用] Write 256 字节 → 驱动缓冲区
t=0-267ms [UART] 以 9600 波特率逐字节发送
t=267ms   [对方 UART] 接收完毕,但... ↓

对方应用层可能看到:
t=50ms    DataReceived 事件,BytesToRead=64   ← 驱动分批上报
t=120ms   DataReceived 事件,BytesToRead=128  ← 又累积一批
t=267ms   DataReceived 事件,BytesToRead=64   ← 最后一批

或者如果驱动延迟高:
t=267ms   一次性 BytesToRead=256  ← 看起来"完整"
TCP的实际案例分析:
cs 复制代码
场景:发送 10KB JSON
网络路径:WiFi → 路由器 → 互联网 → 服务器

结果:
- 可能分 7 个 TCP 段(受 MSS 和拥塞窗口影响)
- 可能乱序到达(段 3 比段 2 先到)
- 可能触发延迟 ACK(40ms 后才确认)
- 接收方 Read 时:先读到 4KB,再读到 6KB(或任意分割)

"间歇性到达"不是设计选择,而是物理现实。

  • 串口:波特率和 FIFO 决定最小延迟,但驱动层仍有聚合

  • TCP:网络延迟、拥塞控制、Nagle 算法导致完全不可预测的分割

应用层:必须自行定义消息边界(长度/分隔符/固定长度)

为什么上一篇笔记TCP读取只需要一次就可以,而串口需要读取多次呢?

| 误解 | 真相 |

| "TCP 读一次就行" | 框架帮你做了多次读取,或数据小碰巧读完 |

| "串口特殊需要多次读" | 串口没有标准长度协议,只能用超时猜 |

| "协议决定是否多次读" | **消息边界定义**决定,不是传输层 |
所以,其实框架底层帮我们做的包装,其实和串口多次去读取一样,也是靠超时来判断对方是否结束了发送

串口接收数据的线程

串口开启的同时,就会开启一个线程,该线程用来接收数据。看接收数据的代码:(所以接收代码里面会有两个while循环,第二个循环是为了接收一条完整的信息而存在)

cs 复制代码
/// <summary>
/// 端口接收到消息时所触发的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ReceiveMsg(int _sectionId)
{
    Thread.Sleep(100);
    byte[] buffers = new byte[1024];
    int offset = 0;
   
    int len = 0;
    int it = 0;
    bool isRecOK = false;
    ThreadRecord.Record_Start($"SerialPort: PortName={this.SerialData.PortName},Name={this.Name}");
    try
    {
        while (true)
        {
            if (IsConnected == false || _sectionId != SectionId) break;
//这个方法就是去获取缓冲区字节数的长度   这个方法不是阻塞的   需要不停的去读取
            len = serialPort.BytesToRead;
            if (len <= 0)
            {
                Thread.Sleep(10);
                continue;
            }
            try
            {

                offset = 0;

                it = 0;
                isRecOK = false;
                Array.Clear(buffers, 0, buffers.Length);

                while (true)
                {
                    if (IsConnected == false || _sectionId != SectionId) return;
                    len = serialPort.BytesToRead;
                    if (len <= 0)
                    {

                        if (it > WaitTimeNumsReadBytesExitLoop) break;
                        Thread.Sleep(10);

                        it++;
                        //break;
                    }
                    else
                    {
                        it = 0;
                       //这里才是读取数据的方法
                        serialPort.Read(buffers, offset, len);
                        offset += len;

                    }

                }

                serialPort.DiscardInBuffer();
                isRecOK = true;


            }
            catch (Exception ex)
            {
                OnRecMessageExecptionEvent($"[{this.Name} RecError]" + ex.Message);
            }
            finally
            {

            }
            RecMessageCount++;
            if (isRecOK)
            {

                isRecOK = false;
                byte[] bytes = new byte[offset];

                Array.Copy(buffers, bytes, bytes.Length);

                OnRecevieBytesBaseAction(bytes);



            }

        }


    }
    catch(Exception ex)
    {
        this.Close();
    }
    finally
    {
        IsConnected = false;
        ThreadRecord.Record_End();
    }
    

   

}

串口接收数据测试:

发送的是字符串,接收的也是字节数组:

界面刷新显示的代码:(接收到消息以后,界面显示事件调用,这就是显示事件注册的方法)

cs 复制代码
SerialPortHelper.RecevieBytesActionEvent -= SerialPortHelper_RecevieBytesActionEvent;

SerialPortHelper.RecevieBytesActionEvent += SerialPortHelper_RecevieBytesActionEvent;


private void SerialPortHelper_RecevieBytesActionEvent(object fromAddress, byte[] bytes)
{
    this.Dispatcher.Invoke(()=> 
    {
        string str = "";
        if (e_FormatShow== E_FormatShow.F_HexString)
        {
            str = $"[{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss fff")},{SerialPortHelper.RecMessageCount}]\n" + bytes.GetHexStringFromBytes();

        }
        else if (e_FormatShow == E_FormatShow.F_ByteString)
        {
            str = $"[{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss fff")},{SerialPortHelper.RecMessageCount}]\n" + string.Join(" ", bytes);
        }
        else
        {
            str = $"[{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss fff")},{SerialPortHelper.RecMessageCount}]\n" + SerialPortHelper.ParaseEncoding.GetString(bytes, 0, bytes.Length).Trim('\0');
        }

        tb_RecMsg.Text = str;
        if (btn_autosend.IsChecked==true)
        {
            SendButton_Click(null, null);
        }
    });
}

接收到的数据处理:

cs 复制代码
 string recMsg = ParaseEncoding.GetString(bytes, 0, bytes.Length).Trim('\0');
//这句代码是根据编码格式更换成对应的数据   而不是字节直接显示

串口发送消息:

发送的字符串hello,通过编码转格式以后,变成了五个字节:

发送数据的具体代码:

cs 复制代码
protected bool OnSendBytes(byte[] buffer)
{
    try
    {
        if (buffer == null) return false;
        if (serialPort.IsOpen)
        {
            lock (obLock)
            {
                int tm = (int)(DateTime.Now - dateTimeSend).TotalMilliseconds;
                int minTime = SendLockTime;
                if (tm < minTime)
                {
                    Thread.Sleep(minTime - tm + 1);
                }

                if (serialPort.IsOpen)
                {
                    serialPort.Write(buffer, 0, buffer.Length);
                }
                dateTimeSend = DateTime.Now;
                return true;
            }
        }
       
    }
    catch (Exception ex)
    {
        OnSendMessageExecption($"{this.Name} SendError:" + ex.Message);
      
        this.Close();
    }

    return false;
}

测试串口通讯的粘包:

这里就不测试了,原理和TCP的粘包一样,处理方法也是一样。

这是TCP粘包相关的链接:TCP(Socket)通讯深度剖析笔记-CSDN博客

相关推荐
新新学长搞科研2 小时前
【CCF主办 | 高认可度会议】第六届人工智能、大数据与算法国际学术会议(CAIBDA 2026)
大数据·开发语言·网络·人工智能·算法·r语言·中国计算机学会
梵刹古音4 小时前
【C语言】 字符数组相关库函数
c语言·开发语言·算法
微风中的麦穗10 小时前
【MATLAB】MATLAB R2025a 详细下载安装图文指南:下一代科学计算与工程仿真平台
开发语言·matlab·开发工具·工程仿真·matlab r2025a·matlab r2025·科学计算与工程仿真
2601_9491465310 小时前
C语言语音通知API示例代码:基于标准C的语音接口开发与底层调用实践
c语言·开发语言
开源技术10 小时前
Python Pillow 优化,打开和保存速度最快提高14倍
开发语言·python·pillow
学嵌入式的小杨同学10 小时前
从零打造 Linux 终端 MP3 播放器!用 C 语言实现音乐自由
linux·c语言·开发语言·前端·vscode·ci/cd·vim
mftang11 小时前
Python 字符串拼接成字节详解
开发语言·python
jasligea12 小时前
构建个人智能助手
开发语言·python·自然语言处理
kokunka12 小时前
【源码+注释】纯C++小游戏开发之射击小球游戏
开发语言·c++·游戏