新闻详情

新闻详情

首页 / 资讯中心 / 详情

C#上位机帧解析引擎深度解析:从字节流到结构体

发布时间:2026/9/5 0:27:00来源:尧图网络
C#上位机帧解析引擎深度解析:从字节流到结构体
www.z-linear.comD223的USB通信发送的是二进制帧上位机收到的是原始字节流。怎么从字节流中找到帧头怎么校验CRC怎么把字节数组转成结构体本文深度解析zlTool上位机的帧解析引擎。一、帧格式定义1.1 通信协议帧结构所有消息统一格式帧头(0x55AA) 命令ID 帧长度 数据 CRC16┌──────┬───────┬──────────┬─────────────┬──────┐ │ Head │ CmdId │ FrameLen │ Data │ CRC │ │ 2B │ 1B │ 2B │ N Bytes │ 2B │ │0x55AA│ │ 大端 │ │ 大端 │ └──────┴───────┴──────────┴─────────────┴──────┘1.2 C#结构体定义// msgStruct.cs - 帧头定义[StructLayout(LayoutKind.Sequential,Pack1)]publicstructmsgHead{publicUInt16head;// 帧头: 0x55AApublicbytecmdId;// 命令IDpublicUInt16frameLen;// 帧长度(含CRC)}Pack 1确保结构体按1字节对齐与C语言的结构体内存布局一致。1.3 大端模式D223通信协议使用大端模式高字节先传输。C#是小端模式低字节先存储需要SWAP16/SWAP32转换。// 大端转换publicstaticUInt16SWAP16(UInt16value){return(UInt16)((value8)|(value8));}// 示例0x55AA存储为 0x55 0xAA大端// C#直接存储为 0xAA 0x55小端// 需要SWAP16(0x55AA) 0xAA55内存中变为 0x55 0xAA二、接收状态机2.1 整体流程帧解析引擎运行在100ms定时器rxTim中每100ms检查一次接收缓冲区。串口/USB接收中断 → 数据追加到 Listbyte 缓冲区 │ ▼ (定时器 rxTim_Tick 触发每100ms) ┌──────────────┐ │ 缓冲区 5B? │──No──→ 等待 └──────┬───────┘ │Yes ▼ ┌──────────────┐ │ 搜索帧头0x55AA│ └──────┬───────┘ │找到 ▼ ┌──────────────┐ │ 长度是否足够? │──No──→ 等待更多数据 └──────┬───────┘ │Yes ▼ ┌──────────────┐ │ CRC16校验 │──Fail──→ 丢弃帧头重新搜索 └──────┬───────┘ │OK ▼ ┌──────────────┐ │ 触发_deviceRxMsg│ → 通知所有订阅页面 │ 移除已处理数据 │ └──────────────┘2.2 逐行解析voidrxTim_Tick(objectsender,EventArgse){// 1. 只有当缓冲区字节数5才进入解析// 最小帧 帧头2B 命令1B 长度2B CRC2B 7Bwhile(usbRxMsgBuf.Count5){boolfindNewHeadfalse;byte[]usbRxBufusbRxMsgBuf.ToArray();// 2. 搜索帧头0x55 0xAAfor(UInt16i0;iusbRxBuf.Length;i){if(usbRxBuf[i]0x55i!(usbRxBuf.Length-1)){if(usbRxBuf[i1]0xaa){// 找到帧头// 如果帧头不在第0位删除前面的脏数据if(i0usbRxMsgBuf.Counti){usbRxMsgBuf.RemoveRange(0,i);break;}findNewHeadtrue;// 3. 将字节数组转成结构体读取帧长度msgHead_pRxHeadnewmsgHead();_pRxHead(msgHead)BytesToDataStruct(usbRxBuf,typeof(msgHead));// 4. 判断一帧数据是否收完整if((usbRxBuf.Length-0)SWAP16(_pRxHead.frameLen)){// 足够 → 继续校验}else{// 不够 → return等下一次100ms再收return;}// 5. CRC16校验stringcrcResultcrcCheck.checkCrc16WithTail(usbRxBuf,SWAP16(_pRxHead.frameLen));if(crcResult!OK){// CRC错误 → 删掉这个错误帧头重新查找usbRxMsgBuf.RemoveRange(0,2);break;}// 6. CRC正确 → 交给业务层处理_deviceRxMsg(usbRxMsgBuf.ToArray());// 7. 删除已处理完的一帧数据usbRxMsgBuf.RemoveRange(0,SWAP16(_pRxHead.frameLen));break;}}}// 8. 遍历完所有数据都没找到0x55AA → 清空缓冲区if(!findNewHead){usbRxMsgBuf.Clear();}}}三、字节数组与结构体转换3.1 BytesToDataStruct// 将byte[]转换为结构体publicstaticobjectBytesToDataStruct(byte[]bytes,Typetype){intsizeMarshal.SizeOf(type);if(bytes.Lengthsize)returnnull;IntPtrptrMarshal.AllocHGlobal(size);Marshal.Copy(bytes,0,ptr,size);objectobjMarshal.PtrToStructure(ptr,type);Marshal.FreeHGlobal(ptr);returnobj;}这个函数通过.NET的Marshal类实现字节数组到结构体的直接内存映射不需要逐字段解析。3.2 结构体到字节数组// 将结构体转换为byte[]publicstaticbyte[]DataStructureToBytes(objectobj){intsizeMarshal.SizeOf(obj);byte[]bytesnewbyte[size];IntPtrptrMarshal.AllocHGlobal(size);Marshal.StructureToPtr(obj,ptr,false);Marshal.Copy(ptr,bytes,0,size);Marshal.FreeHGlobal(ptr);returnbytes;}3.3 使用示例// 接收数据解析byte[]rxBuf{0x55,0xAA,0x16,0x00,0x0C,0xFF,0x00,0x00,0x4E,0x20,0x44,0x77};msgAdcStartOnlineRecordmsgnewmsgAdcStartOnlineRecord();msg(msgAdcStartOnlineRecord)BytesToDataStruct(rxBuf,typeof(msgAdcStartOnlineRecord));// msg.head 0xAA55C#小端需SWAP16还原为0x55AA// msg.cmdId 0x16// msg.frameLen 0x0C00需SWAP16还原为0x000C 12// msg.adc_ch_Enable 0xFF// msg.adcSignalType 0x00// msg.adcRange 0x00// msg.adcSamplingRate 0x204E需SWAP16还原为0x4E20 20000Hz// msg.crc 0x7744需SWAP16还原为0x4477四、CRC16校验4.1 CRC16-Modbus算法Modbus协议中采用了CRC-16标准校验方法。CRC由两个字节组成校验整个报文的内容。publicstringcheckCrc16WithTail(byte[]data,intlen){// CRC16-Modbus查表法bytecrcL0xFF,crcH0xFF;intcalcLenlen-2;// 减去尾部2字节CRC本身for(inti0;icalcLen;i){byteuIndex(byte)(crcH^data[i]);crcH(byte)(crcL^tableH[uIndex]);crcLtableL[uIndex];}// 计算结果ushortcrc(ushort)((crcH8)|crcL);// 与接收到的CRC比较ushortrxCrc(ushort)((data[len-2]8)|data[len-1]);if(crcrxCrc)returnOK;elsereturnERROR;}4.2 CRC查表法原理CRC查表法是将移位异或的计算结果做成了一个表。将0~256放入一个长度为16位的寄存器中的低8位高8位填充0然后将该寄存器与多项式0xA001按照移位异或步骤直到8位全部移出最后寄存器中的值就是表格中的数据。// CRC16-Modbus查找表高字节byte[]tableHnewbyte[256]{0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41,0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40,// ... 256个值};// CRC16-Modbus查找表低字节byte[]tableLnewbyte[256]{0x00,0xC0,0xC1,0x01,0xC3,0x03,0x02,0xC2,0xC6,0x06,0x07,0xC7,0x05,0xC5,0xC4,0x04,// ... 256个值};查表法比逐位计算快8倍因为每个字节只需1次查表而非8次移位异或。五、命令分发5.1 命令ID枚举// 命令ID枚举publicenumCmdId:byte{_cmdId_getDeviceParam0x00,// 获取设备参数_cmdId_setAdcRange0x01,// 设置ADC量程_cmdId_getAdcValue0x03,// 获取ADC值_cmdId_startOnlineRecord0x16,// 启动在线记录_cmdId_stopRecord0x17,// 停止记录_cmdId_startSramRecord0x18,// 启动SRAM记录_cmdId_getSramRecord0x19,// 读取SRAM记录_cmdId_setDdsDacParam0x20,// 设置DAC参数_cmdId_setDio0x30,// 设置DIO_cmdId_setPwmParam0x40,// 设置PWM参数// ... 30个命令}5.2 命令分发器// 下位机命令分发voidfun_msgProcess_task(u8*rxData){msgHead*_pRx(msgHead*)rxData;// 1. 帧头校验if(_pRx-msgHead!SWAP16(0x55aa))return;// 2. CRC校验if(!checkCrc16WithTail(rxData,SWAP16(_pRx-frameLen)))return;// 3. 命令分发switch(_pRx-cmdId){case_cmdId_setAdcRange:fun_setAdcRange(rxData);break;case_cmdId_getDeviceParam:fun_getAdcParam();break;case_cmdId_startSramRecord:fun_msgStartSramRecord(rxData);break;case_cmdId_startOnlineRecord:fun_msgStartOnlineRecord(rxData);break;case_cmdId_setDdsDacParam:fun_msgSetDdsDacParam(rxData);break;case_cmdId_setOutputIO:fun_msgSetOutputIO(rxData);break;case_cmdId_setPwmParam:fun_msgSetPwmParam(rxData);break;// ... 30个命令}}5.3 上位机命令分发// 上位机_deviceRxMsgvoid_deviceRxMsg(byte[]rxData){msgHeadhead(msgHead)BytesToDataStruct(rxData,typeof(msgHead));switch((CmdId)head.cmdId){caseCmdId._cmdId_getAdcValue:ProcessAdcValue(rxData);// 处理ADC数据break;caseCmdId._cmdId_getAdc_waveData:ProcessWaveData(rxData);// 处理波形数据break;caseCmdId._cmdId_pwmComplete:ProcessPwmComplete(rxData);// PWM完成通知break;// ...}}六、消息结构体体系6.1 结构体层级// 通用消息头msgHead →{head(2B),cmdId(1B),frameLen(2B)}// 通用参数消息msgNoParam → headch(1B)crc(2B)msgUbyte → headch(1B)data(1B)crc msgUWord → headch(1B)data(2B)crc msgULong → headch(1B)data(4B)crc msgSLong → headch(1B)data(4B,有符号)crc// ADC数据消息msgGetAdcDatas → headdataLenadc_ch_EnablesignalTyperangetrigChtrigValuesamplingRatewaveData[]crc// 在线记录启动消息msgAdcStartOnlineRecord → headch_EnablesignalTyperangesamplingRatecrc// DAC参数消息msgSetDdsDacParam → headchfreqdutywaveTypeampoffsetcrc// PWM参数消息msgSetPwmParam → headchfreqdutypulseNumaccUnitcrc6.2 使用示例// 发送启动在线记录命令msgAdcStartOnlineRecordmsgnewmsgAdcStartOnlineRecord();msg.msgHeadSWAP16(0x55aa);msg.cmdId0x16;msg.frameLenSWAP16((ushort)Marshal.SizeOf(msg));msg.adc_ch_Enable0xFF;// 8通道全使能msg.adcSignalType0x00;// 电压型msg.adcRange0x00;// ±5Vmsg.adcSamplingRateSWAP16(20000);// 20KSPS// 计算CRCbyte[]txBufDataStructureToBytes(msg);ushortcrccrcCheck.getCrc16WithTail(txBuf,Marshal.SizeOf(msg));txBuf[Marshal.SizeOf(msg)-2](byte)(crc8);txBuf[Marshal.SizeOf(msg)-1](byte)(crc0xFF);// 发送serialPort.Write(txBuf,0,txBuf.Length);七、实际数据帧解析示例7.1 单点记录仪请求帧发送: 55 AA 08 00 08 00 11 A6 55 AA → 帧头 08 → cmdId单点读取 00 08 → frameLen 8字节 00 → 通道号未用 11 A6 → CRC167.2 单点记录仪响应帧接收: 55 AA 08 00 2B 03 00 00 00 00 81 64 C0 00 81 64 BE 00 81 5F 41 00 81 61 A4 00 81 58 45 00 81 59 11 00 81 63 AC 00 81 64 14 0D E6 55 AA → 帧头 08 → cmdId单点读取响应 00 2B → frameLen 43字节 03 → 通道使能状态 00 → 信号类型电压型 00 → ADC量程±5V 00 → DIN状态 00 81 64 C0 → CH1: 0x008164C0 00 81 64 BE → CH2: 0x008164BE 00 81 5F 41 → CH3: 0x00815F41 00 81 61 A4 → CH4: 0x008161A4 00 81 58 45 → CH5: 0x00815845 00 81 59 11 → CH6: 0x00815911 00 81 63 AC → CH7: 0x008163AC 00 81 64 14 → CH8: 0x00816414 0D E6 → CRC167.3 24位ADC值转电压CH1值 0x008164C0 8,494,912 中点 0x00800000 8,388,608 量程 ±5V 电压 (8,494,912 - 8,388,608) / 8,388,608 × 5.0 106,304 / 8,388,608 × 5.0 0.0634V八、多线程与UI更新8.1 接收线程与UI线程// USB接收在独立线程voidUsbReceiveThread(){while(receiving){intlenusbStream.Read(rxBuffer,0,1024);// 数据追加到缓冲区lock(usbRxMsgBuf){for(inti0;ilen;i)usbRxMsgBuf.Add(rxBuffer[i]);}}}// 定时器在UI线程voidrxTim_Tick(objectsender,EventArgse){// 在UI线程中解析和处理// 使用Invoke确保UI更新在UI线程this.Invoke((MethodInvoker)delegate{UpdateWaveform();// 更新波形显示});}8.2 线程安全操作线程安全机制USB接收独立线程lock保护List帧解析UI线程定时器单线程访问波形更新UI线程Invoke确保参数发送UI线程直接操作九、错误处理策略9.1 帧头搜索容错场景: 噪声导致帧头前有杂散字节 接收: [0x12 0x34] 0x55 0xAA 0x08 ... ↑ 帧头在第2字节 处理: 1. 搜索到0x55 0xAA在第2字节 2. 删除前面2字节脏数据 3. 帧头对齐到缓冲区开头 4. 继续解析9.2 CRC错误恢复场景: 数据传输中字节翻转导致CRC错误 接收: 0x55 0xAA 0x08 0x00 0x08 0x00 0x12 0x34CRC错误 处理: 1. CRC校验失败 2. 删除帧头2字节0x55 0xAA 3. 从下一字节开始重新搜索帧头 4. 避免数据堆积9.3 数据不足等待场景: 一帧数据尚未接收完整 缓冲区: 0x55 0xAA 0x08 0x00 0x08 0x00 0x11缺CRC第2字节 处理: 1. 帧头正确 2. frameLen8但缓冲区只有7字节 3. return等待下一次100ms定时器 4. 下次数据到达后继续解析十、总结zlTool上位机帧解析引擎实现了设计要点实现帧格式0x55AA cmdId frameLen data CRC16大端模式SWAP16/SWAP32转换解析周期100ms定时器帧头搜索遍历查找0x55 0xAA长度判断缓冲区≥frameLen才解析CRC校验CRC16-Modbus查表法结构体映射Marshal内存映射命令分发switch-case 30命令错误恢复丢弃脏数据CRC失败重搜线程安全lock保护Invoke更新UI从原始字节流的帧头搜索到结构体内存映射从CRC16查表校验到30命令的分发处理zlTool的帧解析引擎是D223上位机与采集卡通信的核心枢纽。下篇预告《D223全链路数据流追踪从传感器到屏幕》
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

