在工业控制、嵌入式系统等领域,串口通信仍然是一种常用的数据传输方式。使用 C++ 和 MFC 构建上位机软件时,结合 Boost.Asio 库可以方便地实现异步串口通信,提高程序的响应速度和稳定性。然而,在实际应用中,串口设备的连接不稳定、数据传输错误等问题时常发生,因此需要设计一套完善的串口功能,包括发送、异步接收、打开、重连、关闭等,以保证通信的可靠性。
本篇文章将深入探讨如何利用 Boost.Asio 库在 MFC 框架下实现稳定可靠的串口功能,并分享一些实战避坑经验。
Boost.Asio 串口通信底层原理剖析
Boost.Asio 是一个跨平台的 C++ 库,用于网络和底层 I/O 编程。其核心思想是异步操作,通过事件驱动的方式处理 I/O 事件,避免阻塞主线程。在使用 Boost.Asio 进行串口通信时,主要涉及以下几个关键概念:
asio::io_context: I/O 上下文,所有 Asio 操作都需要在一个io_context中执行。类似于 Linux 系统中的 epoll 或者 Windows 系统中的 IOCP,管理着所有异步操作。asio::serial_port: 串口对象,用于表示一个串口设备。我们可以通过指定串口名称、波特率、校验位等参数来配置串口。asio::async_read和asio::async_write: 异步读写函数,用于从串口读取数据或向串口写入数据。这些函数接受一个回调函数作为参数,当读写操作完成时,回调函数会被调用。asio::error_code: 错误码,用于指示操作是否成功。在回调函数中,我们需要检查error_code的值,以判断是否发生了错误。
在异步读取数据时,asio::async_read 函数会立即返回,不会阻塞当前线程。当串口接收到数据时,操作系统会通知 Asio,然后 Asio 会调用我们指定的回调函数。在回调函数中,我们可以处理接收到的数据。
这种异步模式可以有效地提高程序的并发性,避免阻塞主线程,从而提高程序的响应速度。
MFC 界面线程与 Asio I/O 线程的协作
由于 MFC 的界面操作需要在主线程中进行,而 Boost.Asio 的 I/O 操作可以在独立的线程中进行,因此需要在两者之间进行协作。一种常见的做法是使用 PostMessage 函数将数据或事件从 I/O 线程发送到 MFC 主线程,然后在主线程中处理这些数据或事件。
基于 Boost.Asio 的 MFC 串口功能实现
下面是一个简单的示例代码,展示了如何在 MFC 中使用 Boost.Asio 实现串口的打开、发送、异步接收和关闭功能。
// SerialPort.h
#pragma once
#include <boost/asio.hpp>
#include <boost/asio/serial_port.hpp>
#include <string>
#include <vector>
class SerialPort
{
public:
SerialPort(asio::io_context& io_context, const std::string& port_name);
~SerialPort();
bool open();
void close();
bool isOpen() const { return is_open_; }
void async_read(std::vector<unsigned char>& buffer, std::function<void(const asio::error_code&, size_t)> callback);
void async_write(const std::vector<unsigned char>& data, std::function<void(const asio::error_code&, size_t)> callback);
private:
asio::io_context& io_context_;
asio::serial_port serial_port_;
std::string port_name_;
bool is_open_ = false;
void do_read();
};
// SerialPort.cpp
#include "SerialPort.h"
#include <iostream>
SerialPort::SerialPort(asio::io_context& io_context, const std::string& port_name)
: io_context_(io_context),
serial_port_(io_context, port_name),
port_name_(port_name)
{
}
SerialPort::~SerialPort()
{
close();
}
bool SerialPort::open()
{
try
{
serial_port_.set_option(asio::serial_port_base::baud_rate(115200)); // 设置波特率
serial_port_.set_option(asio::serial_port_base::parity(asio::serial_port_base::parity::none)); // 设置校验位
serial_port_.set_option(asio::serial_port_base::stop_bits(asio::serial_port_base::stop_bits::one)); // 设置停止位
serial_port_.set_option(asio::serial_port_base::flow_control(asio::serial_port_base::flow_control::none)); // 设置流控制
is_open_ = true;
return true;
}
catch (const std::exception& e)
{
std::cerr << "Error opening serial port: " << e.what() << std::endl;
is_open_ = false;
return false;
}
}
void SerialPort::close()
{
if (serial_port_.is_open())
{
asio::error_code ec;
serial_port_.close(ec);
if (ec)
{
std::cerr << "Error closing serial port: " << ec.message() << std::endl;
}
is_open_ = false;
}
}
void SerialPort::async_read(std::vector<unsigned char>& buffer, std::function<void(const asio::error_code&, size_t)> callback)
{
serial_port_.async_read_some(asio::buffer(buffer), callback);
}
void SerialPort::async_write(const std::vector<unsigned char>& data, std::function<void(const asio::error_code&, size_t)> callback)
{
asio::async_write(serial_port_, asio::buffer(data), callback);
}
// Example Usage in MFC
asio::io_context io_context;
SerialPort serial_port(io_context, "COM1"); // 替换为你的串口名称
if (serial_port.open()) {
std::vector<unsigned char> data_to_send = {0x01, 0x02, 0x03}; // 要发送的数据
serial_port.async_write(data_to_send, [](const asio::error_code& error, size_t bytes_transferred) {
if (!error) {
std::cout << "Sent " << bytes_transferred << " bytes" << std::endl;
} else {
std::cerr << "Error sending data: " << error.message() << std::endl;
}
});
std::vector<unsigned char> read_buffer(256);
serial_port.async_read(read_buffer, [&read_buffer](const asio::error_code& error, size_t bytes_transferred) {
if (!error) {
std::cout << "Received " << bytes_transferred << " bytes: ";
for (size_t i = 0; i < bytes_transferred; ++i) {
std::cout << std::hex << (int)read_buffer[i] << " ";
}
std::cout << std::endl;
// 再次启动异步读取,确保持续接收数据
//serial_port.async_read(read_buffer, /* ... */);
} else {
std::cerr << "Error receiving data: " << error.message() << std::endl;
}
});
std::thread t([&io_context](){ io_context.run(); }); // 启动 I/O 线程
// 确保 io_context.run() 在单独的线程中运行,避免阻塞 MFC 主线程
// 可以在 MFC 消息循环中调用 io_context.post() 来执行一些需要在 I/O 线程中执行的任务
t.detach(); // 让线程独立运行
}
实现串口重连机制
为了应对串口设备断开连接的情况,我们需要实现串口重连机制。一种简单的做法是在异步读写的回调函数中,当检测到错误时,尝试重新打开串口。为了避免频繁重连,可以设置一个重连间隔时间。
// 在 SerialPort 类中添加重连函数
void SerialPort::reconnect()
{
close();
asio::steady_timer timer(io_context_, std::chrono::seconds(5)); // 设置重连间隔为 5 秒
timer.async_wait([this](const asio::error_code& error) {
if (!error)
{
if (open())
{
std::cout << "Serial port reconnected successfully." << std::endl;
// 重新启动异步读取
//do_read();
}
else
{
std::cerr << "Failed to reconnect serial port." << std::endl;
reconnect(); // 再次尝试重连
}
}
});
}
// 在异步读写的回调函数中调用 reconnect 函数
serial_port.async_read(read_buffer, [this, &read_buffer](const asio::error_code& error, size_t bytes_transferred) {
if (!error) {
// 处理接收到的数据
} else {
std::cerr << "Error receiving data: " << error.message() << std::endl;
reconnect(); // 尝试重连
}
});
实战避坑经验总结
- 串口名称大小写敏感:在 Windows 平台上,串口名称通常以 "COM" 开头,例如 "COM1"、"COM2" 等。需要注意的是,串口名称是大小写敏感的,因此需要确保串口名称的大小写与实际设备一致。
- 线程同步问题:由于 MFC 的界面操作需要在主线程中进行,而 Boost.Asio 的 I/O 操作可以在独立的线程中进行,因此需要注意线程同步问题。可以使用
PostMessage函数将数据或事件从 I/O 线程发送到 MFC 主线程,或者使用std::mutex等同步原语来保护共享资源。 - 异常处理:在使用 Boost.Asio 进行串口通信时,需要注意异常处理。可以使用
try-catch块来捕获异常,并进行相应的处理。例如,当串口无法打开时,可以尝试重新打开串口。 - 缓冲区大小:
async_read_some不会保证读到期望大小的数据,需要循环读取或设置超时机制。 - 资源释放: 程序退出时,务必确保
io_context.stop()被调用,否则可能会导致程序崩溃。
通过以上步骤,我们可以使用 C++ MFC 结合 Boost.Asio 库实现一个稳定可靠的串口功能。在实际应用中,可以根据具体需求进行定制和优化,例如添加数据校验、流量控制等功能。同时,需要注意线程同步、异常处理等问题,以保证程序的稳定性和可靠性。实际开发中,可以使用类似 Nginx 的事件驱动模型,保证高并发下的稳定连接,也可以使用宝塔面板方便管理服务器,但桌面软件的并发量级远小于服务器,所以通常不需要考虑这些。
冠军资讯
代码一只喵