下面我将从 基本概念、核心函数、完整示例、使用步骤和常见问题 五个方面进行详细说明。

(图片来源网络,侵删)
基本概念
在 Windows 中,串口(COM口)被当作一种特殊的 文件 来处理,我们可以使用文件操作的方式来打开、读写和关闭串口。
- 句柄:当你成功打开一个串口(或文件)后,Windows 会返回一个唯一的标识符,称为“句柄”,后续的所有操作(如读写、关闭)都通过这个句柄来进行。
- 同步与异步:
- 同步 I/O:调用
ReadFile或WriteFile函数后,程序会 阻塞(即暂停执行),直到操作完成或超时,这是最简单的方式,适合初学者。 - 异步 I/O (Overlapped I/O):调用
ReadFile或WriteFile后,函数会立即返回,程序可以继续执行其他任务,当 I/O 操作完成时,系统会通过事件或回调通知程序,这种方式更高效,适合复杂的、需要同时处理多个任务的程序。
- 同步 I/O:调用
本教程主要介绍 同步 I/O,因为它更易于理解和实现。
核心函数介绍
a. 打开串口:CreateFile
这是所有操作的第一步,用于获取串口的句柄。
HANDLE CreateFile( LPCSTR lpFileName, // 串口名称,如 "COM1" DWORD dwDesiredAccess, // 访问模式:读、写或读写 DWORD dwShareMode, // 共享模式,串口通常为0 LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性,通常为NULL DWORD dwCreationDisposition, // 创建方式,必须为 OPEN_EXISTING DWORD dwFlagsAndAttributes, // 文件属性,通常为0 HANDLE hTemplateFile // 模板文件,通常为NULL );
参数解释:

(图片来源网络,侵删)
lpFileName: 串口名称,格式为"COMX","COM1","COM3"。dwDesiredAccess:GENERIC_READ: 只读。GENERIC_WRITE: 只写。GENERIC_READ | GENERIC_WRITE: 读写。
dwShareMode: 串口不能共享,所以必须设为0。dwCreationDisposition: 对于已存在的设备(如串口),必须设为OPEN_EXISTING。- 返回值:
- 成功:返回一个有效的
HANDLE句柄。 - 失败:返回
INVALID_HANDLE_VALUE,你可以调用GetLastError()获取具体的错误码。
- 成功:返回一个有效的
b. 配置串口:SetupComm 和 DCB / BuildCommDCB
串口通信前必须配置其参数,如波特率、数据位、停止位、校验位等,这通常分两步:
- 分配缓冲区:
SetupComm - 设置参数:通过
DCB结构体和BuildCommDCB/SetCommState函数。
DCB 结构体
DCB (Device Control Block) 是一个结构体,包含了串口的所有配置信息。
typedef struct _DCB {
DWORD DCBlength; // 结构体大小,使用前需设置
DWORD BaudRate; // 波特率,如 CBR_9600
DWORD fBinary; // 二进制模式,必须为TRUE
DWORD fParity; // 启用奇偶校验
BYTE Parity; // 校验方式 (EVENPARITY, ODDPARITY, NOPARITY等)
BYTE StopBits; // 停止位 (ONESTOPBIT, ONE5STOPBITS, TWOSTOPBITS)
BYTE ByteSize; // 数据位 (5, 6, 7, 8)
char XonLim; // ...
char XoffLim; // ...
char XonChar; // ...
char XoffChar; // ...
char EofChar; // ...
char EvtChar; // ...
WORD wReserved; // 保留,必须为0
DWORD fOutxCtsFlow; // ...
DWORD fOutxDsrFlow; // ...
DWORD fDtrControl; // ...
DWORD fDsrSensitivity; // ...
DWORD fTXContinueOnXoff; // ...
DWORD fOutX; // ...
DWORD fInX; // ...
DWORD fErrorChar; // ...
DWORD fNull; // ...
DWORD fRtsControl; // ...
DWORD fAbortOnError; // ...
DWORD fDummy2; // 保留,必须为0
CTSRTSCTS fDummy; // ...
} DCB, *LPDCB;
BuildCommDCB 和 SetCommState
BuildCommDCB: 可以从一个字符串(如 "9600,n,8,1")轻松填充DCB结构体,非常方便。SetCommState: 将配置好的DCB结构体应用到串口上。
// 1. 定义并初始化 DCB 结构体
DCB dcb;
SecureZeroMemory(&dcb, sizeof(dcb)); // 清零结构体
dcb.DCBlength = sizeof(dcb);
// 方法一:使用字符串配置 (推荐)
if (!BuildCommDCB("9600,n,8,1", &dcb)) {
// 处理错误
}
// 方法二:手动设置每个成员
// dcb.BaudRate = CBR_9600;
// dcb.ByteSize = 8;
// dcb.Parity = NOPARITY;
// dcb.StopBits = ONESTOPBIT;
// 2. 应用配置到串口
if (!SetCommState(hSerialPort, &dcb)) {
// 处理错误
}
c. 超时设置:COMMTIMEOUTS
为了防止 ReadFile 无限等待(如果设备没有发送数据),必须设置超时。
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout; // 读取字符间的最大超时时间
DWORD ReadTotalTimeoutMultiplier; // 总读取超时 = 读取字节数 * 该值
DWORD ReadTotalTimeoutConstant; // 总读取超时 = 读取字节数 * Multiplier + 该值
DWORD WriteTotalTimeoutMultiplier; // 总写入超时 = 写入字节数 * 该值
DWORD WriteTotalTimeoutConstant; // 总写入超时 = 写入字节数 * Multiplier + 该值
} COMMTIMEOUTS, *LPCOMMTIMEOUTS;
对于同步读取,一个简单有效的设置是:

