news 2026/9/24 12:34:54

土壤湿度传感器上位机系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
土壤湿度传感器上位机系统

土壤湿度传感器上位机系统

一、项目概述

本项目是一个基于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.configappSettings节点读取,实现了配置与代码分离。

privatevoidForm1_Load(objectsender,EventArgse){_serialPort.PortName="COM1";_serialPort.BaudRate=9600;MessageBox.Show(ConfigurationManager.AppSettings["PortName"]);_serialPort.DataBits=int.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[]_data=ReadingData(1,3,registerAddress,num);_serialPort.Write(_data,0,_data.Length);}privatebyte[]ReadingData(byteslaveAddress,bytefunction,ushortregisterAddress,ushortnumber){byte[]data1=newbyte[6];data1[0]=slaveAddress;data1[1]=function;// 起始地址:高位在前,低位在后data1[2]=Convert.ToByte(registerAddress>>8&255);data1[3]=Convert.ToByte(registerAddress&255);// 读取数量:高位在前,低位在后data1[4]=Convert.ToByte(number>>8&255);data1[5]=Convert.ToByte(number&255);// 附加 CRC16 校验码byte[]crc=Crc16(data1);byte[]data=newbyte[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(十进制 500)

5.3 CRC16 校验算法

Modbus RTU 协议使用 CRC16 循环冗余校验确保数据完整性。本项目实现了标准的 Modbus CRC16 算法,多项式为0xA001(即0x8005的位反转形式),初始值为0xFFFF

privatebyte[]Crc16(byte[]data){ushortcrc16=0xFFFF;// 初始化for(inti=0;i<data.Length;i++){crc16^=(ushort)data[i];// 与当前字节异或for(intj=0;j<8;j++)// 每个字节处理8位{if((crc16&0x0001)!=0)// 检查最低位{crc16>>=1;// 右移一位crc16^=0xA001;// 异或多项式}else{crc16>>=1;// 仅右移,不异或}}}// 返回结果:低字节在前,高字节在后(Modbus 字节序)returnnewbyte[]{(byte)(crc16&0xFF),(byte)(crc16>>8),};}

算法要点:

  1. 初始化 CRC 寄存器为0xFFFF
  2. 将每个数据字节与 CRC 寄存器低 8 位异或
  3. 对每个字节循环 8 次,每次检查最低位
  4. 最低位为 1 时:右移后异或0xA001;最低位为 0 时:仅右移
  5. 最终结果按低字节在前、高字节在后的顺序附加到数据帧末尾

5.4 数据接收与解析

传感器返回响应帧后,触发DataReceived事件。事件处理方法在后台线程中运行,因此需要通过Invoke切换到 UI 线程更新控件。

privatevoid_serialPort_DataReceived(objectsender,SerialDataReceivedEventArgse){byte[]buffer=newbyte[_serialPort.BytesToRead];intnum=_serialPort.Read(buffer,0,buffer.Length);if(num<=0)return;bytefunc=buffer[1];// 功能码bytecount=buffer[2];// 有效字节数if(func==3)// 功能码03:读保持寄存器响应{Invoke(newAction(()=>{if(count==2)// 一个寄存器占2字节{// 高低位合并:高位 * 256 + 低位stringdataStr=((buffer[3]*256+buffer[4])).ToString();txtwendu.Text=dataStr;}}));}}

高低位合并原理:

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
文本Form1

6.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 version="1.0" encoding="utf-8" ?><configuration><startup><supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.7.2"/></startup><appSettings><addkey="PortName"value="COM3"/><addkey="BaudRate"value="9600"/><addkey="DataBits"value="8"/><addkey="Parity"value="None"/><addkey="StopBits"value="One"/><addkey="SlaveId"value="1"/></appSettings></configuration>
配置项说明
PortNameCOM3串口号,需根据实际连接修改
BaudRate9600波特率,需与传感器一致
DataBits8数据位
ParityNone校验位(None/Even/Odd/Mark/Space)
StopBitsOne停止位(One/OnePointFive/Two)
SlaveId1Modbus 从站地址

八、使用说明

8.1 硬件连接

  1. 将土壤湿度传感器通过 RS485 转 USB 模块连接到电脑
  2. 在设备管理器中确认串口号(如 COM3)
  3. 修改App.config中的PortName为实际串口号
  4. 确认传感器的波特率、从站地址与配置一致

8.2 运行步骤

  1. 用 Visual Studio 打开土壤湿度传感器.sln
  2. F5编译并运行,或直接执行bin/Debug/土壤湿度传感器.exe
  3. 程序启动后自动打开串口(会弹出消息框显示配置的端口名)
  4. 点击"读取"按钮,向传感器发送 Modbus 请求
  5. 电导率数值将显示在对应文本框中

8.3 注意事项

  • 确保串口未被其他程序占用
  • 代码中PortName硬编码为COM1,如需使用配置文件中的COM3,需修改Form1_Load中的代码
  • 当前仅实现了单次读取,"实时"单选按钮和定时器功能尚未完成
  • txtwendu控件命名为"温度"拼音,但实际显示的是电导率数据
  • Modbus 寄存器地址0x01F4(500)需根据传感器手册确认是否正确

九、可扩展方向

  1. 自动轮询:利用已声明的_timer定时器,结合"实时"单选按钮实现数据自动刷新
  2. 多寄存器读取:扩展Read方法支持读取多个连续寄存器,同时显示湿度、温度、电导率等多组数据
  3. 串口动态选择:在 groupBox1 中添加串口列表下拉框和打开/关闭按钮,支持运行时切换端口
  4. 数据校验:在接收端增加 CRC 校验验证,确保响应帧数据完整性
  5. 数据记录:添加数据日志功能,将读取的历史数据保存到文件或数据库
  6. 异常处理:完善串口断开、数据超时等异常场景的处理机制

十、完整核心代码(Form1.cs)

usingSystem;usingSystem.IO.Ports;usingSystem.Configuration;usingSystem.Windows.Forms;namespace土壤湿度传感器{publicpartialclassForm1:Form{SerialPort_serialPort=newSerialPort();System.Timers.Timer_timer=newSystem.Timers.Timer();publicForm1(){InitializeComponent();_timer.Interval=1000;_serialPort.DataReceived+=_serialPort_DataReceived;}// 数据接收事件:解析Modbus响应帧privatevoid_serialPort_DataReceived(objectsender,SerialDataReceivedEventArgse){byte[]buffer=newbyte[_serialPort.BytesToRead];intnum=_serialPort.Read(buffer,0,buffer.Length);if(num<=0)return;bytefunc=buffer[1];bytecount=buffer[2];if(func==3){Invoke(newAction(()=>{if(count==2){// 高低位合并:高位 * 256 + 低位stringdataStr=((buffer[3]*256+buffer[4])).ToString();txtwendu.Text=dataStr;}}));}}// 窗体加载:初始化串口privatevoidForm1_Load(objectsender,EventArgse){_serialPort.PortName="COM1";_serialPort.BaudRate=9600;_serialPort.DataBits=int.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[]_data=ReadingData(1,3,registerAddress,num);_serialPort.Write(_data,0,_data.Length);}// 构造Modbus RTU请求帧(8字节)privatebyte[]ReadingData(byteslaveAddress,bytefunction,ushortregisterAddress,ushortnumber){byte[]data1=newbyte[6];data1[0]=slaveAddress;data1[1]=function;data1[2]=Convert.ToByte(registerAddress>>8&255);data1[3]=Convert.ToByte(registerAddress&255);data1[4]=Convert.ToByte(number>>8&255);data1[5]=Convert.ToByte(number&255);byte[]crc=Crc16(data1);byte[]data=newbyte[8];Array.Copy(data1,0,data,0,6);Array.Copy(crc,0,data,6,2);returndata;}// Modbus CRC16校验算法privatebyte[]Crc16(byte[]data){ushortcrc16=0xFFFF;for(inti=0;i<data.Length;i++){crc16^=(ushort)data[i];for(intj=0;j<8;j++){if((crc16&0x0001)!=0){crc16>>=1;crc16^=0xA001;}else{crc16>>=1;}}}returnnewbyte[]{(byte)(crc16&0xFF),(byte)(crc16>>8)};}}}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 12:34:52

无ST-LINK也能烧录STM32:USB DFU模式详解

/* 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 12:33:45

用Clang在Windows上交叉编译ARM Linux程序(告别GCC实战)

/* 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 12:33:30

从零打造开源游戏掌机:硬件选型、软件栈与端侧AI部署实战

/* 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 12:31:35

GaN快充批量失效元凶:X电容放电芯片可靠性剖析

/* 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 12:30:42

异常断电导致硬盘逻辑崩溃的原理与抢救指南

/* 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 12:30:12

电源防倒灌设计:从二极管到理想二极管控制器的工程演进

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

作者头像 李华