赵婉芳,赵 刚
(1.北京电子科技职业学院 北京 100016;2.中国电话号簿公司 北京 100032)
随着通信业务的迅猛发展,通信设备使用单处理器处理所有任务的工作方式已经很难满足业务需求。为了提高系统业务处理能力,大部分的通信设备引入了双处理器工作方式,即使用两个相同类型或者不同类型的处理器协作完成系统中的不同任务,这种方式不仅提高了设备性能,还大大增强了系统的实时性、可靠性和适用性。该类设备的两个处理器由于需要协作共同完成任务,因此处理器间必须进行数据的交换,处理器间的通信变得非常重要,它不仅会影响数据交换的可靠性,还会影响整个系统的协作处理能力和设备的整体性能。
目前,处理器间通信主要有主从方式和对等方式。在主从方式下,一个处理器作为主端(Master),连接一个或多个处理器作为从端(Slave),所有通信都由主端发起,从端不能主动发送数据,如并行总线、SPI等;而在对等方式中,多个处理器通过网络连接在一起,每个处理器都能主动向外发送数据,当发生冲突时能主动回避并重发,如以太网等。一般来说,对等方式的效率较高,设计较灵活,但是处理器间的网络连接复杂度较高,需要一个外置数据交换芯片来进行数据的交换处理。如何找到一种可靠、低成本、容易实现的双处理器间通信方式,降低嵌入式系统的复杂度和开发成本,对双处理器嵌入式系统的开发和应用具有很好的现实意义。
本文针对通信设备中双处理器间通信的需求和特点,综合考虑开发成本、周期、稳定性等因素,提出了一种基于串口的对等通信方式。该通信方式将TCP/IP与串口结合起来,通过串口传输以太网数据,利用TCP/IP来保证数据传输的正确性,实现了两个处理器间的全双工通信。
处理器间通信一般可以通过数据交换、并行总线或者串行总线等方式实现。由于现代处理器都有丰富的串口可以利用,因此串口通信方式相对于数据交换方式和并行总线方式而言不用增加额外的器件,实现成本低,硬件连接简单,而且传输速率能达到10 KB/s甚至更高,可以满足大部分产品设计的需求,是应用比较广泛的一种通信方式。
在目前采用双处理器结构的通信设备中,两个处理器有明确的分工:数据处理器专门进行数据处理;控制处理器主要完成控制工作。位于两个处理器之间的通信通道则主要用于传递控制信息。由于控制信息的特点是数据量小,对数据传输的及时性和准确性要求高,因此数据传输的稳定、可靠是处理器间通信最重要的条件。另外还需考虑到实现方式的通用性(可以适用于多种操作系统和处理器)和易实现。综上所述,通信通道需要满足以下条件:
·数据传输稳定、可靠;
·数据传输速率在10 KB/s以上;
·移植性好,可用于多种处理器互联;
·易于编写应用软件。
如果在系统中采用串口通信方式来实现处理器间信息的交换,则能够保证实现的低成本和易实现,但是数据传输的正确性无法保证。因为串口传输数据只是利用奇偶位校验数据,这种校验方式只能检测出信息传输过程中的部分误码,即只能检出1位误码,不能检出2位及2位以上误码。另外,奇偶校验方式不能纠错,在发现错误后,只能要求重发,误码率较高,严重影响数据传输的正确性。因此,如何保证数据传输稳定、可靠,提高处理器协作处理能力,是实现处理器间通信需要解决的首要问题。
本文在串口通信方式基础上引入了TCP/IP协议栈,用于在串口上传输以太网数据,实现双处理器间的通信,并利用TCP/IP来保证数据传输的正确性。由于TCP/IP是一种面向连接的协议,TCP在传输前必须先通过 “三重握手”在主机间建立TCP连接,它所传输的数据流采用了顺序号和应答措施,可以发现数据的丢失、段的失序和对传输错误的排除,因此完全能实现数据流的可靠传输。其次,TCP/IP符合开放系统互连(OSI)模型,采用分层结构(传输层、网络层、链路层、应用层),易于移植到串口上使用,保证了实现方式的易移植性。此外,TCP/IP是一种十分成熟的网络协议,可以被移植到各种操作系统中,在主流嵌入式操作系统中都有TCP/IP协议栈,在产品开发中易于实现。可见,该方式在解决了数据传输的高可靠性的同时,保证了易实现、低成本和易移植。
具体的实现原理为:等待发送的数据先通过网络协议栈处理,即给数据加上TCP/IP头部和以太网头部,再放到串口上传输;对方接收到后,将数据包传给网络协议栈处理,去掉TCP/IP头部和以太网头部,最终得到所需的数据。该方式的实现需要软硬件配合,硬件上需要将串口连接起来,保证处理器之间的物理联通,而实现的重点和难点在于软件驱动的设计和实现。
在两个处理器中驱动程序的结构是相同的,在系统中的位置如图1所示。
在图1中可以看到,串口驱动在TCP/IP和以太网协议下方,与网卡驱动在同一层。该串口驱动的作用就是将串口设备模拟成网卡设备。对于上层协议栈,串口成为一个虚拟网卡设备。上层协议栈通过该驱动在串口上收发数据包。这种结构的好处是不破坏系统原有的结构,尽量使用系统已有的软件模块,在编写应用程序时使用标准的socket接口编程,软件移植性好,可在多种操作系统环境中运行。由于Linux系统支持x86、ARM、PowerPC等体系结构的处理器,内核高效、稳定,可根据系统需要进行剪裁,并且支持TCP/IP,在嵌入式系统中应用十分广泛,因此本文的处理器采用Linux作为操作系统。
驱动程序是实现处理器间通信的关键,驱动程序部分的主要任务是将数据包通过串口发送出去,同时将串口接收到的数据包传递给上层协议栈处理,主要解决如何在上层协议栈和串口驱动之间进行数据交换以及在物理串口上传输以太网数据问题,因此驱动程序的设计分为上层协议栈接口和串口驱动两部分。上层协议栈接口部分负责将协议栈与串口驱动连接起来。串口驱动部分则实现串口硬件模块的控制,如发送数据、接收数据和中断处理等功能。
从图1可以看到,该驱动程序处于协议栈的底部,因此需要提供与上层协议栈的接口。由于在Linux 2.6内核源码树include/linux/netdevice.h头文件中有net_device结构体的定义,net_device是网卡驱动向上层协议栈注册时使用的一个专用结构体,该结构体中定义了很多网络参数和供网络协议接口层调用的设备方法。本文所述驱动属于虚拟网卡驱动范畴,因此向上层协议栈注册时也需要通过net_device结构体进行。在net_device结构体中需要定义的网络参数主要有网络设备名称、I/O基地址、最大传输单元等,需要定义的接口函数有设备初始化函数、统计信息查询函数等。当net_device结构被注册到Linux内核中,就完成了上层协议栈与串口驱动的连接,上层协议栈就能通过net_device结构体来使用串口驱动。具体的内容将在下文实现部分中描述。
串口驱动的主要任务是控制串口硬件模块发送数据和接收数据。其中串口中断处理函数是串口驱动实现的核心,串口数据发送和接收过程主要由该函数来实现,中断函数的具体流程如图2所示。
在串口驱动中基于中断函数的数据发送和接收过程如下。
发送数据过程:上层协议栈在组包过程中调用uart_header()来构造以太网包的头部,然后将构造好的以太网包发送给uart_xmit()。在函数 uart_xmit()中,调用netif_stop_queue()暂停上层协议栈继续向串口驱动发送数据,防止过多数据使串口驱动无法处理,然后调用uart_encode()给发送数据添加串口帧格式。处理后的数据放在发送缓存中等待发送。将前15 byte数据写入串口TX_FIFO(发送FIFO),并使能发送中断。接下来的发送工作由串口中断处理程序完成。当TX_FIFO中的数据发送完时,串口产生中断信号,中断处理程序将未发送的数据填入FIFO中,如此循环,直到数据发送完成。完成后,调用netif_start_queue()打开数据发送队列。
接收数据过程:串口的接收中断在初始化时被使能,并设置RX_FIFO(接收FIFO)中断门限为8个字符,即RX_FIFO中的数据超过8个时将触发串口中断。中断处理程序从RX_FIFO中读取数据,边读边解码,数据解码后得到以太网包。在多个中断后,当收到一个完整的以太网包后,调用netif_rx(),将整个数据包传递给上层协议栈处理。
在驱动设计中串口分帧也是一个关键点。由于串口设备是流设备,数据以 byte为单位传输,传输的数据没有帧结构,而网络数据是分帧传输的,因此无法使用串口设备来传输网络数据。此外,在串口硬件模块中也没有分帧机制。为了实现使用串口传输网络数据,可以通过在驱动中添加分帧机制的方法来实现数据分帧。考虑到处理器的开销问题,采用了一种简单的机制来实现分帧,即通过在发送方给数据包添加头部和尾部标识的方法来实现,具体定义如下:在发送数据包的头部添加多个“0x7e”,尾部添加一个“0x7e”,中间是数据包,如图3所示。
由于使用“0x7e”作为标识,因此数据包中的“0x7e”必须用其他方式表示。将数据包中的 “0x7e”替换为“0x7d 0x5e”、“0x7d”替换为“0x7d 0x5d”。接收到数据后,先识别帧的边界,然后还原数据包。数据包中的数据需进行如下处理:
0x7d 0x5e→0x7e
0x7d 0x5d→0x7d
利用以上串口分帧机制就能实现在串口上传输以太网数据。
该驱动在Linux系统中以模块的形式实现,可以动态地加载和卸载。驱动程序主要实现两类函数:一类是以太网协议栈的接口函数;另一类是串口驱动模块函数。在使用串口之前,必须对串口设备进行初始化。在初始化过程中,首先清空FIFO和中断寄存器;然后设置串口的参数,如波特率、FIFO中断触发点;最后注册中断处理函数,使能中断并启动发送队列。
经过初始化,串口进入工作状态,开始收发数据,因此首先要申明Linux驱动模块的初始化函数和卸载函数。初始化函数module_init(uart_init_module)的功能是告知Linux系统当初始化串口驱动模块时调用模块初始化函数uart_init_module()。而卸载函数module_exit(uart_cleanup_module)的功能则是告知系统当卸载该驱动模块时调用函数uart_cleanup_module()来完成驱动模块卸载操作,解注册网络设备并释放该驱动占用的资源。
驱动模块初始化函数uart_init_module()的主要实现代码如下:
#define UART_NAME “etu”
static int__init uart_init_module(void)
{
struct net_device*net_dev=NULL;
/*分配内存空间,并初始化结构net_dev*/
net_dev=alloc_netdev(sizeof(struct uart_priv),UART_NAME,uart_init);
/*给网络设备注册一个名字 */
dev_alloc_name(net_dev,UART_NAME);
/*在系统中注册新的网络设备 */
register_netdev(net_dev);
return 0;
}
uart_init_module()函数首先给一个 net_device结构分配空间,然后定义网络设备名为“etu”,最后向系统注册一个新的网络设备。
结构体net_device描述了该虚拟网络设备的所有信息,包括各种操作函数和参数,其定义在头文件netdevice.h中,主要定义的网络参数和接口函数如下。
(1)网络参数
name:网络设备名称,使用ifconfig命令可以查看。
base_addr,irq:网络设备的I/O基地址、中断号。
hard_header_len:硬件头的长度,以太网的值为14。
mtu:最大传输单元,以太网中值为1 500 byte。
dev_addr[MAX_ADDR_LEN]:硬件 (MAC)地址长度及设备硬件地址,以太网地址长度是48 bit。
flags:网络接口状态标志,它的值可以是IFF_UP、IFF_BROADCAST等。
(2)接口函数
int(*init)(struct net_device*dev):设备初始化函数,仅驱动初始化时调用一次。
int(*open)(struct net_device*dev):设备打开接口函数,当设备被激活时调用,在该函数中注册设备所需的系统资源,如地址空间、IRQ、DMA等,同时激活硬件。
int(*stop)(struct net_device*dev):设备关闭接口函数,这个函数关闭硬件,释放系统资源。
int(*hard_start_xmit)(struct sk_buff*skb,struct net_device*dev):初始化数据包传输的函数。
struct net_device_stats* (*get_stats)(struct net_device*dev):统计信息查询接口函数,调用该函数,可查询设备的各种统计信息,如接收字节数、发送字节数等。
int(*do_ioctl)(struct net_device*dev,struct ifreq*ifr,int cmd):该接口函数提供设置网络设备参数的接口,如修改硬件地址(MAC)。
定义好的net_device结构体在函数uart_init()中被初始化。该初始化函数将驱动中定义的操作函数赋值给结构体net_device中的函数指针,并初始化结构体net_device中的各个成员参数。经过初始化后,net_device结构被注册到Linux内核中,这样就完成了上层协议栈与串口驱动的连接。上层协议栈就能通过net_device结构体来使用串口驱动。
在串口驱动模块中则主要实现以下函数。
static int uart_open(struct net_device*dev):设备使能函数,该函数的工作是配置串口模块参数,注册串口中断处理函数,使能串口中断,启动发送队列。
static int eos_release(struct net_device*dev):设备关闭函数,该函数的工作是停止发送队列,关闭串口模块,禁止串口中断,清除串口的FIFO。
static int uart_xmit(struct sk_buff*skb,struct net_device*dev):发送函数,该函数完成数据的发送工作,对数据进行编码,然后放入串口的FIFO中,使能串口的发送中断。
static int uart_header(struct sk_buff*skb,struct net_device*dev,unsigned short type,void*daddr,void*saddr,unsigned len):填写以太网数据包的头部函数,该函数填写发送数据包的头部,如源MAC、目的MAC等。
在实际使用中要注意串口的波特率可设置为115 200,如果要提高串口的传输速率,除了提高串口波特率外,后面几个因素都会影响串口的传输速率,如串口FIFO深度、FIFO中断触发位置、中断延迟、CPU性能等。另外,在调试过程中要进行串口中断处理程序优化,并尽量减少执行时间。
处理器间的通信一直是多处理器嵌入式系统的研究重点,本文通过在串口上引入TCP/IP协议栈的方式实现了双处理器间的通信,在保证了实现方式的低成本、易移植和易实现的前提下,大大提高了通信的可靠性和稳定性。本文的实现方式具有很好的移植性,不仅适用于通信设备,还适用于任何双处理器的嵌入式系统,对嵌入式系统的开发和应用具有很好的现实意义。
1 Rubin A著.Lisoleg译.Linux设备驱动程序.北京:中国电力出版社,2000
2 Rubin A.Linux device drivers.Sebastopol:O’Reilly Press,2001
3 DanielP,CesatiB M.Understanding the Linux Kernel.Sebastopol:O’Reilly Press,2000
4 赵炯.Linux内核完全注释.北京:机械工业出版社,2004