学懂C++(五十五): C++ 串口通信编程开发详解

一、概念

串口通信(Serial Communication)是指通过串行接口进行数据传输的方式。串行接口指的是数据一位一位地按顺序传输,而不是并行传输。常见的串行通信接口包括 RS-232、RS-485、USB 等。

在 C++ 编程中,串口通信通常用于计算机与外部设备(如传感器、微控制器、调制解调器等)之间的数据交换。

二、原理

串口通信基于 UART(Universal Asynchronous Receiver/Transmitter),UART 是一种硬件设备,它将并行数据转换为串行数据并进行传输,接收端的 UART 将串行数据还原为并行数据。串口通信的关键参数有:

  1. 波特率(Baud Rate):每秒传输的位数。例如,9600 波特率表示每秒传输 9600 位。
  2. 数据位(Data Bits):每个字符的位数,通常为 7 或 8 位。
  3. 停止位(Stop Bits):用于标识数据帧结束,通常为 1 位或 2 位。
  4. 奇偶校验位(Parity Bit):用于错误检测,可以是无校验(None)、奇校验(Odd)或偶校验(Even)。

数据传输分为异步传输和同步传输:

  • 异步传输:每个数据帧独立传输,帧之间不需要同步时钟信号。波特率、数据位、停止位和校验位等参数需要在双方进行通信前设定一致。
  • 同步传输:发送和接收设备使用共同的时钟信号。同步传输速度快,适合长距离传输,但设备间需要保持时钟同步。

三、应用领域

串口通信被广泛应用于各种领域,主要包括:

  1. 嵌入式系统

    • 与传感器、微控制器(如 Arduino、Raspberry Pi)等设备进行通信。
    • 读取传感器数据,控制执行器(如电机、LED 等)。
  2. 工业自动化

    • 连接 PLC(可编程逻辑控制器)和 HMI(人机界面)。
    • 控制工业设备和生产线,实现数据采集和远程监控。
  3. 通信设备

    • 与调制解调器、GPS 模块等设备进行数据交换。
    • 实现远程数据传输和定位服务。
  4. 医疗设备

    • 监控患者生命体征数据,如心电图、血压等。
    • 与其他医疗设备进行数据交换和控制。
  5. 消费电子

    • 家用电器、音频设备的控制和数据传输。
    • 智能家居设备之间的通信和控制。

四、使用场景

  1. 数据采集与监控

    • 通过串口通信采集多个传感器的数据,并将数据传输到计算机或云端进行处理和监控。例如,环境监测系统通过串口与传感器连接,实时采集温度、湿度、空气质量等数据。
  2. 设备控制

    • 通过串口向外部设备发送指令,控制其行为。例如,通过串口通信控制工业机器人或自动化生产线的运作,提升生产效率和安全性。
  3. 调试和测试

    • 在嵌入式系统开发中,串口常用于输出调试信息或进行系统测试。开发者通过串口获取系统运行状态、调试信息和错误日志,有助于快速定位和解决问题。
  4. 远程通信

    • 使用串口通信模块(如无线调制解调器)实现远程数据传输和控制。例如,通过 GSM 模块实现远程监控和控制设备,适用于智能电网、远程抄表等应用场景。

五、实现方式

1. Linux 环境下的串口通信

在 Linux 系统中,可以使用 POSIX 标准 API 进行串口通信。以下是一个简单示例,展示如何打开串口、配置参数、读取和写入数据。

#include <iostream>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>

// 配置串口参数
void configure_serial_port(int fd) {
    struct termios tty;
    
    // 获取当前串口设置
    if (tcgetattr(fd, &tty) != 0) {
        std::cerr << "Error from tcgetattr" << std::endl;
        return;
    }
    
    // 设置输入输出波特率为9600
    cfsetospeed(&tty, B9600);
    cfsetispeed(&tty, B9600);

    // 禁用校验位
    tty.c_cflag &= ~PARENB;
    // 设置一个停止位
    tty.c_cflag &= ~CSTOPB;
    // 设置每个字节8位
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8;
    // 禁用硬件流控制
    tty.c_cflag &= ~CRTSCTS;
    // 开启接收使能和本地模式(忽略调制解调器)
    tty.c_cflag |= CREAD | CLOCAL;

    // 将终端设置为原始模式
    cfmakeraw(&tty);
    // 刷新输入缓冲区
    tcflush(fd, TCIFLUSH);
    // 设置新的串口参数
    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
        std::cerr << "Error from tcsetattr" << std::endl;
    }
}

