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。