基于C++實(shí)現(xiàn)輕量且線程安全的Windows串口通信封裝類
概述
在 Windows 平臺(tái)下操作串口,需要調(diào)用 Win32 API CreateFile、ReadFile、WriteFile 等,代碼稍顯繁瑣。本類對(duì)串口的打開、配置、讀寫操作進(jìn)行封裝,提供簡(jiǎn)潔的 C++ 接口,并內(nèi)置了接收線程和回調(diào)機(jī)制。
源碼共 3 個(gè)文件:
| 文件 | 說明 |
|---|---|
SerialPort.h | 類聲明 |
SerialPort.cpp | 完整實(shí)現(xiàn) |
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;
// 發(fā)送數(shù)據(jù)(線程安全)
bool write(const std::vector<unsigned char>& data);
bool write(const std::string& s);
// 設(shè)置接收回調(diào)
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;
};設(shè)計(jì)要點(diǎn)
std::atomic<bool> m_running:跨線程共享的運(yùn)行標(biāo)志,比volatile bool更安全。std::mutex m_writeMutex:寫操作加鎖,保證多線程并發(fā)調(diào)用write()時(shí)的線程安全。std::function回調(diào):用戶可通過 lambda、函數(shù)指針等靈活注冊(cè)數(shù)據(jù)接收處理邏輯。- RAII 析構(gòu):
close()在析構(gòu)函數(shù)中自動(dòng)調(diào)用,確保資源釋放。
實(shí)現(xiàn)SerialPort.cpp
構(gòu)造函數(shù)與析構(gòu)
SerialPort::SerialPort()
: m_handle(INVALID_HANDLE_VALUE), m_running(false)
{
}
SerialPort::~SerialPort()
{
close();
}
析構(gòu)調(diào)用 close(),保證對(duì)象銷毀時(shí)串口一定被關(guān)閉。
錯(cuò)誤信息輔助函數(shù)
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 的錯(cuò)誤碼轉(zhuǎn)換為可讀的字符串,供調(diào)試使用。
打開串口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)需要加
\\.\前綴。代碼自動(dòng)補(bǔ)全。 - 同步模式:使用同步 I/O,接收由獨(dú)立線程負(fù)責(zé),避免阻塞主線程。
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 結(jié)構(gòu)配置波特率、數(shù)據(jù)位、校驗(yàn)位、停止位,默認(rèn) 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;
}
- 超時(shí)設(shè)置:Read 每次最多等待 50ms,防止
ReadFile永久阻塞。 - 清空緩沖區(qū):
PurgeComm丟棄舊數(shù)據(jù)。 - 啟動(dòng)接收線程:
receiveLoop()在獨(dú)立線程中運(yùn)行。
關(guān)閉串口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;
}
}
關(guān)鍵點(diǎn):
m_running = false通知接收線程退出。CancelIoEx中斷ReadFile,配合超時(shí)設(shè)置使線程盡快退出。join()等待線程結(jié)束,避免析構(gòu)時(shí)線程仍運(yùn)行。- 最后才
CloseHandle,保證線程已安全退出。
發(fā)送數(shù)據(jù)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保證多線程同時(shí)調(diào)用write()時(shí)不會(huì)產(chǎn)生競(jìng)態(tài)。 - 兩個(gè)重載:一個(gè)接受字節(jié)數(shù)組,一個(gè)接受字符串,使用更方便。
接收回調(diào)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 (...) { /* 忽略回調(diào)異常 */ }
}
else {
// 默認(rèn)打?。嚎纱蛴∽址?+ HEX
std::cout << "[串口接收] 字符: " << printable << " HEX: " << ossHex.str() << std::endl;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
邏輯說明:
- 循環(huán)讀取,直到
m_running為 false 或發(fā)生錯(cuò)誤。 ReadFile在超時(shí)設(shè)置下最多阻塞 50ms,之后返回,即使未讀到任何數(shù)據(jù)。- 讀到數(shù)據(jù)后:優(yōu)先調(diào)用用戶回調(diào);無(wú)回調(diào)時(shí)默認(rèn)打印 HEX + 可打印字符。
- 回調(diào)異常捕獲:防止用戶回調(diào)中的崩潰影響串口接收線程。
- 每次循環(huán)
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;
// 發(fā)送數(shù)據(jù)(線程安全)
bool write(const std::vector<unsigned char>& data);
bool write(const std::string& s);
// 設(shè)置接收回調(diào)
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 (...) {
// 忽略回調(diào)異常
}
}
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) << ' ';
}
// 注意:此處為線程中打印,若需線程安全或按序輸出可改為其他機(jī)制
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()
{
// 演示串口類的簡(jiǎn)單使用(需要真實(shí)串口才能收到數(shù)據(jù))
SerialPort sp;
// 示例:打開 COM3,115200 波特(根據(jù)實(shí)際串口修改)
if (sp.open("COM3", CBR_115200)) {
std::cout << "start recv" << std::endl;
// Start the receive thread
sp.setReceiveCallback(recvFunc);
// 示例:發(fā)送字節(jié) 0x55 0xAA
{
std::vector<unsigned char> pkt = { 0x55, 0xAA };
if (sp.write(pkt)) {
std::cout << "已發(fā)送: 0x55 0xAA" << std::endl;
} else {
std::cout << "發(fā)送失敗: " << sp.getLastError() << std::endl;
}
}
system("pause");
sp.close();
std::cout << "串口已關(guān)閉。\n";
}
else {
std::cout << "打開串口失敗:\n";
std::cout << sp.getLastError() << "\n";
}
return 0;
}
以上就是基于C++實(shí)現(xiàn)輕量且線程安全的Windows串口通信封裝類的詳細(xì)內(nèi)容,更多關(guān)于C++串口通信類的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
VC實(shí)現(xiàn)給窗體的一個(gè)按鈕添加事件的方法
這篇文章主要介紹了VC實(shí)現(xiàn)給窗體的一個(gè)按鈕添加事件的方法,通過三個(gè)簡(jiǎn)單步驟實(shí)現(xiàn)窗體按鈕添加事件,需要的朋友可以參考下2015-05-05
C語(yǔ)言實(shí)現(xiàn)哈希搜索算法及原理詳解
本文主要介紹了C語(yǔ)言實(shí)現(xiàn)哈希搜索算法及原理詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
使用C++和代理IP實(shí)現(xiàn)天氣預(yù)報(bào)的采集
在當(dāng)今的互聯(lián)網(wǎng)時(shí)代,網(wǎng)絡(luò)信息的獲取變得日益重要,天氣預(yù)報(bào)信息作為日常生活的重要參考,其獲取方式也隨著技術(shù)的發(fā)展而不斷變化,在本文中,我們將探討如何使用C++和代理IP來(lái)采集天氣預(yù)報(bào)信息,文中通過代碼講解的非常詳細(xì),需要的朋友可以參考下2023-12-12
基于Matlab實(shí)現(xiàn)數(shù)字音頻分析處理系統(tǒng)
這篇文章主要為大家介紹了如何利用Matlab制作一個(gè)帶GUI的數(shù)字音頻分析與處理系統(tǒng)。文中的示例代碼講解詳細(xì),感興趣的小伙伴可以學(xué)習(xí)一下2022-02-02

