嵌入式Linux串口通讯的C++设计

2019-07-12 16:50发布

嵌入式Linux主板EM9160提供了6个标准异步串口:ttyS1——ttyS6,其中ttyS4、ttyS5、ttyS6和GPIO的管脚复用,每个串口都有独立的中断模式,使得多个串口能够同时实时进行数据收发。各个串口的驱动均已经包含在嵌入式Linux操作系统的内核中,EM9160在嵌入式Linux系统启动完成时,各个串口已作为字符设备完成了注册加载,用户的应用程序可以以操作文件的方式对串口进行读写,从而实现数据收发的功能。   串口编程接口函数           在嵌入式Linux系统下,所有的设备文件都位于“/dev”目录下,EM9160上6个串口所对应的设备名依次为“/dev/ttyS1”——“/dev/ttyS6”。           嵌入式Linux下操作设备的方式和操作文件的方式是一样的:调用open( )打开设备文件,再调用read( )、write( )对串口进行数据读写操作。这里需要注意的是打开串口除了设置普通的读写之外,还需要设置O_NOCTTY和O_NDLEAY,以避免该串口成为一个控制终端,因为如果作为一个终端有可能会影响到用户的进程。打开的方式如下:         sprintf( portname, '/dev/ttyS%d', PortNo ); //PortNo为串口端口号,从1开始
        m_fd = open( portname,O_RDWR | O_NOCTTY | O_NONBLOCK);           作为串口通讯还需要一些通讯参数的配置,包括波特率、数据位、停止位、校验位等参数。在实际的操作中,主要是通过设置struct termios结构体的各个成员值来实现,一般会用到的函数包括:         tcgetattr( ) ;
        tcflush( );
        cfsetispeed( );
        cfsetospeed( );
        tcsetattr( );           其中各个函数的具体使用方法这里就不一一介绍了,用户可以参考嵌入式Linux应用程序开发的相关书籍,也可参看Step2_SerialTest中Serial.cpp模块中set_port( )函数代码。   串口应用的C++设计           Step2 _SerialTest是一个支持异步串口数据通讯的示例,该例程采用了面向对象的C++编程,把串口数据通讯作为一个对象进行封装,用户调用该对象提供的接口函数即可方便地完成串口通讯的操作。           CSerial类介绍         利用上一小节中介绍的串口API函数,封装了一个支持异步读写的串口类CSerial,CSerial类中提供了4个公共函数、一个串口数据接收线程以及数据接收用到的数据Buffer。           class CSerial
        {
        private:
                //通讯线程标识符ID
                pthread_t m_thread;
                // 串口数据接收线程
                static int ReceiveThreadFunc( void* lparam );
        public:
                CSerial();
                virtual ~CSerial();

                int m_fd; // 已打开的串口文件描述符
                int m_DatLen;
                char DatBuf[1500];
                int m_ExitThreadFlag;

                // 按照指定的串口参数打开串口,并创建串口接收线程
                int OpenPort( int PortNo, int baudrate, char databits, char stopbits, char parity );
                // 关闭串口并释放相关资源
                int ClosePort( );
                // 向串口写数据
                int WritePort( char* Buf, int len );
                // 接收串口数据处理函数
                virtual int PackagePro(char* Buf, int len );
        };           OpenPort函数用于根据输入串口参数打开串口,并创建串口数据接收线程。在嵌入式Linux环境中是通过函数pthread_create( )创建线程,通过函数pthread_exit( )退出线程。嵌入式Linux线程属性存在有非分离(缺省)和分离两种,在非分离情况下,当一个线程结束时,它所占用的系统资源并没有被释放,也就是没有真正的终止;只有调用pthread_join( )函数返回时,创建的线程才能释放自己占有的资源。在分离属性下,一个线程结束时立即释放所占用的系统资源。基于这个原因,在我们提供的例程中通过相关函数将数据接收线程的属性设置为分离属性。如:         // 设置线程绑定属性
        res = pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM );
        // 设置线程分离属性
        res += pthread_attr_setdetachstate( &attr, THREAD_CREATE_DETACHED );           ReceiveThreadFunc函数是串口数据接收和处理的主要核心代码,在该函数中调用select( ),阻塞等待串口数据的到来。对于接收到的数据处理也是在该函数中实现,在本例程中处理为简单的数据回发,用户可结合实际的应用修改此处代码,修改PackagePro( )函数即可。流程如下:   串口通讯嵌入式Linux应用程序流程           int CSerial::ReceiveThreadFunc(void* lparam)
        {
                CSerial *pSer = (CSerial*)lparam; 

                //定义读事件集合
                fd_set fdRead;
                int ret;
                struct timeval aTime;

                while( 1 )
                {
                        //收到退出事件,结束线程
                        if( pSer->m_ExitThreadFlag )
                        {
                                break;
                        }
                        FD_ZERO(&fdRead);
                        FD_SET(pSer->m_fd,&fdRead);
                        aTime.tv_sec = 0;
                        aTime.tv_usec = 300000;
                        ret = select( pSer->m_fd+1,&fdRead,NULL,NULL,&aTime );
                        if (ret < 0 )
                        {
                                //关闭串口
                                pSer->ClosePort( ); 
                                break;
                        } 
                        if (ret > 0)
                        {
                                //判断是否读事件
                                if (FD_ISSET(pSer->m_fd,&fdRead))
                                {
                                        //data available, so get it!
                                        pSer->m_DatLen =read( pSer->m_fd, pSer->DatBuf, 1500 );
                                        // 对接收的数据进行处理,这里为简单的数据回发
                                        if( pSer->m_DatLen > 0 )
                                        { 
                                                pSer->PackagePro( pSer->DatBuf, pSer->m_DatLen);
                                        }
                                        // 处理完毕
                                }
                        } 
                } 
                printf( 'ReceiveThreadFunc finished ');
                pthread_exit( NULL ); 
                return 0; 
        }           需要注意的是,select( )函数中的时间参数在嵌入式Linux中每次都需要重新赋值,否则会自动归0。           CSerial类的实现代码请参见Serial.CPP文件。           CSerial类的调用         CSerial类的具体使用也比较简单,主要是对于类中定义的4个公共函数的调用,以下为 Step2_SerialTest.cpp中相关代码。           class CSerial m_Serial;
        int main( int argc,char* argv[] )
        { 
                int i1;
                int portno, baudRate;
                char cmdline[256];

                printf( 'Step2_SerialTest V1.0 ' ); 
                // 解析命令行参数:串口号 波特率
                if( argc > 1 ) strcpy( cmdline, argv[1] );
                else portno = 1;
                if( argc > 2 )
                { 
                        strcat( cmdline, ' ' );
                        strcat( cmdline, argv[2] );
                        scanf( cmdline, '%d %d', &portno, &baudRate );
                }
                else 
                {
                        baudRate = 115200;
                }
                printf( 'port:%d baudrate:%d ', portno, baudRate);
                //打开串口相应地启动了串口数据接收线程
             
   i1 = m_Serial.OpenPort( portno, baudRate, '8','1', 'N');
                if( i1<0 )
                {
                        printf( 'serial open fail ');
                        return -1;
                }
                //进入主循环,这里每隔1s输出一个提示信息
               
for( i1=0; i1<10000;i1++)
                {
                        sleep(1);
                        printf( '%d ', i1+1);
                }
                m_Serial.ClosePort( );
                return 0; 
        }           从上面的代码可以看出,程序的主循环只需要实现一些管理性的功能,在本例程中仅仅是每隔1s输出一个提示信息,在实际的应用中,可以把一些定时查询状态的操作、看门狗的喂狗等操作放在主循环中,这样充分利用了嵌入式Linux多任务的编程优势,利用内核的任务调度机制,将各个应用功能模块化,以便于程序的设计和管理。这里顺便再提一下,在进行多个串口编程时,也可以利用本例程中的CSerial类为基类,根据应用需求派生多个CSerial派生类实例,每一个派生类只是重新实现虚函数PackagePro(…),这样每个串口都具有一个独立的串口数据处理线程,利用Linux内核的任务调度机制以实现多串口通讯功能。     一下是我自己封装的com口类 //com.h /*
 * ICom.h
 *
 *  Created on: 2013-4-2
 *      Author: 高辉力
 */ #ifndef ICOM_H_
