AdSense

Mittwoch, 31. Juli 2013

Funkstrecke mit Arduino und Atmega8

(English version) In meinen letzten beiden Posts habe ich die Verwendung der nRF24L01+ Funkmodule beschrieben. Nun möchte ich ein Projekt mit (mehr oder weniger) praktischem Nutzen vorstellen, das die Module verwendet:
Ich habe eine kleine Funkstrecke aufgebaut, bei der ein Atmega8-Mikrocontroller "Umweltdaten" erfasst und sie über Funk an einen Arduino Uno weiter leitet. Dieser stellt die Daten auf einem LCD-Display dar.

Hardware Atmega8

Der Aufbau auf der Atmega-Seite ist recht simpel. Der Funkmodule werden wie bereits beschrieben angeschlossen, zusätzlich habe ich an an den "Analog Input 1" (entspricht Pin 24 am Atmega8) den variablen Abgriff eines Potis angeschlossen (das zwischen 5V und GND liegt) um ein analoges Signal zu erhalten. Außerdem habe ich die digitalen Eingänge 0 und 1 (Pins 2 und 3 am Atmega8) über Schalter mit GND verbunden, um zwei digital Zustände darstellen zu können.

Hardware Arduino

Auch hier hält sich der Verkabelungsaufwand in Grenzen. Zusätzlich zum Funkmodul habe ich an den Arduino ein LCD-Display angeschlossen. Dieses habe ich jedoch nicht direkt mit dem Arduino verbunden, ich benutze der Einfachheit halber das LCD-I2C-Modul von Pollin (viel weniger Kabel ;) ). Dieses wird einfach an die Eingänge SDA und SCL des Arduinos gehängt und kann dann ganz einfach mit der passenden Library über I2C angesprochen werden. Zusätzlich habe ich den digitalen Eingang 2 mit einem Druckschalter auf GND gelegt.

Software Atmega8

Hier habe ich weitestgehend den Code übernommen, den ich in meinem letzten Beitrag zu den Interrupts schon vorgestellt habe:

//Include für Watchdog
#include <avr/wdt.h>

//Includes für Funkmodul
#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

//Variablen
int data;    //übertragene Daten

void setup(){
  
  //Interrupt initialisieren
  attachInterrupt(0, receive, LOW);
   
  //Input-Pins initialisieren
  pinMode(0, INPUT_PULLUP);
  pinMode(1, INPUT_PULLUP);
     
  //Funkmodul initialisieren
  Mirf.spi = &MirfHardwareSpi;
  Mirf.init();
  Mirf.setRADDR((byte *)"atmeg");
  Mirf.payload = sizeof(unsigned int);
  Mirf.channel = 120;
  
  //auf 250kbit/s umstellen (0x26) (2Mbit => 0x0f)
  Mirf.configRegister(RF_SETUP, 0x26);
  
  //Interrupt nur wenn Daten empfangen
  Mirf.configRegister(CONFIG, 0x48);
  
  Mirf.config();
  
  //Watchdog starten
  wdt_enable (WDTO_2S);

}

void loop(){ 
  //hier könnte episches passieren
}

//wenn Daten empfangen wurden
void receive() { 
  noInterrupts();
  
  if(!Mirf.isSending() && Mirf.dataReady()){
    Mirf.getData((byte *)&data);
    
    //ADC einlesen
    if (data == 1) {
      data = analogRead(A1);
    }
    
    //IO einlesen
    else if (data == 2) {
      data = 0xff;
      
      for (int i = 0; i<= 1; i++) {
        bitWrite(data, i, digitalRead(i));
      }
    }
    
    //eingelesene Daten senden  
    Mirf.setTADDR((byte *)"ardui");
    Mirf.send((byte *)&data);
  }
  
  wdt_reset ();    //Watchdog zurücksetzen
  interrupts();
  
}

Da ich den Watchdog verwenden möchte, habe ich zusätzlich #include <avr/wdt.h> eingefügt . Die void setup() is soweit bekannt, neu sind hier nur die Initialisierung der Input-Pins sowie der Start des Watchdogs. Ich betreibe das Funkmodul auf Kanal 120 mit 250kbit/s um Störungen zu vermeiden.
Die void loop() ist (immer noch) leer, könnte aber mit "Leben" gefüllt werden. Die void receive(), die aufgerufen wird, wenn das Funkmodul Daten empfangen hat, hat sich auch nicht großartig verändert. Um Überlappungen zu vermeiden, werden zuerst die Interrupts deaktiviert. Dann wird geprüft, ob die Daten vom ADC oder von den digitalen Input-Pins angefordert wurden. Diese werden dann entsprechend übertragen. Zum Schluss wird der Watchdog zurück gesetzt und die Interrupts werden wieder aktiviert. Den Watchdog habe ich eingefügt, damit der Controller automatisch einen Reset druchführt, sollte er sich einmal (aus welchen Gründen auch immer) "verschlucken" und deswegen nichts mehr senden können.
Ein paar Worte noch zum Einlesen und Übertragen der digitalen Pins: die Pins sind intern mit Pullups verbunden, sind also active low. Um nun die Zustände aller Pins in einer "Sendung" übertragen zu können, gehe ich folgerndermaßen vor: zuerst setze ich die zu übertragende data-Variable auf 0xff (also 0b1111'1111). Dies bedeutet erstmal, dass alle Eingänge inaktiv sind. Dann prüft eine for-Schleife alle (zwei, gerne aber auch mehr) Eingänge und schreibt den Zustand des Eingangs mit bitWrite in das entsprechende Bit von data (also den Zustand von Eingang 0 in Bit 0, den Zustand von Eingang 1 in Bit 1 usw.).

Software Arduino

Hier hat sich ein bisschen mehr getan:

//Includes für Funkmodul
#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

//Includes für LCD-Display
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>


//Variablen
int data;          //übertragene Daten
int data_old = 0;  //beim letzten Mal übertragene Daten
unsigned long time;         //Zeitspeicher für Timeout
unsigned char button = 2;   //Pin des Schalters
unsigned char state = 2;    //Zustand AD (1) oder IO (2)
int debug = 0;

LiquidCrystal_I2C lcd(0x20,16,2);  //I2C-LCD-Display

void setup(){
  //Pins initialisieren
  pinMode(button, INPUT_PULLUP);
  
  //Interrupts initialisieren
  attachInterrupt(0, buttonPressed, LOW);
  
  //LCD-Display initialisieren
  lcd.init();                      
  lcd.backlight();
 
  //Funkmodul initialisieren
  Mirf.spi = &MirfHardwareSpi;
  Mirf.init();
  
  Mirf.setRADDR((byte *)"ardui");
  Mirf.payload = sizeof(unsigned int);
  Mirf.channel = 120;
  
  //auf 250kbit/s umstellen (0x26) (2Mbit => 0x0f)
  Mirf.configRegister(RF_SETUP, 0x26);
      
  Mirf.config();
  
}

void loop(){
  
    Mirf.setTADDR((byte *)"atmeg");
    data = state;                    //AD oder IO erwartet=?
    Mirf.send((byte *)&data);
    while(Mirf.isSending()){
    }
    delay(100);
    time = millis();
    while(!Mirf.dataReady()){
      if ((millis() - time) > 1000) {
        //Timeout beim Empfangen
        lcd.clear();
        lcd.setCursor(0,0);
        lcd.print("Timeout...");
        debug++;
        break;
      }
    }
    
    Mirf.getData((byte *) &data);
    
    //das Display wird nur dann aktualisiert, wenn sich die Daten ändern um flimmern zu verhindern
    if (data != data_old) {  
        
      lcd.clear();
      
      //AD-Wandler-Daten darstellen
      if (state == 1) {
        lcd.setCursor(0,0);
        lcd.print("AD-Wandler");
        lcd.setCursor(11, 0);
        lcd.print(debug);
        lcd.setCursor(0,1);
        lcd.print(data);
      }
      
      //IO-Daten darstellen
      else {
        lcd.setCursor(0,0);
        lcd.print("Digital IO");
        lcd.setCursor(0,1);
        lcd.print(bitRead(data,0));
        lcd.setCursor(2,1);
        lcd.print(bitRead(data,1));
      }
    
    }
    
    data_old = data;
    delay(250);
  
} 

//wenn Button gedrückt (inkl. Entprellfunktion)  
void buttonPressed() {
  noInterrupts();
  delay(50);
  if (digitalRead(button) == LOW) {
    while(digitalRead(button) == LOW);
   
    if (state == 1) {
      state = 2;        //auf IO umstellen
    }
    
    else {
      state = 1;       //auf ADC umstellen
    }    
    
  }
  interrupts();
}
  
Ich habe mir beim kommentieren des Codes extra Mühe gegeben ;) Includes und void setup() sollten also selbsterklärend sein. Die Interrupt-Funktion prüft, ob der Button gedrückt wurde und der Eingang somit auf LOW gezogen wird. Falls ja, wird die entsprechende Funktion aufgerufen, die den Schalter zuerst entprellt und dann entsprechend die Variable state umstellt. In der Variable wird gespeichert, ob der Nutzer den Wert des ADC oder der digitalen Eingänge des Atmegas sehen möchte. Mit dem Button lässt sich also zwischen den zwei Ansichten umschalten.
Die void loop() beginnt zunächst mit dem "üblichen" Code: übertragen des Wertes der state-Variable an den Atmega und warten auf Antwort. Falls binnen einer Sekunde keine Antwort erfolgt, wird ein Timeout ausgelöst und das Programm beginnt wieder von vorne.
Wenn erfolgreich Daten empfangen wurden, sollen diese auf dem LCD-Display dargestellt werden. Dazu wird zuerst geprüft, ob der Benutzer die Daten des ADC oder der digitalen Eingänge sehen möchte. Entsprechen wird dann die Darstellung auf dem LCD-Display aufgebaut. Zunächst habe ich diesen Schritt bei jedem Programmdurchlauf durchgeführt. Dies führte jedoch zu einem Flimmern des Displays. Daher habe ich zusätzlich eine Überprüfung eingefügt, ob sich die empfangenen Daten überhaupt geändert haben. Nur wenn dies der Fall ist, wird die Darstellung aktualisiert.

