Kommunikation by DL1NOS

3.1 Einleitung

Im ersten Schritt soll der Roboter noch nicht eigenständig handeln, sondern alle Daten an einen PC schicken, bzw. von einem PC empfangen. Damit wird die Programmierung erheblich vereinfacht, denn Änderungen am Quellcode können direkt am PC vorgenommen und übertragen werden. Außerdem entfällt das ständige Programmieren des Mikrocontrollers. Erst wenn das Programm ausgereift ist, wird es auf den Roboter portiert.

Als Funkmodul kommt ein preisgünstiges RFM12B-Bauteil zum Einsatz. Die Reichweite ist abhängig von den Umgebungsbedingungen und liegt im Freifeld bei ca. 100m. Im Gebäude verringert sich die Reichweite auf ca. 30m, je nach Bausubstanz. Der Roboter hält sich nur in der Wohnung auf. Dafür reicht die Leistung locker. Leider benötigt das Funkmodul 3,3V Spannung. Deshalb ist ein eigener Spannungsregler auf der Platine fürs Funkmodul untergebracht. Ein zusätzlicher ATMega8 Mikrocontroller erlaubt eine eigenständige Funktion dieser Platine und entlastet den Hauptprozessor. Alle ungenutzten Anschlüsse sind auf die Pinleiste herausgelegt.

Bild 1: Platine des Funkmoduls

Abbildung 1: Platine des Funkmoduls

 

3.1.1 Bestellliste

Es lohnt sich bei den Widerständen mindestens 10 Stck. zu kaufen. Benötigt werden für zwei Platinen mindestens die in der Bestellliste angegebenen Bauteile. Achtung der ATMEGA8 wurde von ATMEL abgekündigt und ist kaum noch verfügbar. Eine Portierung des Quellcodes auf einen alternativen Mikrocontroller, z.B. ATMEGA88 sollte möglich sein.

Nr. Bezugsquelle Best.-Nr. Was Anz. Einz.-Preis Ges.-Preis
1 it-wns.de RFM12B-868-DIP RFM-12B Transceiver 868MHz DIP 2 3,99 3,99
2 reichelt ATMEGA 8L8 DIP ATMega AVR-RISC-Controller, DIL-28 2 2,32 2,32
3 reichelt GS 28P-S IC-Sockel, 28-polig, superflach, gedreht, schmal 2 0,42 0,42
4 reichelt WSL 10G Wannenstecker, 10-polig, gerade 2 0,07 0,07
5 reichelt BL 2X10G 2,00 2x10pol. Buchsenleiste, gerade, RM 2,00 2 0,42 0,42
6 reichelt LM 317 TO 92 Spannungsregler, TO-92 2 0,19 0,19
7 reichelt SL 1X36W 2,54 36pol. Stiftleiste, gewinkelt, RM 2,54 2 0,24 0,24
8 reichelt RAD 1/100 Elektrolytkondensator, 5x11mm, RM 2,0mm 2 0,04 0,04
9 reichelt X7R-2,5 100N Vielschicht-Keramikkondensator 100N, 10% 6 0,04 0,12
10 reichelt METALL 270 Metallschichtwiderstand 270 Ohm 2 0,08 0,08
11 reichelt METALL 470 Metallschichtwiderstand 470 Ohm 2 0,08 0,08
12 reichelt METALL 10,0K Metallschichtwiderstand 10,0 K-Ohm 6 0,08 0,24

 

3.1.2 Schaltpläne, Platinenlayouts

Die Schaltpläne und Platinenlayouts wurden mit dem Layoutprogramm Eagle erstellt und können beim Anklicken des Bildes heruntergeladen werden.

3.1.3 Funktionstest

Wir suchen nach einer Möglichkeit, Daten über ein serielles Terminal am PC einzugeben, die dann per Funk an den Roboter gesendet werden. Nach einer kurzen Recherche im Internet wurde ein sehr gut geschriebener und dokumentierter Quellcode gefunden, der sich hervorragend für unser Projekt eignet.

