EM2011WSP05

Aus Verteilte Systeme - Wiki
Zur Navigation springen Zur Suche springen

Projektbeschreibung

Mein Ziel besteht darin Daten einer Wetterstation über eine DDS Middleware, welche das Contiki Betriebssystem, auszulesen und über eine IPv6 Verbindung für Sensornetze (auch bekannt als 6LoWPAN) zu verteilen.

Hierzu bietet die im Rahmen einer Master-Thesis entstandene sDDS Implementierung Hilfestellung. Diese Implementierung bietet eine leichtgewichtige DDS Middleware für Sensornetze und wird in diesem Projekt so angepasst bzw. erweitert, so dass es das Contiki Betriebssystem nutzen kann.

Zu Beginn haben wir eine Vorabversion der Projektaufgaben und einen kleinen Stapel an Geräten ausgehändigt bekommen.

Bauteile

Chips

Zuerst habe ich die AVR RZ Raven Evaluation and Starter Kit Packung ausgepackt und habe dort 2 AVR Raven Boards und ein AVR USB Stick vorgefunden.

Ebenfalls gab es ein AVR Dragon um die Geräte zu debuggen und zu programmieren. Dem ganzen lagen 2 Standard-B USB Kabel und ein Netzteil (7,5V/4A),um die AVR RZ Raven Geräte ans Laufen zu bringen, bei. Diese Geräte dienen der Entwicklung und dem Rumprobieren.

Die Software soll schließlich auf einem deRFmega128-22A00 Chip laufen, welcher den Transceiver schon integriert hat und auf einem Sensor Terminal Board montiert ist. Das Board ist ein "Radio Controller Board" (kurz: RCB) und der Chip benötigt einen deRFtoRCB Adapter um auf das Board montiert werden zu können.

Sensoren

Die Wetterstation ist in verschiedenen Modulen realisiert.

Dem Datenblatt der Messeinheiten kann man (wenn es korrekt sein würde) entnehmen, wie mit den Geräten interagiert werden muss um brauchbare Resultate zu erzielen.

Die Windgeschwindigkeit-Messeinheit verfügt über einen magnetischen Schalter, der sich bei einer Geschwindigkeit von 2,4 km/h einmal pro Sekunde schließen sollte. Eine Messung jedoch hat ergeben, dass dieser Schalter öffnet statt schließt. Für die Messung werde ich auf der Software-Seite Interrupts benutzen.

Die Regenwanne arbeitet mit dem gleichen Prinzip. Es öffnet (laut Datenblatt: schließt) einen Schalter, sobald 0,2794 mm Wasser geflossen sind. Auch hier habe ich mich dazu entschlossen Interrupts für die Messung zu benutzen.

Die Wetterfahne ändert den Widerstand zwischen den beiden Anschlussklemmen, so dass man mittels Widerstandsmessung oder Spannungsmessung an einem Spannungsteiler die genaue Richtung ermitteln. Auf Software-Seite wird dies mit einer Lookup-Table realisiert.

Entwicklunglungsumgebung für die Microchips

Femto OS

Nach einiger Suche und Rumprobiererei habe ich feststellen müssen, dass entweder der laufende Linux Kernel an den EZBS Rechnern zu alt oder aber die Toolchain Pakete nicht in der Lage waren eine geeignete Umgebung zu liefern.

Dank eines Kommilitonen habe ich das FemtoOS Projekt gefunden. Dies scheint eine Entwicklungsumgebung für AVR basierte Embedded Devices zu bieten und erspart somit das lästige Aufsetzen einer cross-development Toolchain.

Nach dem Entpacken von FemtoOS 0.92 führt man ein script aus um die Installation durchzuführen.

./Install_Scripts/install_toolchain --install ~/es-projekt/femtoos

Bei der Ausführung stellt man leider fest, dass manche Patches nicht mehr verfügbar sind.

Die Downloadquelle kann im script geändert werden. Hierzu muss die variable PATCH_LOCATION mit einem Ersatzlink versehen werden:

http://edge.rit.edu/edge/P10041/public/DocumentingKit2/arduino%20programming%20application/arduino-0018/hardware/tools/avr/source/

Nach der erfolgreichen Installation habe ich den Pfad zu den installierten binaries für die Shell zugänglich gemacht.

export PATH=~/es-projekt/femtoos/bin:$PATH

Dann avr-gcc einmal auf Probe ausgeführt:

impulze@istari ~/es-projekt $ printf 'int main(){}' | avr-gcc -x c - ; echo $?
0
impulze@istari ~/es-projekt $ 

Um zu überprüfen, ob Programme auch auf den Chips ausgeführt werden können, habe ich testweise die beiden Chips mit den mitgelieferten Dateien geflashed. Dazu dient das Tool avrdude, welches FemtoOS gleich mitliefert. Ich habe mir kleine wrapper geschrieben, so dass ich die Parameter nicht jedes mal neu angeben muss.

echo 'avrdude -c dragon_jtag -p m1284p -P usb "$@"' > avrdude_dev
echo 'avrdude -c dragon_jtag -p usb1287 -P usb "$@"' > avrdude_usb
chmod +x avrdude_{dev,usb}

Das Flashen der Chips muss geschehen, während die Raven Boards über das Netzteil und der Stick über USB mit Spannung versorgt werden:

./avrdude_dev -v -v -U flash:w:/media/cdrom/binaries/raven_telnet_shell/atmega1284p/shell.hex
./avrdude_usb -v -v -U flash:w:/media/cdrom/binaries/UsbStick/AVRRZUSBSTICK.hex

Beide Vorgänge werden separat ausgeführt, weil das JTAG Kabel umgesteckt werden muss. Nach erfolgreichem Flash-Vorgang liefert avrdude eine Bestätigung:

