Montag, 19. August 2013

Kleiner Vergleich zwischen Netduino Plus 1 und Netduino Plus 2

NetduinoPlus1u2
Netduino Plus 1 (Links) und Netduino Plus 2 (rechts)

Seit kurzem zählt zu meinem Bastelsortiment der Netduino Plus 2, der mit mehr Takt deutlich schneller rechnet als der Netduino Plus 1. Die Investition hat sich gelohnt und bringt mein Projekt weiter voran.

Kommen wir zunächst zu den grundlegenden technischen Daten. Auch auf der Seite Netduino.com/Hardware/ zu finden.

  Netduino Plus 1 Netduino Plus 2
Speed 48 MHz 168 MHz
Code Storage 64 KB 384 KB
RAM 42 KB 100+ KB
GPIO 20 22
UART 2 4
PWM 4 6
SPI 1 1
I²C 1 1

Klar ist, dass der neue schneller sein muss, allerdings habe ich mich gefragt, wie viel mehr ich verarbeiten kann. Nun, mein Beispiel zeigt eine kleine Rechenaufgabe, in der die Zeit gestoppt wird und man nach 100 Durchläufen den Durchschnittswert erhält.

int counter = 0;
long average = 0;

while (true)
{
    Stopwatch.GetElapsedMicrosecounds();

    for (int i = 0; i < 100; i++)
    {
        float f = (float)System.Math.Pow(1234, 4321);

        double d = System.Math.Sqrt((double)f);

        Int64 i64 = (Int64)(d * f);
    }

    average = (average + Stopwatch.GetElapsedMicrosecounds()) / 2;

    if (counter > 99)
    {
        Debug.Print("Average Time: " + average.ToString() + " Microsecounds");
        counter = 0;
    }
    else
    {
        counter++;
    }
}

Der Programmablauf in der Main() Methode

Die Aufgabe ist eine eher nicht optimal ausgelegte Rechnung, was aber für den Testlauf absichtlich so gestaltet ist. Die Unterschiede bei der Zeitmessung können sich sehen lassen.

Netduino Plus 1 ca. 43182 bis 43232 Mikrosekunden
Netduino Plus 2 ca. 7902 bis 7927 Mikrosekunden

Bei weiteren Tests, die ich bereits durchgeführt habe, funktionierte z.B. die I²C Verbindung deutlich schneller. Mit dem Sensor MPU6050 las der NP+ zuvor nur die Sensor Daten in 3ms, jetzt kann ich noch einen künstlichen Horizont und 4 PIDs berechnen und der NP+2 benötigt für das Ganze gerade mal 2ms.

Ein paar Dinge haben sich geändert. Der I²C Bus liegt jetzt auf Pin 15 und 16, wenn man das so nennen kann. Also quergegenüber auf dem Board. Und wenn man UART 1 bzw. COM1 verwenden will, ist dieser ebenfalls dort. Somit fällt leider eine serielle Verbindung weg, was jedoch nicht weiter schlimm ist, da es 3 weitere davon gibt. Für die Netzwerk Buchse RJ45 ist nur noch eine LED als Status zusehen, was allerdings auch weiterhin ausreicht.

Die Beispiel Solution mit .NET Framework 4.2 für NP+ und NP+2 zum Downloaden.

Donnerstag, 1. August 2013

HMC5883L Sensor auslesen mit dem Netduino

Netduino_HMC5883L_2
HMC5883L Magnet Sensor auf dem Breadboard Shield und PullUp-Widerständen.

Was mit dem Arduino geht, geht auch mit Netduino. Auch hier konnte ich ein funktionales Beispiel für den 3 Achsen Magnet Sensor HMC5883L als Klasse schreiben.

