整体项目概述
本项目基于 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安全更新