使用C++实现串口通信

目录

1. 环境准备

2. 串口通信代码

3. 代码解析

串口配置要点

3.1 打开串口 (open)

3.2 获取和设置串口属性 (tcgetattr 和 tcsetattr)

3.3 配置波特率

3.4 配置数据格式

3.5 配置输入输出模式(原始模式)

3.6 配置读取模式

发送与接收数据

3.7 发送数据(sendCommand)

3.8 接收数据(receiveResponse)

4. 常见问题排查

4.1 串口通信不稳定或数据丢失

原因分析:

排查方法:

解决方法:

4.2 数据不符或乱码

原因分析:

排查方法:

解决方法:

4.3 串口无法打开或权限问题

原因分析:

排查方法:

解决方法:

5. 进阶调试技巧

5.1 使用串口调试工具

5.2 分析波形

5.3 加强错误处理

6. 串口通信的优化

6.1 提高通信效率

6.2 高效的错误恢复机制

7. 总结


在嵌入式和工业设备开发中,串口通信是一种非常常见的数据传输方式。本文将结合一个完整的C++示例代码,详细介绍如何通过串口与设备进行通信。

1. 环境准备

为了实现串口通信,需要以下环境支持:

  • 一个支持串口通信的Linux系统。
  • C++编译器(如g++)。
  • 硬件连接,串口回环测试或实际设备。
  • 必要的头文件:fcntl.htermios.hunistd.h

本文还参考了一些常见问题的解决方案:

  • 串口设置未使用原始模式导致特殊字符被处理。
  • 发送与接收缓冲区未正确清空可能导致数据冲突。
  • 硬件或线缆连接不正确引起通信失败。

我们将在代码中解决这些问题。


2. 串口通信代码

以下是实现串口通信的完整代码,每行都包含详细的注释。

#include <iostream>  // 用于标准输入输出
#include <fcntl.h>   // 包含文件控制相关函数,如 open
#include <termios.h> // 包含串口通信配置的头文件
#include <unistd.h>  // 提供 read、write 等系统调用
#include <vector>    // 用于动态数组
#include <thread>    // 用于实现线程休眠
#include <chrono>    // 提供时间相关功能

