nesC编程迷你教程解读.docx

上传人:b****9 文档编号:25681933 上传时间:2023-06-11 格式:DOCX 页数:38 大小:2.06MB
下载 相关 举报
nesC编程迷你教程解读.docx_第1页
第1页 / 共38页
nesC编程迷你教程解读.docx_第2页
第2页 / 共38页
nesC编程迷你教程解读.docx_第3页
第3页 / 共38页
nesC编程迷你教程解读.docx_第4页
第4页 / 共38页
nesC编程迷你教程解读.docx_第5页
第5页 / 共38页
点击查看更多>>
下载资源
资源描述

nesC编程迷你教程解读.docx

《nesC编程迷你教程解读.docx》由会员分享,可在线阅读,更多相关《nesC编程迷你教程解读.docx(38页珍藏版)》请在冰豆网上搜索。

nesC编程迷你教程解读.docx

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接口需要参数,表示读取的温度数值,是一个长度为16位的正整数。

nesC中还有其他多种整数类型,例如int16_t,int8_t等等。

具体他们的定义可以在stdint.h头文件中找到。

•15行:

用来表示天线是否忙碌的布尔变量。

•16行:

用来表示我们需要发送的数据包。

•18-23行:

所有我们需要实现的事件函数。

我们发送的数据包结构被定义在头文件Message.h内。

•04-06行:

我们需要发送的数据包结构,其中包含了网络通讯中所需要用到的数据类型,都需要nx_前缀,其中包括无线通讯和串口通讯。

•08行:

我们需要发送的数据包的AM类型。

一个传感器可以发送多种不同用途的数据包,温度的数据包、湿度的数据包、光线的数据包。

但是在接收的时候任何区分这些包呢?

我们就需要给每种数据包提供一个标签,也就是所谓的AM类型。

AM类型的命名是有一定的规则的,永远是AM_+<数据包结构的大写>。

6这个数值没有实际意义,只要确保每种AM类型的数值不同即可。

我们之前还说过,对于任何一个nesC程序,都必须有至少一个配置型组件。

没有配置

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 初中教育 > 中考

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1