Aber erst einmal eine Erweiterung unseres Steckbretts um die Funkmodul-Platine. Nicht vergessen, das Funkmodul wird nur mit 3,3V versorgt, demzufolge liegen auch nur 3,3V an den Ausgängen an und es dürfen auch nur 3,3V an den Eingängen des Moduls angelegt werden. Das Steckbrett wird aber mit 5V Spannung versorgt. Den Ausgang der Funkmodul-Platine können wir problemlos direkt mit dem Steckbrett verbinden. In der Transistor-Transistor-Logik ist festgelegt, dass eine Spannung größer 2,0V als High-Pegel erkannt wird. Umgekehrt, also wenn das Steckbrett Daten an das Funkmodul senden will, muss ein Spannungsteiler verwendet werden (Abbildung 2).

Bild 2: Schaltplan des Test-Steckbretts

Abbildung 2: Schaltplan des Test-Steckbretts, den Sourcecode gibts hier

Der Quellcode enthält eine bidirektionale Funkbrücke. Das heißt, dass wenn ein Zeichen auf der Sendeseite über die seriellen Schnittstelle eingegeben wird, dann wird es auf der Empfangsseite über die serielle Schnittstelle ausgegeben. Zur Sicherung der Datenübertragung sind Prüfsummen und Paket-Zähler vorhanden, die einen Verlust eines Paketes oder eine fehlerhafte Übertragung automatisch korrigieren.
3.1.4 Übertragungsprotokoll

Wollen wir den Roboter durchs Zimmer bewegen, müssen ihm entsprechende Kommandos zum fahren, stoppen und Auslesen der Sensordaten übermittelt werden. Es muss eine einheitliche Grundlage zum Senden und Empfangen von Kommandos geschaffen werden. Dazu wird ein Übertragungsprotokoll benötigt. Ein paar Überlegungen sollten wir zuvor anstellen. Zum einen sollten unsere Sende- Daten so klein, wie möglich sein. Zum anderen muss genügend Flexibilität vorhanden sein, um das Protokoll erweitern oder ändern zu können. Außerdem sollten die Daten als ASCII-Werte (also Zeichen) gesendet werden können.

 

Wir beginnen mit ein paar Gedankenspielen. Als erstes Zeichen definieren wir den Befehl, den der Roboter ausführen soll, z. B. „1“ fahre gerade aus oder „2“ fahre eine Kurve. Für diese Befehle brauchen wir Parameter. Beim geradeausfahren wären das: Schrittmotor Halb- oder Vollschritte; vorwärts oder rückwärts; Geschwindigkeit und zu fahrende Zentimeter. Bei einer Kurvenfahrt werden die Parameter: Schrittmotor Halb- oder Vollschritte; links- oder rechts herum; Geschwindigkeit und zu fahrender Winkel benötigt. Jetzt denken wir weiter. Allzuviele Befehle werden wir nicht brauchen. Mit 256 Möglichkeiten sind wir fürs Erste versorgt. Dafür verwenden wir den Datentyp char (8 Bit), der Werte zwischen 0 und 255 annehmen kann. Bei den Parametern wird die Eingrenzung schwierig. Es können beliebig viele Parameter kommen (ausgehend von zukünftigen Erweiterungen) und die Parameter können beliebig groß oder klein sein. Die Zahl unendlich lässt sich nur schlecht in einem Mikrocontroller-Programm abbilden, deshalb müssen wir auch hier ein paar Grenzen festlegen. Gehen wir davon aus, dass der Parameter für die Anzahl der zu fahrenden Zentimeter am Größten wird. Wenn wir den Datentyp unsigned int (16 Bit) verwenden, steht uns ein Zahlenbereich von 0 bis 65535 zur Verfügung. Damit kann unserer Roboter minimal einen Zentimeter und maximal 655 Meter fahren. Das sollte selbst für die größte Industriehalle ausreichen. Optional können wir den Roboter auch so lange fahren lassen, bis er ein Hindernis erkennt, indem wir ihn als Parameter eine „0“ senden. Jetzt sollten wir uns Gedanken machen, durch was wir das Kommando und die Parameter von einander trennen. Eigentlich spielt das Trennzeichen keine wirklich entscheidende Rolle. Wir verwenden Kommata „,“.  Fehlt noch das Endezeichen. Schließlich müssen wir wissen, wann der Befehl zu Ende ist und wann ein neuer Befehl beginnt. Dafür nehmen wir den Zeilenumbruch – also das Carrage Return „\r“. Wir haben einen ersten ASCII Befehl definiert, den wir mit einem einfachen Terminal-Programm (z.B. Hyperterm oder Putty) auf dem PC eingeben können. Zusammengefasst ist das Beispiel eines Sendekommandos für den Roboter zum geradeausfahren von 250cm in Abbildung 3.

