1、概述
一臺(tái)典型的工控設(shè)備通常包括若干通訊接口(網(wǎng)絡(luò)、串口、CAN等),以及若干數(shù)字IO、AD通道等。運(yùn)行于設(shè)備核心平臺(tái)的應(yīng)用程序通過操作這些接口,實(shí)現(xiàn)特定的功能。通常為了高效高精度完成整個(gè)通訊控制流程,應(yīng)用程序采用C/C++語言來編寫。圖1表現(xiàn)了典型工控設(shè)備的組成關(guān)系。
典型工控設(shè)備框圖
工控設(shè)備的另一個(gè)特點(diǎn)是鑒于設(shè)備大多是24小時(shí)連續(xù)運(yùn)行,且無人值守,所以基本的工控設(shè)備是無顯示的。英創(chuàng)的工控主板ESM6800、ESM335x等都大量的應(yīng)用于這類無頭工控設(shè)備之中。
在實(shí)際應(yīng)用中,部分客戶需要基于已有的無頭工控設(shè)備,增加顯示界面功能,以滿足新的應(yīng)用需求。顯然保持已有的基本工控處理程序不變,通過相對獨(dú)立的技術(shù)手段來實(shí)現(xiàn)顯示功能,最符合客戶的利益訴求。為此我們發(fā)展了一種雙進(jìn)程的程序設(shè)計(jì)方案來滿足客戶的這一需求。該方案的第一個(gè)進(jìn)程,以客戶已有的用C/C++寫的基礎(chǔ)工控進(jìn)程為基礎(chǔ),僅增加一個(gè)面向本地IP(127.0.0.1)的偵聽線程,用于向顯示進(jìn)程提供必要的運(yùn)行工況數(shù)據(jù)。圖2為增添了服務(wù)線程的工控進(jìn)程:
帶有偵聽線程的基礎(chǔ)工控進(jìn)程
方案的第二個(gè)進(jìn)程則主要用于實(shí)現(xiàn)顯示界面,可以采用各種手段來實(shí)現(xiàn),本文中介紹了使用Qt的QML語言加通訊插件的界面設(shè)計(jì)方法。第二個(gè)進(jìn)程(具體是通訊插件單元)通過本地IP,以客戶端方式與基礎(chǔ)工控進(jìn)程進(jìn)行Socket通訊,完成進(jìn)程間數(shù)據(jù)交換。顯示進(jìn)程以及與工控進(jìn)程的關(guān)系如圖3所示:
顯示進(jìn)程與工控進(jìn)程
2、系統(tǒng)設(shè)計(jì)
鑒于工業(yè)控制領(lǐng)域?qū)ο到y(tǒng)運(yùn)行的穩(wěn)定性要求,控制系統(tǒng)更加傾向于將底層硬件控制部分與上層界面顯示分開,兩部分以雙進(jìn)程的形式各自獨(dú)立運(yùn)行。底層硬件控制部分將會(huì)監(jiān)控系統(tǒng)硬件,管理外設(shè)等,同時(shí)收集系統(tǒng)的狀態(tài);而上層界面顯示部分主要用于顯示系統(tǒng)狀態(tài),并實(shí)現(xiàn)少量的系統(tǒng)控制功能,方便維護(hù)人員查看系統(tǒng)運(yùn)行狀態(tài)并且根據(jù)當(dāng)前狀態(tài)進(jìn)行系統(tǒng)的調(diào)整。由于顯示界面不一定是所有設(shè)備都配置,而且顯示部分的程序更加復(fù)雜,從而更容易出現(xiàn)程序運(yùn)行時(shí)的錯(cuò)誤,將控制與顯示分開能夠避免由于顯示部分的程序問題而影響到整個(gè)控制系統(tǒng)的運(yùn)行,而且沒有配置顯示屏的設(shè)備也可以直接運(yùn)行底層的控制程序,增加了系統(tǒng)程序的兼容性。顯示與控制分離后,由于顯示界面程序不需要處理底層硬件的管理控制,在設(shè)計(jì)時(shí)可以更加注重于界面的美化,而且界面程序可以采用不同的編程語言進(jìn)行開發(fā),比如使用Qt C++或者Android java,本文將介紹基于Linux + Qt的雙進(jìn)程示例程序供客戶在實(shí)際開發(fā)中參考,關(guān)于Android程序請參考我們官網(wǎng)的另一篇文章:《Android雙應(yīng)用進(jìn)程Demo程序設(shè)計(jì)》。
如上圖所示。整個(gè)系統(tǒng)分為控制和顯示兩個(gè)進(jìn)程,底層硬件控制部分可以獨(dú)立運(yùn)行,使用多線程管理不同的硬件設(shè)備,監(jiān)控硬件狀態(tài),將狀態(tài)發(fā)送給socket服務(wù)器,并且從socket服務(wù)器接收命令來更改設(shè)備狀態(tài)。Socket服務(wù)器也是一個(gè)獨(dú)立的線程,通過本地網(wǎng)絡(luò)通信集中處理來自硬件控制線程以及顯示程序的消息。顯示界面需要連接上socket服務(wù)器才能正確的顯示設(shè)備的狀態(tài),同時(shí)提供必須的人工控制接口,供設(shè)備使用過程中人為調(diào)整設(shè)備運(yùn)行狀態(tài)。目前在ESM6802工控主板上,界面程序可以采用Qt C++編寫,也可以使用Android java進(jìn)行開發(fā),本文僅介紹采用Qt的界面程序。顯示程序界面用QML搭建,與底層通信的部分用獨(dú)立的Qt QML插件實(shí)現(xiàn),這樣顯示部分進(jìn)一部分離為數(shù)據(jù)處理和界面開發(fā),使得界面設(shè)計(jì)可以更加快捷。程序的整體界面效果如下圖所示:
目前我們只提供了串口(SERIAL)和GPIO兩部分的例程。下面將集中介紹程序中通過本地IP實(shí)現(xiàn)兩個(gè)進(jìn)程通信的部分供客戶在實(shí)際開發(fā)中參考。
3、控制端C程序
控制端程序主要分為兩個(gè)部分,一個(gè)部分用于控制具體的硬件運(yùn)行(下文稱為控制器),另一個(gè)部分為socket服務(wù)器,用于與顯示程序之間進(jìn)行通信。由于本方案主要是為了展示在已有控制程序的基礎(chǔ)上,增加顯示界面功能,以滿足新的應(yīng)用需求,所以我們在此重點(diǎn)介紹在已有控制程序中加入socket服務(wù)器的部分,不再詳細(xì)介紹各硬件的具體控制的實(shí)現(xiàn)。
增加本地IP通信的功能,首先需要在控制進(jìn)程中新加入一個(gè)socket服務(wù)器線程,用于消息的集中管理,實(shí)現(xiàn)底層硬件與上層的界面程序的信息交換,socket服務(wù)器線程運(yùn)行的函數(shù)體代碼如下:
staticvoid*_init_server(void*param) { intserver_sockfd, client_sockfd; intserver_len; structsockaddr_inserver_address; structsockaddr_inclient_address; server_sockfd = socket(AF_INET, SOCK_STREAM, 0); server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = inet_addr("127.0.0.1");//通過本地ip通信 server_address.sin_port = htons(9733); server_len =sizeof(server_address); bind(server_sockfd, (structsockaddr*)&server_address, server_len); listen(server_sockfd, 5); intres; pthread_t client_thread; pthread_attr_t attr; charid[4]; client_element*client_t; while(1) { if(!client_has_space(clients)) { printf("to many client, wait for one to quit...\n"); sleep(2); continue; } printf("server waiting\n"); client_sockfd = accept(server_sockfd, (structsockaddr*)&client_address, (socklen_t *)&server_len); //get and save client id read(client_sockfd, &id, 4); if((id[0]!='I') && (id[1]!='D')) { printf("illegal client id, drop it\n"); close(client_sockfd); continue; } client_t = accept_client(clients, id, client_sockfd); printf("client: %s connected\n", id); //create a new thread to handle this connection res = pthread_attr_init(&attr); if( res!=0 ) { printf("Create attribute failed\n"); } //設(shè)置線程綁定屬性 res = pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM ); //設(shè)置線程分離屬性 res += pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_DETACHED ); if( res!=0 ) { printf("Setting attribute failed\n"); } res = pthread_create( &client_thread, &attr, (void*(*) (void*))socked_thread_func, (void*)client_t ); if( res!=0 ) { close( client_sockfd ); del_client(clients, client_sockfd); continue; } pthread_attr_destroy( &attr ); } } |
此函數(shù)創(chuàng)建一個(gè)socket用于監(jiān)聽(listen)等待顯示程序連接,當(dāng)接受(accept)一個(gè)連接之后創(chuàng)建一個(gè)新的線程用于消息處理,主要用于維護(hù)socket連接的狀態(tài),解析消息的收發(fā)方,并將消息轉(zhuǎn)送到對應(yīng)的接收方,在顯示程序建立連接之前或者連接斷開之后,控制器發(fā)送的消息將不會(huì)進(jìn)行發(fā)送了,而控制器依然在正常運(yùn)行,用于處理消息的新線程如下:
staticvoid*socked_thread_func(void*p) { client_element*client_p = (client_element*)p; printf("started socked_thread_func for client: %s\n", client_p->id); fd_set fdRead; intret, lenth; structtimeval aTime; structmsg_headmsg_h; char*buf = (char*)&msg_h;//from:2 char to 2 char msglenth:1 int buf[0] = client_p->id[2]; buf[1] = client_p->id[3]; charmsg[100]; client_element*send_to; structtcp_infoinfo; inttcp_info_len=sizeof(info); while(1) { FD_ZERO(&fdRead); FD_SET(client_p->sockfd, &fdRead); aTime.tv_sec = 2; aTime.tv_usec = 0; getsockopt(client_p->sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&tcp_info_len); if(info.tcpi_state == 1) { //printf("$$$%d tcp connection established...\n", client_p->sockfd); ; } else { printf("$$$%d tcp connection closed...\n", client_p->sockfd); break; } ret = select( client_p->sockfd+1,&fdRead,NULL,NULL,&aTime ); if(ret > 0) { //判斷是否讀事件 if(FD_ISSET(client_p->sockfd, &fdRead)) { //data available, so get it! lenth = read( client_p->sockfd, buf+2, 6 ); if( lenth != 6 ) { continue; } //對接收的數(shù)據(jù)進(jìn)行處理,這里為簡單的數(shù)據(jù)轉(zhuǎn)發(fā) lenth = read(client_p->sockfd, msg, msg_h.lenth); if(lenth == msg_h.lenth) { send_to = find_client(clients, msg_h.to); //printf("try to send to client %s\n", msg_h.to); if(send_to == NULL) { printf("can't find target client\n"); continue; } write(send_to->sockfd, &msg_h,sizeof(structmsg_head)); write(send_to->sockfd, msg, lenth); } //處理完畢 } } } close( client_p->sockfd); del_client(clients, client_p->sockfd); pthread_exit( NULL ); } |
這里收到消息后就解析消息頭,發(fā)送到指定的端口去(控制器或者顯示進(jìn)程),由于實(shí)際應(yīng)用中socket傳送數(shù)據(jù)可能存在分包的情況,客戶需要自行定義消息的數(shù)據(jù)格式來保證數(shù)據(jù)的完整性,以及對數(shù)據(jù)進(jìn)行更嚴(yán)格的驗(yàn)證。
另一方面對于已有的控制器來說,需要在原來的基礎(chǔ)上進(jìn)行修改,在主線程中與socket服務(wù)器建立連接:
sockedfd= socket(AF_INET,SOCK_STREAM,0); address.sin_family=AF_INET; address.sin_addr.s_addr= inet_addr("127.0.0.1"); address.sin_port= htons(9733); len =sizeof(address); do { res = connect(sockedfd, (structsockaddr*)&address, len); if(res == -1) { perror("oops:connecterror"); } }while(res == -1); write(sockedfd,"IDG1",4); printf("###connectedtoserver\n"); |
然后建立兩個(gè)線程分別處理數(shù)據(jù)(data_thread_func)和命令(command_thread_func),其中data_thread_func用于監(jiān)聽硬件狀態(tài),并且發(fā)送相應(yīng)的狀態(tài)消息給socket服務(wù)器,而command_thread_func用于監(jiān)聽socket服務(wù)器的消息等待命令,用于改變硬件運(yùn)行狀態(tài),不需要界面帶有控制功能的客戶可以不實(shí)現(xiàn)commad_thread_func。以GPIO控制器為例:
void*gpio_controller::data_thread_func(void* lparam) { gpio_controller *pSer = (gpio_controller*)lparam; fd_set fdRead; intret=0; structtimeval aTime; unsignedintpinstates = 0; structmsg_head buf_h; while( 1 ) { FD_ZERO(&fdRead); FD_SET(pSer->interface_fd,&fdRead); aTime.tv_sec = 2; aTime.tv_usec = 0; //等待硬件消息,這里是GPIO狀態(tài)改變 ret = select( pSer->interface_fd+1,&fdRead,NULL,NULL,&aTime ); if(ret < 0 ) { //關(guān)閉 perror("select wrong"); pSer->close_interface(pSer->interface_fd); break; } else { //select超時(shí)或者GPIO狀態(tài)發(fā)生了改變,讀取GPIO狀態(tài),發(fā)送給socket服務(wù)器 pinstates = INPINS; ret = GPIO_PinState(pSer->interface_fd, &pinstates); if(ret < 0) { printf("GPIO_PinState::failed %d\n", ret); break; } sprintf((char*)&buf_h.to[0],"D1"); buf_h.lenth =sizeof(pinstates); write(pSer->sockedfd, (void*)&buf_h.to[0], 6); write(pSer->sockedfd, (void*)&pinstates,sizeof(pinstates)); } } printf("ReceiveThreadFunc finished\n"); pthread_exit( NULL ); } void*gpio_controller::command_thread_func(void* lparam) { gpio_controller *pSer = (gpio_controller*)lparam; fd_set fdRead; intret, len; structtimeval aTime; structoutcom{ unsignedintoutpin; unsignedintoutstate; }; structoutcomout; structmsg_head buf_h; while( 1 ) { FD_ZERO(&fdRead); FD_SET(pSer->sockedfd,&fdRead); aTime.tv_sec = 3; aTime.tv_usec = 300000; //等待socket服務(wù)器的消息 ret = select( pSer->sockedfd+1,&fdRead,NULL,NULL,&aTime ); if(ret < 0 ) { //關(guān)閉 pSer->close_interface(pSer->interface_fd); break; } if(ret > 0) { //判斷是否讀事件 if(FD_ISSET(pSer->sockedfd,&fdRead)) { len = read(pSer->sockedfd, &buf_h,sizeof(buf_h)); //獲取socket服務(wù)器發(fā)送的信息,進(jìn)行解析 if(len !=sizeof(structoutcom)) { printf("###invalid command lenth: %d, terminate\n", len); } len = read(pSer->sockedfd, &out, buf_h.lenth); //write command switch(out.outstate) { case0: GPIO_OutClear(pSer->interface_fd, out.outpin); if(ret < 0) printf("GPIO_OutClear::failed %d\n", ret); //printf("GPIO_OutClear::succeed %d\n", ret); break; case1: GPIO_OutSet(pSer->interface_fd, out.outpin); if(ret < 0) printf("GPIO_OutSet::failed %d\n", ret); //printf("GPIO_OutSet::succeed %d\n", ret); break; default: printf("###wrong gpio state %d, no operation\n", out.outstate); ret = -1; break; } if(ret < 0) break; } } } printf("ReceiveThreadFunc finished\n"); pthread_exit( NULL ); } |
這里兩個(gè)函數(shù)主要任務(wù)都是處理數(shù)據(jù),data_thread_func使用select函數(shù)來等待輸入GPIO的狀態(tài)改變事件,如果有狀態(tài)改變或者select等待超時(shí)都讀取一次GPIO的狀態(tài),然后發(fā)送給socket服務(wù)器;command_thread_func監(jiān)聽服務(wù)器的消息,收到消息后進(jìn)行解析,然后根據(jù)消息來操作GPIO輸出信號。
通過這兩個(gè)函數(shù)便與socket服務(wù)器建立了消息溝通通道,而socket服務(wù)器會(huì)自動(dòng)將數(shù)據(jù)轉(zhuǎn)發(fā)到顯示進(jìn)程,這種實(shí)現(xiàn)可以使得對已有程序的改動(dòng)降到很低的程度。實(shí)際實(shí)現(xiàn)中,可以在socket服務(wù)器中增加狀態(tài)機(jī)等其他功能,記錄硬件狀態(tài)信息等。
4、顯示程序
顯示部分我們采用Qt來搭建,主要分為QML搭建的界面以及Qt c++編寫的數(shù)據(jù)處理插件。QML是Qt提供的一種描述性的腳本語言,類似于css,可以在腳本里創(chuàng)建圖形對象,并且支持各種圖形特效,以及狀態(tài)機(jī)等,同時(shí)又能跟Qt寫的C++代碼進(jìn)行方便的交互,使用起來非常方便。采用QML加插件的方式主要是為了將界面設(shè)計(jì)與程序邏輯解耦,一般的系統(tǒng)開發(fā)中界面設(shè)計(jì)的變動(dòng)往往多于后臺(tái)邏輯,因此采用QML加插件的方式將界面設(shè)計(jì)與邏輯分離有利于開發(fā)人員的分工,加速產(chǎn)品迭代速度,降低后期維護(hù)成本。而且QML解釋性語言的特性使得其語法更加簡單,可以將界面設(shè)計(jì)部分交給專業(yè)的設(shè)計(jì)人員開發(fā),而不要求設(shè)計(jì)人員會(huì)c++等編程語言。Qt底層對QML做了優(yōu)化,將會(huì)優(yōu)先使用硬件圖形加速器進(jìn)行界面的渲染,也針對觸摸屏應(yīng)用做了優(yōu)化,使用QML能夠更簡單快捷的搭建流暢、優(yōu)美的界面。QML也支持嵌入Javascript處理邏輯,但是底層邏輯處理使用Qt C++編寫插件,能夠更好的控制數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)處理也更加高效,Qt提供了多種方式將C++數(shù)據(jù)類型導(dǎo)入QML腳本中,更多詳細(xì)資料可以查看Qt官方的文檔。由于篇幅原因,我們在另外一篇文章:《使用QML進(jìn)行界面開發(fā)》中更詳細(xì)地介紹了QML及插件的實(shí)現(xiàn),在此我們還是集中介紹socket消息處理部分。
本例程中數(shù)據(jù)處理插件的任務(wù)就是連接socket服務(wù)器,與服務(wù)器進(jìn)行通信,接收消息進(jìn)行解析然后提供給QML界面,以及從QML界面獲取消息給socket服務(wù)器發(fā)送命令。插件中通過socket進(jìn)行通信的部分代碼如下:
voidMsgClient::cServer(void* param) { MsgClient*client = (MsgClient*)param; intret; intlen; structsockaddr_inaddress; intsockedfd = socket(AF_INET,SOCK_STREAM,0); printf("sockedfd:%d\n", sockedfd); client->sockedfd= sockedfd; address.sin_family=AF_INET; address.sin_addr.s_addr= inet_addr("127.0.0.1");//本地IP通信 address.sin_port= htons(9733); len =sizeof(address); do { printf("Client:connecting...\n"); ret = ::connect(sockedfd, (structsockaddr*)&address, len);//建立連接 if(ret == -1) { perror("oops:connecttoservererror"); } sleep(2); }while(ret == -1); write(sockedfd,"IDD1",4); printf("Client:connectedtoserver\n"); emitclient->serverConnected(); fd_setfdRead; structtimevalaTime; charbuf[100]; unsignedintpinstates; structmsg_headbuf_h; while(!client->exit_flag) { FD_ZERO(&fdRead); FD_SET(sockedfd, &fdRead); aTime.tv_sec=3; aTime.tv_usec=0; ret = select(sockedfd+1, &fdRead,NULL,NULL, &aTime);//等待消息 if(ret 0) { perror("sometingwrongwithselect"); } if(ret >0) { if(FD_ISSET(sockedfd, &fdRead)) { len = read(sockedfd, &buf_h,sizeof(buf_h)); inti; switch(buf_h.from[0]) {//解析消息 case'S': //串口信息 i = buf_h.from[1] -'0'; len = read(sockedfd, buf, buf_h.lenth); client->rmsgQueue[i] << buf; if(i == client->m_interface) emitclient->newMsgRcved(); memset(buf,0,sizeof(buf)); break; case'G': //GPIO信息 len = read(sockedfd, &pinstates, buf_h.lenth); printf("getGPIOpinstates\n"); client->updateGPIOState(pinstates); break; default: break; } } } } close(sockedfd); pthread_exit(NULL); } |
如代碼所示,插件首先通過本地IP127.0.0.1與socket服務(wù)器建立連接(connect),然后等待socket服務(wù)器的消息(select),收到消息后進(jìn)行解析,判斷是哪個(gè)硬件控制器發(fā)送的消息,然后更新相應(yīng)的顯示界面,這里的代碼相對簡單,只是為了展示通過本地IP實(shí)現(xiàn)顯示進(jìn)程與控制進(jìn)程之間的通信,實(shí)際使用中客戶需要對數(shù)據(jù)進(jìn)行更嚴(yán)格的檢驗(yàn)。
使用QML搭建串口控制界面如下圖所示:
GPIO控制器的顯示效果如下:
由于篇幅原因,我們在此不詳細(xì)介紹實(shí)現(xiàn)界面的QML腳本了,將會(huì)在另一篇文章中進(jìn)行專門的介紹,感興趣的用戶可以關(guān)注我們官網(wǎng)上的文章更新,或者向我們要取程序源碼。用戶在實(shí)際開發(fā)中可以參考此方式實(shí)現(xiàn)顯示進(jìn)程與控制進(jìn)程之間的通信,從而實(shí)現(xiàn)單獨(dú)的顯示進(jìn)程,對已有的控制進(jìn)程的更改控制到很小的程度,一方面減少了由于程序修改而造成控制程序的不穩(wěn)定,另一方面使用QML又能快速的搭建界面,解決顯示設(shè)備狀態(tài)的需求。
5、總結(jié)
實(shí)際測試過程中,我們在ESM6802工控板上運(yùn)行本文介紹的程序,底層控制程序直接可以開機(jī)后臺(tái)運(yùn)行,顯示程序開機(jī)后手動(dòng)加載,通過本地IP地址與控制程序的socket服務(wù)器連接,然后實(shí)時(shí)更新系統(tǒng)狀態(tài),也能及時(shí)響應(yīng)人工控制,如改變輸出GPIO的輸出狀態(tài),關(guān)掉顯示程序之后,控制程序繼續(xù)正常運(yùn)行,之后還可以再次啟動(dòng)顯示程序。
將底層控制與顯示分開后,程序開發(fā)分工可以更加細(xì)致,也一定程度上增加了控制系統(tǒng)的穩(wěn)定性,減小了維護(hù)成本。同時(shí)使用QML進(jìn)行界面開發(fā)能夠更加方便快速的更新系統(tǒng)的顯示效果,完成產(chǎn)品迭代。由于底層控制與顯示之間采用socket進(jìn)行通信,顯示部分也可以采用其他的開發(fā)環(huán)境,比如ESM6802也支持Android開發(fā),用戶在產(chǎn)品升級換代的時(shí)候就能夠直接沿用底層控制部分的程序,而只對上層顯示部分的程序進(jìn)行調(diào)整。
-
嵌入式主板
+關(guān)注
關(guān)注
7文章
6086瀏覽量
35522 -
安卓
+關(guān)注
關(guān)注
5文章
2136瀏覽量
57456
發(fā)布評論請先 登錄
相關(guān)推薦
評論