
简介这是一份基于C Builder平台的两机串行通信示例工程面向具备基础C语法、希望上手串口编程的开发者解决如何通过TSerialPort组件完成端口配置、数据读写与错误处理等核心问题也适合课程设计或小型工控通信场景参考。压缩包共33个文件约1.71MB包含cpp/h源程序、dfm窗体设计、bpr工程文件、exe可执行程序以及obj、tds等编译中间文件同时附有txt用户说明和备份文件便于对照学习与二次修改。代码中演示了波特率、数据位、停止位和校验位等参数的设置方法以异步收发思路处理串口读写配合可视化按钮和文本框实现启动、停止与数据展示界面友好操作直观通过阅读核心模块还可理解事件驱动收发与主线程配合的机制。已有125人学习该资源适合想通过完整工程实例快速掌握C Builder串口通信开发要点理解两机通信机制及其异常处理流程的读者参考。1. translate.rar一套能直接编译运行的 C Builder 串口通讯程序手里拿一台老工控机、一根九针串口线、一套 C Builder 写的通讯程序源码包——这是很多做设备联调的开发者会遇到的需求。translate.rar 里就是这样一个组合完整的工程文件、窗体源码、一个独立接收线程、编译好的可执行程序和一份用户说明书目标就是解决两台机器之间通过串口交换数据这件事。C Builder 在这里的优势很明显可视化拖控件搭界面底层还能直接操作串口。这份资源适合两类人一类是要在 PC 与单片机、PC 与 PC 之间快速搭一个带图形界面的串口通讯工具另一类是想看 C Builder 工程里多线程 控件 串口三者怎么协作的开发者。我拆解过这份资源中间有几个坑值得提前说清楚。2. 串行通信的参数不是玄学从工程文件反推程序结构2.1 串口通讯的核心参数波特率、数据位、停止位、校验位串行通信的本质是逐位传输两端必须约好“一拍有多快、一字节几位、怎么对齐、怎么检错”这四个参数就是波特率、数据位、停止位、校验位。C Builder 的 TSerialPort 组件把这些参数直接暴露成属性配置错误时设备不会主动报错只是数据全乱。参数的常用搭配参考这个表参数值典型场景波特率9600 / 19200 / 115200工控设备默认 9600高速传输选 115200数据位8传输字节数据默认值停止位1一字节结束标记默认 1校验位无 / 偶校验 / 奇校验短距离噪声小可以无校验远距离建议偶校验实际调试时优先确认波特率。双方波特率不一致时界面上的现象是“收到一堆乱码但连接正常”字符能显示出来但内容不对这种问题用命令设置参数是查不出来的。C Builder 里配置这些属性就是一行接一行赋值代码很简单真正要在工程层面注意的是是谁在什么时机打开串口、以什么模式接收数据。这些答案在 translate.rar 的源码里都有对应位置。2.2 从文件清单反推代码结构Comm、RecThread、CommTest 三个模块拿到压缩包先不急着解压编译先看文件名字符串能看出这个工程的大致分层。translate.rar 里有一组以 Comm 开头的文件Comm.cpp、Comm.h、Comm.dfm、Comm.bpr这是主窗体和主工程RecThread.cpp、RecThread.h 是独立线程文件CommTest.cpp 是测试入口程序.tds、.~cpp、.~dfm 这类是 IDE 自动备份和调试符号文件编译时用不到。文件作用编译时是否必需Comm.bpr / Comm.cpp / Comm.h / Comm.dfm工程文件、主窗体实现、头文件、窗体资源必需RecThread.cpp / RecThread.h独立接收线程实现必需CommTest.cpp / CommTest.res测试模块与资源非必需可删除.tds / .~cpp / .~dfm调试符号与自动备份不需要从 RecThread 这个文件的存在就能判断这个程序没有把串口接收放在主线程里轮询而是专门开了一个线程。如果接收逻辑放在主界面线程里读串口时如果设备迟迟不返回界面会直接卡住这属于串口编程里最典型的设计问题。RecThread 的出现说明该工程采用异步接收思路主线程只负责界面响应数据到达由线程通知。2.3 窗体文件 .dfm 里藏着界面逻辑按钮、配置项与显示区Comm.dfm 是 VCL 窗体的二进制描述文件C Builder 编译时把它和源码绑定生成窗口。拆这个文件能看出界面构成串口选择下拉框ComboBox、波特率下拉框、数据位和停止位选项打开/关闭按钮发送按钮一块 Memo 用于显示接收内容一个 Edit 用于输入要发送的文本可能还有一个 CheckBox 用于勾选十六进制显示。界面上最常见的分工是串口参数控件负责配置打开按钮触发串口初始化Memo 接收区由线程代码写入发送区独立。如果设备协议需要发送十六进制字节比如 0x01 0x03 0x00界面上通常会有个十六进制转换开关这个逻辑写在发送按钮的 OnClick 事件里。对这种窗体型工程推荐的做法是先用 component 界面直接看控件属性再回到代码里看每个事件函数事件函数名字有 OnCreate、OnOpenClick、OnSendClick代码定位速度比通读所有源文件快得多。3. 把代码跑起来串口初始化的完整实现与发送接收拆解3.1 初始化串口从创建组件到四个关键属性赋值在 C Builder 里用 TSerialPort 组件实现串口初始化核心逻辑在 FormCreate 或打开按钮事件里。一般写法是先在窗体上放置组件然后在代码里对其赋值。常见的初始化代码如下// 窗体创建事件默认配置 COM1 和 9600 void __fastcall TCommForm::FormCreate(TObject *Sender) { // 端口号界面下拉框默认选中第一项 SerialPort1-Port COM1; // 波特率9600 是工控设备最常用的速率 SerialPort1-BaudRate 9600; // 数据位8 位传输字节数据 SerialPort1-DataBits 8; // 停止位1 位 SerialPort1-StopBits 1; // 校验位无校验短距离传输噪声可接受 SerialPort1-Parity 0; // 串口是否已打开标志界面按钮状态由此控制 FCommOpen false; }这段代码的逻辑是把组件的属性参数全部显式赋值避免 IDE 环境差异导致默认值不一致。参数说明Port 属性支持 COM1 这种字符串写法如果电脑只有 USB 转串口线系统会把设备映射成 COM3 或更高的编号这个值通常运行时再动态设置。打开串口的动作和初始化赋值要分开写常见做法是放一个打开/关闭按钮// 打开/关闭按钮点击事件 void __fastcall TCommForm::OpenBtnClick(TObject *Sender) { if (!FCommOpen) { // 把界面上选中的参数同步到组件 SerialPort1-Port PortCombo-Text; SerialPort1-BaudRate StrToInt(BaudCombo-Text); try { // 打开串口失败会抛出异常 SerialPort1-Open(); FCommOpen true; StatusLabel-Caption 串口已打开; } catch (...) { // 常见原因端口被占用或端口不存在 ShowMessage(打开串口失败请检查端口号和占用情况); FCommOpen false; } } else { SerialPort1-Close(); FCommOpen false; StatusLabel-Caption 串口已关闭; } }注意这段代码里的打开和关闭共用同一个按钮点击时先判断当前状态已关闭则打开已打开则关闭用一个布尔变量 FCommOpen 记录状态。我在实际项目里习惯把打开动作单独抽一个函数按钮点击直接调用逻辑更清楚。3.2 发送与接收WriteBuffer、ReadBuffer 的调用时机和边界发送数据是主动行为用户在界面输入框填入内容、点击发送按钮后触发。串口发送操作本身是同步的耗时很短可以直接在主线程里调用。常见的发送代码如下// 发送按钮点击事件 void __fastcall TCommForm::SendBtnClick(TObject *Sender) { // 未打开串口时禁止发送 if (!FCommOpen) { ShowMessage(请先打开串口); return; } // 从输入框取得字符串 AnsiString cmd SendEdit-Text; if (cmd.Length() 0) { return; } // 直接发送字符串含 \0 的二进制数据用 WriteBuffer SerialPort1-WriteString(cmd); }要点在最后一行。WriteString 适合发送普通文本WriteBuffer 适合发送二进制协议帧。设备协议如果是 0xAA 0x55 0x01 这种十六进制字节流必须用 WriteBuffer并且要将字符串先转换为 TBytes 数组。很多开发者直接用 WriteString 发十六进制场景的数据设备端收到的却是 ASCII 文本这是常见的错误用法。接收是串口编程中比发送更需要小心的环节。数据何时到达是时序不确定的所以 translate.rar 专门用 RecThread 线程去读。如果只接收少量数据也可以在组件接收事件里同步处理但阻塞式读不能在主线程执行。线程模式下的核心思路是循环检查串口缓冲区有数据再读取。代码框架如下// 接收线程的执行函数循环读串口缓冲区 void __fastcall TRecThread::Execute() { // 临时缓冲区接收串口数据 char buf[1024]; while (!Terminated) { // 检查缓冲区里是否有数据到达 if (SerialPort-BytesReceived 0) { // 读取数据返回实际读到的字节数 int len SerialPort-ReadBuffer(buf, sizeof(buf)); // 调用线程安全函数更新界面 Synchronize(HandleData); } // 避免空转占用 CPU Sleep(10); } }逻辑说明线程启动后进入死循环每次检查串口缓冲区是否有数据有数据则读出一段Sleep(10) 的作用是让出 CPU 时间片避免高频空转。关键参数说明ReadBuffer 的第二个参数是读取上限返回的是实际读取长度这个返回值决定了后面处理的字节数。Synchronize 在这里非常重要因为 VCL 界面控件不是线程安全的直接在工作线程里往 Memo 加文本行为未定义必须通过 Synchronize 切回主线程做界面更新。ReceivingThread 的终止条件靠 Terminated 标志位窗体关闭时调用线程的 Terminate 方法线程安全退出后再释放。3.3 RecThread 为什么值得关注异步接收与主界面互不阻塞展开讲为什么要把接收放到 RecThread 里这是我第二次强调这个点了因为见过太多串口程序卡死在界面上。串口数据到达时间不可预测如果主线程执行 ReadBuffer 去读设备一直不发数据程序就停在读操作处界面按钮全部点不动。用户一看“程序死了”其实是读阻塞。RecThread.cpp 里保存的就是这套异步接收的思想主线程负责界面操作后台线程持续轮询串口缓冲区。线程获得数据后通过 Synchronize 通知主线程刷新界面这是一个标准的线程安全模式。耗时场景一设备响应慢比如查询指令发出后 3 秒才返回数据主线程模式会在读函数上卡死 3 秒界面无响应线程模式主界面始终可用。耗时场景二数据是连续高频流入的比如每秒 200 帧传感器数据线程模式可以把界面刷新合并成批量操作避免每帧都刷新导致闪烁。所以这个工程文件不是随便写的Comm.cpp 管理界面和配置RecThread.cpp 管理数据接收双层结构清晰这个设计思路值得直接迁移到自己的项目里。4. 串口通讯常见问题与避坑现象、原因、解决4.1 发送正常但接收全是乱码现象程序能打开串口数据也显示出来了但接收区显示的字符完全不对重复出现类似 “ÿ” 或方块符号。原因串口两端波特率不一致。比如发送端设置 9600接收端用 19200数据位和停止位即使相同也是乱码这是串口通讯最典型的错配。解决先确认设备端手册写的是多少再用串口调试助手逐一试 9600、19200、115200 三档直到内容可读。C Builder 界面里的波特率下拉框要确保真正赋值进了 SerialPort1-BaudRate很多人改了界面下拉框但没给属性赋值等于没改。4.2 两边参数都对就是收不到数据现象发送端提示发送成功接收端界面无任何反应也没有报错信息。原因除了交叉接线问题外还有一种隐蔽情况串口线只有三线2、3、5但程序里把硬件流控当成自动默认开启RTS/CTS 信号无人应答数据卡在中间。解决检查两端接线是否为 TX 接 RX、RX 接 TX、GND 接 GND同时把 TSerialPort 的流控属性改成无流控通常用 SetFlowControl 设置为 None再重新测试。我调试这种问题时会先切到软件流控或直接无流控能立竿见影。4.3 命令收发正常但一台机器收到错误数据现象发送端发整数或二进制文件时接收端数据在中间某个位置出现字节错位或丢失。原因串口本身没有“消息边界”的概念它是一个字节流管道。发送端一次发 50 字节接收端可能分 3 次读到每次读到的字节数都不确定。如果程序里默认“一次读到的就是一帧完整数据”就会出现数据拆分错位。解决在接收代码中引入缓冲区和帧协议用帧头、帧尾或固定长度判断数据完整性。具体做法是每次 ReadBuffer 读到的数据先存入累积缓冲区然后按协议判定是否凑齐一帧。不要试图让串口组件保证一次读完整包这不现实。4.4 运行时串口被占用打开失败或程序启动即异常退出现象程序启动后点击打开按钮提示打开失败或者开机自启动时程序直接异常退出界面都不出现。原因串口被其他程序占用常见的有串口调试工具没有关闭、设备管理器还持有该端口、程序上一次退出时没有调用 Close。也有一种情况是虚拟串口软件占用了物理端口。解决程序启动前用系统的设备管理器确认端口是否空闲代码里必须保证程序退出时调用 SerialPort1-Close()并且要把关闭动作放在窗体销毁事件中。另外打开失败时不要直接忽略异常弹窗提示用户检查占用这是最务实的人机交互。4.5 关闭窗口时崩溃卡死现象程序正常使用但关闭窗口时偶尔闪退或提示内存访问错误。原因接收线程还挂在串口上窗体销毁顺序比线程结束早界面释放后被线程访问导致访问非法地址。解决在窗体的 FormCloseQuery 事件里先停止线程再关串口。正规顺序是执行线程 Terminate等待线程真正退出关闭串口最后释放窗体。translate.rar 的 RecThread 用 FreeOnTerminate 属性这个属性能让线程结束后自动释放自己但前提是正确设置了 Terminate 时机否则会造成双击关闭窗口时资源释放竞争。5. 最后的验证技巧串口回环测试与协议层扩展思路建议拿到 translate.rar 编译出来的 Comm.exe 后第一步先做一个回环测试用一个短接线把串口的第 2 脚RXD和第 3 脚TXD短接。这时候在程序里发什么自己就能收到什么不需要第二台设备。如果手头只有 USB 转串口线直接对接线的 2、3 针短接GND 不用接也能跑通。这个测试能验证三件事串口配置是否正确、发送链路是否工作、接收线程是否有数据上来。上述三条全通过就说明程序基本正常接下来才轮到接真实设备。回环测试里大概率会遇到 4.1 和 4.2 的坑乱码几乎一定是波特率设置问题无数据显示了短接后还需要拔插几次确认接触。我一般会把波特率固定一个值然后从 1200 到 115200 全部过一遍看哪个速率下回环数据最干净。进一步升级方案是加一层简单协议在现有收发代码上包装一个帧格式两字节帧头0xAA 0x55加一字节长度加数据区加一字节校验或者单字节累加校验。原因很简单串口是字节流没有任何消息边界只要发送方和接收方约定好帧格式就能彻底解决 4.3 提到的问题。接收线程只负责把原始字节推进缓冲区协议解析函数从缓冲区里寻找到帧头判断长度字段再根据校验结果决定接受或丢弃。用虚拟串口对模拟两机通讯也是个不错的手段。有些工具可以为成对创建虚拟串口两边各看到一个 COM 口这样就能在同一台机器上启动两个 Comm.exe 实例互发数据测试收发和线程逻辑完全不用硬件设备。这适合在开发环境里做无硬件测试等程序逻辑稳定了再上真设备联调。从那以后我每次拿到任何串口通讯源码包第一件事永远是找顶点线短接做一次回环测试而不是直接连设备调协议。回环过了硬件链路基本没问题才谈得上排代码逻辑。这个习惯帮我省掉了大量的联调返工希望也能帮到你。本文还有配套的精品资源点击获取