(图片来源网络,侵删)
ReadIntervalTimeout:MAXDWORD(0xFFFFFFFF)ReadTotalTimeoutMultiplier: 0ReadTotalTimeoutConstant: 50 (毫秒)
这种设置的含义是:只要有一个字符到达,ReadFile 就会立即返回,如果没有任何字符到达,则等待 50 毫秒后超时返回。
COMMTIMEOUTS timeouts; timeouts.ReadIntervalTimeout = MAXDWORD; timeouts.ReadTotalTimeoutMultiplier = 0; timeouts.ReadTotalTimeoutConstant = 50; SetCommTimeouts(hSerialPort, &timeouts);
d. 读写串口:ReadFile / WriteFile
这两个函数和文件读写函数非常相似。
ReadFile
BOOL ReadFile( HANDLE hFile, // 串口句柄 LPVOID lpBuffer, // 存放读取数据的缓冲区 DWORD nNumberOfBytesToRead, // 要读取的字节数 LPDWORD lpNumberOfBytesRead, // 实际读取的字节数 (输出参数) LPOVERLAPPED lpOverlapped // 异步I/O,同步模式设为NULL );
WriteFile
BOOL WriteFile( HANDLE hFile, // 串口句柄 LPCSTR lpBuffer, // 要写入数据的缓冲区 DWORD nNumberOfBytesToWrite,// 要写入的字节数 LPDWORD lpNumberOfBytesWritten, // 实际写入的字节数 (输出参数) LPOVERLAPPED lpOverlapped // 异步I/O,同步模式设为NULL );
e. 关闭串口:CloseHandle
操作完成后,必须关闭串口句柄,释放系统资源。
BOOL CloseHandle(HANDLE hObject);
完整示例代码 (同步模式)
这个示例程序会打开 COM3,配置为 9600 波特率,8数据位,无校验,1停止位,然后循环等待用户从键盘输入并发送,同时循环读取串口数据并打印到屏幕上。
#include <windows.h>
#include <stdio.h>
int main() {
HANDLE hSerial;
DCB dcbSerialParams = { 0 };
COMMTIMEOUTS timeouts = { 0 };
char serialPort[] = "COM3"; // 修改为你的串口号
char writeBuffer[1024];
char readBuffer[1024];
DWORD bytesRead, bytesWritten;
// 1. 打开串口
hSerial = CreateFile(serialPort,
GENERIC_READ | GENERIC_WRITE,
0,
0,
OPEN_EXISTING,
0, // 同步模式,不使用 FILE_FLAG_OVERLAPPED
0);
if (hSerial == INVALID_HANDLE_VALUE) {
printf("错误:无法打开串口 %s,错误码: %d\n", serialPort, GetLastError());
return 1;
}
printf("成功打开串口 %s\n", serialPort);
// 2. 获取当前的串口配置
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
if (!GetCommState(hSerial, &dcbSerialParams)) {
printf("错误:获取串口配置失败,错误码: %d\n", GetLastError());
CloseHandle(hSerial);
return 1;
}
// 3. 配置串口参数
dcbSerialParams.BaudRate = CBR_9600; // 波特率 9600
dcbSerialParams.ByteSize = 8; // 数据位 8
dcbSerialParams.StopBits = ONESTOPBIT; // 停止位 1
dcbSerialParams.Parity = NOPARITY; // 无校验
// 应用新的配置
if (!SetCommState(hSerial, &dcbSerialParams)) {
printf("错误:设置串口配置失败,错误码: %d\n", GetLastError());
CloseHandle(hSerial);
return 1;
}
printf("串口配置已设置: 9600, 8N1\n");
// 4. 设置超时
timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutConstant = 50;
timeouts.ReadTotalTimeoutMultiplier = 0;
if (!SetCommTimeouts(hSerial, &timeouts)) {
printf("错误:设置串口超时失败,错误码: %d\n", GetLastError());
CloseHandle(hSerial);
return 1;
}
// 5. 读写循环
printf("开始通信... (输入 'exit' 退出)\n");
while (1) {
// 发送数据
printf("请输入要发送的数据: ");
fgets(writeBuffer, sizeof(writeBuffer), stdin);
if (strncmp(writeBuffer, "exit", 4) == 0) {
break;
}
if (!WriteFile(hSerial, writeBuffer, strlen(writeBuffer), &bytesWritten, NULL)) {
printf("错误:写入数据失败,错误码: %d\n", GetLastError());
break;
}
printf("已发送 %d 字节\n", bytesWritten);
// 读取数据
if (!ReadFile(hSerial, readBuffer, sizeof(readBuffer) - 1, &bytesRead, NULL)) {
// 如果错误是 "No data waiting",只是没有数据到达,不算严重错误
if (GetLastError() != ERROR_IO_PENDING) {
printf("错误:读取数据失败,错误码: %d\n", GetLastError());
// break; // 可以选择退出或继续
}
} else {
if (bytesRead > 0) {
readBuffer[bytesRead] = '\0'; // 确保字符串正确终止
printf("收到 %d 字节: %s", bytesRead, readBuffer);
}
}
}
// 6. 关闭串口
CloseHandle(hSerial);
printf("串口已关闭,程序退出,\n");
return 0;
}
使用步骤总结
- 包含头文件:
#include <windows.h>。 - 定义串口名:
char port[] = "COMX";。 CreateFile:打开串口,获取句柄。GetCommState:获取当前串口配置到DCB结构体。- 修改
DCB:根据需要修改波特率、数据位等参数。 SetCommState:将修改后的配置应用到串口。SetCommTimeouts:设置合理的读取超时。WriteFile:向串口写入数据。ReadFile:从串口读取数据。CloseHandle:程序结束前,关闭串口句柄。
常见问题与解决方法
-
CreateFile失败,错误码 5 (ERROR_ACCESS_DENIED)- 原因:串口正被其他程序(如串口调试助手、你的程序上次运行未关闭)占用。
- 解决:关闭所有可能使用该串口的程序。
-
CreateFile失败,错误码 2 (ERROR_FILE_NOT_FOUND)- 原因:指定的串口名称不存在(你的电脑没有 COM3 口)。
- 解决:在“设备管理器”中查看你的电脑有哪些可用的 COM 口。
-
ReadFile一直阻塞,不返回- 原因:没有设置超时,或者设备确实没有发送任何数据。
- 解决:使用
SetCommTimeouts设置超时,如果设置了超时但仍不返回,请检查设备是否正常工作并已连接。
-
ReadFile返回成功,但bytesRead为 0- 原因:在超时时间内没有收到任何数据。
- 解决:这是正常现象,只需继续下一次读取即可。
-
发送或接收的数据不正确(乱码、数据丢失)
- 原因:双方的串口参数(波特率、数据位、停止位、校验位)不一致。
- 解决:确保你的 C 程序和另一端设备(单片机、另一台电脑等)的串口配置完全相同。
-
如何枚举所有可用的串口?
- Windows API 没有直接提供枚举串口的函数,通常的变通方法是:
- 从 "COM1" 到 "COM256" 循环调用
CreateFile。 CreateFile返回的不是INVALID_HANDLE_VALUE,则说明该串口存在。- 立即调用
CloseHandle关闭它,以避免占用。 - 或者,可以通过查询注册表
HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM来获取已存在的串口名。
- 从 "COM1" 到 "COM256" 循环调用
- Windows API 没有直接提供枚举串口的函数,通常的变通方法是:
