概述
在 windows 平台下操作串口,需要调用 win32 api createfile、readfile、writefile 等,代码稍显繁琐。本类对串口的打开、配置、读写操作进行封装,提供简洁的 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);
- 路径格式:windows 上超过 com9 的串口名(如 com10、com\.\ physicalcom0)需要加
\\.\前缀。代码自动补全。 - 同步模式:使用同步 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;
}
}
关键点:
m_running = false通知接收线程退出。cancelioex中断readfile,配合超时设置使线程尽快退出。join()等待线程结束,避免析构时线程仍运行。- 最后才
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));
}
}
逻辑说明:
- 循环读取,直到
m_running为 false 或发生错误。 readfile在超时设置下最多阻塞 50ms,之后返回,即使未读到任何数据。- 读到数据后:优先调用用户回调;无回调时默认打印 hex + 可打印字符。
- 回调异常捕获:防止用户回调中的崩溃影响串口接收线程。
- 每次循环
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++串口通信类的资料请关注代码网其它相关文章!
发表评论