Bild 3: Beispiel eines Sendekommandos
Abbildung 3: Beispiel eines Sendekommandos (auf das abschließende Return wurde verzichtet)

Jetzt wissen wir zwar, wie ein Sendekommando aufgebaut ist, aber unserer Roboter kann noch nichts damit anfangen. Auf der Empfängerseite benötigen wir einen Parser. Der Parser muss am Zeilenanfang beginnen, jedes nachfolgende Zeichen bis zum Komma „,“ oder Zeilenumbruch „\r“ sammeln und den gesammelten Puffer vom ASCII ins Ganzzahlenformat wandeln. Nach dem Parsen wird der Befehl ausgeführt und die Parameter übergeben. Doch bevor der Parser den Text parsen kann, werden wir die empfangenen Bytes in einem Empfangspuffer sammeln. Das geschieht über eine Interrupt-Funktion, die wir idealerweise in einer eigenen Datei „UART.c“ und „UART.h“ schreiben. Nachfolgend die Auszüge aus den wichtigsten Funktionen:

in der Robo_Mega8.c-Datei main():

[…]

init_uart( 19200 );                                           //wir initialisieren den UART für die serielle Kommunikation mit
//dem Funkmodul als Baudrate wird 19200 Baud verwendet

sei();                                                                  //mit dem Aktivieren der globalen Interrupts schalten wir alle
//Interrupts „scharf“

[…]

in der UART.c-Datei:

[…]

#define REC_BUF_SIZE 50                    //Größe des Empfangspuffers

typedef struct
{
unsigned receive_started:1;               //UART Empfang gestartet
unsigned receive_finished:1;             //UART Empfang eines kompletten Kommandos beendet
unsigned receive_overflow:1;             //Empfangspufferüberlauf
} s_usart;

s_usart usart;

unsigned char rec_buf[REC_BUF_SIZE];        //UART Empfangspuffer
unsigned char rec_ptr = 0;                            //Zeiger für den Empfangspuffer
s_usart usart;

/** \brief USART interrupt vector function
*
*/
ISR (USART_RXC_vect)                             //Sobald ein Zeichen über UART empfangen wurde
{                                                                       //springen wir in diese Funktion
if(rec_ptr == REC_BUF_SIZE)               //prüfen, ob Empfangspuffer voll?
{
usart.receive_overflow = 1;                //wenn ja, dann setze Überlauf-Flag
rec_ptr = 0;                                            //und beginne den Empfangspuffer wieder bei 0 zu füllen
}
else                                                            //Empfangspuffer noch nicht voll, dann
{
usart.receive_overflow = 0;
rec_buf[rec_ptr] = UDR;                     //speichere das empfangene Zeichen im Empfangspuffer
UDR = rec_buf[rec_ptr];                     //und sende das empfangene Zeichen zurück -> Echo
if(rec_buf[rec_ptr] == ‚\r‘)                    //das Ende-Kennzeichen wurde empfangen, ein vollständiges
{                                                              //Kommando liegt im Empfangspuffer
usart.receive_finished = 1;          //setze das „Empfangen beendet“ Flag
rec_buf[rec_ptr] = ‚\0‘;                    //terminiere das Ende des Empfangspuffers
}
else                            usart.receive_finished = 0;
}

rec_ptr++;                                                  //Erhöhe Empfangszeiger
}