public class HMC5883L
{
    /// Klasse für die I²C Verbindung
    private I2CDevice _i2CDevice;
    /// Die Variabeln zum Speichern der Ergebnisse nach der Messung.
    private byte[] _Data = new byte[6];
    /// Ruft die Achse X ab oder legt diese fest.
    public int AxisX { get; set; }
    /// Ruft die Achse Y ab oder legt diese fest.
    public int AxisY { get; set; }
    /// Ruft die Achse Z ab oder legt diese fest.
    public int AxisZ { get; set; }
    /// Der Konstruktor Initialisiert die Verbindung her und
    /// stellt den Sensor mit einer Standard Konfiguration ein.
    public HMC5883L()
    {
       // I²C Bus verbindung herstellen mit 100kHz
        _i2CDevice = new I2CDevice(new I2CDevice.Configuration(0x1E, 100));
        // Operating Mode (0x02):
        // Continuous-Measurement Mode (0x00)
        StatusMessage(Write(new byte[] { 0x02, 0x00 }));

        // Die Konfiguration besteht in zwei Abschnitten.
        // Der erste Byte bestimmt, wie viel Proben pro Messung vorgenommen
        // werden (Default = 1) sollen. In welcher Bit Rate an die Ausgänge
        // geschrieben wird (Default = 15Hz) und den Messmodus bestimmt
        // die Vorspannung (Default = Normal)
        // Standard Einstellungen, siehe Datenblatt für weitere Einstellungen.

        StatusMessage(Write(new byte[] { 0x00, 0x10 }));

        // Standard Skalierung, siehe ggf. Datenblatt
        // +- 1.3 Ga, 1090 Gain(LSb/Gauss)
        StatusMessage(Write(new byte[] { 0x01, 0x20 }));
    }
    /// Liest die Sensor Messungen ein und schreibt diese in die Properties
    public void ReadMagnetic()
    {
        // Sendet das Byte für die erste Achse
        Write(new byte[] { 0x03 });
        // Nur wenn das Einlesen erfolgreich war.
        _Data[0] = 0x03;
        if (Read(_Data) != 0)
        {
            AxisX = (_Data[0] << 8) | _Data[1];
            AxisY = (_Data[2] << 8) | _Data[3];
            AxisZ = (_Data[4] << 8) | _Data[5];
        }
        else
        {
            Debug.Print("Fehler beim lesen!");
        }
    }
    /// Sendet das Byte Array zum Modul
    private int Write(byte[] buffer)
    {
        I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[]
        {
            I2CDevice.CreateWriteTransaction(buffer)
        };
        return _i2CDevice.Execute(transactions, 1000);
    }
    /// Liest mit den Byte Array die Daten vom Modul
    private int Read(byte[] buffer)
    {
        I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[]
        {
            I2CDevice.CreateWriteTransaction(new byte[] { 0x03 }),
            I2CDevice.CreateReadTransaction(buffer)
        };
        return _i2CDevice.Execute(transactions, 1000);
    }
    /// Gibt mit dem Ergebnis den Status des Vorgangs
    /// über die Ausgabe(Output) in Visual Studio wieder.

    private void StatusMessage(int result)
    {
       if (result == 0)
        {
            Debug.Print("Status: Fehler beim Senden oder Empfangen");
        }
        else
        {
            Debug.Print("Status: OK");
        }
    }
}

Anschließend muss diese Klasse in der Program.cs instanziiert werden und eine Schleife, die fortlaufend die Messungen abfragt, eingetragen werden.

public static void Main()
{
    // Sensor Initialisieren
    HMC5883L sensor = new HMC5883L();

    while (true)
    {
        // Einlesen
        sensor.ReadMagnetic();
        // Ergebnisse ausgeben
        Debug.Print("Raw Measurement: X " + sensor.AxisX +
            "\tY" + sensor.AxisY +
            "\tZ" + sensor.AxisZ);

        // Kurz warten
        Thread.Sleep(100);
    }
}

Was die Schaltung angeht, ist diese identisch mit der für den  Arduino, allerdings mit einer Ausnahme.

Netduino_HMC5883L_Steckplatine
Netduino –> HMC5883L
3,3V      –> VCC
GND     –> GND
A4         –> SDA
A5         –> SCL

HINWEIS: Wenn beim Ausgeben nur 0 heraus kommt oder generell der Status nicht in Ordnung ist, dann kann es an fehlenden PullUp Widerständen liegen. Es gibt einige Module ohne diese, so wie bei mir.

Netduino_HMC5883L_FehlendePullUpResistor
2 Fehlende SMD Widerstände, die eigentlich die offene Schaltung schließen sollten.

Mit dem Ergänzen der zwei fehlenden 2,2kOhm Widerstände ist dies schnell angepasst.

Netduino_HMC5883L_Steckplatine_PullUp
Netduino <–> HMC5883L
3,3V      <–> VCC
GND     <–> GND
A4         <–> SDA
A5         <–> SCL

Netduino <–> Widerstand 2,2kOhm <–> HMC5883L
3,3V     <–> Widerstand <–> SDA
3,3V     <–> Widerstand <–> SCL

Am besten wenn man hier zwei SMD Widerstände verbaut oder gleich einen Sensor kauft, worauf man später nicht mehr achten muss. Zwinkerndes Smiley

Wie immer die Solution für .NET Micro Framework 4.2: Download

HMC5883L Sensor ohne (externe) Library auslesen mit dem Arduino

HMC5883L_auf_Breadboard
HMC5883 Magnet Sensor auf dem Breadboard Shield.

