Browse Month: Februar 2021

Wii Nunchuk Controller als Steuerelement am Arduino

In Bearbeitung …

Der Nunchuck Controller ist ein Eingabe-Instrument für die bekannte Wii-Spieleconsole. Er verfügt über 2 Druckknöpfe, einen Joystick und einen eingebauten 3-Achsen-Beschleunigungssensor. Dieser Controller ist aufgrund der hohen Verbreitung für wenig Geld erhältlich, sehr robust und verfügt über eine gut dokumentierte I2C-Schnittstelle, die es erlaubt, diesen mit dem Arduino zu verbinden. Somit kann der Controller für die verschiedensten Steuerungsaufgaben zweckentfremdet werden.

Älterer Wii-Nunchuk-Controller

Da viele Maker diesen Controller bereits in Ihren Projekten verwenden, finden sich viele Programm-Bibliotheken im Internet, die eine einfache Verwendung erlauben. Die hier verwendete Bibliothek stellt folgende Funktionen bereit:

void nunchuk_setpowerpins()void nunchuk_init()
int nunchuk_get_data()void nunchuk_calibrate_joy()
inline unsigned int nunchuk_zbutton()inline unsigned int nunchuk_cbutton()
inline int nunchuk_joy_x()inline uint16_t nunchuk_accelx()
inline int nunchuk_cjoy_x()inline uint16_t nunchuk_accely()
inline int nunchuk_cjoy_y()inline uint16_t nunchuk_accelz()
inline int nunchuk_joyangle()void nunchuk_calibrate_accelxy()
inline int nunchuk_rollangle()void nunchuk_calibrate_accelz()
inline int nunchuk_pitchangle()
Funktionen der wiinunchuk-Bibliothek

Die Bibliothek bereitet die Daten vom Controller so auf, dass diese über eine Anweisung ausgelesen werden können. Insgesamt benötigt man einen Speicherpuffer mit 75 Zeichen, in die sämtliche Sensorwerte geschrieben werden. Wie das geht, zeigt das erste Beispiel-Programm, dass nur die Joystick-Position und die Zustände der beiden Schalter C und Z ausliest.

#include <Wire.h>
#include <wiinunchuck.h>

void setup() { 
  Serial.begin(9600);
  nunchuk_init(); // Nunchuk initialisieren, Joystick auf Mittelposition
  delay(100);
  nunchuk_calibrate_joy();
  delay(100);
}
 
void loop() {
  nunchuk_get_data();   // Daten (6 Byte) vom Nunchuk Controller auslesen 
  char buffer[25];
  sprintf(buffer, "X:%3d Y:%3d Z:%1d C:%1d", 
          nunchuk_cjoy_x(), nunchuk_cjoy_y(), 
          nunchuk_zbutton(), nunchuk_cbutton() );
  Serial.println(buffer);   // Zusammengesetzten String an seriellen Monitor schicken
  delay(50);
}

Der Nunchuk wird über ein einfaches I²C Protokoll angesprochen. Mit Hilfe eines Befehles können wir alle Daten aus dem Nunchuk lesen. Die Daten kommen dabei als Paket von 6 Bytes in folgendem Format:

Byte 1: Joystick – X
Byte 2: Joystick – Y
Byte 3: ACC X (9..2)
Byte 4: ACC Y (9..2)
Byte 5: ACC Z (9..2)
Byte 6: ist aufgeteilt in Bits
Bit 0: Z-Taste
Bit 1: C-Taste
Bit 2,3: fehlenden Bits 0,1 vom ACC X
Bit 4,5: fehlenden Bits 0,1 vom ACC Y
Bit 6,7: fehlenden Bits 0,1 vom ACC Z

Um die Daten mit einem Arduino verarbeiten zu können, muss der Controller natürlich mit dem Arduino verbunden werden. Es gibt sechs Steckplätze, wobei nur die vier Äusseren belegt sind.

Wii-Steckerbelegung, die Farben können allerdings variieren.