#define ICOM_H_
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include
#include "errno.h"
#include "string.h"
#include
#include "sys/time.h"
#include "sys/epoll.h" #define MAXLEN 1024 namespace std { class ICom {
public:
 ICom();
 virtual ~ICom();  bool OpenCom(const int Com_port);//根据端口号打开com口,并创建接收线程
 bool SetCom(const int Baud_rate, const int Data_bits, char Parity, const int Stop_bits);//设置com口  int ComWrite(char *WriteBuff,const int WriteLen);//com口写数据
 int ComRead(char * ReadBuff,const int ReadLen);//com口读数据  virtual void ReadDataProc(char * DataBuff,const int BuffLen);//数据的处理函数。
private:
 int fd;//文件描述符
 int epid; //epoll标识符
 epoll_event event;
 epoll_event events[6];//事件集合
 char RecvBuff[MAXLEN];//接受到的数据
 pthread_t pid;//接受数据线程的Id
 static void * ReadThreadFunction(void * arg);//接受数据的线程函数
 void ComClose();//关闭com口 }; } /* namespace std */
#endif /* ICOM_H_ */
  //com.app /*
 * ICom.cpp
 *
 *  Created on: 2013-4-2
 *      Author: 高辉力
 */ #include "ICom.h" namespace std { /************************************
 功能:构造函数  参数:无  返回值:无
 ***********************************/