Letztes Jahr habe ich bereits darüber geschrieben, allerdings keinen Code dazu gepostet. Ich habe nur beschrieben, was mit der Library verändert werden musste, damit diese funktioniert. Diesmal hatte ich Zeit, um mich etwas näher damit zu befassen und konnte nun endlich ein funktionales Beispiel schreiben. Um den Code fürs erste übersichtlich zu halten, habe ich das Skalieren und das Berechnen nach Norden ausgelassen.

// Wird für die I²C Verbindung verwendet.
#include <Wire.h>
// Adresse des Sensors
#define sensor_address 0x1E
// Variabelen die für den Lese Vorgang benötigt werden
byte data[6];
int axisX;
int axisY;
int axisZ;
// Konfiguration der Seriellen Verbindung und Initialisierung des Sensors.
void setup()
{
  Serial.begin(9600);
  Serial.println("Init Sensor");
  // I²C Verbindung starten.
  Wire.begin();
  // Operating Mode (0x02):
  // Continuous-Measurement Mode (0x00)
  SetConfiguration(0x02, 0x00);
  // Standard Skalierung, siehe ggf. Datenblatt
  // +- 1.3 Ga, 1090 Gain(LSb/Gauss)
  SetConfiguration(0x01, 0x20);
}
// Hauptschleife
void loop()
{
  // Rohdaten abrufen
  ReadMagnetic();
  // Zeige Roh Messwerte
  Serial.print("Raw Measurement: X ");
  Serial.print(axisX, DEC);
  Serial.print("\tY ");
  Serial.print(axisY, DEC);
  Serial.print("\tZ ");
  Serial.println(axisZ, DEC);
}
// Liest die Messwerte aus dem Magnet Sensor
void ReadMagnetic()
{
  Wire.beginTransmission(sensor_address);
  Wire.write(0x03);
  Wire.endTransmission();
  // Die Array Größe, die hier zu erwarten ist sind 6 Bytes.
  Wire.requestFrom(sensor_address, 6);
  // Die einzelnen Bytes werden nun gelesen
  // und in das Byte Array geschrieben

  for(int i = 0; i < 6; i++)
  {
    data[i] = Wire.read();
  }
  // 2 Bytes bilden zusammen einen Wert von 0 bis 65536.
  // Durch Byte Shifting lassen sich die Bytes zu einem Integer Wert zusammen fügen.

  axisX = (data[0] << 8) | data[1];
  axisY = (data[2] << 8) | data[3];
  axisZ = (data[4] << 8) | data[5];
}
// Legt die Konfiguration fest, mit dem Einstellungs Byte und
// dem Byte für die Einstellungsbereich.
void SetConfiguration(byte reg, byte setting)
{
  Wire.beginTransmission(sensor_address);
  Wire.write(reg);
  Wire.write(setting);
  Wire.endTransmission();
  Wire.requestFrom(sensor_address, 1);
  byte result = Wire.read();
 
// Das Ergebnis aus dem Senden gibt einen Wert zurück. Ist der Wert 0,
  //
war das senden fehlerhaft.
  Serial.print("Status: ");
  if(result != 0)
  {
    Serial.println("OK");
  }
  else
  {
    Serial.println("Failur");
  }
}

Funktionaler Programmcode für das Einlesen der der Rohdaten.

Eigentlich hatte ich vor, noch die Himmelsrichtung zu bestimmen, allerdings habe ich eine Webseite gefunden, die das bereits sehr gut erklärt (Love Elektronics - HMC5883L Compass Tutorial with Arduino Library).

Zuletzt noch ein Schaltbild, mit dem Arduino.

Arduino_HMC5883L_2_Steckplatine
Arduino –> HMC5883L
3,3V      –> VCC
GND     –> GND
A4         –> SDA
A5         –> SCL

Dienstag, 23. Juli 2013

Kreisgeschwindigkeit aus dem Sensor umrechnen (Die Zweite)

image
Von mir entwickeltes Programm zum darstellen eines künstlichen Horizontes und ein Würfel, das die Bewegungen des Sensors wieder gibt.

Im letzten Post zeigte ich eine relativ ungenaue Lösung für das Umrechnen der Winkellage. Grund hierfür ist, dass der Zeitabstand nicht verwendet wird. Als Provisorium reicht es allerdings aus.

Nun, welche Formel wird stattdessen benötigt?

image
Kreisgeschwindigkeit = Drehwinkel / Zeitspanne
image steht für die Winkelgeschwindigkeit
image steht für den Drehwinkel
image steht für die vergangene Zeit

