Fork me on GitHub

TinyOS编程

原文地址:https://blog.csdn.net/utnewbear/article/details/6235519

和大家一样,我是按照 ../tinyos/cygwin/opt/tinyos-1.x/doc/tutorial 中的8个lesson进行操作和学习的。虽然很痛苦,可是还真没有什么别的更好的方法来学习这门奇怪的嵌入式语言。相信绝大多数同学在面对NesC的时候,最大的问题就是不知道从哪里下手,和自己到底要写些什么。以下的步骤,至少可以让你知道,你要使用NesC去做什么。

 

第一步,我们要根据实际情况去选择使用什么组件。 以编写blink为例:

 

首先我们需要main, main是程序开始的组件,是每个的TinyOS 程序(application)都必须的组件。或者可以说是NesC程序的入口,类似于C语言的main(),“Main”调用其他的 component以实现程序的功能。

 

第二,需要一个来控制程序逻辑的组件,或者说具体实现程序逻辑功能的组件。一般表达程序的逻辑思路,用和配置文件一样的名字,但是多了一个M,表示是module文件,本例中就是BlinkM,也就是我们上一篇当中提到的module文件所对应的组件。

 

第三,因为程序中用到了LED,所以需要系统提供的ledc。 没办法,这个是只有多看系统lib才行。

 

第四,因为程序需要时间控制,所以用到系统提供的timer(或者是用户定义的singletimer,其实用户定义的singletimer依然是调用了系统的timer. 后面会附上修改好去掉simpletimer的blink代码,需要的同学自己看)

 

总结,没有任何好方法,只有对系统熟悉,才能完成对底层的控制,必须去了解和学习那些底层的interface,不然是没有办法学习nesC的。

 

第二步,选择合适的组件之后就需要编写顶层配置文件(configuration)

从逻辑上来说,当你选定了组件之后,就需要顶层配置文件来wiring组件们,让他们协同工作,以完成你需要的程序功能。

事实上,一个程序中可以有多个配置文件,但一定要有一个顶级配置文件,通常会以application的名字来命名。


配置文件configuration首先声明了其应用程序下的组件,关键字:components.

本例中: components Main, BlinkM, SingleTimer, LedsC;


声明了组件之后,通过->可以将两个组件的接口连接起来。


本例中:Main.StdControl -> BlinkM.StdControl;

          Main.StdControl -> SingleTimer.StdControl;  

 BlinkM.Timer -> SingleTimer.Timer;   

linkM.Leds -> LedsC

 

回忆上一篇,我们说到:

有两个关键字来实现wiring,我翻译成“连接”好了。关键字 “à”和“ß”永远是将一个使用(uses)的接口于一个提供(provides)的接口相连接。 也就是说只有使用者组件能够调用提供者组件的接口。反过来就不可以。

 

Tinyos中,组件和接口是一个多对多的关系,即一个组件可以连接到很多接口,反过来说,很多组件都可以提供一个相同的接口!(核心!最难理解的地方!)

 

前面说,不同的组件可以提供相同的接口,如果组件ComA,ComB都提供了某一个接口InterfaceC, 那么,当组件ComD需要去访问InterfaceC接口时,怎么办? 它访问的到底是ComA提供的InterfaceC还是ComB提供InterfaceC的呢? 要知道,虽然接口的名称是一样的,但是不同组件提供的相同接口却是实现不同的功能。


那么这里, Main.StdControl -> BlinkM.StdControl;这行代码就是把组件main和blinkm的stdcontrol连接起来,这样,就建立了两个组件之间的联系。当调用main.stdcontrol的时候就相当于调用了blinkm.stdcontrol。Main.StdControl -> SingleTimer.StdControl; 这行代码就是把main和singleTimer的stdcontrol连接起来了,也建立了main和singletimer的联系。可以看到main这个user同时和两个provider连接。Main的stdcontrol在被调用的时候,blinkm.stdcontrol和SingleTimer.StdControl都会被调用。

 

现在,我们已经知道某些组件提供和使用的某些接口,比如blinkM提供StdControl,因为他在箭头的后面(Main.StdControl -> BlinkM.StdControl),他是提供者;同时他还使用Timer和Leds,因为他在箭头的前面(BlinkM.Timer和linkM.Leds),他是使用者。而SingleTimer和LedsC都是提供者,因为他们都是系统提供的lib,让你去控制灯的闪烁和时间。

 

总结:在tinyos中组件也是分层次的。最底层的组件贴近硬件部分,是经过一层一层封装才有了上层的组件,封装的过程中就使用了配置文件。而一个应用程序需要一个顶级配置文件,在所有其他的配置文件的更高一层,编译时会首先参照该文件进行编译。

 

第三步,既然已经有了顶层配置文件,可以写module文件了。