Es gibt Adapter, aber aufgrund der Tatsache, dass ich meine nicht mehr anderswo benötige, habe ich den Stecker einfach abgeschnitten und Arduino-kompatibel mit Steckbrücken versehen.

Zeiger Programmierung

In der Sprache C existiert ein Konstrukt das als Zeiger oder Pointer bezeichnet wird. Zeiger repräsentieren eine besondere Art von Variablen. Ihr Inhalt ist eigentlich nebensächlich. Viel interessanter ist, dass sie auf andere Variablen zeigen können. Man betrachtet also nicht die Zeigervariable selbst, sondern über die Zeigervariable auf den Inhalt einer anderen Variablen.

Diese Indirektion ist nicht ganz leicht zu verstehen und bietet auch bei erfahrenen Programmieren immer wieder Anlass zur Verwirrung. Aus diesem Grund soll im folgenden Beitrag die Funktionsweise eines Zeigers erklärt werden.

Variablen

Eine Variable wird zum speichern von Informationen verwendet. Sie muss üblicherweise deklariert werden. D.h. man ordnet ihr einen eindeutigen Namen und einen Datentyp zu. Diese Informationen werden vom Compiler dann in einer sogenannten Symboltabelle abgelegt und verwaltet.

Nachdem man beispielsweise folgende Deklaration gemacht hat:

int myVar;

Wird in der Symboltabelle folgender Eintrag vorgenommen:

Bei diesem Vorgang fordert der Compiler vom Speichermanagement des Systems eine Adresse an, wo die damit zusammenhängenden Informationen dann gespeichert werden. Diese Speicheradresse wird als I-Wert bezeichnet, und gibt an, wo sich die Variable im Speicher befindet also (I-Wert = Standort).

Wenn wir einer Variablen nun einen konkreten Wert zuweisen, navigieren wir direkt zum Speicherort der Variablen im Speicher (dem I-Wert) und aktualisieren den Speicher an dieser Adresse mit dem neuen Wert. Die Daten, die tatsächlich im Speicher gespeichert sind, werden als R-Wert (R-Wert = Registerwert ) bezeichnet. Die Anzahl der dazu benötigten Speicherplätze hängt massgeblich vom Typ der Variablen ab.

Zeiger

Nachdem wir uns nun mit den Variablen und ihrer Funktionsweise vertraut sind, können wir uns dem Zeiger-Konstrukt zu wenden. Einfach ausgedrückt ist ein Zeiger nichts anderes als eine Variable, die auf die Speicheradresse einer anderen Variablen verweist. 

Unter Verwendung der soeben erlernten Terminologie ist ein Zeiger somit eine Variable, deren R-Wert der I-Wert einer anderen Variablen ist.

Die Deklaration einer Zeiger-Variable ist recht einfach. Um eine Zeigervariable zu definieren, wird zunächst der Typ angegeben, auf den der Zeiger zugreifen kann. Es folgt ein Stern und dann der Name der Variablen:

int *myPointer;

Der Typbezeichner (in diesem Fall int) muss mit dem Datentyp der Variablen übereinstimmen, mit der der Zeiger verwendet werden soll. Das Sternchen zeigt dem Compiler an, dass myPointer ein Zeiger ist. Da Leerzeichen in C keine Rolle spielen, kann das Sternchen an einer beliebigen Stelle zwischen dem Typbezeichner und dem Namen der Zeigervariablen platziert werden, so dass manchmal auch Folgendes angezeigt wird: int * myPointer, int * myPointer usw.

Ein Zeiger, der definiert ist, aber nicht auf irgendetwas zeigt, ist für sich genommen ein ziemlich sinnloser Zeiger. Um auf die Speicheradresse einer anderen Variablen zu verweisen, müssen wir dem Zeiger natürlich die Speicheradresse dieser Variablen zuweisen.

Dazu wird der sogenannte „Adress-of-Operator &“ verwendet. Um unseren neuen Zeiger auf den Speicherort unserer Werttypvariablen myVar zu richten, rufen wir einfach die folgende Anweisung auf:

myPointer = &myVar;

