C++实现串口通信中数据指令的发送与接收(附C++代码及详细图文教程)

在这里插入图片描述

💪 图像算法工程师,专业从事且热爱图像处理,图像处理专栏更新如下👇:
📝《图像去噪》
📝《超分辨率重建》
📝《语义分割》
📝《风格迁移》
📝《目标检测》
📝《图像增强》
📝《模型优化》
📝《模型实战部署》
📝《图像配准融合》
📝《数据集》
📝《高效助手》
📝《C++》
📝《Qt》


在这里插入图片描述

串口通信(Serial Communication),就是一种数据一位一位(逐位)顺序发送和接收的通信方式,是计算机和外部设备(如传感器、单片机、控制器)之间常见的通信方式之一。

一、串口通信

可以把串口通信想象成:两个人通过“对讲机”说话,每次只能说一个字(1位),对方按顺序接收这些字,然后组成完整的信息。

1.1 串口通信特点

串口通信特点见下:

在这里插入图片描述

1.2 串口通信的基本引脚

串口通信的基本引脚,以RS-232为例:

在这里插入图片描述

实例引脚图片见下:

在这里插入图片描述

1.3 串口通信中的数据结构

一个完整的串口数据帧一般包括:

[起始位][数据位][校验位][停止位]

参数解析:

起始位:通常是一个 0,告诉对方“我要开始发数据了”
数据位:一般是 8 位(1个字节)
校验位:可选,用来检测数据有没有出错(奇偶校验)
停止位:1或2位 1,表示“数据发完了”

1.4 串口通信分类

1.4.1 同步串口通信

同步串口通信,有时钟信号(clock),发送接收都依靠统一节奏。
比如 SPI、I2C。

1.4.2 异步串口通信

用的比较多的为异步串口通信,无时钟,通过起始位、停止位等方式同步。
比如 RS-232、RS-485、TTL 串口等。

1.5 常见串口通信接口

常见串口通信接口见下:

在这里插入图片描述

1.6 通俗理解串口通信

结合上面的串口通信原理,下面举一个通俗理解串口通信的例子:

假设有一个串口设备(比如一个智能灯控制板),你用 C++ 程序通过串口向它发送一条指令,让它开灯。

1.6.1 设计协议

约定:每条数据格式是这样的:[帧头][命令][参数][校验]

帧头:0xAA
命令:“打开灯”是 0x01
参数:0x01 表示第1盏灯
校验:简单加法校验 = 命令 + 参数
示例指令:0xAA 0x01 0x01 0x02

1.6.2 发送数据

用串口程序发送这4个字节的数据给设备:

unsigned char buf[] = {0xAA, 0x01, 0x01, 0x02};
write(serial_fd, buf, 4); // 假设 serial_fd 是串口设备句柄

假设小明通过对讲机说:开始、打开灯、灯1、确认。

1.6.3 接收数据

设备收到数据后:

检查帧头是不是 0xAA;
读取命令 0x01,查表知道是“打开灯”;
读取参数 0x01,知道是“第1盏灯”;
校验是否正确(0x01+0x01 = 0x02),OK!

执行动作后,设备可能再通过串口回一条响应:0xAA 0x81 0x01 0x82 表示“灯1打开成功”。

二、串口通信实例

2.1 C++代码

参考了博文https://blog.csdn.net/weixin_44353958/article/details/104156394中的代码,感谢作者,结合我自己的需求修改整合成一套代码,我主要是通过发送十六进制指令到硬件板子上,控制相机的快门开和关。

2.1.2 参数修改

使用代码过程需要修改的参数见下。

2.1.2.1 端口号

注意:在 Windows 系统中,CreateFile 打开串口时:对于 COM1 到 COM9,你可以直接写成:
CreateFile(“COM1”, …),但是如果是 COM10 及以上(如 COM17),你必须写成:
CreateFile(“\\.\COM17”, …)。

下面是代码中修改端口号对应位置:

在这里插入图片描述

2.1.2.2 指令发送

待发送的指令,要先明确好自己需要以什么数据发送,对应的定义数据类型,见下:

在这里插入图片描述