Im nächsten Schritt muss ich die Formel umstellen, um später an das Ergebnis des Drehwinkels heran zu kommen. Der Sensor gibt pro Abfrage die Winkelgeschwindigkeit, und die Zeit wird zwischen der aktuellen Abfrage und der letzten Abfrage bestimmt.

image

Die verwendeten Werte nehme ich wie zuletzt aus dem Sensor MPU6050 mit den entsprechenden Einstellungen. Der Wert 32768 ist der Maximale Einheiten Wert der in eine Richtung gemessen werden kann. Die vergangene Zeit wird durch 1000ms dividiert, weil die Formel eigentlich für Sekunden ausgelegt ist. Die 2000.0 kommt aus der Einstellung für die Gyroskop Empfindlichkeit, also 2000°/s.

image

Daraus kann nun der Programmcode gebildet werden.

double winkel = ((((measurement - _Offset) / 32768.0) *
                                  ((double)timeelapsed / 1000.0))) * 2000.0;
measurement = Messergebnis
_Offset = Abweichungswert das den Messwert an die eigentliche Lage bringt
timeelapsed = vergangene Zeit zwischen der Abfrage und der letzten

Die Aufstellung ist für den Netduino relativ lang und sollte daher gekürzt werden. Am besten geht das auf einem Blatt Papier.

Formelumstellung
Überflüssiges kürzen

Somit sieht dann der Programmcode schon etwas schlanker aus.

double winkel = ((measurement - _Offset) * (double)timeelapsed) / 16384.0;

Der errechnete Wert zeigt den zurückgelegten Winkel. Damit daraus eine Lagebestimmung wird, muss daraus eine Integralrechnung werden, sowie bereits in meinem letzten Blogeintrag erwähnt. Damit sieht nun die Klasse folgendermaßen aus:

public class GyroscopeAxis
{
    /// Derzeitige Winkellage
    private double _Angle = 0;

    /// Ruft ab, ob der Offset gesetzt wurde oder legt diesen fest.
    private bool _IsOffset_Done = false;

    /// Ruft den Offset ab oder legt diesen fest.
    /// Wird benötigt, um die Abweichung runter zurechnen.
    private double _Offset = 0;

    /// Nimmt den aktuellen Beschleunigungswert und summiert den auf den Winkelwert
    public double UpdateAngle(double measurement, ref int timeelapsed)
    { 
        // Offsetwert festlegen, wenn nicht gesetzt.
        if (!_IsOffset_Done)
        {
            _Offset = measurement;
            _IsOffset_Done = true;
        }

       // Aktuellen Wert summieren und den Offset Wert Abziehen
        _Angle -= ((measurement - _Offset) * (double)timeelapsed) / 16384.0;

        return _Angle;
    }
}

Die Klasse für das Bestimmen der Winkellage

Anders als in meinem vorigen Blog Post habe ich die Abweichung (Offset) mit eingebracht. Dieser Wert wird beim Kalibrieren ermittelt, um die waagerechte Haltung anzugleichen. Was allerdings noch bleibt und auch nicht zu verhindern ist, sind weiterhin die Messfehler, die nur durch das Komplementieren mit der Beschleunigungsdaten gefiltert werden können.

Donnerstag, 18. Juli 2013

Kreisgeschwindigkeit aus dem Sensor umrechnen

image
Von mir entwickeltes Programm zum darstellen eines künstlichen Horizontes.

Neben Beschleunigung existiert die Kreisgeschwindigkeit und kann durch ein Gyroskop gemessen werden. Um nun daraus einen Winkel zu ermitteln, ist eine einfache Rechnung erforderlich.

(Aktueller Winkel) = (Aktueller Winkel) + (Messwert)

Sieht also nicht sonderlich wild aus und genauso wenig schwierig ist dies in ein Programm zu schreiben.

private double _Angle = 0;

public double UpdateAngle(double measurement)
{
    _Angle += measurement;
    return _Angle;
}

Von Gyroskop zu Gyroskop sind die Ergebnisse unterschiedlich. Wie immer nehme ich den MPU6050 Sensor, der einen Messbereich von 0 bis 65535 wiedergibt. Um auch hier negative Werte zu erhalten, ist es nötigt, die Werte um 32768 zu verschieben. Sowie ich das im Blog Post mit dem Beschleunigungssensor Ergebnis erledigt habe. Zudem muss noch der Wert skaliert werden, der davon abhängig ist, auf welche Messgeschwindigkeit eingestellt wurde. Hierzu habe ich eine Tabelle mit Werten zusammengestellt.