Dies vervollständigt den im vorherigen Diagramm gezeigten Link und wird als Referenzierung bezeichnet . Aus dem gleichen Grund wird der Adressoperator (&) auch als „Referenzierungsoperator“ bezeichnet.

Beispiel mit der Arduino IDE

Machen wir ein Beispiel für das, was wir bisher besprochen haben in der Arduino IDE :

void setup() {
  Serial.begin(9600);
  
  int myVar = 10;  // Initialize a variable.
  
  Serial.print("myVar's lvalue: ");
  Serial.println((long) &myVar, DEC);  // Grab myVar's lvalue
  Serial.print("myVar's rvalue: ");
  Serial.println(myVar, DEC);
  Serial.println();
  
  int *myPointer;   // Declare your pointer.
  myPointer = &myVar; //Assign myVar's memory address to pointer.
  
  Serial.print("myPointer's lvalue: ");
  Serial.println((long) &myPointer, DEC);  //myPointer's lvalue
  Serial.print("myPointer's rvalue: ");
  Serial.println((long) myPointer, DEC);  //myPointer's rvalue
}

void loop() {
}

Als Ergebnis erhalten wir auf dem Monitor folgende Ausgabe:

Monitor Ausgabe des Zeiger-Beispiel Programms

De-Referenzierung

Wir haben gerade gesehen, dass ein Zeiger auf eine Stelle im Speicher verweisen kann, indem er diesem Zeiger die Speicheradresse einer Variablen mit dem Referenzoperator (&) zuweist.

Wir können noch einen Schritt weiter gehen und den an dieser Speicheradresse gespeicherten tatsächlichen Wert erhalten, indem wir den Zeiger dereferenzieren. Dies wird auch als Indirektion bezeichnet und erfolgt über den Indirektionsoperator (*) mit Ihrem Zeiger.

*myPointer = 5; // Go to memory addressed stored in myPointer's rvalue (myVar's lvalue)
                //  and place the value 5 in that memory address.
De-Referenzierung am Beispiel

Wir erweitern das obige Programm und erhalten folgendes Ergebnis:

void setup() {
  Serial.begin(9600);
  int myVar = 10;
  
  Serial.print("myVar's lvalue: ");
  Serial.println((long) &myVar, DEC);
  Serial.print("myVar's rvalue: ");
  Serial.println(myVar, DEC);
  Serial.println();
  
  int *myPointer;
  myPointer = &myVar;
  
  Serial.print("myPointer's lvalue: ");
  Serial.println((long) &myPointer, DEC);
  Serial.print("myPointer's rvalue: ");
  Serial.println((long) myPointer, DEC);
  Serial.println();

  *myPointer = 5;  //THIS IS OUR DEREFRENCING ADDITION.
  Serial.println("-----------------------");
  Serial.println("Updating *myPointer = 5");
  Serial.println();

  Serial.print("myPointer's lvalue: ");
  Serial.println((long) &myPointer, DEC);
  Serial.print("myPointer's rvalue: ");
  Serial.println((long) myPointer, DEC);
  Serial.println();

  Serial.print("myVar's lvalue: ");
  Serial.println((long) &myVar, DEC);
  Serial.print("myVar's rvalue: ");
  Serial.println(myVar, DEC);
  Serial.println();
}
void loop() {
}

Newton Iteration

Das nach Isaak Newton benannte Verfahren, ist in der Mathematik ein häufig verwendeter Approximations-Algorithmus zur numerischen Lösung von nichtlinearen Gleichungen.

Die grundlegende Idee dieses Verfahrens ist, die Funktion in einem Ausgangspunkt zu linearisieren, d. h. ihre Tangente zu bestimmen, und die Nullstelle der Tangente als verbesserte Näherung der Nullstelle der Funktion zu verwenden.

Die erhaltene Näherung dient dann als Ausgangspunkt für einen weiteren Verbesserungsschritt. Diese Iteration erfolgt solange, bis die Änderung in der Näherungslösung eine festgesetzte Schranke unterschritten hat.