当前位置: 代码网 > 科技>操作系统>Windows > 基于C++实现轻量且线程安全的Windows串口通信封装类

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

2026年04月14日 Windows 我要评论
概述在 windows 平台下操作串口,需要调用 win32 api createfile、readfile、writefile 等,代码稍显繁琐。本类对串口的打开、配置、读写操作进行封装,提供简洁的

概述

在 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++串口通信类的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com