不依赖库!C# 手搓 Modbus RTU 主站读取变送器数据(串口 + WinForms 实战,含帧构造、CRC16、数据解析)
不依赖库C# 手搓 Modbus RTU 主站读取变送器数据串口 WinForms 实战含帧构造、CRC16、数据解析很多人做 Modbus 通信直接用 NModbus 之类的库帧怎么拼、CRC 怎么算一概不知一旦通信出问题就无从下手。本文用一个真实的变送器数据读取WinForms 程序带你不依赖任何第三方库纯手工实现 Modbus RTU 主站从串口参数配置、问询帧逐字节拼装、CRC16 校验计算到接收应答、高低位合并、定时轮询全部讲透。适合有一点 C# 基础、正在学习串口/Modbus 通信的初学者。一、项目背景与基础知识1.1 这个程序做什么程序通过串口RS485 转串口连接一台变送器周期性读取它的 4 个测量值并显示在界面上。界面上的 4 个参数依次为含水量、温度值、盐度、PH。通信方式是工业现场最常用的Modbus RTU本机作为**主站Master主动问询变送器作为从站Slave**被动应答。1.2 Modbus RTU 基础初学者必读Modbus 是一种主从式、一问一答的协议总线同一时刻只能有主站说话从站不会主动上报主站发送问询帧对应地址的从站返回应答帧每条 RTU 报文末尾都带CRC16 校验码用来判断数据在传输中是否出错。本程序使用功能码 0x03读保持寄存器 Read Holding Registers。一个寄存器是 16 位2 字节变送器的测量值就放在这些寄存器里。主站问询帧结构功能码 03共 8 字节字节位置内容本例说明0从站地址0x01要访问的设备地址1功能码0x03读保持寄存器2起始地址高字节0x00寄存器起始地址3起始地址低字节0x00从 0 号寄存器开始4读取数量高字节0x00读几个寄存器5读取数量低字节0x04本例读 4 个6CRC 低字节计算得到CRC 校验低字节在前7CRC 高字节计算得到CRC 校验高字节在后从站应答帧结构读 4 个寄存器共 13 字节字节位置内容说明0从站地址原样返回1功能码原样返回异常时最高位会置 12有效字节数4 个寄存器 × 2 0x083~104 个寄存器数据每个寄存器高字节在前、低字节在后11~12CRC 校验低字节在前、高字节在后调试时如果手头没有真实硬件可以用虚拟串口对软件如 VSPD Modbus 从站模拟软件如 Modbus Slave在一台电脑上自己和自己通信。串口通信参数波特率、数据位、校验位、停止位必须和设备一致本例为9600 / 8 / None / 1。二、程序整体结构与界面窗体核心成员只有三个一个串口对象、一个定时器、一个取消令牌publicpartialclassForm1:Form{SerialPort_serialPortnewSerialPort();// 串口对象CancellationTokenSource_ctsnewCancellationTokenSource();System.Timers.Timer_timernewSystem.Timers.Timer();// 定时轮询用publicForm1(){InitializeComponent();_timer.Interval1000;// 每 1 秒触发一次_timer.Elapsed_timer_Elapsed;_serialPort.DataReceived_serialPort_DataReceived;// 注册接收事件}}界面包含4 个文本框显示含水量、温度值、盐度、PH、一个读取按钮手动读一次、一个实时检测复选框勾选后每秒自动轮询。【插图 1程序运行效果图】三、串口初始化参数从配置文件读取好的实践是把串口参数写在App.config里而不是硬编码换设备、换波特率时改配置即可!-- App.config --appSettingsaddkeyPortNamevalueCOM4/addkeyBaudRatevalue9600/addkeyDataBitsvalue8/addkeyParityvalueNone/addkeyStopBitsvalueOne//appSettings窗体加载时读取配置并打开串口privatevoidForm1_Load(objectsender,EventArgse){_serialPort.PortNameCOM4;// 波特率、数据位是数字用 int.Parse 转换_serialPort.BaudRateint.Parse(ConfigurationManager.AppSettings[BaudRate]);_serialPort.DataBitsint.Parse(ConfigurationManager.AppSettings[DataBits]);// 校验位、停止位是枚举用 Enum.Parse 把字符串转成对应枚举值_serialPort.Parity(Parity)Enum.Parse(typeof(Parity),ConfigurationManager.AppSettings[Parity]);_serialPort.StopBits(StopBits)Enum.Parse(typeof(StopBits),ConfigurationManager.AppSettings[StopBits]);try{_serialPort.Open();// 打开串口}catch(Exceptionex){// 串口被占用、不存在等情况会抛异常弹窗提示MessageBox.Show(ex.Message);}}注意使用ConfigurationManager需要先通过 NuGet 安装System.Configuration.ConfigurationManager并在代码里using System.Configuration;。四、核心手工构造 Modbus RTU 问询帧【插图 2设备的官方文档】.4.1 发起读取点击读取按钮或定时器触发时调用Read方法从 0 号寄存器开始读 4 个// 数据读取privatevoidbutton1_Click(objectsender,EventArgse){if(!_serialPort.IsOpen)return;// 串口没打开直接返回Read(0x00,4);// 起始地址 0读取数量 4}/// summary/// 读取保持寄存器/// /summary/// param nameregisterAddress寄存器起始地址/param/// param namenum读取数量/paramprivatevoidRead(ushortregisterAddress,ushortnum){// 从站地址为 1功能码为 3拼出完整问询帧byte[]_dataReadingData(1,3,registerAddress,num);_serialPort.Write(_data,0,_data.Length);// 通过串口发出}4.2 拼装帧的前 6 个字节起始地址和数量都是 16 位2 字节需要拆成高字节 低字节这里用到了位运算/// summary/// 主机问询帧构造/// /summaryprivatebyte[]ReadingData(byteslaveAddress,bytefunction,ushortregisterAddress,ushortnumber){// 先放前 6 个字节CRC 校验正是基于这 6 个字节计算byte[]data1newbyte[6];data1[0]slaveAddress;// 从站地址data1[1]function;// 功能码// 起始地址是 2 字节要拆成高位、低位高位在前// 是按位右移把 16 位整体右移 8 位原来的高 8 位就落到低 8 位// 255255 二进制是 8 个 1用于只保留低 8 位得到一个 0~255 的字节data1[2]Convert.ToByte(registerAddress8255);// 起始地址高字节data1[3]Convert.ToByte(registerAddress255);// 起始地址低字节// 读取数量同样拆成高、低字节data1[4]Convert.ToByte(number8255);// 数量高字节data1[5]Convert.ToByte(number255);// 数量低字节// 对前 6 字节计算 CRC得到 2 字节校验码byte[]crcCrc16(data1);// 拼成完整 8 字节前 6 字节 CRC 2 字节byte[]datanewbyte[8];Array.Copy(data1,0,data,0,6);Array.Copy(crc,0,data,6,2);returndata;}关于 8 255举个例子地址0x0123十进制 291右移 8 位后高字节0x01落到低位再 255得到1直接 255得到低字节0x23 35。这样就把一个 16 位数拆成了两个字节。4.3 CRC16 校验计算CRC循环冗余校验的根本目的是检验数据在传输过程中是否丢包或被干扰。Modbus RTU 统一使用 CRC-16/MODBUS 算法初始值0xFFFF特征多项式0xA001它是标准多项式0x8005的位反转形式。这套代码是通用的任何 Modbus RTU 场景都能直接用privatebyte[]Crc16(byte[]data){ushortcrc160xFFFF;// 初始化 CRC 寄存器为全 1for(inti0;idata.Length;i){crc16^(ushort)data[i];// 第一步当前字节与 CRC 低 8 位异或for(intj0;j8;j)// 一个字节 8 位逐位处理{if((crc160x0001)!0)// 检查最低位是不是 1{crc161;// 最低位为 1右移一位crc16^0xA001;// 再与特征常数 0xA001 异或}else{crc161;// 最低位为 0只右移不异或}}}// Modbus 规定 CRC 低字节在前、高字节在后所以这里做高低字节交换returnnewbyte[]{(byte)(crc160xFF),// 低字节(byte)(crc168)// 高字节};}五、定时轮询手动 自动两种方式点读取按钮手动读一次勾选实时检测启动定时器每 1 秒自动读一次实现数据实时刷新。// 定时器到点就调用一次按钮的读取逻辑privatevoid_timer_Elapsed(objectsender,System.Timers.ElapsedEventArgse){button1_Click(sender,e);}// 实时检测复选框勾选启动定时器取消勾选停止privatevoidcheckBox1_CheckedChanged(objectsender,EventArgse){if(checkBox1.Checked)_timer.Start();else_timer.Stop();}六、核心接收应答帧并解析数据6.1 DataReceived 事件串口收到数据会在后台线程触发DataReceived事件。注意它不在 UI 线程所以更新界面控件时必须用Invoke切回 UI 线程。privatevoid_serialPort_DataReceived(objectsender,SerialDataReceivedEventArgse){// 一次性读取串口接收缓冲区中的所有字节byte[]buffernewbyte[_serialPort.BytesToRead];intnum_serialPort.Read(buffer,0,buffer.Length);if(num0)return;// 把原始字节打印到输出窗口方便观察真实报文foreach(variteminbuffer){Console.Write(item );}// 应答帧[0]地址码 [1]功能码 [2]有效字节数 [3..]数据区 [末尾]校验码bytefuncbuffer[1];// 功能码bytecountbuffer[2];// 有效字节数if(func3)// 功能码 3寄存器应答结果有效{// DataReceived 在后台线程更新控件必须 Invoke 到 UI 线程Invoke(newAction((){// 每个寄存器占 2 字节高字节 buffer[n]、低字节 buffer[n1]// 合并方法高字节 × 256 低字节即高字节左移 8 位// 多数变送器测量值带 1 位小数寄存器里放大了 10 倍所以再 /10stringdataStr((buffer[3]*256buffer[4])/10).ToString();// 含水量stringdataStr2((buffer[5]*256buffer[6])/10).ToString();// 温度值stringdataStr3((buffer[7]*256buffer[8])).ToString();// 盐度整数stringdataStr4((buffer[9]*256buffer[10])/10).ToString();// PHtxtwendu.TextdataStr;// 含水量textBox2.TextdataStr2;// 温度值textBox3.TextdataStr3;// 盐度textBox4.TextdataStr4;// PH}));}}【插图 3控制台原始字节输出图】6.2 重点理解高低位合并一个寄存器是 16 位分高、低两个字节传输解析时要把它们重新拼成一个数。以数值 300二进制0000 0001 0010 1100为例高字节0000 0001 1放在buffer[3]低字节0010 1100 44放在buffer[4]如果直接相加1 44 45错误正确做法是把高字节左移 8 位1 × 256 44 300。这就是高低位合并和发送时的拆字节正好是逆过程。6.3 关于精度除以 10变送器通常把带小数的测量值放大 10 倍、100 倍后以整数形式存进寄存器比如实际 25.6℃寄存器里是 256。所以解析后要按手册除以对应的倍数。本例大部分数据/10盐度按整数显示没有除——具体除以多少、要不要除一定要查该设备的通信协议手册不能照抄。七、初学者常见坑完全收不到数据先检查串口参数波特率/数据位/校验/停止位是否与设备一致、RS485 的 A/B 线有没有接反、从站地址对不对。程序一运行就弹窗报错多半是串口号不对、被其他软件占用或设备没插好打开串口要用try/catch包住。更新文本框报线程间操作无效DataReceived在后台线程更新 UI 必须用Invoke。数据解析出来明显不对值特别小忘了高低位合并正确公式是高字节 × 256 低字节。数值是实际值的 10 倍寄存器放大了倍数按手册/10或/100。偶尔能读到、偶尔 CRC 错误/丢数据可能是一次Read没读完整帧。DataReceived可能在一帧数据没到齐时就触发严谨做法是把收到的字节先缓存凑够完整帧按字节数字段判断长度再解析。CRC 算出来和设备对不上检查多项式是否为0xA001、初值0xFFFF、结果是否做了高低字节交换、参与计算的是否只有前面的数据字节不含 CRC 本身。定时器轮询过快导致数据错乱轮询间隔要给设备留出应答时间本例 1 秒更严谨的方式是发一帧、等应答回来再发下一帧。配置文件读不到确认 NuGet 装了System.Configuration.ConfigurationManager且 key 名称拼写一致。八、小结本文用一个真实的变送器读取程序完整演示了不依赖第三方库、纯手工实现 Modbus RTU 主站的全过程串口参数集中放在App.config窗体加载时读取并打开串口按地址 功能码 起始地址 数量拼出前 6 字节地址和数量用 8、 255拆成高/低字节用标准 CRC-16/MODBUS初值0xFFFF、多项式0xA001计算校验码低字节在前拼成完整 8 字节问询帧在DataReceived事件里读取应答判断功能码后用高字节 × 256 低字节合并数据、按精度除以倍数并通过Invoke更新界面用定时器实现 1 秒周期的自动轮询。亲手拼一次帧、算一次 CRC比直接调用第三方库理解得深刻得多以后遇到通信问题也能快速定位。建议对照本文代码配合虚拟串口和 Modbus 从站模拟软件在本机跑一遍。

