整体项目概述
本项目基于 WinForm + SerialPort 实现标准 Modbus-RTU 工业串口通信,集成三大核心功能:
标准 Modbus-CRC16 数据校验算法(保证传输数据无误)
Modbus-RTU 03功能码读取寄存器、06功能码写入单个寄存器
传感器数值超限自动邮件告警功能 + 定时器轮询采集
项目架构分层:工具辅助类(CRC+邮件)+ 主界面业务逻辑(串口+组包+收发+解析)
第一部分:项目核心理论基础(必背)
1.1 Modbus 三大协议
Modbus-RTU:二进制字节传输、CRC16校验、工业最常用(本项目采用)
Modbus-ASCII:字符串传输、LRC校验、可读性高、传输慢
Modbus-TCP:以太网网络协议,网口通信
1.2 Modbus 主从通信机制
主站:本电脑软件,主动发送读写指令,掌握通信主动权
从站:传感器、模拟设备、PLC、单片机,被动响应指令
通信规则:一问一答,主站请求、从站响应,从站不会主动上传数据
1.3 常用功能码释义
03:读取保持寄存器(可读可写、数值型寄存器、本项目核心读取功能)
06:写入单个保持寄存器(定点写入单地址数值)
01:读取线圈状态(开关量0/1)
04:读取输入寄存器(只读传感器数据)
16:批量写入多个寄存器
1.4 Modbus-RTU 标准帧结构
1.4.1 读取请求帧(03功能码、8字节)
结构:从机地址(1B) + 功能码(1B) + 起始地址(2B大端) + 读取数量(2B大端) + CRC16(2B小端)
示例:01 03 00 00 00 04 CRC低 CRC高
1.4.2 读取响应帧(13字节)
结构:从机地址(1B) + 功能码(1B) + 数据长度(1B) + 有效数据(8B) + CRC16(2B)
4个寄存器 = 4×2 = 8字节有效数据
1.4.3 写入请求帧(06功能码、8字节)
结构:从机地址 + 06 + 写入地址(大端) + 写入数值(大端) + CRC校验
1.5 字节序核心规则(重难点)
寄存器地址、寄存器数量、写入数值:大端模式(高位在前、低位在后)
CRC16校验码:小端模式(低位在前、高位在后,协议强制)
寄存器数值解析公式:真实值 = 高字节 × 256 + 低字节
第二部分:工具类一 CRC16 校验帮助类(完整源码+逐行解析)
2.1 算法固定参数(Modbus 标准)
16位寄存器初始值:0xFFFF
生成多项式:0xA001(反转多项式)
运算范围:只计算前6位有效指令,最后2位CRC占位字节不参与运算
输出规则:高低字节互换,适配小端传输规则
2.2 完整可运行源码
using System; namespace 串口读写威盟士五插针.Helper { /// <summary> /// Modbus-RTU 标准 CRC16 校验工具类 /// 运算逻辑:逐位右移、判位异或、16位寄存器迭代 /// 传输规则:CRC低位在前,高位在后 /// </summary>> public class CRCHelper { /// <summary> /// 计算Modbus标准CRC16校验码 /// </summary>> /// <param name="data">8字节指令数组(后两位为CRC占位)</param>> /// <returns>2字节CRC校验数组(低位在前)</returns>> public static byte[] CRC16(byte[] data) { // 初始化16位CRC寄存器(Modbus固定初始值) int crc = 0xffff; Console.WriteLine($"========== 开始计算CRC16 =========="); Console.WriteLine($"初始值: {crc:X4} (0xFFFF)"); Console.WriteLine($"数据长度: {data.Length} 字节\n"); // 遍历有效数据:排除末尾2位CRC占位字节 for (int i = 0; i < data.Length - 2; i++) { Console.WriteLine($"--- 处理第 {i + 1} 个字节: 0x{data[i]:X2} ---"); Console.WriteLine($"处理前 CRC = {crc:X4}"); // 1.当前字节与CRC寄存器低8位异或 crc = crc ^ data[i]; Console.WriteLine($"异或后 CRC = {crc:X4} (0x{data[i]:X2} 异或进寄存器)"); // 2.一个字节8个Bit,逐位运算8次 for (int j = 0; j < 8; j++) { // 取出CRC寄存器最低位(0/1) int temp = crc & 1; Console.Write($" 第{j + 1}位: 最低位={temp}, "); // 寄存器整体右移1位,丢弃最低位 crc = crc >> 1; Console.Write($"右移后={crc:X4}, "); // 清零最高位,防止int符号位产生负数 crc = crc & 0x7fff; // 移出位为1,与标准多项式异或修正 if (temp == 1) { crc = crc ^ 0xa001; Console.Write($"异或0xA001后={crc:X4}"); } // 截断高位,强制保留16位有效数据 crc = crc & 0xffff; Console.WriteLine(); } Console.WriteLine($"不移位(保持={crc:X4})\n"); } Console.WriteLine($"========== 计算完成 =========="); Console.WriteLine($"最终CRC值: {crc:X4}"); // 适配Modbus协议:高低字节互换 byte[] crc16 = new byte[2]; crc16[1] = (byte)((crc >> 8) & 0xff); // 高字节后置 crc16[0] = (byte)(crc & 0xff); // 低字节前置 return crc16; } } }2.3 核心代码逐行解析
data.Length - 2:核心关键,跳过CRC占位字节,否则校验结果错误crc ^ data[i]:将当前有效字节融入16位寄存器crc & 1:按位与运算,精准获取二进制最低位crc >> 1:逻辑右移,丢弃最低Bit,模拟寄存器移出数据crc & 0x7fff:清零最高位,消除负数异常crc ^ 0xA001:Modbus专用多项式修正运算crc & 0xFFFF:截断int高位垃圾数据,保证16位有效字节互换:唯一适配Modbus小端传输的关键步骤
第三部分:工具类二 邮件告警帮助类(MailHelper)
3.1 功能说明
当传感器读取的数值(温度)大于100时,自动调用SMTP服务发送QQ邮箱告警邮件,实现设备异常远程提醒。
3.2 完整源码
using System; using System.Net; using System.Net.Mail; using System.Text; namespace _1使用modbus读取模拟设备的数据 { /// <summary> /// QQ邮箱SMTP邮件发送帮助类 /// 用于设备数据超限自动告警 /// </summary>> public class MailHelper { /// <summary> /// 发送邮件方法 /// </summary>> /// <param name="mailTo">接收人邮箱</param>> /// <param name="title">邮件标题</param>> /// <param name="content">邮件正文</param>> /// <returns>发送成功返回true,失败抛异常</returns>> public static bool SendMail(string mailTo, string title, string content) { // 1.配置QQ邮箱SMTP服务器信息 string smtpStr = "smtp.qq.com"; string mailCount = "1181569019@qq.com"; // 2.创建SMTP客户端对象 SmtpClient myClinet = new SmtpClient(); myClinet.DeliveryMethod = SmtpDeliveryMethod.Network; // 网络模式发送 myClinet.Host = smtpStr; // 指定SMTP服务器 myClinet.EnableSsl = true; // 开启安全加密协议 myClinet.UseDefaultCredentials = true; // 使用身份验证 myClinet.Credentials = new NetworkCredential(mailCount, "ctmziagahvyjjceh"); // 账号+授权码 // 3.创建邮件实体 MailMessage mailMessage = new MailMessage(mailCount, mailTo); mailMessage.Subject = title; // 邮件标题 mailMessage.Body = content; // 邮件内容 mailMessage.IsBodyHtml = false; // 不启用HTML格式 mailMessage.BodyEncoding = Encoding.UTF8; // UTF8防止乱码 mailMessage.Priority = MailPriority.Normal; // 普通优先级 // 4.执行发送 try { myClinet.Send(mailMessage); return true; } catch (Exception ex) { throw ex; } } } }3.3 关键知识点
QQ邮箱必须开启SMTP服务,使用授权码而非登录密码
EnableSsl=true 必须开启,否则现代邮箱服务器拒绝发送
UTF8编码解决中文标题、内容乱码问题
第四部分:主界面核心业务逻辑 Form1.cs
4.1 窗体加载事件
作用:初始化默认串口参数、自动扫描本机所有串口
private void Form1_Load(object sender, EventArgs e) { comboBox1.SelectedIndex = 1; // 默认波特率 comboBox2.SelectedIndex = 0; // 默认停止位 comboBox3.SelectedIndex = 0; // 默认奇偶校验 comboBox4.SelectedIndex = 3; // 默认数据位 comboBox5.DataSource = SerialPort.GetPortNames(); // 自动加载串口列表 }4.2 串口开关功能
实现串口打开/关闭切换,打开后锁定参数,防止通信中误修改
public bool isOpen = true; private void button1_Click(object sender, EventArgs e) { if (isOpen) { // 赋值串口通信参数 serialPort1.PortName = comboBox5.SelectedItem.ToString(); serialPort1.BaudRate = Convert.ToInt32(comboBox1.SelectedItem.ToString()); serialPort1.DataBits = Convert.ToInt32(comboBox4.SelectedItem.ToString()); serialPort1.StopBits = (StopBits)Enum.Parse(typeof(StopBits), comboBox2.SelectedItem.ToString()); serialPort1.Parity = (Parity)Enum.Parse(typeof(Parity), comboBox3.SelectedItem.ToString()); // 防重复打开 if (!serialPort1.IsOpen) serialPort1.Open(); // 锁定所有配置下拉框 comboBox1.Enabled = false; comboBox2.Enabled = false; comboBox3.Enabled = false; comboBox4.Enabled = false; comboBox5.Enabled = false; button1.Text = "关闭"; } else { // 关闭串口释放资源 if (serialPort1.IsOpen) { serialPort1.Close(); } button1.Text = "打开"; } isOpen = !isOpen; }4.3 03功能码组包发送(读取寄存器)
自动组装标准8字节读取指令、计算CRC、下发串口
public void SendData() { // 8字节指令数组,后两位预留CRC byte[] buffer = new byte[8]; buffer[0] = 0x01; // 从站地址 buffer[1] = 0x03; // 读取保持寄存器功能码 buffer[2] = 0x00; // 起始地址高字节 buffer[3] = 0x00; // 起始地址低字节 buffer[4] = 0x00; // 读取数量高字节 buffer[5] = 0x04; // 读取数量低字节(读取4个寄存器) // 计算CRC校验 byte[] s = CRCHelper.CRC16(buffer); buffer[6] = s[0]; // CRC低位在前 buffer[7] = s[1]; // CRC高位在后 // 串口发送完整帧 serialPort1.Write(buffer, 0, buffer.Length); } // 手动读取按钮 private void button2_Click(object sender, EventArgs e) { SendData(); }4.4 串口数据接收与解析 + 邮件告警
区分03读取响应、06写入响应,解析4路寄存器数据,温度超限自动发邮件,跨线程刷新UI
private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e) { // 读取缓冲区所有数据 byte[] bs = new byte[serialPort1.BytesToRead]; serialPort1.Read(bs, 0, bs.Length); // 获取功能码,区分读写响应 byte funcode = bs[1]; // 03 读取寄存器响应解析 if (funcode == 0x03) { // 固定13字节帧校验,过滤脏数据 if (bs.Length != 13) return; string str = BitConverter.ToString(bs).Replace("-", " "); Console.WriteLine(str); // 大端数据解析公式:高*256+低 int a = bs[3] * 256 + bs[4]; int b = bs[5] * 256 + bs[6]; int c = bs[7] * 256 + bs[8]; int d = bs[9] * 256 + bs[10]; // 温度超限邮件告警 if (b > 100) { MailHelper.SendMail("710674@qq.com", "设备温度警告", $"当前设备温度为{b}℃,超出阈值,请及时降温!"); } // 跨线程更新UI Invoke(new Action(() => { textBox1.Text = a + ""; textBox2.Text = b + ""; textBox3.Text = c + ""; textBox4.Text = d + ""; })); } // 06 写入寄存器响应 else if (funcode == 0x06) { if (bs.Length != 8) return; Invoke(new Action(() => { MessageBox.Show("写入数据成功"); })); } }4.5 定时器轮询功能
勾选复选框自动定时采集数据,实现自动轮询监控
private void timer1_Tick(object sender, EventArgs e) { // 串口开启状态下持续读取数据 if (serialPort1.IsOpen) { SendData(); } } private void checkBox1_CheckedChanged(object sender, EventArgs e) { if (checkBox1.Checked) { // 开启轮询前校验串口状态 if (!serialPort1.IsOpen) { MessageBox.Show("串口未打开,无法自动轮询!"); checkBox1.Checked = false; return; } timer1.Start(); } else { timer1.Stop(); } }4.6 06功能码动态写入寄存器(自定义地址+数值)
支持界面输入从站地址、寄存器地址、写入数值,自动组包、CRC校验、下发写入指令
private void button3_Click(object sender, EventArgs e) { byte[] bs = new byte[8]; // 从站地址 bs[0] = Convert.ToByte(textBox5.Text); // 写入功能码 bs[1] = 0x06; // 解析写入地址,拆分高低字节(大端) int address = Convert.ToInt32(textBox6.Text); bs[2] = (byte)(address >> 8); bs[3] = (byte)(address & 0xff); // 解析写入数值,拆分高低字节(大端) int value = Convert.ToInt32(textBox7.Text); bs[4] = (byte)(value >> 8); bs[5] = (byte)(value & 0xff); // 计算并填充CRC校验 byte[] crc = CRCHelper.CRC16(bs); bs[6] = crc[0]; bs[7] = crc[1]; // 下发写入指令 serialPort1.Write(bs, 0, bs.Length); }4.7 指定地址固定值写入测试
private void button4_Click(object sender, EventArgs e) { byte[] bs = new byte[8]; bs[0] = 0x01; bs[1] = 0x06; // 固定写入地址 0x07D1 bs[2] = 0x07; bs[3] = 0xD1; // 固定写入数值 1 int value = 1; bs[4] = (byte)(value >> 8); bs[5] = (byte)(value & 0xff); byte[] crc = CRCHelper.CRC16(bs); bs[6] = crc[0]; bs[7] = crc[1]; serialPort1.Write(bs, 0, bs.Length); }第五部分:项目完整运行流程
程序启动,自动初始化串口默认参数、扫描本机串口
选择串口号,点击打开串口,锁定通信参数
点击读取按钮,手动发送03读取指令,或勾选自动轮询持续采集
程序组包 → CRC16校验 → 串口下发指令
下位机返回13字节响应帧,程序过滤脏数据、解析4路寄存器数值
温度数值超限自动触发SMTP邮件告警
支持手动输入地址和数值,06功能码写入设备寄存器
第六部分:核心重难点 & 易错点总结
6.1 CRC16算法易错点
必须排除后两位CRC占位字节参与运算,否则校验彻底错误
必须高低字节互换,严格遵循Modbus小端传输规则
必须截断16位数据,防止int高位垃圾数据干扰结果
6.2 串口通信易错点
波特率、数据位、停止位、校验位必须和下位机完全一致
DataReceived是子线程,禁止直接操作UI,必须使用Invoke
必须增加帧长度校验,过滤粘包、分包、干扰脏数据
6.3 协议字节序易错点
地址、数值:大端模式(高前低后)
CRC校验:小端模式(低前高后)
双字节数值必须通过
高*256+低换算,不能直接拼接
6.4 邮件告警易错点
必须开启QQ邮箱SMTP服务、使用授权码登录
必须开启Ssl加密、UTF8编码防止中文乱码
第七部分:项目核心亮点
标准工业Modbus-RTU协议,可对接真实PLC、传感器设备
自研标准CRC16校验,保证传输数据可靠性
集成数据超限邮件告警,实现远程设备监控
支持手动单次读取 + 定时器自动轮询采集
支持自定义地址、自定义数值写入寄存器
完善容错机制、帧校验、跨线程UI安全更新