Rack'n'Roll

Aus Das Projektwiki
Zur Navigation springen Zur Suche springen
Projekt Rack'n'Roll
das modulare Messsystem
Entwickler Peter Wiese
Hochschule Campus Velbert/Heiligenhaus
Modul Vertiefung Systemtechnik
Semester Wintersemester 2016/17
Professor Prof. Dr. rer. nat. Peter Gerwinski
Programmiersprache C++
genutzte Bibliotheken Arduino-Bibliotheken
Lizenz BSD Simplified Licence

Im Projekt Rack'n'Roll wurde ein System zur Messdatenauswertung eines modularen Messsystems mit Spannungsbegrenzung entwickelt. Dieses System wurde dabei echtzeitfähig entwickelt und basiert auf einem Arduino Mega 2560. Entwickelt wurde das Programm von Peter Wiese im Rahmen des Moduls Vertiefung Systemtechnik am Campus Velber/Heiligenhaus der Hochschule Bochum und wird in einem modularen Messsystem, welches im Modul Elektronik und digitale Signalverarbeitung aufgebaut wurde, eingesetzt. Das Projekt ist mittels der Arduino IDE in C++ geschrieben und steht unter BSD Simplified Licence [1].

Einleitung

Eine Platine des Systems

Im Rahmen des Moduls Sensortechnik und digitale Signalverarbeitung am Campus Velbert/Heiligenhaus wurde eine modulare Messeinrichtung entwickelt und aufgebaut. Dieses besteht aus einzelnen, wechselbaren Platinen welche verschiedene Aufgaben erfüllen sollen. So wurden bisher z.B. eine Platine entwickelt mit denen zwei Signale Addiert oder Subtrahiert werden können, eine Platine zur Strom-Spannungswandlung und eine Platine zur Implementierung diverser Filter. Da in diesen Schaltungen Operationsverstärker verwendet werden, welche nur in begrenzten Spannungsbereichen arbeiten, wurden auf den Platinen außerdem Kontrollschaltungen eingebaut. Diese erkennen, ob ein bestimmter Schwellwert überschritten wurden und informieren daraufhin den Nutzer, da es dadurch zu Messfehlern kommt. Alle Platinen sollen außerdem digital Ausgewertet werden können. Genau bei den letzten beiden Punkten kommt das hier dokumentierte Projekt ins Spiel.

Ziel dieses Projektes ist die Implementierung der digitalen Auswertung der Signale des Messaufbaus, sowie die Verwaltung der Kontrollschaltung. Dabei sollen die Signale mittels eines Arduino Mega 2560 verarbeitet und auf einem Computer ausgegeben werden. Außerdem soll der Arduino die Spannungsbegrenzung zurücksetzen und verwalten können.

Konzept und Echtzeitfähigkeit

der verwendete Arduino Mega

Das Konzept des Systems ist relativ einfach gehalten. Die einzelnen Platinen sind an ein Bussystem angeschlossen, welches sich um die Spannungsversorgung und die Kommunikation mit dem Arduino kümmert. Die genaue Definition des Bussystems ist nachfolgend dargestellt.

Bahnnummer Verwendung
1 -12 V Spannungsversorgung
2 +12 V Spannungsversorgung
3 Masse
4 bis 14 analoge Ausgangssignale der Platinen
15 Reset der Spannungsbegrenzung
16 bis 29 Anzeige der einzelnen Spannungsbegrenzungen
30 ODER-Verknüpfte Begrenzung für der Interrupt
31 +5 V Spannungsversorgung
32 -5 V Spannungsversorgung

Dabei werden die analogen Ausgangssignale der Bahnen 4 bis 14 direkt mit den analogen Eingängen des Arduino verbunden um die schnelle Auswertung zu ermöglichen. Die Bahn 15 wird genutzt um die Spannungsbegrenzung zurückzusetzen und ist mit einem Ausgang des Arduino verbunden. Die Bahn 30 ist mit einem Interrupt-Pin des Arduino verbunden und sorgt dafür, dass ein Interrupt aufge- rufen wird, sobald eine Platine in die Begrenzung gerät. Anschließend können die Bahnen 16 bis 29 ausgewertet werden um herauszufinden, welche Platine dafür verantwortlich ist.

