基于C++实现轻量且线程安全的Windows串口通信封装类

 更新时间:2026年04月14日 09:30:18   作者:沈跃泉  
这篇文章主要为大家详细介绍了如何基于C++实现轻量且线程安全的Windows串口通信封装类,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

概述

在 Windows 平台下操作串口,需要调用 Win32 API CreateFileReadFileWriteFile 等,代码稍显繁琐。本类对串口的打开、配置、读写操作进行封装,提供简洁的 C++ 接口,并内置了接收线程和回调机制。

源码共 3 个文件:

文件说明
SerialPort.h类声明
SerialPort.cpp完整实现
main.cpp使用示例

头文件SerialPort.h

#pragma once
#include <windows.h>
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <functional>
#include <mutex>
class SerialPort {
public:
    SerialPort();
    ~SerialPort();
    bool open(const std::string& portName, unsigned long baudRate);
    void close();
    bool isOpen() const;
    std::string getLastError() const;
    // 发送数据(线程安全)
    bool write(const std::vector<unsigned char>& data);
    bool write(const std::string& s);
    // 设置接收回调
    void setReceiveCallback(std::function<void(const std::vector<unsigned char>&)> cb);
private:
    void receiveLoop();
    HANDLE m_handle;
    std::atomic<bool> m_running;
    std::thread m_thread;
    std::string m_lastError;
    std::function<void(const std::vector<unsigned char>&)> m_callback;
    std::mutex m_writeMutex;
};

设计要点

  • std::atomic<bool> m_running:跨线程共享的运行标志,比 volatile bool 更安全。
  • std::mutex m_writeMutex:写操作加锁,保证多线程并发调用 write() 时的线程安全。
  • std::function 回调:用户可通过 lambda、函数指针等灵活注册数据接收处理逻辑。
  • RAII 析构close() 在析构函数中自动调用,确保资源释放。

实现SerialPort.cpp

构造函数与析构

SerialPort::SerialPort()
    : m_handle(INVALID_HANDLE_VALUE), m_running(false)
{
}

SerialPort::~SerialPort()
{
    close();
}

析构调用 close(),保证对象销毁时串口一定被关闭。

错误信息辅助函数

static std::string GetLastErrorAsString(DWORD err)
{
    if (err == 0) return std::string();
    LPSTR messageBuffer = nullptr;
    DWORD size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        nullptr, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, nullptr);
    std::string message;
    if (messageBuffer && size > 0) message.assign(messageBuffer, size);
    if (messageBuffer) LocalFree(messageBuffer);
    return message;
}

将 Windows API 的错误码转换为可读的字符串,供调试使用。

打开串口open()

bool SerialPort::open(const std::string& portName, unsigned long baudRate)
{
    if (isOpen()) return true;

    std::string fullName = portName;
    if (portName.rfind("\\\\.", 0) != 0) {
        fullName = "\\\\.\\" + portName;
    }

    m_handle = CreateFileA(fullName.c_str(),
        GENERIC_READ | GENERIC_WRITE,
        0, nullptr, OPEN_EXISTING, 0, nullptr);
  1. 路径格式:Windows 上超过 COM9 的串口名(如 COM10、COM\.\ PHYSICALCOM0)需要加 \\.\ 前缀。代码自动补全。
  2. 同步模式:使用同步 I/O,接收由独立线程负责,避免阻塞主线程。
    DCB dcb;
    SecureZeroMemory(&dcb, sizeof(dcb));
    dcb.DCBlength = sizeof(dcb);
    if (!GetCommState(m_handle, &dcb)) { /* ... */ }

    dcb.BaudRate = baudRate;
    dcb.ByteSize = 8;
    dcb.Parity = NOPARITY;
    dcb.StopBits = ONESTOPBIT;
    if (!SetCommState(m_handle, &dcb)) { /* ... */ }

通过 DCB 结构配置波特率、数据位、校验位、停止位,默认 8N1。

    COMMTIMEOUTS timeouts;
    timeouts.ReadIntervalTimeout = 50;
    timeouts.ReadTotalTimeoutMultiplier = 0;
    timeouts.ReadTotalTimeoutConstant = 50;
    timeouts.WriteTotalTimeoutMultiplier = 0;
    timeouts.WriteTotalTimeoutConstant = 50;
    SetCommTimeouts(m_handle, &timeouts);

    PurgeComm(m_handle, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT);

    m_running = true;
    m_thread = std::thread(&SerialPort::receiveLoop, this);
    return true;
}
  • 超时设置:Read 每次最多等待 50ms,防止 ReadFile 永久阻塞。
  • 清空缓冲区PurgeComm 丢弃旧数据。
  • 启动接收线程receiveLoop() 在独立线程中运行。

关闭串口close()

