英创信息技术Linux双进程应用示例

1、概述
一台典型的工控设备通常包括若干通讯接口(网络、串口、can等),以及若干数字io、ad通道等。运行于设备核心平台的应用程序通过操作这些接口,实现特定的功能。通常为了高效高精度完成整个通讯控制流程,应用程序采用c/c++语言来编写。图1表现了典型工控设备的组成关系。
典型工控设备框图
工控设备的另一个特点是鉴于设备大多是24小时连续运行,且无人值守,所以基本的工控设备是无显示的。英创的工控主板esm6800、esm335x等都大量的应用于这类无头工控设备之中。
在实际应用中,部分客户需要基于已有的无头工控设备,增加显示界面功能,以满足新的应用需求。显然保持已有的基本工控处理程序不变,通过相对独立的技术手段来实现显示功能,最符合客户的利益诉求。为此我们发展了一种双进程的程序设计方案来满足客户的这一需求。该方案的第一个进程,以客户已有的用c/c++写的基础工控进程为基础,仅增加一个面向本地ip(127.0.0.1)的侦听线程,用于向显示进程提供必要的运行工况数据。图2为增添了服务线程的工控进程:
带有侦听线程的基础工控进程
方案的第二个进程则主要用于实现显示界面,可以采用各种手段来实现,本文中介绍了使用qt的qml语言加通讯插件的界面设计方法。第二个进程(具体是通讯插件单元)通过本地ip,以客户端方式与基础工控进程进行socket通讯,完成进程间数据交换。显示进程以及与工控进程的关系如图3所示:
显示进程与工控进程
2、系统设计
鉴于工业控制领域对系统运行的稳定性要求,控制系统更加倾向于将底层硬件控制部分与上层界面显示分开,两部分以双进程的形式各自独立运行。底层硬件控制部分将会监控系统硬件,管理外设等,同时收集系统的状态;而上层界面显示部分主要用于显示系统状态,并实现少量的系统控制功能,方便维护人员查看系统运行状态并且根据当前状态进行系统的调整。由于显示界面不一定是所有设备都配置,而且显示部分的程序更加复杂,从而更容易出现程序运行时的错误,将控制与显示分开能够避免由于显示部分的程序问题而影响到整个控制系统的运行,而且没有配置显示屏的设备也可以直接运行底层的控制程序,增加了系统程序的兼容性。显示与控制分离后,由于显示界面程序不需要处理底层硬件的管理控制,在设计时可以更加注重于界面的美化,而且界面程序可以采用不同的编程语言进行开发,比如使用qt c++或者android java,本文将介绍基于linux + qt的双进程示例程序供客户在实际开发中参考,关于android程序请参考我们官网的另一篇文章:《android双应用进程demo程序设计》。
如上图所示。整个系统分为控制和显示两个进程,底层硬件控制部分可以独立运行,使用多线程管理不同的硬件设备,监控硬件状态,将状态发送给socket服务器,并且从socket服务器接收命令来更改设备状态。socket服务器也是一个独立的线程,通过本地网络通信集中处理来自硬件控制线程以及显示程序的消息。显示界面需要连接上socket服务器才能正确的显示设备的状态,同时提供必须的人工控制接口,供设备使用过程中人为调整设备运行状态。目前在esm6802工控主板上,界面程序可以采用qt c++编写,也可以使用android java进行开发,本文仅介绍采用qt的界面程序。显示程序界面用qml搭建,与底层通信的部分用独立的qt qml插件实现,这样显示部分进一部分离为数据处理和界面开发,使得界面设计可以更加快捷。程序的整体界面效果如下图所示:
目前我们只提供了串口(serial)和gpio两部分的例程。下面将集中介绍程序中通过本地ip实现两个进程通信的部分供客户在实际开发中参考。
3、控制端c程序
控制端程序主要分为两个部分,一个部分用于控制具体的硬件运行(下文称为控制器),另一个部分为socket服务器,用于与显示程序之间进行通信。由于本方案主要是为了展示在已有控制程序的基础上,增加显示界面功能,以满足新的应用需求,所以我们在此重点介绍在已有控制程序中加入socket服务器的部分,不再详细介绍各硬件的具体控制的实现。
增加本地ip通信的功能,首先需要在控制进程中新加入一个socket服务器线程,用于消息的集中管理,实现底层硬件与上层的界面程序的信息交换,socket服务器线程运行的函数体代码如下:
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);
}
//设置线程绑定属性
res = pthread_attr_setscope( &attr, pthread_scope_system );
//设置线程分离属性
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 );
}
}
此函数创建一个socket用于监听(listen)等待显示程序连接,当接受(accept)一个连接之后创建一个新的线程用于消息处理,主要用于维护socket连接的状态,解析消息的收发方,并将消息转送到对应的接收方,在显示程序建立连接之前或者连接断开之后,控制器发送的消息将不会进行发送了,而控制器依然在正常运行,用于处理消息的新线程如下:
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;
}
//对接收的数据进行处理,这里为简单的数据转发
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 );
}
这里收到消息后就解析消息头,发送到指定的端口去(控制器或者显示进程),由于实际应用中socket传送数据可能存在分包的情况,客户需要自行定义消息的数据格式来保证数据的完整性,以及对数据进行更严格的验证。
另一方面对于已有的控制器来说,需要在原来的基础上进行修改,在主线程中与socket服务器建立连接:
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);
然后建立两个线程分别处理数据(data_thread_func)和命令(command_thread_func),其中data_thread_func用于监听硬件状态,并且发送相应的状态消息给socket服务器,而command_thread_func用于监听socket服务器的消息等待命令,用于改变硬件运行状态,不需要界面带有控制功能的客户可以不实现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状态改变
ret = select( pser->interface_fd+1,&fdread,null,null,&atime );
if(ret close_interface(pser->interface_fd);
break;
}
else
{
//select超时或者gpio状态发生了改变,读取gpio状态,发送给socket服务器
pinstates = inpins;
ret = gpio_pinstate(pser->interface_fd, &pinstates);
if(ret 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服务器的消息
ret = select( pser->sockedfd+1,&fdread,null,null,&atime );
if(ret 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服务器发送的信息,进行解析
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 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 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)
{
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] 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服务器建立连接(connect),然后等待socket服务器的消息(select),收到消息后进行解析,判断是哪个硬件控制器发送的消息,然后更新相应的显示界面,这里的代码相对简单,只是为了展示通过本地ip实现显示进程与控制进程之间的通信,实际使用中客户需要对数据进行更严格的检验。
使用qml搭建串口控制界面如下图所示:
gpio控制器的显示效果如下:
由于篇幅原因,我们在此不详细介绍实现界面的qml脚本了,将会在另一篇文章中进行专门的介绍,感兴趣的用户可以关注我们官网上的文章更新,或者向我们要取程序源码。用户在实际开发中可以参考此方式实现显示进程与控制进程之间的通信,从而实现单独的显示进程,对已有的控制进程的更改控制到很小的程度,一方面减少了由于程序修改而造成控制程序的不稳定,另一方面使用qml又能快速的搭建界面,解决显示设备状态的需求。
5、总结
实际测试过程中,我们在esm6802工控板上运行本文介绍的程序,底层控制程序直接可以开机后台运行,显示程序开机后手动加载,通过本地ip地址与控制程序的socket服务器连接,然后实时更新系统状态,也能及时响应人工控制,如改变输出gpio的输出状态,关掉显示程序之后,控制程序继续正常运行,之后还可以再次启动显示程序。
将底层控制与显示分开后,程序开发分工可以更加细致,也一定程度上增加了控制系统的稳定性,减小了维护成本。同时使用qml进行界面开发能够更加方便快速的更新系统的显示效果,完成产品迭代。由于底层控制与显示之间采用socket进行通信,显示部分也可以采用其他的开发环境,比如esm6802也支持android开发,用户在产品升级换代的时候就能够直接沿用底层控制部分的程序,而只对上层显示部分的程序进行调整。

4G芯片市场拼的都是哪些卖点?
实例下载 | 如何在Orcad Capture和Allegro PCB软件中实现模块电路设计以及复用
GarminvívomoveHR评测 明明可以靠颜值却偏偏拼实力
红米Note7评测 性价比有多高
电蜂优选:优质的HSL连接线需具备哪些要点?
英创信息技术Linux双进程应用示例
这一车企电动汽车亏了300亿
基于一种支持多资产的去中心化金融协议方案dForce介绍
第九届CHIP China晶芯在线研讨会精彩回顾
4525DO-DS5AI001DP空速传感器配置步骤
5G、快速充电和USB可编程电源的融合
简述储能电池参数详解与选型
移动医疗时代,连接器技术的特点与挑战
互联网电商时代之下 任何事物的价格或将发生改变
家用加湿器系统解决方案——符合用户体验的才是好的
对于光模块和光纤收发器,二者有什么区别
欧洲先进芯片制造业仍面临多重挑战
可靠性陷入死局,如何重生?
介绍集成电路电磁兼容测试的两种方法
芯片竞争异常激烈,恩智浦遭遇困境