不依赖库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 从站模拟软件在本机跑一遍。
企业数字化 ERP 产品动态
相关推荐
热加载为什么难——卸载 DLL 的四个前提 进入阶段三。前面的内容,哪怕你一句都没写对,顶多是功能不对、偶尔崩溃。这一阶段的主题是:不停机把正在用的插件换掉。做错了,是进程直接没了。
先说一个反直觉的事实,也是我当年卡了一整周的地方:QPluginLoader::unload() 你调它,它十有八九返回 false。而且这不是你… · 2026/9/24 13:36:14
深入解析 lann/builder:用 Go 编写不可变、可复用的流式 Builder DSL 人工智能AI AgentAgent 沙箱云原生容器运行时零信任 【免费下载链接】substrate Agent Substrate: the core system 项目地址: https://gitcode.com/GitHub_Trending/substrate7/substrate 点击查看 免费下载 Builder 是 Go 语言中一套面向“流式(fluen… · 2026/9/24 13:36:08
烘焙后城市场景满是黑斑?用6步检查 Lightmap UV 与光照接缝 城市场景完成光照烘焙后,如果出现整面发黑、局部脏斑、模块接缝发亮,先不要急着提高灯光强度。更常见的原因是 Lightmap UV 重叠、UV 岛间距不足、光照贴图分辨率与对象尺寸不匹配,以及薄面、法线或模块边界存在问题。
本文用一个最小场景演… · 2026/9/24 13:36:08
【Dv3Admin】应用URL路由配置文件解析 统一路由配置是 Django 项目结构的重要组成部分。通过集中管理接口、页面和文档入口,可以减少重复代码、提升维护效率,并支持模块化开发与动态扩展,满足中大型项目对灵活性和可控性的要求。
文章解析 application/urls.py 的关键实现,包括系统初始化、文档集成、认证接口注… · 2026/9/24 14:04:02
【Dv3Admin】应用WebSocket配置文件解析 实时通信能力已成为后台系统的关键组成部分,WebSocket 可实现客户端与服务器的长连接,适合消息推送、通知提醒等场景。基于 Django Channels 构建的通信模块,提供了稳定的异步交互机制。
内容聚焦 websocketConfig.py 模块,解析其如何完成用户鉴权、建立连接、分发消息和记… · 2026/9/24 14:04:02
Medical | 医药物流的库存天数应用 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 14:04:02
【Dv3Admin】应用Celery异步任务配置文件解析 异步任务机制是提升系统性能的常用方案,Celery 在 Python 项目中应用广泛,特别适用于处理耗时操作与任务调度。通过将任务从主线程中剥离,可有效减少接口阻塞,提升用户体验和系统吞吐。
文章聚焦 application/celery.py 模块,解析其如何初始化 Celery 实例、兼容多租户架… · 2026/9/24 14:04:02
【Dv3Admin】应用ASGI配置文件解析 异步通信能力是现代后台系统的重要组成,尤其在任务推送、实时通知等交互需求中。ASGI 架构的引入,使得 Django 系统不再局限于同步处理,能够原生支持 WebSocket 等异步协议。
本文解析 application/asgi.py 模块的异步入口配置,说明其如何通过 Channels 结构分流 HTTP 与 … · 2026/9/24 14:04:02
【Dv3Admin】解决vue页面拆分组件在视觉上出现顺序混乱 在前端开发中,经常会遇到布局中元素层级显示异常的问题。最近遇到了一个问题:在布局中,左侧是导航菜单,右侧包含模型菜单和具体模型组件。右侧的模型菜单总是优先显示在左侧导航菜单之上,导致页面层级混乱。
经过一些调试和研究,最终找到了解决方案。以下是问题的详细描… · 2026/9/24 14:03:56
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44