Modbus 通讯协议学习总结
一、Modbus 协议概述
1.1 什么是 Modbus
Modbus 是一种开放的串行通讯协议,主要用于工业领域的智能监控场景中。它定义了数据在传输过程中的格式标准,是全球工业设备中最常用的通讯协议之一。
1.2 Modbus 协议的三种格式
| 协议格式 | 说明 |
|---|---|
| Modbus-RTU | 基于二进制的协议,采用 CRC16 校验,适用于 RS-485、RS-232 等串行通讯 |
| Modbus-ASCII | 基于 ASCII 码的协议,采用 LRC 校验,可读性好但效率低 |
| Modbus-TCP | 基于 TCP/IP 网络的协议,无需 CRC 校验,适用于以太网通讯 |
本篇内容主要讲解 Modbus-RTU 协议格式。
二、Modbus 主从站架构
2.1 主站(Master)
- 至少存在一个主站设备
- 通常是一台运行软件的主机(如监控计算机)
- 负责向从站发送指令
2.2 从站(Slave)
- 执行系统参数测量和控制设备的开关状态
- 接收到主站指令后,需要回复主站(响应指令)
- 每个从站有唯一的地址(默认从站地址为
01)
2.3 通讯流程
主站发送指令 → 从站接收指令 → 从站响应主站 → 主站接收响应
三、Modbus-RTU 帧结构
3.1 请求帧(主站发送)
| 字段 | 字节数 | 说明 | 示例值 |
|---|---|---|---|
| 从机地址 | 1 字节 | 目标从站地址 | 01 |
| 功能码 | 1 字节 | 操作类型 | 03(读取寄存器) |
| 起始地址 | 2 字节 | 高位在前,低位在后 | 00 00 |
| 数据长度 | 2 字节 | 高位在前,低位在后 | 00 04(读取4个寄存器) |
| 校验码 (CRC16) | 2 字节 | 低位在前,高位在后 | 09 44 |
完整请求帧示例:
01 03 00 00 00 04 09 44
含义:从地址为 01 的设备,从寄存器地址 0000 开始,读取 4 个寄存器。
3.2 响应帧(从站回复)
| 字段 | 字节数 | 说明 | 示例值 |
|---|---|---|---|
| 从机地址 | 1 字节 | 响应的从站地址 | 01 |
| 功能码 | 1 字节 | 响应的功能码 | 03 |
| 有效数据长度 | 1 字节 | 返回数据字节数 | 08(4个寄存器 × 2字节) |
| 数据 | N 字节 | 寄存器数据 | 00 03 00 04 00 05 00 06 |
| 校验码 (CRC16) | 2 字节 | 低位在前,高位在后 | 11 12 |
完整响应帧示例:
01 03 08 00 03 00 04 00 05 00 06 11 12
含义:从站 01 响应,返回了 4 个寄存器的值:0003、0004、0005、0006。
3.3 常用功能码
| 功能码 | 说明 |
|---|---|
01 |
读取线圈状态(开关量) |
02 |
读取离散输入状态 |
03 |
读取保持寄存器(模拟量) |
04 |
读取输入寄存器 |
05 |
写单个线圈 |
06 |
写单个寄存器 |
0F |
写多个线圈 |
10 |
写多个寄存器 |
四、CRC16 校验算法
4.1 CRC 校验概述
CRC(Cyclic Redundancy Check,循环冗余校验)是一种用于校验数据传输是否出错的算法。Modbus-RTU 采用 CRC16 校验。
4.2 CRC16-Modbus 参数
| 参数 | 值 |
|---|---|
| 初始值 | 0xFFFF |
| 多项式 | 0xA001(反转多项式) |
| 校验范围 | 从机地址到数据长度(共 6 字节) |
4.3 CRC16 计算步骤
- 初始化 :将 16 位寄存器初始化为
0xFFFF - 逐字节处理:将待校验数据的每个字节与 CRC 寄存器进行异或
- 逐位处理 :对每个字节的 8 个 bit 进行以下操作:
- 取出 CRC 寄存器的最低位
- CRC 寄存器右移 1 位
- 如果被移出的位是 1,则 CRC 寄存器与
0xA001异或
- 高低位互换:计算完成后,将 CRC 值的高低字节互换
4.4 CRC16 代码实现
csharp
/// <summary>
/// 生成CRC16校验码
/// </summary>
/// <param name="data">待校验的字节数组</param>
/// <returns>CRC校验码(2字节,低位在前)</returns>
public static byte[] CRC16(byte[] data)
{
int crc = 0xFFFF; // 初始化CRC寄存器
// 只对前 Length-2 字节进行校验(排除最后2字节的校验码位置)
for (int i = 0; i < data.Length - 2; i++)
{
crc = crc ^ data[i]; // 当前字节与CRC寄存器异或
for (int j = 0; j < 8; j++) // 处理8个bit
{
int temp = crc & 1; // 取出最低位
crc = crc >> 1; // 右移1位
crc = crc & 0x7FFF; // 确保最高位为0
if (temp == 1)
{
crc = crc ^ 0xA001; // 移出位为1时与多项式异或
}
crc = crc & 0xFFFF; // 确保只有16位有效
}
}
// 高低位互换(Modbus协议要求:低字节在前,高字节在后)
byte[] crc16 = new byte[2];
crc16[1] = (byte)((crc >> 8) & 0xFF); // 高位
crc16[0] = (byte)(crc & 0xFF); // 低位
return crc16;
}
4.5 CRC 校验示意图
数据字节 → 异或CRC寄存器 → 逐位右移判断 → 与多项式异或 → 最终CRC值
五、串口通信配置
5.1 串口参数
| 参数 | 说明 | 常用值 |
|---|---|---|
| 波特率 | 每秒传输的符号数 | 9600, 19200, 115200 |
| 数据位 | 每个数据的位数 | 8 |
| 停止位 | 传输结束的停止位数 | 1 |
| 校验位 | 数据校验方式 | None(无), Odd(奇), Even(偶) |
5.2 串口初始化代码
csharp
/// <summary>
/// 初始化串口参数
/// </summary>
private void InitSerialPort()
{
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(); // 打开串口
}
六、Modbus 通讯完整流程
6.1 请求帧构建与发送
csharp
/// <summary>
/// 发送Modbus读取请求帧
/// </summary>
public void SendData()
{
// 1. 构建请求帧(8字节)
byte[] buffer = new byte[8];
buffer[0] = 0x01; // 从机地址
buffer[1] = 0x03; // 功能码:读取保持寄存器
buffer[2] = 0x00; // 起始地址高位
buffer[3] = 0x00; // 起始地址低位
buffer[4] = 0x00; // 数据长度高位
buffer[5] = 0x04; // 数据长度低位(读取4个寄存器)
// 2. 计算CRC16校验码
byte[] crc = CRCHelper.CRC16(buffer);
buffer[6] = crc[0]; // 校验码低位
buffer[7] = crc[1]; // 校验码高位
// 3. 发送数据
serialPort1.Write(buffer, 0, buffer.Length);
}
6.2 响应帧接收与解析
csharp
/// <summary>
/// 串口数据接收事件处理
/// </summary>
private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// 1. 读取接收缓冲区数据
byte[] bs = new byte[serialPort1.BytesToRead];
serialPort1.Read(bs, 0, bs.Length);
// 2. 验证响应帧长度(13字节:1+1+1+8+2)
if (bs.Length != 13) return;
// 3. 解析数据(寄存器数据为高位在前,低位在后)
int a = bs[3] * 256 + bs[4]; // 第1个寄存器值
int b = bs[5] * 256 + bs[6]; // 第2个寄存器值
int c = bs[7] * 256 + bs[8]; // 第3个寄存器值
int d = bs[9] * 256 + bs[10]; // 第4个寄存器值
// 4. 更新UI显示(使用Invoke跨线程更新)
Invoke(new Action(() =>
{
textBox1.Text = a.ToString();
textBox2.Text = b.ToString();
textBox3.Text = c.ToString();
textBox4.Text = d.ToString();
}));
}
6.3 通讯流程图
┌─────────────┐ 构建请求帧 ┌─────────────┐
│ 初始化串口 │ ──────────────→ │ 计算CRC16 │
└─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐ 发送帧数据 ┌─────────────┐
│ 更新UI显示 │ ←────────────── │ 发送请求帧 │
└─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ 等待响应帧 │
└──────┬──────┘
│
▼
┌─────────────┐ 解析数据 ┌─────────────┐
│ 数据解析显示 │ ←────────────── │ 接收响应帧 │
└─────────────┘ └─────────────┘
七、关键知识点总结
7.1 Modbus-RTU 协议要点
| 要点 | 说明 |
|---|---|
| 字节序 | 数据字段高位在前(Big-Endian),校验码低位在前 |
| 寄存器长度 | 一个寄存器 = 2 字节(16 位) |
| 响应帧组成 | 地址 + 功能码 + 数据长度 + 数据 + CRC |
| 异常响应 | 功能码最高位置 1(如 0x83 表示功能码 0x03 异常) |
7.2 数据解析要点
csharp
// 寄存器值解析:高位字节 × 256 + 低位字节
int value = highByte * 256 + lowByte;
// 示例:数据字节为 [0x01, 0xF4]
// value = 1 * 256 + 244 = 500
7.3 线程安全
使用 Invoke 方法在串口数据接收事件中更新 UI,避免跨线程操作异常:
csharp
Invoke(new Action(() =>
{
// 更新UI控件
textBox1.Text = value.ToString();
}));
八、常见问题与注意事项
8.1 通讯失败排查
- 检查串口参数:波特率、数据位、停止位、校验位是否与从站一致
- 检查从站地址:地址是否正确(默认 01)
- 检查接线:RS-485 需要注意 A/B 线连接
- 检查 CRC 校验:确保校验码计算正确
8.2 数据解析注意
- 响应帧数据为 高位在前 ,解析时需
高位 × 256 + 低位 - CRC 校验码为 低位在前,与数据字段相反
- 不同寄存器类型(保持寄存器/输入寄存器)需使用对应功能码
8.3 异常响应码
| 异常码 | 含义 |
|---|---|
01 |
非法功能码 |
02 |
非法数据地址 |
03 |
非法数据值 |
04 |
从站设备故障 |