250°/s (Messwert) / 8192 (oder 10000)
500°/s (Messwert) / 4096
1000°/s (Messwert) / 2048
2000°/s (Messwert) / 1024
  Leider kann es hier Abweichungen zu meinem Beispiel geben. Hier muss man im nachhinein schauen ob das passt.

Mit dieser Information kann nun die Methode ergänzt werden.

public double UpdateAngle(double measurement)
{
    measurement = measurement / 1024;
    _Angle += measurement;
    return _Angle;
}

Indirekt war das nun der zweite Teil für den künstlichen Horizont. Damit auch dieser Code vollständig funktioniert werden zur Korrektur die Ergebnisse aus dem Beschleunigungssensor benötigt. Der Grund hierfür ist, dass Messfehler entstehen können, die dazu führen, dass sich die Lage nach und nach verschiebt. Das Stichwort an dieser Stelle heißt “Komplementieren”.

Sonntag, 7. Juli 2013

MPU6050 Sensor auslesen mit dem Netduino (Repost)

Quadrocopter_49
Fest verlötet auf meinem Shield.

Anfang des Jahres habe ich bereits darüber geschrieben und dazu auch ein Beispiel Projekt gepostet. Nun möchte ich diesen Artikel neu aufsetzen und etwas mehr auf einzelne Punkte eingehen. Zunächst nochmal der Hinweis, es handelt sich wieder um einen reinen Funktionalen Programm Code und hat daher keine Fehlerbehandlung.

Ein paar Informationen zu dem Sensor

Hersteller InvenSens
Bezeichnung MPU-6000 und MPU-6050
Sensor Achsen 6
Gyroskope Einstellungen +-250°/s, +-500°/s, +-1000°/s, +-2000°/s
Beschleunigungseinstellungen +-2g, +-4g, +-8g, +-16g
Verbindung I²C (der 6000er hat zusätzlich SPI)

Kommen wir nun zum eigentlichen Thema. Ursprünglich gab es von Jeff Rowberg bereits eine fertige Library in C++. Leider ließ sich dieser Code nicht für den Netduino verwenden und daher strebte ich an, eine C# und .NET Micro Framework Variante zu schreiben. Zugegeben, alleine mit dem PDF Dokument hätte ich das nicht verstanden und war froh, die Funktionsweise aus der C++ Variante heraus zu lesen.

Fangen wir mit der Hauptklasse an. Der wichtigste Teil hierbei ist, mit welchen Einstellungen der Sensor Initialisiert wird.

public class MPU6050Sensor
{
    // I²C Klasse für die Verbindung zum Sensor
    private I2CDevice _i2CDevice;

    // Initialisiert den Sensor
    public MPU6050Sensor()
    {
       
// I²C Bus verbindung herstellen
        // Die erste Hex Zahl stellt die Adresse vom Sensor und
        // der Wert 100 steht für die Übertragung in kHz.
        // Wahlweise kann hier auch 400kHz verwendet werden,
        // setzt jedoch eine saubere Verbindung voraus,
        // da Störungen das Ergebnis beeinflussen.
        _i2CDevice = new I2CDevice(new I2CDevice.Configuration(0x68, 100));

        // Sensor Initialisieren
        // Sleep und Reset durchführen
        // Der Hex Wert 0x6B steht für das Power Management 1
        // Der zweite Hex Wert beinhaltet das 'Bit' für einen Reset.
        StatusMessage(Write(new byte[] { 0x6B, 0x80 }));
        Thread.Sleep(10);

        // Sleep beenden und Clock einstelllen
        // Der Hex Wert 0x6B steht für das Power Management 1
        // Der zweite Hex Wert 0x00 legt die 'Clock Select'
        // auf 'Internal 8MHz oscillator'
        // Falls der Temperatur Sensor nicht ausgelesen werden soll,
        // dann kann stattdessen der Wert 0x08 eingetragen werden.
        StatusMessage(Write(new byte[] { 0x6B, 0x00 }));

        // Konfiguration festlegen
        // Diese Einstellungen aktiviert den Tief Pass Filter (DLPF) und
        // wird z.B. verwendet, andere Vibrationen heraus zu filtern.
        // Setting => Acc=5Hz, Delay=19.0ms, Gyro=5Hz, Delay=18.6ms, Fs=1kHz
        StatusMessage(Write(new byte[] { 0x1A, 0x06 }));
    }

    // Sendet das Byte Array zum Modul
   private int Write(byte[] buffer)
    {
        I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[]
        {
            I2CDevice.CreateWriteTransaction(buffer)
        };
        return _i2CDevice.Execute(transactions, 1000);
    }

