C# Modbus-RTU 串口读写设备+CRC16+邮件告警

整体项目概述

本项目基于 WinForm + SerialPort 实现 标准 Modbus-RTU 工业串口通信,集成三大核心功能:

  1. 标准 Modbus-CRC16 数据校验算法(保证传输数据无误)

  2. Modbus-RTU 03功能码读取寄存器、06功能码写入单个寄存器

  3. 传感器数值超限自动邮件告警功能 + 定时器轮询采集

项目架构分层:工具辅助类(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&gt>
    public class CRCHelper
    {
        /// <summary>
        /// 计算Modbus标准CRC16校验码
        /// </summary&gt>
        /// <param name="data">8字节指令数组(后两位为CRC占位)</param&gt>
        /// <returns>2字节CRC校验数组(低位在前)</returns&gt>
        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&gt>
    public class MailHelper
    {
        /// <summary>
        /// 发送邮件方法
        /// </summary&gt>
        /// <param name="mailTo">接收人邮箱</param&gt>
        /// <param name="title">邮件标题</param&gt>
        /// <param name="content">邮件正文</param&gt>
        /// <returns>发送成功返回true,失败抛异常</returns&gt>
        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);
}

第五部分:项目完整运行流程

  1. 程序启动,自动初始化串口默认参数、扫描本机串口

  2. 选择串口号,点击打开串口,锁定通信参数

  3. 点击读取按钮,手动发送03读取指令,或勾选自动轮询持续采集

  4. 程序组包 → CRC16校验 → 串口下发指令

  5. 下位机返回13字节响应帧,程序过滤脏数据、解析4路寄存器数值

  6. 温度数值超限自动触发SMTP邮件告警

  7. 支持手动输入地址和数值,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安全更新

相关推荐
DS随心转小程序15 分钟前
Gemini只能导出了一部分Word文档,很少的一部分,不是我这个窗口所有的对话内容?
开发语言·人工智能·c#·word·豆包·deepseek·ai导出鸭
江畔柳前堤16 分钟前
消息队列:从直觉到精通的完整认知地图
开发语言·人工智能·算法·机器学习·ruby on rails·scala
格林威24 分钟前
C# 相机图像配合频闪光源:实现高速稳定拍摄的几个方法
开发语言·网络·人工智能·数码相机·计算机视觉·c#·视觉检测
布莱克60542 分钟前
单例模式详解:什么是单例模式,它能解决哪些问题?
开发语言·qt·单例模式
CSDN_RTKLIB1 小时前
CAD绘图单位
c#
charlie1145141911 小时前
deque、list 与 forward_list:vector 之外的三个选择
开发语言·数据结构·c++·list·开源项目
mayaairi1 小时前
JS DOM属性操作完全指南:内容、样式与自定义属性
开发语言·前端·javascript
feng_you_ying_li1 小时前
linux之Udpsocket实践
linux·运维·c#
avi91111 小时前
Threejs新版本后glb导出提示.x问题GLTFExporter,替代3dmax
开发语言·前端·javascript·threejs·glb·gltfexporter