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