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 ;)

Keine Kommentare:

Kommentar veröffentlichen