基于Visual C++与API封装类的串口调试工具开发实战
简介:串口通信在嵌入式系统和设备控制中具有重要作用。本文介绍如何使用Visual C++和Windows API封装类开发串口调试程序。通过面向对象的设计方式封装串口操作类,并结合MFC框架构建用户界面,实现串口选择、参数设置、数据收发、错误处理等功能。文章还讲解了日志记录与定时器机制,提升调试效率与用户体验。适合初学者和工程实践者掌握串口通信开发核心技术。
1. 串口通信基本原理
串口通信是一种通过串行接口逐位传输数据的通信方式,广泛应用于工业控制、仪器仪表、嵌入式系统等领域。其核心在于通过有限的信号线(如TXD发送、RXD接收、GND地线)实现设备间的数据交换。
1.1 串口通信的定义与作用
串口通信(Serial Communication)是指数据以串行形式,逐位依次传输的通信方式。与并行通信相比,其优势在于布线简单、抗干扰能力强、传输距离远,适合长距离、低速率的设备间通信。
在工业自动化系统中,串口通信常用于PLC、传感器、仪表与PC之间的数据交互。例如,使用串口读取温度传感器的数值,或向嵌入式设备发送控制指令。
常见串口通信标准包括:
| 标准 | 电平标准 | 传输距离 | 传输速率 | 通信方式 |
|---|---|---|---|---|
| RS-232 | ±12V | <15米 | <20kbps | 点对点 |
| RS-422 | 差分信号 | <1200米 | <10Mbps | 点对多点 |
| RS-485 | 差分信号 | <1200米 | <10Mbps | 多点通信 |
其中,RS-232是最基础、最常见的串口通信协议,广泛用于PC与外围设备的连接。
1.2 RS-232协议标准解析
RS-232(Recommended Standard 232)是由EIA(电子工业协会)制定的串行通信物理层标准,定义了数据终端设备(DTE)和数据通信设备(DCE)之间的接口规范。
主要特性:
- 电压电平 :逻辑1为-3V~-15V,逻辑0为+3V~+15V
- 连接方式 :通常使用DB9或DB25接口
- 引脚定义示例(DB9) :
| 引脚 | 名称 | 方向 | 功能说明 |
|---|---|---|---|
| 2 | RXD | 输入 | 接收数据 |
| 3 | TXD | 输出 | 发送数据 |
| 5 | GND | - | 地线 |
| 7 | RTS | 输出 | 请求发送 |
| 8 | CTS | 输入 | 清除发送 |
数据传输过程:
发送端通过TXD发送数据,接收端通过RXD接收数据,GND作为公共参考地。RTS和CTS用于硬件流控制,防止数据丢失。
1.3 数据帧结构详解
串口通信中,数据是以帧(Frame)为单位进行传输的,每一帧包含以下几个部分:
[起始位] [数据位] [奇偶校验位] [停止位]
- 起始位(Start Bit) :1位,逻辑0,表示数据帧开始
- 数据位(Data Bits) :5~8位,通常为8位,表示实际传输的数据
- 校验位(Parity Bit) :0或1位,用于奇偶校验
- 停止位(Stop Bit) :1~2位,逻辑1,表示数据帧结束
例如,一个典型的数据帧配置为:8个数据位、1个停止位、无校验位(常写作8N1)。
1.4 波特率、奇偶校验与停止位参数解析
波特率(Baud Rate)
波特率表示每秒传输的信号变化次数,通常也表示为每秒传输的比特数(bps)。常见波特率包括:9600、19200、38400、57600、115200等。
注意 :通信双方必须设置相同的波特率,否则会导致数据解析错误。
奇偶校验(Parity)
用于简单的数据校验,可选值为:
- None(无校验)
- Even(偶校验):校验位使得1的个数为偶数
- Odd(奇校验):校验位使得1的个数为奇数
停止位(Stop Bits)
表示数据帧结束的位数,通常为1或2位,用于接收端识别帧边界。
1.5 Windows系统下串口设备的基本管理方式
在Windows系统中,串口设备通常以COM端口的形式存在,例如COM1、COM2等。可以通过以下方式查看和管理串口设备:
1. 设备管理器查看串口设备
- 打开“设备管理器” → 展开“端口(COM和LPT)”节点
- 查看当前系统中可用的COM端口号
2. 使用命令行获取串口信息
wmic path Win32_SerialPort get DeviceID,Name
执行结果示例:
DeviceID Name
COM1 Communications Port (COM1)
COM3 USB Serial Port (COM3)
3. 使用注册表查看串口信息
路径: HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM
通过注册表可以获取所有映射到系统中的串口设备路径,如: \Device\Vcpm0 对应 COM3。
这些信息在编程访问串口设备时非常重要,后续章节将详细介绍如何在Windows API中打开和操作串口。
2. Windows API串口操作函数使用
Windows系统下的串口通信开发通常依赖于Windows API函数。本章将深入讲解串口操作的核心API函数,包括串口设备的打开与关闭、通信参数的配置、数据的读写操作以及状态监控与错误处理机制。通过本章的学习,开发者将能够掌握使用Windows API进行串口通信的基础技能,并具备进一步封装和优化的能力。
2.1 Windows串口设备的打开与关闭
串口通信的第一步是打开串口设备并建立连接。Windows API提供了 CreateFile 函数用于打开串口设备,并通过 CloseHandle 函数释放资源。
2.1.1 CreateFile函数的使用方法
CreateFile 函数是Windows API中用于创建或打开文件、设备或通信资源的核心函数。对于串口设备,其函数原型如下:
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
lpFileName:串口设备名,如"COM1"。dwDesiredAccess:访问权限,可设置为GENERIC_READ、GENERIC_WRITE或两者组合。dwShareMode:共享模式,串口通信中通常设为 0,表示独占访问。lpSecurityAttributes:安全属性,通常为NULL。dwCreationDisposition:文件创建方式,对于串口应设为OPEN_EXISTING。dwFlagsAndAttributes:标志和属性,通常设置为FILE_ATTRIBUTE_NORMAL。hTemplateFile:模板文件句柄,设为NULL。
示例代码:
HANDLE hSerial = CreateFile(
"COM1",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hSerial == INVALID_HANDLE_VALUE) {
// 错误处理
printf("Error opening serial port.\n");
}
代码分析:
CreateFile调用尝试打开名为COM1的串口设备。GENERIC_READ | GENERIC_WRITE表示该串口将同时用于读写。- 若返回值为
INVALID_HANDLE_VALUE,则表示打开失败,需调用GetLastError()进一步获取错误码。
2.1.2 COM端口路径的格式与获取
在Windows系统中,COM端口的名称格式为 COMx ,其中 x 为端口号,如 COM1 、 COM2 等。可以通过注册表或设备管理器获取当前系统中可用的COM端口列表。
例如,通过注册表读取串口设备列表的路径为:
HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM
遍历该键值可以获取所有已注册的串口设备名称。
2.1.3 关闭串口资源的CloseHandle函数
串口使用完毕后,应使用 CloseHandle 函数释放资源,防止资源泄露。函数原型如下:
BOOL CloseHandle(HANDLE hObject);
示例代码:
if (!CloseHandle(hSerial)) {
printf("Error closing serial port.\n");
}
说明:
CloseHandle成功返回TRUE,否则返回FALSE。- 在多线程环境中,必须确保所有对串口句柄的操作已完成后再调用
CloseHandle。
2.2 串口通信参数配置
串口通信需要配置波特率、数据位、停止位、校验方式等参数,这些参数通过 DCB 结构体和 SetCommState 函数进行设置。
2.2.1 DCB结构体详解
DCB (Device Control Block)结构体用于设置和获取串口通信的参数。其定义如下:
typedef struct _DCB {
UINT DCBlength;
DWORD BaudRate;
DWORD fBinary:1;
DWORD fParity:1;
DWORD fOutxCtsFlow:1;
DWORD fOutxDsrFlow:1;
DWORD fDtrControl:2;
DWORD fDsrSensitivity:1;
DWORD fTXContinueOnXoff:1;
DWORD fOutX:1;
DWORD fInX:1;
DWORD fErrorChar:1;
DWORD fNull:1;
DWORD fRtsControl:2;
DWORD fAbortOnError:1;
DWORD fDummy2:17;
WORD wReserved;
WORD XonLim;
WORD XoffLim;
BYTE ByteSize;
BYTE Parity;
BYTE StopBits;
char XonChar;
char XoffChar;
char ErrorChar;
char EofChar;
char EvtChar;
WORD wReserved1;
} DCB, *LPDCB;
常用字段说明:
| 字段名 | 含义说明 |
|---|---|
BaudRate |
波特率,如9600、115200等 |
ByteSize |
数据位数,一般为8 |
Parity |
校验方式,0=无校验,1=奇校验,2=偶校验 |
StopBits |
停止位,0=1位,1=1.5位,2=2位 |
2.2.2 SetCommState函数设置波特率与校验方式
SetCommState 函数用于将配置好的 DCB 结构体应用到串口设备中:
BOOL SetCommState(HANDLE hFile, LPDCB lpDCB);
示例代码:
DCB dcbSerialParams = {0};
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
if (!GetCommState(hSerial, &dcbSerialParams)) {
// 错误处理
}
dcbSerialParams.BaudRate = CBR_9600;
dcbSerialParams.ByteSize = 8;
dcbSerialParams.StopBits = ONESTOPBIT;
dcbSerialParams.Parity = NOPARITY;
if (!SetCommState(hSerial, &dcbSerialParams)) {
// 错误处理
}
代码分析:
GetCommState用于获取当前串口配置。- 修改
DCB结构体后,调用SetCommState应用新配置。
2.2.3 超时设置与GetCommTimeouts函数
串口通信中的超时设置通过 COMMTIMEOUTS 结构体实现。Windows API提供 GetCommTimeouts 和 SetCommTimeouts 函数用于获取和设置超时参数。
示例代码:
COMMTIMEOUTS timeouts = {0};
if (!GetCommTimeouts(hSerial, &timeouts)) {
// 错误处理
}
timeouts.ReadIntervalTimeout = 50;
timeouts.ReadTotalTimeoutConstant = 50;
timeouts.ReadTotalTimeoutMultiplier = 10;
timeouts.WriteTotalTimeoutConstant = 50;
timeouts.WriteTotalTimeoutMultiplier = 10;
if (!SetCommTimeouts(hSerial, &timeouts)) {
// 错误处理
}
参数说明:
| 参数名 | 含义说明 |
|---|---|
ReadIntervalTimeout |
相邻两个字符之间的最大时间间隔(毫秒) |
ReadTotalTimeoutConstant |
读操作的固定超时时间(毫秒) |
ReadTotalTimeoutMultiplier |
每个字符的超时时间(毫秒) |
WriteTotalTimeoutConstant |
写操作的固定超时时间(毫秒) |
WriteTotalTimeoutMultiplier |
每个字符的超时时间(毫秒) |
2.3 数据读写操作
串口通信的核心是数据的读写操作,Windows API提供了 ReadFile 和 WriteFile 函数实现同步或异步读写。
2.3.1 ReadFile函数实现数据接收
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
示例代码:
char buffer[256];
DWORD bytesRead;
if (!ReadFile(hSerial, buffer, sizeof(buffer), &bytesRead, NULL)) {
// 错误处理
}
buffer[bytesRead] = '\0';
printf("Received: %s\n", buffer);
说明:
- 如果串口设置为同步模式(非重叠I/O),最后一个参数设为
NULL。 bytesRead返回实际读取的字节数。
2.3.2 WriteFile函数实现数据发送
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
示例代码:
const char *msg = "Hello Serial!";
DWORD bytesWritten;
if (!WriteFile(hSerial, msg, strlen(msg), &bytesWritten, NULL)) {
// 错误处理
}
说明:
- 成功写入后,
bytesWritten包含实际写入的字节数。
2.3.3 同步与异步读写方式对比
| 特性 | 同步方式 | 异步方式 |
|---|---|---|
| 阻塞行为 | 是 | 否 |
| 线程占用 | 单线程可能阻塞 | 可配合线程或事件提高效率 |
| 编程复杂度 | 简单 | 复杂(需处理重叠结构和事件) |
| 适用场景 | 简单应用、调试 | 实时性要求高的应用、多线程程序 |
异步方式需配合 OVERLAPPED 结构体和事件对象使用,例如:
OVERLAPPED o = {0};
o.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
DWORD bytesRead;
if (!ReadFile(hSerial, buffer, sizeof(buffer), &bytesRead, &o)) {
if (GetLastError() == ERROR_IO_PENDING) {
WaitForSingleObject(o.hEvent, INFINITE);
GetOverlappedResult(hSerial, &o, &bytesRead, FALSE);
}
}
2.4 串口状态监控与错误处理
为了确保串口通信的稳定性和健壮性,必须对串口状态进行监控,并处理可能出现的错误。
2.4.1 GetCommState函数获取当前配置
GetCommState 函数可用于获取串口当前的配置信息:
BOOL GetCommState(HANDLE hFile, LPDCB lpDCB);
示例代码:
DCB currentDCB = {0};
currentDCB.DCBlength = sizeof(DCB);
if (!GetCommState(hSerial, ¤tDCB)) {
// 错误处理
}
应用场景:
- 调试时查看当前串口配置。
- 动态调整参数时作为参考。
2.4.2 GetLastError函数获取错误代码
当API调用失败时,可通过 GetLastError 获取错误代码:
DWORD error = GetLastError();
常见错误码:
| 错误码 | 含义 |
|---|---|
| ERROR_FILE_NOT_FOUND | 串口不存在 |
| ERROR_ACCESS_DENIED | 权限不足或被占用 |
| ERROR_INVALID_HANDLE | 句柄无效 |
| ERROR_IO_PENDING | 异步操作未完成 |
2.4.3 清除错误状态的ClearCommError函数
在串口通信中,错误可能发生在读写过程中。 ClearCommError 函数用于清除错误并获取当前串口状态。
BOOL ClearCommError(
HANDLE hFile,
LPDWORD lpErrors,
LPCOMSTAT lpStat
);
示例代码:
DWORD errors;
COMSTAT status;
if (!ClearCommError(hSerial, &errors, &status)) {
// 错误处理
}
if (errors != 0) {
// 处理错误
}
参数说明:
errors:输出错误码(如CE_BREAK、CE_FRAME、CE_OVERRUN等)。status:输出串口状态信息(如缓冲区字节数等)。
本章详细介绍了Windows API中串口通信的核心函数,包括设备打开与关闭、参数配置、数据读写以及错误处理机制。这些函数构成了Windows串口编程的基础,为后续的类封装和界面开发提供了底层支撑。下一章将在此基础上进一步封装串口通信功能,提升代码的可维护性和复用性。
3. 串口类封装设计与实现
在实际开发过程中,直接调用Windows API进行串口通信虽然功能强大,但代码结构松散、可维护性差。为了提升开发效率与代码的复用性,通常我们会将串口通信功能封装为一个类(如 CSerialPort )。本章将围绕串口类的设计与实现展开详细讨论,包括类结构的设计、初始化配置的封装、数据收发机制的实现,以及错误处理与日志记录的集成。
3.1 类结构设计的基本思路
3.1.1 CSerialPort类的功能定位
CSerialPort 类作为串口通信的封装核心,其主要功能包括:
- 打开和关闭串口设备
- 配置串口参数(波特率、校验位、数据位、停止位等)
- 发送和接收数据
- 错误处理与日志记录
- 支持多线程安全操作
该类的设计目标是将底层的Windows串口API操作隐藏,对外提供统一、简洁的接口函数,使得上层业务逻辑可以专注于数据处理,而不必关心底层通信细节。
3.1.2 成员变量与句柄管理
在类中定义以下关键成员变量:
| 成员变量名 | 类型 | 作用描述 |
|---|---|---|
m_hComPort |
HANDLE |
串口设备的句柄 |
m_bIsOpen |
bool |
标记串口是否已打开 |
m_dwBaudRate |
DWORD |
当前波特率 |
m_byteParity |
BYTE |
校验位设置 |
m_byteDataBits |
BYTE |
数据位设置 |
m_byteStopBits |
BYTE |
停止位设置 |
m_dwReadTimeout |
DWORD |
读取超时时间 |
m_dwWriteTimeout |
DWORD |
写入超时时间 |
句柄的生命周期管理是类设计中的重点。打开串口时通过 CreateFile 获取句柄,关闭时通过 CloseHandle 释放资源,避免资源泄露。
3.1.3 类接口函数的定义与封装
对外提供的主要接口函数如下:
| 接口函数名 | 参数说明 | 功能描述 |
|---|---|---|
OpenPort |
COM端口号、波特率、校验位、数据位、停止位等 | 打开指定串口并配置参数 |
ClosePort |
无 | 关闭当前串口 |
SendData |
数据缓冲区、长度 | 向串口发送数据 |
ReceiveData |
接收缓冲区、最大接收长度 | 从串口读取数据 |
SetTimeout |
读取/写入超时时间 | 设置通信超时参数 |
IsOpen |
无 | 返回串口是否处于打开状态 |
通过这些接口函数,可以实现对串口的全生命周期控制,同时屏蔽底层实现细节。
3.2 串口初始化与配置封装
3.2.1 初始化函数InitPort的设计
初始化函数 InitPort 主要负责设置串口的默认参数和基本结构体(如 DCB 和 COMMTIMEOUTS)。
bool CSerialPort::InitPort(DWORD baudRate, BYTE parity, BYTE dataBits, BYTE stopBits)
{
DCB dcb = {0};
dcb.DCBlength = sizeof(DCB);
if (!GetCommState(m_hComPort, &dcb)) {
return false;
}
dcb.BaudRate = baudRate;
dcb.Parity = parity;
dcb.ByteSize = dataBits;
dcb.StopBits = stopBits;
if (!SetCommState(m_hComPort, &dcb)) {
return false;
}
return true;
}
逐行解析:
- 第1~2行:定义
DCB结构体并初始化为0。 - 第3行:获取当前串口的状态配置。
- 第5~8行:设置波特率、校验位、数据位、停止位。
- 第10行:将新的配置应用到串口设备。
- 返回值表示是否设置成功。
3.2.2 设置波特率、数据位、停止位与校验位的函数封装
为了便于用户设置串口参数,可以提供独立的函数进行封装:
void CSerialPort::SetBaudRate(DWORD baudRate) {
m_dwBaudRate = baudRate;
}
void CSerialPort::SetParity(BYTE parity) {
m_byteParity = parity;
}
void CSerialPort::SetDataBits(BYTE dataBits) {
m_byteDataBits = dataBits;
}
void CSerialPort::SetStopBits(BYTE stopBits) {
m_byteStopBits = stopBits;
}
这些函数更新类的成员变量,供后续 OpenPort 调用时使用。
3.2.3 超时参数的动态设置方法
串口通信中,超时设置对于避免程序长时间阻塞至关重要。可以通过以下函数动态设置:
bool CSerialPort::SetTimeout(DWORD readTimeout, DWORD writeTimeout)
{
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = readTimeout;
timeouts.WriteTotalTimeoutConstant = writeTimeout;
if (!SetCommTimeouts(m_hComPort, &timeouts)) {
return false;
}
m_dwReadTimeout = readTimeout;
m_dwWriteTimeout = writeTimeout;
return true;
}
逻辑分析:
COMMTIMEOUTS结构用于设置读写超时。SetCommTimeouts将设置应用到串口。- 成功后更新类内部记录的超时值。
3.3 数据收发功能的封装
3.3.1 SendData函数实现数据发送
发送数据函数封装如下:
bool CSerialPort::SendData(const char* buffer, DWORD length)
{
DWORD dwBytesWritten = 0;
if (!WriteFile(m_hComPort, buffer, length, &dwBytesWritten, NULL)) {
return false;
}
return dwBytesWritten == length;
}
参数说明:
buffer:要发送的数据指针length:数据长度- 返回值:是否成功发送全部数据
逻辑分析:
- 使用
WriteFile函数将数据写入串口。 - 检查实际发送字节数是否与请求长度一致,确保数据完整。
3.3.2 ReceiveData函数实现数据接收
接收函数封装如下:
bool CSerialPort::ReceiveData(char* buffer, DWORD maxLength, DWORD& bytesRead)
{
if (!ReadFile(m_hComPort, buffer, maxLength, &bytesRead, NULL)) {
return false;
}
return true;
}
参数说明:
buffer:接收数据的缓冲区maxLength:缓冲区最大容量bytesRead:实际读取的数据长度- 返回值:是否成功读取数据
3.3.3 缓冲区管理与多线程处理
为了提高数据处理效率,建议采用双缓冲机制和多线程处理:
graph TD
A[主线程] --> B[启动读线程]
B --> C[持续调用ReadFile]
C --> D{是否有新数据?}
D -- 是 --> E[写入接收缓冲区]
D -- 否 --> F[等待下一次读取]
G[业务线程] --> H[从缓冲区读取并处理数据]
说明:
- 使用独立线程负责串口数据读取,避免阻塞主线程。
- 引入环形缓冲区(Circular Buffer)或队列管理接收数据。
- 采用互斥锁(Mutex)或临界区(CriticalSection)保护共享资源。
3.4 错误处理与日志记录集成
3.4.1 异常信息的封装与抛出
为统一错误处理机制,可以定义自定义异常类:
class SerialPortException : public std::runtime_error {
public:
explicit SerialPortException(const std::string& message)
: std::runtime_error(message) {}
};
在函数中抛出异常:
if (!SetCommState(m_hComPort, &dcb)) {
throw SerialPortException("Failed to set communication state.");
}
3.4.2 日志记录函数的调用机制
集成日志记录功能,可以使用第三方库如 spdlog 或 log4cpp ,也可以简单封装:
void CSerialPort::LogError(const std::string& msg)
{
std::ofstream logFile("serialport.log", std::ios_base::app);
if (logFile.is_open()) {
logFile << "[ERROR] " << msg << std::endl;
logFile.close();
}
}
3.4.3 类内部错误状态的维护
维护错误状态码和描述信息:
enum SerialPortError {
ERROR_NONE = 0,
ERROR_OPEN_FAILED,
ERROR_CONFIG_FAILED,
ERROR_READ_FAILED,
ERROR_WRITE_FAILED
};
SerialPortError m_lastError;
std::string m_lastErrorMsg;
void CSerialPort::SetLastError(SerialPortError code, const std::string& msg)
{
m_lastError = code;
m_lastErrorMsg = msg;
LogError(msg);
}
在调用失败时设置错误状态:
if (!GetCommState(m_hComPort, &dcb)) {
SetLastError(ERROR_CONFIG_FAILED, "Failed to get current port settings.");
return false;
}
通过本章的详细设计与实现,我们完成了一个功能完整、结构清晰、可维护性强的串口通信类 CSerialPort ,为后续基于面向对象方式的操作方法设计和MFC界面开发奠定了坚实基础。
4. 面向对象方式设计 Open、Close、Read、Write 方法
在串口通信程序设计中,采用面向对象的方式能够显著提升代码的可维护性、可扩展性与可重用性。本章将围绕串口操作的四个核心方法:Open(打开)、Close(关闭)、Read(读取)、Write(发送)展开详细设计与实现,重点探讨其在 C++ 面向对象封装中的实现逻辑与注意事项。
4.1 Open 方法的实现逻辑
Open 方法是串口通信的第一步,它负责打开指定的 COM 端口并完成基本的初始化工作。一个健壮的 Open 方法应包含参数检查、资源分配、错误码返回等关键流程。
4.1.1 打开串口的完整流程
串口的打开流程如下图所示:
graph TD
A[开始] --> B{端口名是否为空}
B -- 是 --> C[返回错误码: 端口名为空]
B -- 否 --> D[调用CreateFile函数打开串口]
D --> E{是否成功}
E -- 是 --> F[设置串口句柄]
E -- 否 --> G[返回错误码: 打开失败]
F --> H[设置默认通信参数]
H --> I[成功返回]
该流程确保了串口打开过程的健壮性与安全性。
4.1.2 参数检查与资源分配
在调用 CreateFile 前,必须对端口名称、打开模式等参数进行合法性检查:
bool CSerialPort::Open(const std::string& portName, DWORD baudRate) {
if (portName.empty()) {
m_lastError = ERROR_INVALID_PARAMETER;
return false;
}
std::wstring wPortName = std::wstring(portName.begin(), portName.end());
m_hSerial = CreateFileW(wPortName.c_str(),
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (m_hSerial == INVALID_HANDLE_VALUE) {
m_lastError = GetLastError();
return false;
}
// 初始化通信参数
DCB dcbSerialParams = {0};
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
if (!GetCommState(m_hSerial, &dcbSerialParams)) {
CloseHandle(m_hSerial);
m_lastError = GetLastError();
return false;
}
dcbSerialParams.BaudRate = baudRate;
dcbSerialParams.ByteSize = 8;
dcbSerialParams.StopBits = ONESTOPBIT;
dcbSerialParams.Parity = NOPARITY;
if (!SetCommState(m_hSerial, &dcbSerialParams)) {
CloseHandle(m_hSerial);
m_lastError = GetLastError();
return false;
}
return true;
}
代码逻辑分析:
- 参数检查 :
portName是否为空,防止非法参数传入。 - CreateFileW :使用宽字符版本打开串口设备,避免中文路径问题。
- GetCommState :获取当前串口配置,防止直接设置时参数丢失。
- SetCommState :应用新的通信参数(波特率、数据位等)。
- 错误处理 :若任一环节失败,关闭已打开的句柄并记录错误码。
4.1.3 错误码的返回机制
为了提高调用者的调试效率, Open 方法使用 m_lastError 成员变量保存错误码。调用者可以通过 GetLastError() 接口获取详细信息。
| 错误码 | 含义 |
|---|---|
| ERROR_INVALID_PARAMETER | 参数无效 |
| ERROR_FILE_NOT_FOUND | 串口不存在 |
| ERROR_ACCESS_DENIED | 串口被占用 |
| ERROR_INVALID_HANDLE | 句柄无效 |
4.2 Close 方法的设计与实现
Close 方法负责释放串口资源,确保系统资源不泄露,是串口生命周期的终点。
4.2.1 正确关闭串口的步骤
关闭串口的基本流程如下:
graph TD
A[开始] --> B{是否已打开}
B -- 否 --> C[直接返回]
B -- 是 --> D[调用PurgeComm清除缓冲区]
D --> E[调用CancelIo取消异步操作]
E --> F[调用CloseHandle关闭句柄]
F --> G[置空句柄]
4.2.2 清理内部资源与句柄释放
示例代码如下:
void CSerialPort::Close() {
if (m_hSerial != INVALID_HANDLE_VALUE) {
PurgeComm(m_hSerial, PURGE_RXABORT | PURGE_TXABORT | PURGE_RXCLEAR | PURGE_TXCLEAR);
CancelIo(m_hSerial); // 取消正在进行的异步操作
CloseHandle(m_hSerial);
m_hSerial = INVALID_HANDLE_VALUE;
}
}
代码逻辑分析:
- PurgeComm :清空接收与发送缓冲区,避免残留数据影响下一次通信。
- CancelIo :若使用了异步方式,需取消未完成的 I/O 操作。
- CloseHandle :释放串口句柄,避免资源泄露。
- 置空句柄 :便于后续判断串口状态。
4.2.3 多线程环境下的关闭处理
在多线程环境下,关闭串口前应确保其他线程不再使用串口资源:
std::mutex m_serialMutex;
void CSerialPort::Close() {
std::lock_guard<std::mutex> lock(m_serialMutex);
if (m_hSerial != INVALID_HANDLE_VALUE) {
PurgeComm(m_hSerial, PURGE_RXABORT | PURGE_TXABORT | PURGE_RXCLEAR | PURGE_TXCLEAR);
CancelIo(m_hSerial);
CloseHandle(m_hSerial);
m_hSerial = INVALID_HANDLE_VALUE;
}
}
通过加锁机制,确保线程安全。
4.3 Read 方法的数据读取机制
Read 方法用于从串口接收缓冲区读取数据,是数据通信的核心功能之一。
4.3.1 阻塞与非阻塞模式的选择
Windows 串口默认为阻塞模式,可以通过 SetCommTimeouts 设置超时:
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = 50;
timeouts.ReadTotalTimeoutConstant = 50;
timeouts.ReadTotalTimeoutMultiplier = 10;
SetCommTimeouts(m_hSerial, &timeouts);
- 阻塞模式 :等待数据到来后返回,适用于实时性要求高。
- 非阻塞模式 :立即返回当前缓冲区数据,适用于轮询机制。
4.3.2 读取缓冲区的管理
建议使用动态缓冲区管理:
std::vector<uint8_t> buffer(1024);
DWORD bytesRead = 0;
if (!ReadFile(m_hSerial, buffer.data(), buffer.size(), &bytesRead, NULL)) {
m_lastError = GetLastError();
return 0;
}
// 将读取到的数据加入接收队列
m_receiveQueue.insert(m_receiveQueue.end(), buffer.begin(), buffer.begin() + bytesRead);
- buffer :用于临时存储从串口读取的数据。
- bytesRead :记录实际读取的字节数。
- m_receiveQueue :用于存储接收到的完整数据帧。
4.3.3 读取超时处理逻辑
可以结合 WaitCommEvent 实现事件驱动读取:
DWORD dwEventMask = 0;
SetCommMask(m_hSerial, EV_RXCHAR); // 监听接收字符事件
WaitCommEvent(m_hSerial, &dwEventMask, NULL); // 阻塞等待
该方式可避免轮询浪费 CPU 资源。
4.4 Write 方法的数据发送机制
Write 方法用于向串口发送数据,其稳定性和性能直接影响通信质量。
4.4.1 发送数据的格式处理
发送前应对数据进行格式处理,例如文本转为 HEX:
std::vector<uint8_t> CSerialPort::FormatData(const std::string& input, bool isHex) {
std::vector<uint8_t> result;
if (isHex) {
std::istringstream hexStream(input);
std::string byteStr;
while (hexStream >> byteStr) {
uint8_t byte = static_cast<uint8_t>(std::stoul(byteStr, nullptr, 16));
result.push_back(byte);
}
} else {
result.assign(input.begin(), input.end());
}
return result;
}
参数说明:
- input :原始输入字符串。
- isHex :是否为十六进制格式。
4.4.2 写入失败的重试策略
为提高可靠性,可在失败时重试:
bool CSerialPort::WriteData(const std::vector<uint8_t>& data, int retryTimes) {
for (int i = 0; i < retryTimes; ++i) {
DWORD bytesWritten = 0;
if (WriteFile(m_hSerial, data.data(), data.size(), &bytesWritten, NULL)) {
return true;
}
DWORD error = GetLastError();
if (error == ERROR_IO_PENDING) {
// 异步写入处理逻辑
} else if (error == ERROR_ACCESS_DENIED) {
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return false;
}
- retryTimes :最大重试次数。
- ERROR_IO_PENDING :若为异步写入,需等待完成。
- sleep_for :每次失败后延迟再试,避免资源争抢。
4.4.3 发送完成回调机制
为实现异步通知,可引入回调函数:
using WriteCompleteCallback = std::function<void(bool success, DWORD bytesWritten)>;
bool CSerialPort::WriteAsync(const std::vector<uint8_t>& data, WriteCompleteCallback callback) {
DWORD bytesWritten = 0;
OVERLAPPED overlapped = {0};
overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (!WriteFile(m_hSerial, data.data(), data.size(), &bytesWritten, &overlapped)) {
if (GetLastError() == ERROR_IO_PENDING) {
WaitForSingleObject(overlapped.hEvent, INFINITE);
GetOverlappedResult(m_hSerial, &overlapped, &bytesWritten, FALSE);
CloseHandle(overlapped.hEvent);
callback(true, bytesWritten);
return true;
}
callback(false, 0);
return false;
}
callback(true, bytesWritten);
return true;
}
代码说明:
- OVERLAPPED :结构体用于异步 I/O。
- hEvent :用于等待异步操作完成。
- callback :用户自定义的回调函数,用于处理发送完成逻辑。
本章通过面向对象的方式详细实现了串口通信的 Open、Close、Read、Write 四个核心方法,并结合多线程、错误处理、回调机制等关键技术,构建了一个稳定、可扩展的串口通信类框架。下一章将继续深入探讨基于 MFC 的图形界面开发,实现串口通信工具的可视化操作。
5. MFC框架界面开发
MFC(Microsoft Foundation Classes)是Windows平台上广泛使用的C++类库,它封装了Windows API,简化了GUI应用程序的开发流程。在串口通信应用中,MFC不仅提供了强大的界面设计能力,还支持事件驱动的编程模型,使得开发者可以快速构建功能完整的串口通信界面。本章将围绕MFC框架,详细介绍串口通信应用的界面开发过程,涵盖项目结构、控件使用、事件响应及与串口类的交互机制。
5.1 MFC项目结构与界面布局
MFC应用程序通常基于文档/视图结构或对话框结构。对于串口通信这类相对简单的交互需求,基于对话框的应用程序(Dialog-based Application)是更合适的选择。
5.1.1 创建基于对话框的应用程序
- 打开 Visual Studio,选择“MFC Application”模板。
- 在向导中选择“Dialog based”选项。
- 设置项目名称,点击“Finish”生成初始项目结构。
生成的项目包括以下核心文件:
- YourApp.h/cpp :应用程序类,负责程序启动与退出。
- YourDlg.h/cpp :主对话框类,负责界面交互。
- resource.h :资源标识符定义。
- .rc 文件:资源描述文件,包含对话框、菜单、图标等。
5.1.2 界面控件的布局与属性设置
在资源视图中打开主对话框模板( IDD_YOURAPP_DIALOG ),使用工具箱拖拽控件进行界面设计。
常见控件包括:
- CComboBox :用于选择串口端口号。
- CButton :用于“打开”、“发送”等操作。
- CEdit :用于输入发送数据和显示接收数据。
- CStatic :用于显示标签、状态等信息。
- CListBox 或 CEdit 多行:用于显示接收数据。
布局建议:
- 左侧放置串口参数设置区域(COM端口、波特率等)。
- 中间区域用于发送和接收数据编辑框。
- 底部放置操作按钮和状态栏。
5.1.3 控件变量的绑定与更新
MFC通过类向导将控件与成员变量绑定。操作步骤如下:
- 在对话框资源上右键,选择“Add Variable”。
- 选择控件类型(如
CComboBox),指定变量名(如m_ComboPort)。 - 在类中生成对应的成员变量。
示例代码片段:
// YourDlg.h
class CYourDlg : public CDialogEx
{
...
protected:
CComboBox m_ComboPort; // COM端口下拉框
CEdit m_EditSend; // 发送数据输入框
CEdit m_EditRecv; // 接收数据显示框
};
绑定后可通过 UpdateData(TRUE) 获取控件内容,通过 UpdateData(FALSE) 更新控件显示。
5.2 常用控件使用详解
5.2.1 CComboBox控件用于端口选择
CComboBox 是一个下拉选择框,常用于选择串口号。
初始化示例代码:
// 在 OnInitDialog() 中添加
m_ComboPort.AddString(_T("COM1"));
m_ComboPort.AddString(_T("COM2"));
m_ComboPort.SetCurSel(0); // 默认选择第一个
获取当前选择项:
CString strPort;
m_ComboPort.GetWindowText(strPort);
5.2.2 CEdit控件用于数据输入与显示
CEdit 可用于输入发送内容或显示接收内容。
设置接收框为只读:
m_EditRecv.SetReadOnly();
清空内容:
m_EditRecv.SetWindowText(_T(""));
追加内容:
CString strRecv;
m_EditRecv.GetWindowText(strRecv);
strRecv += _T("Received Data\n");
m_EditRecv.SetWindowText(strRecv);
5.2.3 CButton控件用于功能触发
按钮控件用于触发打开串口、发送数据等操作。
添加按钮点击事件响应:
- 在对话框资源中双击按钮,VS会自动生成
OnBnClickedXXX()函数。 - 在函数中添加对应逻辑。
示例:
void CYourDlg::OnBnClickedOpenPort()
{
CString strPort;
m_ComboPort.GetWindowText(strPort);
if (m_serialPort.OpenPort(strPort)) {
AfxMessageBox(_T("串口打开成功"));
} else {
AfxMessageBox(_T("串口打开失败"));
}
}
5.3 界面事件与消息响应
MFC采用消息映射机制来响应用户操作,事件驱动是其核心。
5.3.1 按钮点击事件的响应函数
如上节所示,按钮点击事件通过 ON_BN_CLICKED 映射处理函数。
BEGIN_MESSAGE_MAP(CYourDlg, CDialogEx)
ON_BN_CLICKED(IDC_OPEN_PORT, &CYourDlg::OnBnClickedOpenPort)
END_MESSAGE_MAP()
5.3.2 组合框选择变化的响应
CComboBox 的选择变化事件可通过 ON_CBN_SELCHANGE 捕获。
添加映射:
ON_CBN_SELCHANGE(IDC_COMBO_PORT, &CYourDlg::OnCbnSelchangeComboPort)
处理函数示例:
void CYourDlg::OnCbnSelchangeComboPort()
{
CString strPort;
m_ComboPort.GetWindowText(strPort);
TRACE(_T("当前选择端口: %s\n"), strPort);
}
5.3.3 定时器消息的处理机制
定时器常用于定期检查串口状态或更新UI。
启动定时器:
SetTimer(1, 1000, NULL); // 每秒触发一次
响应函数:
void CYourDlg::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == 1) {
// 检查串口状态
if (m_serialPort.IsOpen()) {
m_StaticStatus.SetWindowText(_T("串口已连接"));
}
}
CDialogEx::OnTimer(nIDEvent);
}
5.4 界面与串口类的交互机制
为了实现串口类与界面类的分离与协作,需建立良好的通信机制。
5.4.1 UI线程与串口线程的通信
串口通信应运行在独立线程中,避免阻塞UI。
创建接收线程:
AfxBeginThread(ReceiveThreadProc, this);
线程函数原型:
UINT CYourDlg::ReceiveThreadProc(LPVOID pParam)
{
CYourDlg* pDlg = (CYourDlg*)pParam;
while (pDlg->m_serialPort.IsOpen()) {
CString strData = pDlg->m_serialPort.ReceiveData();
if (!strData.IsEmpty()) {
pDlg->UpdateReceiveDisplay(strData);
}
}
return 0;
}
更新UI需使用 PostMessage 避免跨线程访问:
pDlg->PostMessage(WM_UPDATE_RECEIVE, (WPARAM)(LPCTSTR)strData, 0);
在对话框类中定义消息处理:
#define WM_UPDATE_RECEIVE (WM_USER + 1)
BEGIN_MESSAGE_MAP(CYourDlg, CDialogEx)
ON_MESSAGE(WM_UPDATE_RECEIVE, OnUpdateReceive)
END_MESSAGE_MAP()
LRESULT CYourDlg::OnUpdateReceive(WPARAM wParam, LPARAM lParam)
{
CString* pData = (CString*)wParam;
m_EditRecv.SetWindowText(*pData);
return 0;
}
5.4.2 数据绑定与更新策略
数据更新策略应考虑性能与用户体验:
- 单次更新 :适用于低频次接收。
- 批量更新 :缓存接收数据,定时刷新。
- 滚动显示 :自动滚动接收框,保持最新内容可见。
示例:
void CYourDlg::UpdateReceiveDisplay(const CString& data)
{
CString current;
m_EditRecv.GetWindowText(current);
current += data + _T("\r\n");
m_EditRecv.SetWindowText(current);
// 自动滚动到底部
int len = m_EditRecv.GetWindowTextLength();
m_EditRecv.SetSel(len, len);
m_EditRecv.ReplaceSel(_T(""));
}
5.4.3 状态栏与提示信息的同步
使用 CStatusBarCtrl 控件显示串口状态、错误信息等。
初始化状态栏:
CStatusBarCtrl& statusBar = m_wndStatusBar.GetStatusBarCtrl();
statusBar.SetText(_T("就绪"), 0, 0);
更新状态信息:
void CYourDlg::UpdateStatusBar(const CString& status)
{
m_wndStatusBar.SetWindowText(status);
}
小结
本章详细讲解了如何在MFC框架下开发串口通信应用的图形界面。从项目结构搭建、控件使用、事件响应机制到界面与串口类的交互方式,全面覆盖了MFC开发串口通信界面的核心知识点。通过合理使用控件绑定、线程通信和消息机制,可以构建出响应迅速、界面友好的串口通信程序,为后续功能扩展打下坚实基础。
流程图:界面与串口类交互流程
graph TD
A[UI线程] --> B{操作触发?}
B -->|打开串口| C[调用串口Open方法]
B -->|发送数据| D[调用串口Write方法]
C --> E[串口线程启动]
E --> F[接收数据]
F --> G[PostMessage到UI线程]
G --> H[更新接收显示]
D --> I[发送完成]
H --> J[状态栏更新]
表格:常用MFC控件与功能对照表
| 控件类型 | 功能用途 | 常用方法 |
|---|---|---|
| CComboBox | 选择串口号、波特率等 | AddString , GetWindowText |
| CEdit | 输入发送数据、显示接收数据 | SetWindowText , GetWindowText |
| CButton | 触发操作 | OnClick 响应函数 |
| CStatusBarCtrl | 显示状态信息 | SetText |
| CListBox | 显示历史数据 | AddString , ResetContent |
6. 串口参数设置功能实现
串口通信的稳定性和准确性在很大程度上依赖于参数设置的正确性。在实际开发中,用户往往需要根据外部设备的通信协议灵活调整波特率、校验位、数据位、停止位等串口参数。本章将深入讲解串口参数设置功能的实现逻辑,包括界面设计、参数保存与读取机制、参数校验流程以及参数变化后的连接处理。我们将结合MFC框架与C++语言,以面向对象的方式实现参数设置模块的完整功能。
6.1 参数设置界面设计
6.1.1 波特率、校验位、数据位、停止位的选择控件
在MFC界面中,串口参数通常通过下拉框( CComboBox )进行选择。以下是参数选择控件的布局示意图:
graph TD
A[参数设置面板] --> B[波特率下拉框]
A --> C[校验位下拉框]
A --> D[数据位下拉框]
A --> E[停止位下拉框]
A --> F[刷新COM端口按钮]
示例代码:初始化参数选择控件
void CSerialConfigDlg::InitParameterCtrls()
{
// 初始化波特率
m_comboBaudRate.AddString(_T("1200"));
m_comboBaudRate.AddString(_T("2400"));
m_comboBaudRate.AddString(_T("4800"));
m_comboBaudRate.AddString(_T("9600"));
m_comboBaudRate.AddString(_T("19200"));
m_comboBaudRate.AddString(_T("38400"));
m_comboBaudRate.AddString(_T("57600"));
m_comboBaudRate.AddString(_T("115200"));
m_comboBaudRate.SetCurSel(3); // 默认选择9600
// 初始化校验位
m_comboParity.AddString(_T("None"));
m_comboParity.AddString(_T("Even"));
m_comboParity.AddString(_T("Odd"));
m_comboParity.SetCurSel(0); // 默认无校验
// 初始化数据位
m_comboDataBits.AddString(_T("5"));
m_comboDataBits.AddString(_T("6"));
m_comboDataBits.AddString(_T("7"));
m_comboDataBits.AddString(_T("8"));
m_comboDataBits.SetCurSel(3); // 默认8位
// 初始化停止位
m_comboStopBits.AddString(_T("1"));
m_comboStopBits.AddString(_T("1.5"));
m_comboStopBits.AddString(_T("2"));
m_comboStopBits.SetCurSel(0); // 默认1位
}
代码分析:
m_comboBaudRate.AddString():向下拉框添加波特率选项。SetCurSel():设置默认选中项。- 每个控件对应串口参数,如波特率(Baud Rate)、校验位(Parity)、数据位(Data Bits)、停止位(Stop Bits)。
6.1.2 自动检测可用COM端口功能
为了提高用户体验,程序应能自动检测当前系统中可用的串口端口。
示例代码:自动获取COM端口列表
void CSerialConfigDlg::RefreshCOMPorts()
{
m_comboPort.ResetContent();
for (int i = 1; i <= 256; ++i)
{
CString strPort;
strPort.Format(_T("COM%d"), i);
HANDLE hPort = CreateFile(strPort, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hPort != INVALID_HANDLE_VALUE)
{
m_comboPort.AddString(strPort);
CloseHandle(hPort);
}
}
m_comboPort.SetCurSel(0); // 默认选中第一个端口
}
代码说明:
- 遍历
COM1到COM256,尝试打开每个端口。 - 若成功打开,则该端口存在,加入到下拉框中。
- 最后关闭句柄以避免资源泄漏。
6.1.3 默认参数与用户自定义配置
程序应提供默认参数,同时允许用户自定义设置。默认参数通常包括:
| 参数 | 默认值 |
|---|---|
| 波特率 | 9600 |
| 校验位 | None |
| 数据位 | 8 |
| 停止位 | 1 |
用户修改后,应将参数保存到配置文件或注册表中,以便下次启动时自动加载。
6.2 参数保存与读取机制
6.2.1 使用注册表或配置文件保存参数
MFC支持使用 CWinApp::WriteProfileString() 和 GetProfileString() 来保存和读取应用程序配置。
示例代码:保存参数到注册表
void CSerialConfigDlg::SaveSettings()
{
CString strPort, strBaud, strParity, strData, strStop;
m_comboPort.GetWindowText(strPort);
m_comboBaudRate.GetWindowText(strBaud);
m_comboParity.GetWindowText(strParity);
m_comboDataBits.GetWindowText(strData);
m_comboStopBits.GetWindowText(strStop);
AfxGetApp()->WriteProfileString(_T("SerialPort"), _T("Port"), strPort);
AfxGetApp()->WriteProfileString(_T("SerialPort"), _T("BaudRate"), strBaud);
AfxGetApp()->WriteProfileString(_T("SerialPort"), _T("Parity"), strParity);
AfxGetApp()->WriteProfileString(_T("SerialPort"), _T("DataBits"), strData);
AfxGetApp()->WriteProfileString(_T("SerialPort"), _T("StopBits"), strStop);
}
示例代码:从注册表读取参数
void CSerialConfigDlg::LoadSettings()
{
CString strPort = AfxGetApp()->GetProfileString(_T("SerialPort"), _T("Port"), _T("COM1"));
CString strBaud = AfxGetApp()->GetProfileString(_T("SerialPort"), _T("BaudRate"), _T("9600"));
CString strParity = AfxGetApp()->GetProfileString(_T("SerialPort"), _T("Parity"), _T("None"));
CString strData = AfxGetApp()->GetProfileString(_T("SerialPort"), _T("DataBits"), _T("8"));
CString strStop = AfxGetApp()->GetProfileString(_T("SerialPort"), _T("StopBits"), _T("1"));
SetComboBoxText(m_comboPort, strPort);
SetComboBoxText(m_comboBaudRate, strBaud);
SetComboBoxText(m_comboParity, strParity);
SetComboBoxText(m_comboDataBits, strData);
SetComboBoxText(m_comboStopBits, strStop);
}
辅助函数:设置下拉框选中项
void CSerialConfigDlg::SetComboBoxText(CComboBox& combo, const CString& text)
{
int nIndex = combo.FindStringExact(0, text);
if (nIndex != CB_ERR)
combo.SetCurSel(nIndex);
}
6.2.2 应用启动时加载上次配置
在应用程序初始化阶段(如对话框的 OnInitDialog() )中调用 LoadSettings() 方法:
BOOL CSerialConfigDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
LoadSettings(); // 加载上次保存的串口参数
RefreshCOMPorts(); // 刷新COM端口列表
return TRUE;
}
6.2.3 用户修改参数的即时应用
当用户在界面上更改串口参数时,应立即应用到串口类中,或在点击“应用”按钮后更新。
示例代码:响应下拉框变更事件
void CSerialConfigDlg::OnCbnSelchangeComboBaudrate()
{
ApplyCurrentSettings(); // 应用当前参数到串口对象
}
6.3 参数校验与合法性检查
6.3.1 输入参数的合法性验证
在设置串口参数前,必须验证其合法性。例如,波特率必须是标准值,数据位应在 5~8 之间。
示例代码:参数合法性校验
bool CSerialConfigDlg::ValidateSettings()
{
CString strBaud, strData;
m_comboBaudRate.GetWindowText(strBaud);
m_comboDataBits.GetWindowText(strData);
int nBaud = _ttoi(strBaud);
int nData = _ttoi(strData);
if (nBaud <= 0 || nData < 5 || nData > 8)
{
AfxMessageBox(_T("波特率或数据位设置不合法!"));
return false;
}
return true;
}
6.3.2 错误提示与恢复机制
若参数无效,应弹出错误提示,并恢复为上一次有效设置。
示例代码:错误提示与恢复
void CSerialConfigDlg::OnBaudrateInvalid()
{
AfxMessageBox(_T("无效的波特率,请重新选择!"));
CString strLastValid = m_strLastValidBaud; // 保存的上次有效值
SetComboBoxText(m_comboBaudRate, strLastValid);
}
6.3.3 参数变化后的重新连接处理
如果串口已经打开,用户修改参数后需要重新连接设备。
示例代码:重新连接串口
void CSerialConfigDlg::ApplyCurrentSettings()
{
if (!ValidateSettings())
return;
if (m_pSerialPort->IsOpen())
{
m_pSerialPort->Close(); // 先关闭旧连接
}
SERIAL_CONFIG config;
GetSelectedConfig(config); // 获取当前选择的参数
if (!m_pSerialPort->Open(config))
{
AfxMessageBox(_T("串口连接失败,请检查参数设置!"));
}
}
代码说明:
- 如果串口已打开,则先关闭。
- 重新读取当前控件中的参数。
- 调用串口类的
Open()方法尝试连接。
小结
本章详细讲解了串口参数设置功能的实现,包括界面设计、参数保存与读取、参数校验与错误处理、以及参数变化后的连接更新机制。通过合理使用MFC控件、注册表存储机制以及面向对象的设计思想,我们实现了一个功能完善、用户体验良好的串口参数设置模块。这些功能为后续的数据收发打下坚实基础,也体现了软件开发中“参数驱动”与“状态管理”的核心理念。
7. 数据发送与接收功能开发
7.1 数据发送功能的实现
在串口通信中,数据发送是用户与设备交互的核心功能之一。为了满足不同场景下的通信需求,通常需要支持 文本发送 和 十六进制发送 两种模式。
7.1.1 文本与十六进制发送模式切换
在MFC界面中,可以通过单选按钮( CRadioButton )实现发送模式的切换。例如:
// 定义发送模式枚举
enum SendMode {
SEND_MODE_TEXT,
SEND_MODE_HEX
};
SendMode m_sendMode; // 当前发送模式
切换事件响应函数:
void CSerialPortDlg::OnBnClickedRadioText()
{
m_sendMode = SEND_MODE_TEXT;
}
void CSerialPortDlg::OnBnClickedRadioHex()
{
m_sendMode = SEND_MODE_HEX;
}
7.1.2 发送数据的格式转换与处理
根据当前模式将用户输入的字符串转换为字节数组。例如:
void CSerialPortDlg::SendData()
{
CString strInput;
GetDlgItemText(IDC_EDIT_SEND, strInput);
std::vector<BYTE> sendData;
if (m_sendMode == SEND_MODE_TEXT) {
sendData.assign(strInput.begin(), strInput.end());
}
else if (m_sendMode == SEND_MODE_HEX) {
// 假设输入格式为:AA BB CC
CString strTemp = strInput;
strTemp.Replace(_T(" "), _T(""));
for (int i = 0; i < strTemp.GetLength(); i += 2) {
CString byteStr = strTemp.Mid(i, 2);
BYTE byteVal = (BYTE)_tcstoul(byteStr, NULL, 16);
sendData.push_back(byteVal);
}
}
// 调用串口类的发送函数
m_serialPort.SendData(&sendData[0], sendData.size());
}
7.1.3 发送缓冲区的队列管理
为了避免频繁调用 WriteFile 导致阻塞,可以使用队列机制管理发送数据:
std::queue<std::vector<BYTE>> m_sendQueue;
CCriticalSection m_sendQueueLock;
void CSerialPortDlg::AddToSendQueue(const BYTE* pData, UINT length)
{
std::vector<BYTE> data(pData, pData + length);
m_sendQueueLock.Lock();
m_sendQueue.push(data);
m_sendQueueLock.Unlock();
}
UINT SendThreadProc(LPVOID pParam)
{
CSerialPortDlg* pDlg = (CSerialPortDlg*)pParam;
while (pDlg->m_bRunning) {
if (!pDlg->m_sendQueue.empty()) {
pDlg->m_sendQueueLock.Lock();
std::vector<BYTE> data = pDlg->m_sendQueue.front();
pDlg->m_sendQueue.pop();
pDlg->m_sendQueueLock.Unlock();
pDlg->m_serialPort.SendData(&data[0], data.size());
}
Sleep(10);
}
return 0;
}
7.2 数据接收功能的实现
数据接收是串口通信中最为关键的部分,需要兼顾实时性和稳定性。
7.2.1 实时接收数据的处理机制
使用多线程接收数据,避免阻塞UI线程:
UINT ReceiveThreadProc(LPVOID pParam)
{
CSerialPortDlg* pDlg = (CSerialPortDlg*)pParam;
BYTE buffer[1024];
DWORD bytesRead;
while (pDlg->m_bReceiving) {
if (pDlg->m_serialPort.ReadData(buffer, sizeof(buffer), &bytesRead)) {
if (bytesRead > 0) {
pDlg->ProcessReceivedData(buffer, bytesRead);
}
}
Sleep(10);
}
return 0;
}
7.2.2 接收数据显示格式的切换
与发送类似,接收数据也应支持文本和十六进制显示。可通过下拉框或单选按钮切换:
enum DisplayMode {
DISPLAY_MODE_TEXT,
DISPLAY_MODE_HEX
};
DisplayMode m_displayMode;
void CSerialPortDlg::SetDisplayMode(DisplayMode mode)
{
m_displayMode = mode;
UpdateReceiveDisplay();
}
7.2.3 接收缓冲区的溢出处理
接收缓冲区应设置最大容量,超过则自动清除旧数据:
std::deque<BYTE> m_receiveBuffer;
const size_t MAX_BUFFER_SIZE = 1024 * 1024; // 1MB
void CSerialPortDlg::ProcessReceivedData(const BYTE* pData, size_t length)
{
m_receiveBufferLock.Lock();
for (size_t i = 0; i < length; ++i) {
m_receiveBuffer.push_back(pData[i]);
}
// 控制缓冲区大小
while (m_receiveBuffer.size() > MAX_BUFFER_SIZE) {
m_receiveBuffer.pop_front();
}
m_receiveBufferLock.Unlock();
UpdateReceiveDisplay();
}
7.3 接收数据的解析与展示
7.3.1 数据帧的识别与拆分
若通信协议中有固定帧头、帧尾(如 STX/ETX),可据此拆分数据帧:
const BYTE STX = 0x02;
const BYTE ETX = 0x03;
void CSerialPortDlg::ParseReceivedData()
{
m_receiveBufferLock.Lock();
std::vector<BYTE> frame;
while (m_receiveBuffer.size() >= 2) {
if (m_receiveBuffer[0] == STX) {
size_t etxPos = 0;
for (etxPos = 1; etxPos < m_receiveBuffer.size(); ++etxPos) {
if (m_receiveBuffer[etxPos] == ETX) break;
}
if (etxPos < m_receiveBuffer.size()) {
frame.assign(m_receiveBuffer.begin() + 1, m_receiveBuffer.begin() + etxPos);
m_receiveBuffer.erase(m_receiveBuffer.begin(), m_receiveBuffer.begin() + etxPos + 1);
HandleDataFrame(frame);
} else {
break; // 未找到帧尾
}
} else {
m_receiveBuffer.pop_front(); // 丢弃无效数据
}
}
m_receiveBufferLock.Unlock();
}
7.3.2 接收数据的文本与十六进制展示
根据当前显示模式更新编辑框内容:
void CSerialPortDlg::UpdateReceiveDisplay()
{
CString strDisplay;
m_receiveBufferLock.Lock();
if (m_displayMode == DISPLAY_MODE_TEXT) {
for (BYTE b : m_receiveBuffer) {
strDisplay += (TCHAR)b;
}
} else {
for (BYTE b : m_receiveBuffer) {
strDisplay.AppendFormat(_T("%02X "), b);
}
}
m_receiveBufferLock.Unlock();
SetDlgItemText(IDC_EDIT_RECEIVE, strDisplay);
}
7.3.3 接收窗口的自动滚动与历史记录
设置编辑框为只读并自动滚动到底部:
CEdit* pEdit = (CEdit*)GetDlgItem(IDC_EDIT_RECEIVE);
pEdit->SetSel(-1, -1);
pEdit->ReplaceSel(strDisplay + _T("\r\n"));
历史记录可保存在 std::list<CString> 中,便于回溯查看。
7.4 收发功能的性能优化
7.4.1 高效的内存管理策略
使用 std::deque 或 std::vector 管理接收缓冲区,避免频繁 new/delete 操作。使用对象池管理帧对象,减少内存碎片。
7.4.2 数据处理线程与UI线程分离
使用 PostMessage 或 SendMessage 在子线程与UI线程间通信:
PostMessage(WM_UPDATE_RECEIVE_DISPLAY, 0, 0);
在消息映射中添加:
ON_MESSAGE(WM_UPDATE_RECEIVE_DISPLAY, OnUpdateReceiveDisplay)
响应函数:
LRESULT CSerialPortDlg::OnUpdateReceiveDisplay(WPARAM wParam, LPARAM lParam)
{
UpdateReceiveDisplay();
return 0;
}
7.4.3 降低CPU占用与资源消耗技巧
- 使用
Sleep()或WaitForSingleObject控制轮询频率。 - 使用
PurgeComm清除无用数据。 - 合理设置串口读写超时参数,避免阻塞:
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutConstant = 0;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 50;
timeouts.WriteTotalTimeoutMultiplier = 10;
SetCommTimeouts(m_hComPort, &timeouts);
本章通过从数据发送到接收、再到解析展示的完整流程,构建了串口通信中数据交互的核心机制。下一章节将继续深入探讨错误处理与日志记录机制,以增强系统的健壮性。
简介:串口通信在嵌入式系统和设备控制中具有重要作用。本文介绍如何使用Visual C++和Windows API封装类开发串口调试程序。通过面向对象的设计方式封装串口操作类,并结合MFC框架构建用户界面,实现串口选择、参数设置、数据收发、错误处理等功能。文章还讲解了日志记录与定时器机制,提升调试效率与用户体验。适合初学者和工程实践者掌握串口通信开发核心技术。
更多推荐



所有评论(0)