有了顶层配置文件相当于我们的房子已经有图纸,那么你知道我们的房子要建多少层,每层有多少房间,卫生间和厨房在什么位置。那么module文件就是在给你的程序添砖加瓦。让它真的能住人。

前面刚刚提到,blinkM提供StdControl接口,使用singleTimer的Timer接口和LedsC的Leds接口。所以blinkM应该这样写:

Blinkm.nc
module BlinkM {
provides {
interface StdControl;
}
uses {
interface Timer;
interface Leds;
}

}

我们前面说过:

一个组件如果provide某个interface,就必须实现这个interface当中所有的command。

现在blinkM provide StdControl,所以他必须提供StdControl的所有command。分别是init(),start(), stop(). 那么blinkM就变成:

 

Blinkm.nc
module BlinkM {
provides {
interface StdControl;
}
uses {
interface Timer;
interface Leds;
}
implementation {
command result_t StdControl.init() {
return SUCCESS;
}
command result_t StdControl.start() {
}
command result_t StdControl.stop() {
}

}

 

原则:在tinyos中,要使用一个组件(模块)必须先要初始化(init)它。

 

main是整个application的启动的入口,那么当然main可以启动与之相连接的模块。Main已经和谁关联了? 对,main和BlinkM以及SingleTimer都关联了。而main与他们关联的接口是什么呢? 没错,是stdcontrol。前面说了,当调用main.stdcontrol的时候就相当于调用了blinkm.stdcontrol和singleTimer.stdcontrol.那么blinkM和singleTimer都被启动了。

那么可以看到,我们顶层配置文件当中的4个组件,main,BlinkM,SingleTimer都启动了,就剩ledC还没有初始化。 但是问题是ledC没有提供stdControl接口,所以不能用main与之关联的方式去启动它。观察LedsC提供的Leds接口, 发现leds接口中有init() command. 我们通过command result_t StdControl.init() 去call Leds.init();进行ledC的初始化。

 

command result_t StdControl.init() {

Leds.init();
return SUCCESS;
}

 

至此,所有的组件都已经初始化完毕。而且blinkM 提供 stdControl接口,也已经实现它。但是还有一个问题:

