3.5 Contiki开始了!

开始编译我们的第一个Contiki例程!打开终端,执行:

cd examples/hello-world
make TARGET=zoul savetarget

这将告诉Contiki为RE-Mote平台编译hello-world例程。如果要使用Z1平台,执行:

make TARGET=z1 savetarget

对于每个应用,你只需要做一遍上面的操作。编译应用程序:

make hello-world

如果没产生问题,将有类似下面的输出:

CC symbols.c
AR contiki-z1.a
CC hello-world.c
CC ../../platform/z1/./contiki-z1-main.c
LD hello-world.z1
rm obj_z1/contiki-z1-main.o hello-world.co

文件夹中应该生成一个文件hello-world.z1。接下来我们烧写程序到设备。

任何时候,你可以在编译时重新定义目标并覆盖原来保存的目标:

make TARGET=zoul hello-world
CC hello-world.c
LD hello-world.elf
arm-none-eabi-objcopy -O binary --gap-fill 0xff hello-world.elf hello-world.bin

这将忽略原先保存的文件Makefile.target,直接使用zoul作为目标。

3.5.1解释hello world

让我们来看看hello world的主要组件。使用gedit或者任何你喜欢的文本编辑器:

  gedit hello-world.c

当开始Contiki编程时,你可以使用一个名字申明一个进程。在每个程序里,你可以有几个进程。你可以像这样申明一个进程:

  PROCESS(hello_world_process, "Hello world process");①
  AUTOSTART_PROCESSES(&hello_world_process);②

①hello_world_process是进程的名字。"Hello world process"是进程可读的名字,比如打印到终端时。 ②AUTOSTART_PROCESSES(&hello_world_process) 告诉Contiki,当系统启动时自动开始运行hello_world_process进程。

/*-------------------------------------------------*/
PROCESS(hello_world_process, "Hello world process");
AUTOSTART_PROCESSES(&hello_world_process);
/*-------------------------------------------------*/
PROCESS_THREAD(hello_world_process, ev, data)①
{
PROCESS_BEGIN();②
printf("Hello, world\n");③
PROCESS_END();④
}

①申明进程线程的内容。包含进程和回调函数的名字(事件handler,数据handler)

②进程开始标识

③做你想做的事

④进程结束标识

3.5.2解释Makefile

应用程序编译时需要一个Makefile文件。我们看看hello-world中的Makefile吧:

CONTIKI_PROJECT = hello-world    ①
all: $(CONTIKI_PROJECT)          ②
CONTIKI = ../..                   ③
include $(CONTIKI)/Makefile.include ④

①告诉编译系统你需要编译哪个应用程序

②如果执行make all,它将编译被定义的程序

③定义变量CONTIKI为Contiki根目录

④Contiki整个系统的Makefile文件,也指明了平台的Makefile文件

虽然我们可以在Makefile中定义相关编译标志,但是推荐使用头文件project-conf.h作为编译配置文件。添加下面这一行到Makefile中:

DEFINES+=PROJECT_CONF_H=\"project-conf.h\"

然后,在例程文件夹里创建一个头文件project-conf.h。

在你正式上手Contiki之前,请记住驱动程序中有类似下面的切换:

#define DEBUG 0
#if DEBUG
#define PRINTF(...) printf(__VA_ARGS__)
#else
#define PRINTF(...)
#endif

将调试开关DEBUG修改为1或者DEBUG_PRINT,将在控制台打印调试信息。通常,你需要为你想调试的每个驱动文件做这样的操作。

3.5.3在例程中添加一个LED

下一步是在例程中添加一个LED与我们的应用程序交互。

你必须添加一个管理LED库的头文件dev/leds.h。你可以在core/dev/leds.h中查看相关函数。

相关函数:

unsigned char leds_get(void);
void leds_set(unsigned char leds);
void leds_on(unsigned char leds);
void leds_off(unsigned char leds);
void leds_toggle(unsigned char leds);

正常情况下,所有的平台都包含下面的LED:

LEDS_GREEN
LEDS_RED
LEDS_BLUE
LEDS_ALL

在Z1 mote中,这些LED相关宏定义在platform/z2/platform-conf.h中。

RE-Mote使用一个RGB LED——在一个设备中有三个通道的LED,可以显示任何红、绿、蓝组合的颜色。在头文件platforms/zoul/remote/borad.h中有如下定义:

LEDS_LIGHT_BLUE
LEDS_YELLOW
LEDS_PURPLE
LEDS_WHITE

现在,我们只将红色LED打开,看看会发生什么。创建一个新的例程文件,并命名为test-leds.c:

#include "contiki.h"
#include "dev/leds.h"
#include <stdio.h>
/*-------------------------------------------------*/
PROCESS(test_leds_process, "Test LEDs");
AUTOSTART_PROCESSES(&test_leds_process);
/*-------------------------------------------------*/
PROCESS_THREAD(test_leds_process, ev, data)
{
   PROCESS_BEGIN();
   leds_on(LEDS_RED);
   PROCESS_END();
}

编译、加载这个新工程:

make clean && make test-leds.upload

命令make clean用来删除先前编译生成的目标。

现在,红色LED应该是亮着的!

