TinyOS Tutorials——1.2 Modules and the TinyOS Execution Model

原文地址http://docs.tinyos.net/tinywiki/index.php/Modules_and_the_TinyOS_Execution_Model

Modules and State

編譯TinyOS的應用爲二進制的文件來控制硬件。一個節點只運行TinyOS的鏡像一次。鏡像由應用程序需要的組件組成。大多數的節點平臺沒有基於硬件的內存保護,沒有區分用戶的地址和系統的地址,所有的組件共享一個地址空間。因此許多的TinyOS組件保持狀態私有化並避免傳遞指針:由於沒有硬件的保護,最好保持內存乾淨的方法是儘可能少得共享內存。

回想lesson 1提到的組件使用和提供的接口的集合。confugurations和modules這兩種組件提供和使用接口。 不同之處體現在其實現部分:configurations是由根據其他組件連線實現的,而modules是可執行的代碼。展開所有的configuration的抽象層後,內部是modules。 Module大多數是由C實現的, 爲nesC的抽象提供額外的構造。

Modules可以聲明狀態變量。組件聲明的任何狀態都是私有的:其他的組件不可對其命名和訪問。兩個組件唯一直接交互的方法是通過接口。重新回顧下Blink的應用。下面是BlinkC的module和implementation:

apps/Blink/BlinkC.nc:
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 void Boot.booted()
  {
    call Timer0.startPeriodic( 250 );
    call Timer1.startPeriodic( 500 );
    call Timer2.startPeriodic( 1000 );
  }

  event void Timer0.fired()
  {
    call Leds.led0Toggle();
  }

  event void Timer1.fired()
  {
    call Leds.led1Toggle();
  }

  event void Timer2.fired()
  {
    call Leds.led2Toggle();
  }
}

BlinkC沒有分配任何的狀態。變換一下它的邏輯:不讓LED燈在三個不同的Timer下變換,而是在同一個Timer下,保留一些狀態從而知道哪一個被觸發。複製Blink應用爲BlinkSingle, 並進入其目錄.

$ cd tinyos-2.x/apps
$ cp -R Blink BlinkSingle
$ cd BlinkSingle 

編輯BlinC的模塊,註釋掉Timer1和Timer2。

  event void Timer1.fired()
  {
    // call Leds.led1Toggle();
  }

  event void Timer2.fired()
  {
    // call Leds.led2Toggle();
  }

下一步爲BlinkC添加一些狀態,一個單一的字節. 如C一樣, 變量和函數必須在其使用前進行聲明,因此把其放在實現的開始:

implementation
{

  uint8_t counter = 0;

  event void Boot.booted()
  {
    call Timer0.startPeriodic( 250 );
    call Timer1.startPeriodic( 500 );
    call Timer2.startPeriodic( 1000 );
  }

不是標準C命名的int, long或char, TinyOS代碼使用較明確的類型,其聲明瞭變量的大小。實際上和基本的C類型是匹配的,但是不同的平臺會有所不同。由於平臺的不同,TinyOS代碼避免了使用int.例如mica和Telos節點,int是16位,然而在IntelMote2,int是32位. 此外, TinyOS代碼經常使用無符號的值,使用負數會導致不可預期的結果。 一般使用的類型如下:

  8 bits 16 bits 32 bits 64 bits
signed int8_t int16_t int32_t int64_t
unsigned uint8_t uint16_t uint32_t uint64_t

對於bool類型來說,可以使用標準的C類型,但是這麼做會引發跨平臺的問題。另外uint32_t較unsigned long好寫。儘管是在軟件上運算而不是硬件上,大多數的平臺支持float類型 (float almost always,double sometimes)。

回到我們修改的BlinkC, 分配其單一的無符號的字節,counter.當節點啓動 ,counter將初始化爲0。下一步,當Timer0觸發時counter遞增,顯示如下:

  event void Timer0.fired()
  {
    counter++;
    if (counter & 0x1) {
      call Leds.led0On();
    }
    else {
      call Leds.led0Off();
    }
    if (counter & 0x2) {
      call Leds.led1On();
    }
    else {
      call Leds.led1Off();
    }
    if (counter & 0x4) {
      call Leds.led2On();
    }
    else {
      call Leds.led2Off();
    }
  }

另一個更簡潔的方法是使用set命令:

  event void Timer0.fired()
  {
    counter++;
    call Leds.set(counter);
  }

編譯程序並安裝到節點上。會看到和之前一樣的效果,但是其是有一個timer實現的,而不是三個timer。

由於只使用了一個timer,即意味着不必使用Timer1和Timer2:它們浪費了CPU資源和內存。打開文件再次移除其簽名(即聲明)和實現。看到如下:

module BlinkC @safe(){
  uses interface Timer<TMilli> as Timer0;
  uses interface Leds;
  uses interface Boot;
}
implementation
{
  uint8_t counter = 0;

  event void Boot.booted()
  {
    call Timer0.startPeriodic( 250 );
  }

  event void Timer0.fired()
  {
    counter++;
    call Leds.set(counter);
  }

}

嘗試編譯這個應用:nesC會拋出錯誤,由於configuration BlinkAppC連線到BlinkC不存在的Timer1和Timer2:

dark /root/src/tinyos-2.x/apps/BlinkSingle -5-> make micaz
mkdir -p build/micaz
    compiling BlinkAppC to a micaz binary
ncc -o build/micaz/main.exe -Os -finline-limit=100000 -Wall -Wshadow -DDEF_TOS_AM_GROUP=0x7d -Wnesc-all -target=micaz 
-fnesc-cfile=build/micaz/app.c -board=micasb  -fnesc-dump=wiring -fnesc-dump='interfaces(!abstract())' 
-fnesc-dump='referenced(interfacedefs, components)' -fnesc-dumpfile=build/micaz/wiring-check.xml BlinkAppC.nc -lm
In component `BlinkAppC':
BlinkAppC.nc:54: cannot find `Timer1'
BlinkAppC.nc:55: cannot find `Timer2'
make: *** [exe0] Error 1

打開BlinkAppC移除兩個Timer和連線. 編譯應用:

mkdir -p build/micaz
    compiling BlinkAppC to a micaz binary
ncc -o build/micaz/main.exe -Os -finline-limit=100000 -Wall -Wshadow -DDEF_TOS_AM_GROUP=0x7d -Wnesc-all -target=micaz 
-fnesc-cfile=build/micaz/app.c -board=micasb  -fnesc-dump=wiring -fnesc-dump='interfaces(!abstract())' 
-fnesc-dump='referenced(interfacedefs, components)' -fnesc-dumpfile=build/micaz/wiring-check.xml BlinkAppC.nc -lm
    compiled BlinkAppC to build/micaz/main.exe
            2428 bytes in ROM
              39 bytes in RAM
avr-objcopy --output-target=srec build/micaz/main.exe build/micaz/main.srec
avr-objcopy --output-target=ihex build/micaz/main.exe build/micaz/main.ihex
    writing TOS image

如果和未更改的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 接口. 看一下這些接口:

tos/interfaces/Boot.nc:
interface Boot {
  event void booted();
}
tos/interfaces/Leds.nc:
interface Leds {

  /**
   * Turn LED n on, off, or toggle its present state.
   */
  async command void led0On();
  async command void led0Off();
  async command void led0Toggle();

  async command void led1On();
  async command void led1Off();
  async command void led1Toggle();

  async command void led2On();
  async command void led2Off();
  async command void led2Toggle();

  /**
   * Get/Set the current LED settings as a bitmask. Each bit corresponds to
   * whether an LED is on; bit 0 is LED 0, bit 1 is LED 1, etc.
   */
  async command uint8_t get();
  async command void set(uint8_t val);

}
tos/lib/timer/Timer.nc: 
interface Timer
{
  // basic interface
  command void startPeriodic( uint32_t dt );
  command void startOneShot( uint32_t dt );
  command void stop();
  event void fired();

  // extended interface omitted (all commands)
}

通過Boot, Leds和Timer接口,由於BlinkC使用了這些接口,它必須爲Boot.booted()和Timer.fired()事件實現handler。Leds接口的聲明不包括任何的事件,因此不必爲調用Leds命令實現任何功能。再次看下BlinkC的實現Boot.booted():

apps/Blink/BlinkC.nc: 
  event void Boot.booted()
  {
    call Timer0.startPeriodic( 250 );
    call Timer1.startPeriodic( 500 );
    call Timer2.startPeriodic( 1000 );
  }

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()的實現:

apps/Blink/BlinkC.nc: 
  event void Timer0.fired()
  {
    call Leds.led0Toggle();
  }

  event void Timer1.fired()
  {
    call Leds.led1Toggle();
  }

  event void Timer2.fired()
  {
    call Leds.led2Toggle();
  }
}

由於它使用了Timer接口的三個實例,BlinkC必須實現三個Timer.fired()事件的實例。 當實現或調用接口函數時,函數的名字經常是interface.function. 例如BlinkC的三個接口命名爲Timer0,Timer1和Timer2,它實現了三個函數Timer0.fired,Timer1.firedTimer2.fired.

TinyOS Execution Model: Tasks

目前,我們看到的所有代碼都是同步的。其運行在單一的可執行環境,沒有任何的提前搶佔。也就是說,當同步代碼開始運行,在執行完畢前,其不放棄CPU給其他的同步代碼。簡單的機制允許TinyOS調度器來最小化RAM的消耗,簡單的維護同步代碼。然而,這也就意味着如果同步代碼的一部分運行很長時間,它阻止了其他同步代碼的運行,其不利於系統的響應能力。例如,長時間運行一段代碼會增加節點響應數據包的時間。

目前爲止,我們看到的所有例子都是直接調用函數。系統組件,例如組件的boot序列或timer,signal事件,會執行一些動作(可能調用一個命令)和返回。在大多數情況下,這種編程方法可以很好的運行。 由於同步代碼是非搶佔的,然而,對於大型計算這個方法是不適用的。組件需要有分割大型計算爲小部分的能力,其可以一次執行一個。同樣,當組件需要做其他事情時,可以延遲完成。TInyOS擁有延遲計算的能力,先等待再處理每一件事。

Tasks使組件可以執行應用中被認爲是“後臺”的處理。 task是組件告訴TinyOS延遲而不是馬上運行的函數。和傳統操作系統最接近的類比是interrupt bottom halves and 延遲處理調用.

複製Blink應用,命名爲BlinkTask:

$ cd tinyos-2.x/apps
$ cp -R Blink BlinkTask
$ cd BlinkTask

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

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

改變一下它的工作,看其可以運行多久。在節點方面, 我們能看到的速率是很慢的(about 24 Hz, or 40 ms) : 在期間micaZ 和Telos可以發送20個數據包。所以這個例子是誇張的,但其足夠簡單來直觀觀察。改變處理如下 :

event void Timer0.fired() {
  uint32_t i;
  dbg("BlinkC", "Timer 0 fired @ %s\n", sim_time_string());
  for (i = 0; i < 400001; i++) {
    call Leds.led0Toggle();
  }
}

這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中聲明使用的語法是:

 
 task void taskname() { ... }

 taskname()是任務的名稱。任務必須返回void並且不帶任何的參數。調度任務執行的語法是:

 post taskname();

組件可以用命令,事件或任務來公佈一個任務。應爲他們是調用圖的根,任務可以安全的調用命令和信號事件。 按照慣例,命令不能signal事件來避免創建遞歸循環穿過組件的界限 (例如,命令X在組件1中發出組件2的事件Y,其調用組件1中的命令X). 這樣的循環很難被程序員察覺(由於他們依賴於應用的連線)並將導致大量棧的使用。

修改BlinkC在任務中的循環:

task void computeTask() {
  uint32_t i;
  for (i = 0; i < 400001; i++) {}
}

event void Timer0.fired() {
  call Leds.led0Toggle();
  post computeTask();
}

Telos平臺仍然掙扎,而 mica平臺操作很好.

post操作task序列,其以FIFO順序處理。 當task被執行,它在下一個任務開始前運行完成。因此,如上例所示,任務不應該運行很長時間。Tasks不彼此搶佔,task可以被硬件中斷搶佔(which we haven't seen yet). 如果想運行一系列長操作,應該爲每個操作調度單獨的任務,而不是使用一個大的任務。post操作返回error_t, 其值是SUCCESS或者FAIL. 當且僅當任務已經掛起去運行纔會post失敗(已經post成功,並還沒有呼喚是)[1].

例如,嘗試以下:

uint32_t i;

task void computeTask() {
  uint32_t start = i;
  for (;i < start + 10000 && i < 400001; i++) {}
  if (i >= 400000) {
    i = 0;
  }
  else {
    post computeTask();
  }
}

這段代碼破壞了計算,將其分成了許多小的任務。每次調用計算任務10,000 次迭代。如果沒完成400,001次迭代, 它重新投回給自己。編譯代碼並運行,在Telos和mica-family節點上運行良好.

注意用這種方法使用任務需要在組件中包含另一變量i。因爲computeTask()在10,000迭代後返回,它需要地方來爲下一次調用存儲狀態。 在這個情況下,i作爲靜態函數變量和在C中的作用一樣。然而,由於 nesC組件狀態是完全私有的,使用static關鍵字來限制命名作用域是沒有用的。例如這段代碼是等價的:

task void computeTask() {
  static uint32_t i;
  uint32_t start = i;
  for (;i < start + 10000 && i < 400001; i++) {}
  if (i >= 400000) {
    i = 0;
  }
  else {
    post computeTask();
  }
}

 

Internal Functions

命令和事件是一個組件調用另一個組件的唯一方法。 組件很多時候需要私有的函數爲其內部自己使用。組件可以定義標準的C函數,其他的組件不能命名,因此不能直接調用。而這些函數沒有命令或事件的修飾,他們可以自由地調用命令或者標記的事件。例如以下的nesC代碼:

module BlinkC {
  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
{

  void startTimers() {
    call Timer0.startPeriodic( 250 );
    call Timer1.startPeriodic( 500 );
    call Timer2.startPeriodic( 1000 );
  }

  event void Boot.booted()
  {
    startTimers();
  }

  event void Timer0.fired()
  {
    call Leds.led0Toggle();
  }

  event void Timer1.fired()
  {
    call Leds.led1Toggle();
  }

  event void Timer2.fired()
  {
    call Leds.led2Toggle();
  }
}

內部函數如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():

 

 

 

下一節,將看到最基本的split-phase操作:發送數據包。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章