资讯动态

Windows C++串口通信全解析:从API调用到环形缓冲区避坑指南

发布时间:2026/10/13 8:26:18 来源:尧图企业网站定制
简介面向Windows平台C开发者的串口通信全代码资源围绕RS-232等常用串行通信标准覆盖串口初始化、参数配置、全双工数据收发及界面交互等关键环节适合需要快速构建串口工具或学习串口机制的工程师、学生及嵌入式开发者使用。资源包含一个完整的串口调试助手工程基于对话框应用程序结构代码组织清晰涵盖串口打开/关闭、波特率与校验位设置、收发数据展示等常见功能可在Visual Studio环境下直接打开编译。压缩包共51个文件约22.09MB主要文件类型有C头文件与源文件.h/.cpp、Visual Studio工程配置.dsw/.vcxproj、动态/静态链接库.dll/.lib以及可直接运行的exe和调试中间产物工程信息齐全兼顾源码学习和运行验证。该代码包已有5262人学习对于串口通信课程设计、上位机开发入门或现有工具改造而言既是可运行成品也是值得对照学习的参考资料。1. 串口通信windows C全代码别急着抄先弄懂串口这套机制串口通信windows C全代码听起来像是一份能直接编译运行的工程但实际拆过之后你会发现真正的坑不在 API 本身而在你对串口机制的认知。我处理过一个模拟项目前期把 CreateFile、SetCommState 都写对了结果读写还是丢数据最后发现是超时参数没配。这篇文章把整套串口代码拆开讲先定参数再走 API 调用链然后给出一份能跑的封装类代码最后是五个踩坑现场和一段进阶技巧。适合做嵌入式上位机、工控设备通信的新手和熟手照着复现就能落地。2. 串口参数与 API 选型为什么 CreateFile 是一切的基础Windows 下的串口通信从本质上看是把 COM 口当作一个设备对象来操作。你拿到的是一个 HANDLE后续所有的读写、配置、清理都必须围绕这个 HANDLE 展开而这个 HANDLE 来自 CreateFile。很多人一上来就搜 SetCommState 怎么配置却忽略了 CreateFile 的打开方式会直接影响后面的超时和异步行为所以这一章先把地基打牢。2.1 串口参数波特率、数据位、停止位、校验位在调用任何 API 之前先把串口的物理层参数定下来。这些参数决定了一帧数据长什么样收端和发端必须完全一致否则就会出现乱码或者偶发丢帧。参数不匹配是串口通信最隐蔽的问题因为在某些波特率下数据看起来能通但内容全错。参数常见值适用场景说明波特率9600低速传感器、GPS 模块抗干扰强但对长帧数据吞吐不利波特率115200高速采集、上位机交互常用但线缆质量不佳时容易丢帧数据位8大多数二进制帧一个字节 8 位做 16 进制解析最方便数据位7ASCII 字符通信老式设备常用传中文会有问题停止位1默认配置一帧结束标志1 位足够停止位2对时序要求高的场合很少用校验位NOPARITY无校验最常见校验位PARITY_ODD奇校验对噪声敏感的场合校验位PARITY_EVEN偶校验同上选择参数时我一般先问对接设备的硬件手册。如果手册写的是 9600, 8, N, 1那就不要自己猜测。8N1 是最常见的组合绝大多数 MCU 和传感器都支持。如果遇到乱码第一步不是怀疑代码而是用串口调试工具把收发的参数核对一遍。2.2 Windows 串口核心 API 调用链Windows 串口编程绕不开六个 APICreateFile、GetCommState、SetCommState、SetupComm、SetCommTimeouts、ReadFile/WriteFile最后用 CloseHandle 收尾。调用顺序是固定的我从先到后说明。第一步是打开串口。CreateFile 的参数值得仔细看尤其是 dwShareMode 必须是 0串口不支持共享。打开方式选择 FILE_FLAG_OVERLAPPED让它支持异步操作这样后续不会因为一次读取阻塞整个线程。// 打开串口用重叠 I/O 方式打开不阻塞调用线程 HANDLE hCom CreateFileA( COM3, GENERIC_READ | GENERIC_WRITE, 0, // 串口必须独占不能共享 NULL, OPEN_EXISTING, // 只打开已经存在的端口 FILE_FLAG_OVERLAPPED, NULL); if (hCom INVALID_HANDLE_VALUE) { // 常见失败原因端口号不存在、端口被占用、权限不足 return false; }这里插几句CreateFileA 用的是 ANSI 字符串如果你的工程是 Unicode 编码最好用 CreateFileW 并传入宽字符端口名或者在字符串前加 L。端口名写法是 COM3 而不是 \.\COM3只有 COM10 以上的端口才需要加设备路径前缀这是个经典易错点。打开成功后接下来要获取当前的设备控制块DCB并修改参数。GetCommState 把当前配置填充到 DCB 结构体中然后改字段最后 SetCommState 写回。很多人图省事直接 memset 清零 DCB那会丢掉该保留的默认字段导致配置后行为异常。DCB dcb; memset(dcb, 0, sizeof(dcb)); dcb.DCBlength sizeof(dcb); if (!GetCommState(hCom, dcb)) { // 获取失败通常是句柄无效或被占用 return false; } // 按手册配参数115200, 8, N, 1 dcb.BaudRate CBR_115200; // 波特率 dcb.ByteSize 8; // 数据位 dcb.Parity NOPARITY; // 无校验 dcb.StopBits ONESTOPBIT; // 一位停止位 // 关键关闭硬件流控和软件流控很多设备不支持 dcb.fOutxCtsFlow FALSE; dcb.fOutxDsrFlow FALSE; dcb.fOutX FALSE; dcb.fInX FALSE; dcb.fRtsControl RTS_CONTROL_DISABLE; dcb.fDtrControl DTR_CONTROL_DISABLE; if (!SetCommState(hCom, dcb)) { return false; }代码里的 fOutX、fInX 控制 XON/XOFF 软件流控fOutxCtsFlow 控制 CTS 硬件流控。如果你不确定对端设备是否实现了握手协议我建议先把这几项全部关掉等链路调通之后再按需开启这是血泪经验。很多设备手册写着支持流控实际固件并没有完整实现开着流控反而把收发卡住了。配完 DCB 之后还需要设置收发缓冲区大小和超时。这一步经常被跳过但缓冲区配太小会导致数据溢出丢失超时配太大会让读取线程长时间阻塞。SetupComm 用来设置缓冲区SetCommTimeouts 用来设置超时策略我会在第四章细讲这里先给出最小配置。// 设置接收和发送缓冲区单位是字节 SetupComm(hCom, 4096, 4096); // 配置超时结构体必须初始化避免随机值 COMMTIMEOUTS timeouts {0}; timeouts.ReadIntervalTimeout 50; timeouts.ReadTotalTimeoutMultiplier 10; timeouts.ReadTotalTimeoutConstant 100; SetCommTimeouts(hCom, timeouts);到这里串口已经完成了打开、参数配置和超时设置可以开始读写。后面所有操作都复用这个 HANDLE。也就是说CreateFile 是整个过程的地基任何一步配置失误都表现为读写阶段的神奇故障这在第五章会有具体的踩坑记录。3. 一个可用的串口类封装从打开到收发数据的完整代码这一章给出一套可以在工程里直接用的 SerialPort 封装类完整的 SerialPort.cpp、SerialPort.h 和一个串口回环测试 Demo 都整理在下载包里。下面把核心部分拆开讲。3.1 打开串口与初始化先把头文件写出来对外只暴露 Open、Read、Write、Close 四个方法。内部持有两个 OVERLAPPED 结构体分别服务读和写这样读线程和写线程各自有独立的事件对象不会互相干扰。// SerialPort.h #pragma once #include windows.h #include string class SerialPort { public: SerialPort(); ~SerialPort(); // 打开串口并配置参数 bool Open(const std::string portName, unsigned long baudRate, unsigned char byteSize 8, unsigned char stopBits ONESTOPBIT, unsigned char parity NOPARITY); // 读取数据按实际返回字节数取值 int Read(unsigned char* buffer, int bufferSize); // 发送数据返回实际写入字节数 int Write(const unsigned char* buffer, int length); void Close(); private: HANDLE handle_; OVERLAPPED readOverlapped_; OVERLAPPED writeOverlapped_; bool isOpen_; };Open 方法内部依次执行 CreateFile、GetCommState、SetCommState、SetupComm、SetCommTimeouts。每一步失败都要回滚避免留下半开状态。// SerialPort.cpp #include SerialPort.h #include cstring SerialPort::SerialPort() : handle_(INVALID_HANDLE_VALUE), isOpen_(false) { memset(readOverlapped_, 0, sizeof(readOverlapped_)); memset(writeOverlapped_, 0, sizeof(writeOverlapped_)); readOverlapped_.hEvent CreateEvent(NULL, TRUE, FALSE, NULL); writeOverlapped_.hEvent CreateEvent(NULL, TRUE, FALSE, NULL); } SerialPort::~SerialPort() { Close(); if (readOverlapped_.hEvent) CloseHandle(readOverlapped_.hEvent); if (writeOverlapped_.hEvent) CloseHandle(writeOverlapped_.hEvent); } bool SerialPort::Open(const std::string portName, unsigned long baudRate, unsigned char byteSize, unsigned char stopBits, unsigned char parity) { handle_ CreateFileA(portName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); if (handle_ INVALID_HANDLE_VALUE) return false; DCB dcb; memset(dcb, 0, sizeof(dcb)); dcb.DCBlength sizeof(dcb); if (!GetCommState(handle_, dcb)) { Close(); return false; } dcb.BaudRate baudRate; dcb.ByteSize byteSize; dcb.StopBits stopBits; dcb.Parity parity; dcb.fOutxCtsFlow FALSE; dcb.fOutxDsrFlow FALSE; dcb.fOutX FALSE; dcb.fInX FALSE; dcb.fRtsControl RTS_CONTROL_DISABLE; dcb.fDtrControl DTR_CONTROL_DISABLE; if (!SetCommState(handle_, dcb)) { Close(); return false; } SetupComm(handle_, 4096, 4096); COMMTIMEOUTS timeouts; memset(timeouts, 0, sizeof(timeouts)); timeouts.ReadIntervalTimeout 50; timeouts.ReadTotalTimeoutMultiplier 10; timeouts.ReadTotalTimeoutConstant 100; SetCommTimeouts(handle_, timeouts); isOpen_ true; return true; }构造函数里 CreateEvent 的第二个参数为 TRUE表示手动复位事件。这是为了配合 WaitForSingleObject 等待读事件时不会被自动重置避免漏掉一次完成通知。SetupComm 设置的 4096 字节缓冲区对大多数工控报文是够用的如果你的单帧数据超过 2KB建议改成 8192 或者 16384要根据实际帧长来定。3.2 同步读写的实现读写是这个封装类的核心。这里用同步等待方式实现读线程里调用 Read 时最多阻塞到超时时间不会无限等下去。int SerialPort::Read(unsigned char* buffer, int bufferSize) { if (!isOpen_ || buffer NULL || bufferSize 0) return 0; unsigned long bytesRead 0; BOOL result ReadFile(handle_, buffer, bufferSize, bytesRead, readOverlapped_); if (!result) { DWORD error GetLastError(); if (error ERROR_IO_PENDING) { // 数据尚未到达等待完成事件最多 2 秒 DWORD wait WaitForSingleObject(readOverlapped_.hEvent, 2000); if (wait WAIT_OBJECT_0) { GetOverlappedResult(handle_, readOverlapped_, bytesRead, FALSE); } else { // 超时或出错取消本次请求后直接返回 0 CancelIo(handle_); return 0; } } else { // 其他错误比如串口被拔掉 return -1; } } return (int)bytesRead; }Read 的流程是先发一次 ReadFile数据立刻到达就同步返回没到的话Windows 返回 ERROR_IO_PENDING此时线程等待事件最多 2 秒。等待时间要和 COMMTIMEOUTS 里的总超时配合不能让 WaitForSingleObject 的等待时间远大于数据到达周期否则读线程会频繁空转。Write 的实现逻辑类似但对超时的容忍度更低因为写操作更怕长时间占用句柄。int SerialPort::Write(const unsigned char* buffer, int length) { if (!isOpen_ || buffer NULL || length 0) return 0; unsigned long bytesWritten 0; BOOL result WriteFile(handle_, buffer, length, bytesWritten, writeOverlapped_); if (!result) { DWORD error GetLastError(); if (error ERROR_IO_PENDING) { DWORD wait WaitForSingleObject(writeOverlapped_.hEvent, 2000); if (wait WAIT_OBJECT_0) { GetOverlappedResult(handle_, writeOverlapped_, bytesWritten, FALSE); } else { CancelIo(handle_); return 0; } } else { return -1; } } return (int)bytesWritten; }Write 里我给的是 2 秒等待。如果对端设备回传 CTS 信号有问题WriteFile 会一直处于 pending 状态2 秒后 CancelIo 会强制取消这样至少不会让线程卡死。实际项目中发一帧的时间如果已经远超 2 秒那就得考虑增大等待时间或者拆帧发送。3.3 资源释放与线程安全Close 方法负责收尾。这里有一个细节先 CancelIo 清理所有挂起的 I/O 请求再关闭句柄顺序反了会导致程序在退出时崩溃。void SerialPort::Close() { if (handle_ ! INVALID_HANDLE_VALUE) { // 先取消所有挂起的 I/O 操作再关句柄 CancelIo(handle_); CloseHandle(handle_); handle_ INVALID_HANDLE_VALUE; isOpen_ false; } }CancelIo 是关闭前必调的一步它让所有 pending 的 ReadFile/WriteFile 请求立即返回。如果不调那些阻塞在 WaitForSingleObject 上的线程会继续等待即使句柄已经关闭也会造成访问已释放句柄的崩溃。我建议把串口的生命周期做成强一致打开、配置、使用、清理全部严格按序执行不要跨线程释放资源。在实际多线程环境中读线程和写线程通常是分开的。读线程持续调用 Read写线程按业务触发 Write。此时要注意两点第一两个 OVERLAPPED 结构体必须分开不能共用一个否则两个线程的完成事件会相互干扰第二关闭串口时不要让别的线程正在阻塞 Read否则 CancelIo 之后那些线程会返回错误码如果你把返回 -1 当成致命错误处理就会触发异常。我的习惯是在 Close 之前先设置一个 isOpen_ 标志让读线程在下一次循环判断退出再调 Close。4. 缓冲区、超时与流量控制参数怎么设才不会丢数据串口调不通的案例里有一大半问题不在代码逻辑而在参数设置。这些参数包括 COMMTIMEOUTS 结构体的几个字段、收发缓冲区大小以及流控模式。这一章逐个拆开讲并给出可复用的推荐值。4.1 COMMTIMEOUTS 超时参数详解COMMTIMEOUTS 是 Windows 串口通信的精髓所在。它有 5 个字段其中和读取相关的有 3 个。很多人在这几个字段上犯迷糊我先把结构体说明列出来。字段作用推荐值ReadIntervalTimeout两个字符到达的最大间隔单位毫秒0 表示不使用50ReadTotalTimeoutMultiplier读取每个字符计时的系数乘以请求字节数10ReadTotalTimeoutConstant每次读取操作允许的固定超时100WriteTotalTimeoutMultiplier写操作的乘法超时10WriteTotalTimeoutConstant写操作的固定超时100举一个例子解释这三个读超时的关系Read 请求读 64 字节ReadTotalTimeoutMultiplier 是 10ReadTotalTimeoutConstant 是 100那么总超时就是 64 × 10 100 740 毫秒。ReadIntervalTimeout 独立生效如果两个字节之间间隔超过 50 毫秒系统认为一帧结束。有个常见场景设备每 100 毫秒发一帧 20 字节的数据。如果把 ReadIntervalTimeout 设得太大Read 会把两帧当作一次返回导致数据堆积设得太小帧内字节间隔稍大就会被截断。我一般先按设备的帧间隔设置 ReadIntervalTimeout取帧间隔的三分之一到二分之一。// 常见设备的推荐配置 COMMTIMEOUTS timeouts {0}; timeouts.ReadIntervalTimeout 50; // 帧内字节间隔阈值 timeouts.ReadTotalTimeoutMultiplier 10; // 每字节超时系数 timeouts.ReadTotalTimeoutConstant 100; // 固定超时 SetCommTimeouts(hCom, timeouts);注意ReadTotalTimeoutMultiplier 和 ReadTotalTimeoutConstant 有一个特殊的 0 值陷阱。两者都为 0 时表示不使用总超时Read 会一直等下去直到读到请求的字节数。这在同步模式下会让线程无限阻塞千万要避免。4.2 缓冲区大小与流控选择缓冲区由 SetupComm 设置。默认情况下系统分配的大小可能只有 1KB 左右如果你接收的报文长度接近这个值读取速度跟不上时就会丢数据。SetupComm 的第二个和第三个参数分别设置接收和发送缓冲区单位是字节。// 按最大帧长 1KB、双缓冲冗余设置 4KB SetupComm(hCom, 4096, 4096);配置建议接收缓冲区大小至少是最大单帧长度的 4 倍。如果对端设备会连续发送多帧缓冲区还要按 burst 的总长度来估算。比如设备一次连续输出 50 帧、每帧 64 字节那你至少需要 4KB 缓冲才不至于在上层处理不及时的时候丢数据。流控的选择比较微妙。软件流控 XON/XOFF 在依赖串口线传输的场合还算可靠但要考虑一个问题XOFF 字符可能正好出现在二进制数据流里面造成误触发。所以发自定义协议二进制帧时我强烈建议不要用软件流控除非你的帧做了字节填充。硬件流控 RTS/CTS 相对可靠但它要求对端设备真的实现了 CTS 信号控制。有的设备手册写着支持实际只是固定拉高 CTS此时你用 RTS_CONTROL_HANDSHAKE 反而会卡住发送。我第一次做设备对接时就在这上面翻车后来全部改成 RTS_CONTROL_DISABLE反而稳定了。// 关闭所有流控最保守的配置 dcb.fOutxCtsFlow FALSE; dcb.fOutxDsrFlow FALSE; dcb.fOutX FALSE; dcb.fInX FALSE; dcb.fRtsControl RTS_CONTROL_DISABLE; dcb.fDtrControl DTR_CONTROL_DISABLE;如果你确认对端硬件实现了流控再按下面方式开启 CTS 硬件流控// 开启 CTS 流控发送前等待对端 CTS 有效 dcb.fOutxCtsFlow TRUE; dcb.fRtsControl RTS_CONTROL_HANDSHAKE;开启之后WriteFile 会一直等到 CTS 信号有效才把数据发出去。如果线缆没接 CTS 信号写操作就会呈现出卡住状态这正好对应第五章的其中一个坑。还有一个容易忽略的点Write 字段的超时设置。默认情况下很多人会把 WriteTotalTimeoutMultiplier 和 WriteTotalTimeoutConstant 也设成和 Read 一样。写超时的意义在于当对端没有及时接收、串口缓冲区满时WriteFile 不会一直阻塞。但它也有副作用如果设备每帧写入时间本来就接近超时上限数据会被意外截断。我一般把 Write 的固定超时设得比 Read 大一倍因为写操作通常要等待硬件就绪不能太苛刻。关于缓冲区溢出Windows 的串口接收缓冲是内核级的应用层 Read 速度跟不上时数据会先存放在内核缓冲。SetupComm 设置的大小决定内核缓冲容量如果你的应用解析慢数据就会在内核缓冲溢出并被丢弃。此时即使你在应用层用大缓冲区也救不回来。解决思路是提高读取线程优先级或者增大内核缓冲这也是我反复提醒把读取和组帧分开线程的原因。5. 串口通信常见问题与避坑记录五个翻车现场这一章全是实打实的踩坑记录。我把拆项目过程中遇到的、以及很多从业者反复栽跟头的五个问题记录下来每个都按现象、原因、解决三步写成方便你直接对照排查。5.1 打开 COM 口失败文件被占用或设备路径不对现象CreateFile 返回 INVALID_HANDLE_VALUEGetLastError 返回 5拒绝访问或 2找不到文件。原因最常见的是串口被其他软件占用。Windows 的串口驱动不支持多个进程同时打开同一个 COM 口无论你的进程是否真的在读写只要有一个进程打开了它其他进程的 CreateFile 就会返回 ACCESS_DENIED。这经常发生在自己调试自己程序时开着串口调试助手然后再启动上位机必然打不开。其次是 COM10 以上的端口名没有加设备路径前缀直接传 COM10 会找不到设备。解决先检查占用用设备管理器确认端口号是否被调试工具抢占。代码层面必须支持对 COM10 及以上端口自动加上 \. 前缀。// 端口名规范化COM10 及以上需要加设备前缀 std::string devicePath portName; if (portName.length() 4 portName[3] 0 portName[3] 9) { devicePath \\\\.\\ portName; }写串口应用时我一般做一个端口枚举功能把所有可用 COM 口列出来让用户选择而不是写死 COM3。5.2 读数据总是少了末尾几字节超时参数没设对现象Read 返回的字节数比实际发送的少尤其在高波特率下明显比如设备发了 30 字节Read 只返回 20 字节。原因ReadIntervalTimeout 设得太大或者总超时设得太小。如果 Read 请求 64 字节而设备只发 30 字节ReadFile 会等满总超时后才返回已经收到的 30 字节。此时如果你用请求多少字节就必须等到多少字节的逻辑就会误判为丢数据。本质上是把 ReadFile 当 socket recv 用了串口是个流式通道读请求的字节数是上限不是下限。解决把 COMMTIMEOUTS 调成读完一帧立即返回。做法是把 ReadIntervalTimeout 设为帧间隔阈值ReadTotalTimeoutMultiplier 和 ReadTotalTimeoutConstant 设一个合理的总上限。另外在 Read 返回后根据实际字节数去组帧不要固定等待完整长度。// 适合不定长帧的读取方式 int len serial.Read(buffer, sizeof(buffer)); if (len 0) { // 按实际长度处理一帧而不是等满固定长度 ProcessFrame(buffer, len); }串口数据是按流到达的你的代码必须做好收流组帧而不是等长接收。5.3 收到乱码波特率或数据位不匹配现象收到的数据字符错乱看起来像火星文偶尔又有一两个字符正常。原因两端波特率不一致是最常见的原因。波特率不同意味着每一位数据的采样时间不对产生位错和帧错数据全乱。其次数据位设成 7 位而发送方按 8 位编码也会导致解析错位错位后有些字符看起来正常有些乱码。校验位不匹配则呈现出随机性数据刚好奇偶匹配时正常不匹配时丢。解决先用串口调试助手两端都选同一组参数确认链路无误再接代码。如果调试助手能通而代码不能通检查 DCB 里字段是否被错误的默认值覆盖。提示排查乱码时先把流控全部关闭再把波特率核对一遍不要盲目调大缓冲区。5.4 WriteFile 卡住不返回CTS 硬件流控没接现象程序执行到 WriteFile 后就不往下走了界面无响应甚至把整个线程卡死。原因开启了硬件流控RTS/CTS 握手线没有接对端一直没有拉高 CTS发送被阻塞。同步模式下 WriteFile 会一直等待如果没设写超时线程就永远卡住。解决要么不启用流控要么确认线缆完整。写代码时还要给 Write 加一个上限等待时间并配合 CancelIo 防止永久阻塞。我一般在代码里加一个超时计数器写超时超过 3 秒就把串口复位重连这样至少能让系统自愈。5.5 程序退出时崩溃关闭句柄的顺序错误现象程序退出时偶发崩溃报错位置不定Debug 下能看到与 handle 相关。原因CloseHandle 被调用时仍有挂起的 ReadFile 或 WriteFile 请求没有清理。或者一个 OVERLAPPED 事件被多个线程同时等待句柄关闭后事件访问变成野指针。关闭顺序不对会直接制造一个野指针现场。解决关闭串口前先 CancelIo取消所有挂起的 I/O 请求然后等每个线程退出后再 CloseHandle最后关闭事件对象。顺序不能反。void SerialPort::Close() { if (handle_ ! INVALID_HANDLE_VALUE) { CancelIo(handle_); // 等待读写线程退出后再执行到这里 CloseHandle(handle_); // 句柄关闭放在最后 handle_ INVALID_HANDLE_VALUE; isOpen_ false; } }6. 最后一步给串口数据加个环形缓冲区解决粘包和半包问题前面提到过串口是按流到达的ReadFile 返回的数据可能是一帧、半帧也可能是两帧连在一起。要处理这种不定长数据一个实用技巧是引入环形缓冲区。它的作用是把收到的原始字节放进一个固定大小的循环队列上层解析器再按帧头帧尾切分完全不受单次 Read 返回长度的影响。这个思路在 Modbus、私有协议解析中非常通用。环形缓冲区实现上只需要一个数组和两个下标。写指针跟着数据进读指针跟着数据出指针到达末尾时回到数组开头。这样一个固定大小的缓冲区能反复利用避免频繁的 memmove 搬移也不会因为数据到达快慢而崩溃。// 环形缓冲区固定大小按字节存取 class RingBuffer { public: explicit RingBuffer(int size) : buffer_(new unsigned char[size]), capacity_(size), head_(0), tail_(0), count_(0) {} // 写入 len 字节返回实际写入数 int Push(const unsigned char* data, int len) { for (int i 0; i len count_ capacity_; i) { buffer_[tail_] data[i]; tail_ (tail_ 1) % capacity_; count_; } return count_ 0 ? len : 0; } // 按长度取出数据返回实际取出字节数 int Pop(unsigned char* out, int len) { int taken 0; while (taken len count_ 0) { out[taken] buffer_[head_]; head_ (head_ 1) % capacity_; --count_; } return taken; } private: std::unique_ptrunsigned char[] buffer_; int capacity_; int head_; int tail_; int count_; };代码里的 head_ 指向下一次读取位置tail_ 指向下一次写入位置count_ 记录当前有效字节数。写入时先判断是否已满满了就丢弃避免覆盖还没被解析的数据读出时按需取出不会越界。取模操作让指针在到达末尾时自动回到开头这是环形队列的标准做法。把环形缓冲接入前面的 SerialPort 类时我会单独开一个接收线程循环里只做两件事Read 拿数据Push 进缓冲。解析逻辑完全放到另一个上下文里去处理这样读取线程不会被业务解析拖慢而导致错过下一个数据包。// 接收线程把 Read 的数据推进环形缓冲 RingBuffer rxBuffer(4096); unsigned char tmp[512]; while (running) { int len serial.Read(tmp, sizeof(tmp)); if (len 0) { rxBuffer.Push(tmp, len); // 组帧解析在这里找帧头帧尾 } }这样一个循环不断从串口拿数据再往缓冲里塞上层解析逻辑随时可以从缓冲里按帧拉数据。好处是读取线程只需要做一件事不会因为业务解析耗时太长而错过下一个数据包。我从那以后每次写串口代码都会强制走一遍缓冲接收、独立解析的思路不管是 Modbus 协议还是私有帧格式都没有再为粘包翻过车。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