相关新闻

热加载为什么难——卸载 DLL 的四个前提

热加载为什么难——卸载 DLL 的四个前提

进入阶段三。前面的内容,哪怕你一句都没写对,顶多是功能不对、偶尔崩溃。这一阶段的主题是:不停机把正在用的插件换掉。做错了,是进程直接没了。 先说一个反直觉的事实,也是我当年卡了一整周的地方:QPluginLoader::unload() 你调它,它十有八九返回 false。而且这不是你…

2026/9/24 13:36:13 阅读更多 →
【Dv2Admin】修改菜单栏左侧显示宽度

【Dv2Admin】修改菜单栏左侧显示宽度

在前端开发中,用户界面往往会因为某些设计或数据量的原因导致显示问题。常见的一个问题是菜单字数过多,导致菜单项无法完整显示。为了优化用户体验,常需要对界面进行调整以确保内容显示的完整性。 在本教程中,将以一个实际的前端问题为例,展示如何通过修改Vue项目中的样式…

2026/9/24 13:35:13 阅读更多 →
思源笔记插件开发从零到上线:20 分钟跑通本地环境并让第一个插件加载成功

思源笔记插件开发从零到上线:20 分钟跑通本地环境并让第一个插件加载成功

思源笔记插件开发从零到上线:20 分钟跑通本地环境并让第一个插件加载成功 【免费下载链接】siyuan An open-source, privacy-first, self-hosted knowledge workspace where humans and AI agents work together 开源、隐私优先、自托管的知识工作空间,让…

