linux串口通信封装(C++)
linux 下使用 C++ 封装串口操作。
1.linux串口权限更改
-
查看串口使用情况,linux下一切皆文件,串口在 /dev/tty*,使用ls查看:
ls -l /dev/tty*
-
对于非 root 用户,Linux串口默认不开放读写权限,可以使用文件授权方式,进行临时授权:
sudo chmod 666 串口号 #例如:sudo chmod 666 ttyS0
-
加入用户组的方式永久获得权限,用ls -l 方式可以查询到串口所在用户组,把需要授权的用户加入到用户组即可完成授权:
sudo usermod -aG 组 用户 #例如: sudo usermod -aG dialout ch
2.封装思路
-
设计一个 Serial 类,将串口的相关操作(打开、关闭、发送、接收)放到该类中去实现
-
在 Serial 类中,对于打开串口的接口(OpenSerial),用户可以自定义串口号、波特率、数据位、校验位、停止位;
int OpenSerial(std::string SerialID, E_BaudRate Bps, E_DataSize DataSize, E_Parity Parity, E_StopBit StopBit);
-
将波特率、数据位、校验位、停止位分别放到一个枚举中,防止用户放飞自我的输入参数,如下所示,我这里只设置了常见的情况,有其他需求可以自行添加:
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; //停止位
-
对于数据的接收和发送,这里的接口设置为,用户传入一个 unsigned char* 类型的数据和数据的长度,之所以这样,主要是为了适配二进制的传输:
int Send(unsigned char *Buff, int length); int Recv(unsigned char *Buff, int length);
-
对于接收数据,为了能让接收和发送分离,打开串口时采用非阻塞方式,另外设置一个接收数据缓冲buff,用户可以更具数据传输的大小,设置缓冲区大小,再在Serial类中开启一个专门进行接收串口数据操作的线程,改线程一直去接收数据,并将接收到的数据放入缓冲区,提供一个接口让用户去缓冲区拿接收到的数据就可以了,达到接收和发送彻底分离的效果:
const long long RecvBufferLen = 1024; //设置接收数据缓冲区大小
3.上代码
-
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; /*设置输入输出波特率*/ 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; }
4.简单测试
-
main.cpp
#include <iostream> #include <cstdio> #include "Serial.hpp" int main() { Serial s1; std::string str = "/dev/ttyS0"; //串口号 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); } std::cout << "hello world" << std::endl; return 0; }
-
编译,Makefile:
CCC=g++ OUTPUT=run LIBS := -lm -lpthread #第三方库 LibOS:= -L ./lib #动态链接库OS OBJS = main.o Serial.o $(OUTPUT): $(OBJS) $(CCC) $(OBJS) -o $(OUTPUT) $(LIBS) $(LibOS) %.o:%.cpp $(CCC) -c -o $@ $^ -std=c++11 clean: rm $(OBJS) rm $(OUTPUT)
-
该封装可扩展性比较高,有其他需求可自行扩展;