    // Liest mit den Byte Array die Daten vom Modul
    private int Read(byte[] buffer)
    {
        I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[]
        {
            I2CDevice.CreateReadTransaction(buffer)
        };
        return _i2CDevice.Execute(transactions, 1000);
    }

    // Status Nachrichten wiedergeben
    private void StatusMessage(int result)
    {
        if (result == 0)
        {
            Debug.Print("Status: Fehler beim Senden oder Empfangen");
        }
        else
        {
            Debug.Print("Status: OK");
        }
    }

    // Ruft den Sensor Daten ab.
   public SensorData GetSensorData()
    {
       
// Byte Array für den Lese Vorgang vorbereiten
        byte[] buffer = new byte[14];
        buffer[0] = 0x3B;

        // Der Hex Wert stellt das erste Byte da
        // für die Beschleunigungsachse X
        Write(new byte[] { 0x3B });
       
// Byte Array übergeben zum beschreiben
        Read(buffer);

        // Ergebnis an das Sensor Objekt,
        // in dem die Byte Werte umgewandelt werden
        return new SensorData(buffer);
    }
}

Nun das war vielleicht etwas viel Green Code, dennoch hoffe ich, das die Beschreibungen etwas Aufschluss geben, was diese Byte Werte alle sollen. Weniger aufwendig und kürzer ist das SensorData Objekt, das die Byte Daten in brauchbare Werte umwandelt.

public class SensorData
{
    public uint Acceleration_X = 0;
    public uint Acceleration_Y = 0;
    public uint Acceleration_Z = 0;

    public uint Temperatur = 0;

    public uint Gyroscope_X = 0;
    public uint Gyroscope_Y = 0;
    public uint Gyroscope_Z = 0;

    public SensorData(byte[] buffer)
    {
        // Ergebnis für den Beschleunigungssensors zusammenlegen durch Bitshifting
        Acceleration_X = (((uint)buffer[0]) << 8) | buffer[1];
        Acceleration_Y = (((uint)buffer[2]) << 8) | buffer[3];
        Acceleration_Z = (((uint)buffer[4]) << 8) | buffer[5];

        // Ergebnis für Temperatur
        Temperatur = (((uint)buffer[6]) << 8) | buffer[7];

        // Ergebnis für den Gyroskopsensors zusammenlegen durch Bitshifting
        Gyroscope_X = (((uint)buffer[8]) << 8) | buffer[9];
        Gyroscope_Y = (((uint)buffer[10]) << 8) | buffer[11];
        Gyroscope_Z = (((uint)buffer[12]) << 8) | buffer[13];
    }
}

Nun fehlt noch der Einsatz in der ‘Main’ Methode, dass hier die Klasse für den Sensor initialisiert, den Abruf in das SensorData Objekt kopiert und schließlich über Debug.Print die Daten anzeigt..

public static void Main()
{
    MPU6050Sensor mpu = new MPU6050Sensor();

    SensorData data = mpu.GetSensorData();

    while (true)
    {
        data = mpu.GetSensorData();

        Debug.Print(
            "Acc X: " + data.Acceleration_X + " " +
            "Acc Y: " + data.Acceleration_Y + " " +
            "Acc Z: " + data.Acceleration_Z + " " +
            "Gyro X: " + data.Gyroscope_X + " " +
            "Gyro Y: " + data.Gyroscope_Y + " " +
            "Gyro X: " + data.Gyroscope_Z + " " +
            "Temperatur: " + data.Temperatur);
    }
}

Ist dieser Teil eingeben, so kann es dann auf dem Netduino weiter gehen. Hierbei kann ich den Tipp geben, auf die Verkabelung zu achten. Zwar passiert nichts, wenn ihr etwas nicht richtig angeschlossen habt, jedoch wenn ein Wackelkontakt vorliegt oder das Kabel zu lang ist für die I²C Verbindung, kann es zu erheblichen Störungen kommen.

Netduino_MPU6050

Die Verbindung von Netduino zum Sensor:

  • 3,3V >> VCC
  • GND >> GND
  • AnalogIn 4 >> SDA
  • AnalogIn 5 >> SCL

Ist der Anschluss erfolgt, kann das Programm auf den Netduino geschrieben werden. Sobald der Sensor Initialisiert wurde und der Programmcode in die Schleife geht, wird das Ergebnis über Debug.Print() ausgegeben.

image
In Visual Studio 2012 wird über die ‘Ausgabe’ oder im Englischen ‘Output’ ausgegeben.

Falls Fragen bestehen kann gerne ein Kommentar abgegeben werden oder einen Eintrag im Netduino Forum schreiben. Dort bin ich unter dem Namen Heroduino zu finden.