int main() {
    const char *portname = "/dev/ttyS1"; // 串口设备文件的名称
    // 以读写、非阻塞和同步模式打开串口
    int fd = open(portname, O_RDWR | O_NOCTTY | O_SYNC);
    if (fd < 0) {
        std::cerr << "Error opening " << portname << std::endl;
        return 1;
    }

    configure_serial_port(fd); // 配置串口参数

    const char *msg = "Hello, Serial Port!"; // 要发送的消息
    // 向串口写入数据
    int n_written = write(fd, msg, strlen(msg));
    if (n_written < 0) {
        std::cerr << "Error writing to " << portname << std::endl;
    }

    char buf[100]; // 缓冲区用于读取数据
    // 从串口读取数据
    int n_read = read(fd, buf, sizeof(buf));
    if (n_read < 0) {
        std::cerr << "Error reading from " << portname << std::endl;
    } else {
        std::cout << "Read: " << std::string(buf, n_read) << std::endl; // 打印读取到的消息
    }

    close(fd); // 关闭串口
    return 0;
}

关键注释解释

  1. configure_serial_port(int fd)

    • 函数用于配置串口的参数,包括波特率、数据位、停止位、校验位和流控制等。
  2. tcgetattr(fd, &tty)

    • 获取当前串口设置,填充到 termios 结构体中。
  3. cfsetospeed(&tty, B9600)cfsetispeed(&tty, B9600)

    • 设置串口的输入和输出波特率为 9600。
  4. 配置 tty.c_cflag

    • tty.c_cflag 用于控制串口的各种选项:
      • ~PARENB:禁用校验位。
      • ~CSTOPB:设置一个停止位。
      • ~CSIZE 和 CS8:设置每个字节 8 位。
      • ~CRTSCTS:禁用硬件流控制。
      • CREAD | CLOCAL:启用接收使能和本地模式(忽略调制解调器)。
  5. cfmakeraw(&tty)

    • 将终端设置为原始模式。
  6. tcflush(fd, TCIFLUSH)

    • 刷新输入缓冲区,丢弃所有的数据。
  7. tcsetattr(fd, TCSANOW, &tty)

    • 设置新的串口参数,立即生效。
  8. main() 函数

    • open():以读写、非阻塞和同步模式打开串口。
    • configure_serial_port(fd):配置串口参数。
    • write(fd, msg, strlen(msg)):向串口写入数据。
    • read(fd, buf, sizeof(buf)):从串口读取数据。
    • close(fd):关闭串口。

 

2. Windows 环境下的串口通信

在 Windows 系统中,可以使用 Win32 API 进行串口通信。以下是简单示例。

方法一:使用宽字符字符串

#include <iostream>
#include <windows.h>

// 配置串口参数
void configure_serial_port(HANDLE hSerial) {
    DCB dcbSerialParams = {0}; // 初始化 DCB 结构
    dcbSerialParams.DCBlength = sizeof(dcbSerialParams);

    // 获取当前串口状态
    if (!GetCommState(hSerial, &dcbSerialParams)) {
        std::wcerr << L"Error getting state: " << GetLastError() << std::endl;
        return;
    }

    // 配置波特率、数据位、停止位和校验位
    dcbSerialParams.BaudRate = CBR_9600; // 设置波特率为 9600
    dcbSerialParams.ByteSize = 8;        // 设置数据位为 8
    dcbSerialParams.StopBits = ONESTOPBIT; // 设置一个停止位
    dcbSerialParams.Parity = NOPARITY;   // 设置无校验位

    // 设置串口状态
    if (!SetCommState(hSerial, &dcbSerialParams)) {
        std::wcerr << L"Error setting state: " << GetLastError() << std::endl;
        return;
    }

    // 设置超时参数
    COMMTIMEOUTS timeouts = {0};
    timeouts.ReadIntervalTimeout = 50;      // 读取间隔超时
    timeouts.ReadTotalTimeoutConstant = 50; // 读取总超时常量
    timeouts.ReadTotalTimeoutMultiplier = 10; // 读取总超时乘数
    timeouts.WriteTotalTimeoutConstant = 50; // 写入总超时常量
    timeouts.WriteTotalTimeoutMultiplier = 10; // 写入总超时乘数

    // 设置串口超时
    if (!SetCommTimeouts(hSerial, &timeouts)) {
        std::wcerr << L"Error setting timeouts: " << GetLastError() << std::endl;
        return;
    }

    // 清理串口缓冲区
    if (!PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR)) {
        std::wcerr << L"Error purging comm: " << GetLastError() << std::endl;
        return;
    }
}