class SerialPort {
public:
    /**
     * 构造函数:打开串口并初始化
     * @param port_name 串口设备名称,例如 /dev/ttyS7
     */
    SerialPort(const std::string& port_name) {
        // 打开串口,以读写模式,禁用控制终端功能
        fd = open(port_name.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
        if (fd == -1) { // 打开失败时退出
            perror("打开串口失败");
            exit(EXIT_FAILURE);
        }
        configurePort(); // 配置串口参数
    }

    /**
     * 析构函数:关闭串口
     */
    ~SerialPort() {
        if (fd != -1) close(fd); // 检查文件描述符有效性后关闭
    }

    /**
     * 发送命令到串口
     * @param command 要发送的数据(字节数组)
     */
    void sendCommand(const std::vector<uint8_t>& command) {
        // 清空串口发送和接收缓冲区
        tcflush(fd, TCIOFLUSH);

        // 打印即将发送的数据
        std::cout << "发送数据: ";
        for (uint8_t byte : command) {
            printf("0x%02X ", byte);
        }
        std::cout << std::endl;

        // 将数据写入串口
        ssize_t written = write(fd, command.data(), command.size());
        if (written == -1) { // 写入失败时输出错误
            perror("写入失败");
        } else {
            std::cout << "实际发送字节数: " << written << std::endl;
        }

        // 延迟一定时间以确保设备能处理数据
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }

    /**
     * 从串口接收响应
     * @param length 要读取的最大字节数
     * @return 接收到的数据(字节数组)
     */
    std::vector<uint8_t> receiveResponse(size_t length) {
        std::vector<uint8_t> response(length); // 为接收数据预留空间
        ssize_t n = read(fd, response.data(), length); // 从串口读取数据
        if (n == -1) { // 读取失败时输出错误
            perror("读取失败");
            response.clear(); // 清空返回数据
        } else {
            response.resize(n); // 调整为实际读取的大小
            std::cout << "接收字节数: " << n << std::endl;
        }
        return response;
    }

private:
    int fd; // 文件描述符,表示串口设备

    /**
     * 配置串口属性
     */
    void configurePort() {
        struct termios tty; // 串口属性结构
        if (tcgetattr(fd, &tty) != 0) { // 获取当前属性
            perror("获取串口属性失败");
            exit(EXIT_FAILURE);
        }

        // 配置波特率为 115200
        cfsetospeed(&tty, B115200);
        cfsetispeed(&tty, B115200);

        // 设置为 8 数据位,无校验位,1 停止位
        tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8; // 8 数据位
        tty.c_cflag &= ~PARENB;                     // 无校验位
        tty.c_cflag &= ~CSTOPB;                     // 1 停止位
        tty.c_cflag |= (CLOCAL | CREAD);            // 开启接收和本地连接

        // 设置为原始模式(重要)
        tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 禁用规范模式和回显
        tty.c_iflag &= ~(IXON | IXOFF | IXANY);         // 禁用软件流控
        tty.c_oflag &= ~OPOST;                         // 禁用输出处理

        // 设置阻塞读取模式
        tty.c_cc[VMIN] = 1;   // 至少读取 1 字节
        tty.c_cc[VTIME] = 10; // 超时时间为 1 秒

        // 应用修改的属性
        if (tcsetattr(fd, TCSANOW, &tty) != 0) {
            perror("设置串口属性失败");
            exit(EXIT_FAILURE);
        }
    }
};

int main() {
    try {
        // 创建串口对象,指定设备名
        SerialPort serial("/dev/ttyS7"); 

        // 准备发送数据
        std::vector<uint8_t> command = {0xA5, 0x00, 0x01};
        serial.sendCommand(command); // 发送数据

        // 接收响应
        auto response = serial.receiveResponse(3); // 假设接收 3 字节
        std::cout << "响应数据: ";
        for (uint8_t byte : response) {
            printf("0x%02X ", byte);
        }
        std::cout << std::endl;

    } catch (const std::exception& e) {
        std::cerr << "错误: " << e.what() << std::endl;
    }

    return 0;
}

3. 代码解析

串口配置要点

在串口通信中,最关键的部分之一就是正确配置串口的各种参数。这些参数包括波特率、数据位、校验位、停止位等。如果配置不正确,数据可能无法正确传输或接收。我们使用 termios 结构体来设置串口参数,具体解析如下:

3.1 打开串口 (open)
fd = open(port_name.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
  • O_RDWR:打开串口设备的读写权限。
  • O_NOCTTY:如果该设备文件对应终端设备,则不会成为该终端的控制终端。
  • O_SYNC:同步I/O模式,要求数据写入到串口时立刻完成(而不是缓存)。

如果 open 返回 -1,则表示打开失败,通常是由于没有访问权限或指定的设备文件路径错误。

3.2 获取和设置串口属性 (tcgetattrtcsetattr)
if (tcgetattr(fd, &tty) != 0) {
    perror("获取串口属性失败");
    exit(EXIT_FAILURE);
}
  • tcgetattr:获取当前串口的配置属性,返回一个 termios 结构体。如果返回值不为 0,表示获取串口属性失败。
  • tcsetattr:将修改后的串口配置应用到串口中。如果返回值不为 0,表示设置串口属性失败。
3.3 配置波特率
cfsetospeed(&tty, B115200);
cfsetispeed(&tty, B115200);
  • cfsetospeedcfsetispeed:分别设置输出和输入的波特率。波特率的选择应与目标设备一致。常见波特率有 B9600, B115200 等。
3.4 配置数据格式
tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;  // 设置数据位为8位
tty.c_cflag &= ~PARENB;                     // 关闭校验位
tty.c_cflag &= ~CSTOPB;                     // 设置停止位为1位
tty.c_cflag |= (CLOCAL | CREAD);            // 开启接收和本地连接
  • CSIZE:掩码,用来指定数据位长度。通过与 ~CSIZE 配合使用,清除已有的配置后再设置为 CS8,即 8 数据位。
  • PARENB:禁用校验位(即选择无校验)。
  • CSTOPB:禁用 2 停止位,设置为 1 停止位。
  • CLOCAL:本地连接,表示设备不受终端控制。
  • CREAD:开启串口接收。
3.5 配置输入输出模式(原始模式)
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 禁用规范模式和回显
tty.c_iflag &= ~(IXON | IXOFF | IXANY);         // 禁用软件流控
tty.c_oflag &= ~OPOST;                         // 禁用输出处理
  • ICANON:禁用规范模式。规范模式下,串口通信会等待输入的行结束符(如回车),而在原始模式下,输入的字符会立即传输。
  • ECHOECHOE:禁用回显,避免串口收到数据时自动显示。
  • ISIG:禁用信号处理(如 Ctrl+C)。
  • IXON, IXOFF, IXANY:禁用软件流控。
  • OPOST:禁用输出处理,确保字符按原样发送。

通过这些设置,我们能够将串口配置为“原始模式”,确保数据不被系统额外处理。

3.6 配置读取模式
tty.c_cc[VMIN] = 1;   // 至少读取1字节
tty.c_cc[VTIME] = 10; // 超时时间1秒
  • VMIN:设置读取操作中最小的字符数,这里设置为 1,表示读取至少 1 个字符。
  • VTIME:设置超时时间,以十分之一秒为单位。这里设置为 10,相当于 1 秒的超时。

发送与接收数据

3.7 发送数据(sendCommand
ssize_t written = write(fd, command.data(), command.size());
  • write 函数将数据写入串口。command.data() 是数据的起始地址,command.size() 是数据的长度。
  • 如果写入成功,返回实际写入的字节数。如果失败,返回 -1。

为了确保数据发送后能够及时处理,我们使用 std::this_thread::sleep_for 进行短暂延迟。

3.8 接收数据(receiveResponse
ssize_t n = read(fd, response.data(), length);
  • read 函数从串口读取数据,response.data() 是接收缓冲区的起始地址,length 是要读取的字节数。
  • 如果读取成功,返回实际读取的字节数。如果读取失败或超时,返回 -1。

在读取数据后,我们调整接收到的字节数(如果读取的字节数小于预期),并返回接收到的数据。


4. 常见问题排查

在使用串口通信时,可能会遇到一些常见问题。以下是对这些问题的分析、排查方法和解决方案。

4.1 串口通信不稳定或数据丢失

原因分析:
  • 波特率设置不一致:发送端和接收端的波特率必须一致。若设置不一致,可能导致数据错误或丢失。
  • 流控设置不匹配:串口通信通常使用硬件流控(RTS/CTS)或软件流控(XON/XOFF)。如果双方流控设置不匹配,可能会导致数据丢失。
  • 不正确的停止位或数据位:数据位和停止位的设置不匹配,可能导致数据错乱。
排查方法:
  1. 检查波特率:确保发送端和接收端的波特率设置一致。
  2. 检查流控设置:检查 termios 配置中的流控选项,确认双方设置相同。
  3. 确认数据位和停止位:确认 termios 中的数据位和停止位设置正确。
解决方法:
  • 统一波特率设置,检查设备文档确认流控及数据格式。
  • 如有必要,可以禁用流控来简化调试。

4.2 数据不符或乱码

原因分析:
  • 没有设置原始模式:如果串口未配置为原始模式(Raw Mode),可能会有特殊字符(如换行符、回车符等)被操作系统自动处理或转换,从而导致数据不符。
  • 缓冲区未清空:如果在发送和接收数据之前没有清空串口缓冲区,可能会有残留数据影响通信。
排查方法:
  1. 检查原始模式设置:确认 termios 配置中的原始模式设置。
  2. 检查串口缓冲区:使用 tcflush 清空串口缓冲区,确保没有旧数据干扰。
解决方法:
  • 使用原始模式来禁用操作系统对串口数据的干预。
  • 在每次发送和接收前使用 tcflush 清空缓冲区。

4.3 串口无法打开或权限问题

原因分析:
  • 权限不足:在 Linux 中,普通用户可能没有访问串口设备的权限,导致无法打开串口。
  • 串口设备不存在:设备文件路径错误或设备没有正确连接。
排查方法:
  1. 检查设备路径:使用 ls /dev/ 命令确认设备是否存在,如 /dev/ttyS7/dev/ttyUSB0
  2. 检查权限:使用 ls -l /dev/ttyS7 检查权限,确认当前用户有读取和写入权限。
解决方法:
  • 使用 sudo 权限运行程序。
  • 修改设备文件的权限,如使用 chmod 666 /dev/ttyS7 赋予所有用户读写权限。

5. 进阶调试技巧

在实际的串口通信中,除了常见的排查方法外,以下几种调试技巧也非常有用:

5.1 使用串口调试工具

使用如 minicomscreen 等串口调试工具,可以帮助你测试串口连接是否正常,并手动发送和接收数据。它们也能帮助你排查硬件连接问题和通信协议错误。

5.2 分析波形

使用示波器或逻辑分析仪监测 TX 和 RX 引脚的波形,可以帮助你确定是否存在电气连接问题或者信号干扰。

5.3 加强错误处理

在代码中加入更多的错误检查和超时机制,可以帮助你尽早发现问题。例如,使用 selectpoll 等系统调用来处理串口读取超时。


6. 串口通信的优化

6.1 提高通信效率

为了提高串口通信效率,可以通过以下方式优化:

  • 调整缓冲区大小:通过修改 termios 中的输入和输出缓冲区大小,减少数据传输的延迟。
  • 使用多线程:如果需要并发处理多个串口设备,可以使用多线程来提高处理效率。

6.2 高效的错误恢复机制

串口通信过程中可能会出现数据丢失或传输错误,设计一个高效的错误恢复机制至关重要。可以通过协议中的校验和重传机制来确保数据的可靠传输。


7. 总结

在本文中,我们详细介绍了如何使用 C++ 实现串口通信,解释了代码中的每个关键部分,包括如何设置波特率、数据位、停止位和流控等。我们还探讨了常见的通信问题及其排查方法,并提供了调试技巧和优化建议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

yy__xzz

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

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

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

打赏作者

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

抵扣说明:

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

余额充值