Description
在串口读取中假设串口设备发送了75个字节,但是在大部分情况下 SerialPort.Read中会收到两次 一次可能40多个字节 一次30多个字节
Reproduction Steps
接上ttl 往串口发送75个字节的数据 波特率设置为115200,然后就可以复现
代码
using System;
using System.IO.Ports;
namespace ConsoleApp1
{
internal class Program
{
private static SerialPort _serialPort;
static void Main(string[] args)
{
// 初始化串口
_serialPort = new SerialPort("COM3", 115200);
_serialPort.DataReceived += SerialPort_DataReceived;
_serialPort.Open();
Console.WriteLine("串口已打开,等待数据...");
Console.ReadLine();
_serialPort.Close();
}
private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
// 读取串口数据
int bytesToRead = _serialPort.BytesToRead;
byte[] buffer = new byte[bytesToRead];
_serialPort.Read(buffer, 0, bytesToRead);
// 输出数据长度和十六进制表示
Console.WriteLine($"接收到 {buffer.Length} 个字节: {BitConverter.ToString(buffer)}");
}
catch (Exception ex)
{
Console.WriteLine($"数据接收错误: {ex.Message}");
}
}
}
}
此代码结果

直接使用底层api的代码
using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.VisualBasic;
using Microsoft.Win32.SafeHandles;
namespace ConsoleApp18
{
internal class Program
{
// Windows API 常量
private const uint GENERIC_READ = 0x80000000;
private const uint GENERIC_WRITE = 0x40000000;
private const uint OPEN_EXISTING = 3;
// DCB 结构体,用于配置串口参数
[StructLayout(LayoutKind.Sequential)]
private struct DCB
{
public uint DCBlength;
public uint BaudRate;
public uint Flags;
public ushort wReserved;
public ushort XonLim;
public ushort XoffLim;
public byte ByteSize;
public byte Parity;
public byte StopBits;
public byte XonChar;
public byte XoffChar;
public byte ErrorChar;
public byte EofChar;
public byte EvtChar;
public ushort wReserved1;
}
// COMMTIMEOUTS 结构体,用于设置超时
[StructLayout(LayoutKind.Sequential)]
private struct COMMTIMEOUTS
{
public int ReadIntervalTimeout;
public int ReadTotalTimeoutMultiplier;
public int ReadTotalTimeoutConstant;
public uint WriteTotalTimeoutMultiplier;
public uint WriteTotalTimeoutConstant;
}
// Windows API 函数声明
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetCommState(IntPtr hFile, ref DCB lpDCB);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetCommState(IntPtr hFile, [In] ref DCB lpDCB);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetCommTimeouts(IntPtr hFile, [In] ref COMMTIMEOUTS lpCommTimeouts);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
static void Main(string[] args)
{
string portName = "COM5"; // 替换为实际的串口名称
System.IO.FileStream stream=new FileStream($"\\\\.\\{portName}", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
if (stream == null)
{
Console.WriteLine("无法打开串口。");
return;
}
IntPtr hSerial = stream.Handle;
if (hSerial == IntPtr.Zero)
{
Console.WriteLine("无法打开串口。");
return;
}
try
{
// 配置串口参数
DCB dcb = new DCB();
dcb.DCBlength = (uint)Marshal.SizeOf(typeof(DCB));
dcb.BaudRate = 115200; // 设置波特率
dcb.ByteSize = 8; // 数据位
dcb.Parity = 0; // 无校验
dcb.StopBits = 1; // 1 个停止位
if (!SetCommState(hSerial, ref dcb))
{
int errorCode = Marshal.GetLastWin32Error();
Console.WriteLine($"设置串口状态失败,错误代码: {errorCode}");
// return;
}
// 设置超时
COMMTIMEOUTS timeouts = new COMMTIMEOUTS
{
ReadIntervalTimeout = 50,//非常重要
ReadTotalTimeoutConstant = 50,
ReadTotalTimeoutMultiplier = 100,
WriteTotalTimeoutConstant = 50,
WriteTotalTimeoutMultiplier = 10
};
if (!SetCommTimeouts(hSerial, ref timeouts))
{
Console.WriteLine("设置超时失败。");
return;
}
// 将句柄包装为 SafeFileHandle
SafeFileHandle safeHandle = new SafeFileHandle(hSerial, ownsHandle: true);
// 使用 FileStream 包装 SafeFileHandle
{
Console.WriteLine("串口已打开,开始读取数据...");
byte[] buffer = new byte[256];
while (true)
{
int bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
string data = BitConverter.ToString(buffer, 0, bytesRead);
Console.WriteLine($"接收到数据: {data}");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"发生错误: {ex.Message}");
}
finally
{
// 确保关闭句柄
CloseHandle(hSerial);
Console.WriteLine("串口已关闭。");
}
}
}
}
此代码结果

Expected behavior
希望能一次性读到对方发送的完整数据
Actual behavior
实际上收到的被拆包了


Regression?
已知的所有版本都有这个问题,
Regression?
windows的解决方案我找到了,就是SetCommTimeouts的ReadIntervalTimeout默认为-1的原因导致的,如果设置成50 就不会出现这个问题,在源码中的https://github.com/dotnet/runtime/blob/main/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs 的第373行 _commTimeouts.ReadIntervalTimeout = Interop.Kernel32.MAXDWORD; 这里将ReadIntervalTimeout值设置为了-1,建议将次参数作为属性放出来,liunx中也有此问题,但是我未找到修复方案,ai告知我也有类似的参数,建议也放出来,或者放出来串口的句柄,让开发人员有渠道去手动设置。
Known Workarounds
.net 全版本
系统 windows10 11、liunx arm32 arm64 其它系统未测试
Configuration
No response
Other information
No response
Description
在串口读取中假设串口设备发送了75个字节,但是在大部分情况下 SerialPort.Read中会收到两次 一次可能40多个字节 一次30多个字节
Reproduction Steps
接上ttl 往串口发送75个字节的数据 波特率设置为115200,然后就可以复现
代码
此代码结果

直接使用底层api的代码
此代码结果
Expected behavior
希望能一次性读到对方发送的完整数据
Actual behavior
实际上收到的被拆包了
Regression?
已知的所有版本都有这个问题,
Regression?
windows的解决方案我找到了,就是SetCommTimeouts的ReadIntervalTimeout默认为-1的原因导致的,如果设置成50 就不会出现这个问题,在源码中的https://github.com/dotnet/runtime/blob/main/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs 的第373行 _commTimeouts.ReadIntervalTimeout = Interop.Kernel32.MAXDWORD; 这里将ReadIntervalTimeout值设置为了-1,建议将次参数作为属性放出来,liunx中也有此问题,但是我未找到修复方案,ai告知我也有类似的参数,建议也放出来,或者放出来串口的句柄,让开发人员有渠道去手动设置。
Known Workarounds
.net 全版本
系统 windows10 11、liunx arm32 arm64 其它系统未测试
Configuration
No response
Other information
No response