2026/9/24 13:35:13 阅读更多 →

最新新闻

蒙特卡罗模拟在工业工程中的应用:从产能瓶颈到投资决策

蒙特卡罗模拟在工业工程中的应用:从产能瓶颈到投资决策

还记得那次复盘会。车间主任把上个月的产量报表往桌上一拍,冲我们IE团队说:“按你们的测算,这条铆接线年产能100万件,怎么每个月都追料追到月底?”我们拿出的产能测算表确实白纸黑字:瓶颈工序节拍乘以稼动率…

2026/9/24 22:19:20 阅读更多 →
ClassIsland桌面课程表:从部署到稳定运行的全流程实践

ClassIsland桌面课程表:从部署到稳定运行的全流程实践

讲个真实场景。我所在的学校前两年还在用最原始的方式管课表:每个教室前面贴一张打印纸,开学第一周刚贴上去还有学生看,两周后边角卷起来,上面被粉笔灰盖得看都看不清。学生问"下节什么课",答不上来的人比答…

2026/9/24 22:19:20 阅读更多 →
GS算法与角谱法:用Python搭建波前重建仿真平台的完整指南

GS算法与角谱法:用Python搭建波前重建仿真平台的完整指南

简介:基于Gerchberg-Saxton迭代算法的光学相位恢复与波前重建仿真平台,以Matlab为运行环境,面向光学工程、物理及相关专业的学生与研究人员,用于演示光强数据反演相位分布、重构波前传播形态的核心过程。资源包共30个文件&#xf…

