目录
- 核心概念
- 什么是串口?
- Windows 串口编程 API (Win32 API)
- 通信流程概述
- 详细步骤与代码示例
- 第 1 步:包含头文件和链接库
- 第 2 步:打开串口
- 第 3 步:配置串口参数
- 第 4 步:读写串口
- 第 5 步:关闭串口
- 完整示例代码 (发送与接收)
- 进阶话题
- 异步 I/O (Overlapped I/O) - 实现非阻塞通信
- 使用事件驱动方式
- 错误处理
- 常见问题与解决方案
- 替代方案:第三方库
核心概念
什么是串口?
串口(Serial Port)是一种按位(bit)进行数据传输的通信接口,在物理上,它通常是一个 DB-9 或 DB-25 的连接器,在现代计算机上,物理串口已逐渐被 USB 取代,但通过 USB 转串口适配器(如 CH340, FT232, CP2102 等),我们仍然可以方便地使用串口进行设备通信(如单片机、PLC、GPS 模块等)。

在 Windows 中,串口被当作一种特殊的文件来处理,叫做“设备文件”,它的命名格式通常是 COMx,x 是端口号,COM1, COM3 等。
Windows 串口编程 API (Win32 API)
Windows 提供了一套完整的 Win32 API 来操作串口,这些 API 主要包含在 windows.h 和 winbase.h 中,核心 API 包括:
CreateFile: 用于“打开”串口设备,获取一个句柄,就像打开文件一样。GetCommState: 获取当前串口的配置参数。SetCommState: 设置串口的配置参数,如波特率、数据位、停止位、校验位等。BuildCommDCB: 更方便地构建设备控制块,用于配置串口。SetupComm: 设置串口输入/输出缓冲区的大小。ReadFile: 从串口读取数据。WriteFile: 向串口写入数据。CloseHandle: 关闭串口句柄。ClearCommError: 获取并清除串口错误状态。PurgeComm: 清空串口输入/输出缓冲区。
一个典型的串口通信程序流程如下:
- 打开串口: 使用
CreateFile函数获取串口句柄。 - 配置串口: 使用
GetCommState获取当前配置,修改后用SetCommState设置新的配置(波特率、数据位、停止位、校验位等)。 - 设置超时: 使用
COMMTIMEOUTS结构体设置读写超时,防止程序永久阻塞。 - 读写数据:
- 同步模式: 直接调用
ReadFile和WriteFile,程序会一直等待,直到数据被成功读写或超时。 - 异步模式 (Overlapped): 使用
ReadFile/WriteFile配合OVERLAPPED结构体,实现非阻塞 I/O,程序可以同时做其他事情。
- 同步模式: 直接调用
- 关闭串口: 通信结束后,使用
CloseHandle关闭串口句柄,释放资源。
详细步骤与代码示例
下面我们来实现一个最简单的同步串口通信程序:配置一个串口,向它发送一串数据,然后读取返回的数据(假设对方设备会回显)。

第 1 步:包含头文件和链接库
在代码文件开头,你需要包含必要的头文件,由于 CreateFile 等函数在 windows.h 中,它已经包含了大部分所需定义。
#include <windows.h> #include <stdio.h> // 用于 printf
在 Visual Studio 中,这些库通常默认链接,如果你使用其他编译器(如 MinGW),可能需要手动链接 kernel32.lib,在编译命令中添加 -lkernel32。
第 2 步:打开串口
使用 CreateFile 函数,关键在于它的参数:
lpFileName: 串口名称,如COM3。dwDesiredAccess:GENERIC_READ | GENERIC_WRITE,表示既读又写。dwShareMode:0,串口不能被共享。lpSecurityAttributes: 通常为NULL。dwCreationDisposition:OPEN_EXISTING,因为串口设备已经存在。dwFlagsAndAttributes:FILE_ATTRIBUTE_NORMAL,对于同步 I/O,这个值通常就够了。hTemplateFile:NULL。
HANDLE hSerial = CreateFile(
"COM3", // 串口名
GENERIC_READ | GENERIC_WRITE, // 读写权限
0, // 不共享
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有设备
0, // 同步I/O
NULL // 无模板文件
);
if (hSerial == INVALID_HANDLE_VALUE) {
// 处理错误
DWORD error = GetLastError();
printf("Error opening serial port: %d\n", error);
// 错误码 2 表示找不到设备
return 1;
}
printf("Serial port COM3 opened successfully.\n");
第 3 步:配置串口
这是最关键的一步,我们需要配置波特率、数据位、停止位、校验位等。

