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博客