2.1.2.3 数据接收

数据的接收格式和长度可以自定义调整,注意不要丢包。

在这里插入图片描述

2.1.3 完整代码

C++实现串口通信中数据指令的发送与接收完整代码见下:

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <WinSock2.h>
#include <windows.h>

#include <iostream>
#include <string>
#include <thread> // 包含线程相关功能,用于延时
#include <chrono> // 包含时间单位定义

#include <vector>

using namespace std;

// === WZSerialPort 类的定义 ===
#ifndef _WZSERIALPORT_H
#define _WZSERIALPORT_H

class WZSerialPort
{
public:
    WZSerialPort();
    ~WZSerialPort();

    // 打开串口,成功返回true,失败返回false
    // 参数:端口号、波特率、校验位、数据位、停止位、同步/异步标志
    bool open(const char* portname, int baudrate = 115200, char parity = 0, char databit = 8, char stopbit = 1, char synchronizeflag = 1);

    // 关闭串口
    void close();

    // 发送数据,成功返回发送数据长度,失败返回0
    int send(string dat);

    // 接收数据,成功返回读取的实际数据,失败返回空字符串
    string receive();

private:
    int pHandle[16]; // 用于存储串口句柄
    char synchronizeflag; // 同步或异步标志
};

#endif

// === WZSerialPort 类的实现 ===
WZSerialPort::WZSerialPort()
{
    // 构造函数,初始化成员变量
}

WZSerialPort::~WZSerialPort()
{
    // 析构函数,释放资源
}

// 打开串口
bool WZSerialPort::open(const char* portname, int baudrate, char parity, char databit, char stopbit, char synchronizeflag)
{
    this->synchronizeflag = synchronizeflag; // 设置同步/异步标志
    HANDLE hCom = NULL; // 串口句柄

    if (this->synchronizeflag)
    {
        // 同步方式打开串口
        hCom = CreateFileA(portname, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
    }
    else
    {
        // 异步方式打开串口
        hCom = CreateFileA(portname, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
    }

    if (hCom == (HANDLE)-1)
    {
        // 打开串口失败
        return false;
    }

    // 配置串口缓冲区大小
    if (!SetupComm(hCom, 1024, 1024))
    {
        return false;
    }

    // 配置串口参数
    DCB p;
    memset(&p, 0, sizeof(p));
    p.DCBlength = sizeof(p);
    p.BaudRate = baudrate; // 设置波特率
    p.ByteSize = databit; // 设置数据位

    // 设置校验位
    switch (parity)
    {
    case 0:
        p.Parity = NOPARITY; // 无校验
        break;
    case 1:
        p.Parity = ODDPARITY; // 奇校验
        break;
    case 2:
        p.Parity = EVENPARITY; // 偶校验
        break;
    case 3:
        p.Parity = MARKPARITY; // 标记校验
        break;
    }

    // 设置停止位
    switch (stopbit)
    {
    case 1:
        p.StopBits = ONESTOPBIT; // 1位停止位
        break;
    case 2:
        p.StopBits = TWOSTOPBITS; // 2位停止位
        break;
    case 3:
        p.StopBits = ONE5STOPBITS; // 1.5位停止位
        break;
    }

    // 应用串口配置
    if (!SetCommState(hCom, &p))
    {
        // 设置参数失败
        return false;
    }

    // 配置超时设置
    COMMTIMEOUTS TimeOuts;
    TimeOuts.ReadIntervalTimeout = 1000; // 读间隔超时
    TimeOuts.ReadTotalTimeoutMultiplier = 500; // 读时间系数
    TimeOuts.ReadTotalTimeoutConstant = 5000; // 读时间常量
    TimeOuts.WriteTotalTimeoutMultiplier = 500; // 写时间系数
    TimeOuts.WriteTotalTimeoutConstant = 2000; // 写时间常量
    SetCommTimeouts(hCom, &TimeOuts);

    // 清空串口缓冲区
    PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR);

    // 保存串口句柄
    memcpy(pHandle, &hCom, sizeof(hCom));

    return true;
}

// 关闭串口
void WZSerialPort::close()
{
    HANDLE hCom = *(HANDLE*)pHandle; // 获取串口句柄
    CloseHandle(hCom); // 关闭句柄
}

// 发送数据
int WZSerialPort::send(string dat)
{
    HANDLE hCom = *(HANDLE*)pHandle; // 获取串口句柄

    if (this->synchronizeflag)
    {
        // 同步方式发送数据
        DWORD dwBytesWrite = dat.length(); // 要发送的数据长度
        BOOL bWriteStat = WriteFile(hCom, (char*)dat.c_str(), dwBytesWrite, &dwBytesWrite, NULL);
        if (!bWriteStat)
        {
            // 发送失败
            return 0;
        }
        return dwBytesWrite; // 返回成功发送的字节数
    }
    else
    {
        // 异步方式发送数据
        DWORD dwBytesWrite = dat.length();
        DWORD dwErrorFlags;
        COMSTAT comStat;
        OVERLAPPED m_osWrite;

        memset(&m_osWrite, 0, sizeof(m_osWrite));
        m_osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, L"WriteEvent");

        ClearCommError(hCom, &dwErrorFlags, &comStat);
        BOOL bWriteStat = WriteFile(hCom, (char*)dat.c_str(), dwBytesWrite, &dwBytesWrite, &m_osWrite);
        if (!bWriteStat)
        {
            if (GetLastError() == ERROR_IO_PENDING)
            {
                // 等待异步操作完成
                WaitForSingleObject(m_osWrite.hEvent, 1000);
            }
            else
            {
                // 发送失败
                ClearCommError(hCom, &dwErrorFlags, &comStat);
                CloseHandle(m_osWrite.hEvent);
                return 0;
            }
        }
        return dwBytesWrite; // 返回成功发送的字节数
    }
}