[…]

Für den Parser werden wir auf offenen Quellcode zurückgreifen und die Funktionen zum Auffinden des Komma-Zeichens in einem Puffer und finden des nächsten Ziffernblocks aus dem Internet benutzen.

Diese Funktion habe ich auf einer Apple-Seite gefunden:
/*
* Find the first occurrence in s1 of a character in s2 (excluding NUL).
*/
char *strpbrk(const char *s1, const char *s2)
{
register const char *scanp;
register int c, sc;

while ((c = *s1++) != 0) {
for (scanp = s2; (sc = *scanp++) != 0;)
if (sc == c)
return ((char *)(s1 – 1));
}
return (NULL);
}
Diese Funktion habe ich im mikrocontroller.net Forum gefunden und leicht auf unsere Anforderungen angepasst:
// variant of strtok_r() which finds empty tokens as well (sequences
// of delimiter characters will not be considered to be a single
// delimiter).
// If the start string contains too few tokens for the
// strtok_r_empty()-calls the excessive calls return empty strings.
unsigned int strtok_r_empty( char *p_str, char **pp_save )
{
// *pp_save will save the pointer to the start of the next
// unprocessed token or NULL if there are no more tokens

char *p_start = ( p_str ? p_str : *pp_save );
if( p_start )
{
// look for start of next token, or NULL

*pp_save = strpbrk( p_start, „,“ );

if( *pp_save )
{
// delimiter found
**pp_save = ‚\0‘; // terminate current token
++*pp_save;       // skip delimiter on next call
}
}

return atoi(p_start); // return current token or NULL
}
Was stellen wir mit den oben geschriebenen Funktionen an? An der Stelle, an der ein kompletter Befehl empfangen wurde, werden wir den Parser aufrufen:
[…]
unsigned char rec_buf[50];                  //unser Empfangspuffer, der den Befehl enthält
char *buf_ptr = NULL;                           //Pointer wird für das Parsen des Empfangspuffers benötigt
uint16_t param[4];                                  //die geparsten Kommando-Parameter
[…]

if(usart.receive_finished)                      //beginne den Empfangspuffer auszuwerten, wenn ein vollständiges
//Paket empfangen wurde
{
buf_ptr = NULL;                                  //wir fangen an der ersten Stelle des Empfangspuffers zu parsen an
cmd = strtok_r_empty(rec_buf, &buf_ptr);//als erster gültiger Parameter steht das Kommando im
//Empfangspuffer
for(unsigned char c=0; c<4; c++)    param[c] = strtok_r_empty(NULL, &buf_ptr);    //gefolgt von den ggf.
//notwendigen Aufrufparametern

switch(cmd)
{
case 1:        //go strait on – example: „1,1,0,200“
{
go_straight_on(        param[0],     //steps_full_half
param[1],     //forth_back
param[2],     //spd
param[3]);    //cm
break;
}
[…]

Beim ersten Aufruf von strtok_r_empty() übergeben wir den Zeiger auf den Puffer und einen Zeiger, an welcher Stelle der Parser beginnen soll, zu arbeiten (parsen). Beim Durchlaufen der nachfolgenden Schleife muss der Puffer nicht mehr angegeben werden. Es wird nur noch der Zeiger auf die zu parsende Stelle im Puffer benötigt, da der Zeiger innerhalb der Funktion geändert wird.

Auf Dauer ist es etwas lästig, jedes Kommando von Hand in ein Terminal-Programm einzugeben. Deshalb habe ich mich entschieden, eine komfortable Oberfläche zu programmieren, die mir diese Arbeit erleichtert (siehe Kapitel 6).

Wird fortgesetzt…

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.