简介:面向Delphi7开发环境的Modbus通信控件,适用于工业自动化场景,帮助开发者解决可编程逻辑控制器与上位机之间的数据交换问题。控件完整支持TCP/IP、RTU、ASCII等通信模式,并内置寄存器地址映射、读写操作、异常恢复以及多设备并发管理,适合快速构建稳定的通信模块。压缩包共31个文件,以源代码、编译组件、工程配置和演示程序为主体,其中pas文件便于深度定制,dcu组件可直接安装引用,dpr工程展示完整项目骨架,整体体积约293KB,轻量易用。已有948人学习下载。通过该控件,开发者只需设置连接参数并调用封装好的读写方法即可完成数据交互,无需自行解析协议;附带的示例工程与配置文件有助于快速理解连接建立、事件触发和调试流程,有效降低开发门槛、缩短项目周期。
1. 老Delphi7项目要接Modbus:这个控件到底解决了什么
手头维护的上位机还跑在 Delphi7 上,控制算法、配方管理、报表都是这十几年磨出来的,客户突然要求把 PLC 换成新设备,或者在同一张总线上再挂几台仪表,通信协议必须走 Modbus。重写整个上位机不现实,老板也不批这个预算,最现实的路就是在 Delphi7 里补一个 Modbus 通信控件,用最小的代价把旧程序接到新设备上。这篇文章就解决这个问题:如何从零封装一套可复用的 Delphi7 Modbus RTU 主站控件,覆盖串口收发、CRC 校验、03/06/16/01/05 这些常用功能码,以及在老 IDE 上容易踩的兼容性坑。适合正在维护工业上位机、又要对接新设备的工程师,新手能照着搭起来,熟手可以直接拿走里面的边界处理。
2. 自写最小Modbus主站控件:选型理由与三层结构
2.1 两条路线的真实成本对比
我在没有现成控件可用的前提下,一般先算一笔账:找现成的第三方控件,还是自己写一个最小实现。很多老项目里其实已经买过商业串口控件,但这些控件的授权、分发 DLL 和国内系统环境的兼容性经常出问题,尤其是换到 Win7/Win10 之后,老控件在“打开串口→等数据→超时”这条链路上是个黑匣子,出了问题只能在窗体上干瞪眼。相比之下,Modbus RTU 的报文格式是公开且极其简单的,核心工作量就是串口收发、CRC16 和几个功能码的拼包拆包,按我的经验,两三天就能出一个够用的主站控件。
| 对比项 | 自写最小主站 | 引入现成控件 |
|---|---|---|
| 工期 | 2~3 天 | 半天到一天 |
| 授权成本 | 无 | 可能有商业授权或开源协议限制 |
| 排查难度 | 源码在手,逐字节可查 | 黑匣子,只能看文档猜 |
| 定制空间 | 可以随手加日志、改超时策略 | 受控件接口限制 |
| 长期维护 | 自己负责 | 依赖第三方是否持续更新 |
所以我现在的习惯是:没有特殊情况一律自写。Modbus 协议简单到不需要为它引入重量级依赖,唯一需要谨慎的是 CRC16 和串口超时处理,这两块写对了,整个控件就稳了一半。实际工作中我也见过有人把第三方控件源码扒出来改的,时间成本反而比自己写更高。
2.2 最小控件应该包含哪几个文件
一个能干活的最小主站,我习惯分成三个文件:通信层(串口打开/关闭/读写)、协议层(Modbus 帧封装、CRC、解析)、业务层(对外暴露读写寄存器的方法)。设备地址、串口参数、超时设置放在一个记录结构里,方便配置。
// CommTypes.pas unit CommTypes; interface type TCommConfig = record PortName: string; // 串口名,如 'COM3' BaudRate: Integer; // 9600 / 19200 / 115200 DataBits: Integer; // 8 Parity: Integer; // 0=None 1=Odd 2=Even StopBits: Integer; // 0=1bit 1=2bit Timeout: Integer; // 毫秒 end; TModbusReadResult = record Success: Boolean; Sequence: Word; // 保留位,可用于分组标记 Values: array[0..124] of Word; // 读回来的寄存器值 Count: Integer; // 实际读到的数量 end; implementation end.这个记录结构把串口参数和返回值集中定义,后面写接收解析时不用到处传参。注意 Values 数组长度我按 Modbus 单帧最多读 125 个寄存器来定,Delphi7 没有动态数组的现代写法,用固定数组最省事。
2.3 串口层用 API 而不是串口控件
Delphi7 时代大家常用的串口控件有很多,但它们的底层 Windows API 并没有区别,只是帮你包了一层状态机和事件回调。直接在 Delphi7 里调 Windows API 没什么可怕的,CreateFile 打开串口、SetCommState 配置参数、ReadFile/WriteFile 收发字节,加起来不到 200 行。
// CommLayer.pas 关键片段 unit CommLayer; interface uses Windows, SysUtils, Classes; type TCommLayer = class private FHandle: THandle; FConfig: TCommConfig; public function Open: Boolean; procedure Close; function WriteBytes(const ABuffer: Pointer; ALen: Integer): Boolean; function ReadBytes(const ABuffer: Pointer; ALen: Integer): Integer; end; implementation function TCommLayer.Open: Boolean; var DCB: TDCB; Timeouts: TCommTimeouts; begin FHandle := CreateFile(PChar('\\.\' + FConfig.PortName), GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0); if FHandle = INVALID_HANDLE_VALUE then begin Result := False; Exit; end; FillChar(DCB, SizeOf(DCB), 0); DCB.DCBlength := SizeOf(DCB); GetCommState(FHandle, DCB); DCB.BaudRate := FConfig.BaudRate; DCB.ByteSize := FConfig.DataBits; DCB.Parity := FConfig.Parity; DCB.StopBits := FConfig.StopBits; SetCommState(FHandle, DCB); FillChar(Timeouts, SizeOf(Timeouts), 0); Timeouts.ReadIntervalTimeout := 50; Timeouts.ReadTotalTimeoutMultiplier := 0; Timeouts.ReadTotalTimeoutConstant := 200; SetCommTimeouts(FHandle, Timeouts); Result := True; end;Open 方法里最关键的是CreateFile(PChar('\\.\' + FConfig.PortName), ...),这个\\.\前缀不是可加可不加的装饰,它告诉 Windows 这是直接操作设备而非普通文件,缺失时某些机器上会打开失败。DCB 结构先 GetCommState 再改几个字段,比直接 BuildCommDCB 传字符串更不容易出错,因为有些老版本对字符串格式很敏感。SetCommTimeouts 的 ReadTotalTimeoutConstant 我一般设成 200ms,Modbus 一个响应帧在 9600 波特率下最长也就几十毫秒,200ms 足够,不会拖慢轮询。
提示:读到 50ms 间隔超时这个参数可以调大,比如 100ms,现场总线如果偶尔抖动,间隔超时太短会把一个帧拆成两半。
3. 把RTU主站核心写出来:CRC、请求帧、响应解析与事务超时
3.1 CRC16-Modbus 查表法与位运算法选哪个
CRC 是 Modbus RTU 唯一的数据完整性校验,多项式固定为0xA001,初始值为0xFFFF。Delphi7 里有两种常见实现:位运算法和查表法。位运算法代码短、占空间小,每字节处理 8 次循环;查表法速度快但需要一个 256 字长的常量表。对上位机而言每秒最多轮询几十帧,位运算法完全够用,而且表不用维护、不容易抄错。
// 位运算法求 CRC16-Modbus function Crc16Modbus(const ABuf: Pointer; ALen: Integer): Word; var i, j: Integer; b: Byte; crc: Word; p: PByte; begin crc := $FFFF; p := ABuf; for i := 0 to ALen - 1 do begin b := p^; crc := crc xor b; for j := 0 to 7 do begin if (crc and 1) <> 0 then crc := (crc shr 1) xor $A001 else crc := crc shr 1; end; Inc(p); end; Result := crc; end;这个函数把$A001放在每次循环里异或,和标准 Modbus CRC 完全一致。要注意的是发送时 CRC 的低字节在前、高字节在后,很多人第一次写会拼反,结果就是设备永远不回包。拼包时直接取crc and $FF放前面,(crc shr 8) and $FF放后面。
3.2 请求帧组装:以 03 功能码读保持寄存器为例
Modbus RTU 主站读保持寄存器(功能码 03)的请求帧固定为 8 个字节:设备地址、功能码、起始寄存器高字节、起始寄存器低字节、寄存器数量高字节、寄存器数量低字节、CRC 低字节、CRC 高字节。
// 组装 03 读保持寄存器请求帧 function BuildReadHoldRequest(const ADeviceId, AStartReg: Word; AQuantity: Word; var AFrame: array of Byte): Integer; var crc: Word; begin if AQuantity > 125 then begin Result := 0; Exit; end; AFrame[0] := ADeviceId; AFrame[1] := $03; AFrame[2] := Hi(AStartReg); AFrame[3] := Lo(AStartReg); AFrame[4] := Hi(AQuantity); AFrame[5] := Lo(AQuantity); crc := Crc16Modbus(@AFrame[0], 6); AFrame[6] := Lo(crc); AFrame[7] := Hi(crc); Result := 8; end;这里 AStartReg 是相对该设备的起始地址,比如从 40001 开始的保持寄存器,实际报文里的地址通常是 0x0000。传入的 AStartReg 已经是协议地址而不是 PLC 的物理地址,上位机内部负责换算。参数 ADeviceId 范围是 0~247,其中 0 是广播地址(一般不用于读写操作)。
3.3 响应帧解析:公共头、功能码高位、CRC、异常码
响应帧比请求帧多了一个字节计数字段,正常响应 03 的格式是:地址、03、字节数(寄存器数量×2)、数据……、CRC。异常响应则是把功能码最高位置 1(即$83),后面跟一个异常码。解析时先校验地址回显,再检查功能码最高位,最后算 CRC,顺序错了很容易把干扰字节误判成正常数据。
// 解析 03 功能码响应,返回读取的寄存器值 function ParseReadHoldResponse(const AFrame: array of Byte; ALength: Integer; ADeviceId: Byte; out AValues: array of Word; var ACount: Integer): Boolean; var crc, crcRecv: Word; i, n: Integer; begin Result := False; if ALength < 5 then Exit; if AFrame[0] <> ADeviceId then Exit; if (AFrame[1] and $80) <> 0 then begin // 异常响应,AFrame[2] 即异常码 Exit; end; if AFrame[1] <> $03 then Exit; n := AFrame[2] div 2; if n * 2 + 5 <> ALength then Exit; crc := Crc16Modbus(@AFrame[0], ALength - 2); crcRecv := AFrame[ALength - 2] or (AFrame[ALength - 1] shl 8); if crc <> crcRecv then Exit; ACount := n; for i := 0 to n - 1 do AValues[i] := (AFrame[3 + i * 2] shl 8) or AFrame[4 + i * 2]; Result := True; end;解析函数里有一个新手容易漏掉的位置:AFrame[2] div 2。这个字节是数据字节数,可以是 0~250,除 2 得到寄存器个数,算出的长度要和实际接收长度一致,不一致就应该丢弃。这个检查能让你的控件对串口噪声免疫。
3.4 串口写读完整流程:清缓存、发送、等响应、超时
单独收发字节和单独拼包解析都不难,真正决定控件好不好用的是把它们串成一个事务的节奏。我通常的做法是:PurgeComm 清空输入输出缓存,发送请求帧,然后循环 ReadFile 直到攒够预期长度或超时。
// 一次完整的 Modbus 事务:发送请求并等待响应 function TModbusClient.ExecuteTransaction( const ARequest: array of Byte; AReqLen: Integer; ARespAddr: Byte; out AResponse: array of Byte; var ARespLen: Integer): Boolean; var total, nread, need: Integer; startTick: DWORD; ok: Boolean; begin Result := False; PurgeComm(FHandle, PURGE_RXCLEAR or PURGE_TXCLEAR); if not WriteBytes(@ARequest[0], AReqLen) then Exit; total := 0; need := 0; startTick := GetTickCount; ok := False; while (GetTickCount - startTick) < DWORD(FConfig.Timeout) do begin nread := ReadBytes(@AResponse[total], 256 - total); if nread > 0 then begin Inc(total, nread); if total >= 5 then // 至少收到地址+功能码+字节数+CRC begin need := AResponse[2] + 5; if total >= need then begin ok := True; Break; end; end; end else Sleep(5); end; if ok then begin ARespLen := total; Result := True; end; end;这个事务函数有个细节值得说明:need := AResponse[2] + 5。Modbus RTU 响应帧的长度由字节数(第 3 个字节)决定,加上地址、功能码、字节计数字段这 3 个字节和末尾 2 个字节 CRC,就是完整帧长度。先收一帧头,再按帧头推断剩余长度,比固定等一个 256 字节更高效,也比盲目 Sleep 更可靠。
注意:Timeout 我建议按波特率动态算,9600 波特率下取 500ms 足够,115200 下可以压到 150ms。设置太长会让故障切换很慢,太短又容易误判超时,现场要根据实际设备性能做微调。
4. 03/06/16/01/05 五个功能码:把读写寄存器与线圈的 API 收拢成一个入口
4.1 写单个寄存器(06)和写多个寄存器(16)
读寄存器解决了数据采集,写寄存器解决控制下发。功能码 06 写单个寄存器,帧只有 8 个字节:地址、06、寄存器地址高、寄存器地址低、数值高、数值低、CRC。功能码 16 写多个寄存器,帧在 06 基础上多一个字节数和一个数据块。这两个功能码在现场使用频率最高,写设备参数、下发设定值、控制输出都是它们。
// 组装 06 写单个寄存器请求帧 function BuildWriteSingleRegRequest(const ADeviceId, ARegAddr, AValue: Word; var AFrame: array of Byte): Integer; var crc: Word; begin AFrame[0] := ADeviceId; AFrame[1] := $06; AFrame[2] := Hi(ARegAddr); AFrame[3] := Lo(ARegAddr); AFrame[4] := Hi(AValue); AFrame[5] := Lo(AValue); crc := Crc16Modbus(@AFrame[0], 6); AFrame[6] := Lo(crc); AFrame[7] := Hi(crc); Result := 8; end;06 的响应帧就是请求帧原样回来,所以解析时可以简单地比对回显。但严谨一点还是先做 CRC 校验再比对内容,因为有些从站对写命令的响应会有轻微延迟或夹杂其他字节。16 功能码的请求帧需要先数一下数据字节数,计算公式是2 + 2 × 寄存器个数,注意别把数量和高低字节写反。
4.2 读线圈(01)和写线圈(05)
线圈操作在 Modbus 里叫位操作,01 读线圈返回的是字节打包的位状态,05 写单个线圈则和 06 结构相似。注意 05 写线圈时,数值字段不是 0/1,而是$FF00表示 ON、$0000表示 OFF,新手用 1 去发送会收到异常码 02。
// 组装 05 写单个线圈请求帧 function BuildWriteCoilRequest(const ADeviceId, ACoilAddr: Word; AOn: Boolean; var AFrame: array of Byte): Integer; var crc: Word; val: Word; begin if AOn then val := $FF00 else val := $0000; AFrame[0] := ADeviceId; AFrame[1] := $05; AFrame[2] := Hi(ACoilAddr); AFrame[3] := Lo(ACoilAddr); AFrame[4] := Hi(val); AFrame[5] := Lo(val); crc := Crc16Modbus(@AFrame[0], 6); AFrame[6] := Lo(crc); AFrame[7] := Hi(crc); Result := 8; end;$FF00这个值很多第一次接触 Modbus 的人会直接踩进去,注释里我特意标清楚了。虽然 PLC 厂商的文档里也写了,但实际调试时看到设备不动,排查半天才发现发的是$0001的私有非标值。另外读线圈返回的位打包规则是低位在前,即第一个线圈在第 0 位,解析时用(B shr i) and 1取。
4.3 把读写操作统一收口到一个私有事务方法
五种功能码全部实现后,代码结构会有些重复:每个方法都是拼帧→发送→等响应→解析。我习惯把它们全部收拢到一个私有方法DoTransaction,公开方法只负责组织请求数据、调事务方法、再把响应转成业务结果。
// 统一事务入口骨架 function TModbusClient.DoTransaction( const ARequest: array of Byte; AReqLen: Integer; ADeviceId: Byte; ARespLen: Integer; out AResponse: array of Byte; var ARespLenOut: Integer): Boolean; begin // 内部持有串口句柄,负责锁、重试、日志 // 1. 进入临界区 // 2. 调用 ExecuteTransaction 发送+接收 // 3. 记录收发报文到日志缓冲区(如果开启) // 4. 离开临界区 end;这么设计的好处有三个:第一,所有串口操作都经过同一个临界区,多线程轮询时不会出现两个功能码乱发的情况;第二,日志只要加在 DoTransaction 里一处,就能记录全部收发帧,调试效率提升很多;第三,后续加 Modbus TCP 时,只需要替换 ExecuteTransaction 的底层实现,上层业务接口完全不动。
// 公开方法示例:读保持寄存器 function TModbusClient.ReadHoldings(ADeviceId, AStart, ACount: Word; var AValues: array of Word; var AReadCount: Integer): Boolean; var req: array[0..7] of Byte; resp: array[0..255] of Byte; reqLen, respLen: Integer; begin reqLen := BuildReadHoldRequest(ADeviceId, AStart, ACount, req); if reqLen = 0 then begin Result := False; Exit; end; Result := DoTransaction(req, reqLen, ADeviceId, resp, respLen); if not Result then Exit; Result := ParseReadHoldResponse(resp, respLen, ADeviceId, AValues, AReadCount); end;调用方拿到的是一个布尔返回值加一个填好的 Word 数组,完全不需要关心 CRC 和串口细节。这也是我在老项目里最常用的接入方式:业务窗体声明一个 TModbusClient 字段,按钮事件里直接调 ReadHoldings 就行。这里有一点要说明,DoTransaction里的 ARespLen 入参其实是冗余的,我保留了它的位置,方便以后加读多帧的扩展,实际 Delphi 里可以去掉只保留 out 参数,看个人习惯。
5. Delphi7与Modbus联调避坑清单:串口打不开、界面假死、字节序对不上
5.1 现象:Win7/Win10 上串口打开报"拒绝访问"
原因:最常见的是串口被别的程序占用,比如原厂调试软件、串口调试助手、甚至上次程序异常退出后句柄没释放。另一个隐蔽原因是 Delphi7 程序在 Win7 后没有管理员权限也能打开串口,但如果系统里杀毒软件或其他服务先行占用了这个 COM 口,就会返回 ERROR_ACCESS_DENIED。 解决:Open 失败时用 GetLastError 把错误码弹出来,自己写一个占用检测:先用 CreateFile 试开,失败就提示用户关闭其他占用串口的软件。我还习惯在打开前先枚举一遍可用串口,避免直接填一个不存在的 COM 口导致 CreateFile 返回INVALID_HANDLE_VALUE却不知道错在哪。
// 打开失败时输出详细错误码 if FHandle = INVALID_HANDLE_VALUE then begin ShowMessage(Format('串口打开失败,错误码: %d', [GetLastError])); Result := False; Exit; end;这个错误码对现场排查非常重要,5 号错误是拒绝访问,2 号是文件不存在,87 号是参数不对。别只看一个失败布尔值,把错误码打印出来能让现场少吵一架。
5.2 现象:把串口读写放在界面线程,程序频繁假死
原因:我见过不少人在 Delphi7 里用 TTimer 定时去读寄存器,TTimer 事件跑在消息循环里,如果 ReadFile 等响应用了阻塞式等待(尤其沿用前面 SetCommTimeouts 设的 200ms),在这 200ms 内窗口拖动、按钮点击全部卡顿。多个设备轮询时更是雪上加霜。 解决:把 TModbusClient 放到一个单独的后台线程里,或者至少用 TThread 的 Synchronize 做数据回传。线程里的读写全部走临界区,主界面只定时去取线程里最新的一份快照。
// 后台轮询线程骨架 type TPollThread = class(TThread) private FClient: TModbusClient; FValues: array[0..124] of Word; protected procedure Execute; override; end; procedure TPollThread.Execute; begin while not Terminated do begin // 加锁,调用 FClient.ReadHoldings // 成功后把快照 copy 到 FValues // Synchronize 更新主界面显示 Sleep(200); // 轮询间隔 end; end;线程方案的另一个收益是即使单次事务超时,主界面依然流畅,操作员不会因为上位机“转圈”而反复点按钮,反倒把负载拉高。
5.3 现象:读回来的寄存器值和 PLC 监控屏上显示不一致
原因:Modbus 寄存器默认是大端字节序,高字节在前低字节在后,而 Delphi7 的 Word 在内存里是小端。很多人在组包时已经注意了高低字节,但在解析响应时直接用AFrame[3] shl 8 or AFrame[4]是正确的,却忘了如果 PLC 里存的是 Float 或 LongInt,还需要自己做一次组合和按项目约定解析,不能直接拿两个寄存器往 Single 上强制转换。 解决:解析 32 位数据时明确写出合并顺序。比如 IEEE 754 单精度浮点数在 Modbus 中常见有两种字序:ABCD 和 CDAB,先确认 PLC 文档里说的是哪一种,再决定(reg1 shl 16) or reg2还是反过来。
// 两个寄存器合成 LongInt,按 A 寄存器为高字处理 function CombineRegs(ARegHi, ARegLo: Word): LongInt; begin Result := (LongInt(ARegHi) shl 16) or ARegLo; end;这个坑现场最容易吵架:上位机显示 42.5,PLC 屏上显示 58.3,两边都对了一遍逻辑,最后发现是大端小端约定不一致。我 Profile 过的设备里,西门子、施耐德、国产 PLC 各有各的习惯,所以一定要在工程文档里固定一种字序,并在配置界面加一个“字序交换”的开关。
5.4 现象:网关或仪表在收到第一条请求后经常超时,第二条就正常
原因:许多从站设备上电后串口收发还没完全就绪,或者前面一个 TCP 网关在建立串口链路时没有完成对设备的握手。另一个常见情况是 USB 转串口线的驱动在系统启动后枚举时间比程序启动慢,程序抢跑了。 解决:给控件加一个“设备就绪探测+首帧重试”的逻辑。第一次事务失败不立刻报错,而是间隔 200ms 重试两次。OpenComm 之后也先主动发一个 03 读 1 个寄存器的短帧,等设备回包后再认为链路真正建立。
// 探测就绪:重试两次,间隔 200ms function TModbusClient.ProbeDevice(ADeviceId: Byte): Boolean; var i: Integer; begin Result := False; for i := 1 to 3 do begin // 发 03 读 1 个寄存器的请求,成功则跳出 if ReadHoldings(ADeviceId, 0, 1, ...) then begin Result := True; Break; end; Sleep(200); end; end;这个探测函数在新设备接入、USB 重新插拔、网关断电上电时都有奇效。有人觉得多这 200ms 影响效率,实际上只有首次上电或掉线恢复才走这个分支,正常轮询完全不受影响。
5.5 现象:现场设备断电重启后,上位机永远收不到数据,必须重启程序才恢复
原因:串口设备掉线后,系统底层已经关闭了句柄或者 USB 转串口设备被系统重新枚举,原来的句柄变成 stale handle。Delphi7 程序里如果只做 ReadFile 超时重试,不重新打开串口,就会一直挂在死句柄上。 解决:在事务连续失败 N 次(比如 3 次)后,主动 Close 再 Open 串口,并做一次 ProbeDevice。同时把 USB 转串口重新拔插的情况纳入考虑,失败后等待操作系统重新枚举 500ms~1s 再重开。
// 掉线恢复策略 procedure TModbusClient.CheckRecovery; begin Inc(FFailCount); if FFailCount >= 3 then begin Close; Sleep(500); Open; FFailCount := 0; end; end;这里要注意,Close 之后不能立刻 Open,要留出系统重新枚举设备的时间。USB 转串口的断开重连通常需要几百毫秒,太急只会继续失败,让重试计数再次累积。
6. 从"能用"扩到"好用":轮询队列、Modbus TCP 与报文日志
控件做到上一步,单台设备读写已经稳定了。但现场常见的是多个从站挂在一条总线上,轮询调度写得不好,效率会很难看。我的扩展做法是:在 DoTransaction 外面包一个轮询队列,每个队列元素包含设备地址、功能码、请求帧、回调地址,线程按顺序逐个执行,某条超时并不影响后续帧。这个设计避免了把每个功能码都写成一次串行 Sleep 的低效方式。
另一个值得加的扩展是 Modbus TCP。物理层从串口换成 Socket,协议上只多了一个 MBAP 头(事务 ID、协议 ID、长度、单元 ID),CRC 没有了。把第 3 节的 ExecuteTransaction 里的串口收发替换成 TCP 收发即可,上层功能码和解析完全复用。这样同一套控件能同时接串口从站和网络网关,现场灵活性大很多。
这里我一般会在调试阶段打开一个报文级日志开关,把每次收发的原始十六进制写到文件里。自己做过 Delph7 项目的都知道,最难查的是那种偶发、无法稳定复现的通信问题,有了报文日志,就能在故障发生后翻出最后几帧对比,直接定位是主站拼包问题还是从站回包异常。这个日志开关平时关闭,现场出问题时打开,成本极低,回报极高。我自己现在接手任何老项目,第一件事就是加日志、加重试、加看门狗,这套组合拳帮我少熬了很多夜,希望帮到你。
本文还有配套的精品资源,点击获取