Delphi 12通信控件库源码:上位机串口丢帧与断线重连实战

发布时间:2026/10/26 2:30:38
Delphi 12通信控件库源码:上位机串口丢帧与断线重连实战 简介面向使用 Delphi 12 的开发者nrCommLib Pro v9.54 完整源码包可用于串口、调制解调器、终端通信及条码扫描等场景的控件集成与二次开发。压缩包共 462 个文件约 18.7MB包含 95 个 pas 源码、66 个 dcu 编译单元、43 个 dfm 窗体、37 个 cpp 与 33 个 dpr 示例工程以及 res、dcr、dproj、exe 等资源与演示文件覆盖源码、示例、帮助文档和可执行演示便于对照学习控件调用方式与工程组织。已有 153 人学习下载适合需要研究通信控件内部实现、快速搭建串口通信 Demo 或进行项目移植的中高级 Delphi 开发者可从中获取完整源码、示例工程和文档支持减少自行封装通信模块的成本。1. 上位机跑到现场就丢帧这套 Delphi 控件库能省掉多少轮子去年帮一个做设备集成的朋友收拾烂摊子上位机用 Delphi 写串口接了七八台仪表实验室里跑得好好的一到现场就开始丢帧、断线后再也连不上日志里干干净净什么都看不到。拆到最后发现问题不在业务代码而在自己手搓的那套串口读写线程——缓冲区没做环形、异常吞掉没往外抛、句柄关了但线程还活着。这类活儿其实早就有成熟轮子我手上这套 Delphi 12 通信控件库全套源码下称 CommKit就是干这个的。它把串口、TCP/UDP、终端仿真、文件传输协议、GPS 与短信这些设备侧常用的通信方式统一封装成一组 VCL 组件附完整 Pascal 源码和包工程能直接编进 Delphi 12 的工程里。适合两类人一是写工控上位机、要做设备对接和私有协议解析的二是手里只有 D12、但老项目还停在早期版本、需要一份能改能调的通信层源码的 Delphi 控件开发者。2. 架构与目录一套控件怎么把串口、网络和协议层串起来拿到这种 Full Source 的包第一件事不是急着装而是先看清它怎么分层。分层没搞明白就直接拖组件上窗体后面改协议、换传输方式的时候会非常难受——我见过有人在串口事件里写死了 Modbus 的解析逻辑后来要改成 TCP 接入等于重写。2.1 三层职责传输层、协议层、设备层各管什么CommKit 这类库的骨架基本是三层理解了这个用起来就顺了传输层负责“字节怎么进来出去”不关心字节是什么意思。串口、TCP 客户端/服务端、UDP 都在这一层对外暴露的是同一组方法打开、关闭、写、读、状态查询以及OnReceive、OnError、OnStateChange这类事件。这一层的关键词是线程和缓冲所有阻塞读写都在工作线程里跑主线程只收事件。协议层负责“字节怎么组织成帧”。帧头帧尾、长度字段、校验CRC16、LRC、异或和、转义、超时重传都在这一层。它的输入是传输层吐出来的原始字节流输出是一个个完整的帧。这一层是二次开发改动最频繁的地方。设备层负责“帧怎么变成业务语义”。比如某型号仪表的 0x03 功能码对应读寄存器、返回的 6 个字节要按大端转成浮点。这层通常是业务代码自己写的控件库最多给几个现成的实现做参考。三层之间靠事件和回调串起来而不是靠互相uses。这点很重要传输层不知道协议层存在协议层不知道业务存在所以你把串口换成 TCP只要换传输层实例上面两层一行不用动。提示判断一个通信控件库值不值得用就看它换传输介质时上层要不要改代码。要改说明分层是假的。2.2 源码包目录结构与包工程dpk的引用顺序解压之后目录名各家版本可能略有出入但结构基本一致。按下面这张表对照着找就行路径内容要不要动Source/Core/类型定义、基类、环形缓冲、日志少动改了影响全局Source/Port/串口实现、端口枚举、波特率常量基本不动Source/Net/TCP/UDP 传输实现换传输方式时看Source/Protocol/帧解析基类、校验算法、常见协议实现二次开发主战场Source/Device/终端仿真、文件传输、GPS、短信按需引用Packages/.dpk包工程按 Delphi 版本分目录装包用Demos/一堆可直接编译的示例工程最好的入门文档Docs/接口说明与变更记录查方法签名包工程这块有个顺序问题。通常会有两个到三个 dpk设计期包带Dcl前缀注册组件到工具箱、运行期包R前缀或纯名字被工程引用。先编译运行期包再编译设计期包反过来会报找不到单元。装的时候走 IDE 的「Component → Install Packages → Add」选中设计期包编出来的 bpl别手动去拷 bpl 到系统目录那是老版本 Delphi 的野路子D12 上容易和别的包抢同名单元。如果不想装进工具箱、只想在工程里用更省事的做法是新建一个包把Source/Core、Source/Port、Source/Protocol三个目录的.pas全部 Add 进去编译成自己的运行期包然后工程引用它。这样做的好处是源码改动完全可控也不会污染 IDE 的组件面板。我一般倾向后者尤其是要动协议层的时候。3. 跑通第一条链路Delphi 12 下装包、开串口、收一帧十六进制数据装完包、能编译通过只是证明环境对了。真正要验证的是“数据能不能进来、能不能出去”这一步跑通后面才有得谈。下面这条链路是我每次拿到新通信库的固定验证流程大概二十分钟能走完。3.1 最小验证工程一个窗体、两个按钮、一个 Memo新建一个 VCL 应用窗体上放ButtonOpen打开串口、ButtonSend发一帧、MemoLog十六进制日志、ComboBoxPort端口列表。然后在uses里加上控件的核心单元具体单元名以包里实际为准一般是Core和Port两个。端口列表别写死COM1..COM8用库里的枚举方法现场机器的串口号能到 COM30 以上写死的代码到了客户那边就是“软件打不开”的投诉uses CommKit.Core, CommKit.Port; procedure TFormMain.FormCreate(Sender: TObject); var Names: TStringList; begin Names : TStringList.Create; try TCommPort.EnumPorts(Names); // 枚举当前可用串口返回 COM3 这类名字 ComboBoxPort.Items.Assign(Names); if ComboBoxPort.Items.Count 0 then ComboBoxPort.ItemIndex : 0; finally Names.Free; end; end;EnumPorts是类方法不需要实例。它读的是系统设备映射比在注册表里翻HARDWARE\DEVICEMAP\SERIALCOMM干净得多也不会把已经拔掉的虚拟串口列出来。3.2 打开串口与参数配置波特率、校验、超时的实际取值参数这一块是所有新手翻车的重灾区。9600/8/N/1 是默认值但现场设备的组合五花八门7 位数据位配偶校验的老仪表我见过不止一次。配置代码如下var FPort: TCommPort; procedure TFormMain.OpenPort; begin if FPort nil then begin FPort : TCommPort.Create(Self); FPort.OnReceive : DoReceive; // 数据到达回调在工作线程里触发 FPort.OnError : DoError; // 断线、超时、硬件错误都走这里 end; if FPort.Active then FPort.Close; FPort.PortName : ComboBoxPort.Text; // 只写 COM3不要带冒号 FPort.BaudRate : 9600; // 波特率用整数别用字符串 FPort.DataBits : 8; FPort.Parity : pNone; // pEven / pOdd / pMark / pSpace FPort.StopBits : sbOne; // sbOnePointFive / sbTwo FPort.ReadTimeout : 200; // 毫秒读不到就返回而不是死等 FPort.WriteTimeout : 500; // 写超时USB 转串口拔线时会用到 FPort.Active : True; // 置 True 等价于 Open置 False 等价于 Close end;几个点值得单独说。PortName不要带冒号很多老代码习惯写COM3:在新版实现里会直接抛异常BaudRate是整数属性写成9600字符串编译器不报错但运行时行为不对ReadTimeout不是越小越好200ms 是串口场景的经验值设成 10ms 会导致高频唤醒、CPU 占用上去设成 0 就是无限阻塞界面关不掉。Active这个属性是这类库的通用设计本质是Open/Close的属性化封装好处是可以直接绑到复选框上。但要注意在OnReceive里不要碰Active那是工作线程上下文关端口会等待线程退出容易自己等自己。3.3 事件回调与线程边界OnReceive 里到底能不能刷 UI这是必须讲清楚的一点。OnReceive是在库内部的读线程里触发的你在里面直接改MemoLog.Lines或者改Label.Caption轻则界面错乱重则访问违例而且是那种“跑一天崩一次”的玄学问题抓不到堆栈。正确写法是把数据拷出来切回主线程procedure TFormMain.DoReceive(Sender: TObject; const Data: TBytes); var Snapshot: TBytes; begin Snapshot : Copy(Data); // 必须拷贝Data 的内存回调返回后会被复用 TThread.Queue(nil, // Queue 不阻塞读线程Synchronize 会 procedure begin MemoLog.Lines.Add(Format([RX %d] %s, [Length(Snapshot), BytesToHex(Snapshot)])); end); end;参数说明一下Data是TBytes动态数组指向库内部缓冲。回调返回后这块内存会被下一帧覆盖所以不拷贝直接存到成员变量里后面读到的全是脏数据——这个坑我踩过现象是日志里偶尔出现重复帧查了半天以为是设备重发。TThread.Queue和Synchronize的区别是前者入队即返回、不阻塞读线程后者要等主线程处理完串口高速率场景下用Synchronize会把读线程拖死进而导致缓冲区溢出丢数据。发送端同理Write的返回值是实际写入的字节数别当成 Boolean 用function TFormMain.SendHex(const AHex: string): Boolean; var Buf: TBytes; Written: Integer; begin Buf : HexToBytes(AHex); Written : FPort.Write(Buf); Result : (Written Length(Buf)); if not Result then MemoLog.Lines.Add(Format([TX 部分写入] %d/%d, [Written, Length(Buf)])); end;写完拿一台设备或者一个串口调试助手对发收到完整的一帧十六进制这条链路就算通了。4. 核心模块拆解读线程、环形缓冲与帧解析的完整链路链路通了之后就该看里面是怎么转的了。这一章讲三个东西数据怎么从硬件进到内存、内存里的字节流怎么切成帧、断了之后怎么自己接回来。这三块是通信层出问题的高发区也是二次开发绕不开的部分。4.1 读线程与环形缓冲区为什么不建议用 TStringList 存数据库内部通常是一个独立的读线程循环调用系统的读接口Windows 上是ReadFile配重叠 I/O或者WaitCommEvent监听事件掩码把拿到的字节塞进一个缓冲区然后触发OnReceive。缓冲区基本都是环形队列ring buffer而不是TStringList或者TMemoryStream。原因很直接串口数据是持续的流TStringList每次追加都要重新分配并可能搬移内存高速率下115200 以上连续收分配次数暴涨GC 压力和内存碎片一起来表现出来就是“跑一会儿就卡”。环形缓冲的写入是定长数组内移动指针O(1)不产生分配。缓冲区容量也要留意。默认值一般是 4KB 到 16KB低速率够用。如果设备是突发式发送比如一次吐 8KB 的曲线数据要么把缓冲调大要么在OnReceive里尽快把数据取走做二级缓冲——但千万别在回调里做耗时解析那是在读线程上干活解析慢了缓冲区照样溢出。我一般的做法是OnReceive里只做拷贝和入队把TBytes塞进一个线程安全的TThreadedQueueTBytes另起一个解析线程消费。这样读线程永远不阻塞解析再慢也只是队列变长不会丢数据而且队列长度本身就是一个很好的监控指标——超过阈值就说明解析跟不上了。4.2 帧解析粘包、半包与重同步的处理套路协议层最容易写错的地方是把“一次OnReceive当成一帧”。串口没有消息边界一次回调可能是半帧也可能是三帧粘在一起这取决于驱动缓冲和到达时序不可控。正确的模型是维护一个接收缓冲区每次把新数据追加进去然后循环尝试从缓冲区头部切出完整帧切不出来就退出等下一批数据。下面这段是这套逻辑的核心几乎每个项目我都会抄一遍type TFrameParser class private FBuf: TBytes; public procedure Feed(const AData: TBytes; AOut: TListTBytes); end; procedure TFrameParser.Feed(const AData: TBytes; AOut: TListTBytes); var Idx, FrameLen, GotCrc, CalcCrc: Integer; Frame: TBytes; begin if Length(AData) 0 then FBuf : FBuf AData; // 追加半包靠这里兜住 while True do begin Idx : FindHead(FBuf); // 找帧头 0xAA 0x55 if Idx 0 then begin if Length(FBuf) MAX_BUF then FBuf : nil; // 全是噪声防内存涨爆 Exit; end; if Idx 0 then FBuf : Copy(FBuf, Idx, Length(FBuf) - Idx); // 丢掉帧头前的杂字节 if Length(FBuf) 4 then Exit; // 长度字段还没收全 FrameLen : FBuf[2] or (FBuf[3] shl 8); // 小端长度含帧头与 CRC if (FrameLen 6) or (FrameLen MAX_FRAME) then begin FBuf : Copy(FBuf, 1, Length(FBuf) - 1); // 长度非法滑一字节重同步 Continue; end; if Length(FBuf) FrameLen then Exit; // 半包等下一批 Frame : Copy(FBuf, 0, FrameLen); GotCrc : Frame[FrameLen - 2] or (Frame[FrameLen - 1] shl 8); CalcCrc : Crc16(Frame, FrameLen - 2); if GotCrc CalcCrc then begin FBuf : Copy(FBuf, 1, Length(FBuf) - 1); // 校验错滑一字节重找帧头 Continue; end; AOut.Add(Frame); // 出一帧完整数据 FBuf : Copy(FBuf, FrameLen, Length(FBuf) - FrameLen); end; end;逐段解释。FBuf : FBuf AData是唯一的输入口无论粘包半包都先进缓冲这一步保证不丢字节。FindHead找不到帧头时如果缓冲区已经超过MAX_BUF比如 64KB说明对面在发非协议数据或者波特率配错了直接清空比继续攒着好否则内存会一路涨上去。FrameLen从第 3、4 字节按小端拼出来这是最常见的做法有些协议用大端或者单字节长度改这两行就行。长度合法性判断是必须的收到一个FrameLen 0会让后面的Copy死循环收到FrameLen 60000会一直等不到数据整个解析器卡死。所以要有上下界一般是 6 到 4096。校验失败和长度非法时的处理是滑一个字节重新找帧头而不是清空缓冲区。清空看着干净实际上会把紧跟在坏帧后面的好帧一起丢掉在现场噪声大的环境里会表现为“偶发丢命令”。滑窗虽然要多循环几次但能自己爬出来这是血泪经验换来的。参数上MAX_BUF建议设成最大帧长的 8 到 16 倍MAX_FRAME必须和协议文档一致别为了“兼容”随手放大放大了等于放弃长度校验这道防线。4.3 断线重连把状态机写清楚别用定时器硬怼USB 转串口最经典的问题就是拔了再插端口号可能变句柄失效但对象还“活着”。用定时器每秒钟try Close; except end; Open;硬怼在 D12 上会偶发句柄泄漏跑几天后端口打不开。靠谱的做法是一个小状态机状态就四个csClosed、csOpening、csOpened、csBackoff。OnError里收到断线错误一般是 I/O 错误或设备移除把状态置到csBackoff记录一个递增的重试间隔1s、2s、4s、8s上限 30s定时器到点在csBackoff状态才尝试重开。重开成功后重置间隔。串口名要在重开前重新枚举一次因为 USB 转串口重新插拔后很可能从 COM3 变成 COM5拿旧名字去开必然失败。另外OnError回调同样在工作线程上状态切换记得加锁或者干脆用TInterlocked.Exchange改状态字别直接读写整型字段。5. 避坑与排查编译不过、现场丢包、句柄泄漏的几条实战记录这一章全是踩过的坑按“现象 → 原因 → 解决”写遇到对得上的直接抄结论。5.1 编译期与安装期的问题现象一装上包之后组件面板里没有或者有但拖到窗体上报“类未注册”。原因通常是只编译了运行期包没编译设计期包或者两个包编译时用的目标平台不一致——D12 默认可能给你编成 Win64而 IDE 是 32 位的注册不进去。解决把两个包的 Target Platforms 都勾成 Win32要 64 位就再各编一份先运行期包Build再设计期包Build然后Install。装完重启 IDE 生效。现象二编译报Unit xxx was compiled with a different version of ...。说明机器上还留着旧版本的 dcU 或者旧 bplD12 的搜索路径里同时存在两份同名单元。解决把Source目录从 IDE 的 Library Path 里移除如果已经加进了工程组就只保留一份删掉所有__history、dcu输出目录重新 Build。这个错误看着吓人其实九成是路径脏了。现象三源码版本是给更高版本 Delphi 写的用 D12 编报一堆语法错。有些 Full Source 里用了新版本才有的语法或者 RTL 单元。解决别硬改核心单元先看Docs里有没有版本兼容说明通常会用条件编译指令{$IF CompilerVersion 36}之类做隔离把不支持的实现分支注释掉留接口桩。真要动动Device层别动Core。5.2 运行期现场问题现象四实验室正常现场丢包日志里OnReceive触发次数明显少于实际发送次数。原因一般有两个。一是缓冲区太小加上回调里做了耗时操作读线程被阻塞导致驱动缓冲溢出这种丢包在应用层完全看不到。二是波特率实际不匹配——现场设备拨码开关设的是 19200软件按 9600 开收到的全是乱码解析器一直找不到帧头表现也是“没数据”。解决先把OnReceive简化成纯计数加日志确认触发频率再看原始十六进制如果有规律的0x00或者0xFF混杂八成是波特率或数据位错了最后调大缓冲、把解析挪出回调。现象五程序退出时报“句柄无效”或者任务管理器里进程一直不消失。原因是在OnClose或者FormDestroy里只做了FPort : nil没先Close。控件内部有工作线程直接释放对象会让线程访问已释放内存。解决析构顺序必须是——先解绑事件OnReceive : nil再Active : False等线程退出最后Free。如果是Owner自动管理的Create(Self)在FormDestroy里显式Close一次就够。这个坑非常隐蔽因为开发期反复启动进程操作系统会帮你兜底到了现场长时间运行才暴露。现象六界面上的数据“跳帧”设备明明发了 10 条界面只显示 7 条。先别怀疑通信库。检查一下MemoLog是不是被设了MaxLength或者Lines.BeginUpdate之后忘了EndUpdate还有一种情况是用了TThread.Queue但窗体已经释放排队的匿名方法被丢弃。解决日志控件设个上限比如 5000 行循环覆盖别无限追加窗体销毁前把队列里剩下的处理完或者干脆在关闭时置一个FClosing标志回调里判断后直接返回。注意排查通信问题永远从“原始字节”开始不要在解析之后的业务层猜。先用串口调试助手或者一份不解析的十六进制日志确认字节是对的再往下走。6. 二次开发把私有协议挂进事件链以及改完怎么自证没改坏源码在手最常见的改动就是把自家的私有协议接进去。我的建议是继承协议层的基类不要改基类本身。基类负责缓冲管理和帧同步这部分是最容易改出问题的派生类只实现“怎么判断一个帧结束了”和“校验怎么算”两个方法改动面小将来换新版本源码也容易合并。设备层的编解码再单独放一个单元跟协议层解耦测试的时候可以脱离串口单独跑。判断帧结束这条别写死在派生类里。有些协议是帧头 长度有些是固定帧长有些是帧尾\r\n。我的做法是在派生类里暴露一个FramingMode枚举Feed里面按模式分支切换协议改一行配置。这样一份代码能同时对接三四种设备省掉复制粘贴。改完怎么验证我固定走两步。第一步是回环验证用两个虚拟串口配对或者一台设备自发自收把一段带噪声的样本数据帧头前插垃圾字节、半帧拆成两次发、故意改坏一个 CRC 字节喂进解析器看输出帧数和内容对不对得上。这一步能把前面说的粘包、半包、重同步问题全暴露出来而且不用等现场。第二步是抓包比对现场录一段真实的原始字节流存成文件回放给解析器输出和你用脚本写的参考实现逐字节比对不一致就 diff。这两步跑完心里就有底了。顺手提一个技巧给解析器加个统计属性记录收到的字节数、成功帧数、CRC 错误数、长度非法数、丢弃的噪声字节数。现场出问题时让运维把这个值报过来比看日志快十倍——CRC 错误多说明线路干扰或波特率不对长度非法多说明帧头识别有误噪声字节多说明设备在发别的东西。这几个数字一看方向基本就定了。从那以后我每次动协议解析不管改的是一行还是一个分支都强制走一遍“回环 抓包比对”哪怕再赶时间也不跳。省下来的那十分钟往往要用一个通宵在现场补回来。这套源码的完整包我放在下载区了按标题找就行先跑Demos里的例子再动自己的协议顺序别反。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询