// 接收数据
string WZSerialPort::receive()
{
    HANDLE hCom = *(HANDLE*)pHandle; // 获取串口句柄
    string rec_str = ""; // 用于存储接收的数据
    char buf[1024]; // 缓冲区
    DWORD wCount = 1024; // 缓冲区大小

    if (this->synchronizeflag)
    {
        // 同步方式接收数据
        BOOL bReadStat = ReadFile(hCom, buf, wCount, &wCount, NULL);
        if (bReadStat)
        {
            for (int i = 0; i < wCount; i++) {
                // 过滤 0x0D 和 0x0A
                if (buf[i] == 0x0D || buf[i] == 0x0A) {
                    continue;
                }
                rec_str += buf[i];
            }
        }
        
        return rec_str; // 返回接收的数据
    }
    else
    {
        // 异步方式接收数据
        DWORD wCount = 1024;
        DWORD dwErrorFlags;
        COMSTAT comStat;
        OVERLAPPED m_osRead;

        memset(&m_osRead, 0, sizeof(m_osRead));
        m_osRead.hEvent = CreateEvent(NULL, TRUE, FALSE, L"ReadEvent");

        ClearCommError(hCom, &dwErrorFlags, &comStat);
        if (!comStat.cbInQue)
            return ""; // 如果输入缓冲区为空,返回空字符串

        BOOL bReadStat = ReadFile(hCom, buf, wCount, &wCount, &m_osRead);
        if (!bReadStat) {
            if (GetLastError() == ERROR_IO_PENDING) {
                GetOverlappedResult(hCom, &m_osRead, &wCount, TRUE);
            }
            else {
                ClearCommError(hCom, &dwErrorFlags, &comStat);
                CloseHandle(m_osRead.hEvent);
                return "";
            }
        }

        // 关键修改:遍历实际读取的字节数 wCount,过滤 0x0D 和 0x0A
        for (DWORD i = 0; i < wCount; i++) {
            // 跳过 0x0D(回车)和 0x0A(换行)
            if (buf[i] == 0x0D || buf[i] == 0x0A) {
                continue;
            }
            rec_str += buf[i];
        }

        // 确保释放 OVERLAPPED 事件句柄(避免内存泄漏)
        CloseHandle(m_osRead.hEvent);

        return rec_str;
    }
}