int main() {
    const wchar_t *portname = L"COM1"; // 使用宽字符字符串表示串口名称
    // 打开串口
    HANDLE hSerial = CreateFileW(portname, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

    // 检查串口是否成功打开
    if (hSerial == INVALID_HANDLE_VALUE) {
        std::wcerr << L"Error opening port: " << GetLastError() << std::endl;
        return 1;
    }

    configure_serial_port(hSerial); // 配置串口参数

    const char *msg = "Hello, Serial Port!"; // 要发送的消息
    DWORD bytes_written;
    // 写入数据到串口
    if (!WriteFile(hSerial, msg, strlen(msg), &bytes_written, NULL)) {
        std::wcerr << L"Error writing to port: " << GetLastError() << std::endl;
        CloseHandle(hSerial); // 关闭串口
        return 1;
    }

    char buf[100] = {0}; // 缓冲区用于读取数据
    DWORD bytes_read;
    // 从串口读取数据
    if (!ReadFile(hSerial, buf, sizeof(buf), &bytes_read, NULL)) {
        std::wcerr << L"Error reading from port: " << GetLastError() << std::endl;
    } else {
        std::wcout << L"Read: " << std::string(buf, bytes_read).c_str() << std::endl; // 打印读取到的消息
    }

    CloseHandle(hSerial); // 关闭串口
    return 0;
}

关键注释解释

  1. configure_serial_port(HANDLE hSerial)

    • 函数用于配置串口的参数,包括波特率、数据位、停止位、校验位和超时设置等。
  2. DCB dcbSerialParams = {0};

    • 初始化一个 DCB 结构,用于存储串口配置参数。
  3. GetCommState(hSerial, &dcbSerialParams)

    • 获取当前串口的状态,并存储到 dcbSerialParams 结构中。
  4. 设置串口参数

    • 波特率dcbSerialParams.BaudRate = CBR_9600;
    • 数据位dcbSerialParams.ByteSize = 8;
    • 停止位dcbSerialParams.StopBits = ONESTOPBIT;
    • 校验位dcbSerialParams.Parity = NOPARITY;
  5. SetCommState(hSerial, &dcbSerialParams)

    • 设置新的串口状态(参数)。
  6. 设置超时参数

    • 读取间隔超时timeouts.ReadIntervalTimeout = 50;
    • 读取总超时常量timeouts.ReadTotalTimeoutConstant = 50;
    • 读取总超时乘数timeouts.ReadTotalTimeoutMultiplier = 10;
    • 写入总超时常量timeouts.WriteTotalTimeoutConstant = 50;
    • 写入总超时乘数timeouts.WriteTotalTimeoutMultiplier = 10;
  7. SetCommTimeouts(hSerial, &timeouts)

    • 设置串口的超时参数。
  8. PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR)

    • 清理串口缓冲区。
  9. 主函数 main

    • CreateFileW():以读写模式打开串口。
    • configure_serial_port(hSerial):配置串口参数。
    • WriteFile():将消息写入串口。
    • ReadFile():从串口读取数据。
    • CloseHandle(hSerial):关闭串口。

 

方法二:使用 ANSI 版本的 Windows API 函数

#include <iostream>
#include <windows.h>