Anwendung

Ich habe zu Beginn des Posts ja einen praktischen Nutzen versprochen. Der momentane Aufbau kann das natürlich nicht bieten. Man könnte aber z.B. am Atmega das Poti durch einen Spannungsteiler mit einem Pt100-Widerstand und die Schalter durch Reed-Kontakte ersetzen und so eine Temperatur und den Öffnungszustand eines Fensters übertragen (Stichwort "Home Automation").
Oder Udo könnte sich damit eine Fernsteuerung für seinen Quadrocopter basteln. Das ist ehrlich gesagt auch der Grund, warum er mich dazu "gezwungen" hat, mich mit diesem Thema auseinander zu setzen und die Blog-Beiträge zu verfassen ;)

Samstag, 27. Juli 2013

Atmega/Arduino: Funkmodul nRF24L01+ mit Interrupts

(Englich version) In meinem letzten Beitrag habe ich die Grundlagen zu den nRF24L01+ bzw. nRF24L01P-Modulen erläutert. Wie bereits am Ende des Beitrags erwähnt war die Umsetzung des Codes (vorallem beim Epfänger) nicht optimal. Der Mikrocontroller lief dauerhaft in einer Endlosschleife die nichts anderes tat, als zu prüfen, ob den nun endlich wieder neue Daten empfangen wurden. In der Praxis ist das natürlich eher unpraktisch, der µC soll schließlich auch noch andere Aufgaben übernehmen.

Genau dafür wurden die Interrupts erfunden ;) Im letzten Beitrag habe ich bereits die Interrupt-Funktionen der Funkmodule angesprochen:
"IRQ ist der Interrupt-Pin des Moduls und kann bei 3 verschiedenen Events active low werden: Daten wurden empfangen, Daten wurden gesendet, Senden fehlgeschlagen. Diese Interrupts lassen sich jedoch frei ein- und ausschalten."
Die Interrupts werden über das Register CONFIG gesteuert. Über das Bit 4 steuert man den Interrupt bei fehlgeschlagenem Senden, Bit 5 schaltet den Interrupt bei erfolgreichem Senden und Bit 6 den beim Empfang von Daten. Wird das Bit auf 1 gesetzt ist der jeweilige Interrupt deaktiviert, steht das Bit  auf 0 wird der IRQ-Pin beim Auftreten des entsprechenden Ereignisses auf LOW gesetzt. Die passende Verkabelung habe ich schon beim Aufbau im letzten Beitrag vorgenommen. Der IRQ-Pin muss mit einem der Interrupt-Pins des Mikrocontrollers verbunden werden.

Dies habe ich nun in Code umgesetzt. Die Software auf der Sender-Seite (also auf dem Arduino) wurde nicht geändert. Nur auf der Emfpänger-(Atmega8-)Seite gab es eine kleine Änderung:

#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

void setup(){
  
  //Interrupt initialisieren
  attachInterrupt(0, receive, LOW);
   
  //Funkmodul initialisieren
  Mirf.spi = &MirfHardwareSpi;
  Mirf.init();
  Mirf.setRADDR((byte *)"serv1");
  Mirf.payload = sizeof(unsigned long);
  
  //auf 250kbit/s umstellen (0x26) (2Mbit => 0x0f)
  Mirf.configRegister(RF_SETUP, 0x26);
  
  //Interrupt nur wenn Daten empfangen
  Mirf.configRegister(CONFIG, 0x48);
  
  Mirf.config();

}

void loop(){ 
  //Hier passieren epische Dinge
}

void receive() {
   
  noInterrupts();
  byte data[Mirf.payload];
    
  if(!Mirf.isSending() && Mirf.dataReady()){
    Mirf.getData(data);
    Mirf.setTADDR((byte *)"clie1");
    Mirf.send(data);
  }
  
  interrupts();
  
}

Die setup()-Funktion ist weitestgehend identisch mit der des ursprünglichen Programms Es gibt nur zwei kleine Änderungen: die Funktion attachInterrupt(0, receive, LOW) aktiviert den Interrupt. Wird der Interrupt 0 (Pin 4 beim Atmega8) auf LOW gezogen, dann wird die Funktion receive() aufgerufen.
Die zweite Änderung ist der Befehlt Mirf.configRegister(CONFIG, 0x48). Damit wird das CONFIG-Register so eingestellt, dass nur beim Empfang von Daten ein Interrupt ausgelöst wird.

Das Hauptprogramm (void loop()) ist komplett leer, hier kann jetzt beliebiger Code stehen.

Die Funktion receive() wird bei einem Interrupt aufgerufen und beinhaltet den Code, der zuvor in der loop()-Funktion enthalten war. Auch hier gibt es nur zwei kleine Erweiterungen: der Befehl noInterrupts() deaktiviert zu Beginn der Funktion die Interrupts, um "Überlagerungen" der Interrupts zu vermeiden. Am Ende der Funktion werden sie mit interrupts() wieder aktiviert.

Freitag, 26. Juli 2013

Atmega/Arduino: Funkmodul nRF24L01+ Grundlagen