Natürlich wie immer die Solution für das .NET Micro Framework Version 4.2

Sonntag, 23. Juni 2013

Zeitkritische Momente mit dem Netduino

Quadrocopter_48
USB TTL UART, Netduino mit Shield und Schaltnetzteil für Tests.

Bei einigen Anwendungen kommt es vor, dass eine Iteration möglichst wenig Zeit benötigen soll. Das bedeutet, dass man sich mit Code Optimierung beschäftigen muss, in dem man zumeist die gewohnte Art zu Programmieren überdenken muss. Grundsätzlich sollte man das nicht tun, wenn man Anfänger ist, und dann auch nicht wenn man professioneller Entwickler ist. Es sei denn, die Anforderung erfordert diese Optimierung. In der Entwicklung für meinen Quadrocopter stieß ich auf dieses Problem, dass eine Iteration viel zu lange dauert. Zwar sauber geschrieben, jedoch nicht Performance-optimiert, kam ich auf ca. 40ms bis 45ms pro Iteration. Das ergibt pro Sekunde 22 bis 25 Durchläufe. Nach den Optimierungen kam ich auf Iterationslaufzeiten von 11ms bis 22ms. Diese weite Zeitspanne entstand durch dynamische Verarbeitung von Byte Werten beim Senden und Empfangen, was jedoch ein anderes Thema abbilden würde. Zunächst benötigen wir eine Methode, um herauszufinden, wie lange ein Vorgang an Zeit benötigt.

public class Stopwatch
{
    private static long _Ticks_Start = 0;
    private static long _TicksPerMillisecound = System.TimeSpan.TicksPerMillisecond;

    public static long GetElapsedMillisecounds()
    {
         long result = (Microsoft.SPOT.Hardware.Utility.GetMachineTime().Ticks
            - _Ticks_Start) / _TicksPerMillisecound;

         _Ticks_Start = Microsoft.SPOT.Hardware.Utility.GetMachineTime().Ticks;

        return result;
    }

    public static long GetElapsedMicrosecounds()
    {
         long result = (Microsoft.SPOT.Hardware.Utility.GetMachineTime().Ticks
            - _Ticks_Start) / (_TicksPerMillisecound / 1000);

         _Ticks_Start = Microsoft.SPOT.Hardware.Utility.GetMachineTime().Ticks;

         return result;
    }
}

Die Klasse habe ich aus dem Netduino Forum und ist für dieses Beispiel angepasst.

Kommen wir zu einigen Beispielen, in dem einfach einer Integer-variablen ein Wert zugewiesen wird der dann durch einen anderen dividiert wird. Nach 100 Durchläufen werden die Durchschnittszeiten der 6 Beispieel ausgegeben. Der Grund ist, dass das Ergebnis nicht konstant bleibt und durch weitere Einflüsse beeinträchtigt ist.

