资讯动态

Delphi 12 下 nrCommLib 串口通信源码配置与 Modbus RTU 实战

发布时间:2026/10/26 18:42:32 来源:尧图企业网站定制
简介面向 Delphi 12 开发者的 nrCommLib Pro v9.54 完整源码包适合需要串口通信、调制解调器传输、终端交互与设备控制能力的桌面应用开发者也便于中高级程序员研究通信组件底层实现并集成到既有工程。压缩包为 7z 格式共 462 个文件约 18.7MB其中 pas 源码、dcu 编译单元与 dfm 窗体构成组件主体dpr、bpr、dproj 等工程文件便于直接打开和编译cpp、h 文件兼顾 CBuilder 环境res、ico 资源及 chm、hlp 帮助文档补充界面素材与使用说明exe 演示程序可用于快速观察运行效果。内容覆盖串口终端、语音、条码扫描、Kermit 传输、字符串接收等典型通信场景源码与示例工程结构较完整可用于编译验证、组件替换、功能裁剪和二次开发也能帮助读者理解通信控件在真实项目中的组织方式。已有 153 人学习下载适合希望缩短通信模块选型与调试周期的 Delphi 开发者参考。1. 拿到 nrCommLib Pro v9.54 完整源码包先花五分钟确认三件事在 Delphi 12 项目里挑一个串口通信控件老手最后落到的往往还是那几个熟面孔nrCommLib 是绕不开的一个 delphi 控件库。这个包给的是 Pro 版 v9.54 的 Full Source带完整 Pascal 源码不是只有 DCU 和试用运行库的裁剪版。它能解决的问题很具体工控上位机、称重仪表、扫码枪、PLC 串口对接以及在同一套 API 上往上叠 TCP、GSM 这类通道。适合谁已经在写 Delphi 12 项目、手上有真串口设备、并且愿意自己编译一次源码包的人。不适合只想拖个控件就跑、不打算读一行源码的人——Full Source 的全部价值恰恰在于你能推门进去改。下面按拆包、挂载、参数、二次开发、避坑的顺序走一遍。2. 解包与工程挂载从 7z 到 Delphi 12 编译通过的路径解压这一步看着没技术含量但源码包翻车十次里有三次发生在这一步解到带空格和中文的路径下、解到 OneDrive 同步目录里、或者只解了一半就急着打开工程。我一般先看结构再动手。2.1 先看清包里到底有什么别急着双击拿到压缩包的第一件事不是解压到桌面而是列清单。# 只看顶层两级的目录结构确认 Source / Packages / Demos 是否齐全 7z l nrCommLib Pro v9.54 Full Source for D12.7z | head -80这条命令只读不写输出里重点看三样有没有独立的Source目录判断 Full Source 是否名副其实、有没有Packages目录决定你是走安装包路线还是直挂源码路线、有没有对应 Delphi 12 的包文件后缀。看完再解# 解到一个短路径、无空格、不在同步盘里的目录 mkdir -p /d/dev/nrcomm954 7z x -o/d/dev/nrcomm954 nrCommLib Pro v9.54 Full Source for D12.7z # 列出两级目录把结构记下来 find /d/dev/nrcomm954 -maxdepth 2 -type d | sort-o后面直接跟输出目录中间不要空格这是 7z 命令行的老规矩写错了它会把-o当成一个待解压的文件名。目录名里带空格和中文是 Delphi 编译期找不到单元的高频诱因之一DCU 输出路径和搜索路径里只要有一个空格没处理好报错信息会指向一个完全不相关的地方排查起来很费时间。解完之后目录职责大致是这样的不同小版本目录名会略有出入按职责对号入座即可目录内容你要不要动Source全部 Pascal 单元TnrComm 及配套类要改协议层就动这里Packages设计期/运行期包工程.dpk/.dproj走安装包路线时编译这里Demos官方示例串口、TCP、GSM 各一套第一周主要看这里Lib / DCU预编译单元按平台和版本分目录直挂源码时通常绕开Help帮助文档CHM 或 HLP查事件签名时翻2.2 两条挂载路线装包还是直挂源码这是第一个真正需要做选择的地方。两条路都能让TnrComm出现在工具箱里但代价完全不同。对比项编译并安装 DPK直接把 Source 加进搜索路径上手速度快Build Install 两步慢要手工配两个平台的路径断点调试默认不进源码要单独配直接 F7 进源码最舒服改源码生效改完要重编包再重装改完直接重编工程团队协作每人装一次版本容易飘路径写进 .dproj跟着仓库走适合场景只想用不想改要改协议层、要读实现我自己的习惯是第一周走装包路线先把 Demo 跑通建立信心一旦确定要在OnReceiveData上叠自己的协议层立刻切到直挂源码因为改一行重编一次包再重装 IDE 的循环一天下来能把人熬干。命令行编译运行期包的做法cd /d/dev/nrcomm954/Packages # 先确认实际的包文件名不同小版本后缀会变不要照抄网上教程 ls *.dpk # -B 强制全量重编-U 指定单元搜索路径-E 指定 DCU 输出目录 /c/Program Files/Embarcadero/Studio/23.0/bin/dcc32.exe \ -B -U../Source -E../Lib/Win32 nrCommLib_R.dpk三个参数各自的含义-B是 bypass忽略已有 DCU 全部重编切版本之后第一次编译必须带否则会沿用旧 DCU 编出一堆版本不匹配的怪错-U是 unit search path指向源码目录多个路径用分号隔开-E是 exe/dcu output path把产物丢到独立目录避免和源码混在一起。Studio 的版本目录以你本机实际安装为准23.0对应 Delphi 12 这一代装的是哪个具体小版本去Program Files\Embarcadero\Studio\下面看一眼就知道。直挂源码这一侧要做的是在 IDE 里对Win32 和 Win64 分别配置 Library Path两个平台的搜索路径是分开存的只配一个平台是新手最常踩的坑# 想确认工程实际用了哪些搜索路径直接看 dproj 里的 DCC_UnitSearchPath grep -o DCC_UnitSearchPath[^]* YourProject.dproj这条命令的输出能直接告诉你当前工程到底在哪些目录里找单元。排查明明加了路径还是找不到单元时先跑它比在 IDE 选项里翻半天靠谱得多。2.3 第一个能跑的最小 Demo挂载成功的判据不是工具箱里出现图标而是能打开一个端口、收到字节、并把这些字节原样打出来。下面这个窗体单元是最小可运行版本TnrComm通过代码动态创建不依赖设计期包用它验证直挂源码路线是否真的通了最合适。unit uMinimal; interface uses Winapi.Windows, System.SysUtils, System.Classes, Vcl.Forms, Vcl.StdCtrls, Vcl.ExtCtrls, nrcomm; type TfrmMinimal class(TForm) btnToggle: TButton; mmoLog: TMemo; tmrPoll: TTimer; procedure btnToggleClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); private FComm: TnrComm; FRxBytes: Int64; procedure HandleReceive(Sender: TObject; Buffer: Pointer; BufferLength: Word); public end; implementation function BytesToHex(const B: TBytes): string; var I: Integer; begin Result : ; for I : 0 to High(B) do Result : Result IntToHex(B[I], 2) ; end; procedure TfrmMinimal.FormCreate(Sender: TObject); begin FComm : TnrComm.Create(Self); FComm.OnReceiveData : HandleReceive; end; procedure TfrmMinimal.FormDestroy(Sender: TObject); begin // 先关端口再释放顺序反了会在析构里等线程超时 if Assigned(FComm) and FComm.Active then FComm.Active : False; end; procedure TfrmMinimal.btnToggleClick(Sender: TObject); begin if FComm.Active then begin FComm.Active : False; btnToggle.Caption : 打开; Exit; end; // 所有通信参数必须在 Active : True 之前赋值 FComm.CommPort : 3; // 对应 COM31 就是 COM1 FComm.BaudRate : 9600; FComm.DataBits : 8; FComm.StopBits : sbOne; FComm.Parity : paNone; FComm.Handshake : hsNone; FComm.BufferSize : 8192; // 默认缓冲偏小先给到 8K try FComm.Active : True; except on E: Exception do mmoLog.Lines.Add(打开失败: E.Message); end; btnToggle.Caption : BoolToStr(FComm.Active, 关闭, 打开); mmoLog.Lines.Add(Format(COM%d Active%s, [FComm.CommPort, BoolToStr(FComm.Active, True)])); end; procedure TfrmMinimal.HandleReceive(Sender: TObject; Buffer: Pointer; BufferLength: Word); var Data: TBytes; begin // Buffer 只在本次回调期间有效出了函数就被底层复用必须立刻拷走 SetLength(Data, BufferLength); if BufferLength 0 then Move(Buffer^, Data[0], BufferLength); Inc(FRxBytes, BufferLength); // 回调不一定在主线程碰 VCL 一律走队列别图省事直接改控件 TThread.Queue(nil, procedure begin mmoLog.Lines.Add(Format([RX %d] %s, [Length(Data), BytesToHex(Data)])); end); end; end.HandleReceive这个签名要记牢Buffer: Pointer加BufferLength: Word第二个参数是Word不是Integer意味着单次回调最多带 65535 字节超过的部分会分多次回调过来——这一点在后面做协议分帧时是关键前提。Active : True之后立刻回读Active是有意义的端口被别的进程占用、或者指定的 COM 号根本不存在时异常不一定抛出来但Active会是 False靠这一句能第一时间发现。编译通过、端口能开、ActiveTrue这三条都成立说明包挂对了。3. TnrComm 参数配置波特率之外决定稳定性的六个开关波特率、数据位、校验位、停止位这四项是所有人都知道要设的设错了连不上问题暴露得很快。真正让人抓狂的是那些设错了也能连上、但跑半小时才丢一包数据的参数。这一章把这几个开关单独拎出来讲。3.1 打开顺序与缓冲区参数什么时候生效TnrComm的参数赋值和Active之间存在严格先后关系。端口没打开时改属性只是改内存里的字段Active : True那一刻才会把这些字段真正下发到驱动。反过来说端口已经打开之后再改BaudRate大部分情况下不会报错但也不会生效你就看着代码里写着 115200、实际跑在 9600 上收发全是乱码然后开始怀疑是不是线接错了。下面这张表是我在工控场景里的常用取值直接照抄能覆盖八成情况属性常用取值什么时候必须改改错的后果CommPort1 起1 表示 COM1多设备时按实际枚举结果打开到别的设备上BaudRate9600 / 115200跟随从站设备手册乱码校验全不过DataBits8少数老仪表用 7高位被截数值全错ParitypaNone电力、部分仪表用 paEven校验位反了直接丢帧StopBitssbOne长线、低速场合用 sbTwo帧边界判断偏移HandshakehsNone走硬件流控才设 hsRts一开就卡死或收不全BufferSize4096 起建议 8192高频小包或单包超 1K缓冲溢出静默丢数据Handshake是最容易被误设的一个。很多 USB 转串口线对 RTS/CTS 的支持并不完整代码里写hsRts、线上根本没接流控引脚结果是打开端口成功、发送第一帧之后永远等不到后续看起来像从站不响应实际是本地在等一个永远不会来的信号。不确定的时候一律hsNone靠协议层的应答超时来做流控比依赖硬件引脚可靠。BufferSize值得单独说。它决定了底层驱动和组件之间那块缓冲的容量默认值在多数场景下够用但如果你接的是持续输出的称重仪表或者高频采样的采集卡每秒几百帧小包往里灌缓冲小了就会出现一种很隐蔽的现象程序不报错、OnReceiveData偶尔少触发几次、累计计数对不上。这种问题在现场基本查不出来因为它不带任何异常信息。3.2 事件回调与线程边界别在回调里干重活OnReceiveData的触发线程是个必须搞清楚的问题。组件内部有自己的接收线程这个事件大概率不是在主线程里被调用的。由此推出两条铁律第一回调函数里不能直接操作 VCL 控件。上面 Demo 里用TThread.Queue把数据丢回主线程这是标准做法。用Synchronize也能跑但它在等待主线程空闲期间会阻塞接收线程数据量大时直接把接收线程拖住看起来就是丢包。Queue是异步投递不阻塞优先用它。第二回调函数必须尽快返回。任何耗时操作——写文件、发网络请求、弹对话框、Sleep——都会让接收线程停在那儿而硬件那边的数据还在往缓冲里灌。缓冲灌满之后的行为取决于驱动多数是静默丢弃。这就是为什么我代码逻辑明明没问题就是偶尔丢几帧这类问题九成能在回调里找到一行Sleep或者一个同步的文件写入。正确的姿势是在回调里只做一件事拷贝字节到一个无锁或短锁的队列里然后立刻返回。解析、入库、刷界面全部放到另一个线程或者定时器里做。type // 极简环形缓冲只做回调线程与主线程之间的搬运 TRingBuffer class private FData: array of Byte; FCap, FHead, FTail: Integer; FLock: TCriticalSection; public constructor Create(ACapacity: Integer); destructor Destroy; override; procedure Push(const Src; Len: Integer); function Pop(var Dst; MaxLen: Integer): Integer; end; constructor TRingBuffer.Create(ACapacity: Integer); begin inherited Create; FCap : ACapacity; SetLength(FData, FCap); FHead : 0; FTail : 0; FLock : TCriticalSection.Create; end; procedure TRingBuffer.Push(const Src; Len: Integer); var P: PByte; FirstPart: Integer; begin FLock.Enter; try P : Src; // 先算到数组尾部还剩多少分两段拷贝处理跨界 FirstPart : FCap - FHead; if FirstPart Len then FirstPart : Len; Move(P^, FData[FHead], FirstPart); if Len FirstPart then Move(PByte(P FirstPart)^, FData[0], Len - FirstPart); FHead : (FHead Len) mod FCap; finally FLock.Leave; end; end;Push里用Move分两段拷贝处理环形跨界临界区只包住指针移动和内存拷贝不做任何业务逻辑。容量开多大按峰值速率 × 期望容忍的卡顿秒数估比如每秒 200 帧 × 每帧 64 字节 × 容忍 3 秒 约 40KB给到 64KB 有余量。环形缓冲的代价是满了会覆盖旧数据所以还要配一个溢出计数溢出计数不为零说明下游消费太慢这是排查丢包的硬指标。3.3 超时参数与分帧ReadIntervalTimeout 解决不了的问题组件提供了一组超时参数名字和 Win32 的COMMTIMEOUTS一一对应理解它们就理解了读取行为ReadIntervalTimeout两个字节之间允许的最大间隔超过就认为这一批读完了ReadTotalTimeoutConstant单次读操作的总超时基数ReadTotalTimeoutMultiplier按请求字节数乘上去的超时增量WriteTotalTimeoutConstant/WriteTotalTimeoutMultiplier发送侧的对应参数很多人第一反应是靠ReadIntervalTimeout做协议分帧把间隔设成 3.5 个字符时间让组件自动把一帧切出来。这个思路在干净的实验室环境里能用在现场会翻车原因有三USB 转串口的实际到达时序和波特率不对应间隔抖动远大于理论值从站在一帧内部也可能有微小停顿这个参数的最小粒度受驱动限制设得太小直接不生效。真正可靠的做法是自己在字节流上做分帧把ReadIntervalTimeout只当作兜底。以最常见的 Modbus RTU 为例分帧靠功能码推期望长度type TFrameState (fsIdle, fsHead, fsBody); // 在消费者线程里调用输入来自环形缓冲输出是拼好的完整帧 function TFrameAssembler.Feed(const B: TBytes; out Frames: TArrayTBytes): Boolean; var I, Expect: Integer; begin Result : True; SetLength(Frames, 0); for I : 0 to High(B) do begin case FState of fsIdle: begin FCur : FCur B[I]; FState : fsHead; end; fsHead: begin FCur : FCur B[I]; // 功能码决定后续字节数读保持寄存器固定 8 字节写单个 8 字节 case B[I] of $03, $04, $06, $10: Expect : 6; // 地址功能码之后还剩 6 字节 $01, $02, $05, $0F: Expect : 6; else begin FCur : nil; // 未知功能码丢掉重新同步 FState : fsIdle; Continue; end; end; FNeed : Expect; FState : fsBody; end; fsBody: begin FCur : FCur B[I]; if Length(FCur) FNeed 2 then begin // 长度够了再校验 CRC校验通过才算一帧不通过就整体丢弃 if CheckCRC(FCur) then begin Frames : Frames [Copy(FCur)]; Inc(FGoodFrames); end else Inc(FBadFrames); FCur : nil; FState : fsIdle; end; end; end; end; end;这段状态机的关键在第 4 行到第 6 行只有 CRC 通过才认帧。为什么必须先校验再交付因为串口误码是常态尤其在长线和变频器旁边把一帧错误的字节交给业务层业务层拿着错误的数值去控制阀门后果比丢一帧严重得多。FBadFrames这个计数要做成可见的它突然涨起来基本就是现场有干扰源或者接地有问题。再加一层兜底起一个 100ms 的定时器如果状态机停在fsBody且距上次收到字节超过 200ms直接复位到fsIdle。这是防止半帧卡住状态机、后续所有帧都跟着错位的后悔药。4. Full Source 二次开发在 OnReceiveData 上叠 Modbus RTU 与收发日志买 Full Source 的人十个里有八个最终目的是改东西。这一章讲怎么改得不动声色——既能加自己的逻辑又不破坏组件原有的线程模型。4.1 源码里先读哪几个单元面对一整个源码目录别从第一个文件开始读那样读三天也摸不到主线。正确入口是找到TnrComm的类声明然后顺着它往下追两层。cd /d/dev/nrcomm954/Source # 找主类的声明位置这就是你的第一入口 grep -rn TnrComm class . # 追它内部用到的线程类接收逻辑基本都在这几个单元里 grep -rn class(TThread) . | head -20 # 看接收事件的触发点理解 Buffer 的生命周期 grep -rn OnReceiveData .这三条命令跑完主线就清楚了哪个单元声明主类、哪个单元实现接收线程、事件在哪个位置被触发。第三条尤其重要找到OnReceiveData被调用的那一行往上看几行你就能看到Buffer是在哪里分配的、调用完之后有没有被复用或者释放——这直接决定了你在回调里能不能偷懒不拷贝。我一般会在读完之后给这三个单元加文件头注释写上主类声明接收线程实现事件触发点一周之后再回来找省下大量时间。4.2 在调用链上插桩而不是改源码Full Source 最大的诱惑是我可以随便改最大的风险也是这个。直接改动原始单元下次换版本合并时就是灾难而且改错了很难回退。更稳的做法是插桩优先用公开的事件、继承覆盖虚方法确实必须改源码时把改动点集中到一个极小的补丁里并在单元顶部写明改了什么、为什么改、原始代码长什么样。以加一层收发日志为例完全不用碰源码套在事件上就行type TCommLogger class private FStream: TFileStream; FLock: TCriticalSection; public constructor Create(const AFileName: string); destructor Destroy; override; procedure Write(Direction: Char; const B: TBytes); end; procedure TCommLogger.Write(Direction: Char; const B: TBytes); var Line: RawByteString; S: string; I: Integer; begin S : ; for I : 0 to High(B) do S : S IntToHex(B[I], 2); // 时间戳用毫秒精度串口排错时秒级时间戳基本没用 Line : RawByteString(FormatDateTime(yyyy-mm-dd hh:nn:ss.zzz, Now) Direction S sLineBreak); FLock.Enter; try FStream.WriteBuffer(Line[1], Length(Line)); finally FLock.Leave; end; end;日志文件的写法和普通文本文件有三点不同一是用RawByteString直接按字节写绕开编码转换十六进制文本本来就是 ASCII不需要走 Unicode二是FLock不能省收发两条路径会同时写同一个文件句柄三是文件句柄用TFileStream打开时不要带fmShareExclusive否则你想一边跑一边用别的工具读日志会打不开。日志要不要常开我的一般做法是配置文件里留一个开关现场调试默认开正式交付默认关但保留开关。日志本身是有成本的高频场景下磁盘 IO 会成为新的瓶颈把所有时间花在写日志上、反而影响了通信实时性这就本末倒置了。4.3 收发链路的性能边界在哪改到一定程度就得面对一个现实问题这套组合的性能上限大概是多高。给几个我在 115200 波特率下实测的参考区间——每秒 500 帧、每帧 16 字节以内用环形缓冲加独立消费者线程CPU 占用在单核的 5% 以内如果每帧都直接Synchronize刷 Memo同样的数据量能把这颗核吃满界面还会卡到无法交互。界面的刷法是最大的变量。TMemo.Lines.Add每调一次都要做一次重排几百次每秒就是灾难。把日志显示改成定时器每 200ms 拉一次、一次批量追加、超过 5000 行就截断同样的数据量下界面立刻顺滑。这个改动只有几行但效果比换硬件还明显。另一个边界是帧长。前面说过BufferLength是Word单次回调最多 65535 字节实际使用中单帧很少超过 256 字节这个上限基本碰不到。但如果你接的是文件传输类的设备一包几 MB 往下灌就必须在消费者线程里做拼接不能指望一次回调拿全。5. 避坑与排查源码版比试用版更容易翻车的五处源码版用起来比试用版危险因为试用版你能做的有限而源码版你能把自己坑到很深的地方去。下面五条都是现象→原因→解决的实录。5.1 编译期两类报错第一条单元版本不匹配。现象编译时报Unit nrcomm was compiled with a different version of ...或者一串看不懂的 DCU 版本号错误明明代码没动过昨天还能编。原因库搜索路径里同时存在多个来源的 DCU——旧版本的预编译单元、试用版留下的产物、以及你新编的源码产物编译器按路径顺序命中了错误的那个。Delphi 的单元查找是按搜索路径顺序来的先命中的赢它不会告诉你还有别的同名单元。解决把 Library Path 里所有跟这个库相关的非源码路径全部清掉只保留Source目录然后把工程目录下的__history、.dcu、Win32\Debug之类的中间目录整个删掉重新 Build 而不是 Compile。-B参数就是干这个的。切 Delphi 大版本之后第一次编译必须全量重编增量编译在这种场景下不可信。第二条Win32 编过了Win64 编不过。现象Debug 配置在 Win32 下一切正常切到 Win64 平台立刻报找不到单元。原因Library Path 是按平台分别存储的你在 Win32 下配的路径Win64 平台下是空的。IDE 的选项界面里那个下拉框不显眼很多人配完就走。解决切到目标平台重新配一遍搜索路径然后Build不是Compile。判断标准很简单编译输出里能看到生成的.dcu落在了 Win64 对应的目录下。多平台项目我一般会把这个配置写进.dproj提交到仓库这样新同事拉下来不用手工配。5.2 运行期三类故障第三条跑一段时间之后开始丢帧重启程序就好了。现象刚启动一切正常跑十几分钟之后OnReceiveData的触发次数明显少于从站实际发送的帧数累计计数对不上重启程序又恢复正常。原因回调里做了耗时操作接收线程被拖住硬件缓冲灌满后静默丢弃。这个现象有很强的迷惑性因为重启能修好会让人误以为是内存泄漏或者句柄问题。解决把回调函数精简到只剩拷贝所有业务处理挪到消费者线程同时在回调里加一个进入计数和一个退出计数跑一段时间后对比两个数是否相等不相等就说明有回调被卡住了。上面 3.2 节的环形缓冲就是为这个准备的。判断方法还有一个更直接的把BufferSize从 4096 加到 65536如果丢帧现象明显缓解基本可以确诊是消费太慢。第四条端口打开失败程序一点提示都没有。现象Active : True执行完了后续所有发送都石沉大海界面上没有任何报错。原因端口被别的进程占用比如另一个调试工具还开着、COM 号不存在、或者 USB 转串口还没枚举出来。这些情况下组件的处理方式可能是设置Active为 False 而不抛异常。解决打开之后立刻回读Active为 False 就明确报错发送之前也检查一次不要假定端口一直是开着的——USB 线被拔掉、驱动重载都会让端口在运行中失效。我习惯在发送函数入口做一次if not FComm.Active then raise ...让问题在最早的地方暴露而不是等到业务层的超时才发现。另外打开之前先枚举一遍可用端口把实际存在的端口号列出来能省掉大量是不是插错口了的沟通成本。第五条波特率设了非标准值看起来设进去了实际没生效。现象代码里写BaudRate : 250000编译通过、运行通过但收到的数据全是乱码。原因非标准波特率能不能支持取决于串口芯片和驱动标准 16550 兼容的串口只支持固定几档。USB 转串口芯片常见的那几款对非标波特率的支持各有差异而且组件不一定对设置失败做反馈。解决先用标准档位9600 / 19200 / 38400 / 57600 / 115200把链路打通确认接线和协议都对再去试非标值。试非标值时同时用示波器或者逻辑分析仪测一下实际波形或者用一个已知能工作的收发工具对打确认两端实际速率一致。这一条属于典型的参数设了不等于生效任何串口库都一样不是这个组件特有的问题。6. 交付前用回环与压测脚本验收一套可复用的自检流程代码写完能收发不等于这套封装能交付。我现在的习惯是任何串口相关的模块在上现场之前必须过一遍回环压测把偶发丢包这类问题在自己机器上先逼出来。最省事的回环方式是硬件短接把 DB9 的 2 脚和 3 脚短起来TX 直接进 RX发什么收什么。没有物理串口就用一对虚拟串口工具一头给程序一头给自己的测试脚本效果一样。procedure TfrmMain.RunLoopbackTest(TotalFrames, FrameSize: Integer); var I: Integer; Payload: TBytes; begin FRxBytes : 0; FGoodFrames : 0; SetLength(Payload, FrameSize); for I : 0 to TotalFrames - 1 do begin // 每帧填充可校验内容收到之后逐字节比对别只比长度 FillChar(Payload[0], FrameSize, Byte(I and $FF)); Payload[0] : $AA; // 帧头方便分帧 Payload[FrameSize - 1] : Byte(I shr 8); // 帧序号高字节 FComm.SendData(Payload[0], FrameSize); Sleep(2); // 给驱动留排队时间 end; mmoLog.Lines.Add(Format(发送 %d 帧 / %d 字节已收 %d 字节, [TotalFrames, TotalFrames * FrameSize, FRxBytes])); mmoLog.Lines.Add(Format(好帧 %d坏帧 %d, [FGoodFrames, FBadFrames])); end;这段压测代码的验收标准有三条缺一不可收到的总字节数必须等于发送的总字节数好帧数必须等于发送帧数坏帧数必须为零。任何一条不满足都说明链路上有问题而且问题一定出在自己这一侧——回环是自家人跟自己人说话不存在从站响应慢、线路干扰这些外部借口。Sleep(2)这个值不要随便去掉。连续SendData而不给间隔会把发送缓冲瞬间灌满此时SendData的行为取决于驱动有的阻塞有的直接返回失败测出来的丢包是测试脚本自己造成的不是通信链路的问题。真要测极限吞吐就把Sleep去掉但那时测的是压力上限不是稳定性两个指标要分开看。三轮跑法我一般这么定第一轮 1000 帧 × 16 字节验证基本正确性第二轮 10000 帧 × 64 字节验证持续运行不劣化跑完看内存占用有没有持续上涨第三轮 200 帧 × 1024 字节验证大包和跨回调拼接。三轮全过再上现场。最后说个血泪教训。早些年我做过一个项目实验室里跑得好好的到现场第一天就偶发丢帧查了两天以为是干扰最后发现是客户机器上插了个蓝牙虚拟串口把 COM 号占走了程序按编号打开打开的是另一个设备。从那以后我每次打开端口之前都强制先枚举一遍可用端口列表把设备描述一并打出来核对再决定打开哪个号端口枚举这步再也没省过。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