(English version) Heute gibt es wieder einen Gastbeitrag von mir, dieses mal zum Funkmodul nRF24L01+ (oder nRF24L01P) von Nordic Semiconductor. Das Modul ist für wenige Euro bei Ebay zu haben. Es handelt sich um einen "Ultra low power 2.4GHz RF Transceiver" - es kann also sowohl Senden als auch Empfangen und arbeitet im 2.4GHz-Band. Dabei arbeitet es laut Hersteller besonders sparsam, es zieht einen Maximalstrom von gerade einmal 15mA. Dabei erreicht es Übertragungsraten von bis 2 MBit/s. Angesprochen wird das Modul über den SPI-Bus.

Nun aber genug der Theorie, alle weiteren Details sind dem Datenblatt zu entnehmen. Dieses wird später noch wichtig werden.

Für das nRF24L01P gibt es fertige Libraries für Arduino und den Atmega. Wie ich in meinem letzten Artikel geschrieben habe, benutze ich wegen der einfacheren Bedienung gerne das Arduino-Paket. Daher werde ich im folgenden auch nur erklären, wie man die Module mit einem Arduino-Board bzw. einem "nackten" Atmega8 mit Arduino-Libraries zum Laufen bekommt.

Als einfaches Beispiel zum Einstieg wähle ich das Ping-Programm, dass die Arduino-Lib mitbringt. Der Arduino Uno sendet seine aktuelle "Systemzeit" an den Atmega, dieser sendet sie sofort wieder zurück. Aus der empfangenen Zeit und der aktuellen Zeit kann der Arduino dann die Laufzeit des Signals, also den Pin bestimmen.

Aufbau

Zuerst geht es an die Verkabelung. Die Funkmodule haben ingesamt 8 Anschlüsse: Vcc und GND für die Spannungsversorgung (WICHTIG: die Module verlangen eine Versorgung mit ~3V), MISO, MOSI und SCK für die SPI-Kommunikation, CSN (kündigt dem Modul einen Befehl an) sowie CE und IRQ. IRQ ist der Interrupt-Pin des Moduls und kann bei 3 verschiedenen Events active low werden: Daten wurden empfangen, Daten wurden gesendet, Senden fehlgeschlagen. Diese Interrupts lassen sich jedoch frei ein- und ausschalten.

Die genaue Pinbelegung hängt von der äußeren Beschaltung des Moduls ab, lässt sich jedoch mit ein bisschen googlen schnell heraus finden. Die meisten Module haben folgende Pinbelegung:
IRQ     8  7  MISO
MOSI  6  5  SCK
CSN    4  3  CE
VCC    2  1  GND

Diese Pins müssen nun entsprechend an das Arduino-Board bzw. den Atmega8 angeschlossen werden:
MISO an Pin 12 des Arduino (Pin 18 beim Atmega, Pin 50 beim Arduino Mega)
MOSI an Pin 11 (Pin 17, 51)
SCK an Pin 13 (Pin 19, 52)
IRQ an Pin 2 oder 3 (Pin 4 oder 5, Interrupts gibt es sehr viele beim Arduino Mega, daher gebe ich hier keinen speziellen Pin an)
Folgende Pins sind im Prinzip frei wählbar, die Standardbelegung der Lib ist allerdings
CSN an Pin 7 (Pin 13, 7)
CE an Pin 8 (Pin 14, 8)

Wie bereits erwähnt sind zwingend 3V zur Spannungsversorgung nötig! Die Signal-Pins vertragen jedoch 5V.

Jetzt sollte soweit alles verkabelt sein und wir können uns der Software widmen.

Software Arduino

Für das Beispiel habe ich wie schon gesagt zum Großteil des mitgelieferte Beispiel der Lib übernommen.

Die Library ist HIER erhältlich.

Folgender Code wird auf das Arduino-Board gespielt:

#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

void setup(){
  Serial.begin(9600);
  
  //Falls CE- bzw. CSN-Pin anders belegt
  
  //Mirf.cePin = 7;
  //Mirf.csnPin = 8;
  
  Mirf.spi = &MirfHardwareSpi;
  Mirf.init();
  
  //Empfänder-Adresse einstellen   
  Mirf.setRADDR((byte *)"clie1");
  
  //Payload-Länge einstellen   
  Mirf.payload = sizeof(unsigned long);
  
  //auf 250kbit/s umstellen (0x26) (2Mbit => 0x0f)
  Mirf.configRegister(RF_SETUP, 0x26);
 
  //ggfs. Kanal ändern
  //Mirf.channel = 10;

  //Konfiguration in Modul übernehmen
  Mirf.config();
  
  Serial.println("Beginning ... "); 
}

void loop(){
  unsigned long time = millis();
  
  Mirf.setTADDR((byte *)"serv1");
  
  Mirf.send((byte *)&time);
  
  while(Mirf.isSending()){
  }
  Serial.println("Finished sending");
  delay(10);
  while(!Mirf.dataReady()){
    if ( ( millis() - time ) > 1000 ) {
      Serial.println("Timeout on response from server!");
      return;
    }
  }
  
  Mirf.getData((byte *) &time);
  
  Serial.print("Ping: ");
  Serial.println((millis() - time));
  
  delay(1000);
}  
 
In der setup()-Funktion wird zunächst das Funkmodul mit dem init()-Befehl initialisiert. Dann wird die Adresse eingestellt, unter der das Modul angesprochen werden soll. Die Payload-Länge gibt an, wie viele Daten-Bytes bei jeder Übertragung gesendet werden sollen. Bis zu 32 Bytes sind hier möglich, jedoch sollte man nie mehr wählen als nötig. Um die Reichweite der Module zu erhöhen, habe ich mich dazu entschieden, die Übertragungsrate von 2 Mbit/s auf 250 kbit/s zu ändern. Hierzu ist ein Zugriff auf die Register des Moduls nötig. Dank der Lib geht dies jedoch problemlos mit einer Zeile Code. Der Befehl configRegister(RF_SETUP, 0x26) schreibt den Werte 0x26 in das Register mit dem Namen RF_SETUP und stellt somit die Übertragungsrate ein. Die Namen der Register und ihre genaue Funktion ist dem Datenblatt ab Seite 57 zu entnehmen.
Zum Abschluss wird die Konfiguration mit dem config()-Befehl in das Modul übernommen.

Das Hauptprogramm ist eigentlich recht simpel und selbsterklärend. Mit dem Befehl setTADDR wird eingestellt, an welche Adresse gesendet werden soll. Mit dem send()-Befehl werden dann die gewünschten Daten (hier die Systemzeit) gesendet. Die folgende while-Schleife sorgt dafür, dass der Arduino solange pausiert, bis die Übertragung abgeschlossen ist. Mit der nächsten while-Schleife und der Bedingungen !Mirf.dataReady wartet der µC so lange, bis das Funkmodul Daten (also eine Antwort) empfangen hat.  Der Befehl getData() liest dann die empfangenen Daten aus dem Pufferspeicher des Moduls.

Software Atmega8

#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

void setup(){

  Mirf.spi = &MirfHardwareSpi;
  Mirf.init();
  
  //Empfänder-Adresse einstellen     
  Mirf.setRADDR((byte *)"serv1");
  
  //Payload-Länge einstellen   
  Mirf.payload = sizeof(unsigned long);
  
  //auf 250kbit/s umstellen (0x26) (2Mbit => 0x0f)
  Mirf.configRegister(RF_SETUP, 0x26);
  
  //ggfs. Kanal ändern
  //Mirf.channel = 10;

  //Konfiguration in Modul übernehmen
  Mirf.config();
  
}

