本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:串口通信在嵌入式系统和设备控制中具有重要作用。本文介绍如何使用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");
}
代码分析:
  1. CreateFile 调用尝试打开名为 COM1 的串口设备。
  2. GENERIC_READ | GENERIC_WRITE 表示该串口将同时用于读写。
  3. 若返回值为 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)) {
    // 错误处理
}
代码分析:
  1. GetCommState 用于获取当前串口配置。
  2. 修改 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, &currentDCB)) {
    // 错误处理
}
应用场景:
  • 调试时查看当前串口配置。
  • 动态调整参数时作为参考。

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 创建基于对话框的应用程序

  1. 打开 Visual Studio,选择“MFC Application”模板。
  2. 在向导中选择“Dialog based”选项。
  3. 设置项目名称,点击“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通过类向导将控件与成员变量绑定。操作步骤如下:

  1. 在对话框资源上右键,选择“Add Variable”。
  2. 选择控件类型(如 CComboBox ),指定变量名(如 m_ComboPort )。
  3. 在类中生成对应的成员变量。

示例代码片段:

// 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控件用于功能触发

按钮控件用于触发打开串口、发送数据等操作。

添加按钮点击事件响应:

  1. 在对话框资源中双击按钮,VS会自动生成 OnBnClickedXXX() 函数。
  2. 在函数中添加对应逻辑。

示例:

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);

本章通过从数据发送到接收、再到解析展示的完整流程,构建了串口通信中数据交互的核心机制。下一章节将继续深入探讨错误处理与日志记录机制,以增强系统的健壮性。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:串口通信在嵌入式系统和设备控制中具有重要作用。本文介绍如何使用Visual C++和Windows API封装类开发串口调试程序。通过面向对象的设计方式封装串口操作类,并结合MFC框架构建用户界面,实现串口选择、参数设置、数据收发、错误处理等功能。文章还讲解了日志记录与定时器机制,提升调试效率与用户体验。适合初学者和工程实践者掌握串口通信开发核心技术。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