1、概述
一臺典型的工控設(shè)備通常包括若干通訊接口(網(wǎng)絡(luò)、串口、CAN等),以及若干數(shù)字IO、AD通道等。運行于設(shè)備核心平臺的應(yīng)用程序通過操作這些接口,實現(xiàn)特定的功能。通常為了高效高精度完成整個通訊控制流程,應(yīng)用程序采用C/C++語言來編寫。圖1表現(xiàn)了典型工控設(shè)備的組成關(guān)系。
典型工控設(shè)備框圖
工控設(shè)備的另一個特點是鑒于設(shè)備大多是24小時連續(xù)運行,且無人值守,所以基本的工控設(shè)備是無顯示的。英創(chuàng)的工控主板ESM6800、ESM335x等都大量的應(yīng)用于這類無頭工控設(shè)備之中。
在實際應(yīng)用中,部分客戶需要基于已有的無頭工控設(shè)備,增加顯示界面功能,以滿足新的應(yīng)用需求。顯然保持已有的基本工控處理程序不變,通過相對獨立的技術(shù)手段來實現(xiàn)顯示功能,最符合客戶的利益訴求。為此我們發(fā)展了一種雙進程的程序設(shè)計方案來滿足客戶的這一需求。該方案的第一個進程,以客戶已有的用C/C++寫的基礎(chǔ)工控進程為基礎(chǔ),僅增加一個面向本地IP(127.0.0.1)的偵聽線程,用于向顯示進程提供必要的運行工況數(shù)據(jù)。圖2為增添了服務(wù)線程的工控進程:
帶有偵聽線程的基礎(chǔ)工控進程
方案的第二個進程則主要用于實現(xiàn)顯示界面,可以采用各種手段來實現(xiàn),本文中介紹了使用Qt的QML語言加通訊插件的界面設(shè)計方法。第二個進程(具體是通訊插件單元)通過本地IP,以客戶端方式與基礎(chǔ)工控進程進行Socket通訊,完成進程間數(shù)據(jù)交換。顯示進程以及與工控進程的關(guān)系如圖3所示:
顯示進程與工控進程
2、系統(tǒng)設(shè)計
鑒于工業(yè)控制領(lǐng)域?qū)ο到y(tǒng)運行的穩(wěn)定性要求,控制系統(tǒng)更加傾向于將底層硬件控制部分與上層界面顯示分開,兩部分以雙進程的形式各自獨立運行。底層硬件控制部分將會監(jiān)控系統(tǒng)硬件,管理外設(shè)等,同時收集系統(tǒng)的狀態(tài);而上層界面顯示部分主要用于顯示系統(tǒng)狀態(tài),并實現(xiàn)少量的系統(tǒng)控制功能,方便維護人員查看系統(tǒng)運行狀態(tài)并且根據(jù)當(dāng)前狀態(tài)進行系統(tǒng)的調(diào)整。由于顯示界面不一定是所有設(shè)備都配置,而且顯示部分的程序更加復(fù)雜,從而更容易出現(xiàn)程序運行時的錯誤,將控制與顯示分開能夠避免由于顯示部分的程序問題而影響到整個控制系統(tǒng)的運行,而且沒有配置顯示屏的設(shè)備也可以直接運行底層的控制程序,增加了系統(tǒng)程序的兼容性。顯示與控制分離后,由于顯示界面程序不需要處理底層硬件的管理控制,在設(shè)計時可以更加注重于界面的美化,而且界面程序可以采用不同的編程語言進行開發(fā),比如使用Qt C++或者Android java,本文將介紹基于Linux + Qt的雙進程示例程序供客戶在實際開發(fā)中參考,關(guān)于Android程序請參考我們官網(wǎng)的另一篇文章:《Android雙應(yīng)用進程Demo程序設(shè)計》。
如上圖所示。整個系統(tǒng)分為控制和顯示兩個進程,底層硬件控制部分可以獨立運行,使用多線程管理不同的硬件設(shè)備,監(jiān)控硬件狀態(tài),將狀態(tài)發(fā)送給socket服務(wù)器,并且從socket服務(wù)器接收命令來更改設(shè)備狀態(tài)。Socket服務(wù)器也是一個獨立的線程,通過本地網(wǎng)絡(luò)通信集中處理來自硬件控制線程以及顯示程序的消息。顯示界面需要連接上socket服務(wù)器才能正確的顯示設(shè)備的狀態(tài),同時提供必須的人工控制接口,供設(shè)備使用過程中人為調(diào)整設(shè)備運行狀態(tài)。目前在ESM6802工控主板上,界面程序可以采用Qt C++編寫,也可以使用Android java進行開發(fā),本文僅介紹采用Qt的界面程序。顯示程序界面用QML搭建,與底層通信的部分用獨立的Qt QML插件實現(xiàn),這樣顯示部分進一部分離為數(shù)據(jù)處理和界面開發(fā),使得界面設(shè)計可以更加快捷。程序的整體界面效果如下圖所示:
目前我們只提供了串口(SERIAL)和GPIO兩部分的例程。下面將集中介紹程序中通過本地IP實現(xiàn)兩個進程通信的部分供客戶在實際開發(fā)中參考。
3、控制端C程序
控制端程序主要分為兩個部分,一個部分用于控制具體的硬件運行(下文稱為控制器),另一個部分為socket服務(wù)器,用于與顯示程序之間進行通信。由于本方案主要是為了展示在已有控制程序的基礎(chǔ)上,增加顯示界面功能,以滿足新的應(yīng)用需求,所以我們在此重點介紹在已有控制程序中加入socket服務(wù)器的部分,不再詳細介紹各硬件的具體控制的實現(xiàn)。
增加本地IP通信的功能,首先需要在控制進程中新加入一個socket服務(wù)器線程,用于消息的集中管理,實現(xiàn)底層硬件與上層的界面程序的信息交換,socket服務(wù)器線程運行的函數(shù)體代碼如下:
static void *_init_server(void *param) { int server_sockfd, client_sockfd; int server_len; struct sockaddr_in server_address; struct sockaddr_in client_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, (struct sockaddr *)&server_address, server_len);
listen(server_sockfd, 5);
int res; pthread_t client_thread; pthread_attr_t attr; char id[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, (struct sockaddr *)&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)建一個socket用于監(jiān)聽(listen)等待顯示程序連接,當(dāng)接受(accept)一個連接之后創(chuàng)建一個新的線程用于消息處理,主要用于維護socket連接的狀態(tài),解析消息的收發(fā)方,并將消息轉(zhuǎn)送到對應(yīng)的接收方,在顯示程序建立連接之前或者連接斷開之后,控制器發(fā)送的消息將不會進行發(fā)送了,而控制器依然在正常運行,用于處理消息的新線程如下:
static void *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; int ret, lenth; struct timeval aTime; struct msg_head msg_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]; char msg[100]; client_element *send_to; struct tcp_info info; int tcp_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ù)進行處理,這里為簡單的數(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(struct msg_head)); write(send_to->sockfd, msg, lenth); } // 處理完畢 } } } close( client_p->sockfd ); del_client(clients, client_p->sockfd); pthread_exit( NULL ); } |
這里收到消息后就解析消息頭,發(fā)送到指定的端口去(控制器或者顯示進程),由于實際應(yīng)用中socket傳送數(shù)據(jù)可能存在分包的情況,客戶需要自行定義消息的數(shù)據(jù)格式來保證數(shù)據(jù)的完整性,以及對數(shù)據(jù)進行更嚴格的驗證。
另一方面對于已有的控制器來說,需要在原來的基礎(chǔ)上進行修改,在主線程中與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, (struct sockaddr *)&address, len); if(res == -1) { perror("oops: connect error"); } }while(res == -1); write(sockedfd, "IDG1", 4); printf("###connected to server\n"); |
然后建立兩個線程分別處理數(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ù)器的消息等待命令,用于改變硬件運行狀態(tài),不需要界面帶有控制功能的客戶可以不實現(xiàn)commad_thread_func。以GPIO控制器為例:
void *gpio_controller::data_thread_func(void* lparam) { gpio_controller *pSer = (gpio_controller*)lparam;
fd_set fdRead; int ret=0; struct timeval aTime; unsigned int pinstates = 0; struct msg_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超時或者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; int ret, len; struct timeval aTime; struct outcom{ unsigned int outpin; unsigned int outstate; }; struct outcom out; struct msg_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ā)送的信息,進行解析 if(len != sizeof(struct outcom)) { printf("###invalid command lenth: %d, terminate\n", len); } len = read(pSer->sockedfd, &out, buf_h.lenth);
//write command switch(out.outstate) { case 0: 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; case 1: 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 ); } |
這里兩個函數(shù)主要任務(wù)都是處理數(shù)據(jù),data_thread_func使用select函數(shù)來等待輸入GPIO的狀態(tài)改變事件,如果有狀態(tài)改變或者select等待超時都讀取一次GPIO的狀態(tài),然后發(fā)送給socket服務(wù)器;command_thread_func監(jiān)聽服務(wù)器的消息,收到消息后進行解析,然后根據(jù)消息來操作GPIO輸出信號。
通過這兩個函數(shù)便與socket服務(wù)器建立了消息溝通通道,而socket服務(wù)器會自動將數(shù)據(jù)轉(zhuǎn)發(fā)到顯示進程,這種實現(xiàn)可以使得對已有程序的改動降到很低的程度。實際實現(xiàn)中,可以在socket服務(wù)器中增加狀態(tài)機等其他功能,記錄硬件狀態(tài)信息等。
4、顯示程序
顯示部分我們采用Qt來搭建,主要分為QML搭建的界面以及Qt c++編寫的數(shù)據(jù)處理插件。QML是Qt提供的一種描述性的腳本語言,類似于css,可以在腳本里創(chuàng)建圖形對象,并且支持各種圖形特效,以及狀態(tài)機等,同時又能跟Qt寫的C++代碼進行方便的交互,使用起來非常方便。采用QML加插件的方式主要是為了將界面設(shè)計與程序邏輯解耦,一般的系統(tǒng)開發(fā)中界面設(shè)計的變動往往多于后臺邏輯,因此采用QML加插件的方式將界面設(shè)計與邏輯分離有利于開發(fā)人員的分工,加速產(chǎn)品迭代速度,降低后期維護成本。而且QML解釋性語言的特性使得其語法更加簡單,可以將界面設(shè)計部分交給專業(yè)的設(shè)計人員開發(fā),而不要求設(shè)計人員會c++等編程語言。Qt底層對QML做了優(yōu)化,將會優(yōu)先使用硬件圖形加速器進行界面的渲染,也針對觸摸屏應(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腳本中,更多詳細資料可以查看Qt官方的文檔。由于篇幅原因,我們在另外一篇文章:《使用QML進行界面開發(fā)》中更詳細地介紹了QML及插件的實現(xiàn),在此我們還是集中介紹socket消息處理部分。
本例程中數(shù)據(jù)處理插件的任務(wù)就是連接socket服務(wù)器,與服務(wù)器進行通信,接收消息進行解析然后提供給QML界面,以及從QML界面獲取消息給socket服務(wù)器發(fā)送命令。插件中通過socket進行通信的部分代碼如下:
void MsgClient::cServer(void* param) { MsgClient *client = (MsgClient *)param; int ret; int len; struct sockaddr_in address; int sockedfd = 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, (struct sockaddr *)&address, len); //建立連接 if(ret == -1) { perror("oops: connect to server error"); } sleep(2); }while(ret == -1); write(sockedfd, "IDD1", 4); printf("Client: connected to server\n"); emit client->serverConnected(); fd_set fdRead; struct timeval aTime; char buf[100]; unsigned int pinstates; struct msg_head buf_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("someting wrong with select"); } if(ret > 0) { if(FD_ISSET(sockedfd, &fdRead)) { len = read(sockedfd, &buf_h, sizeof(buf_h)); int i; 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) emit client->newMsgRcved(); memset(buf, 0, sizeof(buf)); break; case 'G': //GPIO信息 len = read(sockedfd, &pinstates, buf_h.lenth); printf("get GPIO pinstates\n"); client->updateGPIOState(pinstates); break; default: break; } } } } close(sockedfd); pthread_exit(NULL); } |
如代碼所示,插件首先通過本地IP127.0.0.1與socket服務(wù)器建立連接(connect),然后等待socket服務(wù)器的消息(select),收到消息后進行解析,判斷是哪個硬件控制器發(fā)送的消息,然后更新相應(yīng)的顯示界面,這里的代碼相對簡單,只是為了展示通過本地IP實現(xiàn)顯示進程與控制進程之間的通信,實際使用中客戶需要對數(shù)據(jù)進行更嚴格的檢驗。
使用QML搭建串口控制界面如下圖所示:
GPIO控制器的顯示效果如下:
由于篇幅原因,我們在此不詳細介紹實現(xiàn)界面的QML腳本了,將會在另一篇文章中進行專門的介紹,感興趣的用戶可以關(guān)注我們官網(wǎng)上的文章更新,或者向我們要取程序源碼。用戶在實際開發(fā)中可以參考此方式實現(xiàn)顯示進程與控制進程之間的通信,從而實現(xiàn)單獨的顯示進程,對已有的控制進程的更改控制到很小的程度,一方面減少了由于程序修改而造成控制程序的不穩(wěn)定,另一方面使用QML又能快速的搭建界面,解決顯示設(shè)備狀態(tài)的需求。
5、總結(jié)
實際測試過程中,我們在ESM6802工控板上運行本文介紹的程序,底層控制程序直接可以開機后臺運行,顯示程序開機后手動加載,通過本地IP地址與控制程序的socket服務(wù)器連接,然后實時更新系統(tǒng)狀態(tài),也能及時響應(yīng)人工控制,如改變輸出GPIO的輸出狀態(tài),關(guān)掉顯示程序之后,控制程序繼續(xù)正常運行,之后還可以再次啟動顯示程序。
將底層控制與顯示分開后,程序開發(fā)分工可以更加細致,也一定程度上增加了控制系統(tǒng)的穩(wěn)定性,減小了維護成本。同時使用QML進行界面開發(fā)能夠更加方便快速的更新系統(tǒng)的顯示效果,完成產(chǎn)品迭代。由于底層控制與顯示之間采用socket進行通信,顯示部分也可以采用其他的開發(fā)環(huán)境,比如ESM6802也支持Android開發(fā),用戶在產(chǎn)品升級換代的時候就能夠直接沿用底層控制部分的程序,而只對上層顯示部分的程序進行調(diào)整。
有興趣的客戶可以和我們的工程師進行溝通獲取更多信息以及程序代碼。
本文PDF下載:Linux雙進程應(yīng)用示例
成都英創(chuàng)信息技術(shù)有限公司 028-8618 0660