如果你修改了源代码,你必须重新编译,否者你所做的修改不会被加载到硬件中。我们总是推荐在编译之前使用make clean命令清除之前生成的目标文件。

3.5.4 打印消息到控制台

你可以使用printf将消息打印到控制台,从而知道应用程序是如何运行的。这是调试代码的很有用的手段,因为你可以打印变量的值、知道什么时候代码将阻塞等等。

我们尝试使用函数unsigned char leds_get(void);打印LED的状态。

获取LED的状态,并将其打印到屏幕。修改test-leds.c如下:

#include "contiki.h"
#include "dev/leds.h"
#include <stdio.h>
char hello[] = "hello from the mote!";
/*-------------------------------------------------*/
PROCESS(test_leds_process, "Test LEDs");
AUTOSTART_PROCESSES(&test_leds_process);
/*-------------------------------------------------*/
PROCESS_THREAD(test_leds_process, ev, data)
{
  PROCESS_BEGIN();
  leds_on(LEDS_RED);
  printf("%s\n", hello);
  printf("The LED %u is %u\n", LEDS_RED, leds_get());
  PROCESS_END();
}

如果有一个LED是点亮状态,你将得到LED的数量。这是在每个平台的platform-conf.h或者board.h中定义的。

练习:如果你打开多个LED将会怎样?你将得到多少数目?对于Z1和RE-Mote,他们的数目是相同的吗?

3.5.5 添加按键事件

现在我们想去检测用户是有将按键按下了。

在Contiki中,将按键当做是一个传感器。我们将会使用库core/dev/button-sensor.h

RE-Mote平台实现了额外的按键功能,比如长按检测,可用于将来扩展由按键触发的事件。文件platform/zoul/dev/button-sensor.c中有更详细的介绍,文件examples/zolertia/zoul/zoul-demo.c中有相关例程。

创建一个新的例程文件,命令为test-button.c,包含头文件dev/button-sensor.h和按键事件如下:

#include "contiki.h"
#include "dev/leds.h"
#include "dev/button-sensor.h"
#include <stdio.h>
/*-------------------------------------------------*/
PROCESS(test_button_process, "Test button");
AUTOSTART_PROCESSES(&test_button_process);
/*-------------------------------------------------*/
PROCESS_THREAD(test_button_process, ev, data)
{
  PROCESS_BEGIN();
  SENSORS_ACTIVATE(button_sensor);
  while(1) {
    PROCESS_WAIT_EVENT_UNTIL((ev==sensors_event) && (data == &button_sensor));
    printf("I pushed the button!\n");
  }
  PROCESS_END();
}

这个进程中有一个主循环。使用wait语句等待按键被按下。当你按下按键时,这两个条件将会被满足(事件来着传感器、事件是按键被按下),从而会在控制台上打印相关字符。

练习:当按下按键时,打开LED。当再次按键时,关闭LED。

3.5.6定时器

定时器允许我们在一个给定的时间触发事件,加快了从一个状态到另一个状态的转变,自动处理一个给定的进程或任务。例如,不需要按键就能每隔5秒闪烁一次LED灯。

Contiki OS包含4种定时器:

  • 简单定时器:一个简单的滴答。应用程序需要手动检查定时器是否到期了。更多信息请看core/sys/timer.h
  • 回调定时器:当一个定时器到期时,它能回调一个给定的函数。更多信息请看core/sys/ctimer.h
  • 事件定时器:与上面的基本相同,只是定时器到期时它不是回调一个函数,而是邮寄一个事件信号。更多信息请看core/sys/etimer.h
  • 实时定时器:实时模块处理实时任务的调度和执行。在同一时刻只允许一个这样的定时器。更多信息请看core/sys/rtimer.h

为了实现我们的目的,我们选择事件定时器,因为我们需要定时器到达一个给定周期时改变应用程序的行为。

我们创建一个定时器,并设定定时器超过给定秒数后所做的事。当定时器到期后,我们执行相应代码,并重置定时器。

创建一个例程文件,命名为test-timer.c

#include "contiki.h"
#include "dev/leds.h"
#include <stdio.h>
/*-------------------------------------------------*/
#define SECONDS 2
/*-------------------------------------------------*/
PROCESS(test_timer_process, "Test timer");
AUTOSTART_PROCESSES(&test_timer_process);
/*-------------------------------------------------*/
PROCESS_THREAD(test_timer_process, ev, data)
{
PROCESS_BEGIN();
static struct etimer et;
while(1) {
etimer_set(&et, CLOCK_SECOND*SECONDS); <1>
PROCESS_WAIT_EVENT():<2>
if(etimer_expired(&et)) {
printf("Hello world!\n");
etimer_reset(&et);
}
}
PROCESS_END();
}

<1>CLOCK_SECOND是与微控制器每秒滴答数相关的数目。由于Contiki运行在不同的平台、不同的硬件上,CLOCK_SECOND也是各不相同的。

<2>PRCESS_WAIT_EVENT()用于等待如何事件发生


练习:你可以打印CLOCK_SECOND的值来计算1秒有多少个滴答吗?尝试在某个确定的秒数时闪烁LED。写一个新的应用程序,当按下按键时才开始闪烁LED,当再次按键时才停止闪烁LED。