void loop(){

  //Pufferspeicher für die Daten
  byte data[Mirf.payload];
   
  if(!Mirf.isSending() && Mirf.dataReady()){
         
    Mirf.getData(data);
         
    Mirf.setTADDR((byte *)"clie1");
   
    Mirf.send(data);
    
  }
}

Die setup()-Funktion des Atmega-Programms ist im Prinzip identisch mit der des Arduino-Programms, ich werde den Code daher nicht näher erläutern.
Das Hauptprogramm ist auch ziemlich simpel. Das Programm läuft die ganze Zeit "im Kreis" und wartet, bis das Funkmodul Daten zur Vefügung stellt (Mirf.dataReady()). Diese werden dann zwischengespeichert, es wird die Adresse des Empfängers gesetzt und die Daten werden sofort wieder zurück gesendet. Um Überschneidungen beim Senden zu verhindern, ist zusätzlich noch die !Mirf.isSending()-Bedigungen in der if-Abfrage eingefügt worden.

Der Code auf der Empfänger-Seite (also auf dem Atmega) ist so natürlich nicht besonders effizient, da der Controller nichts anderes macht als zu warten, ob den nun endlich Daten empfangen wurden. Viel praktischer und schöner lässt sich das mit den erwähnten Interrupts lösen. Wie genau das funktioniert, werde ich in meinem nächsten Beitrag erklären.

Donnerstag, 25. Juli 2013

C# WPF - 3D-Grafik

(English version) Bisher hatte ich es nicht geschafft, 3D-Grafiken zu erzeugen (DirectX, und andere habe ich ausprobiert, von denen hat jedoch nichts schön funktioniert). Heute bin ich per Zufall darauf gestoßen, dass WPF das auch kann. Also hab ich mich hingesetzt und in 2 Stunden war ein kleiner Panzer, den man mit Pfeiltasten fahren kann fertig. Mit WPF 3D-Grafik zu machen ist höchstgradig trivial.

Wie man im Code sehen kann besteht der Hauptteil des Programms aus der Funktion Tank(), welche daraus besteht, dass ich alle Objekte von Hand baue. Das geht prinzipiell auch viel einfacher mit 3D-Editoren oder aus XML-Dateien, für den ersten Test werde ich es aber mal so belassen.

Ansonsten gibt es da noch den moveThread, welcher den Panzer bewegt und dann noch die Funktion RefreshEverything, welche die Position des Panzers verändert. Falls es Fragen zu irgendwas gibt stehe ich gerne Rede und Antwort.
 
