Modbus RTU 与 Modbus TCP 深入指南-通信模型与定时机制
五、通信模型与定时机制5.1 Modbus RTU 主从模式5.1.1 通信规则单主站总线上只能有一个主站Master多从站1-247个从站Slave被动响应轮询机制主站发起请求 → 等待响应 → 发送下一请求广播地址0所有从站执行但不回复5.1.2 帧间间隔3.5字符| 帧1 | 静默 ≥3.5字符 | 帧2 | 静默 ≥3.5字符 | 帧3 | ...计算表8N110位/字符波特率1字符时间(ms)3.5字符时间(ms)实际取值(ms)24004.16714.581548002.0837.29896001.0423.654192000.5211.822384000.2600.9111152000.0870.300.55.1.3 完整主站轮询实现#include stdint.h #include stdio.h #include unistd.h // 超时设置毫秒 #define RESPONSE_TIMEOUT_MS 200 #define FRAME_GAP_MS 4 // 9600bps 时 3.5字符≈3.65ms取4ms int poll_rtu_slave(int fd, uint8_t slave_addr, uint8_t func, uint16_t reg_addr, uint16_t reg_count, uint8_t *response, int *resp_len) { // 1. 构建请求帧 uint8_t request[8]; request[0] slave_addr; request[1] func; request[2] (reg_addr 8) 0xFF; request[3] reg_addr 0xFF; request[4] (reg_count 8) 0xFF; request[5] reg_count 0xFF; uint16_t crc modbus_crc(request, 6); request[6] crc 0xFF; // CRC低字节 request[7] (crc 8) 0xFF; // CRC高字节 // 2. 清空接收缓冲区 tcflush(fd, TCIFLUSH); // 3. 发送请求 write(fd, request, 8); // 4. 等待响应带超时 uint8_t buffer[256]; int idx 0; long last_byte_time current_time_ms(); long start_time last_byte_time; while ((current_time_ms() - start_time) RESPONSE_TIMEOUT_MS) { if (serial_available(fd)) { buffer[idx] read_serial(fd); last_byte_time current_time_ms(); } // 帧结束判断超过3.5字符时间没有新数据 if (idx 0 (current_time_ms() - last_byte_time) FRAME_GAP_MS) { break; } } // 5. 验证响应长度 if (idx 5) { return -1; // 响应太短 } // 6. 验证CRC uint16_t recv_crc buffer[idx-2] | (buffer[idx-1] 8); uint16_t calc_crc modbus_crc(buffer, idx-2); if (recv_crc ! calc_crc) { return -2; // CRC错误 } // 7. 验证地址和功能码 if (buffer[0] ! slave_addr) { return -3; // 地址不匹配 } if ((buffer[1] 0x7F) func) { // 正常响应 *resp_len idx; memcpy(response, buffer, idx); return 0; } else if ((buffer[1] 0x80)) { // 异常响应: 功能码高位1 return -4 - buffer[2]; // 返回负的异常码 } return -5; // 未知错误 }5.2 Modbus TCP 客户端/服务器模式5.2.1 连接模型客户端 A ──┐ 客户端 B ──┼── 交换机 ── 服务器 (端口502) 客户端 C ──┘多客户端允许多个客户端同时连接同一服务器并发处理服务器可同时处理多个请求通过事务ID区分全双工可同时收发5.2.2 事务标识符详解事务ID是Modbus TCP实现并发请求的关键时间线: 客户端: 发送请求(TransID100) → 等待匹配响应 客户端: 发送请求(TransID101) → 等待匹配响应 服务器: 处理请求101 (快) → 响应(TransID101) → 客户端匹配 服务器: 处理请求100 (慢) → 响应(TransID100) → 客户端匹配5.2.3 健壮的TCP客户端实现import socket import struct import time from threading import Lock class ModbusTCPClient: def __init__(self, host, port502, timeout_ms1000): self.host host self.port port self.timeout_ms timeout_ms self.sock None self.next_trans_id 1 self.lock Lock() self.auto_reconnect True def connect(self): 建立TCP连接 self.sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(self.timeout_ms / 1000.0) self.sock.connect((self.host, self.port)) # 禁用Nagle算法减少延迟 self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # 启用TCP Keep-Alive self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) def close(self): if self.sock: self.sock.close() self.sock None def _next_trans_id(self): 生成下一个事务ID1-65535循环 tid self.next_trans_id self.next_trans_id (self.next_trans_id % 65535) 1 return tid def send_request(self, pdu, unit_id1): 发送请求并等待响应 with self.lock: for attempt in range(3): # 最多重试3次 try: if self.sock is None and self.auto_reconnect: self.connect() trans_id self._next_trans_id() mbap struct.pack(HHHB, trans_id, 0, len(pdu) 1, unit_id) self.sock.send(mbap pdu) # 接收响应读MBAP头部前7字节 # 简化版直接读取足够大的缓冲区 self.sock.settimeout(self.timeout_ms / 1000.0) response self.sock.recv(2048) if len(response) 8: raise Exception(Response too short) # 解析MBAP头部 resp_trans_id, proto_id, length, resp_unit_id struct.unpack(HHHB, response[:7]) pdu_len length - 1 # 减去unit_id if resp_trans_id ! trans_id: # 收到旧响应丢弃并等待正确的 continue if proto_id ! 0: raise Exception(fInvalid protocol ID: {proto_id}) return response[7:7pdu_len] except (socket.timeout, socket.error) as e: print(fAttempt {attempt1} failed: {e}) self.close() if attempt 2: raise time.sleep(0.5) def read_holding_registers(self, address, count, unit_id1): 读保持寄存器功能码03 pdu struct.pack(BHH, 0x03, address, count) response self.send_request(pdu, unit_id) if len(response) 3 or response[0] ! 0x03: raise Exception(Invalid response) byte_count response[1] data response[2:2byte_count] # 将字节转换为寄存器值大端序 registers [struct.unpack(H, data[i:i2])[0] for i in range(0, byte_count, 2)] return registers def write_single_register(self, address, value, unit_id1): 写单个寄存器功能码06 pdu struct.pack(BHH, 0x06, address, value) response self.send_request(pdu, unit_id) # 验证响应应与请求相同 if response ! pdu[1:]: # 跳过功能码后的比较 raise Exception(Write confirmation mismatch) return True # 使用示例 client ModbusTCPClient(192.168.1.100, timeout_ms500) try: values client.read_holding_registers(0x0000, 10) print(fValues: {values}) client.write_single_register(0x0100, 0x1234) except Exception as e: print(fError: {e}) finally: client.close()