-
获取当前配置: 定义一个
DCB(Device Control Block) 结构体,并用GetCommState填充它。 -
修改配置: 修改
DCB结构体中的成员,一个更简单的方法是使用BuildCommDCB函数,它可以直接从字符串(如 "9600,n,8,1")构建DCB结构体。 -
应用新配置: 使用
SetCommState将修改后的DCB结构体应用到串口。
DCB dcbSerialParams = { 0 };
// 获取当前DCB配置
if (!GetCommState(hSerial, &dcbSerialParams)) {
printf("Error getting device state: %d\n", GetLastError());
CloseHandle(hSerial);
return 1;
}
dcbSerialParams.BaudRate = CBR_9600; // 波特率 9600
dcbSerialParams.ByteSize = 8; // 数据位 8
dcbSerialParams.StopBits = ONESTOPBIT; // 停止位 1
dcbSerialParams.Parity = NOPARITY; // 无校验
// 应用新的DCB配置
if (!SetCommState(hSerial, &dcbSerialParams)) {
printf("Error setting device state: %d\n", GetLastError());
CloseHandle(hSerial);
return 1;
}
printf("Serial port configured successfully.\n");
第 4 步:设置超时
如果不设置超时,ReadFile 函数可能会无限期地等待数据,导致程序“卡死”,我们需要配置 COMMTIMEOUTS 结构体。
COMMTIMEOUTS timeouts = { 0 };
timeouts.ReadIntervalTimeout = 50; // 最大字符间间隔 (ms)
timeouts.ReadTotalTimeoutConstant = 50; // 总读取超时常数 (ms)
timeouts.ReadTotalTimeoutMultiplier = 10; // 总读取超时乘数
timeouts.WriteTotalTimeoutConstant = 50; // 总写入超时常数 (ms)
timeouts.WriteTotalTimeoutMultiplier = 10; // 总写入超时乘数
if (!SetCommTimeouts(hSerial, &timeouts)) {
printf("Error setting timeouts: %d\n", GetLastError());
CloseHandle(hSerial);
return 1;
}
printf("Timeouts set successfully.\n");
第 5 步:读写串口
现在可以开始读写数据了。
写入数据:
char dataToSend[] = "Hello, Serial Port!\r\n";
DWORD bytesWritten;
if (!WriteFile(hSerial, dataToSend, (DWORD)strlen(dataToSend), &bytesWritten, NULL)) {
printf("Error writing to serial port: %d\n", GetLastError());
} else {
printf("Successfully wrote %d bytes.\n", bytesWritten);
}
读取数据:
char buffer[1024];
DWORD bytesRead;
if (!ReadFile(hSerial, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) {
printf("Error reading from serial port: %d\n", GetLastError());
} else {
buffer[bytesRead] = '\0'; // 确保字符串以 null
printf("Successfully read %d bytes: %s\n", bytesRead, buffer);
}
第 6 步:关闭串口
操作完成后,必须关闭句柄,否则会造成资源泄露。
CloseHandle(hSerial);
printf("Serial port closed.\n");
完整示例代码 (发送与接收)
这是一个将上述步骤整合起来的完整控制台程序。
#include <windows.h>
#include <stdio.h>
int main() {
HANDLE hSerial;
DCB dcbSerialParams = { 0 };
COMMTIMEOUTS timeouts = { 0 };
char dataToSend[] = "AT\r\n"; // 常见的AT指令,用于测试调制解调器
char buffer[1024] = { 0 };
DWORD bytesWritten, bytesRead;
// --- 1. 打开串口 ---
hSerial = CreateFile(
"COM3", // 请根据你的串口修改
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hSerial == INVALID_HANDLE_VALUE) {
printf("Error opening serial port. Error code: %d\n", GetLastError());
return 1;
}
// --- 2. 配置串口 ---
if (!GetCommState(hSerial, &dcbSerialParams)) {
printf("Error getting device state. Error code: %d\n", GetLastError());
CloseHandle(hSerial);
return 1;
}
dcbSerialParams.BaudRate = CBR_9600;
dcbSerialParams.ByteSize = 8;
dcbSerialParams.StopBits = ONESTOPBIT;
dcbSerialParams.Parity = NOPARITY;
if (!SetCommState(hSerial, &dcbSerialParams)) {
printf("Error setting device state. Error code: %d\n", GetLastError());
CloseHandle(hSerial);
return 1;
}
// --- 3. 设置超时 ---
timeouts.ReadIntervalTimeout = 50;
timeouts.ReadTotalTimeoutConstant = 50;
timeouts.ReadTotalTimeoutMultiplier = 10;
timeouts.WriteTotalTimeoutConstant = 50;
timeouts.WriteTotalTimeoutMultiplier = 10;
if (!SetCommTimeouts(hSerial, &timeouts)) {
printf("Error setting timeouts. Error code: %d\n", GetLastError());
CloseHandle(hSerial);
return 1;
}
printf("Serial port configured and timeouts set.\n");
// --- 4. 写入数据 ---
if (!WriteFile(hSerial, dataToSend, (DWORD)strlen(dataToSend), &bytesWritten, NULL)) {
printf("Error writing to serial port. Error code: %d\n", GetLastError());
} else {
printf("Successfully wrote %d bytes: '%s'\n", bytesWritten, dataToSend);
}
// 等待一小段时间,让设备有时间处理和响应
Sleep(100); // 等待100毫秒
// --- 5. 读取数据 ---
if (!ReadFile(hSerial, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) {
printf("Error reading from serial port. Error code: %d\n", GetLastError());
} else {
buffer[bytesRead] = '\0';
if (bytesRead > 0) {
printf("Successfully read %d bytes: '%s'\n", bytesRead, buffer);
} else {
printf("No data read from serial port (timeout).\n");
}
}
// --- 6. 关闭串口 ---
CloseHandle(hSerial);
printf("Serial port closed.\n");
return 0;
}
如何编译和运行 (Visual Studio):
- 创建一个新的 "Windows Desktop Application" 项目。
- 将上面的代码复制到
main.c文件中。 - 确保你的电脑上插好了 USB 转串口线,并且设备管理器中能看到
COMx端口。 - 修改代码中的
COM3为你实际的串口号。 - 编译并运行。
进阶话题
异步 I/O (Overlapped I/O)
同步 I/O 简单但效率低,因为读写时会阻塞主线程,异步 I/O 允许你在读写数据的同时,继续执行其他代码,非常适合构建高性能的 GUI 应用或服务器程序。
核心思想是使用 OVERLAPPED 结构体和 WaitForSingleObject / GetOverlappedResult。
OVERLAPPED osWrite = { 0 };
osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // 创建一个事件对象
WriteFile(hSerial, dataToSend, strlen(dataToSend), &bytesWritten, &osWrite);
// 主线程可以做其他事情...
// 等待写入操作完成
WaitForSingleObject(osWrite.hEvent, INFINITE);
GetOverlappedResult(hSerial, &osWrite, &bytesWritten, TRUE);
CloseHandle(osWrite.hEvent);
读取操作也类似,只是你需要在一个循环中等待事件触发,表示有数据到达。
事件驱动方式
这是更高级的异步模式,你可以让串口驱动在特定事件发生时(如接收到字符、线路状态改变等)向你的程序发送一个信号,这需要设置 SetCommMask 并使用 WaitCommEvent,这种方式非常高效,但实现起来也更复杂。
错误处理
上面的示例只是简单打印了 GetLastError(),在实际应用中,你应该根据错误码采取不同的恢复或重试策略。ERROR_IO_PENDING 表示异步操作已启动,这是正常情况。
常见问题与解决方案
-
错误码 2 (ERROR_FILE_NOT_FOUND)
- 原因: 串口名
COMx不存在。 - 解决: 打开“设备管理器”,查看“端口 (COM 和 LPT)”下是否有你的设备,并确认端口号是否正确。
- 原因: 串口名
-
程序在
ReadFile处卡住,一直不返回- 原因: 没有设置超时,或者对方设备没有发送数据。
- 解决: 确保已经配置了
COMMTIMEOUTS结构体,如果对方设备不响应,检查接线或设备本身是否正常工作。
-
读取到的数据不完整或乱码
- 原因:
- 串口参数(波特率、数据位等)与对方设备不匹配。
- 接线错误(TX-RX, RX-TX 交叉接反)。
- 数据流中缺少结束符。
- 解决: 仔细核对串口配置,使用串口调试助手等工具先测试一下设备是否正常工作。
- 原因:
-
WriteFile或ReadFile返回FALSE,GetLastError()返回 127 (ERROR_PROC_NOT_FOUND)- 原因: 链接了错误的库。
ERROR_PROC_NOT_FOUND表示找不到指定的入口点,通常是kernel32.dll中的函数。 - 解决: 检查项目链接器设置,确保正确链接了
kernel32.lib。
- 原因: 链接了错误的库。
替代方案:第三方库
直接使用 Win32 API 虽然灵活,但代码量较大且繁琐,对于快速开发,可以考虑使用第三方库,它们封装了底层的复杂性。
- SerialPort: 一个简单易用的 C++ 库,提供了非常简洁的 API,如果你项目允许用 C++,这是首选。
- Boost.Asio: 一个功能强大的跨平台 C++ 网络和底层 I/O 库,也支持串口通信,重量级但非常强大。
- libserialport: 一个轻量级的 C 语言库,旨在提供一个简单、跨平台的串口 API,如果你需要跨平台(Linux/macOS),这是个好选择。
在 Windows C 语言中操作串口,核心就是围绕 Win32 API 的一系列函数,基本流程是 打开 -> 配置 -> 读写 -> 关闭。
- 同步模式:简单直接,适合初学者和简单的控制台程序。
- 异步模式:功能强大,适合构建需要同时处理 UI 和通信的复杂应用程序。
从上面的完整示例开始,逐步修改和扩展,你就能掌握 Windows 下的串口编程,祝你编程顺利!
