土壤湿度传感器上位机系统一、项目概述本项目是一个基于C# WinForms开发的土壤湿度传感器数据采集上位机系统。系统通过串口SerialPort与土壤湿度传感器进行通信采用Modbus RTU协议读取传感器保持寄存器中的数据并将解析后的电导率等数值实时显示在界面上。项目实现了 Modbus RTU 协议的核心功能包括请求帧构造、CRC16 冗余校验、响应帧解析以及高低位数据合并是学习工业串口通信与 Modbus 协议的典型实践案例。二、系统架构系统采用三层架构PC 上位机 → 串口/RS485 转换器 → 土壤湿度传感器。上位机通过System.IO.Ports.SerialPort类操作串口按照 Modbus RTU 协议格式构造 8 字节请求帧发送给传感器传感器收到后返回 7 字节响应帧上位机解析响应帧中的寄存器数据并显示。通信参数参数值说明端口COM3串口名称可在 App.config 中配置波特率9600每秒传输位数数据位8每个字节的数据位数校验位None无校验停止位1一个停止位从站地址1传感器的 Modbus 设备地址功能码0x03读保持寄存器三、开发环境项目说明开发语言C#框架.NET Framework 4.7.2界面框架Windows Forms (WinForms)开发工具Visual Studio 2022项目类型Windows 桌面应用程序WinExe外部依赖System.Configuration.ConfigurationManager 10.0.12四、项目结构土壤湿度传感器/ ├── 土壤湿度传感器.sln # 解决方案文件 ├── packages/ # NuGet 包目录 │ └── System.Configuration.ConfigurationManager.10.0.12/ └── 土壤湿度传感器/ # 项目主目录 ├── App.config # 应用配置文件串口参数 ├── packages.config # NuGet 包引用配置 ├── Program.cs # 程序入口点 ├── Form1.cs # 主窗体逻辑串口通信、协议解析 ├── Form1.Designer.cs # 窗体设计器自动生成代码 ├── Form1.resx # 窗体资源文件 ├── Properties/ │ ├── AssemblyInfo.cs # 程序集信息 │ ├── Resources.resx # 资源文件 │ ├── Settings.settings # 设置文件 │ └── ... └── bin/Debug/ # 编译输出目录 └── 土壤湿度传感器.exe # 可执行文件核心文件说明Program.cs应用程序主入口调用Application.Run(new Form1())启动主窗体。Form1.cs核心业务逻辑包含串口初始化、Modbus 请求帧构造、CRC16 校验、数据接收与解析。Form1.Designer.cs界面布局定义由设计器自动生成。App.config存储串口配置参数支持运行时修改无需重新编译。五、核心功能实现5.1 串口初始化与配置程序启动时在Form1_Load事件中完成串口初始化。串口参数从App.config的appSettings节点读取实现了配置与代码分离。privatevoidForm1_Load(objectsender,EventArgse){_serialPort.PortNameCOM1;_serialPort.BaudRate9600;MessageBox.Show(ConfigurationManager.AppSettings[PortName]);_serialPort.DataBitsint.Parse(ConfigurationManager.AppSettings[DataBits]);_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});}}注意代码中PortName被硬编码为COM1而App.config中配置的是COM3。实际使用时应将硬编码改为从配置读取_serialPort.PortName ConfigurationManager.AppSettings[PortName];5.2 Modbus RTU 协议帧构造点击读取按钮后系统调用Read方法向传感器发送读保持寄存器请求。请求帧共 8 字节由ReadingData方法构造。请求帧格式8 字节字节序号12345678含义从站地址功能码起始地址高位起始地址低位寄存器数量高位寄存器数量低位CRC低字节CRC高字节示例值0x010x030x010xF40x000x01CRC_LCRC_H响应帧格式7 字节字节序号1234567含义从站地址功能码有效字节数数据高位数据低位CRC低字节CRC高字节privatevoidbutton1_Click(objectsender,EventArgse){if(!_serialPort.IsOpen)return;Read(0x1F4,1);// 读取寄存器地址 0x01F4十进制500数量1个}privatevoidRead(ushortregisterAddress,ushortnum){byte[]_dataReadingData(1,3,registerAddress,num);_serialPort.Write(_data,0,_data.Length);}privatebyte[]ReadingData(byteslaveAddress,bytefunction,ushortregisterAddress,ushortnumber){byte[]data1newbyte[6];data1[0]slaveAddress;data1[1]function;// 起始地址高位在前低位在后data1[2]Convert.ToByte(registerAddress8255);data1[3]Convert.ToByte(registerAddress255);// 读取数量高位在前低位在后data1[4]Convert.ToByte(number8255);data1[5]Convert.ToByte(number255);// 附加 CRC16 校验码byte[]crcCrc16(data1);byte[]datanewbyte[8];Array.Copy(data1,0,data,0,6);Array.Copy(crc,0,data,6,2);returndata;}关键位运算说明registerAddress 8将 16 位数据右移 8 位取高 8 位 255按位与11111111确保结果为 0~255 的单字节本项目读取的寄存器地址为0x01F4十进制 5005.3 CRC16 校验算法Modbus RTU 协议使用 CRC16 循环冗余校验确保数据完整性。本项目实现了标准的 Modbus CRC16 算法多项式为0xA001即0x8005的位反转形式初始值为0xFFFF。privatebyte[]Crc16(byte[]data){ushortcrc160xFFFF;// 初始化for(inti0;idata.Length;i){crc16^(ushort)data[i];// 与当前字节异或for(intj0;j8;j)// 每个字节处理8位{if((crc160x0001)!0)// 检查最低位{crc161;// 右移一位crc16^0xA001;// 异或多项式}else{crc161;// 仅右移不异或}}}// 返回结果低字节在前高字节在后Modbus 字节序returnnewbyte[]{(byte)(crc160xFF),(byte)(crc168),};}算法要点初始化 CRC 寄存器为0xFFFF将每个数据字节与 CRC 寄存器低 8 位异或对每个字节循环 8 次每次检查最低位最低位为 1 时右移后异或0xA001最低位为 0 时仅右移最终结果按低字节在前、高字节在后的顺序附加到数据帧末尾5.4 数据接收与解析传感器返回响应帧后触发DataReceived事件。事件处理方法在后台线程中运行因此需要通过Invoke切换到 UI 线程更新控件。privatevoid_serialPort_DataReceived(objectsender,SerialDataReceivedEventArgse){byte[]buffernewbyte[_serialPort.BytesToRead];intnum_serialPort.Read(buffer,0,buffer.Length);if(num0)return;bytefuncbuffer[1];// 功能码bytecountbuffer[2];// 有效字节数if(func3)// 功能码03读保持寄存器响应{Invoke(newAction((){if(count2)// 一个寄存器占2字节{// 高低位合并高位 * 256 低位stringdataStr((buffer[3]*256buffer[4])).ToString();txtwendu.TextdataStr;}}));}}高低位合并原理Modbus 协议中每个保持寄存器为 16 位分高字节和低字节传输。以数值 300 为例二进制0000 0001 0010 1100高字节buffer[3]0000 0001 十进制 1低字节buffer[4]0010 1100 十进制 44正确合并1 × 256 44 300错误做法直接相加1 44 45六、界面设计主窗体Form1采用分组式布局背景色为RosyBrown玫瑰棕色包含两个主要分组框。6.1 窗体属性属性值窗体大小817 × 556背景色RosyBrown文本Form16.2 控件布局groupBox1 — 串口信息左侧属性值位置(39, 35)大小189 × 425背景色Chocolate巧克力色文本串口信息该分组框当前为空容器预留用于显示串口状态、端口列表等信息。groupBox2 — 设备数据读取右侧属性值位置(256, 35)大小189 × 425背景色Salmon橙红色文本设备数据读取groupBox2 内包含以下控件控件名称位置说明Labellabel1(6, 35)显示PM2.5:TextBoxtextBox1(68, 32)PM2.5 数据显示框预留Labellabel2(6, 77)显示电导率TextBoxtxtwendu(68, 74)电导率数据显示框实际接收传感器数据Buttonbutton1(109, 164)读取按钮点击发送读取指令RadioButtonradioButton1(109, 204)实时单选按钮预留自动读取功能说明界面中虽然有 PM2.5 和电导率两个数据项但当前代码仅实现了电导率txtwendu的数据接收与显示。radioButton1实时模式和_timer定时器已声明但尚未实现自动轮询逻辑。七、配置文件说明App.config中定义了串口通信的全部参数修改后无需重新编译即可生效。?xml version1.0 encodingutf-8 ?configurationstartupsupportedRuntimeversionv4.0sku.NETFramework,Versionv4.7.2//startupappSettingsaddkeyPortNamevalueCOM3/addkeyBaudRatevalue9600/addkeyDataBitsvalue8/addkeyParityvalueNone/addkeyStopBitsvalueOne/addkeySlaveIdvalue1//appSettings/configuration配置项值说明PortNameCOM3串口号需根据实际连接修改BaudRate9600波特率需与传感器一致DataBits8数据位ParityNone校验位None/Even/Odd/Mark/SpaceStopBitsOne停止位One/OnePointFive/TwoSlaveId1Modbus 从站地址八、使用说明8.1 硬件连接将土壤湿度传感器通过 RS485 转 USB 模块连接到电脑在设备管理器中确认串口号如 COM3修改App.config中的PortName为实际串口号确认传感器的波特率、从站地址与配置一致8.2 运行步骤用 Visual Studio 打开土壤湿度传感器.sln按F5编译并运行或直接执行bin/Debug/土壤湿度传感器.exe程序启动后自动打开串口会弹出消息框显示配置的端口名点击读取按钮向传感器发送 Modbus 请求电导率数值将显示在对应文本框中8.3 注意事项确保串口未被其他程序占用代码中PortName硬编码为COM1如需使用配置文件中的COM3需修改Form1_Load中的代码当前仅实现了单次读取实时单选按钮和定时器功能尚未完成txtwendu控件命名为温度拼音但实际显示的是电导率数据Modbus 寄存器地址0x01F4500需根据传感器手册确认是否正确九、可扩展方向自动轮询利用已声明的_timer定时器结合实时单选按钮实现数据自动刷新多寄存器读取扩展Read方法支持读取多个连续寄存器同时显示湿度、温度、电导率等多组数据串口动态选择在 groupBox1 中添加串口列表下拉框和打开/关闭按钮支持运行时切换端口数据校验在接收端增加 CRC 校验验证确保响应帧数据完整性数据记录添加数据日志功能将读取的历史数据保存到文件或数据库异常处理完善串口断开、数据超时等异常场景的处理机制十、完整核心代码Form1.csusingSystem;usingSystem.IO.Ports;usingSystem.Configuration;usingSystem.Windows.Forms;namespace土壤湿度传感器{publicpartialclassForm1:Form{SerialPort_serialPortnewSerialPort();System.Timers.Timer_timernewSystem.Timers.Timer();publicForm1(){InitializeComponent();_timer.Interval1000;_serialPort.DataReceived_serialPort_DataReceived;}// 数据接收事件解析Modbus响应帧privatevoid_serialPort_DataReceived(objectsender,SerialDataReceivedEventArgse){byte[]buffernewbyte[_serialPort.BytesToRead];intnum_serialPort.Read(buffer,0,buffer.Length);if(num0)return;bytefuncbuffer[1];bytecountbuffer[2];if(func3){Invoke(newAction((){if(count2){// 高低位合并高位 * 256 低位stringdataStr((buffer[3]*256buffer[4])).ToString();txtwendu.TextdataStr;}}));}}// 窗体加载初始化串口privatevoidForm1_Load(objectsender,EventArgse){_serialPort.PortNameCOM1;_serialPort.BaudRate9600;_serialPort.DataBitsint.Parse(ConfigurationManager.AppSettings[DataBits]);_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});}}// 读取按钮点击事件privatevoidbutton1_Click(objectsender,EventArgse){if(!_serialPort.IsOpen)return;Read(0x1F4,1);}// 发送读保持寄存器请求privatevoidRead(ushortregisterAddress,ushortnum){byte[]_dataReadingData(1,3,registerAddress,num);_serialPort.Write(_data,0,_data.Length);}// 构造Modbus RTU请求帧8字节privatebyte[]ReadingData(byteslaveAddress,bytefunction,ushortregisterAddress,ushortnumber){byte[]data1newbyte[6];data1[0]slaveAddress;data1[1]function;data1[2]Convert.ToByte(registerAddress8255);data1[3]Convert.ToByte(registerAddress255);data1[4]Convert.ToByte(number8255);data1[5]Convert.ToByte(number255);byte[]crcCrc16(data1);byte[]datanewbyte[8];Array.Copy(data1,0,data,0,6);Array.Copy(crc,0,data,6,2);returndata;}// Modbus CRC16校验算法privatebyte[]Crc16(byte[]data){ushortcrc160xFFFF;for(inti0;idata.Length;i){crc16^(ushort)data[i];for(intj0;j8;j){if((crc160x0001)!0){crc161;crc16^0xA001;}else{crc161;}}}returnnewbyte[]{(byte)(crc160xFF),(byte)(crc168)};}}}
