Rack'n'Roll: Unterschied zwischen den Versionen
Pwiese (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Pwiese (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
(9 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
Zeile 1: | Zeile 1: | ||
{| class="wikitable" style="float:right; margin-left: 10px; width: 40%;" |
|||
!colspan="2"|Projekt Rack'n'Roll |
|||
|- |
|||
|colspan="2"| [[Datei:messsystem.jpg|300px|thumb|center|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'' <ref>[https://opensource.org/licenses/BSD-2-Clause BSD Simplified Licence]</ref>. |
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'' <ref>[https://opensource.org/licenses/BSD-2-Clause BSD Simplified Licence]</ref>. |
||
==Einleitung== |
==Einleitung== |
||
[[Datei:racknroll_platine.jpg|300px|thumb|right|Eine Platine des Systems]] |
|||
Im Rahmen des Moduls Sensortechnik und digitale Signalverarbeitung am Campus Velbert/Heiligenhaus wurde eine modulare Messeinrichtung entwickelt und |
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 |
aufgebaut. Dieses besteht aus einzelnen, wechselbaren Platinen welche verschiedene Aufgaben erfüllen sollen. So wurden bisher z.B. eine Platine entwickelt mit |
||
Zeile 10: | Zeile 41: | ||
==Konzept und Echtzeitfähigkeit== |
==Konzept und Echtzeitfähigkeit== |
||
[[Datei:ArduinoMega.jpg|300px|thumb|right|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. |
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. |
||
Zeile 17: | Zeile 49: | ||
|- |
|- |
||
|1 |
|1 |
||
|<nowiki>-12 |
|<nowiki>-</nowiki>12 V Spannungsversorgung |
||
|- |
|- |
||
|2 |
|2 |
||
|<nowiki>+12 |
|<nowiki>+</nowiki>12 V Spannungsversorgung |
||
|- |
|- |
||
|3 |
|3 |
||
Zeile 38: | Zeile 70: | ||
|- |
|- |
||
|31 |
|31 |
||
|<nowiki>+5 |
|<nowiki>+</nowiki>5 V Spannungsversorgung |
||
|- |
|- |
||
|32 |
|32 |
||
|<nowiki>-5 |
|<nowiki>-</nowiki>5 V Spannungsversorgung |
||
|} |
|} |
||
Zeile 67: | Zeile 99: | ||
|<nowiki>Spannungsbegrenzungen, Bahnen 16 bis 29 vom Bus</nowiki> |
|<nowiki>Spannungsbegrenzungen, Bahnen 16 bis 29 vom Bus</nowiki> |
||
|} |
|} |
||
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<ref>Claude E. Shannon: ''Communication in the Presence of Noise''. In: ''Proceedings of the IEEE 37'' (1948)</ref>, 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== |
==Programmierung== |
||
===Allgemeines=== |
===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''<ref>[https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public Licence]. Abgerufen am 22.02.2017</ref> veröffentlichten, Arduino-Bibliotheken verwendet werden. |
|||
Der Vorteil der ''Arduino IDE'' sind die beiden vordefinierten Methoden void <code>setup()</code> und <code>void loop()</code>. Die Methode <code>setup()</code> 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 <code>loop()</code> 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 <code>void pinMode(uint8_t pin, uint8_t mode)</code>, welche verwendet wird um die Pins als Input oder Output-Pins zu initialisieren und die Methode <code>int digitalRead(uint8_t pin)</code>, welche es ermöglicht den aktuellen Zustand eines Input-Pins auszulesen. |
|||
===Zeitgesteuerte Interrupts=== |
===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: |
|||
<pre> |
|||
f_timer = f_Arduino / Prescaler |
|||
</pre> |
|||
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: |
|||
{| class="wikitable" style="text-align: center;" |
|||
!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.<ref>[http://www.instructables.com/id/Arduino-Timer-Interrupts/ Arduino Timer Interrupts]. Abgerufen am 06.02.2017</ref> |
|||
<pre> |
|||
Vergleichswert = (f_Arduino / (Prescaler * f_gewünscht)) - 1 |
|||
</pre> |
|||
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 <code>setup()</code>-Methode getroffen werden. Im nachfolgenden Beispiel ist eine <code>setup()</code>-Methode dargestellt, mit der ''timer2'' auf 8 kHz festgelegt wird. |
|||
<pre> |
|||
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 |
|||
} |
|||
</pre> |
|||
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: |
|||
<pre> |
|||
ISR(TIMER2_COMPA_vect) { |
|||
// Meine Interrupt Service Routine |
|||
} |
|||
</pre> |
|||
Innerhalb der ISR wird dann die Funktion zum Auslesen der Messdaten aufgerufen, welche im nächsten Abschnitt erläutert wird. |
|||
===Auslesen der Messdaten=== |
===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 |
|||
<code>setup()</code> initialisiert werden. Die Realisierung der Methode ist im nachfolgenden |
|||
Listing zu sehen. |
|||
<pre> |
|||
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; |
|||
} |
|||
</pre> |
|||
Dabei werden als erstes alle Pins von 22 bis 51 als Input-Pins initialisiert. Anschließend wird die Methode <code>setupPorts()</code> aufgerufen. In dieser Methode wird als erstes ausgewertet, wie viele der analogen Eingänge ausgewertet werden sollen. Anschließend wird dem Zeiger <code>activePorts</code> 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 <code>startAnalogRead(uint8_t pin)</code> 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 <code>analogRead(uint8_t pin)</code> 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 <code>analogRead(uint8_t pin)</code> 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 <code>startAnalogRead(uint8_t pin)</code> und <code>endAnalogRead()</code> ausgelagert. In der Methode <code>startAnalogRead(uint8_t pin)</code> wird dabei der auszulesende Pin an den ADC übergeben und die Messung gestartet. Die Methode <code>endAnalogRead()</code> gibt dann das Ergebnis des ADC aus. Ausgeführt werden beide Methoden letztendlich in der ISR. |
|||
<pre> |
|||
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; |
|||
} |
|||
</pre> |
|||
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: |
|||
<pre> |
|||
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++]); |
|||
} |
|||
} |
|||
</pre> |
|||
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 <code>isActive</code> 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 <code>analogRead(uint8_t pin)</code> 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 <code>setup()</code>-Methode noch folgende Zeile eingefügt werden muss: |
|||
<pre> |
|||
Serial.begin(250000); |
|||
</pre> |
|||
===Realisierung der Kontrollfunktion=== |
===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 <code>setup()</code>-Methode initialisiert werden. Dies erfolgt über folgende Zeile: |
|||
<pre> |
|||
void setup() { |
|||
attachInterrupt(digitalPinToInterrupt(2), isr, RISING); |
|||
attachInterrupt(digitalPinToInterrupt(3), reset, RISING); |
|||
} |
|||
</pre> |
|||
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 <code>setup()</code>-Methode werden demnach die Pins 2 und 3 als Interruptpins initialisiert, welche die Methoden <code>isr ()</code> bzw. <code>reset()</code> aufrufen, sobald sich der Zustand der Pins von LOW auf HIGH ändert. Die Methode <code>isr ()</code> stoppt dann die Messung und gibt aus, an welcher Platine eine zu hohe Spannung anliegt. |
|||
<pre> |
|||
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"); |
|||
} |
|||
} |
|||
} |
|||
</pre> |
|||
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<ref>Marcel Schwarz, Nils Janke und Peter Wiese: ''Modulares Messverstärkersystem''. ''Hochschule Bochum Campus Velbert/Heiligenhaus'' (2017). Betreuender Professor: Prof. Dr.-Ing. Dietmar Gerhardt</ref> 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 <code>reset()</code> aufgerufen. In dieser Methode werden die Zeiger zurückgesetzt und die auszulesenden Ports abgefragt. Anschließend wird die Messung wieder gestartet. |
|||
<pre> |
|||
void reset() { |
|||
reader = 0; |
|||
writer = 0; |
|||
setupPorts(); |
|||
isActive = 1; |
|||
} |
|||
</pre> |
|||
==Zusammenfassung und Ausblick== |
==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== |
==Einzelnachweise== |
Aktuelle Version vom 23. Februar 2017, 09:44 Uhr
Projekt Rack'n'Roll | |
---|---|
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
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
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
- ↑ BSD Simplified Licence
- ↑ Claude E. Shannon: Communication in the Presence of Noise. In: Proceedings of the IEEE 37 (1948)
- ↑ GNU Lesser General Public Licence. Abgerufen am 22.02.2017
- ↑ Arduino Timer Interrupts. Abgerufen am 06.02.2017
- ↑ Marcel Schwarz, Nils Janke und Peter Wiese: Modulares Messverstärkersystem. Hochschule Bochum Campus Velbert/Heiligenhaus (2017). Betreuender Professor: Prof. Dr.-Ing. Dietmar Gerhardt