Low Power Wireless: 6LoWPAN, IEEE802.15.4 and the Raspberry Pi

The Internet of Things (IoT) is one of the new kids on the block. It promises connection of sensors and actuators to the internet, for data to flow both ways, and once on the internet, to become part of new and exciting business systems, reaching up into the realms of big data and artificial intelligence.

IoT applications will rely on a large and complex system. One of the components in this will be the connections between sensors and actuators and the internet. This will most likely be wireless, and it will have to be low power. If you have a thousand sensors, they will most likely be running off batteries, and you will want those batteries to last years, not days.

Low power wireless is heading in two directions right now: personal-area networks (LoWPAN) spanning up to 20–30 meters and wide-area networking (LPWAN) of up to 20 or more kilometers. The technologies at the physical layer are completely different and lead to different Linux solutions. This article deals only with LoWPAN.

The physical layer for LoWPAN is specified by IEEE802.15.4. This defines communication using various wireless bands, such as 2.4GHz, with a range of about 10 meters and data transfer rates of 250kb/s—good enough for most sensors, but not good enough to stream MP3s!

On top of IEEE802.15.4 is a variety of protocols: Zigbee, Z-Wave, Thread and so on. Of these, only the IETF 6LoWPAN is an open standard, and this is where the Linux development community has settled. This article covers only 6LoWPAN. I also ignore other wireless systems, such as Bluetooth LE.

6LoWPAN and Linux

6LoWPAN is IPv6 over IEEE802.15.4 wireless. That isn't easy. IPv6 is designed for the current internet, while IEEE802.15.4 is designed for a different environment. You don't need to worry about how this mismatch has been overcome, but it does mean you need to be aware that two different levels are dealt with here: getting two wireless devices to talk to each other and getting a networking layer talking over these devices.

The device layer is where physical hardware choices come into play. Linux supports several devices, such as the AT86RF230 series, the MRF24J40 and several others. The kernel needs to have those device drivers compiled in or available as dynamically loadable modules.

The networking layer requires 6LoWPAN support. Again, the kernel needs to have this compiled in or available as modules. These modules are the ieee802154_6LoWPAN, ieee802154 and mac802154 modules.

6LoWPAN Devices and the Raspberry Pi

The Raspberry Pi is a wonderful toy or a full-blown Linux computer, depending on your viewpoint. With its GPIO pins, it can act as a connection into the realm of sensors and actuators, while with Ethernet (and on the RPi3, Wi-Fi), it can be a part of LANs and WANs. For the IoT, it (and the Arduino) form an excellent bridge between the physical and ICT worlds. But, there are now IEEE802.15.4 modules available, and they can be used to turn an RPi into a "full-function 6LoWPAN device".

I used the RPi with the OpenLabs "Raspberry Pi 802.15.4 radio". This is an Atmel AT86RF233 radio on a small board with a header that allows it to be plugged straight onto pins 15–26 of the RPi. It can be plugged in facing out or facing in—facing in the right way to do it.

I started off using the standard Raspbian distro (dated May 27, 2016). This can be set up to recognize the radio, but—oh dear!—the 4.4 Linux kernel it uses has 6LoWPAN modules, but they don't work properly in that kernel. The IPv6 packets get corrupted even for pinging itself, so this Raspbian distro won't support 6LoWPAN.

The hunt is on then for a setup that allows the RPi to support 6LoWPAN with an AT86RF233 radio. This is painful: there are many helpful sites that are outdated or with instructions that I just couldn't get to work. I finally was pointed by Sebastian Meiling to his page "Create a generic Raspbian image with 6LoWPAN support". In summary, what is needed is an upstream Linux kernel, 4.7 or 4.8, recent firmware and suitable configuration of the /boot/config.txt file. At the time of this writing, these instructions work only for the RPi 1 and 2. The RPi 3 isn't working yet, but it may be by the time this article is published.

Installing a 6LoWPAN Kernel