Hier nun mein Code von MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace Wpf3dTest1
{
    public partial class MainWindow : Window
    {
        Dictionary<Key, Boolean> pressedKeys = new Dictionary<Key, Boolean>();
        bool quit = false;
        public MainWindow()
        {
            InitializeComponent();

            pressedKeys.Add(Key.Left, false);
            pressedKeys.Add(Key.Right, false);
            pressedKeys.Add(Key.Up, false);
            pressedKeys.Add(Key.Down, false);

            this.KeyDown += MainWindow_KeyDown;
            this.KeyUp += MainWindow_KeyUp;

            this.Closing += OnWindowClosing;

            //create a Tank object and add to viewport
            mod = Tank(new Point3D(xPos, 0, 0), 2.2, 1.8, 0.7, 0.3, 0.25, 0.5, 0.25, new DiffuseMaterial(Brushes.Green), new DiffuseMaterial(Brushes.Black));
            viewport3D.Children.Add(mod);

            //start a Thread which moves the object
            Thread thread = new Thread(moveThread);
            thread.Start();
        }

        void MainWindow_KeyUp(object sender, KeyEventArgs e)
        {
            if (pressedKeys.ContainsKey(e.Key))
            {
                pressedKeys[e.Key] = false;
            }
        }

        void MainWindow_KeyDown(object sender, KeyEventArgs e)
        {
            if (pressedKeys.ContainsKey(e.Key))
            {
                pressedKeys[e.Key] = true;
            }
        }

        ModelVisual3D mod;

        private delegate void RefreshEverythingDelegate();
        private void RefreshEverything()
        {
            TranslateTransform3D translate = new TranslateTransform3D();
            translate.OffsetX = xPos;
            translate.OffsetZ = zPos;

            RotateTransform3D rotate = new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 1,0),angle));
            rotate.CenterX = xPos;
            rotate.CenterZ = zPos;


            Transform3DGroup transform = new Transform3DGroup();
            transform.Children.Add(translate);
            transform.Children.Add(rotate);
            mod.Transform = transform;
        }

        //Parameters for turn and move
        private double posIncrementor = 0.02;
        private double xPos = 0;
        private double zPos = 0;

        private double angle = 0;
        private double angleIncrementor = 0.4;

        private void moveThread()
        {
            while (!quit)
            {
                if (pressedKeys[Key.Up])
                {
                    xPos += posIncrementor * Math.Cos(angle * Math.PI/180) ;
                    zPos -= posIncrementor * Math.Sin(angle * Math.PI / 180);
                }
                if (pressedKeys[Key.Down])
                {
                    xPos -= posIncrementor * Math.Cos(angle * Math.PI / 180);
                    zPos += posIncrementor * Math.Sin(angle * Math.PI / 180);
                }
                if (pressedKeys[Key.Left])
                {
                    angle += angleIncrementor;
                }
                if (pressedKeys[Key.Right])
                {
                    angle -= angleIncrementor;
                }
                DispatcherOperation dispOp = this.viewport3D.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new RefreshEverythingDelegate(RefreshEverything));
                Thread.Sleep(10);
            }
        }

        public void OnWindowClosing(object sender, CancelEventArgs e)
        {
            quit = true;
        }

        private ModelVisual3D Tank(Point3D position, double length, double width, double height, double chainWidth, double chainHeight1, double chainHeight2, double chainNotLength, Material bodyMaterial, Material chainMaterial)
        {
            //Tank Body
            MeshGeometry3D bodyMesh = new MeshGeometry3D();
            Point3D a = new Point3D(position.X - length / 2, 0, position.Z + width / 2);
            Point3D b = new Point3D(position.X + length / 2, 0, position.Z + width / 2);
            Point3D c = new Point3D(position.X + length / 2, 0, position.Z - width / 2);
            Point3D d = new Point3D(position.X - length / 2, 0, position.Z - width / 2);
            Point3D e = new Point3D(position.X - length / 2, position.Y + height, position.Z + width / 2);
            Point3D f = new Point3D(position.X + length / 2, position.Y + height, position.Z + width / 2);
            Point3D g = new Point3D(position.X + length / 2, position.Y + height, position.Z - width / 2);
            Point3D h = new Point3D(position.X - length / 2, position.Y + height, position.Z - width / 2);
            BuildRectangle(bodyMesh, a, b, f, e, new Vector3D(0, 0, 1));
            BuildRectangle(bodyMesh, b, c, g, f, new Vector3D(1, 0, 0));
            BuildRectangle(bodyMesh, c, d, h, g, new Vector3D(0, 0, -1));
            BuildRectangle(bodyMesh, d, a, e, h, new Vector3D(-1, 0, 0));
            BuildRectangle(bodyMesh, e, f, g, h, new Vector3D(0, 1, 0));
            BuildRectangle(bodyMesh, a, d, c, b, new Vector3D(0, -1, 0));

            //Build the model object
            GeometryModel3D BodyModel = new GeometryModel3D(
            bodyMesh,
            bodyMaterial);


            //Chain 1
            MeshGeometry3D chain1Mesh = new MeshGeometry3D();//links, also -width
            a = new Point3D(position.X - length / 2 + chainNotLength, 0, position.Z - width / 2 - chainWidth);
            b = new Point3D(position.X - length / 2 + chainNotLength, 0, position.Z - width / 2);
            c = new Point3D(position.X + length / 2 - chainNotLength, 0, position.Z - width / 2 - chainWidth);
            d = new Point3D(position.X + length / 2 - chainNotLength, 0, position.Z - width / 2);
            e = new Point3D(position.X - length / 2, position.Y + chainHeight1, position.Z - width / 2 - chainWidth);
            f = new Point3D(position.X - length / 2, position.Y + chainHeight1, position.Z - width / 2);
            g = new Point3D(position.X + length / 2, position.Y + chainHeight1, position.Z - width / 2 - chainWidth);
            h = new Point3D(position.X + length / 2, position.Y + chainHeight1, position.Z - width / 2);
            Point3D i = new Point3D(position.X - length / 2, position.Y + chainHeight2, position.Z - width / 2 - chainWidth);
            Point3D j = new Point3D(position.X - length / 2, position.Y + chainHeight2, position.Z - width / 2);
            Point3D k = new Point3D(position.X + length / 2, position.Y + chainHeight2, position.Z - width / 2 - chainWidth);
            Point3D l = new Point3D(position.X + length / 2, position.Y + chainHeight2, position.Z - width / 2);

            BuildRectangle(chain1Mesh, a, b, d, c, new Vector3D(0, -1, 0));
            BuildRectangle(chain1Mesh, a, e, f, b, new Vector3D(-1, -1, 0));
            BuildRectangle(chain1Mesh, c, d, h, g, new Vector3D(1, -1, 0));
            BuildRectangle(chain1Mesh, a,c,g,e, new Vector3D(0, 0, -1));
            BuildRectangle(chain1Mesh, i, j, f, e, new Vector3D(-1, 0, 0));
            BuildRectangle(chain1Mesh, k, g, h, l, new Vector3D(1, 0, 0));
            BuildRectangle(chain1Mesh, e,g,k,i, new Vector3D(0, 0, -1));
            BuildRectangle(chain1Mesh, i, k, l, j, new Vector3D(0, 1, 0));

            //Build the model object
            GeometryModel3D Chain1Model = new GeometryModel3D(
            chain1Mesh,
            chainMaterial);


            //Chain 2
            MeshGeometry3D chain2Mesh = new MeshGeometry3D();//rechts, also +width
            a = new Point3D(position.X - length / 2 + chainNotLength, 0, position.Z + width / 2);
            b = new Point3D(position.X - length / 2 + chainNotLength, 0, position.Z + width / 2 + chainWidth);
            c = new Point3D(position.X + length / 2 - chainNotLength, 0, position.Z + width / 2);
            d = new Point3D(position.X + length / 2 - chainNotLength, 0, position.Z + width / 2 + chainWidth);
            e = new Point3D(position.X - length / 2, position.Y + chainHeight1, position.Z + width / 2);
            f = new Point3D(position.X - length / 2, position.Y + chainHeight1, position.Z + width / 2 + chainWidth);
            g = new Point3D(position.X + length / 2, position.Y + chainHeight1, position.Z + width / 2);
            h = new Point3D(position.X + length / 2, position.Y + chainHeight1, position.Z + width / 2 + chainWidth);
            i = new Point3D(position.X - length / 2, position.Y + chainHeight2, position.Z + width / 2);
            j = new Point3D(position.X - length / 2, position.Y + chainHeight2, position.Z + width / 2 + chainWidth);
            k = new Point3D(position.X + length / 2, position.Y + chainHeight2, position.Z + width / 2);
            l = new Point3D(position.X + length / 2, position.Y + chainHeight2, position.Z + width / 2 + chainWidth);

            BuildRectangle(chain2Mesh, a, b, d, c, new Vector3D(0, -1, 0));
            BuildRectangle(chain2Mesh, a, e, f, b, new Vector3D(-1, -1, 0));
            BuildRectangle(chain2Mesh, c, d, h, g, new Vector3D(1, -1, 0));
            BuildRectangle(chain2Mesh, b, f, h, d, new Vector3D(0, 0, 1));
            BuildRectangle(chain2Mesh, i, j, f, e, new Vector3D(-1, 0, 0));
            BuildRectangle(chain2Mesh, k, g, h, l, new Vector3D(1, 0, 0));
            BuildRectangle(chain2Mesh, f, j, l, h, new Vector3D(0, 0, 1));
            BuildRectangle(chain2Mesh, i, k, l, j, new Vector3D(0, 1, 0));

            //Build the model object
            GeometryModel3D Chain2Model = new GeometryModel3D(
            chain2Mesh,
            chainMaterial);


            //Build the whole object
            Model3DGroup model3DGroup = new Model3DGroup();
            model3DGroup.Children.Add(BodyModel);
            model3DGroup.Children.Add(Chain1Model);
            model3DGroup.Children.Add(Chain2Model);

            ModelVisual3D model = new ModelVisual3D();
            model.Content = model3DGroup;
            return model;
        }

        //Funktioniert auch mit nicht-Rechtecken, also wenn nicht alle Innenwinkel 90° sind.
        private void BuildRectangle(MeshGeometry3D geometry,Point3D a, Point3D b, Point3D c, Point3D d,Vector3D normal)
        {
            int baseIndex = geometry.Positions.Count;

            //Add vertices
            geometry.Positions.Add(a);
            geometry.Positions.Add(b);
            geometry.Positions.Add(c);
            geometry.Positions.Add(d);

            //Add normals
            geometry.Normals.Add(normal);
            geometry.Normals.Add(normal);
            geometry.Normals.Add(normal);
            geometry.Normals.Add(normal);

            //Add indices
            geometry.TriangleIndices.Add(baseIndex + 0);
            geometry.TriangleIndices.Add(baseIndex + 2);
            geometry.TriangleIndices.Add(baseIndex + 1);
            geometry.TriangleIndices.Add(baseIndex + 2);
            geometry.TriangleIndices.Add(baseIndex + 0);
            geometry.TriangleIndices.Add(baseIndex + 3);
        }
    }
}


Und hier noch der entsprechende Code für MainWindow.xaml:

<Window x:Class="Wpf3dTest1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Viewport3D Name="viewport3D">
            <Viewport3D.Camera>
                <PerspectiveCamera Position="20,10,10"
                                   LookDirection="-20,-10,-10"
                                   UpDirection="0,1,0"
                                   FieldOfView="45"
                                   NearPlaneDistance="1"
                                   FarPlaneDistance="100"></PerspectiveCamera>
            </Viewport3D.Camera>
            <ModelUIElement3D>
                <AmbientLight Color="White"></AmbientLight>
            </ModelUIElement3D>
        </Viewport3D>
    </Grid>
</Window>

Mittwoch, 24. Juli 2013

Windows 7 - Tageszeitabhängiger Hintergrund mit C#

(English version) Ich wollte unbedingt einen tageszeitabhängigen Hintergrund am PC haben. Windows kann das prinzipiell nicht, und ich wollte auch keine fremde Software installieren. Mit ein paar Zeilen C# war es dann jedoch ganz einfach möglich.

Ich habe einen Ordner, in dem 24 Dateien drin sind, 00.jpg, 01.jpg, ... 23.jpg. Diese sind also für von 0 bis 1 Uhr, und so weiter. Über C# wird die Datei dann als Desktophintergrund gesetzt, wie es auch hier beschrieben ist. Mein Code sieht folgendermaßen aus:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.InteropServices;

namespace TimeDependantBg
{
    class Program
    {
        [DllImport("user32.dll")]
        private static extern Int32 SystemParametersInfo(UInt32 uiAction, UInt32 uiParam, String pvParam, UInt32 fWinIni);

