因仑“3+1”工程特种兵精英论坛

标题: TinyOS Tutorials——1.2 Modules [打印本页]

作者: leixiaofeng    时间: 2015-3-25 17:01
标题: TinyOS Tutorials——1.2 Modules
Modules and State

编译TinyOS的应用为二进制的文件来控制硬件。一个节点只运行TinyOS的镜像一次。镜像由应用程序需要的组件组成。大多数的节点平台没有基于硬件的内存保护,没有区分用户的地址和系统的地址,所有的组件共享一个地址空间。因此许多的TinyOS组件保持状态私有化并避免传递指针:由于没有硬件的保护,最好保持内存干净的方法是尽可能少得共享内存。

回想lesson 1提到的组件使用和提供的接口的集合。confugurations和modules这两种组件提供和使用接口。 不同之处体现在其实现部分:configurations是由根据其他组件连线实现的,而modules是可执行的代码。展开所有的configuration的抽象层后,内部是modules。 Module大多数是由C实现的, 为nesC的抽象提供额外的构造。

Modules可以声明状态变量。组件声明的任何状态都是私有的:其他的组件不可对其命名和访问。两个组件唯一直接交互的方法是通过接口。重新回顾下Blink的应用。下面是BlinkC的module和implementation:

[plain] view plaincopy



BlinkC没有分配任何的状态。变换一下它的逻辑:不让LED灯在三个不同的Timer下变换,而是在同一个Timer下,保留一些状态从而知道哪一个被触发。复制Blink应用为BlinkSingle, 并进入其目录.

[plain] view plaincopy



编辑BlinC的模块,注释掉Timer1和Timer2。

[plain] view plaincopy



下一步为BlinkC添加一些状态,一个单一的字节. 如C一样, 变量和函数必须在其使用前进行声明,因此把其放在实现的开始:

[plain] view plaincopy



不是标准C命名的int, long或char, TinyOS代码使用较明确的类型,其声明了变量的大小。实际上和基本的C类型是匹配的,但是不同的平台会有所不同。由于平台的不同,TinyOS代码避免了使用int.例如mica和Telos节点,int是16位,然而在IntelMote2,int是32位. 此外, TinyOS代码经常使用无符号的值,使用负数会导致不可预期的结果。 一般使用的类型如下:

8 bits16 bits32 bits64 bits
signedint8_tint16_tint32_tint64_t
unsigneduint8_tuint16_tuint32_tuint64_t

对于bool类型来说,可以使用标准的C类型,但是这么做会引发跨平台的问题。另外uint32_t较unsigned long好写。尽管是在软件上运算而不是硬件上,大多数的平台支持float类型 (float almost always,double sometimes)。

回到我们修改的BlinkC, 分配其单一的无符号的字节,counter.当节点启动 ,counter将初始化为0。下一步,当Timer0触发时counter递增,显示如下:

[plain] view plaincopy




另一个更简洁的方法是使用set命令:

[plain] view plaincopy



编译程序并安装到节点上。会看到和之前一样的效果,但是其是有一个timer实现的,而不是三个timer。

由于只使用了一个timer,即意味着不必使用Timer1和Timer2:它们浪费了CPU资源和内存。打开文件再次移除其签名(即声明)和实现。看到如下:

[plain] view plaincopy



尝试编译这个应用:nesC会抛出错误,由于configuration BlinkAppC连线到BlinkC不存在的Timer1和Timer2:

[plain] view plaincopy



打开BlinkAppC移除两个Timer和连线. 编译应用:

[plain] view plaincopy



如果和未更改的Blink比较ROM and RAM的大小,可以看到小了1bit:TinyOS只分配了状态和一个timer,只有一个timer的事件代码。

总结:编程时考虑如何才能优化代码,占用更少ROM和RAM而实现同样地功能。

Interfaces, Commands, and Events

回到tinyos-2.x/apps/Blink.lesson 1我们学到如果一个组件使用接口,它可以调用接口的命令,必须为其事件实现handlers。 BlinkC的组件使用了Timer, Leds和Boot 接口. 看一下这些接口:

