Modbus RTU/TCP 通讯
Darra PLC 通过 ModbusRtuAdapter 支持 Modbus RTU (RS-485) 和 Modbus TCP 两种模式, 可同时作为主站轮询多个从站设备。
支持模式
| 模式 | 传输层 | 典型场景 |
|---|---|---|
| Modbus RTU | RS-485 串口 | 变频器、温控器、仪表 |
| Modbus TCP | TCP/IP 网络 | 远程 IO、网关、智能设备 |
支持功能码
| 功能码 | 名称 | 操作 | 数据类型 |
|---|---|---|---|
| 01 | Read Coils | 读线圈 | BOOL |
| 02 | Read Discrete Inputs | 读离散输入 | BOOL |
| 03 | Read Holding Registers | 读保持寄存器 | WORD/DWORD/REAL |
| 04 | Read Input Registers | 读输入寄存器 | WORD/DWORD/REAL |
| 05 | Write Single Coil | 写单个线圈 | BOOL |
| 06 | Write Single Register | 写单个寄存器 | WORD |
| 15 | Write Multiple Coils | 写多个线圈 | BOOL[] |
| 16 | Write Multiple Registers | 写多个寄存器 | WORD[] |
连接配置
Modbus RTU
{
"protocol": "ModbusRTU",
"serialPort": "COM3",
"baudRate": 9600,
"dataBits": 8,
"parity": "Even",
"stopBits": 1,
"slaveId": 1,
"timeout": 1000
}
Modbus TCP
{
"protocol": "ModbusTCP",
"host": "192.168.1.100",
"port": 502,
"slaveId": 1,
"timeout": 3000,
"keepAlive": true
}
PLC 变量映射
Modbus 地址与 PLC 变量之间通过 PLCDataServer 建立映射:
| Modbus 地址范围 | 功能码 | PLC 地址 | 数据类型 |
|---|---|---|---|
| 00001 ~ 09999 | 01/05/15 | MX0.0 ~ MX1249.7 | BOOL |
| 10001 ~ 19999 | 02 | IX0.0 ~ IX1249.7 | BOOL (只读) |
| 30001 ~ 39999 | 04 | IW0 ~ IW9998 | WORD (只读) |
| 40001 ~ 49999 | 03/06/16 | MW0 ~ MW9998 | WORD |
Modbus 协议默认大端序 (Big-Endian)。对于 32 位浮点数 (REAL), 支持配置字节/字序:
- AB CD (大端, 默认)
- CD AB (字交换)
- BA DC (字节交换)
- DC BA (小端)
轮询配置
支持多从站、多区域的分组轮询:
// 添加轮询组
var group = new ModbusPollGroup
{
SlaveId = 1,
FunctionCode = 3, // 读保持寄存器
StartAddress = 0,
Quantity = 100,
PollIntervalMs = 100, // 100ms 轮询周期
Priority = PollPriority.High
};
adapter.AddPollGroup(group);
轮询策略:
- 高优先级组: 50~100ms (控制数据)
- 中优先级组: 200~500ms (状态监控)
- 低优先级组: 1000~5000ms (参数读取)
自动重连
Modbus 适配器在连接断开后自动重连, 采用指数退避策略:
- 首次重试: 1 秒
- 后续重试: 2s → 4s → 8s → ... → 最大 60s
- 重连期间所有读写操作返回上次缓存值, 并标记为
Quality.Bad
代码示例
// 创建 Modbus TCP 连接
var adapter = PLCDriverFactory.Create(ProtocolId.ModbusTCP);
await adapter.ConnectAsync(new ConnectionConfig
{
Host = "192.168.1.100",
Port = 502,
SlaveId = 1
});
// 读取保持寄存器 (40001~40010)
var data = await adapter.ReadAsync("40001", 10);
float temperature = BitConverter.ToSingle(data, 0);
// 写入单个寄存器
await adapter.WriteAsync("40001", BitConverter.GetBytes((short)1000));
// 断开连接
await adapter.DisconnectAsync();
Modbus 帧格式
Modbus RTU 帧
| 从站ID (1B) | 功能码 (1B) | 数据 (N B) | CRC16 (2B) |
- 帧间隔: 3.5 字符时间, 用于区分相邻帧
- 字符帧: 起始位 1 + 8 数据位 + 校验位 0/1 + 停止位 1/2 = 10/11 位
- CRC16 多项式: 0xA001 (逆序表示)
Modbus TCP 帧
| 事务 ID (2B) | 协议 ID (2B) | 长度 (2B) | 单元 ID (1B) | 功能码 (1B) | 数据 (N B) |
- 事务 ID: 客户端分配, 用于匹配响应
- 协议 ID: 固定 0x0000 (Modbus)
- 单元 ID: 对应 RTU 的从站 ID, TCP 下作为网关后端从站标识
ASCII (较少用)
每字节用 2 个 ASCII 十六进制字符表示, 帧以 : 开头, 以 \r\n 结尾, 校验用 LRC。常见于老设备或需要调试的场景。
多从站管理
一个 Modbus 适配器实例可管理多个从站 (TCP 同一网关下, RTU 同一串口上):
adapter.AddSlave(new SlaveConfig { SlaveId = 1, Name = "变频器A" });
adapter.AddSlave(new SlaveConfig { SlaveId = 2, Name = "温控器B" });
adapter.AddSlave(new SlaveConfig { SlaveId = 3, Name = "IO模块C" });
// 带从站参数的读取
var data = await adapter.ReadAsync(slaveId: 2, "40001", 10);
主站 / 从站角色
Darra PLC 默认作 主站 (Master, Client) 主动请求。也支持作 从站 (Slave, Server) 被动响应:
var server = new ModbusTcpServer();
server.Start(port: 502);
server.RegisterHoldingRegisters(startAddress: 0, count: 1000);
// 外部客户端读 40001~41000 时, 自动从 PLC 的 MW0~MW999 返回
从站模式下, Modbus 地址与 PLC 地址的映射由 PLCDataServer 管理。PLC 不用写代码, 外部客户端即可访问。
字节序与字序
32 位浮点 (REAL) 和 32 位整数跨 2 个寄存器, 实际字节排列有 4 种:
| 模式 | 表示 | 备注 |
|---|---|---|
| ABCD | Big-Endian | Modbus 原生, 多数厂商 |
| CDAB | Word-Swap | Schneider 等部分厂商 |
| BADC | Byte-Swap | 极少见 |
| DCBA | Little-Endian | Intel 本机 |
连接配置里可指定默认字节序, 也可以在地址配置时按字段覆盖:
{
"byteOrder": "ABCD",
"defaultFloatOrder": "CDAB"
}
数据类型映射
| Modbus 表示 | PLC 类型 | 寄存器数 | 说明 |
|---|---|---|---|
| 1 Coil | BOOL | - | 只读 1 位 |
| 1 Register | WORD / INT | 1 (16 bit) | 有无符号看 PLC 声明 |
| 2 Registers | DWORD / DINT | 2 (32 bit) | 字序看配置 |
| 2 Registers | REAL | 2 (32 bit IEEE 754) | 字序看配置 |
| 4 Registers | LINT / LREAL | 4 (64 bit) | 字序看配置 |
| N Registers | STRING | N (2N 字符) | ASCII / UTF-8 |
读写优化
地址合并
如果要读 40001, 40003, 40005 三个独立位置, 适配器会自动合并为一次 Read 40001 Count=5 的请求, 然后拆出需要的三个值。合并阈值由"合并间隙"参数控制 (默认 ≤ 8 个寄存器)。
轮询优先级
var groups = new[] {
new ModbusPollGroup { Name = "高频控制", Priority = High, PollMs = 50 },
new ModbusPollGroup { Name = "中频监控", Priority = Medium, PollMs = 500 },
new ModbusPollGroup { Name = "低频参数", Priority = Low, PollMs = 5000 }
};
调度器保证高优先级组在设定周期内完成。低优先级组让位。
常见异常码
| 异常码 | 含义 | 典型原因 |
|---|---|---|
| 01 | Illegal Function | 从站不支持该功能码 |
| 02 | Illegal Data Address | 寄存器地址越界 |
| 03 | Illegal Data Value | 数据格式不合法 |
| 04 | Slave Device Failure | 从站内部错误 |
| 05 | Acknowledge | 请求已接收但未完成 (长任务) |
| 06 | Slave Device Busy | 从站忙, 稍后重试 |
| 08 | Memory Parity Error | 内存奇偶校验错 |
| 0A | Gateway Path Unavailable | 网关不通 |
| 0B | Gateway Target No Response | 后端从站无响应 |
配置案例
案例 1: 读取变频器实际频率
变频器地址 40100 为实际频率 (单位 0.01 Hz):
var data = await adapter.ReadAsync(slaveId: 3, "40100", 1);
short raw = BitConverter.ToInt16(data.Reverse().ToArray(), 0); // 大端
float freqHz = raw * 0.01f;
案例 2: 设定变频器目标频率
目标频率寄存器 40010, 写入前需把 50Hz 换算为 5000:
short raw = (short)(50.0f * 100);
await adapter.WriteAsync(slaveId: 3, "40010", BitConverter.GetBytes(raw).Reverse().ToArray());
案例 3: 读多个温控仪表
10 个温控仪表, 每个有 PV (过程值) 和 SV (设定值), 分别在 40001 和 40002:
for (int id = 1; id <= 10; id++) {
var data = await adapter.ReadAsync(slaveId: id, "40001", 2);
short pv = BitConverter.ToInt16(data.Take(2).Reverse().ToArray(), 0);
short sv = BitConverter.ToInt16(data.Skip(2).Take(2).Reverse().ToArray(), 0);
}
建议改用主动轮询组, 避免在循环里 await。
排错
| 症状 | 原因 | 处理 |
|---|---|---|
| CRC 错 | 波特率/校验位不匹配 | 核对串口参数 |
| 超时 | 从站 ID 错 / 线缆断 | Ping 从站 ID |
| TCP 连接拒绝 | 端口未开 / IP 错 | Telnet 502 |
| 数值是原值的 100 倍或 0.01 | 字节序或 scale 错 | 试 CDAB 或除/乘 100 |
| 功能码 01 异常 | 从站固件只支持部分功能码 | 看手册 |
| 偶发 0x06 (Busy) | 从站处理慢 | 加重试 / 降低轮询频率 |
| 多从站串扰 | 总线终端电阻缺失 | RS-485 两端加 120 ohm |