        private static UInt32 SPI_SETDESKWALLPAPER = 20;
        private static UInt32 SPIF_UPDATEINIFILE = 0x1;

        static void Main(string[] args)
        {
            int hour;
            string basePath = @"C:\Users\Udo\TimeDependantBg\";
            string fileName;
            while (true)
            {
                hour = DateTime.Now.Hour;

                fileName = basePath + (hour % 24).ToString("00") + ".jpg";

                if (System.IO.File.Exists(fileName))
                {
                    SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, fileName, SPIF_UPDATEINIFILE);
                }
                object lockObj = new object();
                lock (lockObj)
                {
                    Monitor.Wait(lockObj, 100000);// ms, alle 100 Sekunden check
                }
            }
        }
    }
}


Danach wird noch eine Verknüpfung in C:\Users\Udo\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup zu TimeDependantBg.exe angelegt, sodass das Prorgamm automatisch beim Systemstart geöffnet wird.

Sonntag, 21. Juli 2013

ATmega als I²C Master

(English version) Um am ATmega I²C als Master zum laufen zu kriegen war ein riesiger Kampf. Ich bin jedoch auf eine Version gekommen, die KEINE Probleme mit Compiler oder ähnlichem macht. Im Endeffekt habe ich Code aus einer fertigen Implementierung von Manfred Langemann benutzt, diese Implementierung einzubinden klappte bei mir leider nicht, daher habe ich den Code herauskopiert. Um also an einem ATmega I²C benutzen zu können muss man (wenn man es so wie ich macht) die folgenden Zeilen am Anfang des Programmes einfügen:

#define F_CPU 16000000

#define TRUE 1
#define FALSE 0

#include <avr/interrupt.h>

//#include "General.h"
//#include "TWI_Master.h"

/****************************************************************************
  TWI State codes
****************************************************************************/
// General TWI Master staus codes                     
#define TWI_START                    0x08  // START has been transmitted 
#define TWI_REP_START                0x10  // Repeated START has been transmitted
#define TWI_ARB_LOST                0x38  // Arbitration lost

// TWI Master Transmitter staus codes                     
#define TWI_MTX_ADR_ACK                0x18  // SLA+W has been tramsmitted and ACK received
#define TWI_MTX_ADR_NACK            0x20  // SLA+W has been tramsmitted and NACK received
#define TWI_MTX_DATA_ACK            0x28  // Data byte has been tramsmitted and ACK received
#define TWI_MTX_DATA_NACK            0x30  // Data byte has been tramsmitted and NACK received

// TWI Master Receiver staus codes 
#define TWI_MRX_ADR_ACK                0x40  // SLA+R has been tramsmitted and ACK received
#define TWI_MRX_ADR_NACK            0x48  // SLA+R has been tramsmitted and NACK received
#define TWI_MRX_DATA_ACK            0x50  // Data byte has been received and ACK tramsmitted
#define TWI_MRX_DATA_NACK            0x58  // Data byte has been received and NACK tramsmitted

// TWI Slave Transmitter staus codes
#define TWI_STX_ADR_ACK                0xA8  // Own SLA+R has been received; ACK has been returned
#define TWI_STX_ADR_ACK_M_ARB_LOST    0xB0  // Arbitration lost in SLA+R/W as Master; own SLA+R has been received; ACK has been returned
#define TWI_STX_DATA_ACK            0xB8  // Data byte in TWDR has been transmitted; ACK has been received
#define TWI_STX_DATA_NACK            0xC0  // Data byte in TWDR has been transmitted; NOT ACK has been received
#define TWI_STX_DATA_ACK_LAST_BYTE    0xC8  // Last data byte in TWDR has been transmitted (TWEA = “0”); ACK has been received

// TWI Slave Receiver staus codes
#define TWI_SRX_ADR_ACK                0x60  // Own SLA+W has been received ACK has been returned
#define TWI_SRX_ADR_ACK_M_ARB_LOST    0x68  // Arbitration lost in SLA+R/W as Master; own SLA+W has been received; ACK has been returned
#define TWI_SRX_GEN_ACK                0x70  // General call address has been received; ACK has been returned
#define TWI_SRX_GEN_ACK_M_ARB_LOST    0x78  // Arbitration lost in SLA+R/W as Master; General call address has been received; ACK has been returned
#define TWI_SRX_ADR_DATA_ACK        0x80  // Previously addressed with own SLA+W; data has been received; ACK has been returned
#define TWI_SRX_ADR_DATA_NACK        0x88  // Previously addressed with own SLA+W; data has been received; NOT ACK has been returned
#define TWI_SRX_GEN_DATA_ACK        0x90  // Previously addressed with general call; data has been received; ACK has been returned
#define TWI_SRX_GEN_DATA_NACK        0x98  // Previously addressed with general call; data has been received; NOT ACK has been returned
#define TWI_SRX_STOP_RESTART        0xA0  // A STOP condition or repeated START condition has been received while still addressed as Slave

// TWI Miscellaneous status codes
#define TWI_NO_STATE                0xF8  // No relevant state information available; TWINT = “0”
#define TWI_BUS_ERROR                0x00  // Bus error due to an illegal START or STOP condition

#define TWIM_READ    1
#define TWIM_WRITE   0

/*******************************************************
 Public Function: TWIM_Init

 Purpose: Initialise the TWI Master Interface

 Input Parameter:
     - uint16_t    TWI_Bitrate (Hz)

 Return Value: uint8_t
     - FALSE:    Bitrate too high
     - TRUE:        Bitrate OK

*******************************************************/
uint8_t TWIM_Init (uint32_t TWI_Bitrate)
    {
/*
** Set TWI bitrate
** If bitrate is too high, then error return
*/
    TWBR = ((F_CPU/TWI_Bitrate)-16)/2;
    if (TWBR < 11) return FALSE;

    return TRUE;
    }
/*******************************************************
 Public Function: TWIM_Start

 Purpose: Start the TWI Master Interface

 Input Parameter:
     - uint8_t    Device address
     - uint8_t    Type of required Operation:
                TWIM_READ: Read data from the slave
                TWIM_WRITE: Write data to the slave

 Return Value: uint8_t
      - TRUE:        OK, TWI Master accessible
     - FALSE:    Error in starting TWI Master

*******************************************************/
uint8_t TWIM_Start (uint8_t Address, uint8_t TWIM_Type)//1 = read, 0 = write
    {
    uint8_t        twst;
/*
** Send START condition
*/
    TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN);
/*
** Wait until transmission completed
*/
    while (!(TWCR & (1<<TWINT)));
/*
** Check value of TWI Status Register. Mask prescaler bits.
*/
    twst = TWSR & 0xF8;
    if ((twst != TWI_START) && (twst != TWI_REP_START)) return FALSE;
/*
** Send device address
*/
    TWDR = (Address<<1) + TWIM_Type;
    TWCR = (1<<TWINT)|(1<<TWEN);
/*
** Wait until transmission completed and ACK/NACK has been received
*/
    while (!(TWCR & (1<<TWINT)));
/*
** Check value of TWI Status Register. Mask prescaler bits.
*/
    twst = TWSR & 0xF8;
    if ((twst != TWI_MTX_ADR_ACK) && (twst != TWI_MRX_ADR_ACK)) return FALSE;

    return TRUE;
    }
/*******************************************************
 Public Function: TWIM_Stop

 Purpose: Stop the TWI Master

 Input Parameter: None

 Return Value: None

*******************************************************/
void TWIM_Stop (void)
    {
/*
** Send stop condition
*/
    TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWSTO);
/*
** Wait until stop condition is executed and bus released
*/
    while (TWCR & (1<<TWINT));
    }