For this article I'm using the OpenLabs module on the RPi 2B. For other modules and RPis, see Sebastian's page. I'm also going to assume a reasonable amount of Linux savvy in installing software and building from source.

Start by installing the latest Raspbian image. If that runs a 4.7 (or later) kernel, you may be okay already; otherwise, you need to build and install an upstream 4.7 kernel. You probably will need extra tools for this, such as rpi-update, git, libncurses5-dev, bc and maybe development tools that you can install using apt-get.

Before you do anything else, make sure your system is up to date by running:


rpi-update

This will install the latest firmware bootloader.

Download a 4.7 kernel into the linux-rpi2 directory with:


git clone --depth 1 https://github.com/raspberrypi/linux.git \
          --branch rpi-4.7.y --single-branch linux-rpi2

Building a kernel means compiling a lot of files and is very slow on the RPi. Most people recommend cross-compiling, but that's more complex, and I like things simple. So, I prefer to build on the RPi itself. It takes only about 5 hours, so start it up, and either go to bed or go out, listen to some jazz and stay out late.

In the linux-rpi2 directory, set up a configuration file for the RPi 2B with:


make bcm2709_defconfig

Then run menuconfig to do two things:

1) Install the device driver as a module from the menu entry:


Device Drivers
--> Network device support
  --> IEEE 802.15.4 drivers

2) Install 6LoWPAN support as a module from the menu entry:


Networking support
--> Networking options
  --> IEEE Std 802.15.4 Low-Rate Wireless Personal Area
      Networks support

Build the kernel and associated files with:


make zImage modules dtbs -j4

Five hours later, install the modules and dtbs files:


sudo make modules_install dtbs_install

The safest way to install the kernel is to copy it to an appropriate location. When I run make kernelversion in the source tree, it tells me I have built 4.7.2. So I use that number in copying the kernel:


sudo cp arch/arm/boot/zImage /boot/kernel.4.7.2.img

That way I don't destroy any existing images, so I have a safe fallback to the previous system.

Finally, you need to tell the RPi to boot into the new kernel. As root, edit /boot/config.txt and add these lines at the end:


kernel=kernel.4.7.2.img
device_tree=bcm2709-rpi-2-b.dtb
dtoverlay=at86rf233

What does that do? First, it tells the RPi to use the new boot image kernel.4.7.2.img. Second—and this is currently ARM-specific—it tells the RPi to pick up hardware default values using the device tree system from bcm2709-rpi-2-b.dtb. And third—and this is RPi-specific—it says to add in the at86rf233 device in an additional file to the device tree file.

Finally...reboot. If all went well, you should have the new kernel running. Check this with:


uname -a

It should show something like this:


Linux raspberrypi 4.7.2-v7+ #1 SMP Fri Aug 26 15:45:29 UTC 2016
 ↪armv7l GNU/Linux

If it didn't boot or showed the wrong kernel, take your SD card back to somewhere else so you can comment out the lines you added to /boot/config.txt. Back on the RPi, reboot back into the default kernel, and try to figure out which step went wrong. I skipped some steps from Sebastian's guide because I didn't need them, but if your system isn't working, pay very close attention to his guide. He seems to be pretty diligent about updating it.

Setting Up 6LoWPAN

Are you there yet? Sorry, no. You've built and installed an upstream kernel with 6LoWPAN support. You're more than half-way there though. To configure the 6LoWPAN stack, you need another tool, wpan-tools. Get this from GitHub:


git clone --depth 1 https://github.com/linux-wpan/wpan-tools.git
 ↪wpan-tools

Before you can build this though, you need autoreconf:


sudo apt-get install dh-autoreconf

Then in the wpan-tools directory, you can run:


./autogen.sh
./configure CFLAGS='-g -O0' --prefix=/usr --sysconfdir=/etc
 ↪--libdir=/usr/lib
make
sudo make install

