引言
在很多传统设备中,如数据采集设备、数据监控设备、安防设备、医疗设备等,都使用RS232/422/485等串行方式与外界通信。这种通信方式有很大的局限性——通信距离短。目前,网络发展迅速,对具备联网能力,以达到更高性能的嵌入式设备的需求越来越迫切。可以说,使嵌入式设备具备网络接入能力是发展的趋势。这就需要设计串口通信转网络通信的数据终端将这些传统设备接入网络,同时不需要改变这些设备的结构。
1基本原理
串行通信转网络通信就是将串行通信协议转换为网络(比如TCP/IP)通信协议,能够将串行通信数据转化为网络(比如TCP/IP)数据包发出,将收到的网络数据包用串行通信的方式传输,实现透明转换。串行通信转网络通信,实际上就是设计协议转换网关。
实际模型以全双工居多,可以拆分为两个基本模型: 上行链路STN(Serial To Network)和下行链路NTS(Network To Serial),如图1、图2所示。两者组合,还可以形成更为复杂的NTN(Network To Network)网关。如果只保留STN功能或者是NTS功能,那就是单工模型。由于实际中各种通信协议一般支持全双工,所以区分单工还是全双工,主要是由应用程序处理。
无论是STN还是NTS,都需要一个公用的数据缓冲区。这个公用的数据缓冲区从硬件层次上看,就是连续的内存单元;从软件层次上看,就是一个一维数组。STN原理是经过SR单元把串口帧还原出数据,存储到数据缓冲区;然后读取缓冲区的数据,经过NS单元的处理,打成网络数据包,发送目的地址。NTS原理是经过NR单元把网络数据包还原为数据,存储到数据缓冲区;然后读取缓冲区的数据,经过SS单元,处理成串口帧发送。数据缓冲机制可以说是数据终端最为重要的部分,既需要根据实际应用选择成本低的存储器,又要设计优良的算法进行维护操作。
2实例详解
下面结合工业控制实践简化出一个串口转以太网的开发实例。通过该实例,很容易扩展实现更为复杂的功能。
硬件资源: Atmel AT91RM9200DK。该DEMO板提供了3个串口,其中UART1作为DEBUG口,UART2和UART3作为一般串行通信接口;还提供了一个RJ45的以太网接口。硬件需求可以满足该实例要求。
软件资源: Bootloader、Linux2.4.21、Ramdisk。其中bootloader包括Atmel官方提供的loader.bin、boot.bin和已经移植好的Uboot 1.1.2。TCP/IP协议栈已与Linux内核无缝结合,可以满足该实例的要求。
图1STN模型
图2NTS模型
实现目标: 实时监测串口和网络。上行链路实现——如果有UART2数据输入,则处理发送到服务器;下行链路实现——如果有服务器发送数据,则处理发送到COM2。
关于开发环境的搭建、Uboot的移植、内核的裁减,有很多优秀的论文可供参考。本文从实际产品开发的角度,介绍如何利用现有的软硬件资源来缩减研发时间,以节省成本,并详细介绍最为关键的数据帧设计和网络异常处理问题,希望能为产品开发者提供参考。
2.1开发流程
完整的开发流程为:
① 系统的需求分析;
② 硬件平台的选择和设计;
③ 软件系统的移植与应用开发——搭建开发环境、引导装载程序、内核裁减与编译、建立文件系统、应用程序开发。
在实际开发过程中,为了缩短研发时间,最好软硬件开发同时进行。选择一个与可以满足项目需求的DEMO板,作为硬件平台进行软件研发,这样就实现了软硬件并行开发。这里主要介绍在DEMO板的基础上进行应用程序开发的流程。模型如图3所示。
图3开发环境模型
搭建嵌入式开发环境包括两个方面: 一是主机端的开发环境,二是目标机端的开发环境。主要完成TFTP开发环境的搭建,NFS开发环境的搭建,GDB调试环境的搭建,以及主机上交叉编译工具链的搭建。具体步骤可以参考http: //piaoxiang.cublog.cn。
如图3,主机的操作系统为RedHat Linux 9.0,目标机端的操作系统为基于ARM架构的Linux 2.4.21。
2.2应用程序
该部分不具体介绍串口通信和网络socket编程的细节(可见参考文献[12]),而只详细介绍数据帧的设计和网络异常情况的处理。
2.2.1数据帧的设计
应用层的协议一般自己规定。数据帧如何设计,会影响系统性能。该实例需要考虑的内容不多,主要有包序号、数据、校验3个部分。应用层数据帧格式如下:
包序号针对实际应用情况,1字节、2字节、4字节都可以,以不溢出为原则。
数据要考虑的问题比较多。以太网数据链路的MTU(Maximum Transmission Unit,最大传输单元)为1 500字节,应用层的数据帧最大为1 500-40=1 460字节。如果数据帧中数据长度过大,那么会拆包传输,产生很多数据包碎片,增加丢包率,降低网络速度。所以此处数据长度的选择也需要根据实际情况进行测试。一般地,可以选择1 KB为数据长度。
校验方式可以有奇偶校验、CRC校验(Cyclical Redundancy Check,循环冗余码校验)等,其中CRC校验是最有效的方式。基本原理就是在发送端用数学方法产生一个循环码,叫做“循环冗余检验码”。在信息码位之后随信息一起发出。在接收端也用同样方法产生一个循环冗余校验码。将这两个校验码进行比较,如果一致就证明所传信息无误;如果不一致就表明传输中有差错,并要求发送端再传输。这里使用的生成多项式为x16+x12+x5+1。
设计数据包结构如下:
struct serial_packet {
short block;
char data[MAXDATASIZE];
char crc[2];
}packet;
2.2.2网络异常情况的处理
在嵌入式系统中,必须对异常情况进行妥善处理。因为嵌入式设备往往工作在无人职守的环境中,如果出现异常而无法处理,就会陷入瘫痪状态。在这个实例中,必须能够对网络连接状态进行监测。如果服务器中网络断开,那么client必须执行异常回复机制。
在这里采取的是信号机制。可以分为如下几步处理:
① client在连接到服务器后,要设置为非阻塞连接状态。
fcntl(sockfd, F_SETFL, O_NONBLOCK);
② 编写信号处理函数,不执行默认的关闭操作,而是执行空操作。
/*
*函数名称: handle_signal
*入口参数: int signum 信号的值,可以是除SIGKILL、
* SIGSTOP外的任何有效信号
*出口参数: 无
*函数功能: 在服务器关闭出现broken pipe(断开的管道)时,
*默认忽略此处错误。这样可以使client不断重
*新连接server,直到连接成功
*调用格式: handle_signal()
*/
void handle_signal(int signum) {
}
③ 初始化sigaction结构。
sigaction可以指定信号关联函数。这里可以是用户自定义的处理函数,还可以是SIG_DFL(采用缺省的处理方式)或SIG_IGN(忽略函数)。它的处理函数只有一个参数,就是信号值。sigaction的数据结构如下:
struct sigaction {
void (*sa_handler)(int signo);
sigset_t sa_mask;
int sa_flags;
void (*sa_restore)(void);
}
对网络异常中断引起的SIGPIPE信号,默认方式是关闭,现在可以指定处理函数为handle_signal,也就是不退出程序。首先必须对sigaction的数据结构初始化:
action.sa_handler = handle_signal;
/*初始化信号集合为空*/
sigemptyset(&action.sa_mask);
/*指定信号处理的选项*/
action.sa_flags = 0;
/*对管道信号进行设定处理*/
sigaction(SIGPIPE,&action,NULL);
然后执行处理操作:
if (send(sockfd, &packet, readbytes, 0) < 0) {
close(sockfd);
sockfd = connect_server(server_sockaddr);
}
2.3调试
调试模型如图3,数据流向为:
Host Com2→数据→Target UART2→Target→Ethernet→Host→Host Com2。
调试时要注意,连接Host Com1和Target UART1要使用交叉线,即将RX和TX交叉相连。
2.4Ramdisk的修改制作方法
应用程序开发完成后,需要固化到非易失性存储介质里,这里为Flash。这就需要把开发的应用程序添加到Ramdisk内,把原来Ramdisk中多余的功能去除。这就用到Ramdisk的修改了。方法如下:
① 首先使用file查看Ramdisk的属性,如果为max compression,则应该首先更改名称,然后解压。只有在非压缩状态下才可以挂载成功。
[armlinux@lqm embeddedsystem]$ file ramdisk
ramdisk: gzip compressed data, was "ramdisk", from Unix, max compression
[armlinux@lqm embeddedsystem]$ mv ramdisk ramdisk.gz
//重命令
[armlinux@lqm embeddedsystem]$ gunzip ramdisk.gz
//解压
[root@lqm embeddedsystem]# file ramdisk
//查看
ramdisk: Linux rev 1.0 ext2 filesystem data (mounted or unclean)
② 建立一个挂载点,比如tmp。然后把Ramdisk采用回环挂载到tmp文件夹,也就是说对tmp下文件的操作和对Ramdisk的修改是同步进行的。
[armlinux@lqm embeddedsystem]$ mkdir tmp
//建立挂载点
[armlinux@lqm embeddedsystem]$ mount o loop ramdisk ./tmp
//回环挂载
mount: only root can do that //没有root权限
[armlinux@lqm embeddedsystem]$ su //更改权限
Password:
[root@lqm embeddedsystem]# mount o loop ramdisk ./tmp
//再次挂载
[root@lqm embeddedsystem]# cd tmp //挂载成功
[root@lqm tmp]# ls //查看内容
bin dev etc home lib lost+found mnt proc rd root sbin tmp usr var
③ 把开发的应用程序复制到tmp下相应文件夹内。
④ 修改完成,首先卸载Ramdisk,注意当前路径不可以在tmp下,否则无法卸载。然后重新压缩Ramdisk。
[root@lqm embeddedsystem]# ls ./tmp
bin dev etc home lib lost+found mnt proc rd root sbin tmp usr var
[root@lqm embeddedsystem]# umount tmp //卸载
[root@lqm embeddedsystem]# ls ./tmp
[root@lqm embeddedsystem]# gzip c v9 ramdisk > ramdisk.gz //压缩并重定向
ramdisk:74.0%
[root@lqm embeddedsystem]# mv ramdisk.gz ramdisk //重命名
mv: 是否覆盖‘ramdisk’? y
[root@lqm embeddedsystem]# file ramdisk //制作成功
ramdisk: gzip compressed data, was "ramdisk", from Unix, max compression
⑤ 修改完成,就可以重新烧录到Flash中。
3总结
此实例虽然功能有限,但是足以说明很多问题,并且可以作为一个功能模块实现可重用。