         一个组件如果use某个interface,就必须实现这个interface当中的event。

blinkM 使用了leds接口和timer接口。

那么必须检查 leds 和timer接口,看是否有event,如果有event就必须实现。观察到leds是没有event,而timer接口是有event。
Timer.nc
interface Timer {
command result_t start(char type, uint32_t interval);
command result_t stop();
event result_t fired();
}
Timer接口有两个command和一个event。Start()命令被用于指定timer 的类型和那些即将过期的时间间隔。我们使用毫秒来计算时间间隔。有TIMER_REPEAT 和TIMER_ONE_SHOT 两种可用的类型。在使用TIMER_REPEAT模式的时候,我们可以用Start()命令形成一个循环,在指定的时间间隔过后,timer 将会结束,下一个重复的timer 将会继续执行,直到被stop()命令所终止。而当一个间隔到来时,事件 fired()就会被触发。

 

考虑程序的逻辑流程:

在我们需要的所有组件都启动后,Timer然后开始记录时间,当一个时间间隔过后,fired()事件被触发,并控制led,让灯闪烁。

 

所以把timer的start()放到blinkM的result_t StdControl.start()里,把timer的stop()放到blinkM的result_t StdControl.stop()里。所以最终的代码是:

Blinkm.nc
implementation {
command result_t StdControl.init() {
call Leds.init();
return SUCCESS;
}
command result_t StdControl.start() {
return call Timer.start(TIMER_REPEAT, 1000) ;
}
command result_t StdControl.stop() {
return call Timer.stop();
}
event result_t Timer.fired()
{
call Leds.redToggle();
return SUCCESS;
}
}

 

看到这里,其实一个标准的NesC程序就差不多明白了。

 

 

 

最后给出用tossim来模拟blink的方法,关键是给手头没有mote的同学看看tinyos程序的运行结果:

 

1、开始

在cygwin下,进入目录:c:/cygwin/opt/tinyos-1.x/apps/blink

运行命令:make pc

然后运行命令:export DBG=led

最后运行:build/pc/main.exe 3(这里的3指设置了3个传感器节点)

你就在console可以看到节点的输出


 

原文地址:http://lemondy.github.io/2015/06/10/Blink-tinyos/

简要的说下 Tinyos 的背景:它是 UC Berkeley(加州大学伯克利分校)开发的开放源代码操作系统,专为嵌入式无线传感网络设计,操作系统基于构件(component-based)的架构使得快速的更新成为可能,而这又减小了受传感网络存储器限制的代码长度。在物联网如火如荼的今天,这个系统大有作为。它是嵌入式,轻量级。可以对无线传感器节点,SmartNode 节点等各种设备进行编程监控环境等状况,物联网必将渗透在生活的各个角落。

该系统中程序的开发是利用 Nesc 语言,该语言是的语法是类似于 C 语言,但是与 C 语言也有些区别。有 C 语言基础的稍微花几天时间就可以进行 Nesc 编程了。

对于该系统的更多背景了解和一些配置教程请点击

Nesc 中接口(interface)

NesC 程序主要由各式组件(component)构成,组件和组件之间通过特定的接口(interface)互相沟通。一个接口内声明了提供相关服务的方法。例如数据读取接口(Read)内就包含了读取(read)、读取结束(readDone)函数。接口只是制定了组件之间交流的规范,也就是通过某一个接口,只能通过该接口提供的方法实现两个组件之间的交流。但是接口终归只是接口,只是一组函数的声明,并未包含对接口的实现。例如以下便是读取接口的代码:

1
2
3
4
5
6
7
 interface Read<val_t> {

command error_t read();


event void readDone( error_t result, val_t val );
}

只有函数的声明,但是没有函数体,所以需要有一个组件来实现(implementation)这个接口。实现某一个接口的组件,称之为提供者(provider),而使用该接口进行通信的,称之为用户(user)。

接口内的函数分两类,一类为命令(command),另一类为事件(event)。用户可以调用某一组件提供的接口命令,然后等待相应的事件被触发。也就是说命令是有外部主动的被调用,而事件是被动的执行。简单的假设就是:组件 A 提供了 Read 接口以便其他组件与之对话,组件 B 调用组件 A 的 Read 接口的 read 命令来读取某一个数据,例如温度,然后等温度读取完毕之后,系统返回一个 readDone(读取结束)的事件给组件 B。

Nesc 中的组件(component)

NesC 程序由组件构成。组件内主要是包含了对各类接口的使用(uses)和提供(provides)。例如组件 A 提供了Read 接口,那 A 就需要负责实现 Read 接口内的 read 命令,也就是 read 命令的函数体,即“具体这个值是如何读取出来的”。因为命令(command)是由接口的提供者(provider)负责实现的。如果组件 B 使用了 A 提供的Read 接口,那在读取数据结束以后,系统会返回给 B 一个“读取结束”的事件,而B则需要负责处理这个事件,即“数据读取完毕以后,我用这个数据干什么”,将值返回给计算机,或者是通过无线发送给其他传感器等等,所以事件(event)是由接口的使用者(user)来负责实现的。

组件分为两类。分别是模块(module)和配置(configuration)。

模块内包含了程序的主要逻辑实现代码,也就是对各类命令和事件的实现,是 NesC 程序的可执行代码的主体。而配置则是负责将各个模块,通过特定的接口连接起来,其本身并不负责实现任何特定的命令或者事件。

以 TinyOS 附带的 Blink(闪烁发光二极管)程序为例,

模块组件的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 // BlinkC.nc
#include "Timer.h"

//模块组件
module BlinkC @safe()
{
//声明使用的接口
uses interface Timer<TMilli> as Timer0;
uses interface Timer<TMilli> as Timer1;
uses interface Timer<TMilli> as Timer2;
uses interface Leds;
uses interface Boot;
}
implementation
{
//event时间必须在使用方中实现
event void Boot.booted()
{
call Timer0.startPeriodic( 250 );
call Timer1.startPeriodic( 500 );
call Timer2.startPeriodic( 1000 );
}

event void Timer0.fired()
{
dbg("BlinkC", "Timer 0 fired @ %s.\n", sim_time_string());
call Leds.led0Toggle();
}

event void Timer1.fired()
{
dbg("BlinkC", "Timer 1 fired @ %s \n", sim_time_string());
call Leds.led1Toggle();
}

event void Timer2.fired()
{
dbg("BlinkC", "Timer 2 fired @ %s.\n", sim_time_string());
call Leds.led2Toggle();
}
}

配置组件的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 代码
//BlinkAppC.nc
configuration BlinkAppC
{
}
implementation
{
components MainC, BlinkC, LedsC;
components new TimerMilliC() as Timer0;
components new TimerMilliC() as Timer1;
components new TimerMilliC() as Timer2;

//接口的使用方和提供方声明
BlinkC -> MainC.Boot;

BlinkC.Timer0 -> Timer0;
BlinkC.Timer1 -> Timer1;
BlinkC.Timer2 -> Timer2;
BlinkC.Leds -> LedsC;
}

Blink 程序由两个组件构成。BlinkC.nc 为模块,BlinkAppC.nc 为配置。

在模块 BlinkC 的声明内(module BlinkC {…})内表明了该程序需要用到的全部接口。因为 Blink 程序的主要目的是将 TelosB 传感器上的三盏 LED 发光二极管以不同的频率闪烁。所以我们需要三个精度为毫秒(TMilli)的计时器接口(Timer),分辨使用as关键字重命名为 Timer0,Timer1 和 Timer2。既然需要点亮发光二极管,自然需要一个操控发光二极管的接口,也就是 Leds,最后就是程序启动负责初始化的接口 Boot。

接下去在实现部分(implementation {…})。在实现部分需要实现所有我们用到的接口的事件,在这个程序里面,我们只是使用了接口,而作为这些接口的用户,我们只需要负责去实现他们的事件。这些接口内的命令,则由接口的提供者负责实现。
这里主要是两个事件,一个是 Boot 接口的 booted 事件,另一个是计时器被触发的 fired 事件。在 booted 事件中,也就是程序启动以后,我们的主要任务就一个,启动三个计时器:

1
2
3
call Timer0.startPeriodic( 250 );
call Timer1.startPeriodic( 500 );
call Timer2.startPeriodic( 1000 );

0号 Led 灯的频率为 4Hz,1 号 Led 灯的频率为 2Hz,2 号 Led 灯的频率为 1Hz。这里 startPeriodic 是一个启动计时器的命令,呼叫命令需要使用 call 关键字。同样,因为是命令,所以它们由接口的提供者负责实现,我们只负责使用就可以了。另一个需要我们处理的事件就是计时器的触发,因为有三个计时器,所以需要书写三个触发事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
event void Timer0.fired()
{
dbg("BlinkC", "Timer 0 fired @ %s.\n", sim_time_string());
call Leds.led0Toggle();
}

event void Timer1.fired()
{
dbg("BlinkC", "Timer 1 fired @ %s \n", sim_time_string());
call Leds.led1Toggle();
}

event void Timer2.fired()
{
dbg("BlinkC", "Timer 2 fired @ %s.\n", sim_time_string());
call Leds.led2Toggle();
}

先不关心 dbg 的那一行。我们可以看到 0 号计时器触发的时候,我们切换 0 号发光二极管的状态(如果是亮的则熄灭,如果是灭的则点亮);1 号计时器触发时则切换 1 号发光二极管;3 号计时器同理。同样的道理,led0Toggle,led1Toggle 和 led2Toggle 属于 Leds 接口的三个命令,只管用 call 调用使用便可。

接下去是看配置 BlinkAppC。这个组件本身并不使用或者提供任何接口,所以在其声明部分为空(configuration BlinkAppC{})。而在其实现(implementation)部分则需要实现对组件的连接。因为 BlinkC 模块使用了 Boot、Leds 和 Timer接口,所以必须指明这些接口都是由其他哪些组件提供的。所以:

1
2
3
4
5
6
7
8
9
10
11
components MainC, BlinkC, LedsC;
components new TimerMilliC() as Timer0;
components new TimerMilliC() as Timer1;
components new TimerMilliC() as Timer2;

BlinkC -> MainC.Boot;

BlinkC.Timer0 -> Timer0;
BlinkC.Timer1 -> Timer1;
BlinkC.Timer2 -> Timer2;
BlinkC.Leds -> LedsC;

先使用 component 关键字标明,这个程序当中,总共要用到哪几个组件。其中包括我们自己编写的 BlinkC 模块。还有负责提供 Boot 接口的 MainC 组件,负责提供 Leds 接口的 LedsC 组件。还有提供 Timer 接口的TimerMilliC,其属于泛型(generic)配置,支持被实例化。这里先不细说,因为我们需要用到三个计时器,所以需要使用 new 关键字创建三个计时器的实例,然后分别用 as 被重命名为 Timer0、Timer1 和 Timer2。

再往下就是组件之间的连接了。BlinkC 使用了 Boot 接口,而 MainC 正好提供了 BlinkC 所需的 Boot 接口,所以我们将他们进行连接。箭头所指方向为从使用者指向提供者。

1
2
3
BlinkC->MainC.Boot
// 或者像下面这样也是可以的。
MainC.Boot<-BlinkC

因为 BlinkC 内部就使用了一个 Boot 接口,所以 BlinkC 后面的 Boot 被省略了。完整的书写格式为:

1
2
// 意为:Blink组件内使用的Boot接口由MainC组件提供。
BlinkC.Boot->Mainc.Boot

接着是控制发光二极管的 Leds 接口,由 LedsC 组件提供。这里也进行了简写,完整的书写格式为:

1
BlinkC.Leds->LedsC.Leds

计数器的连接同理。
通过 Blink 程序,可以帮助我们理解 NesC 程序的构成和编程思路。这理解当然还有很多其他的技巧。

 

posted @ 2020-10-14 11:05  stardsd  阅读(1024)  评论(0编辑  收藏  举报