2026/9/24 22:19:20 阅读更多 →
免费永久域名eu.org申请全攻略:注册、解析与绑定实践

免费永久域名eu.org申请全攻略:注册、解析与绑定实践

我手头常年躺着一堆“上不了台面”的小项目:GitHub Pages上写了半截的博客、一台用来跑自动化脚本的轻量云服务器、一个想发给客户看效果的后台Demo。这类东西有个共同问题:需要一个域名,但又不想为它每年花几十上百。免费申请永久域名这件事…

2026/9/24 22:19:20 阅读更多 →
基于Python的人脸识别签到系统开发实战

基于Python的人脸识别签到系统开发实战

简介:人脸识别技术是计算机视觉领域的重要应用,其核心原理是通过深度学习模型提取人脸特征向量,并利用欧氏距离进行身份比对。这一技术无需额外硬件,仅需普通摄像头即可实现高精度身份验证,在考勤签到、门禁系统等场景…

2026/9/24 22:19:20 阅读更多 →
在家复刻日式牛肉饭全攻略:选肉、调味汁到火候详解

在家复刻日式牛肉饭全攻略:选肉、调味汁到火候详解

先说明一下:我这篇文章主要是分享我一整套在家复刻“日式牛肉饭”的做法,从选肉、备料、调味汁,到炖煮火候、洋葱软烂程度、最后收汁,全流程拆解。标题里提到的“白银价格剧烈走势”,其实是我的输入信息里混入了一条毫…

2026/9/24 22:18:20 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →