如何将串口接收的二进制字节数组转换为对象?
问题原因分析
你用BinaryFormatter解析失败是必然的——BinaryFormatter是.NET专属的序列化工具,它序列化的二进制数据包含类型元数据、额外结构信息,和Arduino直接发送的C语言结构体二进制格式完全不兼容。Arduino发送的是结构体字段按内存布局直接转的原始二进制,没有任何额外信息,必须按字段类型、顺序、字节序手动解析。
解决方案
1. 定义C#对应结构体
先在WPF项目里定义和Arduino结构体字段完全匹配的C#结构体,注意对齐方式(Arduino默认1字节对齐,C#里要显式指定):
using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct MainCompPacket { public byte status; // 对应uint8_t public byte sensorStatus; // 对应uint8_t public float pressure; // 对应float(4字节IEEE754) public float accX; // 对应float public float accY; // 对应float public float accZ; // 对应float public float gyroX; // 对应float public float gyroY; // 对应float public float gyroZ; // 对应float public double latitude; // 对应double(8字节IEEE754) public double longitude; // 对应double }
2. 处理串口接收的完整数据包
SerialPort的DataReceived事件可能只返回部分数据,所以需要先缓存数据,直到凑够一个完整的结构体字节数(计算得:1+1+4*7+8*2=46字节):
private readonly MemoryStream _receiveCache = new MemoryStream(); private readonly int _fullPacketSize = Marshal.SizeOf(typeof(MainCompPacket)); // 结果为46 private void dataRecieved(object sender, SerialDataReceivedEventArgs e) { var serialPort = (SerialPort)sender; var tempBuf = new byte[serialPort.BytesToRead]; int readCount = serialPort.Read(tempBuf, 0, tempBuf.Length); // 将新数据写入缓存 _receiveCache.Write(tempBuf, 0, readCount); // 循环处理所有完整的数据包 while (_receiveCache.Length >= _fullPacketSize) { // 读取一个完整包 _receiveCache.Position = 0; var packetBuf = new byte[_fullPacketSize]; _receiveCache.Read(packetBuf, 0, _fullPacketSize); // 保留缓存中剩余的不完整数据 var remainingData = new byte[_receiveCache.Length - _fullPacketSize]; _receiveCache.Read(remainingData, 0, remainingData.Length); _receiveCache.SetLength(0); _receiveCache.Write(remainingData, 0, remainingData.Length); // 解析数据包 var parsedPacket = ParsePacket(packetBuf); // 更新UI(必须用Dispatcher,因为串口事件在后台线程) Dispatcher.Invoke(() => { // 这里写你的UI更新逻辑,比如绑定数据到界面控件 // 示例:txtPressure.Text = parsedPacket.pressure.ToString("F2"); }); } }
3. 解析二进制数据包
有两种可靠的解析方式,选一种即可:
方式一:用Marshal直接转换(高效)
private MainCompPacket ParsePacket(byte[] buffer) { GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { IntPtr ptr = handle.AddrOfPinnedObject(); return (MainCompPacket)Marshal.PtrToStructure(ptr, typeof(MainCompPacket)); } finally { handle.Free(); } }
方式二:手动逐字段解析(更直观,无需非托管内存)
private MainCompPacket ParsePacket(byte[] buffer) { var packet = new MainCompPacket(); int offset = 0; // 解析byte类型字段 packet.status = buffer[offset++]; packet.sensorStatus = buffer[offset++]; // 解析float类型字段(4字节,小端序,和Arduino一致) packet.pressure = BitConverter.ToSingle(buffer, offset); offset += 4; packet.accX = BitConverter.ToSingle(buffer, offset); offset += 4; packet.accY = BitConverter.ToSingle(buffer, offset); offset += 4; packet.accZ = BitConverter.ToSingle(buffer, offset); offset += 4; packet.gyroX = BitConverter.ToSingle(buffer, offset); offset += 4; packet.gyroY = BitConverter.ToSingle(buffer, offset); offset += 4; packet.gyroZ = BitConverter.ToSingle(buffer, offset); offset += 4; // 解析double类型字段(8字节) packet.latitude = BitConverter.ToDouble(buffer, offset); offset += 8; packet.longitude = BitConverter.ToDouble(buffer, offset); return packet; }
注意事项
- 字节序:Arduino的AVR架构是小端序,x86/x64平台的WPF程序默认也是小端序,所以无需转换;如果运行在ARM大端平台,需要手动反转每个字段的字节顺序。
- 数据校验:如果串口传输可能丢包/出错,可以在Arduino的结构体前后加校验位(比如CRC),解析前先校验数据包完整性。
内容的提问来源于stack exchange,提问作者Emin Niftiyev
相关产品推荐
相关产品推荐