/*******************************************************
 Public Function: TWIM_Write

 Purpose: Write a byte to the slave

 Input Parameter:
     - uint8_t    Byte to be sent

 Return Value: uint8_t
      - TRUE:        OK, Byte sent
     - FALSE:    Error in byte transmission

*******************************************************/
uint8_t TWIM_Write (uint8_t byte)
    {
    uint8_t   twst;
/*
** Send data to the previously addressed device
*/
    TWDR = byte;
    TWCR = (1<<TWINT)|(1<<TWEN);
/*
** Wait until transmission completed
*/
    while (!(TWCR & (1<<TWINT)));
/*
** Check value of TWI Status Register. Mask prescaler bits
*/
    twst = TWSR & 0xF8;
    if (twst != TWI_MTX_DATA_ACK) return 1;

    return 0;
    }
/*******************************************************
 Public Function: TWIM_ReadAck

 Purpose: Read a byte from the slave and request next byte

 Input Parameter: None

 Return Value: uint8_t
      - uint8_t    Read byte

*******************************************************/
uint8_t TWIM_ReadAck (void)
    {
    TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWEA);
    while (!(TWCR & (1<<TWINT)));   

    return TWDR;
    }
/*******************************************************
 Public Function: TWIM_ReadAck

 Purpose: Read the last byte from the slave

 Input Parameter: None

 Return Value: uint8_t
      - uint8_t    Read byte

*******************************************************/
uint8_t TWIM_ReadNack (void)
    {
    TWCR = (1<<TWINT)|(1<<TWEN);
    while(!(TWCR & (1<<TWINT)));
   
    return TWDR;
    }

Beschleunigungssensor MPU-6050 mit Raspberry PI auslesen

(English version) Der Beschleunigungssensor MPU-6050 ist ziemlich vielseitig, klein und billig. Er kann mit bis zu 6 V betrieben werden und liefert Beschleunigungswerte und sogar Gyroskopwerte (also Drehung in Grad pro Sekunde). So ein Sensor ist also das ideale Kernstück für einen Quadrocopter.

Da man mit einem Raspberry PI deutlich einfacher debuggen kann als mit Atmegas habe ich den Beschleunigungssensor zuerst mal mit dem Raspberry PI angesteuert. Das Grundprinzip ist ziemlich einfach, man muss nur einmal herausfinden wie es geht. Ich benutze am Raspberry PI die bcm2835-Library (http://www.airspayce.com/mikem/bcm2835/).

Das ganze lässt sich mit I²C auch über einen ATmega machen, dafür benutze ich diese Implementierung von I²C, dieser Post hilft da sicherlich weiter.

Um jetzt z.B. die x-Beschleunigung (Am Raspberry PI) auszumessen geht man folgendermaßen vor:

#include <bcm2835.h>
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main (int atgc, char** argv)
{
    bcm2835_init();
    bcm2835_i2c_begin();

    char addr = 0x68;

    //I found this address somewhere in the internet...
    char buf[1];
    char regaddr[2];
    int x = 0;
    int ret;

    bcm2835_i2c_setSlaveAddress(addr);

    //disable sleep mode!!!!!

    regaddr[0] = 107;
    regaddr[1] = 0;
    //This is the basic operation to write to an register
    //regaddr[0] is the register address
    //regaddr[1] is the value
    bcm2835_i2c_write(regaddr, 2);
   
    regaddr[0] = 59;
    ret = BCM2835_I2C_REASON_ERROR_DATA;
    while(ret != BCM2835_I2C_REASON_OK)
    {
        //This is the basic operation to read an register
        //regaddr[0] is the register address
        //buf[0] is the value
        bcm2835_i2c_write(regaddr, 1);
        ret = bcm2835_i2c_read(buf, 1);
    }
    x = buf[0]<<8;

    regaddr[0] = 60;
    ret = BCM2835_I2C_REASON_ERROR_DATA;
    while(buf[0] == 99)
    {
        bcm2835_i2c_write(regaddr, 1);
        ret = bcm2835_i2c_read(buf, 1);
    }
    x += buf[0];


    //because of the sign, we have here 32-bit integers,
    //the value is 16-bit signed.

    if (x & 1<<15)
    {
        x -= 1<<16;
    }

    double x_val = x;
    x_val = x_val / 16384;

    //This is only valid if the accel-mode is +- 2g
    //The range can be controlled via the 
    //GYRO_CONFIG and ACCEL_CONFIG registers

    printf("accel: %g\n", x_val);

    bcm2835_i2c_end();
}


Dies kann jetzt natürlich noch beliebig erweitert werden, um Beschleunigungs-Werte und Gyro-Werte auszulesen. Da hiermit aber der grundlegende Zugriff auf die Register geklärt sein sollte, werde ich hier nur auf die Register Map verweisen: http://www.invensense.com/mems/gyro/documents/RM-MPU-6000A.pdf

Samstag, 20. Juli 2013

Eine kleine Klimaanlage für daheim

(English version) Die Kühlmethode, welche für die Kühlung von Bose-Einstein-Kondensaten auf wenige Nanokelvin benutzt wird funktioniert prinzipiell auch in normalen Umgebungen. Sie wird evaporative cooling bzw. Verdunstungskühlen genannt. Jeder kennt das von einem Tee, bei dem die heißen Wasserpartikel verdampfen (das ist der Dunst über der Tasse), sodass die Durchschnittstemperatur im Tee kälter wird.

Um mit diesem Prinzip ein Zimmer zu kühlen braucht man lediglich Wasser, welches verdunstet. Das erhöht zwar die Luftfeuchtigkeit im Zimmer, jedoch senkt es auch die Temperatur. Um das Wasser nun effektiv zum Verdunsten zu bringen habe ich folgenden Aufbau gebaut:
Ein Lüfter (xilence, 92 mm Kantenlänge, man hört so gut wie nichts von diesem Lüfter) steht auf Stelzen im Wasserbad. Die Stelzen sind 6 mm Alu-Stangen, in welche ich den Lüfter geschraubt habe. Von der Verwendung von Holz würde ich abraten, Holz schimmelt und verfault im Wasser. Danach wird um alles einfach ein Tuch gewickelt (das Tuch, welches ich momentan habe saugt das Wasser nur sehr langsam hoch, sodass nur die untere Hälfte des Tuches nass ist), welches oben am Lüfter mit einem Gummi befestigt ist. Der Lüfter pustet nun von oben Luft rein, welche dann durch das Tuch entweicht. Dabei nimmt diese Luft Wasser aus dem Tuch mit, welches für dieses Verdunsten Energie aufnimmt, was den Raum kühlt.

Ganz rechts im Bild sieht man mein Thermometer (Thermometer) mit dem ich die Lufttemperatur messe. Das Ergebnis: Die Luft, in 5 cm Abstand zur "Klimaanlage" ist 1 Grad kälter als das restliche Zimmer. Der Lüfter pumpt etwa 60 m³ pro Stunde durch das Tuch, das heißt, dass die Luft in einem Zimmer der Größe 5 x 4 x 2 m³ alle 40 Minuten komplett ausgetauscht wird. Natürlich kann man jetzt nicht sagen, dass die Temperatur jede Stunde um 1,5 Grad runter geht, da der Raum, die Möbel und die Menschen in dem Raum auch noch eine gewisse Temperatur haben, jedoch sollte dieses Gerät die Temperatur in heißen Sommermonaten etwas erträglicher machen.

Ich werde in Zukunft etwas weiter experimentieren, evtl ein anderes (saugstärkeres) Tuch, ein größerer Lüfter, etc. Falls es bedeutende Verbesserungen in der Kühlleistung gibt werde ich hier noch etwas dazu schreiben.

Freitag, 19. Juli 2013

Arduino-Code auf dem Atmega

(English version) Der folgende Beitrag stammt nicht von Udo. Ich habe die große Ehre, einen Gastbeitrag für seinen Blog verfassen zu dürfen ;)

