一是什么
工业串口 问答 协议 走RS485/RS232,主站问,从站答
RTU(Remote Terminal Unit)远程终端单元
二RTU帧格式
| 从站地址 | 功能码 | 数据区 | CRC16 校验 |
|---|---|---|---|
| 1 字节 | 1 字节 | N 字节 | 2 字节(低字节在前) |
三串口基础
3.1引用
cs
using System.IO.Ports;
3.2串口参数(必须和从站完全一致!)
cs
SerialPort sp = new SerialPort("COM1")
{
BaudRate=9600,
DataBits =8,
Parity = StopBits.One,
ReadTimeout = 1000,
WriteTiemout = 1000
};
sp.Open();
sp.Close();
3.3CRC校验(Modbus)
发之前算,收之后验。 CRC 不包含自己的两个字节。
cs
// Modbus CRC16
public static ushort Crc16(byte[] data, int len)
{
ushort crc = 0xFFFF;
for (int i = 0; i < len; i++)
{
crc ^= data[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x0001) != 0)
crc = (ushort)((crc >> 1) ^ 0xA001);
else
crc >>= 1;
}
}
return crc;
}
使用
cs
byte[] frame = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x02 };
ushort crc = Crc16(frame, frame.Length);
// 拼到后面:低字节在前
byte[] sendBuf = frame.Concat(new byte[] { (byte)crc, (byte)(crc >> 8) }).ToArray();
四发送RTU请求
目标:从站地址 1,读寄存器 0,数量 2
cs
// 1. 构造帧(不含 CRC)
byte[] req = new byte[]
{
0x01, // 从站地址
0x03, // 功能码:读保持寄存器
0x00, 0x00, // 起始地址 0
0x00, 0x02 // 寄存器数量 2
};
// 2. 加 CRC
ushort crc = Crc16(req, req.Length);
byte[] send = new byte[req.Length + 2];
Buffer.BlockCopy(req, 0, send, 0, req.Length);
send[req.Length] = (byte)crc;
send[req.Length + 1] = (byte)(crc >> 8);
// 3. 串口发送
sp.Write(send, 0, send.Length);
五C# 接收 RTU 应答
应答帧格式(03 应答)
从站地址 03 字节数 N 数据1 数据2 ... CRC低 CRC高
接收 + 校验 + 解析
cs
// 收 1 帧(先按最大长度收,再处理)
byte[] recvBuf = new byte[256];
int len = sp.Read(recvBuf, 0, recvBuf.Length);
byte[] frame = recvBuf.Take(len).ToArray();
// 1. 校验 CRC
if (frame.Length < 2) return;
ushort recvCrc = (ushort)(frame[frame.Length - 2] | (frame[frame.Length - 1] << 8));
ushort calcCrc = Crc16(frame, frame.Length - 2);
if (recvCrc != calcCrc)
{
Console.WriteLine("CRC 错误");
return;
}
// 2. 检查地址、功能码
byte slaveId = frame[0];
byte func = frame[1];
if (slaveId != 1 || func != 0x03)
{
Console.WriteLine("应答不匹配");
return;
}
// 3. 解析数据
byte numBytes = frame[2];
for (int i = 0; i < numBytes; i += 2)
{
ushort val = (ushort)(frame[3 + i] << 8 | frame[3 + i + 1]);
Console.WriteLine("寄存器值:" + val);
}
完整示例
cs
using System;
using System.IO.Ports;
using System.Linq;
class ModbusRtuDemo
{
static SerialPort sp;
static void Main()
{
sp = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One)
{
ReadTimeout = 1000,
WriteTimeout = 1000
};
sp.Open();
Console.WriteLine("串口打开");
// 发:从站1,读寄存器0,2个
byte[] req = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x02 };
ushort crc = Crc16(req, req.Length);
byte[] send = req.Concat(new byte[] { (byte)crc, (byte)(crc >> 8) }).ToArray();
sp.Write(send, 0, send.Length);
Console.WriteLine("已发送:" + BitConverter.ToString(send));
// 收
byte[] buf = new byte[256];
int len = sp.Read(buf, 0, buf.Length);
byte[] frame = buf.Take(len).ToArray();
Console.WriteLine("收到:" + BitConverter.ToString(frame));
// CRC 校验
ushort rCrc = (ushort)(frame[frame.Length - 2] | (frame[frame.Length - 1] << 8));
ushort cCrc = Crc16(frame, frame.Length - 2);
Console.WriteLine(rCrc == cCrc ? "CRC 正确" : "CRC 错误");
sp.Close();
}
public static ushort Crc16(byte[] data, int len)
{
ushort crc = 0xFFFF;
for (int i = 0; i < len; i++)
{
crc ^= data[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x0001) != 0)
crc = (ushort)((crc >> 1) ^ 0xA001);
else
crc >>= 1;
}
}
return crc;
}
}
六第三方库
NModbus4/新Modbus
NModbus4 示例
cs
using Modbus.Device;
using System.IO.Ports;
var sp = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);
sp.Open();
var master = ModbusFactory.CreateRtuMaster(sp);
ushort[] regs = master.ReadHoldingRegisters(1, 0, 2); // 从站1,地址0,2个