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

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

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

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

基本概念

在 Windows 中,串口(COM口)被当作一种特殊的 文件 来处理,我们可以使用文件操作的方式来打开、读写和关闭串口。

  • 句柄:当你成功打开一个串口(或文件)后,Windows 会返回一个唯一的标识符,称为“句柄”,后续的所有操作(如读写、关闭)都通过这个句柄来进行。
  • 同步与异步
    • 同步 I/O:调用 ReadFileWriteFile 函数后,程序会 阻塞(即暂停执行),直到操作完成或超时,这是最简单的方式,适合初学者。
    • 异步 I/O (Overlapped I/O):调用 ReadFileWriteFile 后,函数会立即返回,程序可以继续执行其他任务,当 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
);

参数解释:

c语言 windows 串口
(图片来源网络,侵删)
  • lpFileName: 串口名称,格式为 "COMX""COM1", "COM3"
  • dwDesiredAccess:
    • GENERIC_READ: 只读。
    • GENERIC_WRITE: 只写。
    • GENERIC_READ | GENERIC_WRITE: 读写。
  • dwShareMode: 串口不能共享,所以必须设为 0
  • dwCreationDisposition: 对于已存在的设备(如串口),必须设为 OPEN_EXISTING
  • 返回值:
    • 成功:返回一个有效的 HANDLE 句柄。
    • 失败:返回 INVALID_HANDLE_VALUE,你可以调用 GetLastError() 获取具体的错误码。

b. 配置串口:SetupCommDCB / BuildCommDCB

串口通信前必须配置其参数,如波特率、数据位、停止位、校验位等,这通常分两步:

  1. 分配缓冲区SetupComm
  2. 设置参数:通过 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;

BuildCommDCBSetCommState

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

对于同步读取,一个简单有效的设置是:

c语言 windows 串口
(图片来源网络,侵删)
  • ReadIntervalTimeout: MAXDWORD (0xFFFFFFFF)
  • ReadTotalTimeoutMultiplier: 0
  • ReadTotalTimeoutConstant: 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;
}

使用步骤总结

  1. 包含头文件#include <windows.h>
  2. 定义串口名char port[] = "COMX";
  3. CreateFile:打开串口,获取句柄。
  4. GetCommState:获取当前串口配置到 DCB 结构体。
  5. 修改 DCB:根据需要修改波特率、数据位等参数。
  6. SetCommState:将修改后的配置应用到串口。
  7. SetCommTimeouts:设置合理的读取超时。
  8. WriteFile:向串口写入数据。
  9. ReadFile:从串口读取数据。
  10. CloseHandle:程序结束前,关闭串口句柄。

常见问题与解决方法

  1. CreateFile 失败,错误码 5 (ERROR_ACCESS_DENIED)

    • 原因:串口正被其他程序(如串口调试助手、你的程序上次运行未关闭)占用。
    • 解决:关闭所有可能使用该串口的程序。
  2. CreateFile 失败,错误码 2 (ERROR_FILE_NOT_FOUND)

    • 原因:指定的串口名称不存在(你的电脑没有 COM3 口)。
    • 解决:在“设备管理器”中查看你的电脑有哪些可用的 COM 口。
  3. ReadFile 一直阻塞,不返回

    • 原因:没有设置超时,或者设备确实没有发送任何数据。
    • 解决:使用 SetCommTimeouts 设置超时,如果设置了超时但仍不返回,请检查设备是否正常工作并已连接。
  4. ReadFile 返回成功,但 bytesRead 为 0

    • 原因:在超时时间内没有收到任何数据。
    • 解决:这是正常现象,只需继续下一次读取即可。
  5. 发送或接收的数据不正确(乱码、数据丢失)

    • 原因:双方的串口参数(波特率、数据位、停止位、校验位)不一致。
    • 解决:确保你的 C 程序和另一端设备(单片机、另一台电脑等)的串口配置完全相同。
  6. 如何枚举所有可用的串口?

    • Windows API 没有直接提供枚举串口的函数,通常的变通方法是:
      1. 从 "COM1" 到 "COM256" 循环调用 CreateFile
      2. CreateFile 返回的不是 INVALID_HANDLE_VALUE,则说明该串口存在。
      3. 立即调用 CloseHandle 关闭它,以避免占用。
      4. 或者,可以通过查询注册表 HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM 来获取已存在的串口名。
-- 展开阅读全文 --
头像
dede列表页如何调用body内容?
« 上一篇 今天
C语言程序设计与C语言,两者究竟有何不同?
下一篇 » 今天
取消
微信二维码
支付宝二维码

目录[+]