为什么美国律师一开口,就要你的销售数据? 2026/9/5 2:09:20

为什么美国律师一开口,就要你的销售数据?

知产纠纷数据应对指南 Guide to Handling Data Requests in US IP Disputes “ 跨境卖家收美国知产律师函,别轻易交销售数据!这是对方在评估案件价值、谈判空间。不同阶段披露策略不同,要先辨风险,有边界沟通,避免误判…

阅读更多 →
测速站如何对接家宽节点?DNSPup API、节点调度与结果展示方法 2026/9/5 2:09:20

测速站如何对接家宽节点?DNSPup API、节点调度与结果展示方法

背景 一个测速站如果只有几个云主机探针,往往只能反映数据中心视角。普通用户使用的是家庭宽带,网络路径更复杂,运营商之间也存在明显差异。DNSPup 提供 API 对接能力,并拥有 300 家宽测速节点,适合将家宽样本接入已有…

阅读更多 →
从零实现一个 Android AI Agent:ReAct + Function Calling 实战 2026/9/5 2:09:20

从零实现一个 Android AI Agent:ReAct + Function Calling 实战

从零实现一个 Android AI Agent:ReAct Function Calling 实战 本文配套开源项目:AI Agent Demo(Apache 2.0 协议) 技术栈:Kotlin Jetpack Compose MVVM Retrofit DeepSeek API 一、为什么要在手机上做 AI Agent …

