Windows环境下串口通信与多线程编程
引言
本文旨在详细解析一个在Windows环境下实现串口通信的C语言程序,并结合多线程技术来提高程序的效率和稳定性。通过这个示例,我们将了解如何枚举系统中的串口设备、配置串口参数、以及如何利用多线程机制同时进行数据的接收和发送。
代码简单、结构清晰、注释明确,能够直接编译执行,可以作为初学者的实战学习案例,帮助初学者在windows下的串口使用、多线程编程进行入门学习和实验。
代码概述
以下是完整的C语言代码,用于实现在Windows环境下对串口设备的数据收发操作:
c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define MAX_PORTS 256
HANDLE serial_handle;
OVERLAPPED ovRead, ovWrite;
// 接收线程函数,负责从串口读取数据并打印到控制台
DWORD WINAPI ReceiveThread(LPVOID lpParam) {
char buffer[256];
DWORD bytesRead;
while (1) {
if (!ReadFile(serial_handle, buffer, sizeof(buffer) - 1, &bytesRead, &ovRead)) {
if (GetLastError() != ERROR_IO_PENDING) {
printf("Error reading from serial port\n");
break;
}
WaitForSingleObject(ovRead.hEvent, INFINITE);
GetOverlappedResult(serial_handle, &ovRead, &bytesRead, FALSE);
}
if (bytesRead > 0) {
buffer[bytesRead] = '\0'; // Null-terminate the received data
printf("Received: %s", buffer);
}
}
return 0;
}
// 发送线程函数,负责从标准输入读取消息并通过串口发送
DWORD WINAPI SendThread(LPVOID lpParam) {
char message[256];
DWORD bytesWritten;
while (1) {
if (fgets(message, sizeof(message), stdin) != NULL) {
if (!WriteFile(serial_handle, message, strlen(message), &bytesWritten, &ovWrite)) {
if (GetLastError() != ERROR_IO_PENDING) {
printf("Error writing to serial port\n");
break;
}
WaitForSingleObject(ovWrite.hEvent, INFINITE);
GetOverlappedResult(serial_handle, &ovWrite, &bytesWritten, FALSE);
}
}
}
return 0;
}
// 枚举系统中可用的串口,并将它们存储在 ports 数组中
int EnumerateSerialPorts(char ports[MAX_PORTS][MAX_PATH]) {
int count = 0;
HKEY hKey;
LONG retCode;
DWORD i = 0;
TCHAR szSubKeyName[MAX_PATH];
DWORD cbName;
TCHAR szData[MAX_PATH];
DWORD cbData;
DWORD dwType;
retCode = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
TEXT("HARDWARE\\DEVICEMAP\\SERIALCOMM"),
0,
KEY_READ,
&hKey);
if (retCode != ERROR_SUCCESS) {
printf("Failed to open registry key\n");
return 0;
}
while (1) {
cbName = MAX_PATH;
cbData = MAX_PATH * sizeof(TCHAR);
retCode = RegEnumValue(hKey, i++, szSubKeyName, &cbName, NULL, &dwType, (LPBYTE)szData, &cbData);
if (retCode == ERROR_NO_MORE_ITEMS) {
break;
}
if (retCode != ERROR_SUCCESS) {
printf("Failed to enumerate registry values\n");
break;
}
if (dwType == REG_SZ) {
lstrcpy(ports[count], szData);
count++;
}
}
RegCloseKey(hKey);
return count;
}
// 主函数,负责初始化串口、创建接收和发送线程,并处理错误
int main() {
char ports[MAX_PORTS][MAX_PATH];
int num_ports = EnumerateSerialPorts(ports);
int selected_port = -1;
if (num_ports == 0) {
printf("No serial ports found.\n");
return EXIT_FAILURE;
}
printf("Available serial ports:\n");
for (int i = 0; i < num_ports; i++) {
printf("%d: %s\n", i + 1, ports[i]);
}
printf("Enter the number of the serial port you want to use: ");
scanf("%d", &selected_port);
if (selected_port < 1 || selected_port > num_ports) {
printf("Invalid selection.\n");
return EXIT_FAILURE;
}
char serial_port[MAX_PATH];
sprintf(serial_port, "\\\\.\\%s", ports[selected_port - 1]);
// 打开选定的串口
serial_handle = CreateFileA(serial_port,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
if (serial_handle == INVALID_HANDLE_VALUE) {
DWORD error_code = GetLastError();
switch (error_code) {
case ERROR_ACCESS_DENIED:
printf("Access denied. Ensure you have the necessary permissions to access the serial port.\n");
break;
case ERROR_FILE_NOT_FOUND:
printf("The specified file could not be found. Check the serial port name.\n");
break;
case ERROR_INVALID_NAME:
printf("The filename, directory name, or volume label syntax is incorrect.\n");
break;
default:
printf("Error opening serial port. Error code: %lu\n", error_code);
break;
}
return EXIT_FAILURE;
}
DCB dcbSerialParams = { 0 };
COMMTIMEOUTS timeouts = { 0 };
// 获取当前串口配置
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
if (!GetCommState(serial_handle, &dcbSerialParams)) {
printf("Error getting state of serial port\n");
CloseHandle(serial_handle);
return EXIT_FAILURE;
}
// 设置串口参数
dcbSerialParams.BaudRate = CBR_115200; // 设置波特率为 115200
dcbSerialParams.ByteSize = 8;
dcbSerialParams.StopBits = ONESTOPBIT;
dcbSerialParams.Parity = NOPARITY;
if (!SetCommState(serial_handle, &dcbSerialParams)) {
printf("Error setting state of serial port\n");
CloseHandle(serial_handle);
return EXIT_FAILURE;
}
// 设置超时时间
timeouts.ReadIntervalTimeout = 50;
timeouts.ReadTotalTimeoutConstant = 50;
timeouts.ReadTotalTimeoutMultiplier = 10;
timeouts.WriteTotalTimeoutConstant = 50;
timeouts.WriteTotalTimeoutMultiplier = 10;
if (!SetCommTimeouts(serial_handle, &timeouts)) {
printf("Error setting timeouts for serial port\n");
CloseHandle(serial_handle);
return EXIT_FAILURE;
}
// 初始化 OVERLAPPED 结构体
memset(&ovRead, 0, sizeof(OVERLAPPED));
memset(&ovWrite, 0, sizeof(OVERLAPPED));
ovRead.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
ovWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (ovRead.hEvent == NULL || ovWrite.hEvent == NULL) {
printf("Error creating event objects\n");
CloseHandle(serial_handle);
return EXIT_FAILURE;
}
// 创建接收和发送线程
HANDLE hRecvThread = CreateThread(NULL, 0, ReceiveThread, NULL, 0, NULL);
HANDLE hSendThread = CreateThread(NULL, 0, SendThread, NULL, 0, NULL);
if (hRecvThread == NULL || hSendThread == NULL) {
printf("Error creating threads\n");
CloseHandle(serial_handle);
CloseHandle(ovRead.hEvent);
CloseHandle(ovWrite.hEvent);
return EXIT_FAILURE;
}
// 等待线程结束(实际应用中这两个线程不会结束)
WaitForSingleObject(hRecvThread, INFINITE);
WaitForSingleObject(hSendThread, INFINITE);
CloseHandle(serial_handle);
CloseHandle(ovRead.hEvent);
CloseHandle(ovWrite.hEvent);
return 0;
}
串口外部直接将串口收发短接,在程序执行控制台中输入任何数据,会从串口发出,因此收发被短接,发出的数据会被串口接收到而打印到控制台上,具体如下图所示。
串口访问
枚举串口设备
在Windows操作系统中,串口设备的信息通常保存在注册表中。本程序通过调用RegOpenKeyEx
打开注册表项HARDWARE\DEVICEMAP\SERIALCOMM
,然后使用RegEnumValue
函数遍历该键下的所有值名及其对应的数据。每个值代表一个串口设备,其名称为COM端口号(如"COM1"),而对应的字符串则是该设备的实际路径(如"\.\COM1")。这些信息被收集到数组ports
中供后续使用。
配置串口参数
为了使串口能够正常工作,必须对其进行适当的配置,包括设置波特率、数据位数、停止位和校验方式等。这一步骤通过获取DCB结构体(Device Control Block)完成,它包含了串口的所有配置信息。我们首先调用GetCommState
函数获取当前串口的状态,接着修改其中的相关字段以满足需求(例如设置波特率为115200bps),最后调用SetCommState
函数应用新的配置。
此外,还需要设置串口的超时属性,以便在指定时间内没有接收到或发送完数据时返回错误码。这里定义了一个COMMTIMEOUTS
结构体,并设置了各个超时成员变量,最后通过SetCommTimeouts
函数将其应用于串口。
打开串口
当确定了要使用的串口后,需要调用CreateFileA
函数打开它。此函数接受多个参数,包括文件路径(在这里就是之前查找到的串口路径)、访问模式(这里是读写模式)、共享模式(由于串口通常是独占资源,所以设置为0表示不允许其他进程同时打开同一个串口)、安全属性指针、打开方式(OPEN_EXISTING表示仅打开已存在的设备对象)、标志和属性(FILE_FLAG_OVERLAPPED允许异步I/O操作)以及模板文件句柄指针。如果成功打开,则返回有效的文件句柄;否则返回INVALID_HANDLE_VALUE,并可通过GetLastError
获取具体的错误原因。
多线程编程
创建事件对象
在Windows API中,OVERLAPPED结构体用于支持异步I/O操作。为了正确地执行非阻塞式的读写操作,我们需要预先初始化两个OVERLAPPED类型的变量------ovRead
和ovWrite
,并将它们关联到各自的事件对象上。这样,在执行ReadFile
和WriteFile
时,若发生IO等待情况,线程可以立即返回而不被阻塞,而是可以通过等待相应的事件通知来判断何时继续执行。
启动接收和发送线程
本程序采用多线程设计,分别创建了两个独立的工作线程:ReceiveThread和SendThread。前者负责不断地从串口接收数据并输出至控制台,后者则持续监听用户的标准输入,并将输入的内容发送到串口。这种分离使得主线程得以专注于管理任务,如初始化、启动子线程及监控异常情况等,从而提升了整个系统的响应能力和灵活性。
接收线程(ReceiveThread)
此线程的主要功能是从串口读取数据,并立即将其显示出来。具体流程如下:
- 调用
ReadFile
尝试从串口读取数据。 - 如果遇到ERROR_IO_PENDING错误,则表明正在等待硬件信号,此时应暂停执行直至相关事件触发。
- 利用
WaitForSingleObject
函数监视与ovRead
关联的事件对象,一旦有数据到达,则继续往下执行。 - 使用
GetOverlappedResult
函数获取实际读取的字节数。 - 将接收到的数据转换为字符串形式并在屏幕上展示。
发送线程(SendThread)
该线程的作用是不断接收来自用户的命令行输入,并将之传输给目标串口设备。主要步骤如下:
- 使用
fgets
函数捕获用户输入的消息。 - 执行
WriteFile
命令尝试将消息内容写入串口缓冲区。 - 若出现ERROR_IO_PENDING状态,则同样进入等待阶段,直到对应的事件唤醒为止。
- 再次借助
GetOverlappedResult
确认有多少字节已被成功写入。 - 返回循环开始处继续等待下一次输入。
总结
本文通过对上述代码的深入剖析,展示了如何在Windows平台上实现高效的串口通信以及如何运用多线程技术提升应用程序的整体性能。内容思维导图如下:
无论是初学者还是有一定经验的开发者,都可以从中学习到宝贵的知识点和技术细节。希望这篇博客能为大家提供有价值的参考!