void SerialPort::close()
{
    if (!isOpen()) return;

    m_running = false;
    CancelIoEx(m_handle, nullptr);   // 取消阻塞中的 IO

    if (m_thread.joinable()) m_thread.join();

    if (m_handle != INVALID_HANDLE_VALUE) {
        CloseHandle(m_handle);
        m_handle = INVALID_HANDLE_VALUE;
    }
}

关键点:

  1. m_running = false 通知接收线程退出。
  2. CancelIoEx 中断 ReadFile,配合超时设置使线程尽快退出。
  3. join() 等待线程结束,避免析构时线程仍运行。
  4. 最后才 CloseHandle,保证线程已安全退出。

发送数据write()

bool SerialPort::write(const std::vector<unsigned char>& data)
{
    if (!isOpen()) { m_lastError = "Port not open"; return false; }

    std::lock_guard<std::mutex> lock(m_writeMutex);

    DWORD bytesWritten = 0;
    BOOL ok = WriteFile(m_handle, data.data(), static_cast<DWORD>(data.size()), &bytesWritten, nullptr);
    if (!ok) { m_lastError = "WriteFile failed: " + GetLastErrorAsString(GetLastError()); return false; }

    return bytesWritten == data.size();
}

bool SerialPort::write(const std::string& s)
{
    return write(std::vector<unsigned char>(s.begin(), s.end()));
}
  • 写锁std::lock_guard 保证多线程同时调用 write() 时不会产生竞态。
  • 两个重载:一个接受字节数组,一个接受字符串,使用更方便。

接收回调setReceiveCallback()

void SerialPort::setReceiveCallback(std::function<void(const std::vector<unsigned char>&)> cb)
{
    m_callback = std::move(cb);
}

使用 std::move 避免不必要的拷贝。

接收线程receiveLoop()

