Contiki is an open source operating system for the Internet of Things, it connects tiny low-cost, low-power microcontrollers to the Internet.
Contiki provides powerful low-consumption Internet communication, it supports fully standard IPv6 and IPv4, along with the recent low-power wireless standards: 6lowpan, RPL, CoAP. With Contiki’s ContikiMAC and sleepy routers, even wireless routers can be battery-operated.
With Contiki, development is easy and fast: Contiki applications are written in standard C, with the Cooja simulator Contiki networks can be emulated before burned into hardware, and Instant Contiki provides an entire development environment in a single download.
Visit the Contiki OS site for more information.
The remainder of the chapter intends to be a thoughtful introduction to Contiki 3.0, its core components and features. The following references are the must-go places to search for detailed information:
There are several ways to install Contiki, from scratch by installing from sources or using virtual environments, depending on the flavour and time availability.
To work with Contiki you will need three items:
-
The Contiki source code.
-
A target platform (virtual platform or a real hardware one).
-
A toolchain to compile the source code for such target platform.
The remainder of the book assumes Contiki will run in an Unix environment, as the virtualized environments run on Ubuntu.
The following instructions will guide you to install Contiki on your machine. These instructions were tested in Ubuntu-like devices (version 12.04 and onwards). If you are looking for a ready to use setup, skip this section and download one of the Virtual Machines in the next section.
This will install support for the ARM Cortex-M3 and MSP430 platforms, as well as support for Cooja, the Contiki’s emulator to be discussed in the next sections.
The "IoT in five days" Virtual Machine has a comprehensive lists of additional tools and libraries installed, check the next section for a complete list.
The following are the minimal recomended libraries to run Contiki.
To install the toolchain and required dependencies, run in a terminal the following:
sudo apt-get update
sudo apt-get install gcc-arm-none-eabi gdb-arm-none-eabi
sudo apt-get -y install build-essential automake gettext
sudo apt-get -y install gcc-arm-none-eabi curl graphviz unzip wget
sudo apt-get -y install gcc gcc-msp430
sudo apt-get -y install openjdk-7-jdk openjdk-7-jre ant
# Install Homebrew - http://brew.sh/
brew tap PX4/homebrew-px4
brew update
# Install GCC Arm Toolchain
brew install gcc-arm-none-eabi-49
Download the GCC ARM toolchain (Windows installer) from:
Tested with gcc-arm-none-eabi-5_3-2016q1-20160330-win32.exe
.
Execute and select the add path to environment variable
option.
Next download MINGW, install and make sure the following packages are selected: mingw32-base, mingw32-gcc-g++, msys-base.
Under "All Packages" select the msys-mintty package for terminal support. This will install MINTTY
in the default location C:\MinGW\msys\1.0\bin\mintty.exe
. Create a shortcut and add this to the Shortcut target command:
C:\MinGW\msys\1.0\bin\mintty.exe /bin/bash -l
Add the following paths to your $path
environment variable:
C:\MinGW\bin;C:\MinGW\msys\1.0\bin
Run MINTTY
and execute the following commands:
mingw-get update
mingw-get install msys-wget
mingw-get install msys-zlib
mingw-get install msys-unzip
Contiki source code is actively supported by contributors from universities, research centers and developers from all over the world.
The source code is hosted at Contiki GitHub repository:
The latest Contiki release is 3.0, the release tag is available at:
Nevertheless you should use the latest commit available, as Contiki releases are produced on a yearly base. Many bug fixes, new features and improved support is normally present on the latest master
branch.
To grab the source code open a terminal and execute the following:
sudo apt-get -y install git
git clone --recursive https://github.com/contiki-os/contiki.git
Note
|
At the moment of this release, the current Contiki commit corresponds to the
Replace As Contiki ensures the platform and application support by using a strict code revision procedure and regression tests, this is a safe point if you encounter any problem. Be sure to update your Contiki local repository if using |
Git is a free and open source distributed version control system, designed for speed and efficiency.
The main difference with other change control tools is the possibility to work locally since your local copy is a repository, and you can commit to it and get all benefits of source control.
There are some great tutorials online to learn more about git:
GitHub is a GIT repository web-based hosting service, which offers all of the distributed revision control and source code management (SCM) functionality of Git as well as adding its own features. GitHub provides a web-based graphical interface and desktop as well as mobile integration. It also provides access control and several collaboration features such as wikis, task management, bug tracking and feature requests for every project.
The advantage of using GIT and hosting the code at github is that of allowing people to fork the code, further develop it, and then contribute back to share their improvements.
Additionally there is a branch
available with the IoT in five days
content in a workshop-like format. The branch is available at:
It is recomended to add this branch
as a remote repository
of the already cloned Contiki repository, so you can keep track of the development done in the master
branch.
cd /home/user/contiki
git remote add iot-workshop https://github.com/alignan/contiki
git fetch iot-workshop
git checkout iot-workshop
The above commands will add the https://github.com/alignan/contiki
repository to the list of remote
repositories, making the iot-workshop
branch available in your machine as a working copy
. Now you can track any changes and updates.
This branch has ready to use examples and applications to guide you through Contiki and building real IoT applications. In the next sections this content will be further discussed.
Even if installing Contiki should be straightforward, there are already built Virtual Machines available for you to download and use out of the box.
The available Virtual Machines images are available for VMWare.
Download VMWare player for Windowws and Linux to run Contiki’s virtual machine, it is free and widely used.
In OSX you can download VMWare Fusion
Instant Contiki is an entire Contiki development environment in a single download. It is an Ubuntu Linux virtual machine and has Contiki OS and all the development tools, compilers, and simulators required already pre-installed.
Grab Instant Contiki from the Contiki website:
The latest Instant Contiki release is 3.0, following the Contiki 3.0 source code release.
Using VMWare just open the Instant_Contiki_Ubuntu_12.04_32-bit.vmx
file, if prompted about the VM source just choose I copied it
then wait for the virtual Ubuntu Linux boot up.
Log into Instant Contiki. The password and user name is user
. Don’t upgrade right now.
Remember to update the Contiki repository and get the latest upgrades:
cd /home/user/contiki
git fetch origin
git pull origin master
Notice the Instant Contiki
does not have the IoT workshop
branch with the examples of this book, read the previous section on how to clone.
A Virtual Machine with the content of the book is provided as a free download from the following location:
In a nutshell it packs everything the Instant Contiki
has, but it has been built using Ubuntu Server 16.04 LTS (Xenial)
instead. Additionally several packages has been installed to make development easier, including a fully-configured Eclipse IDE workspace.
The "IoT in five days" book, its source and ready to use Contiki examples are included.
Log into the Virtual Machine. The password and user name is user
.
The "IoT in five days" Virtual Machine has already the iot-workshop
branch cloned and available, with the suggested book examples and a ready-to-use Contiki configuration.
Let us first check the toolchain installation. The MSP430 toolchain can be tested with:
msp430-gcc --version
msp430-gcc (GCC) 4.7.0 20120322 (mspgcc dev 20120716)
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
And for the ARM Cortex-M3 toolchain:
arm-none-eabi-gcc --version
arm-none-eabi-gcc (GNU Tools for ARM Embedded Processors) 4.9.3 20150529 (release) [ARM/embedded-4_9-branch revision 224288]
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Contiki has the following file structure:
Folder | Description | Zolertia files |
---|---|---|
examples |
Ready to build examples |
examples/zolertia, examples/cc2538-common |
app |
Contiki applications |
- |
cpu |
Specific MCU files |
msp430, cc2538 |
dev |
External chip and devices |
cc2420, cc1200 |
platform |
Specific files and platform drivers |
z1, zoul |
core |
Contiki core files and libraries |
- |
tools |
Tools for flashing, debuging, simulating, etc. |
zolertia, sky |
doc |
Self-generated doxygen documentation |
- |
regression-tests |
nightly regression tests |
- |
If you cloned the iot-workshop
branch (or if you are using the "IoT in five days" Virtual Machine), you will find also the following folder:
contiki/examples/zolertia/tutorial
The content of the tutorial
is:
The examples work out of the box for both Zolertia Z1
and RE-Mote
platforms to be shown next.
The examples are written to be followed in order, so be sure to start from the very first and make your way to the last in an ordered fashion.
In the following sections we will cover the examples and specific platform files for the Zolertia platforms.
For the remainder of the book we will use the Zolertia Z1 and RE-Mote hardware development platforms. Other platforms can be used as well, but are out of the scope of this book.
Contiki drivers and libraries are (normally) platform independent, most examples can be run in both Z1 and RE-Mote platforms by taking into consideration specific platform settings.
There are platform-specific examples for the Z1 platform at examples/zolertia/z1
, and at examples/zolertia/zoul
for the RE-Mote. Additionaly CC2538
specific examples can be found at examples/cc2538-common
. This folder has CC2538 ARM Cortex-M3 related examples and applications.
More information about both platforms and updates guides can be found at:
The Zoul is a module based in the CC2538 ARM Cortex-M3 system on chip (SoC), with an on-board 2.4 GHz IEEE 802.15.4 RF interface, running at up to 32 MHz with 512 kB of programmable flash and 32 kB of RAM, bundled with a CC1200 868/915 MHz RF transceiver to allow dual band operation.
The Zoul allows fast reusability on designs and quick scaling from prototyping to production.
The RE-Mote (revision A) has a Zoul on board and also packs:
-
ISM 2.4-GHz IEEE 802.15.4 & Zigbee compliant radio.
-
ISM 863-950-MHz ISM/SRD band radio.
-
AES-128/256, SHA2 Hardware Encryption Engine.
-
ECC-128/256, RSA Hardware Acceleration Engine for Secure Key Exchange.
-
Consumption down to 150 nA using the shutdown mode.
-
Programming over BSL without requiring to press any button to enter bootloader mode.
-
Built-in battery charger (500 mA), facilitating Energy Harvesting and direct connection to Solar Panels and to standards LiPo batteries.
-
Wide range DC Power input: 2-16 V.
-
Small form-factor.
-
MicroSD over SPI.
-
On board RTC (programmable real time clock) and external watchdog timer (WDT).
-
Programmable RF switch to connect an external antenna either to the 2.4 GHz or to the Sub 1 GHz RF interface through the RP-SMA connector.
The RE-Mote has been developed jointly with universities and industrial partners in the frame of the European research project RERUM(RERUM: REliable, Resilient and secUre IoT for sMart city applications).
The Z1 mote features a second generation MSP430F2617 low power 16-bit RISC CPU @ 16 MHz MCU, 8 kB RAM and a 92 kB Flash memory. It includes the well known CC2420 transceiver, IEEE 802.15.4 compliant, which operates at 2.4 GHz with an effective data rate of 250 kbps.
The Zolertia Z1 mote can run TinyOS, Contiki OS, OpenWSN and RIOT, and has been used actively for over 5 years in universities, research and development centers and in commercial products in more than 43 countries, being featured in more than 50 scientific publications.
The Z1 mote is fully emulated in both MSPSIM and Cooja.
In a nutshell: power and coolness.
The RE-Mote has 4 times more RAM than the Z1 mote, 5 times more flash memory, twice the frequency of operation and 120 times less power consumption in its lowest power mode (shutdown mode).
Another major difference is that (at the time of writing this book) the Z1 mote is supported by Cooja, the Contiki emulator, while the RE-Mote is not. However efforts are on going to provide emulation framework support in the EMUL8 project.
To check in depth the differences between the RE-Mote and the Z1 mote, and also obtain guidelines to port applications developed for the Z1 to the RE-Mote, visit the "Migrate from Z1 to RE-Mote" wiki page.
Let’s compile our first Contiki example! Open a terminal and write:
cd examples/zolertia/tutorial/01-basics
make TARGET=zoul savetarget
This will tell Contiki to compile the hello world example for the RE-Mote platform from now on. Alternatively, to use the Z1 mote instead, just run:
make TARGET=z1 savetarget
You need to do this only once per application. Not let’s compile the application:
make 01-hello-world
if everything works OK you should see something like for the Z1
mote:
CC symbols.c
AR contiki-z1.a
CC 01-hello-world.c
CC ../../../../../platform/z1/./contiki-z1-main.c
LD 01-hello-world.z1
rm obj_z1/contiki-z1-main.o 01-hello-world.co
The 01-hello-world.z1
file should have been created and we are ready to flash the application to the device.
And likewise if we were to compile for the RE-Mote
platform:
make TARGET=zoul 01-hello-world
CC 01-hello-world.c
LD 01-hello-world.elf
arm-none-eabi-objcopy -O binary --gap-fill 0xff 01-hello-world.elf 01-hello-world.bin
Tip
|
If a |
In the following sections and chapters the examples can be compiled for both the Z1 and RE-Mote platforms.
Note
|
The RE-Mote takes two arguments: |
Let’s see the main components of the Hello World example. Browse the code with:
gedit 01-hello-world.c
Or your preferred text editor (Sublime text, Eclipse, etc).
When starting Contiki, you declare processes with a name. In each code you can have several processes. You declare the process like this:
PROCESS(hello_world_process, "Hello world process"); // (1)
AUTOSTART_PROCESSES(&hello_world_process); // (2)
-
hello_world_process
is the name of the process and"Hello world process"
is the readable name of the process when you print it to the terminal. -
The
AUTOSTART_PROCESSES(&hello_world_process)
tells Contiki to start that process when it finishes booting.
/*-------------------------------------------------*/
PROCESS(hello_world_process, "Hello world process");
AUTOSTART_PROCESSES(&hello_world_process);
/*-------------------------------------------------*/
PROCESS_THREAD(hello_world_process, ev, data) (1)
{
PROCESS_BEGIN(); (2)
printf("Hello, world\n"); (3)
printf("%s\n", hello);
printf("This is a value in hex 0x%02X, the same as %u\n", num, num);
PROCESS_END(); (4)
}
-
You declare the content of the process in the process thread. You have the name of the process and callback functions (event handler and data handler).
-
Inside the thread you begin the process,
-
do what you want and
-
finally end the process.
In this concrete example it is shown how to print values of different types: a numeric value (using different format qualifiers) and a string (literal or stored in a variable).
Applications require a Makefile to compile, let us take a look at the hello-world
Makefile:
CONTIKI_PROJECT = 01-hello-world (1)
all: $(CONTIKI_PROJECT) (2)
CONTIKI = ../../../.. (3)
include $(CONTIKI)/Makefile.include (4)
-
Tells the build system which application to compile
-
If using
make all
it will compile the defined applications -
Specify our indentation level respect to Contiki root folder
-
The system-wide Contiki Makefile, also points out to the platform’s Makefile
We can define specific compilation flags in the Makefile, although the
recommended way would be to add a project-conf.h
header, and define there
any compilation flag or value. This is done by adding this to the Makefile:
DEFINES+=PROJECT_CONF_H=\"project-conf.h\"
And then creating a project-conf.h
header file in the example location.
Tip
|
The make login PORT=/dev/ttyUSB0 Note this will keep a connection open in the terminal, if you close the terminal the connection will be closed. Also, as programing the devices uses the USB port, if there is an existing connection open with the You can also concatenate commands: make 01-hello-world.upload && make upload |
The 01-hello-world
shows the following when connected over the USB with the make login
command:
using saved target 'zoul'
../../../../tools/sky/serialdump-linux -b115200 /dev/ttyUSB0
connecting to /dev/ttyUSB0 (115200) [OK]
Contiki-3.x-2612-g1d456b1
Zolertia RE-Mote platform
CC2538: ID: 0xb964, rev.: PG2.0, Flash: 512 KiB, SRAM: 32 KiB, AES/SHA: 1, ECC/RSA: 1
System clock: 16000000 Hz
I/O clock: 16000000 Hz
Reset cause: External reset
Rime configured with address 00:12:4b:00:06:15:ab:25
Net: sicslowpan
MAC: CSMA
RDC: ContikiMAC
Hello, world
Hello world, again!
This is a value in hex 0xABCD, the same as 43981
This section will show how to use the LED (light emitting diode) to interact with our application. Also it will be shown how to use the on-board user button
to trigger specific events and change the way our application works.
You have to add the dev/leds.h
which is the library to manage the LEDs. To check the available functions go to core/dev/leds.h
.
Available LEDs commands:
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);
Normally all platforms comply to the following available LEDs:
LEDS_GREEN
LEDS_RED
LEDS_BLUE
LEDS_ALL
In the Z1
mote these LEDs are defined in platform/z1/platform-conf.h
, as well as other hardware definitions.
The RE-Mote
uses an RGB LED, basically 3-channel LEDs in a single device, allowing to show any color by the proper combination of Blue, Red and Green. In platforms/zoul/remote/board.h
header the following are defined:
LEDS_LIGHT_BLUE
LEDS_YELLOW
LEDS_PURPLE
LEDS_WHITE
Accordingly you need to add dev/button-sensor.h
to add support for the user button
.
The 02-led-and-button.c
example show how the configuration is done.
#include "contiki.h"
#include "dev/leds.h"
#include "dev/button-sensor.h"
#include <stdio.h>
/*-------------------------------------------------*/
PROCESS(led_button_process, "LEDs and button example process");
AUTOSTART_PROCESSES(&led_button_process);
/*-------------------------------------------------*/
PROCESS_THREAD(led_button_process, ev, data)
{
PROCESS_BEGIN();
SENSORS_ACTIVATE(button_sensor); (1)
while(1) { (2)
printf("Press the User Button\n");
PROCESS_WAIT_EVENT_UNTIL(ev == sensors_event && data == &button_sensor); (3)
/* When the user button is pressed, we toggle the LED on/off... */
leds_toggle(LEDS_GREEN); (4)
/*
* And we print its status: when zero the LED is off, else on.
* The number printed when the sensor is on is the LED ID, this value is
* used as a mask, to allow turning on and off multiple LED at the same
* time (for example using "LEDS_GREEN + LEDS_RED" or "LEDS_ALL"
*/
printf("The sensor is: %u\n", leds_get()); (5)
}
PROCESS_END();
}
-
The LED initialization is done at booting by the platform itself, so there is no need to initialize again. The
user button
is required to initialize -
Loop forever: the code inside the while-loop will run always and not reach
PROCESS_END()
macro. -
To avoid wasting processing cycles, the application is paused until we press the
user button
-
When the
user button
is pressed, alternate between turning the LED on and off -
Prints the current LED mask, that is, which LED channels are on and off
Tip
|
The RE-Mote platform has extra button functionalities, such as detect long-press sequences, enabling to further expand the events that can be triggered using the button. Check platform/zoul/dev/button-sensor.c for more details, and examples/zolertia/zoul/zoul-demo.c for an example.
|
Now let’s compile and upload the new project with:
make clean && make 02-led-and-button.upload
The make clean
command is used to erase previously compiled objects.
Caution
|
If you make changes to the source code, you must rebuild the files, otherwise your change might not be pulled in. It is always recommended to do a |
The RE-Mote
user and reset buttons are shown below:
Tip
|
Exercise: try to toggle the other LEDs and write down the LED mask values… Are these the same numbers for the Z1 and the RE-Mote ? In case of the RE-Mote , try combining colours to produce new ones (remember is RGB!).
|
Using timers will allow us to trigger events at a given time, speeding up the transition from one state to another and automating a given process or task, for example blinking an LED every 5 seconds, without the user having to press the button each time.
Contiki OS provides 4 kind of timers:
-
Simple timer: A simple ticker, the application should check manually if the timer has expired. More information at
core/sys/timer.h
. -
Callback timer: When a timer expires it can callback a given function. More information at
core/sys/ctimer.h
. -
Event timer: Same as above, but instead of calling a function, when the timer expires it posts an event signalling its expiration. More information at
core/sys/etimer.h
. -
Real time timer: The real-time module handles the scheduling and execution of real-time tasks, there’s only 1 timer available at the moment. More information at
core/sys/rtimer.h
The 03-timers.c
example implements the aforementioned timers and shows their functionality.
First let’s see how the timer’s libraries are included:
/* The event timer library */
#include "sys/etimer.h"
/* The seconds timer library */
#include "sys/stimer.h"
/* The regular timer library */
#include "sys/timer.h"
/* The callback timer library */
#include "sys/ctimer.h"
/* The "real-time" timer library */
#include "sys/rtimer.h"
Next each timer needs to have its corresponding timer structure defined, this will allow the application to store the configuration and operational values of each timer at request.
/* The following are the structures used when you need to include a timer, it
* serves to keep the timer information.
*/
static struct timer nt;
static struct stimer st;
static struct etimer et;
static struct ctimer ct;
static struct rtimer rt;
Then we need to implement callbacks
, generally these are functions called by the application whenever something occurs. As said before, both ctimer
and rtimer
are capable of invoking a function whenever an event happens (like a timer expiration). Below is the format of the callbacks
:
/*---------------------------------------------------------------------------*/
static void
rtimer_callback_example(struct rtimer *timer, void *ptr)
{
/* Something happens here */
}
/*---------------------------------------------------------------------------*/
static void
ctimer_callback_example(void *ptr)
{
/* Something happens here */
}
/*---------------------------------------------------------------------------*/
The actual 03-timers.c
has the missing snippet of code in each callback, we will return later to these.
Here’s what our thread looks like:
/*-------------------------------------------------*/
PROCESS(test_timer_process, "Test timer");
AUTOSTART_PROCESSES(&test_timer_process);
/*-------------------------------------------------*/
PROCESS_THREAD(test_timer_process, ev, data)
{
PROCESS_BEGIN();
static uint32_t ticks = 0;
timer_set(&nt, CLOCK_SECOND); (1)(2)
while(!timer_expired(&nt)) { (3)
ticks++;
}
printf("timer, ticks: \t%ld\n", ticks);
}
-
CLOCK_SECOND
is a value related to the number of the microcontroller’s ticks per second. As Contiki runs on different platforms with different hardware, the value ofCLOCK_SECOND
also differs. -
The most basic timer type is
timer
, it will keep running up toCLOCK_SECOND
-
We need to manually check if the
timer
has expired
Tip
|
Exercise: can you print the value of CLOCK_SECOND to count how many ticks you have in one second?
|
The remaining of our thread is as follows:
ticks = 0;
stimer_set(&st, 1); (1)
while(!stimer_expired(&st)) { (2)
ticks++;
}
printf("stimer, ticks: \t%ld\n", ticks);
-
Same as
timer
, but its time-base is seconds -
And we also need to check if the
stimer
has expired
Probably now in this point it is clear the disadvantages of using the previous timers: we need to manually check for expiration in a while-loop, which means a lot of wasted CPU cycles.
The next timer allow us to yield our application, meaning it will generate an event informing the application about its expiration, thus the system can perform other tasks in the meantime.
ticks = 0;
etimer_set(&et, CLOCK_SECOND * 2);
ticks++;
PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_TIMER);
printf("etimer, now: \t%ld\n", ticks);
When the etimer
is scheduled, we yield our application and wait for a PROCESS_EVENT_TIMER
event, this is a type of process event sent by the timer to notify applications.
Tip
|
If you run the example the first noticeable thing is the |
The next timer to test is the ctimer
. Let’s see how it is implemented in the example:
ticks++;
ctimer_set(&ct, CLOCK_SECOND * 2, ctimer_callback_example, &ticks);
/* And we keep the process halt while we wait for the callback timer to
* expire.
*/
while(1) {
PROCESS_YIELD();
}
As comented before the ctimer
will invoke a function whenever it expires, in this case the ctimer_callback_example
function after 2 seconds since triggered. Notice we are also passing to the ctimer_callback_example
the content of the variable ticks
, meaning we can pass as argument any type of variable to the callback function, as it defines this parameter as void *ptr
. Let’s check now the complete callback implementation:
static void
ctimer_callback_example(void *ptr)
{
uint32_t *ctimer_ticks = ptr; (1)
printf("ctimer, now: \t%ld\n", *ctimer_ticks);
/* The real timer allows execution of real-time tasks (with predictable
* execution times).
* The function RTIMER_NOW() is used to get the current system time in ticks
* and RTIMER_SECOND specifies the number of ticks per second.
*/
(*ctimer_ticks)++; (2)
rtimer_set(&rt, RTIMER_NOW() + RTIMER_SECOND, 0, (3)
rtimer_callback_example, ctimer_ticks);
}
-
Create a pointer to the variable passed as argument
-
Increment the variable
ticks
using our pointer -
Schedule a
rtimer
to expire after 1 second and pass as argument our pointer to theticks
variable
In our example when ctimer
expires it will set a real-timer event using the rtimer
type of timer. The RTIMER_NOW()
is a macro used to retrieve the current tick value (rtimer
is always ticking thus running in the background), we use this as base value then add the number of ticks in which we want our event to be triggered in the future. The RTIMER_SECOND
in the same fashion as CLOCK_SECOND
defines the number of ticks in a second.
Tip
|
Print the value of |
When rtimer
expires it invokes its corresponding callback function, let’s check its full implementation in our example:
static void
rtimer_callback_example(struct rtimer *timer, void *ptr)
{
uint32_t *rtimer_ticks = ptr;
printf("rtimer, now: \t%ld\n", *rtimer_ticks);
/* We can restart the ctimer and keep the counting going */
(*rtimer_ticks)++;
ctimer_restart(&ct);
}
As done in the ctimer
callback we also create a pointer to the original ticks
variable, print and increment its value. We restart the ctimer
using the initial expiration value configured before. From now on, ctimer
callback will configure a new rtimer
timer and viceversa, incrementing the ticks
value in a linear way.
We learned in previous section how to create a PROCESS
and the meaning of the PROCESS_BEGIN()
and PROCESS_END()
macros. Let’s take a closer look at how processes are implemented in Contiki.
A process thread contains the code of the process. The process thread is a single protothread that is invoked from the process scheduler. A protothread is a way to structure code in a way that allows the system to run other activities when the code is waiting for something to happen. The concept of protothreads was developed within the context of Contiki.
Protothread provides a way for C functions to work in a way that is similar to threads, without the memory overhead of threads. Reducing the memory overhead is important on the memory-constrained systems on which Contiki runs.
The protothreads starts and ends with two special macros, PROCESS_BEGIN
and PROCESS_END()
. Between these macros, a set of protothread functions can be used.
Libraries and applications in Contiki uses processes to run and to communicate with other modules, by creating its own processes or using the already available. The timers for example use the existing PROCESS_EVENT_TIMER
, and applications could also use the PROCESS_EVENT_EXITED
process to indicate an early exit. These are event identifiers
reserved by the Contiki Kernel.
As probably noticed by now, we normally refer to yielding and waiting for a process. Contiki has two execution contexts: cooperative and preemptive. Processes are cooperative and sequential, interrupts (button, sensors events) and the real-timer are preemptive.
As we have learned before, there are two ways so far to yield a protothread:
PROCESS_YIELD()
will wait for any type of process event to happen, it will not check which process has occured. Often a check like if(ev == PROCESS_EVENT_NAME){}
is used to catch one or more processes.
On the other hand the PROCESS_WAIT_EVENT_UNTIL(…)
allows to catch a process event direclty, in the case of the etimer
as previously shown, we can yield until:
PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_TIMER)
Or simply:
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et))
To catch just an etimer
event from timer et
.
Similar to PROCESS_YIELD()
, the PROCESS_PAUSE()
macro will pause the thread, but instead of waiting for an event to happen it will temporatily pause the process, by posting an event to itself and yielding. This allows the system to execute other processes and whenever done then resume our process.
The PROCESS_EXIT()
macro will allow the application to exit before reaching PROCESS_END()
, it will send a PROCESS_EVENT_EXITED
process notification whenever that happens.
The 04-processes.c
example shows how to create and terminate process, more importantly how to communicate between process.
Processes are useful as they allow to run different parts of our application, in its own semantic context, allowing a cleaner and efficient implementation. For example imagine a process running all the networking part, while separate processes take readings from individual sensors, each one running on its own context.
The example consist of 3 processes, one will spawn another process and receive events about a variable being incremented. When the variable value reaches a limit, the first process will terminate its child process. A third process listening in stand-by will receive a notification about the terminated process and restart it again.
PROCESS(process1, "Main process");
PROCESS(process2, "Auxiliary process");
PROCESS(process3, "Another auxiliary process");
/* But we are only going to automatically start the first two */
AUTOSTART_PROCESSES(&process1, &process2);
We define the three processes to run, but notice we only automatically start the first two. As opposite to what we have been working with before, after the Z1
or RE-Mote
platform finishes booting it will only call the processes declared within the AUTOSTART_PROCESSES(…)
macro, leaving process3
not started for now.
Let’s review how the first process is implemented:
PROCESS_THREAD(process1, ev, data)
{
PROCESS_BEGIN();
static uint8_t counter;
printf("Process 1 started\n");
process_start(&process3, "Process 1"); (1)
while(1) {
PROCESS_YIELD(); (2)
if(ev == event_from_process3) { (3)
counter = *((uint8_t *)data); (4)
printf("Process 3 has requested shutdown in %u seconds\n", counter);
etimer_set(&et1, CLOCK_SECOND);
}
if(ev == PROCESS_EVENT_TIMER) { (5)
if(counter <= 0) {
process_exit(&process3); (6)
} else {
printf("Process 3 will be terminated in: %u\n", counter);
counter--;
leds_toggle(LEDS_RED);
etimer_reset(&et1);
}
}
}
PROCESS_END();
}
-
When
process1
starts, it will launchprocess3
, when starting a process one can pass variables or data as a argument (in this example a string "Process 1") -
And then will wait for any event…
-
Note the
process1
has two additional arguments:ev
anddata
, when a process posts information to another process, it uses these arguments to pass information such as a pointer to a variable. -
If we receive an event from
process3
it will mean that process has requested an early termination, so we start anetimer
… -
And when our
etimer
expires… -
We will terminate
process3
As seen above process3
is terminated by process1
whenever "something" happens at process3
, but what about process2
?
PROCESS_THREAD(process2, ev, data)
{
PROCESS_BEGIN();
printf("Process 2 started\n");
while(1) {
PROCESS_YIELD();
if(ev == PROCESS_EVENT_EXITED) { (1)
printf("* Process 3 has been stopped by Process 1!\n");
etimer_set(&et2, CLOCK_SECOND * 5); (2)
}
if(ev == PROCESS_EVENT_TIMER) { (3)
printf("Process 2 is restarting Process 3\n");
process_start(&process3, "Process 2"); (4)
}
}
PROCESS_END();
}
-
When
process3
is terminated byprocess1
, we receive a notification -
And start a 5-second timer…
-
When the
etimer
expires… -
process2
will launch back againprocess3
, and will send its own name as string
We have now read the implementation of process1
and process2
and learned how to start and end processes, and receive data using the data
argument. Let’s check how process3
is implemented:
PROCESS_THREAD(process3, ev, data)
{
PROCESS_BEGIN();
static char *parent;
parent = (char * )data; (1)
static uint8_t counter; (2)
printf("Process 3 started by %s\n", parent);
event_from_process3 = process_alloc_event(); (3)
etimer_set(&et3, CLOCK_SECOND);
counter = 0;
while(1) {
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et3));
counter++;
leds_toggle(LEDS_GREEN);
if(counter == 10) {
process_post(&process1, event_from_process3, &counter); (4)
}
etimer_reset(&et3); (5)
}
PROCESS_END();
}
-
Create a pointer to read the processes name passed as string argument to us
-
Create a variable to increment
-
Allocate a process event to use and communicate to other processes
-
When the counter reaches 10, send an
event_from_process3
event toprocess1
with a pointer to thecounter
variable -
Reset the
etimer
using the current expiration value previously configured
The event_from_process3
process event was previously declared in our example as:
process_event_t event_from_process3;
A sensor is a transducer whose purpose is to sense or detect a characteristic of its environment, providing a corresponding output, generally as an electrical or optical signal, related to the quantity of the measured variable.
Sensors in Contiki are implemented as follow:
SENSORS_SENSOR (sensor, SENSOR_NAME, value, configure, status);
This means that a method to configure
the sensor, poll the sensor status
and request a value
have to be implemented. The sensor
structure contains pointers to these functions. The arguments for each function are shown below.
struct sensors_sensor {
char * type;
int (* value) (int type);
int (* configure) (int type, int value);
int (* status) (int type);
};
Tip
|
If you need to further expand the available functions (change the |
To better understand please refer to the platform/zoul/dev/adc-sensors.c
, for an example of how analogue external sensors can be implemented (to be further discussed in the next sections).
The following functions and macros can be used to work with the sensor:
SENSORS_ACTIVATE(sensor) (1)
SENSORS_DEACTIVATE(sensor) (2)
sensor.value(type); (3)
-
Enable the sensor, typically configures and turns the sensor on
-
Disables the sensor, it is useful to save power
-
Request a value to the sensor. As one sensor chip might have different types of readings (temperature, humidity, etc.), this is used to specify which measure to take.
Important
|
Although this is the default way to implement sensors in Contiki, there might be alternatives, depending on the sensor and developer. Always check the specific sensor driver implementation for details. Normally sensors are provided in the same location as the example, in the |
The SENSORS
macro provides external linkage to the sensors defined for the platform and application, allowing to iterate between the available sensors. The following are the sensors defined as default for the RE-Mote platform:
SENSORS (&button_sensor, &vdd3_sensor, &cc2538_temp_sensor, &adc_sensors);
Note
|
The button is considered a sensor in Contiki, and it shares the same sensor implementation |
This macro extends to:
const struct sensors_sensor *sensors[] = {
&button_sensor,
&vdd3_sensor,
&cc2538_temp_sensor,
&adc_sensors,
((void *)0)
};
For the Z1
mote the SENSORS
macro is defined within the platforms/z1/contiki-z1-main.c
:
SENSORS(&button_sensor);
The number of sensors can be found with the SENSORS_NUM
macro:
#define SENSORS_NUM (sizeof(sensors) / sizeof(struct sensors_sensor *))
Sensors can be of two types: analogue or digital. In the following sections examples will be shown for both types, detailing how to connect and use both the platform’s internal sensors as well as the external ones.
Tip
|
Enable the You can use printf to visualize on the console what is happening in your application. It is really useful to debug your code, as you can print values of variables, when certain block of code is being executed, etc. |
Analogue sensors typically require being connected to an ADC (analogue to digital converter) to translate the analogue (continuous) reading to an equivalent digital value in millivolts. The quality and resolution of the measure depends on both the ADC resolution (up to 12 bits in the Z1 and RE-Mote) and the sampling frequency.
As a rule of thumb, the sampling frequency must be at least twice that of the phenomenon you are measuring. As an example, if you want to sample human speech (which may contain frequencies up to 8 kHz) you need to sample at twice that frequency (16 kHz).
The Z1
mote and the RE-Mote
have a built-in voltage sensor provided as an ADC input channel, as well as a generic implementation to read external analogue sensors.
Tip
|
Analogue sensors can be connected to both the Z1 and the RE-Mote over analogue_ ports, which are basically 3-pin connectors (Ground, VCC and signal) with 2.54 mm spacing. This port is compatible with Phidget sensors. Nowadays there are also sensors from other providers, such as seeedstudio that have the same pin-out but with a different pin spacing (2 mm). This is not a problem, as there are cables to adapt the pin spacing. |
The ADC results in the RE-Mote
returns the reading in voltage directly, there’s no need to further conversion.
For the Z1 mote the ADC output must be converted by means of the following formula: We multiply the measured value by the voltage reference and divide the product by the ADC’s maximum output. As the resolution of the ADC is 12 bits (212 = 4096), we get:
Voltage, mV = (units * Vref) / 4096;
The voltage reference limits the range of our measure, meaning if we use a Vref
of 2.5 V, the sensor will saturate when reading a higher voltage. The reference can be chosen while configuring the ADC, for both the Z1
and RE-Mote
normally 3.3 V is used.
Both the Z1
and the RE-Mote
allow up to 6 analogue sensors to be connected, but only two analogue connectors can be soldered at the same time.
For the RE-Mote
it is possible to have one analogue connector for a 3.3 V analogue sensor (ADC1) and another for 5 V sensors (ADC3).
Note the location of the analogue connectors in the image below.
Tip
|
The |
For the Z1
mote is possible to have the following combinations (see Figure below):
-
Two phidget connectors for 3.3 V sensors.
-
Two phidget connectors for 5 V sensors.
-
Two phidget connectors, one for 3.3 V and one for 5 V sensors.
In the snippet below the ADC channels ADC0 (5V1), ADC3 (5V2), ADC1 (3V1) and ADC7 (3V2) are mapped by default to be used as input for external analogue sensors.
/* MemReg6 == P6.0/A0 == 5V 1 */
ADC12MCTL0 = (INCH_0 + SREF_0);
/* MemReg7 == P6.3/A3 == 5V 2 */
ADC12MCTL1 = (INCH_3 + SREF_0);
/* MemReg8 == P6.1/A1 == 3V 1 */
ADC12MCTL2 = (INCH_1 + SREF_0);
/* MemReg9 == P6.7/A7 == 3V_2 */
ADC12MCTL3 = (INCH_7 + SREF_0);
To read from a sensor connected to the ADC7 channel of the Z1
mote, in my code I would need to make the following steps:
#include "dev/z1-phidgets.h" (1)
SENSORS_ACTIVATE(phidgets); (2)
printf("Phidget 5V 1:%d\n", phidgets.value(PHIDGET3V_2)); (3)
-
Include the driver header
-
Enable the ADC sensors, as default all 4 ADC channels are enabled
-
Request a reading
The Z1
is powered at 3.3 V, but when connected over the USB (standard voltage 5 V) it allows to connect 5 V sensors to the phidget ports, namely 5V1 (ADC3) and 5V2 (ADC0) as there is a voltage divider in the input to adapt the reading from 5 V to 3.3V.
Details about the Z1
implementation of the analogue sensors driver are available at platform/z1/dev/z1-phidgets.c
.
There is also a driver for the MSP430 internal voltage sensor at platform/z1/dev/battery-sensor.c
, with an example at examples/zolertia/z1/test-battery.c
.
#include "dev/battery-sensor.h"
/*---------------------------------------------------------------------------*/
PROCESS(test_battery_process, "Battery Sensor Test");
AUTOSTART_PROCESSES(&test_battery_process);
/*---------------------------------------------------------------------------*/
PROCESS_THREAD(test_battery_process, ev, data)
{
static uint32_t battery;
PROCESS_BEGIN();
SENSORS_ACTIVATE(battery_sensor);
while(1) {
battery = battery_sensor.value(0);
battery *= 5000;
battery /= 4096;
printf("Battery: %u.%02uV\n", (uint16_t)battery / 1000, (uint16_t)battery % 1000);
}
PROCESS_END();
}
When the USB is connected or the battery is full, the result is similar to:
Battery: 3.21V
Battery: 3.18V
Battery: 3.20V
Battery: 3.18V
Battery: 3.20V
Battery: 3.20V
Battery: 3.18V
Battery: 3.20V
Battery: 3.18V
It is possible also to connect an external analogue sensor, like the Phidget 1142 precision Light Sensor or the Grove/Seeedstudio Light Sensor (P).
The example called test-phidgets.c
in examples/zolertia/z1
will read values from an analog sensor and print them to the terminal.
Connect the light sensor to the 3V2 Phidget connector and compile the example:
make clean && make test-phidgets.upload && make z1-reset && make login
Tip
|
You can pipeline commands to be executed one after another, in this case the |
This is the result:
Starting 'Test Button & Phidgets'
Please press the User Button
Phidget 5V 1:123
Phidget 5V 2:301
Phidget 3V 1:1710
Phidget 3V 2:2202
The light sensor is connected to the 3V2 connector, so the raw value is 2202. Try to illuminate the sensor with a flashlight (from your mobile phone, for example) and then cover it with your hand so that no light can reach it.
From the Phidget website we have the following information about the sensor:
Parameter | Value |
---|---|
Sensor type |
Light |
Light level min |
1 lux |
Supply Voltage Min |
2.4 V |
Supply Voltage Max |
5.5 V |
Max current consumption |
5mA |
Light level max (3.3 V) |
660 lux |
Light level max (5 V) |
1000 lux |
As you can see, the light sensor can be connected to either the 5 V or the 3.3 V analogue connector. The max measurable value depends on where you connect it.
The formula to translate SensorValue into luminosity is:
Luminosity(lux)=SensorValue
The RE-Mote platform also allows to connect 5 V sensors to the ADC3 port, using the same voltage divider as the Z1 mote. As seen in the next snippet, the ADC3 port is mapped to the PA2 pin.
/*
* This driver supports analogue sensors connected to ADC1, ADC2 and AND3 inputs
* This is controlled by the type argument of the value() function. Possible
* choices are:
* - REMOTE_SENSORS_ADC1 (channel 5)
* - REMOTE_SENSORS_ADC2 (channel 4)
* - REMOTE_SENSORS_ADC3 (channel 2)
*/
Then to read data from an attached sensor (as we did in the previous example):
#include "dev/zoul-sensors.h" (1)
adc_sensors.configure(SENSORS_HW_INIT, REMOTE_SENSORS_ADC_ALL); (2)
printf("ADC1 = %d raw\n", adc_sensors.value(REMOTE_SENSORS_ADC1)); (3)
printf("ADC3 = %d raw\n", adc_sensors.value(REMOTE_SENSORS_ADC3));
-
Include the ADC driver header
-
Enable and configure the ADC channels (can selectively choose which to enable)
-
Request a reading
Note the ADC3 voltage reading has to be multiplied by 3/2 to get the actual 0-5V range value, as there is a voltage divider with a 500K/200K relationship.
Now let’s connect a Grove Light Sensor
to the RE-Mote
:
Compile and program the RE-Mote example at example/zolertia/zoul/zoul-demo.c
, the output should be similar to:
connecting to /dev/ttyUSB0 (115200) [OK]
-----------------------------------------
Counter = 0x00000000
VDD = 3299 mV
Temperature = 33333 mC
ADC1 = 112 raw
ADC3 = 1516 raw
The above result was taken with the light sensor covered, so aproximately with no light the voltage value is 0.112V. This light sensor has a light-dependent resistor (LDR), the output in voltage units correlates to the equivalent LDR. The Grove wiki page shows how the resistance/luxes graphs to obtain proper lux values, but probably a better option is to calibrate by using a known lux source, or just knowing the smaller the value the darkest the ambient is.
Tip
|
Exercise: make the sensor take readings as fast as possible. Print to the screen the ADC raw values and the millivolts (as this sensor is linear, the voltage corresponds to the luxes). What are the max and min values you can get? What is the average light value of the room? Create an application that turns the red LED on when it is dark and the green LED on when the room is bright. In between, all the LEDs should be off. Add a timer and measure the light every 10 seconds. |
Note the RE-Mote
has built-in voltage and core temperature sensors. The voltage sensor provides the voltage level of the CC2538
system on chip, whereas the core temperature gives the operation temperature. As shown in the previous code snippet, the zoul-demo.c
example also shows how to use both sensors:
printf("VDD = %d mV\n",
vdd3_sensor.value(CC2538_SENSORS_VALUE_TYPE_CONVERTED));
printf("Temperature = %d mC\n",
cc2538_temp_sensor.value(CC2538_SENSORS_VALUE_TYPE_CONVERTED));
Both sensors are initialized at booting by the RE-Mote
.
Digital sensors are normally interfaced over a digital communication protocol such as I2C, SPI, 1-Wire, Serial or depending on the manufacturer, a proprietary protocol normally on a ticking clock.
Digital sensors allow a more extended set of commands (turn on, turn off, configure interrupts). With a digital light sensor for example, you could set a threshold value and let the sensor send an interrupt when reached, without the need for continuous polling.
Remember to check the specific sensor information and data sheet for more information.
The Z1 mote has two built in digital sensors: temperature and 3-axis accelerometer. The ADXL345
accelerometer has an example named test-adxl345.c
in examples/zolertia/z1
showing how the sensor works.
The ADXL345
is an I2C ultra-low power sensor able to read up to 16g, well suited for mobile device applications. It measures the static acceleration of gravity in tilt-sensing applications, as well as dynamic acceleration resulting from motion or shock. Its high resolution (3.9mg/LSB) enables measurement of inclination changes smaller than 1.0°.
[37] DoubleTap detected! (0xE3) -- DoubleTap Tap
x: -1 y: 12 z: 223
[38] Tap detected! (0xC3) -- Tap
x: -2 y: 8 z: 220
x: 2 y: 4 z: 221
x: 3 y: 5 z: 221
x: 4 y: 5 z: 222
The accelerometer can read data in the x, y and z axis and has three types of interrupts: a single tap, a double tap and a free-fall (be careful to not damage the mote!).
Having an accelerometer on-board is quite useful: you can detect if anyone is tampering with the device, or detect if someone falls, monitor vibration and generate an alarm if exceeds a given threshold (causing a component to break), etc.
The code has two threads, one for the interruptions and the other for the LEDs. When Contiki starts, it triggers both processes.
The led_process
thread triggers a timer that waits before turning off the LEDs. This is mostly done to filter the rapid signal coming from the accelerometer. The other process is the acceleration. It assigns the callback for the led_off
event.
Interrupts can happen at any given time, are non periodic and totally asynchronous.
Interrupts can be triggered by external sources (sensors, GPIOs, Watchdog Timer, etc) and should be cleared as soon as possible. When an interrupts happens, the interrupt handler (which is a process that checks the interrupt registers to find out which is the interrupt source) manages it and forwards it to the subscribed callback.
In this example, the accelerometer is initialized and then the interrupts are mapped to a specific callback functions. Interrupt source 1 is mapped to the free fall callback handler and the tap interrupts are mapped to the interrupt source 2.
/*
* Start and setup the accelerometer with default
* values, _i.e_ no interrupts enabled.
*/
SENSORS_ACTIVATE(adxl345);
/* Register the callback functions */
ACCM_REGISTER_INT1_CB(accm_ff_cb);
ACCM_REGISTER_INT2_CB(accm_tap_cb);
We then need to enable the interrupts like this:
accm_set_irq(ADXL345_INT_FREEFALL,
ADXL345_INT_TAP +
ADXL345_INT_DOUBLETAP);
In the while loop we read the values from each axis every second. If there are no interrupts, this will be the only thing shown in the terminal.
Tip
|
Exercise: put the mote in different positions and check the values of the accelerometer. Try to understand which is x, y and z. Measure the maximum acceleration by shaking the mote. Turn on and off the LED according to the acceleration on one axis. |
The TMP102
digital temperature sensor on-board the Z1
platform has a +/-5ºC accuracy and can measure temperatures from -25ºC to 85ºC. There is an example ready to use at examples/zolertia/z1/test-tmp102.c
showing how the sensor works.
PROCESS_THREAD(temp_process, ev, data)
{
PROCESS_BEGIN();
int16_t temp;
SENSORS_ACTIVATE(tmp102);
while(1) {
etimer_set(&et, TMP102_READ_INTERVAL);
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et));
temp = tmp102.value(TMP102_READ);
printf("Temp = %d.%02d\n", temp / 100, temp % 100);
}
PROCESS_END();
}
The output is similar to below:
Temp = 27.75
Temp = 28.12
Temp = 28.31
Temp = 28.18
Temp = 28.12
Temp = 28.06
Warning
|
The TMP102 is placed close to the CP2102 Serial to USB converter of the |
External digital sensors can be connected to the Z1
and RE-Mote
platforms.
As shown in Figure 8, the RE-Mote
and the Z1
allows I2C and SPI sensors to be connected to the Ziglet
port: a 5-pin connector with 2.54mm pitch spacing as follows:
For the next example we will use the SHT25, an I2C digital temperature and humidity sensor from Sensirion.
Parameter | Value |
---|---|
Sensor type |
Temperature and Humidity |
Supply Voltage [V] |
2.1 - 3.6 |
Energy Consumption |
3.2 uW (at 8 bit, 1 measurement / s) |
Data range |
0-100 % RH (humidity), -40-125ºC (temperature) |
Max resolution |
14 bits (temperature), 12 bits (humidity) |
Max current consumption |
300 uA |
As usual you can check the driver implementation at the platform/z1/dev
and platform/zoul/dev
directories. Check out these locations for other sensors and examples to guide you to implement your own.
When adding an external sensor to your application, you will need to tell the compiler to include the sensor libraries. In the previous examples this was not required as the platforms include its on-board sensor automatically. Adding the external sensor libraries to the application is done in the Makefile
. This is how the RE-Mote
Makefile looks like:
DEFINES+=PROJECT_CONF_H=\"project-conf.h\" (1)
CONTIKI_PROJECT = zoul-demo test-tsl2563 test-sht25 test-power-mgmt (2)
CONTIKI_PROJECT += test-bmp085-bmp180 test-motion test-rotation-sensor
CONTIKI_PROJECT += test-grove-light-sensor test-grove-loudness-sensor
CONTIKI_PROJECT += test-weather-meter test-grove-gyro test-lcd test-iaq
CONTIKI_PROJECT += test-pm10-sensor test-vac-sensor test-aac-sensor
CONTIKI_PROJECT += test-zonik
CONTIKI_TARGET_SOURCEFILES += tsl2563.c sht25.c bmpx8x.c motion-sensor.c (3)
CONTIKI_TARGET_SOURCEFILES += adc-sensors.c weather-meter.c grove-gyro.c
CONTIKI_TARGET_SOURCEFILES += rgb-bl-lcd.c pm10-sensor.c iaq.c zonik.c
all: $(CONTIKI_PROJECT)
CONTIKI = ../../..
include $(CONTIKI)/Makefile.include
-
The location of the
project-conf.h
file for specific application configuration. -
The test examples to compile automatically if no application is defined. If you run
make
it will compile all of them at once! -
The name of the libraries to include. As default it will search the
platform/dev
folder
The sht25.c
is the library of the SHT25 sensor.
The SHT25 sensor example is available in both examples/zolertia/zoul/test-sht25.c
and examples/zolertia/z1/test-sht25.c
.
/*---------------------------------------------------------------------------*/
#include "dev/sht25.h"
/*---------------------------------------------------------------------------*/
PROCESS(remote_sht25_process, "SHT25 test");
AUTOSTART_PROCESSES(&remote_sht25_process);
/*---------------------------------------------------------------------------*/
static struct etimer et;
/*---------------------------------------------------------------------------*/
PROCESS_THREAD(remote_sht25_process, ev, data)
{
int16_t temperature, humidity;
PROCESS_BEGIN();
SENSORS_ACTIVATE(sht25); (1)
/* Check if the sensor voltage operation is over 2.25V */
if(sht25.value(SHT25_VOLTAGE_ALARM)) { (2)
printf("Voltage is lower than recommended for the sensor operation\n");
PROCESS_EXIT();
}
/* Configure the sensor for maximum resolution (14-bit temperature, 12-bit
* relative humidity), this will require up to 85ms for the temperature
* integration, and 29ms for the relative humidity (this is the default
* setting at power on). To achieve a faster integration time at the cost
* of a lower resolution, change the value below accordingly, see sht25.h.
*/
sht25.configure(SHT25_RESOLUTION, SHT2X_RES_14T_12RH); (3)
/* Let it spin and read sensor data */
while(1) {
etimer_set(&et, CLOCK_SECOND);
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et));
temperature = sht25.value(SHT25_VAL_TEMP); (4)
printf("Temperature %02d.%02d ºC, ", temperature / 100, temperature % 100);
humidity = sht25.value(SHT25_VAL_HUM); (5)
printf("Humidity %02d.%02d RH\n", humidity / 100, humidity % 100);
}
PROCESS_END();
}
-
Enable the sensor
-
Check if the sensor powering voltage is below 2.25V. The sensor requires at least 2.25V to work properly, else it might yield unreliable measures
-
Configure the sensor resolution
-
Retrieve a temperature reading
-
Retrieve a humidity reading
An output example is given below:
Starting 'SHT25 test'
Temperature 23.71 ºC
Humidity 42.95 % RH
Temperature 23.71 ºC
Humidity 42.95 % RH
Temperature 23.71 ºC
Humidity 42.95 % RH
Temperature 23.71 ºC
Humidity 42.98 %RH
Tip
|
Exercise: convert the temperature to Fahrenheit. Try to get the temperature and humidity as high as possible (without damaging the mote!). Try to print only “possible” values (if you disconnect the sensor, you should not print anything, or print an error message!). |
The next section will continue with the walkthrough on how to work with the on-board sensors, shown in the analogue and digital sensors section.
Tip
|
Normally when working with sensors it is often recommeded to operate as follows: SENSOR_ACTIVATE(...)
/* configure, retrieve readings, etc */
SENSOR_DEACTIVATE(); This ensures the sensor is configured as expected. It is quite common to have more than one digital sensor sharing the same communication bus (I2C/SPI), or having a driver configuring the ADC ports with a general configuration thus overriding the expected one. |
The 05-onboard-sensors.c
example shows a mixture of the on-board sensors of each platforms shown in the section before. This example is compatible with both the Z1
and the RE-Mote
platform, so it can be compiler for both.
The following snippet shows how is this done:
#if CONTIKI_TARGET_ZOUL
batt = vdd3_sensor.value(CC2538_SENSORS_VALUE_TYPE_CONVERTED);
printf("VDD = %u mV\n", (uint16_t)batt);
temp = cc2538_temp_sensor.value(CC2538_SENSORS_VALUE_TYPE_CONVERTED);
printf("Core temperature = %d.%u C\n", temp / 1000, temp % 1000);
#else /* Assumes Z1 mote */
x_axis = adxl345.value(X_AXIS);
y_axis = adxl345.value(Y_AXIS);
z_axis = adxl345.value(Z_AXIS);
temp = tmp102.value(TMP102_READ);
batt = battery_sensor.value(1);
/* Print the readings */
printf("Acceleration: X %d Y %d Z %d\n", x_axis, y_axis, z_axis);
printf("Temperature: %d.%u C\n", temp / 100, temp % 100);
/* Convert the ADC readings to mV */
batt *= 5000;
batt /= 4095;
printf("Battery: %u\n\n", (uint16_t)batt);
#endif
The key is the CONTIKI_TARGET_ZOUL
preprocesor value.
Whenever we compile using TARGET=zoul
the CONTIKI_TARGET_ZOUL
is defined, thus allowing us to differentiate at compilation time which block of code will be included in our application.
For the Z1
mote the output is similar to:
Acceleration: X -13 Y -73 Z -38
Temperature: 28.50 C
Battery: 3018
Acceleration: X -14 Y -75 Z -39
Temperature: 28.43 C
Battery: 3020
And for the RE-Mote
:
VDD = 3300 mV
Core temperature = 33.571 C
VDD = 3297 mV
Core temperature = 33.809 C
The 06-adc.c
example also summarizes the analogue sensors into a single test application compatible for both platforms: The Z1
and the RE-Mote
:
#if CONTIKI_TARGET_ZOUL
printf("ADC1 = %u mV\n", adc_zoul.value(ZOUL_SENSORS_ADC1));
printf("ADC3 = %u mV\n", adc_zoul.value(ZOUL_SENSORS_ADC3));
#else
printf("Phidget 5V 1:%d\n", phidgets.value(PHIDGET5V_1));
printf("Phidget 5V 2:%d\n", phidgets.value(PHIDGET5V_2));
printf("Phidget 3V 1:%d\n", phidgets.value(PHIDGET3V_1));
printf("Phidget 3V 2:%d\n\n", phidgets.value(PHIDGET3V_2));
#endif
The General input/output pins are generic pins used either as input or output (0-3.3V), useful in case you need to actuate on something, or read the digital voltage level as high/low (3.3V or 0V).
Some examples are:
-
Turn a lamp on and off by controlling a relay
-
Check if a window is open or close using a reed switch
The way the LEDs
works is setting a given pin as output, and setting its value high or low (depending on the actual pin logic and pull-up or pull-down resistors it might have).
The user button
is actually a GPIO configured as input, and then detecting whenever the value transitions from one level to another (high to low or viceversa), thus generating an interrupt event.
The Z1
and the RE-Mote
have a very different way to configure and work with GPIO, as currently Contiki lacks an unified GPIO API. The 07-gpio.c
example covers both cases.
The RE-Mote
and the CC2538
library implements macros to directly configure and set the pins/ports registers. The CC2538
has four ports (namely A, B, C, D) each one with a numeric value from zero to three:
/** \name Numeric representation of the four GPIO ports
* @{
*/
#define GPIO_A_NUM 0 /**< GPIO_A: 0 */
#define GPIO_B_NUM 1 /**< GPIO_B: 1 */
#define GPIO_C_NUM 2 /**< GPIO_C: 2 */
#define GPIO_D_NUM 3 /**< GPIO_D: 3 */
/** @} */
Subsequently each port has up to eight pins, numbered from zero to seven. The pins then are normally referred to PA0
and alike, indicating is the pin zero at the port A. The image below shows the pin-out of the RE-Mote
:
It is possible to configure and use one or more pins at the same time, this is done using masks. As we have 8 pins per port, we treat the pin relative position in a mask fashion. For example if we want to enable pins 2 and 4 of the port A at the same time:
bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
So the value we would need to use is 0x14 in hexadecimal or 20 in decimal base.
The conversion of a single pin number to the required pin mask can be done using the following macro:
GPIO_PIN_MASK(pin number)
Also the actual port register address can be obtained from the port number by using the following macro:
GPIO_PORT_TO_BASE(port number)
In the 07-gpio.c
example this is how a pin is configured as an output pin (PA5
).
The GPIO_SOFTWARE_CONTROL(…)
macro configures the pin to be controlled by the user, instead of being driven by the hardware as other function (I2C, SPI, etc).
/* The masks below converts the Port number and Pin number to base and mask values */
#define EXAMPLE_PORT_BASE GPIO_PORT_TO_BASE(GPIO_A_NUM)
#define EXAMPLE_PIN_MASK GPIO_PIN_MASK(5)
/* We tell the system the application will drive the pin */
GPIO_SOFTWARE_CONTROL(EXAMPLE_PORT_BASE, EXAMPLE_PIN_MASK);
/* And set as output, starting as high */
GPIO_SET_OUTPUT(EXAMPLE_PORT_BASE, EXAMPLE_PIN_MASK);
GPIO_SET_PIN(EXAMPLE_PORT_BASE, EXAMPLE_PIN_MASK);
Then in a loop we read the pin status (if it is high or low), and toggle its state. Notice the GPIO_READ_PIN(…)
macro also uses a pin mask as argument, meaning you can read in a single instruction the status of all the pins of the port. The return of this macro will give a mask of pins, in which all pins set as high will have a value of one, and zero otherwise.
In this example if pin PA5
is active, the GPIO_READ_PIN(…)
will return a value of 0x20 in hexadecimal, or 20 in decimal base, as this is the pin 5 mask value (1 << 5).
while(1) {
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et));
if(GPIO_READ_PIN(EXAMPLE_PORT_BASE, EXAMPLE_PIN_MASK)) {
GPIO_CLR_PIN(EXAMPLE_PORT_BASE, EXAMPLE_PIN_MASK);
} else {
GPIO_SET_PIN(EXAMPLE_PORT_BASE, EXAMPLE_PIN_MASK);
}
leds_toggle(LEDS_GREEN);
etimer_reset(&et);
}
The Z1
mote on the other hand uses directly the register values to configure and actuate on the pins:
#define EXAMPLE_PIN_MASK (1<<2)
/* The MSP430 MCU names the pins as a tupple of PORT + PIN, thus P6.1 refers
* to the Pin number 1 of the Port 6.
* We need first to select the operation mode of the Pin, as a Pin can have
* a special function (UART, ADC, I2C) also. The GPIO operation is selected
* by writting the pin's bit as zero to the PxSEL register. The below snippet
* changes 00010000 to 11101111, so when writting to the register we only
* clear our pin, leaving the others untouched (as it is an AND operation)
*/
P4SEL &= ~EXAMPLE_PIN_MASK;
/* Next we set the direction of the pin. Output means the pin can be high
* (3.3V) or low (0V), while being as Input will allow us to read the digital
* value the pin has. To enable the pin as Output, write an 1.
*/
P4DIR |= EXAMPLE_PIN_MASK;
We need to configure first the pin function (we select the pin as GPIO by setting the pin mas value on the PxSEL
register to zero). The PxDIR
configures the direction of the pin, if writting a zero to the pin mask value it will be configured as input, otherwise if set to one it will be configured as output.
/* This toggles the pin, if low then sets the pin high, and viceversa.
* Alternatively to set the pin high, use P4OUT |= EXAMPLE_PIN_MASK, and to
* drive low use P4OUT &= ~EXAMPLE_PIN_MASK
*/
P4OUT ^= EXAMPLE_PIN_MASK;
And then to set the pin high or low we use the PxOUT
register (it assumes the pin is configured as output, its value is ignored otherwise). Writting a one to the pin mask value on the register will set the pin as high, and low otherwise is writting a zero.