阅读更多 →
硬件设计笔记:1μH功率电感选型实战——线艺 XFL4020-102MEC 与 国产 pin to pin MVLH4020-1R0M 详细参数测评 2026/9/5 2:09:20

硬件设计笔记:1μH功率电感选型实战——线艺 XFL4020-102MEC 与 国产 pin to pin MVLH4020-1R0M 详细参数测评

在嵌入式硬件设计中,DC-DC电源部分的电感选型往往决定了项目的成败。最近在评估一个12V转3.3V的Buck电路,手头有两款可选的1μH、4020封装(4x4mm)的屏蔽电感:Coilcraft XFL4020-102MEC和TONEVEE MVLH4020-1R0M。两者引…

阅读更多 →
中电金信连续九年位居IDC中国银行业IT解决方案市场第一:源启与AI如何构筑长期竞争力 2026/9/5 2:09:20

中电金信连续九年位居IDC中国银行业IT解决方案市场第一:源启与AI如何构筑长期竞争力

中电金信连续九年位居IDC中国银行业IT解决方案市场第一,并非单一产品驱动,而是核心系统、数据智能、业务解决方案、工程交付与AI能力长期积累的结果。以“源启”为代表的数字基础设施,以及智能体、AI原生软件研发和本体驱动的数据智能&#x…

阅读更多 →
SpringBoot宿舍维修管理系统:从零构建企业级Java Web应用实战 2026/9/5 2:06:20

SpringBoot宿舍维修管理系统:从零构建企业级Java Web应用实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