[plain] view plaincopy



通过Boot, Leds和Timer接口,由于BlinkC使用了这些接口,它必须为Boot.booted()和Timer.fired()事件实现handler。Leds接口的声明不包括任何的事件,因此不必为调用Leds命令实现任何功能。再次看下BlinkC的实现Boot.booted():

[plain] view plaincopy



BlinkC使用了3个TimerMilliC组件的实例,分别连线到接口Timer0,Timer1, andTimer2。Boot.booted()事件处理每一个实例的开始。在时间启动后,startPeriodic()的参数指定在 milliseconds级别(it's millseconds because of the<TMilli> in the interface). 因为 the timer 使用startPeriodic()command开始的,在每个n毫秒fired()事件被 触发,the timer 会重置。

调用接口命令需要用call关键字,调用接口事件需要用signal关键字。BlinkC不提供任何的接口,所以它的代码没有任何的signal声明:在后续课程中,会看到boot序列,其利用signal调用了Boot.booted()事件。

以下是Timer.fired()的实现:

[plain] view plaincopy



由于它使用了Timer接口的三个实例,BlinkC必须实现三个Timer.fired()事件的实例。 当实现或调用接口函数时,函数的名字经常是interface.function. 例如BlinkC的三个接口命名为Timer0,Timer1和Timer2,它实现了三个函数Timer0.fired,Timer1.fired和Timer2.fired.

TinyOS Execution Model: Tasks

目前,我们看到的所有代码都是同步的。其运行在单一的可执行环境,没有任何的提前抢占。也就是说,当同步代码开始运行,在执行完毕前,其不放弃CPU给其他的同步代码。简单的机制允许TinyOS调度器来最小化RAM的消耗,简单的维护同步代码。然而,这也就意味着如果同步代码的一部分运行很长时间,它阻止了其他同步代码的运行,其不利于系统的响应能力。例如,长时间运行一段代码会增加节点响应数据包的时间。

目前为止,我们看到的所有例子都是直接调用函数。系统组件,例如组件的boot序列或timer,signal事件,会执行一些动作(可能调用一个命令)和返回。在大多数情况下,这种编程方法可以很好的运行。 由于同步代码是非抢占的,然而,对于大型计算这个方法是不适用的。组件需要有分割大型计算为小部分的能力,其可以一次执行一个。同样,当组件需要做其他事情时,可以延迟完成。TInyOS拥有延迟计算的能力,先等待再处理每一件事。

Tasks使组件可以执行应用中被认为是“后台”的处理。 task是组件告诉TinyOS延迟而不是马上运行的函数。和传统操作系统最接近的类比是interrupt bottom halves and 延迟处理调用.

复制Blink应用,命名为BlinkTask:

[plain] view plaincopy



打开BlinkC.nc. Currently, the event handler for Timer0.fired() is:

[plain] view plaincopy



改变一下它的工作,看其可以运行多久。在节点方面, 我们能看到的速率是很慢的(about 24 Hz, or 40 ms) : 在期间micaZ 和Telos可以发送20个数据包。所以这个例子是夸张的,但其足够简单来直观观察。改变处理如下 :

[plain] view plaincopy



这timer将触发开关400,001次,而不是1次。因为是奇数,最后的结果是单一个改变,在两者之间闪烁.编译并安装程序,会看到Led 0有延迟,看不到 Led 1和Led 2变化。 在 TelosB节点上,长时间的运行 task 会引发Timer的堆叠完全地跳过事件(try setting the count to 200,001 or 100,001).

这个问题是计算干涉了timer的操作。我们想要做的是告诉TinyOS延迟执行计算。可以利用task完成。

task在实现的module中声明使用的语法是:

[plain] view plaincopy



taskname()是任务的名称。任务必须返回void并且不带任何的参数。调度任务执行的语法是:
[plain] view plaincopy



组件可以用命令,事件或任务来公布一个任务。应为他们是调用图的根,任务可以安全的调用命令和信号事件。 按照惯例,命令不能signal事件来避免创建递归循环穿过组件的界限 (例如,命令X在组件1中发出组件2的事件Y,其调用组件1中的命令X). 这样的循环很难被程序员察觉(由于他们依赖于应用的连线)并将导致大量栈的使用。
修改BlinkC在任务中的循环:
[plain] view plaincopy



Telos平台仍然挣扎,而 mica平台操作很好.
post操作task序列,其以FIFO顺序处理。 当task被执行,它在下一个任务开始前运行完成。因此,如上例所示,任务不应该运行很长时间。Tasks不彼此抢占,task可以被硬件中断抢占(which we haven't seen yet). 如果想运行一系列长操作,应该为每个操作调度单独的任务,而不是使用一个大的任务。post操作返回error_t, 其值是SUCCESS或者FAIL. 当且仅当任务已经挂起去运行才会post失败(已经post成功,并还没有呼唤是)[1].
例如,尝试以下:
[plain] view plaincopy



这段代码破坏了计算,将其分成了许多小的任务。每次调用计算任务10,000 次迭代。如果没完成400,001次迭代, 它重新投回给自己。编译代码并运行,在Telos和mica-family节点上运行良好.
注意用这种方法使用任务需要在组件中包含另一变量i。因为computeTask()在10,000迭代后返回,它需要地方来为下一次调用存储状态。 在这个情况下,i作为静态函数变量和在C中的作用一样。然而,由于 nesC组件状态是完全私有的,使用static关键字来限制命名作用域是没有用的。例如这段代码是等价的:
[plain] view plaincopy




Internal Functions命令和事件是一个组件调用另一个组件的唯一方法。 组件很多时候需要私有的函数为其内部自己使用。组件可以定义标准的C函数,其他的组件不能命名,因此不能直接调用。而这些函数没有命令或事件的修饰,他们可以自由地调用命令或者标记的事件。例如以下的nesC代码:
[plain] view plaincopy



内部函数如C函数一样:他们不必调用或者用关键字标记。
Split-Phase Operations因为nesC接口是在编译时进行连线的,在TinyOS中回调是很高效的。在大多数类似C的语言中,回调利用一个函数指针注册在运行时。这可以编译器通过回调路径优化代码。由于他们在nesC中是静态地连接的,编译器准确地知道在哪里调用什么函数并可以很好的进行优化。
在TinyOS中因为没有块操作,通过组件边界优化的能力是非常重要的。 反而,每一个长时间运行的操作进行split-phase. 在一个块系统中,当程序调用一个长时间运行的操作,直到操作完成调用才返回. 在分阶段系统中,当程序调用一个长时间运行的操作,调用立即返回,当它完成时调用的抽象分发一个回调。这个方法叫做split-phase是因为它分离调用和完成为两个分开的阶段执行。这是一个简单的例子说明两者的不同:



Split-phase代码经常较顺序的代码冗长复杂。但是它有几个优点。首先,当执行时,split-phase调用不占用栈内存。其次,其保持系统的响应速度:从不会出现当应用需要执行而所有它的线程在块调用中被占用的情况。再次,因为在栈中创建大变量是没有必要的,其倾向于减少栈的使用。
Split-phase接口使得TinyOS组件容易立即开始几个操作,并并行的执行他们。同样, split-phase操作可以节省内存。这是由于当程序调用块操作时,所有的状态存储在调用栈中,(例如函数中变量的声明). 由于准确地决定栈的大小事困难的,操作系统选择经常非常保守,因此是较大的空间。当然,如果在调用过程中有数据需要维护,split-phase操作同样需要保存它。
命令Timer.startOneShot是一个split-phase调用的例子。Timer接口使用者调用命令, 其立即返回。过一会儿(有参数指定), 组件调用Timer的Timer.fired. 在块系统中,程序可能使用sleep():







欢迎光临 因仑“3+1”工程特种兵精英论坛 (http://bbs.enlern.com/) Powered by Discuz! X3.4