public class Program
{
    public static void Main()
    {
        long timeResult = 0;
        int result = 0;

        long countIteration = 1;
        long avarageExample1 = 0;
        long avarageExample2 = 0;
        long avarageExample3 = 0;
        long avarageExample4 = 0;
        long avarageExample5 = 0;
        long avarageExample6 = 0;

        while (true)
        {

           // Beispiel mit einer Devisions Rechnung in Millisekunden
            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            result = result / 67890;

            timeResult = Stopwatch.GetElapsedMillisecounds();
            Debug.Print("Beispiel 1: " + timeResult +
                " Millisekunde, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample1 += timeResult;

           // Beispiel mit einer Devisions Rechnung in Mikrosekunden
            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            result = result / 67890;

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 2: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample2 += timeResult;

            // Beispiel mit einer Devisions Rechnung
            // über eine Methode und den Integer kopieren.

            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            result = GetDivided(result);

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 3: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample3 += timeResult;

            // Beispiel mit einer Devisions Rechnung
            // über eine Methode und den Integer referenzieren.
            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            GetDividedByRef(ref result);

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 4: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample4 += timeResult;

            // Beispiel mit einer Devisions Rechnung
            // über eine Methode aus einer anderen Klasse und den Integer kopieren.
            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            result = MethodeInOtherClass.GetDivided(result);

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 5: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample5 += timeResult;

            // Beispiel mit einer Devisions Rechnung
            // über eine Methode aus einer anderen Klasse und den Integer referenzieren.

            Stopwatch.GetElapsedMillisecounds();

            result = 12345;
            MethodeInOtherClass.GetDividedByRef(ref result);

            timeResult = Stopwatch.GetElapsedMicrosecounds();
            Debug.Print("Beispiel 6: " + timeResult +
                " Mikrosekunden, Freier Speicher: " + Debug.GC(true) + "kb");
            avarageExample6 += timeResult;

            if (countIteration > 100)
            {
                // Durchschnitts Ergebnisse
                Debug.Print("Beispiel 1, Durchscnittliche Zeit: " +
                    (avarageExample1 / countIteration) + " Millisekunde");
                Debug.Print("Beispiel 2, Durchscnittliche Zeit: " +
                    (avarageExample2 / countIteration) + " Mikrosekunden");
                Debug.Print("Beispiel 3, Durchscnittliche Zeit: " +
                    (avarageExample3 / countIteration) + " Mikrosekunden");
                Debug.Print("Beispiel 4, Durchscnittliche Zeit: " +
                    (avarageExample4 / countIteration) + " Mikrosekunden");
                Debug.Print("Beispiel 5, Durchscnittliche Zeit: " +
                    (avarageExample5 / countIteration) + " Mikrosekunden");
                Debug.Print("Beispiel 6, Durchscnittliche Zeit: " +
                    (avarageExample6 / countIteration) + " Mikrosekunden");
            }

            countIteration++;
        }
    }

   private static int GetDivided(int result)
    {
        return result / 67890;
    }
    private static void GetDividedByRef(ref int result)
    {
        result = result / 67890;
    }
}

public static class MethodeInOtherClass
{
    public static int GetDivided(int result)
    {
        return result / 67890;
    }
    public static void GetDividedByRef(ref int result)
    {
        result = result / 67890;
    }
}

image
Ermittelt eine Durchschnittszeit, da das Ergebnis schwankt.

Das erste Beispiel zeigt das Ergebnis in Millisekunden, jedoch reicht das für mein zeitkritisches Thema nicht aus. Daher zeigt das zweite Beispiel das Ergebnis in Mikrosekunden an. Sieht man sich die Durchschnittszeit an, dann passt diese Vorgangsrechnung praktisch 3 mal pro Millisekunde. Die Beispiele 3 bis 6 zeigen eher unwesentliche Unterschiede. Hier zeigt der Vorgang über eine Methode eine längere Zeitspanne von ca. 60 Mikrosekunden, egal wo sich die Methode befindet. Geht man die mehrmals errechneten Durchschnittszeiten durch, ist zu erkennen, dass die Ergebnisse schwanken. Vielleicht irre ich mich da, aber ich konnte erkennen, dass es nur beim Debuggen auftrat. Stabil blieben die Werte, nachdem ich die Ergebnisse immer über eine UART Verbindung an den PC gesendet habe und noch ein wenig besser wurden die Ergebnisse, wenn der Netduino über eine andere Spannungsversorgung von mindestens 7,5V angeschlossen wurde. Ich selbst verwendete ein 12V / 1A Schalt Netzteil oder einen 9V / mAh Block Akku für meine Anwendungen und Tests. Für die folgende Ausgabe habe ich noch 3 weitere Arten des Methodenaufrufes eingesetzt.

  • Beispiel 7 ist das Ergebnis über einen Methodenaufruf in einer anderen DLL.
  • Beispiel 8 ist das Ergebnis über einen Methodenaufruf mit zwei übergebenen Werten.
  • Beispiel 9 ist das Ergebnis über einen Methodenaufruf, wobei der referenzierte Integer vorher angelegt wird.
image
Ergebnis ohne Debug und Netduino weiterhin an USB angeschlossen.

Seltsamerweise war das Ergebnis unwesentlich besser.

image
Ergebnis mit einem 12V Schaltnetzteil, mit dem Akku sah die Ausgabe geradezu identisch aus.

Bei diesen Beispielen mit den verschiedenen Spannungsquellen, lohnt sich der Test allerdings erst, wenn der Netduino aufwendigere Aufgaben zu erledigen hat, auf dem weitere Module gesteckt sind und viele Algorithmen verwendet werden.

image
Akkus und Schaltnetzteile stellen zum Glück vernachlässigbare Unterschiede.

Zum Abschluss diesen Blog Eintrages bildet sich eines auf jeden Fall klar ab: Es kostet Zeit! Und je aufwendiger das Programm, um so exponentiell steigt die Möglichkeit der Optimierung und macht erst Sinn, wenn das Ziel diese Anforderung unausweichlich benötigt.

Beispiel Projekt zum Downloaden. (.NET Micro Framework 4.2)

Ameisen Simulation und andere Dinge

Wer lange sich schon mit C# beschäftig, hat sicherlich schon mal was von AntMe gelesen oder gehört. Diese Idee hatte ich aufgegriffen und mi...