Auf der Gegenseite, also am Arduino, sind dann die Bahnen mit den Ports verbunden. Die konkrete Pin-Belegung des Arduino kann der nachfolgenden Tabelle entnommen werden. Dabei sind neben den Äquivalenten zum Bussystem auch noch weitere Pins definiert, welche für Schalter reserviert sind, mit denen ausgewählt werden kann, welche Analog-Pins abgefragt werden.

Port Verwendung
A4 bis A14 analoger Eingang, Bahnen 4 bis 14 vom Bus
2 Interrupt-Pin für Begrenzung, Bahn 30 vom Bus
3 Interrupt-Pin zum Neustart, Bahn 15 vom Bus
ungerade Pins 31 bis 51 Input-Pins für Schalter
gerade Pins 22 bis 48 Spannungsbegrenzungen, Bahnen 16 bis 29 vom Bus

Das System muss außerdem echtzeitfähig sein, damit die Messdaten mit einer definierten Abtastrate aufgenommen werden können. Die Abtastrate muss dabei für eine genaue Messung, nach dem nyquist-shannonschen Abtasttheorem[2], mindestens der doppelten maximalen Frequenz des abzutastenen Signals entsprechen.

Da die Werte alle in einem äquidistanten Zeitabstand abgetastet werden sollen, handelt es sich hierbei um ein System mit fester Echtzeit, bei dem eine Abtastrate von genau 8 kHz gegeben ist. Technisch realisiert wird dies über einen zeitlich gesteuerten Interrupt, welcher alle 125 µs aufgerufen wird und den Messwert in einem FIFO-Stack speichert. Die Ausgabe erfolgt über die serielle Schnittstelle mit einer Baud-Rate von 250000 Bd und wird immer in der Zeit, in der kein Interrupt aufgerufen wird ausgeführt. Die genauen Details der Realisierung werden im nachfolgenden Abschnitt erläutert.

Programmierung

Allgemeines

Für die softwareseitige Umsetzung des Projekts wird die offizielle Entwicklungsumgebung, die Arduino IDE, in der Version 1.8.1 genutzt. Die Programmierung selber erfolgt in C bzw. C++, wobei hauptsächlich die, unter der GNU Lesser General Public Licence[3] veröffentlichten, Arduino-Bibliotheken verwendet werden.

Der Vorteil der Arduino IDE sind die beiden vordefinierten Methoden void setup() und void loop(). Die Methode setup() wird dabei direkt zu Programmstart aufgeru- fen und dient hauptsächlich zur Initialisierung der I/O-Pins, der seriellen Schnittstelle und der Interrupts. Die Methode loop() ist dann für die zyklische Ausführung des Programmcodes zuständig.

Ein weiterer Vorteil der Arduino IDE sind die automatisch eingebundenen Arduino-Bibliotheken, welche eine einfache Ansteuerung der Pins ermöglichen. Die wichtigsten Methoden daraus sind void pinMode(uint8_t pin, uint8_t mode), welche verwendet wird um die Pins als Input oder Output-Pins zu initialisieren und die Methode int digitalRead(uint8_t pin), welche es ermöglicht den aktuellen Zustand eines Input-Pins auszulesen.

Zeitgesteuerte Interrupts

Für die Realisierung der Echtzeitfähigkeit wurden zeitgesteuerte Interrupts verwendet. Diese haben den Vorteil, dass sie in fest definierten Intervallen, unabhängig vom anderen Programmcode aufgerufen werden. Das einzige was man dabei beachten muss, dass die Funktion während des Interrupts so kurz wie möglich gehalten werden sollte, da zeitgleich keine anderen Methoden auf dem Arduino ausgeführt werden können. Die verwendete Methode des Interrupts wird auch als Clear Timer on Compare Match bzw. als CTC-Mode bezeichnet.