What's going on here? Linux is part of the UNIX family of operating systems (including BSD, among many others). They all have quirks, and source code authors have to deal with those. There have been many tools to make this management easier, and wpan-tools uses autoreconf to build a configuration file, then configure to work out the specifics of your RPi system so that when you make your application, all of the correct pieces are in place.

The result of this is that the application iwpan is now in the /usr/bin directory for use.

You're nearly there! Remember in the kernel configuration you set the 6LoWPAN and device drivers to be dynamic modules. They won't have been installed by default like you would expect modules to be. That's what all this device tree stuff is about—bringing devices into the system when it can't detect them normally. So the next step is to load the modules:


sudo modprobe at86rf230

Then lsmod should include something like this:


Module                  Size  Used by
ieee802154_6LoWPAN     19335  0
6LoWPAN                13191  8 nhc_fragment,ieee802154_6LoWPAN
at86rf230              22211  0
mac802154              49035  1 at86rf230
ieee802154             55698  2 ieee802154_6LoWPAN,mac802154
crc_ccitt               1278  1 mac802154

And now—ta-da!—iwpan list shows something like this:


wpan_phy phy0
supported channels:
page 0: 11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26
current_page: 0
current_channel: 13,  2415 MHz
cca_mode: (1) Energy above threshold
cca_ed_level: -77
tx_power: 4
capabilities:
iftypes: node,monitor
channels:
page 0:
[11]  2405 MHz, [12]  2410 MHz, [13]  2415 MHz,
[14]  2420 MHz, [15]  2425 MHz, [16]  2430 MHz,
[17]  2435 MHz, [18]  2440 MHz, [19]  2445 MHz,
[20]  2450 MHz, [21]  2455 MHz, [22]  2460 MHz,
[23]  2465 MHz, [24]  2470 MHz, [25]  2475 MHz,
[26]  2480 MHz
tx_powers: 4,3.7,3.4,3,2.5,2,1,0,-1,-2,-3,-4,-6,-8,-12,-17
cca_ed_levels: -91,-89,-87,-85,-83,-81,-79,-77,-75,-73,-71,
↪-69,-67,-65,-63,-61
cca_modes:
(1) Energy above threshold
(2) Carrier sense only
(3, cca_opt: 0) Carrier sense with energy above threshold
 ↪(logical operator is 'and')
(3, cca_opt: 1) Carrier sense with energy above threshold
 ↪(logical operator is 'or')
min_be: 0,1,2,3,4,5,6,7,8
max_be: 3,4,5,6,7,8
csma_backoffs: 0,1,2,3,4,5
frame_retries: 0,1,2,3,4,5,6,7
lbt: false
Supported commands:
...

Your 6LoWPAN device is now known to the Linux system.

Configuring 6LoWPAN

So now you have a new kernel, you have the at86rf230 device recognized, and the 6LoWPAN networking stack is in place. The final steps are to configure networking and bring the device up. You likely are used to Wi-Fi networks having an SSID. IEEE802.15.4 networks have a similar concept, a PAN ID. Two devices will be on the same network only if they have the same PAN ID. You use iwpan to set this:


iwpan dev wpan0 set pan_id 0xbeef

The ID of 0xbeef isn't fixed, but every example seems to use it!

Then, you bring up the interface using normal networking tools:


ip link add link wpan0 name lowpan0 type lowpan
ifconfig wpan0 up
ifconfig lowpan0 up

What have you got now? ifconfig returns something like this:


lowpan0   Link encap:UNSPEC  HWaddr
 ↪EE-0B-FB-0F-76-B9-F3-93-00-00-00-00-00-00-00-00
inet6 addr: fe80::ec0b:fb0f:76b9:f393/64 Scope:Link
UP BROADCAST RUNNING MULTICAST  MTU:1280  Metric:1
RX packets:38 errors:0 dropped:0 overruns:0 frame:0
TX packets:39 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:5205 (5.0 KiB)  TX bytes:5205 (5.0 KiB)

