在嵌入式Linux系統的UI設計中,比較常見的是使用Qt庫來實現。而在Qt中進行程序設計時,也經常會用到串口(UART)通信。現在基於Qt5.1以上的版本中,集成有串口模塊(如QSerialPort),或者使用第三方開發的串口模塊控件(如qextserialport等)。但無論采用哪種方式,在Linux系統下對於串口的數據接收都只能使用查詢(Polling)的方式來實現,而在Windows系統下就可以使用效率較高的所謂事件驅動(EventDriven)方式。查詢方式需要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個,都能夠實現正常通信,並沒有發生數據丟失的情況。