// === 主函数 ===
int main()
{
    WZSerialPort w; // 创建串口对象

    // 打开串口,这里以 COM17 为例
    if (w.open("\\\\.\\COM19"))
    {
        cout << "串口打开成功" << endl;

        // 发送指令
        string instruction_send = "aa 55 55 aa 80 02 02 00 04 ff";
        string instruction_send = "aa 55 55 aa 80 02 02 01 00 ff";    // 关闭快门
        string instruction_send = "aa 55 55 aa 80 02 02 02 00 ff";    // 打开快门
        //string instruction_send = "aa 55 55 aa 81 01 00 00 00 ff";    // 发送指令后返回foc数据
        /*string instruction_send = "aa 55 55 aa 80 02 02 00 04 ff";
        w.send(instruction_send);
        std::cout << "成功发送指令:" << instruction_send << std::endl;*/

        // 十六进制指令数组
        std::vector<unsigned char> instruction_send = {
            0xAA, 0x55, 0x55, 0xAA, 0x81, 0x01, 0x00, 0x00, 0x00, 0xFF                   // 获取机芯的返回数据
            
            //0xAA, 0x55, 0x55, 0xAA, 0x80, 0x02, 0x02, 0x01, 0x00, 0xFF                // 关闭快门
            //0xAA, 0x55, 0x55, 0xAA, 0x80, 0x02, 0x02, 0x02, 0x00, 0xFF                // 打开快门
        };

        // 发送指令    将 vector 转换为 string,明确指定长度
       // w.send(reinterpret_cast<const char*>(instruction_send.data()));   // 原始写法
        std::string data_str(reinterpret_cast<const char*>(instruction_send.data()), instruction_send.size());
        w.send(data_str);
        std::cout << "成功发送16进制指令" << std::endl;

    }
    else
    {
        cout << "串口打开失败" << endl;
    }

    // 无限循环接收数据
    while (true)
    {
        //std::string msg = w.receive(); // 接收数据
        //std::cout << "receive (" << msg.size() << " bytes): " << msg << std::endl;

         延时1秒
        //std::this_thread::sleep_for(std::chrono::seconds(1));

        std::string msg = w.receive();
        std::cout << "Received " << msg.size() << " bytes: ";
        for (char c : msg) {
            printf("%02X ", (unsigned char)c);
        }
        std::this_thread::sleep_for(std::chrono::seconds(1));   // 示延时1秒。如果需要更短的时间,可以使用std::chrono::milliseconds(100)表示延时100毫秒。

        std::cout << std::endl;
    }

    return 0;
}

2.2 串口通信测试

2.2.1 设备准备

测试本教程代码发送指令是否成功,先准备两台电脑,一个串口模块和杜邦线,还有ATK XCOM通信软件,见下:

在这里插入图片描述

解释一下为什么要用两台电脑:同一台电脑上同一个端口,假设被ATK XCOM软件占用,运行代码时就找不到此端口了,串口冲突。

串口模型接线图见上面1.2小节。

2.2.2 运行代码收发指令

在第一台电脑上运行上面2.1.3中C++脚本,发送指令:aa 55 55 aa 80 02 02 01 00 ff,指令发送成功见下:

在这里插入图片描述

第二台电脑上接收到的指令见下:

在这里插入图片描述

从第二台电脑上发送直接到第一台电脑的终端接收,见下:

在这里插入图片描述

第一台电脑终端中接收到的指令见下:

在这里插入图片描述

通过上面测试方法,证明能准确发送十六进制指令,连接相机测试,通过此代码,可以准确控制相机快门开关。

三、总结

以上介绍了什么是串口通信,已经给出了详细代码和收发实例讲解C++实现串口通信,希望能帮到你!

学者在使用此代码时,要结合自己的指令需求调整修改代码,我在调试期间遇到了丢数据的问题,代码中已经解决了此问题。

感谢您阅读到最后!😊总结不易,多多支持呀🌹 点赞👍收藏⭐评论✍️,您的三连是我持续更新的动力💖

关注下面「视觉研坊」,获取干货教程、实战案例、技术解答、行业资讯!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

视觉研坊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值