Das Grundprinzip der Interrupt ist dabei relativ einfach. Es gibt im Mikrocontroller verschiedene Timer mit einzelnen Countern, welche bei jedem Tick der Systemuhr hochgezählt werden. Erreicht dieser Counter einen bestimmten Wert, welcher im Compare Match Register festgelegt ist, wird der Interrupt ausgelöst und der Counter zurückgesetzt. Dabei muss beachtet werden, dass es 16-Bit- und 8-Bit-Timer gibt, welche maximal 65535 bzw. 255 Counter-Werte speichern können. Würde der Counter jetzt mit der normalen Taktfrequenz von 16 MHz inkrementiert werden, wäre die langsamste Zeit pro Interrupt 4 ms für den 16-Bit- und 16 µs für den 8-Bit-Timer. Da dies für den allgemeinen Gebrauch eher ungeeignet ist, muss die Taktfrequenz des Timers über einen sogenannten Prescaler begrenzt werden. Die Taktfrequenz wird dann über folgende Formel berechnet:

f_timer = f_Arduino / Prescaler

Zum Festlegen des Prescalers müssen verschiedene Bits im Register des Timers gesetzt werden. In diesem Projekt wird ausschließlich mit dem 8-Bit-Timer timer2 gearbeitet. Die nötigen Bits zum setzen des Prescalers von diesem Timer sind in der nachfolgenden Tabelle angegeben:

CS22 CS21 CS20 Prescaler
0 0 1 1
0 1 0 8
0 1 1 32
1 0 0 64

Wurde der Prescaler entsprechend eingestellt, muss als nächstes der entsprechende Wert für das Compare Match Register gefunden werden, damit der Interrupt in der gewünschten Frequenz aufgerufen wird. Der Vergleichswert kann über folgende Formel bestimmt werden.[4]

Vergleichswert = (f_Arduino / (Prescaler * f_gewünscht)) - 1

Für die in diesem Projekt gewünschte Frequenz von 8 kHz bedeutet dies, dass bei einem Prescaler von 8 der Wert im Compare Match Register 249 betragen muss. Im Quellcode müssen diese Vorbereitungen in der setup()-Methode getroffen werden. Im nachfolgenden Beispiel ist eine setup()-Methode dargestellt, mit der timer2 auf 8 kHz festgelegt wird.

void setup() {
  cli(); //alle Interrupts deaktiviert
  TCCR2A = 0;// TCCR2A-Register auf 0 setzen
  TCCR2B = 0;// TCCR2B-Register auf 0 setzen
  TCNT2  = 0;// Initialisierung des Counter-Werts
  OCR2A = 249;// Vergleichswert setzen
  TCCR2A |= (1 << WGM21); // CTC-Mode aktivieren
  TCCR2B |= (1 << CS21); // CS21-Bit setzen
  TIMSK2 |= (1 << OCIE2A); //Timer Compare Interrupt aktivieren
  sei(); // alle Interrupts aktivieren
}

Wurde der zeitgesteuerte Interrupt ordentlich initialisiert, muss die Interrupt Service Routine (kurz ISR) implementiert werden. Die ISR ist dabei die Methode, welche immer in der vorher festgelegten Frequenz aufgerufen werden soll. Für den bereits erläuterten timer2 wird die ISR wie folgt implementiert:

ISR(TIMER2_COMPA_vect) {
	// Meine Interrupt Service Routine 
}

Innerhalb der ISR wird dann die Funktion zum Auslesen der Messdaten aufgerufen, welche im nächsten Abschnitt erläutert wird.

Auslesen der Messdaten