// 配置串口参数
void configure_serial_port(HANDLE hSerial) {
    DCB dcbSerialParams = {0}; // 初始化 DCB 结构
    dcbSerialParams.DCBlength = sizeof(dcbSerialParams);

    // 获取当前串口状态
    if (!GetCommState(hSerial, &dcbSerialParams)) {
        std::cerr << "Error getting state: " << GetLastError() << std::endl;
        return;
    }

    // 配置波特率、数据位、停止位和校验位
    dcbSerialParams.BaudRate = CBR_9600; // 设置波特率为 9600
    dcbSerialParams.ByteSize = 8;        // 设置数据位为 8
    dcbSerialParams.StopBits = ONESTOPBIT; // 设置一个停止位
    dcbSerialParams.Parity = NOPARITY;   // 设置无校验位

    // 设置串口状态
    if (!SetCommState(hSerial, &dcbSerialParams)) {
        std::cerr << "Error setting state: " << GetLastError() << std::endl;
        return;
    }

    // 设置超时参数
    COMMTIMEOUTS timeouts = {0};
    timeouts.ReadIntervalTimeout = 50;      // 读取间隔超时
    timeouts.ReadTotalTimeoutConstant = 50; // 读取总超时常量
    timeouts.ReadTotalTimeoutMultiplier = 10; // 读取总超时乘数
    timeouts.WriteTotalTimeoutConstant = 50; // 写入总超时常量
    timeouts.WriteTotalTimeoutMultiplier = 10; // 写入总超时乘数

    // 设置串口超时
    if (!SetCommTimeouts(hSerial, &timeouts)) {
        std::cerr << "Error setting timeouts: " << GetLastError() << std::endl;
        return;
    }

    // 清理串口缓冲区
    if (!PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR)) {
        std::cerr << "Error purging comm: " << GetLastError() << std::endl;
        return;
    }
}

int main() {
    const char *portname = "COM1"; // 使用 ANSI 字符串表示串口名称
    // 打开串口
    HANDLE hSerial = CreateFileA(portname, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

    // 检查串口是否成功打开
    if (hSerial == INVALID_HANDLE_VALUE) {
        std::cerr << "Error opening port: " << GetLastError() << std::endl;
        return 1;
    }

    configure_serial_port(hSerial); // 配置串口参数

    const char *msg = "Hello, Serial Port!"; // 要发送的消息
    DWORD bytes_written;
    // 写入数据到串口
    if (!WriteFile(hSerial, msg, strlen(msg), &bytes_written, NULL)) {
        std::cerr << "Error writing to port: " << GetLastError() << std::endl;
        CloseHandle(hSerial); // 关闭串口
        return 1;
    }

    char buf[100] = {0}; // 缓冲区用于读取数据
    DWORD bytes_read;
    // 从串口读取数据
    if (!ReadFile(hSerial, buf, sizeof(buf), &bytes_read, NULL)) {
        std::cerr << "Error reading from port: " << GetLastError() << std::endl;
    } else {
        std::cout << "Read: " << std::string(buf, bytes_read) << std::endl; // 打印读取到的消息
    }

    CloseHandle(hSerial); // 关闭串口
    return 0;
}

 关键注释解释

  1. configure_serial_port(HANDLE hSerial)

    • 函数用于配置串口的参数,包括波特率、数据位、停止位、校验位和超时设置等。
  2. DCB dcbSerialParams = {0};

    • 初始化一个 DCB 结构,用于存储串口配置参数。
  3. GetCommState(hSerial, &dcbSerialParams)

    • 获取当前串口的状态,并存储到 dcbSerialParams 结构中。
  4. 设置串口参数

    • 波特率dcbSerialParams.BaudRate = CBR_9600;
    • 数据位dcbSerialParams.ByteSize = 8;
    • 停止位dcbSerialParams.StopBits = ONESTOPBIT;
    • 校验位dcbSerialParams.Parity = NOPARITY;
  5. SetCommState(hSerial, &dcbSerialParams)

    • 设置新的串口状态(参数)。
  6. 设置超时参数

    • 读取间隔超时timeouts.ReadIntervalTimeout = 50;
    • 读取总超时常量timeouts.ReadTotalTimeoutConstant = 50;
    • 读取总超时乘数timeouts.ReadTotalTimeoutMultiplier = 10;
    • 写入总超时常量timeouts.WriteTotalTimeoutConstant = 50;
    • 写入总超时乘数timeouts.WriteTotalTimeoutMultiplier = 10;
  7. SetCommTimeouts(hSerial, &timeouts)

    • 设置串口的超时参数。
  8. PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR)

    • 清理串口缓冲区。
  9. 主函数 main

    • CreateFileA():以读写模式打开串口。
    • configure_serial_port(hSerial):配置串口参数。
    • WriteFile():将消息写入串口。
    • ReadFile():从串口读取数据。
    • CloseHandle(hSerial):关闭串口。