void SerialPort::receiveLoop()
{
    const DWORD bufSize = 1024;
    std::vector<unsigned char> buffer(bufSize);

    while (m_running && isOpen()) {
        DWORD bytesRead = 0;
        BOOL ok = ReadFile(m_handle, buffer.data(), bufSize, &bytesRead, nullptr);
        if (!ok) {
            DWORD err = GetLastError();
            if (err != ERROR_IO_PENDING && err != ERROR_TIMEOUT && err != ERROR_SUCCESS) {
                m_lastError = "ReadFile failed: " + GetLastErrorAsString(err);
                break;
            }
        }

        if (bytesRead > 0) {
            std::vector<unsigned char> data(buffer.begin(), buffer.begin() + bytesRead);
            if (m_callback) {
                try { m_callback(data); }
                catch (...) { /* 忽略回调异常 */ }
            }
            else {
                // 默认打印:可打印字符 + HEX
                std::cout << "[串口接收] 字符: " << printable << "  HEX: " << ossHex.str() << std::endl;
            }
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

逻辑说明:

  1. 循环读取,直到 m_running 为 false 或发生错误。
  2. ReadFile 在超时设置下最多阻塞 50ms,之后返回,即使未读到任何数据。
  3. 读到数据后:优先调用用户回调;无回调时默认打印 HEX + 可打印字符。
  4. 回调异常捕获:防止用户回调中的崩溃影响串口接收线程。
  5. 每次循环 sleep_for(10ms) 降低 CPU 占用。

完整源码

SerialPort.h

#pragma once
#include <windows.h>
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <functional>
#include <mutex>

class SerialPort {
public:
    SerialPort();
    ~SerialPort();

    bool open(const std::string& portName, unsigned long baudRate);
    void close();
    bool isOpen() const;
    std::string getLastError() const;

    // 发送数据(线程安全)
    bool write(const std::vector<unsigned char>& data);
    bool write(const std::string& s);

    // 设置接收回调
    void setReceiveCallback(std::function<void(const std::vector<unsigned char>&)> cb);

private:
    void receiveLoop();

    HANDLE m_handle;
    std::atomic<bool> m_running;
    std::thread m_thread;
    std::string m_lastError;
    std::function<void(const std::vector<unsigned char>&)> m_callback;
    std::mutex m_writeMutex;
};

SerialPort.cpp

#include "SerialPort.h"
#include <iostream>
#include <sstream>
#include <chrono>
#include <iomanip>
#include <mutex>

SerialPort::SerialPort()
    : m_handle(INVALID_HANDLE_VALUE), m_running(false)
{
}

SerialPort::~SerialPort()
{
    close();
}

static std::string GetLastErrorAsString(DWORD err)
{
    if (err == 0) return std::string();
    LPSTR messageBuffer = nullptr;
    DWORD size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        nullptr, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, nullptr);
    std::string message;
    if (messageBuffer && size > 0) message.assign(messageBuffer, size);
    if (messageBuffer) LocalFree(messageBuffer);
    return message;
}

bool SerialPort::open(const std::string& portName, unsigned long baudRate)
{
    if (isOpen()) return true;

    std::string fullName = portName;
    if (portName.rfind("\\\\.", 0) != 0) {
        fullName = "\\\\.\\" + portName;
    }

    m_handle = CreateFileA(fullName.c_str(),
        GENERIC_READ | GENERIC_WRITE,
        0,
        nullptr,
        OPEN_EXISTING,
        0,
        nullptr);

    if (m_handle == INVALID_HANDLE_VALUE) {
        m_lastError = "CreateFile failed: " + GetLastErrorAsString(GetLastError());
        return false;
    }

    DCB dcb;
    SecureZeroMemory(&dcb, sizeof(dcb));
    dcb.DCBlength = sizeof(dcb);
    if (!GetCommState(m_handle, &dcb)) {
        m_lastError = "GetCommState failed: " + GetLastErrorAsString(GetLastError());
        CloseHandle(m_handle);
        m_handle = INVALID_HANDLE_VALUE;
        return false;
    }

    dcb.BaudRate = baudRate;
    dcb.ByteSize = 8;
    dcb.Parity = NOPARITY;
    dcb.StopBits = ONESTOPBIT;
    if (!SetCommState(m_handle, &dcb)) {
        m_lastError = "SetCommState failed: " + GetLastErrorAsString(GetLastError());
        CloseHandle(m_handle);
        m_handle = INVALID_HANDLE_VALUE;
        return false;
    }

    COMMTIMEOUTS timeouts;
    timeouts.ReadIntervalTimeout = 50;
    timeouts.ReadTotalTimeoutMultiplier = 0;
    timeouts.ReadTotalTimeoutConstant = 50;
    timeouts.WriteTotalTimeoutMultiplier = 0;
    timeouts.WriteTotalTimeoutConstant = 50;
    SetCommTimeouts(m_handle, &timeouts);

    PurgeComm(m_handle, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT);

    m_running = true;
    m_thread = std::thread(&SerialPort::receiveLoop, this);
    return true;
}

void SerialPort::close()
{
    if (!isOpen()) return;

    m_running = false;

    // 取消可能的阻塞 IO,尝试使 ReadFile 返回
    CancelIoEx(m_handle, nullptr);

    if (m_thread.joinable()) m_thread.join();

    if (m_handle != INVALID_HANDLE_VALUE) {
        CloseHandle(m_handle);
        m_handle = INVALID_HANDLE_VALUE;
    }
}

bool SerialPort::isOpen() const
{
    return m_handle != INVALID_HANDLE_VALUE;
}

std::string SerialPort::getLastError() const
{
    return m_lastError;
}

bool SerialPort::write(const std::vector<unsigned char>& data)
{
    if (!isOpen()) {
        m_lastError = "Port not open";
        return false;
    }

    std::lock_guard<std::mutex> lock(m_writeMutex);

    DWORD bytesWritten = 0;
    BOOL ok = WriteFile(m_handle, data.data(), static_cast<DWORD>(data.size()), &bytesWritten, nullptr);
    if (!ok) {
        m_lastError = "WriteFile failed: " + GetLastErrorAsString(GetLastError());
        return false;
    }

    return bytesWritten == data.size();
}

bool SerialPort::write(const std::string& s)
{
    return write(std::vector<unsigned char>(s.begin(), s.end()));
}

void SerialPort::setReceiveCallback(std::function<void(const std::vector<unsigned char>&)> cb)
{
    m_callback = std::move(cb);
}

void SerialPort::receiveLoop()
{
    const DWORD bufSize = 1024;
    std::vector<unsigned char> buffer(bufSize);

    while (m_running && isOpen()) {
        DWORD bytesRead = 0;
        BOOL ok = ReadFile(m_handle, buffer.data(), bufSize, &bytesRead, nullptr);
        if (!ok) {
            DWORD err = GetLastError();
            if (err != ERROR_IO_PENDING && err != ERROR_TIMEOUT && err != ERROR_SUCCESS) {
                m_lastError = "ReadFile failed: " + GetLastErrorAsString(err);
                break;
            }
        }

        if (bytesRead > 0) {
            std::vector<unsigned char> data(buffer.begin(), buffer.begin() + bytesRead);
            if (m_callback) {
                try {
                    m_callback(data);
                }
                catch (...) {
                    // 忽略回调异常
                }
            }
            else {
                std::ostringstream ossHex;
                std::string printable;
                for (unsigned char b : data) {
                    if (b >= 0x20 && b <= 0x7E) printable.push_back(static_cast<char>(b));
                    else printable.push_back('.');
                    ossHex << std::hex << std::uppercase << std::setw(2) << std::setfill('0')
                        << static_cast<int>(b) << ' ';
                }
                // 注意:此处为线程中打印,若需线程安全或按序输出可改为其他机制
                std::cout << "[串口接收] 字符: " << printable << "  HEX: " << ossHex.str() << std::endl;
            }
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

使用示例main.cpp

#include <conio.h>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <thread>
#include "SerialPort.h"
#include <io.h>
#include <fcntl.h>
#include <windows.h>

void recvFunc(const std::vector<unsigned char>& data)
{
    std::ostringstream ossHex;
    std::string printable;
    for (unsigned char b : data) {
        if (b >= 0x20 && b <= 0x7E) printable.push_back(static_cast<char>(b));
        else printable.push_back('.');
        ossHex << std::hex << std::uppercase << std::setw(2) << std::setfill('0')
            << static_cast<int>(b) << ' ';
    }
    // use ASCII prefix to avoid encoding issues in callback thread
    std::cout << "[Receive] ASCII: " << printable << "  HEX: " << ossHex.str() << std::endl;
}

int main()
{
    // 演示串口类的简单使用(需要真实串口才能收到数据)
    SerialPort sp;
    // 示例:打开 COM3,115200 波特(根据实际串口修改)
    if (sp.open("COM3", CBR_115200)) {
        std::cout << "start recv" << std::endl;

        // Start the receive thread
		sp.setReceiveCallback(recvFunc);

        // 示例:发送字节 0x55 0xAA
        {
            std::vector<unsigned char> pkt = { 0x55, 0xAA };
            if (sp.write(pkt)) {
                std::cout << "已发送: 0x55 0xAA" << std::endl;
            } else {
                std::cout << "发送失败: " << sp.getLastError() << std::endl;
            }
        }

        system("pause");

        sp.close();
        std::cout << "串口已关闭。\n";
    }
    else {
        std::cout << "打开串口失败:\n";
        std::cout << sp.getLastError() << "\n";
    }

    return 0;
}

以上就是基于C++实现轻量且线程安全的Windows串口通信封装类的详细内容,更多关于C++串口通信类的资料请关注脚本之家其它相关文章!

相关文章

  • C++解析wav文件方法介绍

    C++解析wav文件方法介绍

    最近将项目改为跨平台,于是音频模块从微软的XAudio2改用OpenAL库。之前使用MSDN的代码,所以现在改为了C++标准的写法,适用性更广
    2022-09-09
  • C语言中的sscanf和sprintf常见用途

    C语言中的sscanf和sprintf常见用途

    sscanf和sprintf是C语言中用于字符串与数据转换的函数,类似scanf/printf但处理字符串而非输入输出流,本文给大家介绍C语言中的sscanf和sprintf常见用途,感兴趣的朋友一起看看吧
    2025-09-09
  • 使用VS Code通过SSH编译Linux上的C++程序的详细步骤

    使用VS Code通过SSH编译Linux上的C++程序的详细步骤

    在软件开发领域,跨平台开发是一项常见需求,特别是对于C++开发者来说,有时需要在Windows环境下编写代码,但却需要在Linux环境中编译和运行,VS Code提供了强大的远程开发功能,本文将详细介绍如何配置和使用VS Code的SSH远程开发功能,实现无缝的跨平台C++开发体验
    2025-05-05
  • VS2019开发简单的C/C++动态链接库并进行调用的实现

    VS2019开发简单的C/C++动态链接库并进行调用的实现

    这篇文章主要介绍了VS2019开发简单的C/C++动态链接库并进行调用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • C 语言中实现环形缓冲区

    C 语言中实现环形缓冲区

    本文主要是介绍 C语言实现环形缓冲区,并附有详细实现代码,具有一定的参考价值,希望能帮助有需要的小伙伴
    2016-07-07
  • VSCode多根工作区功能实现

    VSCode多根工作区功能实现

    VSCode的多根工作区功能允许在一个窗口内同时处理多个文件夹,适用于前后端分离项目、Monorepo项目管理、微服务架构开发等场景,本文主要介绍了VSCode多根工作区功能实现,具有一定的参考价值,感兴趣的可以了解一下
    2025-12-12
  • c++ 头文件<cwchar>中常见函数的实现代码

    c++ 头文件<cwchar>中常见函数的实现代码

    本文记录了c++ 头文件<cwchar>中常见函数的实现,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-12-12
  • C语言中的BYTE和char深入解析

    C语言中的BYTE和char深入解析

    在C语言中,字符(character)这个术语具有两个层次上的含义:书写源程序的字符和程序处理的字符
    2013-10-10
  • C++语言实现hash表详解及实例代码

    C++语言实现hash表详解及实例代码

    这篇文章主要介绍了C++语言实现hash表详解及实例代码的相关资料,需要的朋友可以参考下
    2017-01-01
  • C语言银行储蓄系统源码

    C语言银行储蓄系统源码

    这篇文章主要为大家详细介绍了C语言银行储蓄系统源码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01

最新评论