wpan0     Link encap:UNSPEC  HWaddr
 ↪EE-0B-FB-0F-76-B9-F3-93-00-00-00-00-00-00-00-00
UP BROADCAST RUNNING NOARP  MTU:123  Metric:1
RX packets:58 errors:0 dropped:0 overruns:0 frame:0
TX packets:55 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:300
RX bytes:4111 (4.0 KiB)  TX bytes:4904 (4.7 KiB)

The interface wpan0 is the wireless device. The interface lowpan0 is the 6LoWPAN network device, just like eth0, the loopback device and so on. Note how it has an IPv6 address, but no IPv4 address—this is next-generation IP only!

Ping!

You are done! Well, almost. There is an old B.C. comic strip where one character invents the telephone. "Who do we ring?" asks his friend. "I only invented one" is the reply. You need someone to talk to. So, do all this over again with another RPi. You did buy two RPis and two wireless modules, didn't you?

The ifconfig command tells you the IPv6 address of the 6LoWPAN device. From the other device, once you have it set up, do:


ping6 -I lowpan0 fe80::ec0b:fb0f:76b9:f393 # IPv6 address of
                                           # the other device

or:


ping6  fe80::ec0b:fb0f:76b9:f393%lowpan0

The ping6 command is the IPv6 version of ping. The IPv6 address of each network interface is assigned automatically and is a link local address.

If you have multiple interfaces, each of them can be on a network segment with non-routable link local addresses. Hosts on these different network segments can have the same address. These are like IPv4 link local addresses 169.254.0.0/16, and they can't be routed across different network segments. So in Linux, you need to specify the interface to use (lowpan0) to avoid possible confusion. There are two ways of doing this: either use the -I lowpan0 option or append %lowpan0 to the IPv6 address.

On my system, this produces:


$ping6 -I lowpan0 fe80::ec0b:fb0f:76b9:f393
PING fe80::ec0b:fb0f:76b9:f393(fe80::ec0b:fb0f:76b9:f393) from
 ↪fe80::f0f9:a4ed:3cad:d1de lowpan0: 56 data bytes
64 bytes from fe80::ec0b:fb0f:76b9:f393: icmp_seq=1 ttl=64
 ↪time=11.6 ms
64 bytes from fe80::ec0b:fb0f:76b9:f393: icmp_seq=2 ttl=64
 ↪time=11.1 ms
64 bytes from fe80::ec0b:fb0f:76b9:f393: icmp_seq=3 ttl=64
 ↪time=10.5 ms

Success! The two devices can ping each other across 6LoWPAN. What if it doesn't work? Well, it didn't work for me for a long time, and working out where the failure occurred was painful. It turned out to be a wrong kernel for 6LoWPAN. To troubleshoot, first keep running ifconfig. This tells you which interfaces are getting and sending packets. It told me that the wireless layer (wpan0) was getting and receiving packets, but the networking layer wasn't. Then I ran wireshark using selector ip6 on packets, and it showed me errors at the network layer. The command dmesg gave gold, telling me the IPv6 packets were corrupted, even when pinging myself.

In desperation, I turned to Sebastian, giving him as much information as I could (uname, firmware version using /opt/vc/bin/vcgencmd, contents of /boot/config.txt, decompiling the device tree using dtc -I fs /proc/device-tree, and then wireshark and dmesg reports). He needed only the first line: wrong kernel. But, spending time working out a detailed report at least shows you are serious. "Duh, it doesn't work" isn't helpful to a maintainer!

A Sensor and a Receiver

You don't really need 6LoWPAN to communicate between Raspberry Pis. Wi-Fi and Ethernet are better. But now suppose one of them is a sensor running off a battery or solar panel. Wi-Fi is estimated to drain a battery within a fortnight; whereas 6LoWPAN on batteries can be expected to run for several years. I'm simulating this here by using one of the RPis as sensor for convenience.

