一篇文章解决Linux串口通信 Ubuntu23.0系统

目录

前言

虚拟串口的建立

在C++中实现串口通信

串口通信常见问题汇总

创建端口时显示找不到文件

在虚拟端口中可以输出但无法输出到设备中

Linux/Ubuntu系统常见问题

程序无法关闭时应该怎么办

无法使用sudo命令

参考资料


前言

        写这篇文章的缘由是清明节时参加的校内赛,准备时常常出现不知道输入怎样的命令或者出了问题要怎么做的情况。在被折磨了数天之后终于算是顺利的比完了赛,但是回过头来,为了不让后继者再承受一遍搜索资料的痛苦,所谓前人栽树后人乘凉,特地写下一篇博客介绍怎么用Linux系统实现串口通讯。

        本篇文章的步骤在树莓派的Ubuntu23.0系统上测试成功,而且在树莓派4和5上都能运行(大概),有些问题可能我没有遇到,但是我所见到的问题基本都在这里给出了解决方法。


虚拟串口的建立

        在第一次进行串口通信时,首先要安装socat

sudo apt install socat

        安装完成后输入以下命令创建虚拟串口

socat -d -d pty,raw,echo=0 pty,raw,echo=0

        创建成功后会显示如下的几行,代表端口创建成功,并且已经在1和2两个端口间建立了连接

        此时创建的两个端口就是/dev/pts/1和/dev/pts/2,经过测试,/dev/pts/0端口似乎就是第一个打开的终端,如果选择这个端口进行输出会直接在终端里看见输出结果

        然后新建一个终端进行端口监控,在新建的终端里输入以下命令,通过终端来显示串口通信的输出结果

cat < /dev/pts/1

        再新建一个终端,输入以下命令,如果能在刚才的监控终端里找到输出结果,则说明串口可以正常使用

echo "Hello World!" > /dev/pts/1

        至此,创建虚拟串口的工作完成


在C++中实现串口通信

        由于我们是使用C++进行的编程,因此在这里仅对C++实现串口通信进行说明,python等语言实现串口通信可以参考其他博客的代码,我挑选了几篇放在文章末尾

          在使用串口之前,由于Ubuntu系统中用户需要加入dialout组才能访问ttyS设备,因此需要在终端输入以下命令来获得访问权限(将user_name替换为自己的用户名)

sudo usermod -a -G dialout user_name

Serial.hpp

#pragma once
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#include <limits.h>
#include <string.h>
#include <string>

const long long RecvBufferLen = 1024;   //设置接收数据缓冲区大小

typedef enum
{
    _2400,
    _4800,
    _9600,
    _19200,
    _38400,
    _57600,
    _115200,
    _460800,
}E_BaudRate;  //波特率

typedef enum
{
    _5,
    _6,
    _7,
    _8,
}E_DataSize;  //数据位

typedef enum
{
    None,
    Odd,
    Even,
}E_Parity;  //校验位

typedef enum
{
    _1,
    _2,
}E_StopBit;  //停止位


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

    int OpenSerial(std::string SerialID, E_BaudRate Bps, E_DataSize DataSize, E_Parity Parity, E_StopBit StopBit);

    int Send(unsigned char *Buff, int length);
    int Recv(unsigned char *Buff, int length);

    int Close();

private:
    void RunConnect();
    void RunRecv();
    int RefreshBuffer(unsigned char *pBuf, int Len, bool RecvTypet);

private:
    int nSerialID;  //串口

    bool b_OpenSign;   //串口打开标志

    struct termios ProtoOpt;   //存放串口原始配置
};

Serial.cpp

#include "Serial.hpp"
#include <thread>
#include <iostream>

Serial::Serial()
{
    b_OpenSign = false;
    nSerialID = 0;
}

Serial::~Serial()
{
    Close();
}