3. 使用 Boost.Asio 进行跨平台串口通信

Boost.Asio 是一个跨平台的库,提供了用于网络和低级 I/O 编程的广泛功能,包括串口通信。以下是一个使用 Boost.Asio 进行串口通信的示例。

#include <iostream>
#include <boost/asio.hpp>

int main() {
    boost::asio::io_service io; // 创建 io_service 对象,用于管理 I/O 操作
    boost::asio::serial_port serial(io, "/dev/ttyS1"); // 创建 serial_port 对象并打开串口

    // 配置串口参数
    serial.set_option(boost::asio::serial_port_base::baud_rate(9600)); // 设置波特率为 9600
    serial.set_option(boost::asio::serial_port_base::character_size(8)); // 设置数据位为 8
    serial.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none)); // 设置无校验位
    serial.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one)); // 设置一个停止位
    serial.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none)); // 设置无流控制

    const std::string msg = "Hello, Serial Port!"; // 要发送的消息
    boost::asio::write(serial, boost::asio::buffer(msg)); // 将消息写入串口

    char buf[100]; // 缓冲区用于读取数据
    boost::asio::read(serial, boost::asio::buffer(buf, msg.size())); // 从串口读取数据到缓冲区
    std::cout << "Read: " << std::string(buf, msg.size()) << std::endl; // 打印读取到的消息

    return 0;
}

关键注释解释

  1. boost::asio::io_service io

    • 创建一个 io_service 对象,它是 Boost.Asio 中 I/O 操作的核心对象,负责管理所有异步操作。
  2. boost::asio::serial_port serial(io, "/dev/ttyS1")

    • 创建一个 serial_port 对象并打开指定的串口。
    • 在 Linux 系统中,串口设备通常表示为 /dev/ttyS1(或其他名称),在 Windows 中,你可以使用 "COM1" 等名称。
  3. 设置串口参数

    • 波特率serial.set_option(boost::asio::serial_port_base::baud_rate(9600));
      • 设置串口通信的波特率为 9600。
    • 数据位serial.set_option(boost::asio::serial_port_base::character_size(8));
      • 设置数据位为 8。
    • 校验位serial.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none));
      • 设置无校验位。
    • 停止位serial.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one));
      • 设置一个停止位。
    • 流控制serial.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none));
      • 设置无流控制。
  4. 发送数据

    • const std::string msg = "Hello, Serial Port!";
      • 要发送的消息。
    • boost::asio::write(serial, boost::asio::buffer(msg));
      • 使用 boost::asio::write 函数将消息写入串口。
  5. 读取数据

    • char buf[100];
      • 定义一个缓冲区用于读取数据。
    • boost::asio::read(serial, boost::asio::buffer(buf, msg.size()));
      • 使用 boost::asio::read 函数从串口读取数据到缓冲区。
    • std::cout << "Read: " << std::string(buf, msg.size()) << std::endl;
      • 打印读取到的数据。

 

 

六、小结

C++ 串口通信涉及了解串口的基本概念和原理,掌握如何在不同操作系统环境下进行串口操作。无论是使用原生 API 还是第三方库,串口通信在嵌入式系统、工业自动化、通信设备等领域都有广泛的应用。通过学习和掌握这些技术,开发者可以实现设备之间的高效数据传输和控制。

 

  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿享天开

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值