Windows C语言如何实现串口通信?

99ANYc3cd6
预计阅读时长 36 分钟
位置: 首页 C语言 正文

目录

  1. 核心概念
    • 什么是串口?
    • Windows 串口编程 API (Win32 API)
    • 通信流程概述
  2. 详细步骤与代码示例
    • 第 1 步:包含头文件和链接库
    • 第 2 步:打开串口
    • 第 3 步:配置串口参数
    • 第 4 步:读写串口
    • 第 5 步:关闭串口
    • 完整示例代码 (发送与接收)
  3. 进阶话题
    • 异步 I/O (Overlapped I/O) - 实现非阻塞通信
    • 使用事件驱动方式
    • 错误处理
  4. 常见问题与解决方案
  5. 替代方案:第三方库

核心概念

什么是串口?

串口(Serial Port)是一种按位(bit)进行数据传输的通信接口,在物理上,它通常是一个 DB-9DB-25 的连接器,在现代计算机上,物理串口已逐渐被 USB 取代,但通过 USB 转串口适配器(如 CH340, FT232, CP2102 等),我们仍然可以方便地使用串口进行设备通信(如单片机、PLC、GPS 模块等)。

windows c语言 串口
(图片来源网络,侵删)

在 Windows 中,串口被当作一种特殊的文件来处理,叫做“设备文件”,它的命名格式通常是 COMxx 是端口号,COM1, COM3 等。

Windows 串口编程 API (Win32 API)

Windows 提供了一套完整的 Win32 API 来操作串口,这些 API 主要包含在 windows.hwinbase.h 中,核心 API 包括:

  • CreateFile: 用于“打开”串口设备,获取一个句柄,就像打开文件一样。
  • GetCommState: 获取当前串口的配置参数。
  • SetCommState: 设置串口的配置参数,如波特率、数据位、停止位、校验位等。
  • BuildCommDCB: 更方便地构建设备控制块,用于配置串口。
  • SetupComm: 设置串口输入/输出缓冲区的大小。
  • ReadFile: 从串口读取数据。
  • WriteFile: 向串口写入数据。
  • CloseHandle: 关闭串口句柄。
  • ClearCommError: 获取并清除串口错误状态。
  • PurgeComm: 清空串口输入/输出缓冲区。

一个典型的串口通信程序流程如下:

  1. 打开串口: 使用 CreateFile 函数获取串口句柄。
  2. 配置串口: 使用 GetCommState 获取当前配置,修改后用 SetCommState 设置新的配置(波特率、数据位、停止位、校验位等)。
  3. 设置超时: 使用 COMMTIMEOUTS 结构体设置读写超时,防止程序永久阻塞。
  4. 读写数据:
    • 同步模式: 直接调用 ReadFileWriteFile,程序会一直等待,直到数据被成功读写或超时。
    • 异步模式 (Overlapped): 使用 ReadFile/WriteFile 配合 OVERLAPPED 结构体,实现非阻塞 I/O,程序可以同时做其他事情。
  5. 关闭串口: 通信结束后,使用 CloseHandle 关闭串口句柄,释放资源。

详细步骤与代码示例

下面我们来实现一个最简单的同步串口通信程序:配置一个串口,向它发送一串数据,然后读取返回的数据(假设对方设备会回显)。

windows c语言 串口
(图片来源网络,侵删)

第 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 步:配置串口

这是最关键的一步,我们需要配置波特率、数据位、停止位、校验位等。

windows c语言 串口
(图片来源网络,侵删)
  1. 获取当前配置: 定义一个 DCB (Device Control Block) 结构体,并用 GetCommState 填充它。

  2. 修改配置: 修改 DCB 结构体中的成员,一个更简单的方法是使用 BuildCommDCB 函数,它可以直接从字符串(如 "9600,n,8,1")构建 DCB 结构体。

  3. 应用新配置: 使用 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):

  1. 创建一个新的 "Windows Desktop Application" 项目。
  2. 将上面的代码复制到 main.c 文件中。
  3. 确保你的电脑上插好了 USB 转串口线,并且设备管理器中能看到 COMx 端口。
  4. 修改代码中的 COM3 为你实际的串口号。
  5. 编译并运行。

进阶话题

异步 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 表示异步操作已启动,这是正常情况。


常见问题与解决方案

  1. 错误码 2 (ERROR_FILE_NOT_FOUND)

    • 原因: 串口名 COMx 不存在。
    • 解决: 打开“设备管理器”,查看“端口 (COM 和 LPT)”下是否有你的设备,并确认端口号是否正确。
  2. 程序在 ReadFile 处卡住,一直不返回

    • 原因: 没有设置超时,或者对方设备没有发送数据。
    • 解决: 确保已经配置了 COMMTIMEOUTS 结构体,如果对方设备不响应,检查接线或设备本身是否正常工作。
  3. 读取到的数据不完整或乱码

    • 原因:
      • 串口参数(波特率、数据位等)与对方设备不匹配。
      • 接线错误(TX-RX, RX-TX 交叉接反)。
      • 数据流中缺少结束符。
    • 解决: 仔细核对串口配置,使用串口调试助手等工具先测试一下设备是否正常工作。
  4. WriteFileReadFile 返回 FALSEGetLastError() 返回 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 下的串口编程,祝你编程顺利!

-- 展开阅读全文 --
头像
织梦CMS百度分享插件如何正确安装使用?
« 上一篇 02-14
const char在C语言中到底该如何正确使用?
下一篇 » 02-14
取消
微信二维码
支付宝二维码

目录[+]