Die zu messenden Daten des Systems werden über das Bussystem des Racks direkt in die analogen Eingänge des Arduino eingespeist. Dafür werden die Pins A4-A14 des Boards genutzt. Da die analogen Pins jedoch nur nacheinander abgefragt werden können, teilen sich alle Eingänge die Abtastfrequenz von 8 kHz. Dies bedeutet, wenn 8 Pins abgefragt werden sollen, wird jeder Pin nur mit einer Frequenz von 1 kHz abgetastet. Um trotzdem eine möglichst hohe Frequenz zu gewährleisten kann über Kippschalter direkt ausgewählt werden, welche Pins abgetastet werden sollen. Die Schalter sind dabei mit den ungeraden Pins von 31 bis 51 verbunden. Dementsprechend muss um den Pin A4 auszulesen der Pin 31, für A5 die 33 und für A14 die 51 geschaltet werden.

Um dies entsprechend zu realisieren müssen die Pins entsprechend in der Methode setup() initialisiert werden. Die Realisierung der Methode ist im nachfolgenden Listing zu sehen.

uint8_t* activePorts;
uint8_t  numPorts = 0;
uint8_t  portCount = 0;

void setup() {
  for (int i = 22; i <= 51; i++)
  	pinMode(i, INPUT);
  setupPorts();
  startAnalogRead(portCount);
}

void setupPorts(){
  int ports[11];
  int count = 0;
  for(int i=31;i<=51;i+=2)
    ports[count++] = digitalRead(i);    
  for(int i=0;i<11;i++)
    if(ports[i])
      numPorts++;
  activePorts = (uint8_t*)malloc(numPorts*(sizeof(uint8_t)));
  count = 0;
  for(int i=0;i<11;i++)
    if(ports[i])
    	activePorts[count++] = i+4;
}

Dabei werden als erstes alle Pins von 22 bis 51 als Input-Pins initialisiert. Anschließend wird die Methode setupPorts() aufgerufen. In dieser Methode wird als erstes ausgewertet, wie viele der analogen Eingänge ausgewertet werden sollen. Anschließend wird dem Zeiger activePorts genau so viel Speicher wie benötigt zugewiesen um im nächsten Schritt die Nummern der abzufragenden Ports in das Array zu schreiben. Ist die geschehen, ist die Initialisierung beendet und die Methode startAnalogRead(uint8_t pin) wird aufgerufen. Die genaue Funktionsweise dieser wird zu einem späteren Zeitpunkt erläutert.

Sind alle Initialisierungen abgeschlossen, beginnt das eigentliche Programm, das Auslesen der Messdaten. Ursprünglich wurde dafür die Methode analogRead(uint8_t pin) aus der Arduino-Bibliothek verwendet. Jedoch benötigt diese Funktion ca. 112 µs für das Auslesen. Da der Interrupt jedoch alle 125 µs aufgerufen wird, ist die Ausführung dieser Methode innerhalb der ISR eher ungeschickt.

Da die Arduino IDE jedoch den Quelltext zu allen Funktionen der Arduino-Bibliothek mitliefert, wurde sich die Methode analogRead(uint8_t pin) genauer angeschaut. Dabei ist aufgefallen, dass der Großteil der Zeit den diese Funktion benötigt darin besteht auf den Analog-Digital-Wandler (kurz ADC) zu warten. Also wurde die Funktion umgeschrieben und in die beiden Methoden startAnalogRead(uint8_t pin) und endAnalogRead() ausgelagert. In der Methode startAnalogRead(uint8_t pin) wird dabei der auszulesende Pin an den ADC übergeben und die Messung gestartet. Die Methode endAnalogRead() gibt dann das Ergebnis des ADC aus. Ausgeführt werden beide Methoden letztendlich in der ISR.

void startAnalogRead(uint8_t pin) {
  #if defined(ADMUX)
    ADMUX = (analog_reference << 6) | (pin & 0x07);
  #endif
  
  #if defined(ADCSRB) && defined(MUX5)
    ADCSRB = (ADCSRB & ~(1 << MUX5)) | (((pin >> 3) & 0x01) << MUX5);
  #endif
    sbi(ADCSRA, ADSC);
}

