1、nesC编程迷你教程解读nesC编程迷你教程寿颜波UniversitdeFranche-Comt,France内 容 目 录1引子 12基础概念 12.1接口(interface) 12.2命令与事件(Command and Event) 32.3模块与配置(Model and Configuration) 32.3.1模块 32.3.2配置 62.3.3可以提供接口的配置组件 72.3.4任务和事件 103工作环境 124编程开发 134.1Blink 134.2 TempRadio 154.2.1数据的采集与发送 164.2.2数据的接收 235 TOSSIM仿真 295.1使用TOSSI
2、M编译nesC程序 295.2捕捉、生成运行记录 305.3仿真 305.4运行中的变量值 326结束语 331引子目前在研究领域有多款针对无线传感器网络开发的操作系统,其中最为著名的项目之一便是TinyOS。它最早由美国Berkeley大学负责开发和维护,并且支持多种传感器平台,例如在研究领域广泛使用的mica系列传感器节点和telos系列。在本教程的编写过程当中,我们统一使用Crossbow公司开发的telosb节点。TinyOS完全由nesC编写,nesC全名Network Embedded System C,它可以被看作是C语言的近亲,在语法上和C语言有非常多的相似之处,如果你有C语言
3、的编写基础,那么针对nesC的学习就会变得轻松很多。nesC主要是为事件驱动编程而设计的,它也是我们开发TinyOS应用程序的主要编程语言。本文档的目的在于向读者展示TinyOS的基本运作模式,并且让读者可以在最短的时间掌握TinyOS下程序开发的要领。而且在编写过程当中,作者假设读者已经具备了基本的编程经验。如果你需要更为详细的nesC参考资料,可以查阅TinyOS官方网站上面的教程,或者阅读Philip Levis编写的TinyOS Programming Manual。因为已经很久没用使用中文编写文档,所以文档中的一些语句可能显得生硬。而且因为时间关系,文档中肯定还有不少的错误。如对你的
4、学习过程造成困扰,再次先表示歉意。2基础概念在开始正式学习nesC编程之前,我们需要先学习nesC的几个比较重要的概念。相对于其他编程模式,例如面向过程编程和面向对象编程,事件驱动编程,或者是面向事件编程显得比较特别,尤其是在无线传感器网络当中。因为无线传感器节点的程序储存空间十分有限,而且通常采用电池供电,所以要求我们的程序必须短小、精炼、高效。2.1接口(interface)一个完整的nesC程序是由一系列组件构成的,这些组件彼此之间通过事先定义好的接口进行沟通,从而协调程序各部分间的合作。与Java语言相似,在一个接口的内部,我们定义一系列相关的方法,也就是相当于C语言中的函数。在下面的
5、代码中我们给出一个简单的例子,Read接口。该接口主要用来读取某一个环境数据(温度、湿度等)。它只包含两个函数,用于读取数值的read和表示读取结束的readDone。我们可以看到接口内的函数只包含了函数的声明,但是并不包含函数体,也就是说它们是空的!接口需要被某一个nesC组件实现才能具备真正的执行能力,如果一个接口没有被实现,那它就不具备实用价值。负责实现某一个接口的nesC组件称之为该接口的提供者,而需要使用该接口的程序组件,则成为这一组件的使用者。当我们开发一个nesC程序的时候,我们需要首先考虑以下几个问题:我们的程序需要实现哪几种功能?哪些功能是可以通过使用TinyOS自带的接口来
6、实现的?实现这些接口的组件又是哪些?哪些功能是需要定义属于我们自己的接口?同一个接口可以由不同的组件来实现,例如我们此前提到过的,关于环境数据读取的问题。我们知道我们需要通过使用Read接口来读取温度,但是如果传感器平台不同,Read接口的提供者就未必相同。例如telosb节点和micaz节点未必使用同一组件来提供Read接口。2.2命令与事件(Command and Event)在此前的例子当中,有的读者可能已经注意到,read和readDone两个函数采用两个不同的前置关键字,command和event。命令和事件是nesC中两种函数类型。命令类型的函数由接口的提供者负责实现。有别于C语言
7、中的函数呼叫,我们需要等待函数运行结束,才能继续执行接下去的指令。在TinyOS中,我们推崇一种叫做Split-Phase的程序运作模式,也就是说将一项任务分为任务的投递、执行和反馈三个步骤。当我们呼叫一个命令时,该项任务就被投递到一个任务执行序列当中,等待逐一被系统执行。而主进程不会被锁死,可以继续执行接下去的指令。当此投递的任务被成功执行时,任务会返回一个事件给主进程,以告知任务运行结束。相反事件类型的函数则由接口的使用者负责实现,因为在接口的使用者呼叫一个命令之后,使用者需要等待命令返回的事件,并且在事件函数内对返回的数据进行处理。关于nesC编程中事件和任务的控制,将在稍后的小节中介绍
8、。我们举一个比较具体点的例子,某一个nesC程序有两个组件构成,A和B。A(使用者)想读取环境温度,所以它就需要使用接口Read,而Read接口由B组件来实现,B就是接口的提供者。A呼叫接口Read的read命令,然后继续忙自己的工作。B通过接口收到该呼叫,开始调用传感器节点上的硬件设备读取温度。一旦温度读取工作完成,B就发送一个readDone事件给A。A作为接口Read的使用者,需要实现接口内的readDone事件。在该事件内部,A取得读取的温度值,然后再计划下一步的工作。2.3模块与配置(Model and Configuration)nesC程序由两种类型的组件构成:模块和配置。2.3
9、.1模块在模块类组件主要包含了对它所操作接口的实现。如果一个模块使用了某个接口,则需要实现该接口内的所有事件函数,如果它提供某个接口,则需要实现该接口的所有命令函数。下面是例程Blink中的BlinkC模块的源代码,其主要功能是让传感器节点上的三枚发光二极管(LED)按照不同的频率闪烁。04-11行:模块的声明。我们可以看到该模块总共需要使用5个接口,其中3个计时器(Timer)接口。每个计时器控制一枚LED。Leds接口中包含了我们点亮和熄灭LED所需的命令函数,而Boot接口中则负责控制传感器节点的启动。12-38行:所使用的接口的事件函数的实现。14-19行:在一般情况下,booted是
10、程序接收到的第一个事件,表示我们的传感器节点已经正常启动。通常我们在booted事件函数内放置初始化代码。对于Blink程序,当节点启动的时候,我们需要通过呼叫startPeriodic命令函数来初始化三个计时器,让它们以不同的时间间隔开始计时。注意呼叫一个命令函数,我们需要使用call关键字。21-25行,27-31行,33-37行:针对三个计时器的fired事件函数的实现。当timer0被激活时,我们变更0号LED的状态(点亮或者熄灭)。timer1和timer2同理。就目前而言,当读者尝试去理解这段程序时,不要太过拘泥于一些语法上的细节,把注意力集中在程序的总体构成上。2.3.2配置在此
11、前的一个小节当中,我们列举出了BlinkC模块所需的各种接口。正如此前我们所说的,一个接口必须被实现,也就是说必须找到提供该接口的组件(提供者),不然该接口无法真正接受任何工作。所以一个完整的nesC程序还需要另外一类组件:配置,主要负责将接口的使用者和提供者紧密联系起来。我们可以看到程序的开头始终是组件的声明,在这段程序中,我们声明了一个配置类型的组件,称之为BlinkAppC。该组件不提供任何新的接口(没错,一个配置组件也可以提供接口,但是提供的方式方法有别于模块组件,我们将在接下来的小节中学习)。03-06行:列举出了Blink程序所需要的各种组件,其中自然也包括了BlinkC模块。Ma
12、inC组件和LedsC组件分别提供了Boot接口和Leds接口。TimerMilliC提供了Timer计时器接口,因为我们需要3个计时器,所以我们需要用as关键字对他们进行重命名,分别为Timer0,Timer1和Timer2。08-13行:建立起接口使用者和提供者之间的联系。例如第08行,我们读作“BlinkC模块中的Blink接口由MainC组件提供”。第13行则是一种简化的书写,因为LedsC组件只提供一个叫做Leds的接口,所以nesC可以自动识别。在建立起接口使用者和提供者之间的联系之后,我们的程序就可以编译了,因为MainC,LedsC和TimerMilliC三个组件已经包含在Ti
13、nyOS的发行版当中,无需再重新编写。一个完整的nesC程序包含至少一个配置组件。2.3.3可以提供接口的配置组件通常情况下,尤其是在小型的程序当中,在配置类组件内部,我们只做对接口使用者和提供者的连接。但是在某些特定的情况下,我们需要配置类组件也能够扮演接口提供者的角色。当一个模块类组件作为接口提供者的时候,我们需要在模块内部实现被提供接口的所有命令类函数,但是在一个配置类组件内部,我们无权放置接口的具体实现,所以我们唯一能做的,就是把该配置类组件所提供的接口直接与其真正的提供者连接。但是这么做的意义何在呢?为什么我们不直接把接口的使用者和提供者连接起来呢?为什么需要通过一个配置组件来绕一个
14、弯呢?假设我们现在正在开发一个叫做Encryption的nesC程序,用于进行数据加密。和Blink一样,该程序由两个组件构成,分别是TestEncryptC和TestEncryptAppC。TestEncryptC为模块型组件,在其内部我们放置所有接口的实现,例如Boot.booted,Timer.fired,等等。而TestEncryptAppC则是配置组件,在其内部我们将TestEncryptC所使用的接口连接到它们的提供者那里。TestEncryptAppC的源代码如下:这里我们可以看到TestEncryptC(被重命名为App)使用了一个叫做Encryption的接口,主要包含了数据
15、加密、解密的命令函数。该接口被连接到一个叫做EncryptionC的组件上,也就是说EncryptionC是Encryption接口的提供者。那么EncryptionC到底是什么类型的组件呢?模块?配置?前者不难理解,模块可以提供接口。但是出于灵活性考虑,EncryptionC最好是配置型组件。为什么呢?请看接下去的代码:我们看到EncyptionC是一个配置组件,但是它提供Encryption接口,而Encryption接口则是直接用=符号连接到另一个组件rsaP处,而rsaP才是真正实现Encryption接口的模块类组件。直到这里我们还是要问,那我们为什么不直接把TestEncryptC
16、.Encryption连接到rsaP.Encryption,而是要去EncryptionC那里绕一个远路呢?是的,我们当然可以这么干,而且程序的运行也不会受影响。但是如果哪天我们需要把RSA算法替换成ECC算法,那我们该怎么办?如果我们有多款应用程序,同时用到了Encryption接口,那该怎么办呢?如何以最便捷的方法实现对加密算法的替换呢?难道我们把所有的应用程序的配置文件都打开,然后逐个替换?那样效率太低了,而且容易出错。但是如果我们通过EncryptionC配置组件一绕,一切就变得简单得多了。只需要在EncryptionC内将rsaP替换成eccP即可。新版本的EncryptionC的源
17、代码就变成下面这样:对于其他应用程序,不需要修改任何东西,因为他们只和EncryptionC打交道,而且Encryption接口还是一如既往由EncryptionC组件来提供。虽然后台Encryption真正的提供者已经发生了改变,但是对于其他应用程序而言,它们对此并不感兴趣。就好比你去家乐福购物,某件商品的真正供货商是谁,你无需知道,你也无法知道,因为你只对商品本身感兴趣。最后请注意各组件的名称。rsaP和eccP都以P结尾,意为“私有”,表示在我们自己开发的应用程序当中,应当避免直接使用这类组件。这只是一种命名规则,并不能真正影响程序的执行,你完全可以在你的程序中,直接把一个接口连接到某个
18、“私有”的提供者上,但是并不建议这么做。当我们使用某个第三方nesC开发包时,我们不应直接碰那些私有组件。另外以C结尾的是普通组件,AppC结尾的是应用程序的总配置组件。2.3.4任务和事件在之前的小节当中我们有提到,我们建议在TinyOS中将相对繁重的工作放置在一个任务函数中执行。假设我们需要编写一个数据加密工具,我们定义一个接口Encryption,代码如下:不用太多的解释,我们也可以大致看明白该接口的工作方式。如果使用这个接口对数据进行加密,我们可以呼叫encrypt命令函数,因为加密运算通常需要耗费一定的时间,所以我们不希望在呼叫完encrypt之后,还得继续等待加密运算结束。所以在这
19、里就需要采用Split-Phase手法。当我们呼叫encrypt命令函数之后,我们将加密运算投递到任务执行序列当中,等待被执行。一旦该任务被成功执行,我们再返回encryptDone事件。数据解密同理。有了接口之后,我们就需要建立一个模块组件来实现该接口,称之为EncryptionC。05-06行:用于保存被加密数据和密钥的全局变量。实际应用中的密钥长度远远不只16位,此处只是一个例子。12-16行:encrypt命令函数。在投递加密任务前,先将数据和密钥保存入全局变量data和key内。因为任务型函数是不接受任何参数的。最后使用post命令将加密任务投递至任务执行序列当中。07-11行:用于
20、数据加密的任务函数。此处数据加密的算法与过程被略去了,因为不是我们要讲解的重点,在加密完成以后,我们使用signal命令返回encryptDone事件,同时返回保存有计算结果的cipher变量。在编写我们自己的Split-Phase过程时,要注意格外注意两点。需要注意task函数的复杂程度,因为TinyOS只有一条任务执行序列,如果你向其中投递了一个非常复杂庞大的任务,那会导致后续的任务无法被执行,导致整个系统失去响应。所以当你的task非常负责的时候,建议将其分割成一系列小型的task。也可以使用同一个task,但是需要被处理的数据保存入一个全局数组内,每次只处理其中的一小部分数据。如果数组
21、内的数据尚未被处理完,我们就再次post,如果数据已经被处理完毕,我们就signal运算结束的事件。最后一点,永远不要在命令函数内signal事件,为了避免在事件函数内,接口的使用者再次呼叫该命令,从而使得整个系统陷入到无尽的函数呼叫循环当中。我们总是在task任务函数内返回一个事件。3工作环境到目前为止,我们已经对nesC程序的构成有了简单的理解,现在我们可以开始做些简单的练习了。在开始写程序之前,自然是需要一个稳定的开发环境。在这里我们有一个好消息和一个坏消息。好消息是在TinyOS的官网上面,他们提供了多种在你电脑上安装、配置TinyOS开发环境的方法;坏消息是这些方法几乎都已经过时,在
22、新版的操作系统下很难为你创建一个良好的开发环境(囧rz)。TinyOS实质上是一整套由nesC编写的开发包,其主要任务是实现应用程序与底层硬件之间的通讯。对于nesC开发人员,他并不需要关心底层硬件的运作机制,他只需要把他的注意力完全集中到应用层。当我们编译nesC程序的时候,系统会先用nescc将nesC代码翻译成指定传感器平台的C语言代码,然后再用对应的编译器进行真正的编译。例如telosb平台使用的是MSP 430单片机,那系统就会调用msp-gcc进行编译,但如果是micaz节点,就会调用avr-gcc。通常此类编译器都是由单片机生产厂家直接提供,而且他们对系统的配置也有一定的要求。如
23、果我们尝试把TinyOS安装到最新版的Ubuntu或者Cygwin下面,那十有八九是要出问题的。或者是一开始的时候可以正常工作,但是在一两次系统更新之后,所有系统配置会被重新打乱。如果你不是Linux配置的高手,那我个人建议还是使用预先配置好的虚拟机。毕竟我们需要的是一个稳定的工作,并且能尽快开展工作,而不是把大把的时间浪费在系统的调试和测试上面。这里向大家推荐的是XubunTOS,是一套基于Xubuntu 7.04的VMware虚拟镜像。其内部已经安装配置好了TinyOS 2.1.0,默认编辑器是Emacs。读者可以在TinyOS的官方网站上面找到其下载链接,还可以在我的个人主页上面找到Em
24、acs的基本操作教程。下载完毕之后,只需将其导入到VMware内即可。7.04版的Ubuntu系统早以失去了官方的支持,所以如果你想安装其他的软件会显得比较麻烦。但是目前还有一些第三方软件源在为老版本的Ubuntu系统提供软件支持,我们只需修改/etc/apt/source.lst中的软件源链接即可。4编程开发终于可以开始讲解编程了,因为nesC的语法风格和标准C非常相似,所以我们不会花大篇幅讲解语法,而是直接通过更实际的例子来展现nesC程序的编写过程。首先我们会学习如果为telosb节点编译、安装一个nesC程序,然后是编写我们的nesC程序:TempRadio。4.1Blink此前在讲解
25、模块与配置的时候,我们已经看过Blink的源代码,这是一个随TinyOS一起发布的例程,很多教程都用它作为例子来讲解TinyOS的应用。Blink程序的源代码可以在/opt/tinyos-2.1.0/app/Blink下找到。首先我们把Blink目录拷贝至我们的home下面,然后将一个telosb节点用USB电缆连接至PC。在编译、安装Blink之前,我们需要检查,telosb节点是否被成功识别。我们可以使用motelist命令来罗列出所有连接至PC并且被成功识别的传感器节点:如果我们对这条命令进行解读,可以读作:为telosb平台编译此程序,并且将其安装至/dev/ttyUSB2的设备上。如
26、果程序被成功编译、安装,我们就会看到telosb节点上的三枚LED开始有规则是闪烁。如果我们打开Blink自带的Makefile,我们可以看到这个Makefile只包含两条语句。01行:整个编译工序的切入口,也是Blink这则程序的根配置组件。02行:将TinyOS自带的编译系统包含进来,继续接下去的编译工作。TinyOS自带的编译系统非常完善,它可以根据目标平台,自动包含所需的头文件,以及其他编译指令。我们可以在/opt/tinyos-2.1.0/support/make目录下找到TinyOS的整套编译工具。4.2 TempRadio在这个小节当中,我们将学习如何一步一步地构建起我们的第一个
27、nesC程序,TempRadio。读者也可以把它当作是一份小型的家庭作业,因为它包含了nesC编程中的所有基础技术:无线电通讯,串口通讯,温度测量。这个小程序主要由两部分构成:信号发送部分和信号接收部分。前者被放置在远处,负责读取环境温度,并且把温度值通过无线信号发送回基站。后者则扮演基站的角色,直接和PC通过USB电缆连接,把接收到的温度数据发送回PC。而在PC上面还有另外一个Java程序把温度数值逐一显示出来。正如我们在教程开头时候所说的一样,在开始真正编写代码之前,我们需要把这则程序所需的全部功能统统列举出来:读取环境数据(温度、湿度、光);无线通讯;串口通讯;PC上数据的解读与显示。我
28、们此前已经说过,在nesC中两个组件如果需要沟通,必须通过特定的接口。假设我们的程序(模块1)想通过天线(模块2)来发送信息,那我们的程序就需要使用接口AMSend。我们的程序(模块1)成为了AMSend的使用者,而天线模块(模块2)则是该接口的提供者。下面的列表给出了我们这个程序当中所需要用到的全部接口。读取温度:Timer:每隔一段时间,读取一次温度。Read:对于telosb平台,该接口由SensirionSht11C提供。这也是telosb平台自带的温度检测设备(Sensirion SHT11)。无线数据发送:以下三个接口均由ActiveMessageC组件负责提供。Packet:负责
29、管理数据包的接口。SplitControl:负责启动、关闭天线的接口。AMSend:该接口的send命令可用于发送数据包。无线数据接收:Receive:也由ActiveMessageC提供。串口数据发送:使用和无线数据发送一样的3个接口,但是由SerialActiveMessageC负责提供。PC端数据解读(Java):net.tinyos.message.MessageListner:用于监听、收取串口数据的Java接口。但是TinyOS已经内置了一个Java用具,net.tinyos.tools.MsgReader,用于显示收取到的串口信息。有的时候为了找到某一个接口的提供者,我们还不得不
30、去查阅相关传感器产品的Datasheet、各类例程,或者是去TinyOS的目录中逐级寻找。4.2.1数据的采集与发送我们首先从数据的发送端开始。我们首先创建一个模块和一个配置,分别命名为SenderC和SenderAppC。其中在SenderC内,我们列出所有我们需要使用的接口,然后实现所有这些接口内部的事件函数。如果想查阅需要实现的事件函数,可以直接查看接口的源代码,然后找出所有前缀为event的函数。绝大多数TinyOS的接口都可以在/opt/tinyos-2.1.0/tos/interfaces/目录内找到。以下是对SenderC模块组件的简单点评:01行:在Message.h头文件内我
31、们定义了我们需要发送的数据包的结构,为了能够使用这一结构,所以我们需要将这个头文件包含到当前模块内。03行:定义常量TIMER_PERIOD,我们要求每两秒读取一次温度。05-13行:模块的声明。在里面我们列出所有我们所需的接口。其中我们可以看到一些接口需要我们提供额外的参数。例如Timer接口,需要参数,表示计时器的等待时间是用毫秒来标注的。另外Read接口需要参数,表示读取的温度数值,是一个长度为16位的正整数。nesC中还有其他多种整数类型,例如int16_t,int8_t等等。具体他们的定义可以在stdint.h头文件中找到。15行:用来表示天线是否忙碌的布尔变量。16行:用来表示我们
32、需要发送的数据包。18-23行:所有我们需要实现的事件函数。我们发送的数据包结构被定义在头文件Message.h内。04-06行:我们需要发送的数据包结构,其中包含了网络通讯中所需要用到的数据类型,都需要nx_前缀,其中包括无线通讯和串口通讯。08行:我们需要发送的数据包的AM类型。一个传感器可以发送多种不同用途的数据包,温度的数据包、湿度的数据包、光线的数据包。但是在接收的时候任何区分这些包呢?我们就需要给每种数据包提供一个标签,也就是所谓的AM类型。AM类型的命名是有一定的规则的,永远是AM_ + 。6这个数值没有实际意义,只要确保每种AM类型的数值不同即可。我们之前还说过,对于任何一个nesC程序,都必须有至少一个配置型组件。没有配置
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1