Einen Atmega "von Hand" in C zu programmieren kann ganz schön aufwändig sein - besonders dann, wenn man "besondere" Features wie SPI, I2C etc. nutzen möchte. Hier muss man sich häufig mit kryptischem Code und Register-Geschiebe herumschlagen. Viel einfacher hat man es da, wenn man für Arduino entwickelt. Dank der benutzerfreundlichen Befehlssätze lassen sich diese Wünsche schnell und einfach auch von Anfängern umsetzen. Außerdem gibt es für eine Vielzahl an externer Hardware (z.B. Funk- oder Ultraschallmodule) fertige Libraries. Der Nachteil des Arduinos ist allerdings sein recht hoher Preis. Man möchte schließlich nicht für jedes Projekt einen neuen Arduino für 20 Euro anschaffen.

Da wäre es doch optimal, wenn sich der niedrige Preis und die kompakte Bauform eines Atmegas mit der Benutzerfreundlichkeit eines Arduinos kombinieren liese. Gute Nachricht: dies ist problemlos möglich!

Ich werde es hier am Beispiel eines Atmega8 erklären. Als Erstes braucht man den Mikrocontroller mit passender Grundbeschaltung (Stromversorgung und externer Quarz). Der genaue Aufbau ist z.B auf dieser Seite dokumentiert.

Zudem benötigt man einen Programmer für den Atmega. Ich habe einen USBasp verwendet, den ich günstig bei Ebay erstanden habe. Auch der Programmer muss an den Mikrocontroller angeschlossen werden - wie genau ist der obene genannten Seite zu entnehmen.

Nun kann es mit der Programmierung losgehen. Dazu benötigt man die Arduino-Entwicklungsumgebung. Diese kann hier heruntergeladen werden. Nach der Installation wählt man unter "Datei - Beispiel - 01.Basics" das Beispiel "Blink". Dieses Beispiel lässt eine LED im Sekundentakt blinken. Bevor man den Code auf den Controller spielen kann, muss natürlich noch die LED anschließen. Hier muss man ein bisschen aufpassen: im Code wird mit der Arduino-Pinnummerierung gearbeitet (hier im Beispiel wird Pin 13 verwendet). Welchem Pin das am Atmega entspricht kann man dieser Übersicht entnehmen. Pin 13 des Arduino entspricht also Pin 19 am Atmega8. An diesen Pin wird dann die LED angeschlossen.

Jetzt kann der Code compiliert und auf den µC übertragen werden. Dazu muss zuerst unter "Tools - Programmer" der passende Programmer ausgewählt werden - in meinem Fall der USBasp. Je nach verwendetem Controller müsst ihr auch noch das passende Board auswählen. Ich verwende einen Atmega8, also wähle ich unter "Tools - Board" den "Arduino NG with Atmega8".
Um das Programm auf den Chip zu bringen, wird einfach "Datei - Upload mit Programmer" gewählt. Die Entwicklungsumgebung compiliert nun den Code und überträgt ihn auf den Atmega. Fertig ;)

Auf diesem Weg könnt ihr natürlich auch komplexere Programme auf den Mikrocontroller übertragen. Ich habe beispielsweise über SPI ansteuerte Funkmodule an den Atmega8 angeschlossen. Wie ich das gemacht habe, erkläre ich im nächsten Post.

Mittwoch, 17. Juli 2013

C# Windows Forms bzw. WPF - Fensterposition und Größe beibehalten

(English version) Es kann ziemlich praktisch sein, wenn bei einem Neustart der Anwendung jedes Fenster wieder seine Größe und Position hat. Dafür habe ich in C# eine eigene Klasse geschrieben: SizeSavedWindow. Der Quellcode für diese Klasse ist ganz unten im Post. Um nun ein Fenster genau an seiner Position zu halten muss hinter InitializeComponent(); einfach die folgende Zeile eingefügt werden:

SizeSavedWindow.addToSizeSavedWindows(this);

Diese Funktion kann auch für viele verschiedene Fenster aufgerufen werden, die Fenster werden per Name in die xml-Datei gespeichert, wenn es also keine Fenster mit gleichem Namen gibt klappt alles wie es soll. Diese Klasse gibt es auch für WPF Windows, dafür muss jedes "Form" durch "Window" ersetzt werden und auch die Events müssen etwas überarbeitet werden (window_IsVisibleChanged statt window_Shown). Falls jemand Interesse an dem Code für WPF hat kann ich den auch noch posten. Im Anschluss nun den Code für die Klasse SizeSavedWindow (der Namespace muss natürlich noch an das jeweilige Projekt angepasst werden):

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Runtime.Serialization;
using System.Xml;
using System.Windows.Forms;

namespace KeepSizeTest1
{
    public class SizeSavedWindow
    {
        // To keep a window at the same size and position, just add
        // SizeSavedWindow.addToSizeSavedWindows(this);
        // right after initialieComponent
        public static void addToSizeSavedWindows(Form window)
        {
            window.Shown += window_Shown;
        }

        static void window_Shown(object sender, EventArgs e)
        {
            Form window = (Form)sender;
            if (!window.Visible)
            {
                return;
            }
            if (File.Exists("sizes.xml"))
            {
                var stream = new FileStream("sizes.xml", FileMode.Open);
                var reader = XmlDictionaryReader.CreateTextReader(stream, new XmlDictionaryReaderQuotas());
                var deserializer = new DataContractSerializer(typeof(Dictionary<string, int[]>));
                windows = (Dictionary<string, int[]>)deserializer.ReadObject(reader, true);
                stream.Close();

                foreach (KeyValuePair<string, int[]> pair in windows)
                {
                    if (pair.Key == window.Name)
                    {
                        window.Height = pair.Value[0];
                        window.Width = pair.Value[1];
                        window.Top = pair.Value[2];
                        window.Left = pair.Value[3];
                        break;
                    }
                }
            }
            int[] sizes = new int[4];
            sizes[0] = window.Height;
            sizes[1] = window.Width;
            sizes[2] = window.Top;
            sizes[3] = window.Left;
            if (windows.ContainsKey(window.Name))
            {
                windows.Remove(window.Name);
            }
            windows.Add(window.Name, sizes);
            window.SizeChanged += window_SizeChanged;
            window.LocationChanged += window_LocationChanged;
        }

        static void window_SizeChanged(object sender, EventArgs e)
        {
            Form realSender = (Form)sender;
            if (windows.ContainsKey(realSender.Name))
            {
                windows[realSender.Name][0] = realSender.Height;
                windows[realSender.Name][1] = realSender.Width;

                var writer = new FileStream("sizes.xml", FileMode.Create);
                Type type = windows.GetType();
                var serializer = new DataContractSerializer(type);
                serializer.WriteObject(writer, windows);
                writer.Close();
            }
        }

        static void window_LocationChanged(object sender, EventArgs e)
        {
            Form realSender = (Form)sender;
            if (windows.ContainsKey(realSender.Name))
            {
                windows[realSender.Name][2] = realSender.Top;
                windows[realSender.Name][3] = realSender.Left;

                var writer = new FileStream("sizes.xml", FileMode.Create);
                Type type = windows.GetType();
                var serializer = new DataContractSerializer(type);
                serializer.WriteObject(writer, windows);
                writer.Close();
            }
        }

        static Dictionary<string, int[]> windows = new Dictionary<string, int[]>();
    }
}