int endAnalogRead() {
  #if defined(ADCSRA) && defined(ADCL)
    while (bit_is_set(ADCSRA, ADSC));
    low  = ADCL;
    high = ADCH;
  #else
    low  = 0;
    high = 0;
  #endif
    return (high << 8) | low;
}

Um die Messwerte in der ISR zu speichern und im Hauptprogramm auszugeben wurde ein FIFO-Stack genutzt. Die Implementierung des Stack in der ISR und dem Hauptprogramm sieht dabei wie folgt aus:

uint8_t reader = 0,writer = 0,numPorts = 0,portCount = 0;
int dataStack[256];
uint8_t portStack[256];
uint8_t* activePorts;
int isActive = 1;

ISR(TIMER2_COMPA_vect) {
  portStack[writer] = activePorts[portCount];
  dataStack[writer++] = endAnalogRead();
  if((++portCount)>=numPorts)
    portCount = 0;
  startAnalogRead(activePorts[portCount]);
}

void loop() {
  if(isActive)
    if (reader != writer){
      Serial.println(portStack[reader]);
      Serial.println(dataStack[reader++]);
    }
}

Gearbeitet wird hierbei mit den beiden Zeigern reader und writer . Dabei wird in der ISR in die beiden Stacks geschrieben, während im Hauptprogramm immer der nächste Wert des Stacks ausgegeben wird, wenn die Zeiger unterschiedliche Werte haben. Außerdem wird im Hauptprogramm noch die Variable isActive abgefragt. Diese sorgt dafür, dass keine Messwerte ausgegeben werden, wenn eine Platine die maximale Spannung überschreitet und die Kontrollfunktion aktiv wird.

Insgesamt wurde durch die Änderungen an der Methode analogRead(uint8_t pin) die Rechenzeit in der ISR stark verkürzt. Jetzt benötigt die Methode nur noch ca. 6 µs. Durch die Realisierung der Auswertung mittels zeitgesteuertem Interrupt kann außerdem die Echtzeitfähigkeit des Systems garantiert werden.

Für die Umsetzung der seriellen Ausgabe ist noch zu beachten, dass in der setup()-Methode noch folgende Zeile eingefügt werden muss:

Serial.begin(250000);

Realisierung der Kontrollfunktion

Die wirklich entscheidende Funktion des Mikrocontrollers ist neben der Messdatenauswertung ist die Erkennung, ob an einer der Platinen des Racks eine zu hohe Spannung anliegt. Realisiert wird dies über einen extern ausgelösten Interrupt. Diese können bei einem Arduino Mega an den Pins 2, 3, 18, 19, 20 und 21 ausgelöst werden. Dafür müssen diese jedoch in der setup()-Methode initialisiert werden. Dies erfolgt über folgende Zeile:

void setup() {
  attachInterrupt(digitalPinToInterrupt(2), isr, RISING);
  attachInterrupt(digitalPinToInterrupt(3), reset, RISING);
}

Dabei wird in der Methode zuerst der Pin angegeben, anschließend die Funktion, welche beim Interrupt aufgerufen werden soll und abschließend noch der Modus des Interrupts. Für den Modus gibt es diverse Möglichkeiten, welche im Folgenden vorgestellt werden.

  • LOW: löst immer aus, wenn keine Spannung am Pin anliegt
  • CHANGE: löst aus, sobald sich der Zustand des Pins ändert
  • HIGH: löst aus, wenn Pin von LOW auf HIGH wechselt
  • FALLING: löst aus, wenn Pin von HIGH auf LOW wechselt