ICom::ICom() {
 this->fd = 0;
 this->epid = epoll_create(6);
} /************************************
 功能:根据端口号打开对应的com口,并创建数据接受线程  参数:const int Com_port 端口号  返回值: 0:则失败,否则返回1
 ***********************************/
bool ICom::OpenCom(const int Com_port) {
 bool result = false;
 char pathname[20];  if ((Com_port < 0) || (Com_port > 6)) //判断Com_port是否越界。
   {
  printf("the port is out range");
  return result;
 }  sprintf(pathname, "/dev/ttyS%d", Com_port);  if (fd != 0) { //判断文件__detachstate是否被占用。
  printf("com is busying! ");
  return result;
 }  fd = open(pathname, O_RDWR | O_NOCTTY | O_NDELAY);  if (fd < 0) //如果文件操作符小于0表示打开文件失败。
   {
  printf("Can‘t open serial port");
  return result;
 }
 if (isatty(fd) == 0) //判断文件操作符所代表的文件是否为非终端。
   {
  printf("isatty is not a terminal device");
  return result;
 }
 //下面开始创建接受线程。
 pthread_attr_t attr;
 pthread_attr_init(&attr);
 // 设置线程绑定属性
 int res = pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
 // 设置线程分离属性
 res += pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
 //创建线程
 pthread_create(&pid, &attr, ReadThreadFunction, (void *) this);
 result = true;
 return result;
} /************************************
 功能:配置com口  参数:const int Baud_rate 波特率,const int Data_bits 数据位 , char Parity 校验位,const int Stop_bits 停止位  返回值: 0:则失败,否则返回1
 ***********************************/
bool ICom::SetCom(const int Baud_rate, const int Data_bits, char Parity,
  const int Stop_bits) {
 bool result = false;
 struct termios newtio;  bzero(&newtio, sizeof(newtio));  newtio.c_cflag |= CLOCAL | CREAD;
 newtio.c_cflag &= ~CSIZE;  //设置波特率。
 switch (Baud_rate) {
 case 2400:
  cfsetispeed(&newtio, B2400);
  cfsetospeed(&newtio, B2400);
  break;
 case 4800:
  cfsetispeed(&newtio, B4800);
  cfsetospeed(&newtio, B4800);
  break;
 case 9600:
  cfsetispeed(&newtio, B9600);
  cfsetospeed(&newtio, B9600);
  break;
 case 19200:
  cfsetispeed(&newtio, B19200);
  cfsetospeed(&newtio, B19200);
  break;
 case 38400:
  cfsetispeed(&newtio, B38400);
  cfsetospeed(&newtio, B38400);
  break;
 default:
 case 115200:
  cfsetispeed(&newtio, B115200);
  cfsetospeed(&newtio, B115200);
  break;
 }  //设置数据位,只支持7,8
 switch (Data_bits) {
 case 7:
  newtio.c_cflag |= CS7;
  break;
 case 8:
  newtio.c_cflag |= CS8;
  break;
 default:
  printf("Unsupported Data_bits ");
  return result;
 }  //设置校验位
 switch (Parity) {
 default:
 case 'N':
 case 'n': {
  newtio.c_cflag &= ~PARENB;
  newtio.c_iflag &= ~INPCK;
 }
  break;
 case 'o':
 case 'O': {
  newtio.c_cflag |= (PARODD | PARENB);
  newtio.c_iflag |= INPCK;
 }
  break;
 case 'e':
 case 'E': {
  newtio.c_cflag |= PARENB;
  newtio.c_cflag &= ~PARODD;
  newtio.c_iflag |= INPCK;
 }
  break;  case 's':
 case 'S': {
  newtio.c_cflag &= ~PARENB;
  newtio.c_cflag &= ~CSTOPB;
 }
  break;
 }
 //设置停止位,值为1 or 2
 switch (Stop_bits) {
 case 1: {
  newtio.c_cflag &= ~CSTOPB;
  break;
 }
 case 2: {
  newtio.c_cflag |= CSTOPB;
  break;
 }
 default:
  printf("Unsupported Stop_bits. ");
  return result;  }
 //设置最少字符和等待时间,对于接收字符和等待时间没有特别的要求时,可设为0:
 newtio.c_cc[VTIME] = 0;
 newtio.c_cc[VMIN] = 0;  //刷清输入和输出队列
 tcflush(0, TCIOFLUSH);  //激活配置,TCSANOW表示更改后立即生效。
 if ((tcsetattr(fd, TCSANOW, &newtio)) != 0) { //判断是否激活成功。
  printf("Com set error ");
  return result;
 }
 result = true;
 return result;
}
/************************************
 功能:数据接收线程的函数  参数:对象指针  返回值: 无
 ***********************************/
