引言
整体的变电站将由智能化一次设备和网络化二次设备分层构建,建立在IEC61850通信规范基础上,能够实现变电站内智能电气设备间信息共享和互操作的现代化变电站[13]。变电站内常规的二次设备全部基于标准化、模块化的微处理机设计制造,设备之间的连接全部采用高速的网络通信,二次设备不再出现常规功能装置重复的I/O现场接口,通过网络真正实现数据共享、资源其享,常规的功能装置在这里变成了逻辑的功能模块[4]。
为了适应越来越复杂的功能需求,二次保护设备的硬件以及软件的复杂程度被大幅度提升。随之带来的后果是无可避免的提升了装置的故障率。针对于这种状况研发了一套调试软件。该软件通过基于TCP的私有通信规约与装置进行通信。通过该软件,现在工程服务人员可以利用友好的图形化的人机界面从装置处得到一系列调试所需要的信息,并可以控制装置进行各种调试操作。在该调试工具的辅助下,当装置出现异常时,工程服务人员可以将装置内部的数据采集后交由技术支持人员和研发人员进行故障定位,并在分析后提供相应的修复措施。可以利用该调试工具对装置进行一系列的操作,实现一些工程调试的工作。
该调试工具目前工作效果显著,但由于其通信规约及功能实现较为复杂,目前仅由上层应用程序实现其通信协议并提供支持。现场工程服务人员有时会遇到应用程序无法正常启动,或是装置反复复位的情况。在这种情况下,由于装置内部的应用不能正常工作,导致该调试工具因失去支持而无法工作。为了能够适应这种应用情况,笔者在装置的引导阶段即加入该私有通信规约的支持,从而可以在应用程序无法正常启动的情况下利用现有工具对装置内部的数据进行采集,并进行一系列的控制操作以帮助对装置的故障进行定位。
虽然引导程序UBoot的功能已经演进得非常强大,但出于对代码尺寸以及自身需要的考虑,其对以太网协议的支持一直较为“简陋”。其所支持的以太网协议主要是一些在常规的操作系统启动中所需要使用到的以太网协议,如TFTP、BOOTP、DHCP、DNS等,并且没有实现对TCP/IP等较复杂协议的支持。因此,如果需要在UBoot中实现基于TCP以太网协议的通信规约时,首先需要在UBoot中实现TCP/IP协议栈的支持。
1 独立TCP/IP协议栈的选择
因为设计目标的不同,uIP与lwIP之间存在较多的差异。
uIP的设计目标以低系统资源需求为第一目标,从而牺牲了较多性能作为交换。在uIP中仅有半双工的以太网数据包缓存区,该缓存区只能缓存一个以太网数据包,且在发送与接收之间共享。由于该单包半双工的缓存机制,uIP的应答帧总是回复的很快,甚至快过通信对侧装置或PC的响应能力,需要在代码和驱动程序中对该缺陷进行弥补。出于减少资源的消耗以及减少对操作系统的依赖,uIP并没有对每一个连接建立一个线程进行,而是在每一次调用时依次处理系统中现存的连接。另外,由于uIP设计时的针对应用场景为8位和16位单片机,故其代码针对于8位和16位特别进行了优化,从而受限于最大16位变量的存储信息量,在该协议栈中存在着如时钟溢出等由最大16位数据位宽所带来的问题。
lwIP需要嵌入式系统能够提供至少十几KB的内存,其生成的可执行代码最小约为40 KB左右。为了能够使得lwIP适应更多的应用场景,lwIP在实现时提供了非常灵活的配置功能,可以在实际的应用中根据需求以及嵌入式系统的资源情况进行各种功能以及性能上的权衡。lwIP实现的各种协议,如DHCP、SNMP等特性可以按照需求进行配置是否编译进入最终的实现代码中,系统中的缓存大小、缓存数量、内存分配方式、最大连接数量等亦可以进行配置。这些配置使得lwIP的可伸缩性比较大,在性能关键的应用中,lwIP可以通过使用更多的系统资源提升其性能。lwIP的设计的针对应用场景主要为32位单片机,在其实现中并未针对于8位和16位单片机进行优化,存在有大量的32位宽变量以及32位宽变量的数学运算操作。因此lwIP并不适合运行在8位和16位单片机上。
目前,uIP项目已经被合并进入Contiki项目(一个小型的嵌入式操作系统),不再进行单独的开发。而lwIP目前正在被近30名来自不同地方的开发者共同进行开发和维护。笔者所维护和研发的二次保护设备的处理器为高性能的32位应用处理器,装置内部的内存配置在16 MB以上,而存储设备拥有16 MB以上的空间,相对于lwIP十多KB的内存以及40 KB以上的存储空间的最低需求而言,二次保护设备完全能够支撑lwIP的资源消耗。而且移植的目标处理器为32位处理器,lwIP更适合在该种类型的处理器下工作。因此,最终笔者选择了lwIP作为TCP/IP协议栈的移植目标。
2 lwIP的移植
这里的移植为无操作系统支持的移植。lwIP的可移植性非常好,开发人员只需要向lwIP描述移植目标的架构、所使用的编译器信息、以太网数据包的发送和接收的办法、操作系统的一些同步和通信机制等即可完成移植工作。针对于无操作系统移植而言,只需要描述前三项即可以完成移植。
lwIP对于描述信息的放置有自己的定义,对于无操作系统支持的移植,lwIP需要实现如下文件的定义:
arch/cc.h:编译器以及处理器的相关定义。
arch/perf.h:对于函数执行时间的评估。
lwipopts.h:对lwIP的工作方式、功能选择等的优化配置。
netif/ethernetif.c:实现以太网数据包收发的框架。
编译器以及处理器相关的定义文件是lwIP移植的核心头文件,将被所有的lwIP源代码所使用,主要包含一些与编译器和移植的目标处理器相关的定义。具体如下:
① 指定目标处理器的字节序是大端(bigendian)还是小端(littleendian)。这将影响到以太网部分的数据处理方式。笔者移植的目标处理器为PowerPC?架构,目前在系统中工作于大端模式,故需要定义宏:#define BYTE_ORDERBIG_ENDIAN。
② 定义各种不同位宽的变量类型以及内存指针的宽度。由于lwIP在设计时并不知道工作于何种处理器,何种编译器下,因此所有的变量以及指向内存指针等的定义均未使用诸如int或long等通用类型,而是使用u8_t表示无符号8位宽变量,s16_t表示有符号16位宽变量等移植性好的类型。针对目标32位PowerPC处理器的类型定义如下略——编者注。
③ 定义各种不同位长度的变量类型在字符串打印中对应的标示符。针对于UBoot而言,该部分的定义略——编者注。
④ 定义与编译器相关的无对齐的结构定义的辅义符。在C编译器编译中,默认会根据当前目标处理器的位宽,自动在结构体中各变量中间或是结构体最后填充无意义的空白,从而使得各变量或整体的结构体与处理器的位宽对齐,比如32位处理器对应于4字节对齐,从而可以提高读写效率,或是避免出现读写异常。而此处定义的辅义符则是告之编译器,此处的结构体的内部或尾部不允许添加字节对齐目的的填充。针对于GNU GCC编译器,该部分的定义略——编者注。
⑤ 定义lwIP的分析信息输出和严重错误时的行为。针对于UBoot,该部分的定义略——编者注。
lwIP性能评估相关的定义主要用于对lwIP的核心部分进行性能评估。通过该性能评估,可以针对性地对lwIP进行优化。该部分在UBoot内的实现略——编者注。
lwIP的配置放置在lwipopts.h中。lwIP给几乎所有的配置项一个默认值,所以仅在实际应用需求与lwIP的默认工作方式不同时,在该文件中给出对应的配置以覆盖lwIP的默认行为即可。lwIP共提供了257个配置项,涉及lwIP具体需要实现何种协议、实现何种接口、各具体协议的工作方式、lwIP可使用内存大小、缓存的大小及个数等。由于配置项定义非常细,使用者可以根据实际的应用需求对lwIP进行深度的定制,而并不需要对lwIP的源代码进行任何改动。
首先,于笔者移植的目标应用场景为无操作系统场景,因为需要定义NO_SYS,从而改变lwIP内部很多地方的实现机制。其次,由于目标协议经常需要缓存4 MB左右的数据包,因此为lwIP分配了8 MB内存以及512个以太网包的缓存区。再次,目标处理器为32位处理器,需要设置内存对齐为4字节对应。最后,在测试中,由于目标处理器的性能较之调试工具的预期要差一些,lwIP默认的TCP协议窗口过小,经常造成调试工具认为发送超时进行重新发送,影响了整体的处理性能。这里对TCP的窗口大小以及TCP的最大段大小均进行优化。
该部分的配置略——编者注。
关于以太网数据包的收发,lwIP在netif/ethernetif.c给出了一个简单的以太网设备的驱动框架,相应的移植工具即为将该框架中具体的收发函数进行实现。由于UBoot的关系,该部分的代码实现相对简单,并可以从UBoot得到很大的帮助。
3 在UBoot上应用lwIP
实现了上述4部分的移植工作,lwIP本身的移植已经基本完成。但lwIP本身仅是一个TCP/IP协议栈,必须要依附于其他可在处理器上运行的代码,如操作系统等,才能够真正地运行并发挥作用。在笔者的移植中,该可运行的代码为UBoot的独立应用程序。
UBoot以跳转表形式向独立应用提供一系列的函数调用接口,包括有控制台的输出、系统复位控制、系统时间获取、延时、环境变量读写、系统设备操作等[16]。
通常,一个基于UBoot API的独立应用的结构如图1所示。
图1 基于UBoot API的独立应用结构
3.1 独立应用的结构
笔者需要基于UBoot的独立应用API以及lwIP协议栈实现私有的基于TCP协议。具体的独立应用的实现结构如图2所示。
图2 基于UBoot API和lwIP的独立应用结构
3.2 独立应用的Makefile建立
要编译独立应用,需要首先建立独立应用的Makefile,该文件让编译器了解应当如何编译该独立应用。UBoot提供了该Makefile的例子,对Makefile作出修改,主要是需要给出UBoot API兼容层、lwIP协议栈、私有协议栈、独立应用主程序三个部分的代码的编译方法的描述。
针对于图2中的Adapter部分的实现,其对应于目录中的crt0.S、glue.c、libgenwrap.c。Main App部分,对应于独立应用目录中的lwip.c文件Private Protocol部分,对应于独立应用目录的sgview.c文件。在Makefile中添加的内容略——编者注。
图2中的lwIP、Ethernet Driver的实现代码被放置在src目录,部分移植的头文件放置于根目录的include目录中。而由于lwIP本身对头文件的目录结构有一定的要求,针对于该要求,对编译器的头文件路径指定作了一定的修改:
LWIPDIR=src
CFLAGS+=-Iinclude -I$(LWIPDIR)/include -I$(LWIPDIR)/include/ipv4
CPPFLAGS+=-Iinclude -I$(LWIPDIR)/include -I$(LWIPDIR)/include/ipv4
将lwIP编译入该独立应用,需要在Makefile中将涉及的lwIP的文件对应的编译目标添加进来。不用担心添加过多的文件进入这里,如果某个文件所实现的功能在配置中被去掉,比如UDP,在编译中该部分的代码会被选择编译去掉。在这里添加该文件仅会造成预编译的处理时间的延长,并不会实际造成最终生成的代码的尺寸变大。具体的Makefile修改略——编者注。
3.3 lwIP与UBoot的冲突的解决
UBoot和lwIP同时定义了名为ip_hdr的结构体用来解析IP协议头部分的信息,因此造成了重复定义的冲突。通过对lwIP定义的ip_hdr结构体进行重命名,该冲突很容易就被解决了。
其次,lwIP需要在头文件的路径下存在string.h以实现一系列的内存和字符串相关的操作。而UBoot中,该头文件位于linux子目录下,造成了头文件无法找到的问题。针对于这一冲突,将linux子目录下的string.h文件利用文件系统的链接功能直接链接至头文件的根目录下,解决了该冲突。
至此,lwIP已经可以和UBoot共同编译。
3.4 以太网设备的驱动
lwIP针对于最底层的以太网数据包的收发功能,给出了一个基本的以太网数据收发的框架,放置于lwIP源代码的netif/ethernetif.c中。该部分的实现极大的依赖于UBoot提供的API接口。
UBoot的API接口给出了一个最基本的设备的概念,目前仅支持块存储设备和以太网设备。在该部分的以太网设备的实现中,提供了对当前以太网设备数据收发的支持。笔者利用该API实现了lwIP的以太网设备的驱动。与具体以太网设备相关的代码均被UBoot所封装起来,因此该以太网设备驱动较为通用,所有基于UBoot的lwIP协议栈均可以使用该驱动。
利用API_DEV_ENUM接口对UBoot中所有的设备进行获取。对获取到的所有设备进行查找,并得到当前的以太网设备。由于UBoot本身的实现机制限制,UBoot在同一时间只能激活一个以太网接口,因此在UBoot中只会存在一个以太网设备。在得到以太网设备后,利用API_DEV_OPEN接口打开设备,利用API_DEV_WRITE接口发送以太网数据包,API_DEV_READ接口从以太网获取数据包。
由于lwIP提供的以太网设备驱动框架本身的完成度已经很高,只需要在其中填入具体的数据收发等基本功能,该以太网设备驱动即可以正常工作。所有需要修改的部分在该框架中均以伪代码的形式给出。限于篇幅,这里仅给出几个需要修改的地方的描述:根据应用的需要对以太网设备的结构体定义进行修改;
在lower_level_init中实现对lwIP的if设备的MAC地址的初始化;
在lower_level_output中利用UBoot的API实现实际的以太网设备的数据包的发送;
在lower_level_input中利用UBoot的API实现实际的以太网设备的数据包的接收;
在ethernetif_init中利用UBoot的API查找当前的以太网设备并打开,供后续使用。
3.5 独立应用的加载机制
在独立应用中实现了公司的私有协议后,所有的移植及开发工作全部完成。该独立应用需要在UBoot启动完成后,由UBoot加载进入内存并执行。该功能的实现需要借由UBoot的脚本功能实现。