在嵌入式Linux系统的UI设计中,比较常见的是使用Qt库来实现。而在Qt中进行程序设计时,也经常会用到串口(UART)通信。现在基于Qt5以上的版本中,集成有串口模块(如QSerialPort),或者使用第三方开发的串口模块控件(如qextserialport等)。但无论采用哪种方式,在Linux系统下对于串口的数据接收都只能使用查询(polling)的方式来实现,而在Windows系统下就可以使用效率较高的所谓事件(envent)方式。查询方式需要CPU反复对串口进行读取,看是否有发送来的可读数据,因此会消耗大量的CPU资源,一般的做法是把串口查询放到一个新建的线程中,以获得较高的效率。而对于事件方式则不同,只要串口接收到数据,就会以事件的方式通知CPU去执行相关的操作,在没有接收到数据时CPU可以做其他事情,所以效率较高,使用起来也很方便。
其实有Qt的官方文档中,并不推荐使用线程的方式来处理,因此给出了其替代的多种方案,其中之一就是使用QSocketNotifier的方式。在Qt4.0及以上的版本中,新增加了一个名为QSocketNotifier的模块,它用来监听系统文件的操作,把操作转换为Qt事件进入系统的消息循环队列,并调用预先设置的事件接受函数来处理事件。这就为Linux下的Qt串口通信提供了另外的解决方案。下面就来讨论一下,结合Qt中的QSocketNotifier模块,如何实现一个通用的、基于事件驱动的串口通信程序。
QSocketNotifier一共设置了三类事件:read、write、exception,具体如下表所示。
在使用QSocketNotifier来处理串口时,只需要设置为Read属性即可。每个QSocketNotifie对象监听一个事件,在使用open方法打开串口并设置好属性后,就可以使用Qt的类 QSocketNotifier来监听串口是否有数据可读,它是事件驱动的,配合Qt的信号/槽机制,当有数据可读时,QSocketNotifier就会发射ativated信号,只需要创建一个槽连接到该信号即可,然后在槽函数中处理串口读到的数据。这样一来,读取串口的任务就不用新开辟线程来处理了,这就是Qt官方给出的建议。
主程序代码如下。
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QSocketNotifier> #include <fcntl.h> #include <termios.h> #include <stdio.h> int fdUart; int len=0,count=0; char read_data[100]; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); readTimer = new QTimer(this); //新建一个定时器,用于接收超时控制 connect(readTimer,SIGNAL(timeout()),this,SLOT(readMyCom())); //定时器连接槽函数 UART_Init(); //串口初始化 QSocketNotifier *m_notifier; m_notifier = new QSocketNotifier(fdUart, QSocketNotifier::Read, this);//新建一个QSocketNotifier对象,用于侦测串口是否有数据可读取 connect(m_notifier, SIGNAL(activated(int)), this, SLOT(remoteDataIncoming(void)));//QSocketNotifier对象连接槽函数 } MainWindow::~MainWindow() { delete ui; } void MainWindow::remoteDataIncoming(void) //串口接收槽函数 { char buff[8]={0,0,0,0,0,0,0,0}; //定义8字节的缓冲区 int lenth=0; //定义接收长度 lenth = read(fdUart,buff,8); //读取串口8字节到缓冲区buff中并返回实际长度 if(lenth>0 && lenth<8) //本次只接收到小于8个字节的数据 { if(count == 0) //本帧数据小于8字节 { len = lenth; for(int i=0;i<lenth;i++) read_data[i] = buff[i]; //把长度和内容赋值给全局变量 } else //上次已经接收到至少8字节数据,本次为最后的小于8字节数据 { readTimer->stop(); //关闭定时器 len = len + lenth; for(int i=0;i<lenth;i++) read_data[i+8*count] = buff[i]; } readMyCom(); //只要接收数据少于8字节,即完成接收 } else if(lenth>0) //接收了8个字节,本帧数据很可能不止8字节 { readTimer->stop(); //关闭定时器 len = len + lenth; for(int i=0;i<8;i++) read_data[i+8*count] = buff[i]; count++; //count为接收到8字节数据的次数 readTimer->start(70); //设置定时为70ms } } void MainWindow::readMyCom(void) //定时超时槽函数 { readTimer->stop(); //关闭定时器 ReadCom(len, read_data); //调用串口接收服务函数并把参数(实际长度及其内容)传递过去 for(int i=0;i<len;i++) //清空接收内容 read_data[i] = 0; len = 0; //长度归零 count = 0; //次数归零 } void MainWindow::ReadCom(int leng, char data[]) //串口接收服务函数 { ui->label->setText(QString::number(leng)); ui->label_2->setText(data); } int MainWindow::UART_Init(void) { fdUart = open("/dev/ttySAC1", O_RDWR | O_NOCTTY | O_NDELAY);//打开串口,配置为可读可写、不分配为控制终端、无延时 printf("fcntl=%d\n",fcntl(fdUart,F_SETFL,0)); //让串口进入阻塞状态 printf("isatty=%d\n",isatty(STDIN_FILENO)); //确定是否为一个终端设备 Setup_Serial(fdUart, 9600, 8, 'N', 1); //进行串口相关配置 return fdUart; } int MainWindow::Setup_Serial(int fd,int nSpeed, int nBits, char nEvent, int nStop)//串口配置函数 { struct termios newtio,oldtio; if (tcgetattr(fd,&oldtio) != 0) { perror("Setup Serial save error!"); return -1; } bzero(&newtio, sizeof(newtio)); newtio.c_cflag |= CLOCAL | CREAD; newtio.c_cflag &= ~CSIZE; switch(nBits) { case 5: newtio.c_cflag |= CS5; break; case 6: newtio.c_cflag |= CS6; break; case 7: newtio.c_cflag |= CS7; break; case 8: newtio.c_cflag |= CS8; break; } switch(nEvent) { case 'O': newtio.c_cflag |= PARENB; newtio.c_cflag |= PARODD; newtio.c_iflag |= (INPCK | ISTRIP); break; case 'E': newtio.c_iflag |= (INPCK | ISTRIP); newtio.c_cflag |= PARENB; newtio.c_cflag &= ~PARODD; break; case 'N': newtio.c_cflag &= ~PARENB; break; } switch(nSpeed) { case 1200: cfsetispeed(&newtio, B1200); cfsetospeed(&newtio, B1200); break; 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; case 57600: cfsetispeed(&newtio, B57600); cfsetospeed(&newtio, B57600); break; case 115200: cfsetispeed(&newtio, B115200); cfsetospeed(&newtio, B115200); break; default: cfsetispeed(&newtio, B9600); cfsetospeed(&newtio, B9600); break; } if(nStop == 1) newtio.c_cflag &= ~CSTOPB; else if (nStop == 2) newtio.c_cflag |= CSTOPB; newtio.c_cc[VTIME] = 0; newtio.c_cc[VMIN] = 0; tcflush(fd,TCIFLUSH); if((tcsetattr(fd,TCSANOW,&newtio))!=0) { perror("Setup Serial error!"); return -1; } printf("Setup Serial complete!\n"); return 0; } void MainWindow::WriteCom(int leng, char data[]) { write(fdUart, data, leng); }
头文件内容如下。
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QTimer> namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private: Ui::MainWindow *ui; QTimer *readTimer; int UART_Init(void); int Setup_Serial(int fd,int nSpeed, int nBits, char nEvent, int nStop); void ReadCom(int leng, char data[]); void WriteCom(int leng, char data[]); private slots: void remoteDataIncoming(void); void readMyCom(void); }; #endif // MAINWINDOW_H
经过实验,上述代码在S3C2416+Linux3.6.6+Qt4.8.7的系统中,波特率从1200到115200,字符数量从1个到100个,都能够实现正常通信,并没有发生数据丢失的情况。