void * ICom::ReadThreadFunction(void *arg) {
 printf("------------start ReadThread-------------- ");  ICom *com = (ICom*) arg;  //epoll设置
 com->event.data.fd = com->fd;
 com->event.events = EPOLLET | EPOLLIN;
 if (epoll_ctl(com->epid, EPOLL_CTL_ADD, com->fd, &com->event) != 0) { //将读事件添加到epoll的事件队列中
  printf("set epoll error! ");
  return NULL;
 }
 printf("------------set epoll ok!----------------- ");  //下面开始epoll等待
 int i = 0, waiteNum = 0;
 while (true) {
  waiteNum = epoll_wait(com->epid, com->events, 0, 0);
  for (i = 0; i < waiteNum; i++) {
    if (com->events[i].events & EPOLLIN) { //判断是否有数据进入
    //接受数据
    com->ComRead(com->RecvBuff, MAXLEN);
    //数据处理
    com->ReadDataProc(com->RecvBuff, MAXLEN);
   }
  }
 }
 return NULL;
}
/************************************
 功能:数据处理的虚函数,可以在子类中实现  参数:char * DataBuff 存储数据指针, const int BuffLen 数据长度  返回值: 无
 ***********************************/
void ICom::ReadDataProc(char * DataBuff, const int BuffLen) {  printf("I am ReadDataProc! ");
 printf("RecvData is %s ", DataBuff);
} /************************************
 功能:Com口读数据  参数:char * ReadBuff 存储读出的数据,const int ReadLen 读出的长度  返回值:实际读取长度,-1表示读取失败
 ***********************************/
int ICom::ComRead(char * ReadBuff, const int ReadLen) {
 if (fd < 0) { //判断操作符是否打开
  printf("Com error! ");
  return -1;
 }
 int len = 0;
 int rdlen = 0;
 while (true) {
  rdlen = read(fd, ReadBuff + len, ReadLen);
  len += rdlen;
  if (len == ReadLen) {
   return len;
  }
 }
 return len;
}
/************************************
 功能:Com口读数据  参数:char * WriteBuff 写入的数据,const int ReadLen 写入的长度  返回值:写入长度,-1表示写入失败
 ***********************************/
int ICom::ComWrite(char *WriteBuff, const int WriteLen) {
 if (fd < 0) { //判断操作符是否打开
  printf("Com error! ");
  return -1;
 }
 int wlen = write(fd, WriteBuff, WriteLen);
 return wlen;
} /************************************
 功能:Com关闭  参数:无  返回值:无
 ***********************************/
void ICom::ComClose() {
 if (this->fd > 0) { //判断文件描述符是否存在
  close(this->fd);
 }
} /************************************
 功能:析构函数  参数:无  返回值:无
 ***********************************/
ICom::~ICom() {
 ComClose();
} } /* namespace std */