To follow along, you will need to set up a client-server system. Usually, people think of servers as big grunty machines somewhere, but in the IoT world, the sensors will be the servers, handling requests for values from clients elsewhere in the network.

The server is just like a normal IPv6 server as described in the Python documentation: 18.1. socket —Low-level networking interface. But note that just as with the ping6 command above, you need to specify the network interface to be used. This means you have to use Python 3 rather than Python 2, as this has the socket function socket.if_nametoindex() that allows you to specify the IPv6 "scope id", which is the interface you use.

I don't want to complicate this article with how to add sensors to an RPi. Instead, I'll just measure the temperature of the RPi's CPU, as this can be found really easily by running this command from a shell:


vcgencmd measure_temp

This will return a string like:


temp=36.9'C

Within Python, you create a process to run this command using Popen and read from the stdout pipeline.

Here's an IPv6 TCP server that waits for connections, sends the temperature and then closes the connection:


#!/usr/bin/python3

import socket
from subprocess import PIPE, Popen

HOST = ''    # Symbolic name meaning all available interfaces
PORT = 2016  # Arbitrary non-privileged port

def get_cpu_temperature():
    process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE)
    output, _error = process.communicate()
    return output

def main():
    s6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
    scope_id = socket.if_nametoindex('lowpan0')
    s6.bind((HOST, PORT, 0, scope_id))
    s6.listen(1)

    while True:
        conn, addr = s6.accept()
        conn.send(get_cpu_temperature())
        conn.close()

if __name__ == '__main__':
    main()

And, here's a client that opens a connection and reads the temperature every ten seconds:


#!/usr/bin/python3

import socket
import time


ADDR = 'fd28:e5e1:86:0:e40c:932d:df85:4be9' # the other RPi
PORT = 2016

def main():
    # scope_id = socket.if_nametoindex('lowpan0')
    while True:
        s6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
        s6.connect((ADDR, PORT, 0, 0))
        data = s6.recv(1024)
        print(data.decode('utf-8'), end='')

        # get it again after 10 seconds
        time.sleep(10)

if __name__ == '__main__':
    main()

The output looks like this:


temp=37.4'C
temp=37.4'C
temp=37.9'C

What's My Server's Address?

So imagine you've now got 1,000 of these sensors scattered out in the wild somewhere, and they are all running IPv6 servers. What are their addresses? How do you talk to them? Unfortunately, the OpenLabs module generates a new MAC address each time it is booted, so it generates a new IPv6 address each time. Running multi-cast discovery is not recommended for these low power networks as it is a power drain. I will cheat a bit in the next article, but show better ways in the third article.

Conclusion

The scenario presented in the last section is still a bit unrealistic. If you have enough power to drive an RPi as a sensor, you probably have enough power for it to use Wi-Fi or Ethernet. But soon there will be genuine low power sensors using 6LoWPAN, and this article has shown you how to bring them into one particular Linux system. It's been pretty heavy going, but right now this is cutting-edge stuff, so expect to bleed a bit!

In my next article, I'll describe how to bring a 6LoWPAN network into the standard IPv6 world, and in the third article, I plan to look at CoAP, the equivalent of HTTP for low power networks.

Resources

OpenLabs Raspberry Pi 802.15.4 Radio

Python API: socket — Low-level networking interface

IETF RFC4944: Transmission of IPv6 Packets over IEEE 802.15.4 Networks (6LoWPAN)

6LoWPAN: The Wireless Embedded Internet by Zach Shelby, Wiley 2009

Create a generic Raspbian image with 6LoWPAN support by Sebastian Meiling

Jan Newmarch has written several articles for the Linux Journal in the past, as well as more than 80 papers and six books. He is Professor of IoT at Box Hill Institute, and also Adjunct Professor at the University of Canberra and Adjunct Lecturer at Charles Sturt University. He expects to retire soon to a life of leisure, hacking and, of course, more Linux. His LinkedIn handle is https://www.linkedin.com/in/jannewmarch.

Load Disqus comments