int Serial::OpenSerial(std::string SerialID, E_BaudRate Bps, E_DataSize DataSize, E_Parity Parity, E_StopBit StopBit)
{
    Close();
    nSerialID = open( SerialID.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (-1 == nSerialID)
    {
        /* 不能打开串口一*/
        std::string str = SerialID + " open fail !!!";
        perror(str.c_str());
        return -1;
    }
    struct termios Opt;
    tcgetattr(nSerialID, &ProtoOpt);  //获取设备当前的设置

    Opt = ProtoOpt;colorImage

    /*设置输入输出波特率*/
    switch(Bps)
    {
        case E_BaudRate::_2400:
            cfsetispeed(&Opt,B2400);
            cfsetospeed(&Opt,B2400);
            break;
        case E_BaudRate::_4800:
            cfsetispeed(&Opt,B4800);
            cfsetospeed(&Opt,B4800);
            break;
        case E_BaudRate::_9600:
            cfsetispeed(&Opt,B9600);
            cfsetospeed(&Opt,B9600);
            break;
        case E_BaudRate::_19200:
            cfsetispeed(&Opt,B19200);
            cfsetospeed(&Opt,B19200);
            break;
        case E_BaudRate::_38400:
            cfsetispeed(&Opt,B38400);
            cfsetospeed(&Opt,B38400);
            break;
        case E_BaudRate::_57600:
            cfsetispeed(&Opt,B57600);
            cfsetospeed(&Opt,B57600);
            break;
        case E_BaudRate::_115200:
            cfsetispeed(&Opt,B115200);
            cfsetospeed(&Opt,B115200);
            break;
        case E_BaudRate::_460800:
            cfsetispeed(&Opt,B460800);
            cfsetospeed(&Opt,B460800);
            break;
        default		:
            printf("Don't exist baudrate %d !\n",Bps);
            return (-1);
    }

    /*设置数据位*/
    Opt.c_cflag &= (~CSIZE);
    switch( DataSize )
    {
        case E_DataSize::_5:
            Opt.c_cflag |= CS5;
            break;
        case E_DataSize::_6:
            Opt.c_cflag |= CS6;
        case E_DataSize::_7:
            Opt.c_cflag |= CS7;
            break;
        case E_DataSize::_8:
            Opt.c_cflag |= CS8;
            break;
        default:
            /*perror("Don't exist iDataSize !");*/
            printf("Don't exist DataSize %d !\n",DataSize);
            return (-1);
    }

    /*设置校验位*/
    switch( Parity )
    {
        case E_Parity::None:					/*无校验*/
            Opt.c_cflag &= (~PARENB);
            break;
        case E_Parity::Odd:					/*奇校验*/
            Opt.c_cflag |= PARENB;
            Opt.c_cflag |= PARODD;
            Opt.c_iflag |= (INPCK | ISTRIP);
            break;
        case E_Parity::Even:					/*偶校验*/
            Opt.c_cflag |= PARENB;
            Opt.c_cflag &= (~PARODD);
            Opt.c_iflag |= (INPCK | ISTRIP);
            break;
        default:
            /*perror("Don't exist cParity  !");*/
            printf("Don't exist Parity %c !\n",Parity);
            return (-1);
    }

    /*设置停止位*/

    switch( StopBit )
    {
        case E_StopBit::_1:
            Opt.c_cflag &= (~CSTOPB);
            break;
        case E_StopBit::_2:
            Opt.c_cflag |= CSTOPB;
            break;
        default:
            printf("Don't exist iStopBit %d !\n",StopBit);
            return (-1);
    }

    //如果只是串口传输数据,而不需要串口来处理,那么使用原始模式(Raw Mode)方式来通讯,设置方式如下:
    Opt.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    Opt.c_oflag &= ~OPOST;

    tcflush(nSerialID,TCIOFLUSH);		/*刷新输入队列(TCIOFLUSH为刷新输入输出队列)*/

    Opt.c_cc[VTIME] = 0;	/*设置等待时间*/
    Opt.c_cc[VMIN] = 0;	/*设置最小字符*/

    int Result = tcsetattr(nSerialID,TCSANOW,&Opt);  //使这些设置生效

    if( Result )
    {
        perror("Set new terminal description error !");
        return (-1);
    }

    b_OpenSign = true;

    RunRecv();

    return 0;
}

int Serial::Send(unsigned char *Buff, int length)
{
    int iLen = 0;
    if(length <= 0)
    {
        printf("Send byte number error !\n");
        return -1;
    }

    iLen = write(nSerialID,Buff,length);

    return iLen;
}

int Serial::Recv(unsigned char *Buff, int length)
{
    int res =  RefreshBuffer(Buff, length, true);

    return res;
}

int Serial::Close()
{
    if(nSerialID > 0)
    {
        tcsetattr (nSerialID, TCSADRAIN, &ProtoOpt);  //恢复原始串口配置
    }
    close(nSerialID);
    b_OpenSign = false;
}


void Serial::RunRecv()
{
    std::thread ThRecv	= std::thread
            {
                    [&]()
                    {
                        unsigned char RecvBuf[4096] = {0};
                        while (b_OpenSign)
                        {
                            usleep(10*1000);
                            if((nSerialID < 0))
                            {
                                continue;
                            }

                            memset(RecvBuf, 0, 4096);

                            int res = read(nSerialID, RecvBuf, sizeof(RecvBuf));
                            //std::cout << "res = " << res << std::endl;
                            if(res > 0)
                            {
                                RefreshBuffer(RecvBuf, res, false);
                            }
                        }
                    }
            };

    ThRecv.detach();
}


int Serial::RefreshBuffer(unsigned char *pBuf, int Len, bool RecvTypet)
{
    static unsigned char  Buffer[RecvBufferLen + 1] = {0};
    static int 					nSum=0;				    //	缓冲区中数据总长度
    signed int 					nStop=0;

    int ren = 0;

    if(false == RecvTypet)
    {
        //************************ 将接收到的数据加入缓冲区中 ************************/
        //std::cout<<"recv = "<< Len <<std::endl;

        if((Len + nSum) <= RecvBufferLen)		//	总长度小于1K
        {
            memcpy(&Buffer[nSum], pBuf, Len);
            nSum = Len + nSum;
        }
        else
        {
            if(Len <= RecvBufferLen)			//	拷贝满1K空间,丢弃掉aucT[0]开始的字符,并进行填充,!!!!!!!!!!!
            {
                memcpy(Buffer, pBuf, Len);
                nSum = Len;
            }
            else						//	本次接收到的数据长度大于1K
            {
                memcpy(Buffer, pBuf + (Len - RecvBufferLen), RecvBufferLen);
                nSum = RecvBufferLen;
            }
        }
        //std::cout<<"----> nSum = "<< nSum <<std::endl;
        ren = 0;
    }
    else
    {
        if(Len <= 0)
        {
            return -1;
        }
        if(nSum <= 0)
        {
            return 0;
        }

        if(Len <= nSum)
        {
            memcpy(pBuf, Buffer, Len);

            nStop =  Len;
            ren = Len;
        }
        else
        {
            memcpy(pBuf, Buffer, nSum);

            nStop = nSum;
            ren = nSum;
        }


        //************ 移动取出数据 ***************/
        if(nStop==0)
        {
            return 0;
        }
        else if(nSum > nStop)	  // 把没有解析到的数据移动到最开始位置
        {
            for(int i=0; i<(nSum-nStop); i++)
            {
                Buffer[i] = Buffer[nStop + i];
            }
            nSum = nSum - nStop;
        }
        else if(nSum == nStop)
        {
            nSum = 0;
        }

    }

    return ren;
}

main.cpp

#include <iostream>
#include <cstdio>
#include "Serial.hpp"

int main()
{
    Serial s1;

    std::string str = "/dev/pts/3";   //串口号

    s1.OpenSerial(str, E_BaudRate::_115200, E_DataSize::_8, E_Parity::None, E_StopBit::_1);

    while (true)
    {
        unsigned char buff[] = "123456789\r\n" ;
        s1.Send(buff, sizeof(buff));

        unsigned char bf[100] = {0};
        int len = s1.Recv(bf, sizeof(bf));
        //std::cout << "len = " << len << std::endl;
        if(len > 0)
        {
            for(int i=0; i<len; i++)
            {
                printf("%.2X ", bf[i]);
            }
            std::cout << std::endl;
        }
        usleep(100*1000);
    }
    return 0;
}

        这几段代码已经不知道是从哪里找到的了所以就直接拿过来用了,OpenSerial函数中可以设置波特率、数据位、奇校验、偶校验等参数,可以看需要进行调整。使用虚拟端口时可以将端口地址最后的数字更改为别的数字。一般连接其他设备使用的端口都是如下地址

/dev/ttyUSB0
# 可以通过以下方式寻找/dev目录下的所有串口
ls -l

        可以通过字符串类型来传输0-255范围内的数字,如果想要传输其它类型的数据可以参考以下文章

怎样用串口发送结构体-简单协议的封包和解包

        usleep内的数字可以按照需求更改,以此来改变停顿的时长

        在监视虚拟串口的终端内如果找到了buff[]内的内容,则代表传输成功


串口通信常见问题汇总

创建端口时显示找不到文件

        重启大法好

        可以尝试重新建立虚拟端口或者开一个新的终端监视端口,如果没用,建议重启。

在虚拟端口中可以输出但无法输出到设备中

        首先检查两边的传输协议是不是一样的,其次检查OpenSerial函数中各项参数是否与设备匹配。似乎可能有刷新时间不同的问题,可以更改usleep的时间。如果依然解决不了并且虚拟端口中确实可以输出,建议检查连接的设备是否出了问题


Linux/Ubuntu系统常见问题

程序无法关闭时应该怎么办

        一般出现这种问题最可能的就是运行的程序出现了死循环之类的问题,如果系统还能用,就在终端里输入以下命令来关闭程序

killall program_name

        将program_name替换为程序的名称即可。如果是系统卡死建议重启,要不然可能会等上很长很长很长——的时间(本人曾经等了一下午都没反应,最终只能重启)

无法使用sudo命令

        使用虚拟机时可能出现此问题,树莓派目前没遇到过,解决方法是在终端输入su进入root模式,然后再使用sudo(就是输入su两个字母然后回车即可)


参考资料

在Ubuntu上创建虚拟串口

C++实现串口收发和简单校验以及指定Ubuntu系统USB设备

ubuntu16.04 放开串口权限

Linux串口信息查询

怎样用串口发送结构体-简单协议的封包和解包

串口是怎样传输数据的


如果以后遇到其他问题的话都会进行补充,希望能为大家提供一些便利

  • 29
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值