Mit der gezeigten Initialisierung aus der setup()-Methode werden demnach die Pins 2 und 3 als Interruptpins initialisiert, welche die Methoden isr () bzw. reset() aufrufen, sobald sich der Zustand der Pins von LOW auf HIGH ändert. Die Methode isr () stoppt dann die Messung und gibt aus, an welcher Platine eine zu hohe Spannung anliegt.

void isr() {
  isActive = 0;
  int port = 16;
  for(int i=22;i<51;i+=2){
    if(digitalRead(i)){
      Serial.print("Line number ");
      Serial.print(port);
      Serial.print(" is causing an interrupt");
    }
  }
}

Schaltungstechnisch ist dies mittels Komperatoren realisiert, welcher den Interrupt auslöst und ein Flip-Flop schalten, sobald eine zu hohe Spannung anliegt. Der Ausgang des Flip-Flops ist dabei direkt mit einer der Bahnen 16 bis 29 des Bussystems verbunden, welche wiederum an den gerade Pins von 22 bis 48 angeschlossen sind. Der Interrupt an Pin 2 des Arduino wird von der Bahn 30 des Bussystems ausgelöst. Dabei sind dort alle Platinen ODER-Verknüpft angeschlossen. Für weitere Details zum elektrotechnischen Aufbau der Platinen und der Kontrollschaltung sei auf die Dokumentation des Projekts im Modul Elektronik[5] verwiesen.

Um die Messung wieder zu starten ist außerdem ein Druckknopf eingebaut. Dieser löst bei Betätigung einen Interrupt am Pin 3 des Arduino aus und setzt die Flip-Flops auf den einzelnen Platinen zurück. Beim Auslösen des Interrupts wird die Methode reset() aufgerufen. In dieser Methode werden die Zeiger zurückgesetzt und die auszulesenden Ports abgefragt. Anschließend wird die Messung wieder gestartet.

void reset() {
  reader = 0;
  writer = 0;
  setupPorts();
  isActive = 1;
}

Zusammenfassung und Ausblick

Im Projekt Rack’n’Roll wurde ein Programm für einen Mikrocontroller vom Typ Arduino Mega 2560 zur Aufnahme von Messdaten eines modularen Messsystems geschrieben. Eine Anforderung an dieses Programm war die Echtzeitfähigkeit. So sollte eine Abtastrate von genau 8 kHz gegeben sein. Realisiert wurde dies über einen zeitgesteuerten Interrupt, in dessen ISR die Messdaten in einem FIFO-Stack gespeichert werden. Die Ausgabe der Daten aus diesem Stack erfolgt dann im Hauptprogramm. Des Weiteren wurden auch noch ein Interrupt implementiert, der die Messung stoppt, sobald in einer der Platinen des Messsystems eine zu hohe Spannung anliegt. Im fertigen Programm wurden alle geforderten Funktionen implementiert.

Aufbauend auf diesem Projekt können noch einige weitere Projekte umgesetzt werden. Aktuell können die Messdaten vom Arduino nur mit dem seriellen Monitor der Arduino IDE ausgelesen werden. Dies ist für ein Messsystem jedoch nicht praktikabel, da auf diese Weise die Messdaten nur schwer weiterverarbeitet werden können. Daher muss im nächsten Schritt in diesem Projekt ein Programm geschrieben werden, welches die Daten vom Arduino empfängt, in einem gängigen Format (z.B. als CSV-Datei) speichert und eventuell sie direkt grafisch darstellt.

Einzelnachweise

  1. BSD Simplified Licence
  2. Claude E. Shannon: Communication in the Presence of Noise. In: Proceedings of the IEEE 37 (1948)
  3. GNU Lesser General Public Licence. Abgerufen am 22.02.2017
  4. Arduino Timer Interrupts. Abgerufen am 06.02.2017
  5. Marcel Schwarz, Nils Janke und Peter Wiese: Modulares Messverstärkersystem. Hochschule Bochum Campus Velbert/Heiligenhaus (2017). Betreuender Professor: Prof. Dr.-Ing. Dietmar Gerhardt