avrdude done.  Thank you.

Eigene Toolchain

Im späteren Verlauf des Projekts musste ich leider doch eine eigene Toolchain bauen. Als Grundlage habe ich jedoch das Installationsscript von FemtoOS benutzt. Diese neue Toolchain war notwendig, damit ich den ATmega128RFA1 chip flashen konnte. Dank hervorragender Vorarbeit, war das script mit geringen Änderungen schnell angepasst.

Contiki

Nun habe ich mir vom Contiki Projekt das GIT repository lokal gecloned.

Um die Entwicklungsumgebung näher kennen zu lernen, habe ich versucht ein Beispielprojekt zu bauen.

cd ~/es-projekt/contiki/examples/telnet-server
make TARGET=avr-raven

Leider schlägt dies beim Linken des Programms fehl:

[...]examples/telnet-server/../../apps/shell/shell-file.c:274: undefined reference to `cfs_remove'

Es werden auch weitere Symbole nicht gefunden werden.

Nachdem ich gemerkt habe, dass ich durch mein Vorgehen eine Shell inkl. Filesystem und weiteres produziere, habe ich mich nach einem anderen Beispiel umgesehen und habe mich für das ping-ipv6 Beispiel entschieden.

cd ~/es-projekt/contiki/examples/ping-ipv6
make TARGET=avr-raven

Dann habe ich mir ein Projekt angelegt, um eine statische Contiki Bibliothek zu bauen.

mkdir ~/es-projekt/contiki-library
cd ~/es-projekt/contiki-library
cat >Makefile<<EOF
CONTIKI=\$(shell echo \$\$HOME)/es-projekt/contiki
TARGET=avr-raven
include \$(CONTIKI)/Makefile.include
EOF

Nun habe ich die statische Bibliothek erstellt:

make contiki-avr-raven.a

Das Bauen des ping Beispiels und der statischen Bibliothek waren erfolgreich.

Nun wollte ich jedoch schauen, ob ich Contiki irgendwie auf den USB Stick flashen kann. Dabei habe ich die Applikation ravenusbstick gefunden. Hier ist zu beachten, dass das TARGET angepasst werden muss. Dies habe ich einem AVR Raven Contiki Wiki Eintrag entnommen. Jedoch macht das Makefile in diesem Verzeichnis das schon für mich.

cd ~/es-projekt/contiki/examples/ravenusbstick
make

Nun befindet sich in diesem Verzeichnis eine Datei namens ravenusbstick.hex. Da diese die gleiche Endung hat wie die Dateien, die sich auf der CD befinden, habe ich mich dazu entschlossen diese einfach auf den USB Stick zu flashen.

~/es-projekt/contiki/examples/ravenusbstick $ ~/es-projekt/avrdude_usb -U flash:w:ravenusbstick.hex -v -v

Debugging

Um zu sehen was mein Code so tut, habe ich mir eine Schnittstelle für GDB, names avarice, vorbereitet. Dazu habe ich die aktuelle Version runtergeladen und entpackt und schließlich klassisch mit autotools installiert.

~/es-projekt/avarice/avarice -g -f contiki/examples/ravenusbstick/ravenusbstick.elf localhost:4242

Nebenbei habe ich mir entsprechende udev Regeln angelegt, um das USB Gerät unter einem festen Namen ansprechen zu können:

istari ~ # cat /etc/udev/rules.d/99-own_usb.rules 
SUBSYSTEM=="usb", ACTION=="add", ENV{ID_SERIAL}=="ATMEL_AVRDRAGON_00A20000689D", OWNER="impulze", SYMLINK+="avr_dragon"
SUBSYSTEM=="usb", ACTION=="add", ENV{ID_SERIAL}=="Atmel_Jackdaw_6LoWPAN_Adaptor_021213141516", OWNER="impulze", SYMLINK+="avr_jackdaw"
istari ~ # udevadm control --reload-rules

Dies war nicht erforderlich. Jedoch lag mir der Gedanke nahe, dass es während der Softwareentwicklung von Vorteil sein kann, nicht ständig die USB Bus- und Gerätenummer zu ermitteln.

Dadurch, dass avarice mir zu dem GDB Server zusätzlich noch die Flash-Aufgaben übernimmt, habe ich mich dazu entschlossen zum Flashen der Chips weiterhin avarice zu verwenden.

Da es sich bei GDB um ein Projekt handelt, welches autotools nutzt, war die Installation straight-forward:

impulze@istari ~/es-projekt/gdb-7.3 $ ./configure --prefix="$HOME"/es-projekt/gdb --target=avr
impulze@istari ~/es-projekt/gdb-7.3 $ make
impulze@istari ~/es-projekt/gdb-7.3 $ make install

Nach Ausführen des Debuggers, scheint sich der Code tatsächlich auf dem Chip in Ausführung zu befinden:

impulze@istari ~/es-projekt $ ./gdb/bin/avr-gdb ./contiki/examples/ravenusbstick/ravenusbstick.elf
[...]
(gdb) target remote localhost:4242
Remote debugging using localhost:4242
0x0000fffe in ?? ()
(gdb) c
Continuing.

Da RAM begrenzt ist und manche Daten auf dem Flash gespeichert sind (siehe Data in Program Space Dokumentation), werden falsche oder keine Daten angezeigt, wenn man sich die Zeichenketten anzeigen lassen will.

[...]
Breakpoint 1, send_headers (s=0x80030b, statushdr=0x800189 "ven LCD interface process")

Um die korrekte Zeichenkette zu sehen, zieht man das Offset 0x800000 ab und schaut sich das Programm an der resultierenden Adresse an:

impulze@istari ~/es-projekt/contiki/examples/webserver-ipv6-raven $ avr-nm webserver6-avr-raven.elf  | grep ^00000189
00000189 T httpd_200ok

Komplikationen

Es scheint dass das Gerät willkürlich rumbockt. Wenn man gdb startet und sich folgendes Terminal präsentiert:

0x0001fffe in ?? ()
(gdb) 

dann sollte man versuchen das Gerät nochmal vom Strom zu trennen und neu anzuschließen, so dass sich folgendes präsentiert:

0x00000000 in __vectors ()
(gdb)

Im späteren Verlauf des Projekts nahm man an, dass der Chip defekt ist, da es bei anderen nicht aufgetreten ist. Viel weiter wurde dieses Phänomen nicht verfolgt, da ich zu diesem Zeitpunkt bereits mit der End-Hardware in Berührung gekommen bin.

Damit beim Debuggen keine Symbole wegoptimiert werden, werden Optimierungen beim verwendeten Compiler abgeschaltet. Dies hat zur Folge, dass dieser nicht mehr in der Lage ist die Makros zum blockierenden Warten zu verwenden.

[...] util/delay.h:95:3: warning: #warning "Compiler optimizations disabled; functions from <util/delay.h> won't work as designed"

Somit habe ich anhand von Recherche habe ich herausgefunden, welche Anzahl von Zyklen in einer bestimmten Instruktion verwendet werden und habe mir damit Makros gebaut um das Warten selbst zu implementieren.

#define custom_delay_8cyc(x) \
        do \
        { \
                asm volatile( \
                        "nop\n"                    /* [1 cycle] */ \
                        "nop\n"                    /* [1 cycle] */ \
                        "cp  %A0, __zero_reg__ \n" /* count & 0xFF == 0     [1 cycle] */ \
                        "cpc %B0, __zero_reg__ \n" /* (count & 0xFF00 >> 8) [1 cycle] */ \
                        "breq 2f\n"                /* jump to end [1 cycle] */ \
                        "1:\n" \
                        "sbiw %0, 1\n"             /* count-- [count * 2 cycles] */ \
                        "brne 1b\n"                /* loop if != 0 [(count - 1) * 2 cycles + 1 cycle] */ \
                        "2:" \
                                                   /* 4 * (count + 1) if count > 0, else 6 */ \
                ::"w"((x)) \
                ); \
        } while(0);
#define _delay_ms(x) custom_delay_8cyc(((x) * F_CPU) / 1000 / 8)
#define _delay_us(x) custom_delay_8cyc(((x) * F_CPU) / 1000 / 1000 / 8)

Raven USB Stick als Netzwerkschnittstelle in Linux

Dank einer Beschreibung in einem Wiki Eintrag und dem schon erwähnten AVR Raven Contiki Wiki Eintrag habe ich herausgefunden, dass ich für meinen Linux Kernel CONFIG_USB_NET_RNDIS_HOST=y und CONFIG_USB_ACM=y benötige.

Nach Neukompilieren und Reboot des Kernels konnte ich dann Ausgaben im Kernel-Ringbuffer auffinden:

usb 1-4.2: new full speed USB device number 5 using ehci_hcd
rndis_host 1-4.2:129.0: dev can't take 1558 byte packets (max 1356), adjusting MTU to 1298
rndis_host 1-4.2:129.0: usb0: register 'rndis_host' at usb-0000:00:0b.1-4.2, RNDIS device, 02:12:13:14:15:16
cdc_acm 1-4.2:129.2: ttyACM0: USB ACM device

Dem Gerät wird nachdem man den Status auf up geändert hat eine link-local Adresse zugewiesen. RFC2373 spezifiziert wie diese auszusehen hat. Man invertiert das 7. Bit der MAC Adresse, füllt nach dem 3. Byte die Adresse mit 0xFFFE und erhält so aus der MAC 02:12:13:14:15:16 die link-local Adresse FE80::12:13FF:FE14:1516.

istari ~ # ip link set dev usb0 up
istari ~ # ip -6 a show dev usb0 scope link
10: usb0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1298 qlen 1000
    inet6 fe80::12:13ff:fe14:1516/64 scope link 
       valid_lft forever preferred_lft forever
istari ~ # ping6 fe80::12:13ff:fe14:1516%usb0 -c1
PING fe80::12:13ff:fe14:1516%usb0(fe80::12:13ff:fe14:1516) from fe80::12:13ff:fe14:1516 usb0: 56 data bytes
64 bytes from fe80::12:13ff:fe14:1516: icmp_seq=1 ttl=64 time=0.031 ms

--- fe80::12:13ff:fe14:1516%usb0 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.031/0.031/0.031/0.000 ms

Dann habe ich mit screen erfolgreich auf die serielle Schnittstelle zugegriffen:

screen /dev/ttyACM0

Wenn ich auf der Tastatur h drücke, zeigt sich folgendes Bild:

*********** Jackdaw Menu **********
        [Built Oct 21 2011]      
*  m        Print current mode    *
*  s        Set to sniffer mode   *
*  n        Set to network mode   *
*  c        Set RF channel        *
*  p        Set RF power          *
*  6        Toggle 6lowpan        *
*  r        Toggle raw mode       *
*  d        Toggle RS232 output   *
*  S        Enable sneezer mode   *
*  e        Energy Scan           *
*  R        Reset (via WDT)       *
*  h,?      Print this menu       *
*                                 *
* Make selection at any time by   *
* pressing your choice on keyboard*
***********************************

Raven Board global über IPv6 erreichen

Da nun der USB Stick über eine link-local IPv6 Adresse erreicht werden kann und eingestellt werden kann, ist es Zeit, sich mit dem Raven Board zu befassen.

Dazu habe ich mir zuerst das Contiki Beispiel webserver-ipv6-raven auf das Board gepackt.

In ~/es-projekt/contiki/platform/avr-raven/apps/raven-webserver/httpd-fs/makefsdata.h kann man erkennen, mit welcher link-local Adresse das Gerät erreichbar sein wird.

/* Link layer ipv6 address will become fe80::11:22ff:fe33:4455 */
uint8_t default_mac_address[8]  PROGMEM = {0x02, 0x11, 0x22, 0xff, 0xfe, 0x33, 0x44, 0x55};

Ein kurzer Test verläuft erfolgreich.

istari ~ # ping6 fe80::11:22ff:fe33:4455%usb0 -c1
PING fe80::11:22ff:fe33:4455%usb0(fe80::11:22ff:fe33:4455) 56 data bytes
64 bytes from fe80::11:22ff:fe33:4455: icmp_seq=1 ttl=128 time=49.5 ms

--- fe80::11:22ff:fe33:4455%usb0 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 49.575/49.575/49.575/0.000 ms

Da aber die Notation einer link-local Adresse nicht weiter spezifiziert ist wollte ich eine globale Adresse einrichten. Laut Dokumentation in ~/es-projekt/contiki/doc/tutorial-raven.txt kann man nun den Router Advertisement Daemon laufen lassen, so dass sich das Board eine Adresse selbstständig aus dem beworbenen Präfix zusammenbaut.

istari ~ # cat /etc/radvd.conf 
interface usb0
{
    AdvSendAdvert on;
    AdvLinkMTU 1280;
    AdvCurHopLimit 128;
    AdvReachableTime 360000;
    MinRtrAdvInterval 100;
    MaxRtrAdvInterval 150;
    AdvDefaultLifetime 200;
    prefix AAAA::/64
    {
        AdvOnLink on;
        AdvAutonomous on;
        AdvPreferredLifetime 4294967295;
        AdvValidLifetime 4294967295;
    };
};
istari ~ # radvd

Und wie erwartet, hat sich das Raven Board eine globale Adresse zugewiesen.

istari ~ # ping6 aaaa::11:22ff:fe33:4455 -c1
PING aaaa::11:22ff:fe33:4455(aaaa::11:22ff:fe33:4455) 56 data bytes
64 bytes from aaaa::11:22ff:fe33:4455: icmp_seq=1 ttl=128 time=44.9 ms

--- aaaa::11:22ff:fe33:4455 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 44.950/44.950/44.950/0.000 ms

Und tatsächlich läuft dort auch ein Webserver:

istari ~ # wget http://[aaaa::11:22ff:fe33:4455] -O - -q

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html><head><title>Welcome to the Contiki-demo server!</title>
<link rel="stylesheet" type="text/css" href="/style.css"><link rel="icon" href="favicon.png" type="image/png">
</head><body bgcolor="#fffeec" text="black">
<div class="menublock"><div class="menu"><p class="border-title">Menu</p><p class="menu">
<a href="/">Front page</a><br><a href="status.shtml">Status</a><br><a href="files.shtml">File statistics</a><br>
<a href="tcp.shtml">Network connections</a><br><a href="processes.shtml">System processes</a><br>
<a href="sensor.shtml">Sensor Readings</a>
</p></div></div>
<div class="contentblock"><p class="border-title">Welcome to the <a href="http://www.sics.se/contiki/">Contiki</a> web server!</p>
<p class="intro">These pages are from a webserver running under the <a href="http://www.sics.se/contiki/">Contiki operating system</a>.
<p class=right><br><br><i>This page has been sent 4 times</i></div></body></html>

sDDS

Ziemlich früh im Projekt habe ich dann den Quellcode zu sDDS erhalten und habe gleich damit ein GIT repository erstellt.

Arbeiten an der Linux/POSIX Implementierung

Dank der schon vorhandenen Netzwerkimplementierung für Linux, war nur noch wenig Portierarbeit notwendig. Ein Makel der Linux-Implementierung war die fehlende Unterstützung für IPv6. Dadurch, dass ich in der Lage sein muss mit meinen Geräten über IPv6 zu kommunizieren, war es notwendig diese Unterstüzung in sDDS einzuarbeiten.

Die Unterstützung für IPv6 unter POSIX ist dank POSIX 2001.1 und dem darin erhaltenen getaddrinfo Aufruf relativ einfach. Es muss beim Erstellen lediglich eine Datenstruktur verwendet werden, die beide Protokolle unterstützt. Eine Umwandlung in die spezifische Protokoll-Datenstruktur erfolgt nur dann, wenn man die protokollspezifischen Daten (z.B. Adresse) benötigt.

Ein weiteres Problem besteht darin, dass der sDDS Autor eine Verteilung der Topics bzw. das Konzept der DDS built-in topics Kapitel 7.1.5 OMG DDS Spezifikation realisieren wollte. Er hatte vorgesehen Nachrichten über IPv4 Broadcast Pakete an die teilnehmenden Sensoren zu verschicken. Dies ist mit IPv6 nicht möglich, da es kein Broadcast in diesem Sinne unterstützt. Da diese Implementierung jedoch derzeit nicht in meinem Projekt beachtet werden musste, habe ich mich einfach dazu entschlossen IPv6 Multicast zu verwenden. Speziell habe ich mich für die all-nodes multicast address entschieden.

addr->sin6_family = AF_INET6;
addr->sin6_addr.s6_addr[0] = 0xff;
addr->sin6_addr.s6_addr[1] = 0x02;
addr->sin6_addr.s6_addr[15] = 1;
addr->sin6_port = htons(g_network.port);

Mittels Makrodefinition kann man nun Protokoll, Adresse, sowie Sende- und Empfangsadresse bei der Übersetzung angeben. Die POSIX Implementierung empfängt auf der Wildcard-Adresse und sendet an den lokalen Rechner. Der Port über den die Kommunikation abläuft ist 23234, so wie der Autor von sDDS dies ursprünglich vorgesehen hat. Das Standardprotokoll wurde aus Bequemlichkeitsgründen auf IPv6 gestellt. Ich denke, dass gängige Linux Distributionen und andere die diese Software verwenden wollen Kernel besitzen, die standardmäßig IPv6 unterstützen. Auszug aus dem Quelltext der POSIX Implementierung:

#ifndef SDDS_LINUX_PORT
#define SDDS_LINUX_PORT 23234
#endif
#ifndef SDDS_LINUX_PROTOCOL
// only use AF_INET or AF_INET6
#define SDDS_LINUX_PROTOCOL AF_INET6
#endif
#ifndef SDDS_LINUX_LISTEN_ADDRESS
#define SDDS_LINUX_LISTEN_ADDRESS "::"
#endif
#ifndef SDDS_LINUX_SEND_ADDRESS
#define SDDS_LINUX_SEND_ADDRESS "::1"
#endif

Arbeiten an der Contiki Implementierung

Contiki bietet zur Netzwerkkommunikation einen TCP/IP Stack namens µIP, welcher vom Contiki Autor selbst geschrieben wurde und derzeit von Entwicklern weltweit angepasst/verändert bzw. weiterentwickelt wird und somit eigenständig und losgelöst von Contiki (unter Einhaltung einer BSD Lizenz) genutzt werden kann. Das µIP, welches vom Contiki Code zur Verfügung gestellt wird, kommt in einer IPv4 und IPv6 Variante. Da in meinem Projekt eine Kommunikation über IPv6 erforderlich ist, muss man die Version wird mittels des Makros UIP_CONF_IPV6 bei der Übersetzung festlegen. Befindet man sich in der Contiki-Entwicklungsumgebung, so kann man einfach die gleichnamige Makevariable setzen.

Ein großer Vorteil der µIP Nutzung liegt darin, dass es auf 6LoWPAN, worüber die Nachrichtenkommunikation im Projekt stattfinden soll, aufbaut. Das von Contiki angebotene µIP hält sich an die Vorgaben des IPv6 over IEEE 802.15.4 RFC und implementiert die Spezifikationen, die es für nötig und wichtig hält.

Our implementation is based on RFC4944 Transmission of IPv6 Packets over IEEE 802.15.4 Networks, draft-hui-6lowpan-interop-00 Interoperability Test for 6LoWPAN, and draft-hui-6lowpan-hc-01 Compression format for IPv6 datagrams in 6lowpan Networks.

So ist es nicht mehr nötig sich mit den Details von 6LoWPAN zu befassen, weil Contiki automatisch zu sendene IPv6 Pakete nach 6LoWPAN und empfangene empfangene 6LoWPAN Pakete nach IPv6 konvertiert.

In meiner Implementierung wird sowohl µIP direkt, als auch die darüber liegende UDP Protokollimplementierung von µIP genutzt. µIP bietet eine (wie von POSIX gewohnte) BSD socket-artige API namens Protosockets und eine low-level RAW API, die zwar komplexer, aber speichereffizienter ist. Ich entschied mich dazu, die speichereffiziente Lösung aus Lernzwecken und als Herausforderung zu bevorzugen. Nach ca. 80% der Implementierung, bemerkte ich jedoch, dass µIP eine wesentlich einfachere Alternative anbot, die aber auf der low-level API aufbaut.

The default Contiki UDP API is difficult to use.

The simple-udp module provides a significantly simpler API.

Das Callback welches stehts aufgerufen wird, sobald neue Pakete eingehen

void receive(struct simple_udp_connection *connection,
             uip_ipaddr_t const *src_addr, uint16_t src_port,
             uip_ipaddr_t const *dest_addr, uint16_t dest_port,
             uint8_t const *data, uint16_t data_len)

sucht nach dem Sender in einer Datenbank und ruft dann das sDDS Subsystem zum Verarbeiten von eingehenden Daten auf. Das sDDS benutzt locator um die Sensoren in der Implementierung zu repräsentieren. Die für Contiki spezifische Implementierung orientiert sich an der POSIX Implementierung:

struct contiki_locator
{
        locator_t sdds_loc;
        uip_ipaddr_t address;
        u16_t port;
};

Im sdds_loc ist der sDDS spezifische Kontext, welcher z.B. auch einen Reference-Counter enthält. Desweiteren wird die Adresse und der Port gespeichert, welches das eigentliche Identifikationsmerkmal des Endpunkts ist. Wird nun ein UDP Paket auf dem Microchip emfpangen, so schaut die Software ob es diesen Endpunkt schon kennt und erstellt, falls dies nicht der Fall ist, einen neuen locator und fügt diesen in die Datenbank ein. Muss ein neuer erstellt werden, so genügen die folgenden µIP Aufrufe:

uiplib_ipaddrconv(SDDS_CONTIKI_SEND_ADDRESS, &contiki_loc->address);
contiki_loc->port = UIP_HTONS(SDDS_CONTIKI_PORT);

Die Konstanten sind jene, die bei der Übersetzung mit angegeben wurden. Die Verbindung, die gleichzeitig das Callback registriert und anschließend den erstellten UDP socket benennt wird wie folgt realisiert:

uip_ipaddr_t address;

uiplib_ipaddrconv(SDDS_CONTIKI_LISTEN_ADDRESS, &address);
simple_udp_register(&g_connection, SDDS_CONTIKI_PORT, &address, 0, receive);

Auch hier werden die Konstanten genutzt die dem Compiler mitgegeben wurden. Im Idealfall sollte hier zu einem späteren Zeitpunkt alles etwas dynamischer sein.

Beim Senden der Datenpakete musste ich leider auf die non-simple API zurückgreifen, denn die einfache API würde die Pakete genau an den Port senden, den wir bei der Registrierung angegeben haben. Da der sDDS Autor jedoch den Port 23234 sowohl für ein- als auch ausgehende Pakete vorgesehen hat, so musste ich folgenden Aufruf benutzen:

uip_udp_packet_sendto(g_connection.udp_conn, buffer->buffer_start, buffer->position,
                      &address, UIP_HTONS(SDDS_CONTIKI_PORT));

Hierbei ist address die Adresse des Endpunkts. Das hier die low-level API benutzt wird sieht man daran, dass auf die internen Variablen der Datenstruktur g_connection zugegriffen wird.

Sonstige Veränderungen

Außerhalb des Projektrahmens habe ich ein Programm geschrieben, welches es ermöglicht die sDDS Implementierungen mittels Python und einfachen Textdateien zu generieren. In meinem sDDS Anwendungsfall Makefile stehen z.B. folgende Zeilen:

*-ds.c *-ds.h: datastructures
        $(shell python ./generate_ds.py $<)

%_sdds_impl.c %_sdds_impl.h: %-dds-roles datastructures
        $(shell python ./generate_sdds.py $(<:-dds-roles=))

Mithilfe dieser Aufrufe bin ich in der Lage die Datenstrukturen, die Methoden für das (De-)Serialisieren dieser und die notwendigen Funktionen zum Erstellen von Schreib- und Leseinstanzen. Die Textdatei, aus der diese Datenstrukturen entwickelt werden, ist wie folgt aufgebaut:

[anemometer]
domain: 0
topic: 1

/* [0, 65536), current wind speed in 0.01 km/h */
dds_unsigned_short_t speed

Hier werden Name, sowie Domain/Topic und eine Folge von optionalem Kommentar und einer Variablendeklaration ohne Semikolon eingefügt. Die Kommentare stehen schließlich nach der Generierung im Interface (Headerdatei), so dass der Programmierer weiß, wie er mit den Datentypen umzugehen hat.

Ein Auszug aus dem genierten Interface:

struct anemometer_data
{
        /* [0, 65536), current wind speed in 0.01 km/h */
        dds_unsigned_short_t speed;
};

typedef struct anemometer_data anemometer_data_t;

#ifdef sDDS_TOPIC_HAS_PUB
dds_return_t anemometer_data_reader_take_next_sample(
        dds_data_reader_t *_this,
        anemometer_data_t **values,
        dds_sample_info_t *sample_info
);
rc_t topic_marshalling_anemometer_decode(byte_t *buf, data_t *data, size_t *size);
#endif

Sensoren

BMP085 Druck- und Temperatursensor

Der BMP085 Drucksensor wird über den seriellen Datenbus I²C angesprochen. Der serielle Datenbus wird vom Chip direkt unterstützt und brauch so nicht selbstständig implementiert werden. Die Initialisierung des Sensors entspricht der Initialisierung des I²C Bus und dem Auslesen von statischen Werten aus dem EEPROM des Sensors, die zur Umrechnung der ausgelesenen Werte benötigt werden.

Um den I²C Bus auf dem Chip zu initialisieren genügt folgender Code:

        power_twi_enable();

        TWI_SCL_PORTR |= _BV(TWI_SCL_DD);
        TWI_SDA_PORTR |= _BV(TWI_SDA_DD);

        TWSR = 0;
        TWBR = (F_CPU / TWI_BUS_SPEED - 16) / 2;
        TWDR = 0xFF;
        TWCR = _BV(TWEN);

Hier wird der Chip aus einem möglichen Schlafzustand geweckt, um für den I²C Bus Strom zur Verfügung zu stellen und es wird das Datenregister für die I²C Clock- und Datenpins auf 1 gesetzt. Dann wird die Busgeschwindigkeit in das entsprechende Register in Abhängigkeit von der CPU Frequenz eingetragen und eine Initialisierung laut Busspezifizierung vorgenommen und.

Das Übertragen von Daten auf dem I²C Bus kann dem AVR TWI Artikel entnommen werden. Es wird in dem dortigen Kontext (und in AVR Dokumentationen) TWI (Two Wire Interface) genannt, um patentrechtlichen Problemen aus dem Weg zugehen.

Meine Abstraktion für diesen Bus sieht eine Benutzung von Kommando/Byte Paaren vor, so dass die Initialisierung dieses Sensors folgendermaßen abläuft:

        twi_data_t data[7];
        uint8_t failed_command;

        twi_init();

        data[0].type = TWI_TYPE_START;

        data[1].type = TWI_TYPE_TRANSMIT_ADDRESS;
        data[1].byte = 0xEE;

        data[2].type = TWI_TYPE_TRANSMIT_DATA;
        data[2].byte = 0xF4;

        data[3].type = TWI_TYPE_TRANSMIT_DATA;
        data[3].byte = 0x2E;

        if (twi_communicate(data, 4, &failed_command) != 0)
                return 1;

Welche Bytes welchen Einfluss auf den Sensoren haben können dem BMP085 Datenblatt entnommen werden.

Die 2 Werte, die mittels sDDS über DDS verbreitet werden, können mit folgenden Aufrufen ausgelesen werden:

int bmp085_read_temperature(int16_t *temperature);
int bmp085_read_pressure(int32_t *pressure);

Die Werte entsprechen nur Koeffizienten, da hier mit Festkommazahlen gearbeitet wird. Die ausgelesen Temperatur hat eine Granularität von 0,01°C und der Druck eine Granularität von 0,01 mbar (hPa). Die entsprechende Datenstruktur, die von sDDS angeboten wird hat die folgende Repräsentation:

struct temperature_data
{
        /* [-65536, 65536), the temperature in 0.01°C */
        dds_short_t temperature;
};

struct pressure_data
{
        /* [-4294967296, 4294967295), the pressure in 1Pa (0.01hPa, 0.01mbar) */
        dds_long_t pressure;
};

SHT15 Feuchtigkeits- & Temperatursensor

Der SHT15 Sensor ist leider nicht I²C kompatibel, wodurch ich eigenes Protokoll mit Bit-Banging implementieren musste. Die genaue Spezifikation kann dem SHT15 Datenblatt entnommen werden. Die Initialisierung und Kommunikation läuft ähnlich wie zuvor bei der Benutzung von I²C ab. Es folgen Codeauszüge zur Initialisierung und Kommunikation:

static inline uint8_t sht15_sda_bit_is_set(void) { return (SHT15_SDA_PINR & _BV(SHT15_SDA_PIN)) != 0; }
static inline void sht15_scl_high(void) { SHT15_SCL_PORTR |= _BV(SHT15_SCL_PORT); }
static inline void sht15_scl_low(void) { SHT15_SCL_PORTR &= ~_BV(SHT15_SCL_PORT); }
static inline void sht15_sda_high(void) { SHT15_SDA_DDR &= ~_BV(SHT15_SDA_DD); }
static inline void sht15_sda_low(void) { SHT15_SDA_DDR |= _BV(SHT15_SDA_DD); }
static inline void sht15_delay(void) { delay_us(1.0 / SHT15_BUS_SPEED * 1000 * 1000 / 2); }
static inline void sht15_pulse(void)
{
        sht15_scl_high();
        sht15_delay();
        sht15_scl_low();
}

static void sht15_start(void)
{
        sht15_scl_high();
        sht15_delay();
        sht15_sda_low();
        sht15_delay();
        // now low pulse
        sht15_scl_low();
        [...]
}

static uint8_t sht15_read(uint8_t ack)
{
	uint8_t i;
	uint8_t byte;

	byte = 0;

	for (i = 0; i < 8; i++)
	{
		if (sht15_sda_bit_is_set())
 			byte |= 1;

		if (i != 7)
			byte <<= 1;

		sht15_pulse();
		sht15_delay();
	}
 
 	// master pulls down data if ACKed
	if (ack)
		sht15_sda_low();
	else
		sht15_sda_high();

	sht15_delay();
	sht15_pulse();

	if (ack)
		sht15_sda_high();

	sht15_delay();
	
	return byte;
}

Die Umrechnungen die im SHT15 Datenblatt aufgeführt sind benutzen Gleitkomma-Konstanten und Rechnungen, die ich in meiner Implementierung zu Festkommazahlen und Rechnungen umgewandelt habe.

Die Funktionen zum Auslesen des Sensors lauten:

int16_t sht15_read_temperature(void);
uint16_t sht15_read_relative_humidity(void);

Die Werte entsprechen, genau wie die Werte des BMP085 Sensors, nur Koeffizienten. Die ausgelesen Temperatur hat eine Granularität von 0,01°C und die relative Luftfeuchtigkeit eine Granularität von 0,01%. Die entsprechende Datenstruktur, die von sDDS angeboten wird hat die folgende Repräsentation:

struct temperature_data
{
        /* [-65536, 65536), the temperature in 0.01°C */
        dds_short_t temperature;
};

struct humidity_data
{
        /* [0, 65536), the relative humidity in 0.01% */
        dds_unsigned_short_t relative_humidity;
};

Wettermesseinheiten für Windgeschwindigkeit und Niederschlagsmenge

Diese beiden Sensoren verfügen über einen Schaltmechanismus. Wie im [#Bauteile Bauteile Kapitel] beschrieben, wird ein Schalter geschlossen sobald eine gewisse Menge an Wasser oder Anzahl an Umdrehungen erreicht wurde. In meinem Aufbau schließt der Schalter eine Verbindung zum Massepotenzial, wodurch der Pin-Eingang am Chip 0 (low) wird. Darauf muss bei der Initialisierung des Interrupt-Handlings geachtet werden. Der prinzipielle Ablauf der Initialisierung wird im Atmega128RFA1 Datenblatt beschrieben. Als Beispiel wird der Code zum Initialisieren der Regenwanne gezeigt:

        RAINGAUGE_PORTR |= _BV(RAINGAUGE_PORTVAL);

        // prevent an interrupt to occur when changing ISC bits
        EIMSK &= ~_BV(RAINGAUGE_INT);

#if 1
        // positive edge triggered
        RAINGAUGE_EICR |= _BV(RAINGAUGE_ISC(1)) | _BV(RAINGAUGE_ISC(0));
#else
        // negative edge triggered
        RAINGAUGE_EICR |= _BV(RAINGAUGE_ISC(1));
        RAINGAUGE_EICR &= ~_BV(RAINGAUGE_ISC(0));
#endif

        // clear interrupt flag, so it can't get fired until re-enabling
        EIFR |= _BV(RAINGAUGE_INTF);

        // enable the interrupt again
        EIMSK |= _BV(RAINGAUGE_INT);

Diese beiden Sensoren sollen Messwerte über einen bestimmten Zeitraum liefern, so dass es nötig war, eine Zeitkomponente in die Messung mit einfließen zu lassen. Zunächst dachte ich über eine Nutzung der Timer-Interrupts nach, wodurch ich dann in der Lage gewesen wäre, die erfasste Menge von Interrupts durch die verstrichene Zeit, seit der letzten Messung zu teilen und somit ein sinnvolles Messergebnis bereitzustellen. Nach genauer Überlegung jedoch kam ich zu dem Schluss, dass Betriebssysteme oder der Nutzer der Sensoren wohlmöglich selbst auf diese Interrupts zugreifen wird.

Schließlich kam ich, nach einem Gespräch mit den Leitern zu dem Entschluss eine Callback Funktion in meiner Schnittstelle anzubieten, die jede Sekunde (der Wert ist in der Treiber-Konfigurationsdatei festgelegt) von der Anwendung aufgerufen werden muss. Dies kann z.B. durch vom Betriebssystem bereitgestellte Timer-Abstraktionen geschehen.

Die Werte können dann mit diesen Funktionsaufrufen abgeholt werden:

int raingauge_read_current(uint32_t *value);
int raingauge_read_minute(uint32_t *value);
int raingauge_read_hour(uint32_t *value);
int anemometer_read_current(uint16_t *value);
int anemometer_read_minute(uint16_t *value);
int anemometer_read_hour(uint16_t *value);

Wie man erkennen kann, gibt es je einen Funktionsaufruf für den derzeitigen Wert, den Wert seit der letzten Minute und den Wert seit der letzten Stunde. Das Bereitstellen dieser zeitabhängigen Werte wird durch je 4 globale Variablen ermöglicht:

/* 65536 * 240 / 3600 ~= 4369km/h (should be enough) :) */
static uint8_t g_anemometer_ticks_since_last_call;
static uint16_t g_anemometer_ticks;
static uint16_t g_anemometer_callback_calls;
static uint16_t g_anemometer_values[2]; // hour, minute

/* 65536 * 2794 / 3600 ~= 50mm/h (record is 401 per hour) -> 32bit
 * though the value is still 16bit (655.36 mm/h) ought to be enough
 */
static uint8_t g_raingauge_ticks_since_last_call;
static uint32_t g_raingauge_ticks;
static uint16_t g_raingauge_callback_calls;
static uint16_t g_raingauge_values[2]; // hour, minute

Die erste globale Variable speichert die Anzahl an produzierten Interrupts seit des letzten Callback-Aufrufs (im Standardfall: 1 Sekunde). So kann die Funktion mit dem Suffix ``read_current die Geschwindigkeit bzw. Menge seit des letzten Aufrufs zurückliefern:

int raingauge_read_current(uint32_t *value)
{
        *value = (g_raingauge_ticks_since_last_call * 2749) / RAINGAUGE_CALLBACK_PERIOD;

        return 0;
}

Die zweite globale Variable speichert die Anzahl an produzierten Interrupts für den längsten Zeitraum (hier: 1 Stunde). Im Callback wird dann, zum Mitzählen der Aufrufe des Callbacks, die Zählervariable hochgezählt. Das Callback ist der Kern der Implementierung dieser Sensoren und sieht so aus:

static void raingauge_callback(void)
{
        uint16_t time_passed;

        g_raingauge_callback_calls++;

        time_passed = g_raingauge_callback_calls * RAINGAUGE_CALLBACK_PERIOD;

        if (time_passed % 60 == 0)
        {
                uint32_t value;

                value = (g_raingauge_values[0] + (g_raingauge_ticks * 2749) / time_passed) / 2;

                g_raingauge_values[0] = INTERNAL_MIN(((uint32_t)1 << 16), value);
        }

        if (time_passed % 3600 == 0)
        {
                uint32_t value;

                value = (g_raingauge_values[1] + (g_raingauge_ticks * 2749) / time_passed) / 2;

                g_raingauge_values[1] = INTERNAL_MIN(((uint32_t)1 << 16), value);

                // reset every hour
                g_raingauge_ticks = 0;
                g_raingauge_callback_calls = 0;
        }

        g_raingauge_ticks_since_last_call = 0;
}

Nach Ablauf der Minuten bzw. der Stunde wird in der 4. globalen Variable der Messwert abgelegt, so dass die Funktionen mit dem Suffix 'read_minute' bzw. 'read_hour' nichts weiter tun, als den Wert der Variable zurückzuliefern. Nach Ablauf des längsten Zeitraumes wird die Zählervariable für Interrupts und eigener Aufrufe auf 0 zurückgesetzt, so dass ein neuer Erfassungszeitraum beginnen kann.

Die hingegen sehr einfache Interrupt-Routine hat folgende Definition:

ISR(RAINGAUGE_VECTOR)
{
        g_raingauge_ticks_since_last_call++;
        g_raingauge_ticks++;
}

Anmerkungen

Da 2 Sensoren zum Auslesen der Temperatur zur Verfügung stehen, kann zwischen einen von beiden gewählt werden. Eine Untersuchung hinsichtlich der Genauigkeit oder Dauer des Messvorgangs wurde nicht durchgeführt, womit keine Präferenz besteht. Wie sich herausstellte, war die Funktionalität der I²C Schnittstelle, der eigenen Schnittstelle und das Behandeln von Interrupts dem Zufall überlassen, da Aufgrund von einer Kompatibiliäts-Pinbelegung des deRFtoRCB Adapter sämtliche Pins zwei Funktionen besaßen. Leider wurde dies erst relativ spät im Projekt bemerkt, so dass einige Überlegungen zur Verbesserung oder Erweiterung der Schnittstellen bzw. Kommunikation der Sensoren nicht mehr möglich waren.