简介:本资源是一套专为Delphi开发者设计的Modbus主从站通信控件源码,面向工控自动化领域的新手与进阶开发人员,解决PLC设备与上位机间标准Modbus协议通信的快速集成问题,适用于工业现场数据采集、设备监控等典型场景。压缩包共54个文件,涵盖10个核心Pas单元(如IdModbusClient/Server、ModbusTypes)、6个Dpk组件包(支持Delphi 5.0–7.0多版本)、3个DFM界面文件及配套DCU编译单元、RES资源与EXE演示程序,完整呈现控件封装、注册、调用与测试全流程;包体仅851KB,轻量易集成。已有283人学习下载,资源由‘工控老马’出品并经实测校正,包含详细readme与history说明文档、多版本兼容包结构、双模式Demo工程(master/slave)及About组件编辑器等实用模块,开箱即可用于二次开发或教学验证。
1. Delphi 下的 Modbus Master/Slave 控件:不是“拿来即用”的组件,而是串口与 TCP 通信的底层 glue layer
在工业现场调试 PLC、电表或温控器时,很多 Delphi 开发者会直接搜索“Delphi Modbus 控件”,期待拖一个 TModbusMaster 组件到窗体上、填几个 IP 或 COM 口就完成数据读写。现实却是:多数所谓“控件”只是封装了 Win32 API 的串口操作或简单 TCP socket,缺少协议状态机、异常重试、线程安全队列和寄存器缓存机制——结果是读取保持寄存器(0x03)时偶发丢包,写多个线圈(0x0F)后从站无响应,甚至在多线程轮询中触发 Access Violation。这类控件真正价值不在“可视化设计”,而在提供可嵌入业务逻辑的稳定通信基座:它必须明确区分 Master(主站,发起请求)与 Slave(从站,响应请求)角色,支持 RTU/ASCII/TCP 三种帧格式,且能暴露底层帧收发日志、超时阈值、重试策略等关键控制点。适合需要长期运行、对接国产工控设备、或需与 OPC UA 网关桥接的 Delphi 工程师——尤其是使用 Delphi 10.4 Sydney 至 12.3 的项目,因新版 RTL 对 Unicode 串口字符串处理更严谨,旧控件易出现 485 协议帧校验失败或乱码。
1.1 Modbus 协议栈在 Delphi 中的分层实现必要性
Modbus 协议本身极简:功能码 + 地址 + 数据 + CRC(RTU)或 LRC(ASCII),但真实工业环境要求远超协议规范。例如,某水厂 SCADA 系统需同时轮询 12 台变频器(Modbus RTU over RS-485),每台含 200+ 个寄存器;若采用单线程顺序读取,一轮扫描耗时超 8 秒,无法满足 1 秒级监控要求。此时必须拆解为三层:
- 物理层:COM 口配置(波特率、校验位、停止位)、TCP 连接池管理、RS-485 方向控制(DE/RE 引脚);
- 协议层:帧组装/解析、事务 ID(TCP)、单元 ID(RTU/ASCII)、超时重传(默认 1.5 秒)、重复请求抑制;
- 应用层:寄存器地址映射(如 40001 → $0000)、数据类型转换(INT16/UINT32/IEEE754)、批量读写合并(Read Multiple Holding Registers → 0x03)。
Delphi 原生 VCL 缺乏现成的 Modbus 栈,因此“控件”本质是将这三层胶合的 Pascal 类库。常见错误是把物理层操作(如CreateFile打开 COM3)和协议层逻辑(如计算 CRC16)混写在一个 TForm 事件里——这导致无法复用于服务进程(Windows Service)或跨线程调用。正确做法是:Master 控件继承自TComponent,内部持有一个TModbusTransport(负责串口/TCP 通信)和TModbusProtocol(负责帧编解码),Slave 控件则需实现TModbusServer抽象类,注册回调函数处理OnReadHoldingRegisters等事件。
提示:不要依赖“自动识别 Modbus 设备”的控件。Modbus 无设备发现机制,所有从站地址(Slave ID)必须手动配置。所谓“扫描”功能实为遍历 1~247 地址发送 0x03 请求,耗时且可能干扰现场设备。
1.2 为什么 Delphi 开发者必须亲手验证控件的线程模型
Delphi 的 VCL 窗体控件默认非线程安全,而工业通信要求后台线程持续轮询。若控件未显式声明线程模型,极易引发 GDI 资源冲突。典型症状:在TThread.Execute中调用TModbusMaster.ReadHoldingRegisters(1, 0, 10)后,主线程TStringGrid.Cells[0,0] := IntToStr(Value)触发EAccessViolation。根本原因在于 VCL 的TCanvas、TFont等对象绑定到主线程消息循环。解决方案有二:
- 推荐:控件内部使用
Synchronize或Queue将 UI 更新委托给主线程,自身通信逻辑完全运行在工作线程; - 替代:改用
TIdModbus(基于 Indy)等纯数据类,不继承TComponent,由开发者自行管理线程同步。
验证方法:编写压力测试用例,在TThread中连续 1000 次调用WriteMultipleCoils,同时主线程每 100ms 刷新TEdit.Text。若 5 分钟内无崩溃,则控件线程模型合格。注意:Delphi 12.3 新增的System.Threading.TTask不适用于此场景,因其调度器仍依赖主线程消息泵,无法保证实时性。
2. 用 TModbusMaster 在本地跑通 Modbus RTU 的最小命令集
2.1 创建最小可运行工程:从 COM 口初始化开始
Delphi 工程需引用System.SysUtils,System.Classes,Winapi.Windows,核心是正确打开串口并设置 DCB 结构。以下代码是 TModbusMaster 初始化 COM3 的最小实现(非第三方控件,展示底层逻辑):
function OpenModbusPort(const PortName: string; BaudRate: DWORD): THandle; var hPort: THandle; dcb: TDCB; comTimeOuts: TCommTimeouts; begin Result := INVALID_HANDLE_VALUE; hPort := CreateFile(PChar('\\.\' + PortName), GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); if hPort = INVALID_HANDLE_VALUE then Exit; // 清除串口缓冲区 PurgeComm(hPort, PURGE_TXCLEAR or PURGE_RXCLEAR); // 配置串口参数 FillChar(dcb, SizeOf(dcb), 0); dcb.DCBlength := SizeOf(dcb); if not GetCommState(hPort, dcb) then begin CloseHandle(hPort); Exit; end; dcb.BaudRate := BaudRate; dcb.fBinary := True; dcb.fParity := False; dcb.fOutxCtsFlow := False; dcb.fOutxDsrFlow := False; dcb.fDtrControl := DTR_CONTROL_ENABLE; dcb.fDsrSensitivity := False; dcb.fTXContinueOnXoff := True; dcb.fOutX := False; dcb.fInX := False; dcb.fErrorChar := #0; dcb.fNull := False; dcb.fRtsControl := RTS_CONTROL_ENABLE; dcb.fAbortOnError := False; dcb.ByteSize := 8; dcb.Parity := NOPARITY; dcb.StopBits := ONESTOPBIT; if not SetCommState(hPort, dcb) then begin CloseHandle(hPort); Exit; end; // 设置超时 FillChar(comTimeOuts, SizeOf(comTimeOuts), 0); comTimeOuts.ReadIntervalTimeout := MAXDWORD; comTimeOuts.ReadTotalTimeoutConstant := 1000; // 1秒读超时 comTimeOuts.ReadTotalTimeoutMultiplier := 0; comTimeOuts.WriteTotalTimeoutConstant := 1000; comTimeOuts.WriteTotalTimeoutMultiplier := 0; SetCommTimeouts(hPort, comTimeOuts); Result := hPort; end;该函数返回有效THandle是后续一切通信的前提。关键参数说明:
BaudRate:必须与从站设备一致,常见值为 9600、19200、115200;PurgeComm:清除残留数据,避免历史帧干扰新会话;ReadTotalTimeoutConstant:Modbus RTU 帧间间隔(T1.5)通常为 1.75 字符时间,设为 1000ms 可覆盖绝大多数设备;RTS_CONTROL_ENABLE:RS-485 方向控制必需,否则 DE 引脚无法拉高发送。
注意:
\\.\COM3格式仅 Windows 支持,Linux 下需用/dev/ttyUSB0;Delphi 12.3 的TSerialPort类虽简化操作,但不支持 RTS 控制,故工业场景仍需 WinAPI。
2.2 构建标准 Modbus RTU 请求帧:以读取保持寄存器为例
Modbus RTU 帧结构为[Slave ID][Function Code][Start Address Hi][Start Address Lo][Register Count Hi][Register Count Lo][CRC Lo][CRC Hi]。以下函数生成读取从站 ID=1、地址 0x0000 开始的 10 个保持寄存器的请求帧:
function BuildReadHoldingRequest(SlaveID, StartAddr, RegCount: Word): TBytes; var i: Integer; crc: Word; buf: array[0..7] of Byte; begin // 填充固定字段 buf[0] := Byte(SlaveID); // 从站地址 buf[1] := $03; // 功能码:读保持寄存器 buf[2] := HiByte(StartAddr); // 起始地址高位 buf[3] := LoByte(StartAddr); // 起始地址低位 buf[4] := HiByte(RegCount); // 寄存器数量高位 buf[5] := LoByte(RegCount); // 寄存器数量低位 // 计算 CRC16-MODBUS crc := $FFFF; for i := 0 to 5 do begin crc := crc xor buf[i]; for j := 0 to 7 do begin if (crc and 1) = 1 then crc := (crc shr 1) xor $A001 else crc := crc shr 1; end; end; // 追加 CRC SetLength(Result, 8); Move(buf, Result[0], 6); Result[6] := LoByte(crc); Result[7] := HiByte(crc); end;调用BuildReadHoldingRequest(1, $0000, 10)返回 8 字节数组:[01, 03, 00, 00, 00, 0A, 44, 8C]。其中44 8C是 CRC 校验值,必须严格匹配,否则从站丢弃该帧。常见错误:
- 地址偏移错误:Modbus 地址 40001 对应寄存器索引 0,而非 40001;
- CRC 算法混淆:误用 CRC16-CCITT(初始值 $FFFF,多项式 $1021);
- 字节序颠倒:
HiByte/LoByte顺序写反导致地址错位。
2.3 发送与接收完整帧:处理 T1.5 和 T3.5 时序
Modbus RTU 依赖精确的字符间隔超时判断帧边界。标准规定:
- T1.5:两个字符间最大间隔,用于判断一帧结束;
- T3.5:帧间最小间隔,用于区分不同帧。
Delphi 中需用WaitCommEvent或轮询GetOverlappedResult实现。以下为简化版接收逻辑:
function ReadModbusResponse(hPort: THandle; var Response: TBytes): Boolean; var bytesRead: DWORD; buffer: array[0..255] of Byte; startTime: DWORD; timeout: DWORD; begin Result := False; SetLength(Response, 0); timeout := 1500; // 总超时 1.5秒 startTime := GetTickCount; // 读取至少 2 字节(Slave ID + Function Code) while Length(Response) < 2 do begin if not ReadFile(hPort, buffer, 1, bytesRead, nil) or (bytesRead = 0) then Break; SetLength(Response, Length(Response) + 1); Response[High(Response)] := buffer[0]; // 检查是否超时 if GetTickCount - startTime > timeout then Exit; end; // 读取剩余字节:根据功能码确定长度 if (Length(Response) >= 2) and (Response[1] in [$03, $04]) then begin // 0x03/0x04 响应:[ID][FC][Byte Count][Data...][CRC] if Length(Response) >= 3 then begin // 读取 Byte Count 字段,再读取对应数据 + 2 字节 CRC SetLength(Response, 3 + Response[2] + 2); if not ReadFile(hPort, buffer, Response[2] + 2, bytesRead, nil) then Exit; Move(buffer, Response[3], Response[2] + 2); Result := True; end; end; end;该函数关键点:
- 不假设一次
ReadFile读完整帧,而是按协议动态计算需读字节数; Response[2]是数据字节数(Byte Count),其后紧跟Byte Count个数据字节 + 2 字节 CRC;- 超时判断基于
GetTickCount,避免Sleep阻塞线程。
3. TModbusSlave 控件的 3 个必调参数与内存映射实现
3.1 Slave ID、响应延迟与异常码映射:从站行为的三大控制点
TModbusSlave 控件并非被动监听,其行为由三个核心参数驱动:
- Slave ID:必须与主站请求中的
Slave ID字段严格匹配,否则静默丢弃; - Response Delay (ms):模拟从站处理时间,设为 0 则立即响应,设为 50 则在解析请求后延时 50ms 再发回,用于测试主站超时逻辑;
- Exception Code Mapping:定义非法地址、非法功能码等错误的响应码,如
0x02(非法地址)或0x04(从站故障)。
以下为 Delphi 中设置这些参数的典型代码:
// 创建从站实例 fModbusSlave := TModbusSlave.Create(Self); fModbusSlave.SlaveID := 1; // 必须与主站请求一致 fModbusSlave.ResponseDelay := 20; // 模拟 20ms 处理延迟 // 注册异常码处理 fModbusSlave.OnException := procedure(Sender: TObject; const RequestFrame: TBytes; ExceptionCode: Byte) begin case ExceptionCode of $01: ShowMessage('非法功能码'); $02: ShowMessage(Format('非法地址 %d', [GetAddressFromRequest(RequestFrame)])); $03: ShowMessage('非法数据值'); $04: ShowMessage('从站故障'); end; end;提示:
ResponseDelay不是“增加延迟”,而是“最小响应延迟”。若从站逻辑执行快于该值,仍会等待至超时才返回,确保主站收到稳定时序。
3.2 内存映射寄存器:用 TMemoryMap 实现高速读写
从站的核心是寄存器数据存储。传统做法用array[0..65535] of Word,但存在两大问题:
- 内存占用大(65536×2=128KB),实际只用前 1000 个;
- 无法区分“未初始化”与“值为 0”。
TModbusSlave 应提供TMemoryMap类,按需分配页(Page),每页 256 个寄存器:
type TMemoryMap = class private FPages: TDictionary<Word, TBytes>; // Key: Page Index (0,1,2...), Value: 256*2 bytes public function ReadHoldingRegisters(StartAddr, Count: Word): TBytes; procedure WriteHoldingRegisters(StartAddr: Word; const Data: TBytes); function GetRegisterValue(Addr: Word): Word; procedure SetRegisterValue(Addr: Word; Value: Word); end; function TMemoryMap.ReadHoldingRegisters(StartAddr, Count: Word): TBytes; var pageIndex, offsetInPage, i: Word; pageData: TBytes; begin SetLength(Result, Count * 2); for i := 0 to Count - 1 do begin pageIndex := (StartAddr + i) div 256; offsetInPage := ((StartAddr + i) mod 256) * 2; if not FPages.TryGetValue(pageIndex, pageData) then begin // 按需创建新页,初始值全 0 SetLength(pageData, 256 * 2); FPages.Add(pageIndex, pageData); end; Result[i * 2] := pageData[offsetInPage]; Result[i * 2 + 1] := pageData[offsetInPage + 1]; end; end;该实现优势:
- 内存按需分配,1000 个寄存器仅占 2 页(1KB);
GetRegisterValue可返回0或nil表示未写入,便于调试;WriteHoldingRegisters直接操作TBytes,避免Word类型转换开销。
3.3 模拟真实从站行为:支持线圈、离散输入、输入寄存器
Modbus 定义四类寄存器,TModbusSlave 必须全部支持:
| 类型 | 功能码 | 地址范围 | 读写性 |
|---|---|---|---|
| 线圈(Coils) | 0x01/0x05/0x0F | 00001-09999 | 读/写 |
| 离散输入(Discrete Inputs) | 0x02 | 10001-19999 | 只读 |
| 输入寄存器(Input Registers) | 0x04 | 30001-39999 | 只读 |
| 保持寄存器(Holding Registers) | 0x03/0x06/0x10 | 40001-49999 | 读/写 |
以下为线圈操作示例(地址 00001 对应索引 0):
// 写单个线圈(0x05) procedure TModbusSlave.HandleWriteSingleCoil(const Request: TBytes); var addr: Word; value: Word; begin addr := (Request[2] shl 8) or Request[3]; // 地址 value := (Request[4] shl 8) or Request[5]; // 值:0xFF00=ON, 0x0000=OFF fCoilMap.SetBit(addr, value = $FF00); end; // 读线圈(0x01) function TModbusSlave.BuildReadCoilsResponse(StartAddr, Count: Word): TBytes; var byteCount: Byte; i, bitPos: Integer; data: TBytes; begin byteCount := (Count + 7) div 8; SetLength(Result, 3 + byteCount); Result[0] := fSlaveID; Result[1] := $01; Result[2] := byteCount; SetLength(data, byteCount); for i := 0 to Count - 1 do begin bitPos := StartAddr + i; if fCoilMap.GetBit(bitPos) then data[i div 8] := data[i div 8] or (1 shl (i mod 8)); end; Move(data, Result[3], byteCount); end;关键细节:
- 线圈地址
00001→ 索引0,00002→1,以此类推; 0x01响应中Byte Count字段表示后续数据字节数,非位数;fCoilMap应为位图(BitMap),节省内存(1000 线圈仅需 125 字节)。
4. Modbus TCP 的 Socket 层优化:解决连接复用与粘包问题
4.1 为何不能直接用 TIdTCPClient:连接池与心跳机制缺失
Indy 的TIdTCPClient适合一次性请求,但 Modbus TCP 要求:
- 主站需维持长连接,避免频繁
Connect/Disconnect增加 TCP 开销; - 从站需支持多客户端并发,每个连接独立事务 ID;
- 网络中断后需自动重连,而非抛出异常终止。
因此 TModbusMasterTCP 必须内置连接池。以下为连接管理核心逻辑:
type TModbusConnection = class private FSocket: TSocket; FIP: string; FPort: Word; FLastActive: DWORD; FIsConnected: Boolean; public constructor Create(const AIP: string; APort: Word); function Connect: Boolean; function SendFrame(const Frame: TBytes): Boolean; function ReceiveFrame(var Frame: TBytes): Boolean; property IsConnected: Boolean read FIsConnected; end; TModbusConnectionPool = class private FConnections: TList<TModbusConnection>; FCriticalSection: TRTLCriticalSection; public constructor Create; destructor Destroy; override; function GetConnection(const IP: string; Port: Word): TModbusConnection; procedure ReleaseConnection(Connection: TModbusConnection); end; function TModbusConnectionPool.GetConnection(const IP: string; Port: Word): TModbusConnection; var conn: TModbusConnection; i: Integer; begin EnterCriticalSection(FCriticalSection); try // 查找空闲连接 for i := 0 to FConnections.Count - 1 do begin conn := FConnections[i]; if (conn.FIP = IP) and (conn.FPort = Port) and conn.IsConnected then begin conn.FLastActive := GetTickCount; Result := conn; Exit; end; end; // 创建新连接 conn := TModbusConnection.Create(IP, Port); if conn.Connect then begin FConnections.Add(conn); Result := conn; end else Result := nil; finally LeaveCriticalSection(FCriticalSection); end; end;该连接池特点:
GetConnection返回已连接实例,避免重复connect();FLastActive记录最后使用时间,后台线程可清理闲置连接(如 >300 秒未用);TRTLCriticalSection保证多线程安全,比TCriticalSection更轻量。
4.2 解决 TCP 粘包:用 MBAP 头部解析帧边界
Modbus TCP 帧以 MBAP(Modbus Application Protocol)头起始:[Transaction ID Hi][Transaction ID Lo][Protocol ID Hi][Protocol ID Lo][Length Hi][Length Lo][Unit ID]
其中Length字段表示后续字节数(不含 MBAP 头的 6 字节)。粘包即多个帧被 OS 合并为一次recv(),需按Length拆分:
function TModbusConnection.ReceiveFrame(var Frame: TBytes): Boolean; var mbapHeader: array[0..5] of Byte; frameLength: Word; totalBytes: Integer; buffer: array[0..1023] of Byte; bytesRead: Integer; begin Result := False; // 先读 MBAP 头(6 字节) if not ReceiveExact(mbapHeader, 6) then Exit; // 解析 Length 字段(字节 4-5) frameLength := (mbapHeader[4] shl 8) or mbapHeader[5]; totalBytes := 6 + frameLength; // MBAP 头 + 协议数据 // 分配缓冲区并读取完整帧 SetLength(Frame, totalBytes); Move(mbapHeader, Frame[0], 6); // 读取剩余部分 if not ReceiveExact(@Frame[6], frameLength) then Exit; Result := True; end; function TModbusConnection.ReceiveExact(var Buffer; BytesToRead: Integer): Boolean; var totalRead: Integer; bytesRead: Integer; begin Result := False; totalRead := 0; while totalRead < BytesToRead do begin if recv(FSocket, PByte(@Buffer)[totalRead]^, BytesToRead - totalRead, 0) <= 0 then Exit; Inc(totalRead, bytesRead); end; Result := True; end;关键点:
ReceiveExact循环调用recv()直至读满指定字节数,避免recv()返回少于请求长度;frameLength是协议数据长度,不含 MBAP 头,故总帧长为6 + frameLength;- 若
recv()返回 0,表示连接关闭,需触发重连逻辑。
4.3 事务 ID(Transaction ID)的线程安全生成策略
Modbus TCP 要求同一连接内事务 ID 唯一,且主站需匹配响应中的 ID。常见错误是全局静态变量NextID: Word,多线程下产生冲突。正确做法是每个连接维护独立 ID 计数器:
type TModbusConnection = class private FTransactionID: Word; FCriticalSection: TRTLCriticalSection; public function GetNextTransactionID: Word; end; function TModbusConnection.GetNextTransactionID: Word; begin EnterCriticalSection(FCriticalSection); try Inc(FTransactionID); if FTransactionID = 0 then FTransactionID := 1; // 避免 0 Result := FTransactionID; finally LeaveCriticalSection(FCriticalSection); end; end;生成请求帧时调用GetNextTransactionID,并将该值写入 MBAP 头部前两字节。响应帧到达后,用TDictionary<Word, TProc>存储待处理回调,Key 为 Transaction ID,Value 为解析后执行的匿名方法,确保请求与响应严格配对。
5. 实战排错:用 Modbus Poll 与 Wireshark 定位 Delphi 控件通信失败
5.1 Modbus Poll 作为黄金标准对比工具:配置与抓包步骤
Modbus Poll 是工业界事实标准测试工具,其配置直接影响 Delphi 控件调试效率。关键设置:
- Connection → Read/Write:选择 RTU 或 TCP,RTU 下填 COM 口、波特率等;TCP 下填 IP 和端口;
- Setup → Read/Write:设置功能码、起始地址、寄存器数量;
- Display → Response Data:勾选 “Show response data as hex” 和 “Show request/response in separate windows”;
- Log → Enable Log:启用日志,保存为
.txt文件供比对。
当 Delphi 主站无法读取从站时,先用 Modbus Poll 连接同一设备:
- 若 Modbus Poll 成功,说明硬件链路正常,问题在 Delphi 控件的帧构造或超时设置;
- 若 Modbus Poll 失败,检查 COM 口权限(Windows 10/11 默认禁用)、USB 转 485 驱动版本、终端电阻(RS-485 需 120Ω)。
抓包对比要点:
- Delphi 发送帧 vs Modbus Poll 发送帧:逐字节比对 Slave ID、功能码、地址、CRC;
- 响应帧长度:Delphi 接收长度是否等于
6 + ByteCount + 2(RTU)或6 + Length(TCP); - CRC 校验:用在线 CRC16-MODBUS 计算器验证 Delphi 生成的 CRC 是否匹配。
5.2 Wireshark 过滤 Modbus TCP 流量:快速定位网络层问题
Wireshark 是诊断 Modbus TCP 的终极武器。安装后按以下步骤:
- 启动捕获,选择网卡;
- 在过滤栏输入
tcp.port == 502(Modbus TCP 默认端口); - 运行 Delphi 主站程序,触发一次读请求;
- 停止捕获,右键任一 Modbus 包 → “Follow → TCP Stream”。
关键分析点:
- 三次握手是否完成:查看 SYN/SYN-ACK/ACK 包,若无 ACK,说明防火墙拦截或从站未监听 502 端口;
- 请求帧 MBAP 头部:确认
Transaction ID非零、Protocol ID=0、Length字段正确; - 响应帧延迟:若请求后 >1 秒才出现响应,检查从站 CPU 占用率或 Delphi 从站线程是否阻塞;
- RST 包:出现 RST 表示连接被主动重置,常见于从站程序崩溃或
bind()失败。
提示:Wireshark 显示的 “Modbus/TCP” 解析可能误判,务必切换到 “Hex Dump” 标签页,手动核对字节序列。
5.3 Delphi 控件常见崩溃点与修复方案
| 现象 | 根本原因 | 修复方案 |
|---|---|---|
Access Violation at address XXXX | VCL 控件在非主线程更新 UI(如TLabel.Caption := ...) | 使用TThread.Synchronize包裹 UI 操作,或改用TIdModbus等无 UI 类 |
ReadFile returns 0 bytes | COM 口被其他程序独占(如串口调试助手) | 调用CreateFile前检查ERROR_ACCESS_DENIED,提示用户关闭冲突程序 |
CRC mismatch | CRC16 计算时字节序错误或初始值非$FFFF | 重写 CRC 函数,用已知正确帧(如[01,03,00,00,00,01]→44,8C)验证 |
TCP connection reset by peer | 从站未实现SO_KEEPALIVE,网络空闲断连 | 在setsockopt()中启用SO_KEEPALIVE,或主站定期发0x00心跳 |
Unicode string truncation | Delphi 12.3string默认 UTF-16,串口驱动期望 ANSI | 强制转换:AnsiString(UTF8Encode(MyString)),或使用RawByteString |
最后验证技巧:在 Delphi 主站中添加OnFrameSent和OnFrameReceived事件,将原始帧写入TStringList.SaveToFile('log.txt'),与 Modbus Poll 日志逐行比对。真正的稳定控件,其日志应与 Modbus Poll 完全一致——差异即 bug 根源。
本文还有配套的精品资源,点击获取