Mit dem Arduino lassen sich ja sehr einfach verschiedenste Steuerungsaufgaben erledigen. Die Möglichkeit über Steckbretter und Steckbrücken, die Aufbauten schnell umzusetzen sind schon sehr praktisch. Wenn man allerdings, verschiedene Steuerungen und Anzeigeeinheiten immer wieder benötigt, habe ich mir überlegt ein Arduino-Steuerpult zu bauen. Mit dem Ziel immer wieder benötigte Module stationär in ein Gehäuse mit Netzteil zu integrieren, so dass immer wieder darauf zugegriffen werden kann.
In meinem Steuerpult sind in der ersten Ausbaustufe folgende Module enthalten:
Netzteil mit 12V und 5V bei ca. 2A Ausgangsleistung
Digitales Voltmeter
LCD-Anzeige, 4 Zeilen und I2C Schnittstelle
2×14-Segement-Anzeigen mit I2C Schnittstelle
Potentiometer
Dreh-Encoder
3-Tasten, mit Pulldowns Widerständen
12er Tastenfeld
Thermo-Drucker
Bepper um Töne zu erzeugen
Schalt-Transistoren (in Ausbaustufe 2)
Motor-Regler (in Ausbaustufe 2)
Dazu wurde eine Frontplatte entworfen Mund mit einer Holzfräse von Inventables realisiert:
Frontplatten-Entwurf mit Easel
Nach dem die Frontplatte entworfen und ausgefräst war, habe ich dazu passend ein Gehäuse geplant… und mit Hilfe von MDF Platten in meiner Holzwerkstatt zusammengebaut.
Die Idee ist nun, die Anschlüsse Module an der Vorderseite über Klemmleisten heraus zu führen, um diese von einem Arduino ansteuern zu können. Die immer wieder verwendeten Leitungen wie die Strom-Versorgung werden intern fest mit dem Netzgerät verdrahtet, so dass der Arduino davon unbelastet bleibt. Einzig die gemeinsame Masseleitung wird benötigt. Das verringert den Verkabelungsaufwand bei neuen Projekten erheblich.
Am Ende sieht das Ganze dan beispielsweise so aus:
Natürlich können noch wesentlich mehr Funktionsmodule eingebaut werden. Der Phantasie sind hier keine Grenzen gesetzt.
Es gibt verschiedene Anwendungen wo es Sinn macht, dass Daten ausgedruckt werden können. Dazu ist kein großer Drucker erforderlich, kompakte Thermodrucker ähnlich einem Kassenbon-Drucker eignen sich ganz hervorragend dafür.
Im Internet (z.B. bei Adafruit) kann man an verschiedenen Stellen einen günstigen Thermo-Drucker bestellen. Dieser Drucker entspricht einfachen Kassenbon-Drucker. Der Thermodrucker nimmt ein 2,55″ (57 mm) breites Thermopapier mit einem maximalen Rollendurchmesser von 1,5″ (39 mm) auf. Möglicherweise muss etwas Papier von diesen Rollen entfernen, damit es in den Drucker passt. Aber man kann immer das überschüssige Papier im Drucker verwenden, da kein „Kern“ im Papier vorhanden sein muss, damit es eingezogen werden kann.
Die Rückseite meines Druckers hat zwei Anschlüsse; einen für die Stromversorgung und einen (5-poligen) für die serielle Kommunikation (Varianten die davon abweichen sind möglich). Der Thermodrucker wird mit einer Standard-Baudrate von 19200 Bit ausgeliefert. Wenn der Drucker in Verbindung mit einem Arduino verwendet werden soll, muß ein externes Netzteil (5V/2A) angeschlossen werden, da der Drucker mehr Strom aufnimmt, als USB liefern kann.
Rückseite mit Anschlussbildern des Thermodruckers
Ich habe den Drucker in ein einfaches Holzgehäuse eingebaut um erste Test durchführen zu können. Zum Test habe ich 3 der 5 Leitungen verwendet und zwar die blaue mit dem RX-Pin und die grüne mit dem TX-Pin am Arduino (näheres siehe im Quellcode). Ebenso wurde die schwarze Leitung mit GND am Arduino verbunden und natürlich die externe Stromversorgung des Druckers mit 5-9V. Wichtig ist noch auf die Baudrate zu achten. Hier gibt es 2 Versionen. Die 19200 und die 9600 Rate… je nach dem mit welcher der Drucker vorkonfiguriert wurde.
Ich habe den Drucker in ein einfaches Holzgehäuse eingebaut um erste Test durchführen zu können. Zum Test habe ich 3 der 5 Leitungen verwendet und zwar die blaue mit dem RX-Pin und die grüne mit dem TX-Pin am Arduino (näheres siehe im Quellcode). Ebenso wurde die schwarze Leitung mit GND am Arduino verbunden und natürlich die externe Stromversorgung des Druckers mit 5-9V. Wichtig ist noch auf die Baudrate zu achten. Hier gibt es 2 Versionen. Die 19200 und die 9600 Rate… je nach dem mit welcher der Drucker vorkonfiguriert wurde.
Ansteuerung
Um Daten an den Drucker zu übertragen, wird eine 5V TTL Verbindung aufgebaut. Bitte nicht verwechseln, mit der RS232 Schnittstelle eines PCs, die mit 10V arbeitet, damit würde man den Drucker zerstören.
Die Verwendung mit dem Arduino ist sehr einfach möglich, da Adafruit eine entsprechende Bibliothek entwickelt hat, die die Kommunikation mit dem Drucker sehr erleichtert. Die Bibliothek stellt folgende Funktionen bereit:
Inverted text: this is invoked by calling inverseOn() — you will get text that’s white-on-black instead of black-on-white. inverseOff() turns this off.
Double height: this makes text thats extra tall, call doubleHeightOn() — likewise, turn off with doubleHeightOff()
Left/Center/Right justified: this aligns text to the left or right edge of the page, or centered. You can set the alignment by calling justify(‚R‘) (for right-justified), justify(‚C‘) (for centered) or justify(‚L‘) (for left-justified). Left-justified is the default state.
Bold text: makes it stand out a bit more, enable with boldOn() and turn off with boldOff()
Underlined text: makes it stand out a bit more, enable with underlineOn() and turn off with underlineOff()
Large/Medium/Small text: by default we use small, medium is twice as tall, large is twice as wide/tall. Set the size with setSize(‚L‘), setSize(‚M‘) or setSize(‚S‘)
Line spacing: you can change the space between lines of text by calling setLineHeight() where numpix is the number of pixels. The minimum is 24 (no extra space between lines), the default spacing is 32, and double-spaced text would be 64.
Der erste Test erfolgte mit der Library von Adafruit und funktionierte nach einigem Drucken einwandfrei. Das Beispiel-Programm basiert auf dem das mit der Bibliothek mit geliefert wird. Ich habe es etwas modifiziert, da bei mir keine Bitmaps ausgedruckt werden sollen, sondern im wesentlichen Texte.
//==============================
// PH TestThermo Printer
//==============================
#include "Adafruit_Thermal.h"
#include "SoftwareSerial.h"
#define TX_PIN 6 // Arduino transmit YELLOW WIRE labeled RX on printer
#define RX_PIN 5 // Arduino receive GREEN WIRE labeled TX on printer
SoftwareSerial mySerial(RX_PIN, TX_PIN); // Declare SoftwareSerial obj first
Adafruit_Thermal printer(&mySerial); // Pass addr to printer constructor
void setup() {
mySerial.begin(9600); // Initialize SoftwareSerial
printer.begin(); // Init printer
// Test inverse on & off
printer.inverseOn(); printer.println(F("Inverse ON"));
printer.inverseOff();
// Test character double-height on & off
printer.doubleHeightOn(); printer.println(F("Double Height ON"));
printer.doubleHeightOff();
// Set text justification (right, center, left)
//-- accepts 'L', 'C', 'R'
printer.justify('R');
printer.println(F("Right justified"));
printer.justify('C');
printer.println(F("Center justified"));
printer.justify('L');
printer.println(F("Left justified"));
// Test more styles
printer.boldOn();
printer.println(F("Bold text"));
printer.boldOff();
printer.underlineOn();
printer.println(F("Underlined text"));
printer.underlineOff();
printer.setSize('L'); // Set type size, accepts 'S', 'M', 'L'
printer.println(F("Large"));
printer.setSize('M');
printer.println(F("Medium"));
printer.setSize('S');
printer.println(F("Small"));
printer.justify('C');
printer.println(F("normal\nline\nspacing"));
printer.setLineHeight(50);
printer.println(F("Taller\nline\nspacing"));
printer.setLineHeight(); // Reset to default
printer.justify('L');
printer.sleep(); // Tell printer to sleep
delay(3000L); // Sleep for 3 seconds
printer.wake(); // MUST wake() before printing again, even if reset
printer.setDefault(); // Restore printer to defaults
}
void loop() {}
Mit folgendem Testaufbau kann man sehr einfach mit dem Arduino Ereignisse zählen. Der Testaufbau zeigt einen Gleichstrommotor der mit einem kleinen Zeiger verbunden ist, auf dem wiederum ein Magnet aufgeklebt wurde. Dieser Motor wird über einen selbstgebauten PWM-Motor-Regler auf NE555 Basis geregelt.
Versuchsaufbau für den Event-Zähler
Über diesem „Event-Generator“ befindet sich ein Reedkontakt, der über einen Pulldown Widerstand, beim Schliessen eindeutige Impulse generiert, die über die PulsIn-Funktion des Arduino erfasst und ausgewertet werden können.
Mit folgendem Programm können die Events erfasst und gezählt werden:
int pin = 8;
unsigned long T; //Periodendauer in us
double f; //Frequenz in MHz
void setup()
{Serial.begin(9600);
Serial.println("IMPULSZAEHLER/FREQUENZZAEHLER");
pinMode(pin, INPUT);
}
void loop() {
T = pulseIn(pin, HIGH) + pulseIn(pin, LOW);
if (T==0) Serial.println("Timeout.");
else
{f=1/(double)T; // f=1/T
Serial.print(f*1e6); Serial.println(" HZ"); //Ausgabe in Hertz
}
}
Mit Hilfe eines astabilen Multivibrator, dessen primäre Aufgabe es ist, in Abhängigkeit von einem RC-Glied Rechteck-Impulse zu erzeugen, ist es leicht möglich zusammen mit einem Arduino die Kapazität von Kondensatoren zu bestimmen. Die Länge dieser Impulse dient in unserem Aufbau, als Basis um die Kapazitäten von Kondensatoren zu bestimmen.
Schaltbild des Kapazitätsmessers
Der IC Baustein NE555 eignet sich besonders, um mit nur wenigen zusätzlichen Bauteilen eine derartige Kippstufe zu entwickeln. Für die Funktionsweise sind im wesentlichen die Bauteile R1 , R2 und C1 verantwortlich. Der Kondensator C2 sorgt nur dafür, dass die Schaltung nicht schwingt.
Die Dimensionierung der Bauteile kann entsprechend dem gewünschten Messbereich angepasst werden. Im vorliegenden Meßaufbau haben wir folgende Werte verwendet: R1 = 1 k und R2 = 10 k. Damit werden ausgeglichene Signallaufzeiten erreicht. Ein nachgeschalteter Schmitt-Trigger (zB SN7414) erzeugt für den nachfolgenden Arduino-Eingang steile Signalflanken und damit eindeutige Signalpegel.
Die Periodendauer eines astabilen Multivibrators berechnet sich nach folgenden Beziehungen:
Somit ergibt sich für die Periodendauer folgende Gleichung:
Das Programm um die Kapazitäten mit dem Arduino zu bestimmen ist sehr übersichtlich. Es beginnt mit der Deklaration der benötigten Variablen. Im Hauptteil werden die Zeiten wo der Impuls auf HIGH und auf LOW steht bestimmt und gemittelt. Die Kapazität des zu bestimmenden Kondensators ergibt sich dann über die Formel:
Der Programmcode für den Testaufbau beinhaltet noch beide Ausgabeformen über den integrierten Monitor der IDE und über ein 4 Zeilen LCD mit I2C Schnittstelle und lässt sich natürlich beliebig an individuellen Vorstellungen anpassen:
//-----------------------------------
// Messen von Kapazitäten mit NEE555
// Version 2.0, (c) PHOF 18.01.2020
//-----------------------------------
long Htime;
long Ltime;
float Ttime;
float frequency;
float capacitance;
const int inputPin=8;
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27,20,4);
void setup(){
Serial.begin(9600);
lcd.init(); lcd.backlight();
lcd.setCursor(0,0); lcd.print("Kapazitaetsmesser");
pinMode(inputPin,INPUT);
}
void loop() {
for (int i=0;i<5;i++){
Ltime=(pulseIn(inputPin,HIGH)+Ltime)/2;
Htime=(pulseIn(inputPin,LOW)+Htime)/2;
}//end for
Ttime = Htime+Ltime;
frequency=1000000/Ttime;
capacitance = (1.44*1000000000)/(20800*frequency);
// Ausgabe auf LCD Display
lcd.setCursor(1,2);
lcd.print("C:= "); lcd.print(capacitance);
lcd.print(" nF ");
// Ausgabe auf Monitor
Serial.println("==========================");
Serial.println("Kapazitätsbestimmung");
Serial.println("==========================");
Serial.print(" Periodendauer T: ");
Serial.print(Ttime);
Serial.println(" s ");
Serial.println("-------------------------");
Serial.print(" Frequenz ");
Serial.print(frequency);
Serial.println(" Hz ");
Serial.println("-------------------------");
Serial.print(" Kapazität: ");
Serial.print(capacitance);
Serial.println(" nF ");
Serial.println("=========================");
Serial.println(""); Serial.println("");
delay(750);
}//end loop
Das ermittelte Ergebnis ist erstaunlich genau, wie die nachfolgende Rechnung un der Blick aufs Osszilloskop zeigen.
Ergebnis der Messung vom Arduino und Überprüfung mit dem Osszilloskop
Wie man sieht stimmen Messung und Rechnung für diese sehr einfache Schaltung erstaunlich gut überein.
Überarbeitete Version für 2×16 LCD mit I2C Schnittstelle
Für den Einsatz in der Praxis ist es Vorteilhaft anstelle des Monitors ein eigenständiges Display zu verwenden. Darüber hinaus habe ich eine Fallunterscheidung eingebaut um die Kapazität in nF und µF anzuzeigen. Der leicht überarbeitete Programmcode sieht nun wie folgt aus:
Die I/O-Pins des Arduino können nur maximal 40mA aufbringen. Das reicht, um andere Digitaleingänge oder einzelne LED’s anzusteuern. Bei großen Lasten, wie z.B. Power-LEDS, Gleichstrommotoren, Relais benötigt man eine Zusatzschaltung, um den I/O-Pin zu verstärken.
Die Grundschaltung
Für das Schalten von größeren Strömen verwendet man Bipolare-Transistoren oder wenn es sich um große Lasten handelt auch sogenannte Power-MOSFET’s. Der Unterschied ist (vereinfacht) das Bipolar-Transitoren mit Strom gesteuert werden und FETs mit Spannung.
Schaltbilder mit Bipolar- und Feldeffekt-Transistoren
Ein MOSFET ist ein elektronischer Schalter, welcher hohe Ströme durch Anlegen einer Steuerspannung am Gate schalten kann. Ein sehr kleiner Strom (<1 mA) „öffnet“ dieses Gatter und lässt den Strom fließen. Dies ist sehr praktisch, da wir die Arduino-PWM-Ausgabe an dieses Gate senden können, wodurch ein weiterer PWM-Impulszug mit demselben Tastverhältnis durch den MOSFET erzeugt wird, wodurch Spannungen und Ströme ermöglicht werden, die den Arduino zerstören würden. Ein voll durchgesteuerter MOSFET hat zwischen Drain und Source im eingeschalteten Zustand nur wenige Milliohm Widerstand. Die Verlustleistung am MOSFET bleibt deshalb gering.
Allerdings muss man die richtigen Typen auswählen, welche sich mit der geringen Steuerspannung von 0..5V des Arduino vollständig öffnen lassen. Diese MOSFETs werden auch als „Logic Level MOSFET“ bezeichnet. Ein sehr verbreiteter Transistor ist der N-Kanal-Typ IRLIZ44N. Den gibt es bereits für wenige Cent in allen Fachgeschäften. Das Datenblatt findet sich hier!
Steuern von Gleichstrommotoren
Viele elektrische Verbraucher können in ihrer Leistung reguliert werden, indem die Versorgungsspannung in weiten Bereichen verändert wird. Ein normaler Gleichstrommotor wird z. B. langsamer laufen, wenn er mit einer geringeren Spannung versorgt wird, bzw. schneller laufen, wenn er mit einer höheren Spannung versorgt wird. Anstatt die Spannung abzusenken, ist es auch möglich, die volle Versorgungsspannung über einen geringeren Zeitraum anzulegen. Und genau das ist das Prinzip der Pulsweiten-Modulation (PWM). Durch die Abgabe von Pulsen mit unterschiedlichen Pulsweiten wird die abgegebene Energiemenge gesteuert.
Pulsweitenmodulation
Praktisch nimmt man bei der Pulsweiten-Modulation ein Rechtecksignal mit einer festen Frequenz und variiert die Breite der jeweiligen Pulse, um so die abgegebene Energiemenge zu steuern. Der Arduino Uno stellt standardmässig mit der Funktion analogWrite (pin, wert) eine einfache Möglichkeit bereit, PWM-Signale zu erzeugen. Bei dieser eingebauten Funktion ist die Auflösung konstant bei 8 Bit und die Frequenz bei den Pins (D3, D9, D10 und D11) 490 Hz bzw. 980 Hz bei den Pins (D5 und D6).
Wer für seine Anwendung eine höhere Auflösung benötigt, muss die entsprechenden Timer-Funktionen nutzen, mit der eine flexiblere Plus-Erzeugung möglich ist.
Die Funktion analogWrite sendet „pseudo-analoge“ Werte (Pulse mit einem definiertem Puls/Pausen Verhältnis ) an den adressierten Ausgangspin mit folgender Syntax:
PWM-Signal Charakteristika
Ein PWM-Signal ist somit durch zwei Größen charakterisiert: Die Frequenz f (normalerweise Prozessor-abbhängig aber konstant) und den Tastgrad g auch als dutycycle bezeichnet.
Damit wird deutlich, dass der Parameter value in der Funktion analogWrite() im direkten Zusammenhang mit der festgelegten Frequenz und der Zeit steht. Über den in der Abbildung beschriebenen Zusammenhang
lassen sich dann die jeweiligen Tastgrade bestimmen.
In der praktischen Anwendung kann man damit ohne weiteres eine einfache Geschwindigkeitssteuerung für einen Motor realisieren. Komplexere Steuerungsaufgaben erfordern dann aber spezifischere Motorsteuerungen, um die es hier aber nicht gehen soll.
Es soll ein Motor gemäß der oben im Beitrag dargestellten Schaltung mit Hilfe eines MOSFETs IRF24A gesteuert werden. Das verwendete Arduino Programm, gibt in einer Schleife die PDM Werte aus und steuert damit nach dem PWM-Verfahren die Geschwindigkeit eines Motors (z.B eines Lüfters).
Im nachfolgenden Beispiel ist gezeigt, wie man eine eigene Pulsweitensteuerung aufbauen kann. Hier wird nicht die eingebaute analogWrite-Funktion verwendet, sondern man erzeugt zwei digitale Pulse, deren Zeiten durch einen Potentiometer gesteuert werden. Könnte man natürlich einfacher machen, aber um zu zeigen, wie die Pulsdauern variiert werden, taugt dieses kleine Beispiel sicher besser.
Beispiel einer selbsterzeugten Pulsweiten Steuerung
Seit ich als Student die ersten Berührungspunkte mit Mathematica bekommen habe, fasziniert mich der dadurch aufgespannte Möglichkeitsraum der funktionalen Programmierung. Ursprünglich als Software für symbolische Mathematik gedacht, ist es heute in der Version 12 ein Softwaresystem für alle denkbaren Anwendungsmöglichkeit, weit über die Mathematik oder Physik hinaus.
Deutsche Dokumentation der Version 2.0 aus dem Jahr 1992
Die Sprache ist immer noch die selbe wie 1988 in der Version 1 und wurde aber zwischenzeitlich ergänzt und überarbeitet und heisst nun Wolfram Language. Das Handbuch gibt es nur noch elektronisch, da es mehrere tausend Seiten umfasst.
Der Umgang ist leider etwas gewöhnungsbedürftig und ich lerne heute immer noch dazu. In den folgenden Abschnitten werde ich meine Einsichten die nicht in den gängigen Büchern stehen versuchen als eine Art lose Blatt Sammlung (Codex) zu dokumentieren.
Darstellung von Funktionen
Mathematica stellt umfassende Funktionen für die Darstellung von Funktionen zur Verfügung. Die zentrale Funktion ist die Plot[]-Funktion. Im nachfolgenden Bild, ist die Grundmenge Funktion und dann die zentralen Einstellungs-Möglichkeiten wie die Darstellung ergänzt werden kann dargestellt:
Elementare Plot-Funktion
Wie man sieht, wird zwar den groben Funktion Verlauf, wichtige Eigenschaften sind aber nicht ersichtlich. Dazu muss man die Darstellung-Bereiche in x- und y-Richtung anpassen und weitere Möglichkeiten einsetzen, um mehr über den Funktionsgraph zu erfahren.
Erweiterte Plot-Funktionen
Mathematica stellt eine Vielzahl von Darstellung-Optionen bereit, die alle in der Online-Dokumentation aufgeführt sind.
Kurvendiskussion
Eine sehr häufig vorkommende Aufgabe ist die Untersuchung einer gegeben Funktion hinsichtlich Ihrer Eigenschaften wie Nullstellen, Extrema, Asymptoten etc. Wie das mit Hilfe von Mathematica gemacht werden kann, wird im folgenden vorgestellt.
Ausgangspunkt sei folgende Funktion und ihr Graph wie im nachfolgenden Bild dargestellt.
Bestimmung der Nullstellen
Um die Nullstellen zu bestimmen, sind die Punkte zu berechnen, an denen der Funktionswert von f(x) = 0 ist. Dazu gibt es zwei vordefinierte Funktionen in Mathematica:Solve und NSolve. NSolve versucht die Funktion mit hilfe numerischer Methoden zu lösen wo hingegen Solve versucht die Funktion aufzulösen, was natürlich mehr Rechenzeit erfordern kann und bisweilen auch nicht möglich ist.
Die beiden Funktionen bestimmen sämtliche realen und komplexen Nullstellen eines Polynoms und geben das Ergebnis in Form einer Liste wieder. Also nicht in Form von Variablen denen die Werte bereits zugewiesen wurden. D.h. wir können auf diese Lösungen noch nicht ohne weitere Schritte zugreifen.
Bestimmung der 1. und 2. Ableitungen
Mathematica erlaubt es Ableitungen in Analogie zur händischen Form einfach durch Anfügen eines Ableitungsstrichs zu erzeugen. Formal richtig wäre aber die Benutzung der Funktion D[f[x], x].
f'[x] ==> -9 + 3 x^2
f''[x] ==> 6x
Mit diesen Vorarbeiten lässt sich die komplette Kurvendiskussion, d.h. die Bestimmung der Nullstelle und der Extremwerte mit wenigen Zeilen berechnen:
Anschliessend sorgt die Plot-Funktion für die visuelle Darstellung:
Darstellung von Funktionen
Hierbei ist es wichtig die Vielzahl der Optionen kennen zu lernen. Anbei eine kleine und wie ich finde nützliche Auswahl:
Plot[-2 + 5 x - 3 x^2 - x^3 + x^4, {x, -6., 6.}]
So sieht der von Mathematica vorgeschlagene Graph als Standard dargestellt. Da hier aber alle wesentlichen zur Kurvendiskussion erfoderlichen Bereiche nicht gut dargestellt sind, stellt sich die Frage wie man das verändern kann. Nachfolgend der gleiche Graph mit einigen Plot-Optionen und den zugehörigen Ableitungen:
Der Mittelwert beschreibt den statistischen Durchschnittswert. Für die Ermittlung des Mittelwertes addiert man alle Werte einer Datenmenge und teilt die Summe durch die Anzahl aller Werte.
Der Median oder Zentralwert ist der Wert, der genau in der Mitte einer Datenmenge liegt. Die eine Hälfte aller ist immer kleiner, die andere größer als der Median. Bei einer geraden Anzahl von Individualdaten ist der Median die Hälfte der Summe der beiden in der Mitte liegenden Werte.
Gleitende Mittelwerte dienen zur Glättung eines gegebenen Datenverlaufes. Die Glättung erfolgt durch das Abschwächen von besonders hohen oder niedrigen Werten. Die geschieht, indem eine neue Datenreihe erstellt wird, die aus den Durchschnitten von gleich großen Datenmengen berechnet werden. Dabei werden die Datenpunkte der Reihe durch den arithmetischen Mittelwert der Nachbarpunkte (oder einer gewichteten Form davon) ersetzt. Im einfachsten Fall geschieht dies durch Mittelung von drei Datenpunkten (der ausgewählte Datenpunkt und seine beiden Nachbarn):
MovingAverage[{a, b, c, d, e, f, g}, 3]
{1/3 (a + b + c), 1/3 (b + c + d), 1/3 (c + d + e), 1/3 (d + e + f),
1/3 (e + f + g)}
Beispiel
Ein Unternehmen liefert nachfolgende dargestellte Menge von Umsatzzahlen. Wir berechnen nun den gleitenden Mittelwert 3- Ordnung:
Wie oben beschrieben, nimmt man die n-Datenpunkte im Beispiel ist n=3, berechnet den Mittelwert und ersetzt das erste Datenelement durch diesen Mittelwert. Dann rückt man ein Feld weiter, berechnet wieder den Mittelwert, das ergibt dann den zweiten Eintrag usw.
Damit werden Datenreihen geglättet. Der gleitende Mittelwert wirkt wie ein Dämpfungsfilder, der die größten Ausreisser in der Zahlenreihe glättet.
Wie man das in der Praxis anwenden kann ist sehr anschaulich auf den Seiten von Matthias Busse vorgestellt. Er zeigt wie man einen Datenstrom (simuliert durch einen Strom von Zufallszahlen) glätten kann.
// Über viele Integer Werte einen Mittelwert bilden.
// Nach Matthias Busse Version 1.0
#define anzahlMittelWerte 10
int werte[anzahlMittelWerte]
int zaehlerMittelWerte=0;
void setup() {
Serial.begin(9600);
}
void loop() {
int n;
float f;
n=random(1,10);
f= mittelWert(n);
Serial.println(f);
delay(100);
}
float mittelWert(int neuerWert) {// neuen Datenwert aufnehmen und Mittelwert zurück geben
float mittel, summe=0;
werte[zaehlerMittelWerte] = neuerWert;
for(int k=0; k < anzahlMittelWerte; k++)
summe += werte[k];
mittel=(float) summe / anzahlMittelWerte;
zaehlerMittelWerte++;
if(zaehlerMittelWerte >= anzahlMittelWerte)
zaehlerMittelWerte=0;
return mittel;
}
Eine etwas andere Lösung findet sich im Arduino Forum:
/*
Smoothing
http://www.arduino.cc/en/Tutorial/Smoothing
*/
// Define the number of samples to keep track of. The higher the number, the
// more the readings will be smoothed, but the slower the output will respond to
// the input. Using a constant rather than a normal variable lets us use this
// value to determine the size of the readings array.
const int numReadings = 10;
int readings[numReadings]; // the readings from the analog input
int readIndex = 0; // the index of the current reading
int total = 0; // the running total
int average = 0; // the average
int inputPin = A0;
void setup() {
// initialize serial communication with computer:
Serial.begin(9600);
// initialize all the readings to 0:
for (int thisReading = 0; thisReading < numReadings; thisReading++) {
readings[thisReading] = 0;
}
}
void loop() {
// subtract the last reading:
total = total - readings[readIndex];
// read from the sensor:
readings[readIndex] = random(1,10); ;
// add the reading to the total:
total = total + readings[readIndex];
// advance to the next position in the array:
readIndex = readIndex + 1;
// if we're at the end of the array...
if (readIndex >= numReadings) {
// ...wrap around to the beginning:
readIndex = 0;
}
// calculate the average:
average = total / numReadings;
// send it to the computer as ASCII digits
Serial.println(average);
delay(100); // delay in between reads for stability
}
Die Programmierung eines Speicherchips war in den 1980er Jahren eines der spannendsten Themen wenn man sich mit dem Bau eines eigenen Computers beschäftigt hat. Heute stehen neben den klassischen RAM Bausteinen auch EEPROMS zur Verfügung, die einfacher zu programmieren sein sollen.
Die Abkürzung EEPROM steht für „Electrically Erasable Programmable Read-Only Memory“. Es handelt sich um einen Halbleiterspeicher, der sich durch Spannungsimpulse beschreiben und löschen lässt. Die auf dem Chip abgelegten Informationen sind nicht-flüchtig und bleiben ohne eine angelegte Versorgungsspannung erhalten.
Man unterscheidet verschiedene Arten von EEPROMS. Die EPROMS (z.B. 28F…, 29C…, 29F…) lassen es zu einzelne Bytes zu lesen und zu schreiben. Gleichzeitig lassen sich diese Bausteine auch komplett oder blockweise elektrisch löschen und einige auch blockweise (wie die AT28C-Serie) programmieren.
Daneben gibt es serielle EEPROMS (z.B. 24C…, 93C…). Seriell bedeutet bei diesen Bausteinen, das die Datenausgabe sowie die Adressangabe Bit für Bit (=seriell) erfolgt. Damit kann zwar nur ein Bit zur Zeit ausgegeben werden und die angesteuerte Adresse muss auch Bit für Bit übertragen werden aber dafür hat es den großen Vorteil das dass serielle EEPROM mit einem kleinen 8 Pin Gehäuse auskommt. Diese Bausteine werden demnach gerne eingesetzt, wenn Platz oder Ansteuerungsleitungen gespart werden sollen und dabei keine großen Datenmengen oder große Geschwindigkeit gefordert sind.
Programmiergerät für parallele EEPROMS
Für das EEPROM Programmiergerät soll ein EEPROM vom Typ 28C64-150 und einer vom Typ 28C256 verwendet werden. Wie man dem Datenblatt entnehmen kann handelt es sich um ein Baustein der 64k bzw. 256kByte Speicherkapazität enthält und eine Zugriffszeit von 150ns erlaubt. Wenn mann sich die Pin-Belegung eines EEPROMs anschaut erkennt man folgende grundsätzliche Aufteilung die man bei allen Typen dieser Familie findet:
Die Idee besteht nun darin, einen Arduino Mega mit seinen ausreichenden Schnittstellen zum programmieren von EEPROM Bausteinen zu verwenden. Dabei wurden die Pins so gewählt, dass man über die Port-Manipulations-Mechanismen die Ein- und Ausgabe der Daten steuern kann. Da das größte mir vorliegende EEPROM über 15 Adressleitungen verfügt, habe ich mich entschieden, dafür die Analogen-Pins A0 bis A14 zu verwenden. Hinzu kommt, dass diese Pins über die beiden Ports F und K gesteuert werden können, was die Handhabung sehr erleichtert.
Lesen von EEPROM Daten
Wie das gemacht werden muss ist im Datenblatt mehr oder weniger verständlich beschrieben. Man muss sich die Beschreibung in Ruhe mehrmals durchlesen, dann ergibt sich folgender Ablauf:
Setze ChipEnable CE auf auf LOW, d.h verbinde mit Masse. Da wir ständig mit dem Chip arbeiten kann dies eine statische Verbindung sein.
Setze OutputEnable OE auf LOW, d.h. Daten werden ausgegeben.
Lege die Adresse an, die ausgelesen werden soll
Setze WriteEnable W E auf HIGH , dass heisst Daten können nur gelesen werden.
Damit sind wir in der Lage, Daten aus einem EEPROM auszulesen.
Schreiben von Daten in das EEPROM
Um Daten auszulesen muss CE und OE auf LOW und WE auf High gesetzt werden. Dann muss die entsprechende Adresse des auszulesenden Bereichs angelegt werden und an den 8 Datenbits werden dann die gespeicherten Datenbits ausgegeben.
In den Datenblättern der Chip-Hersteller wird ausführlich auf die einzuhaltenden Zeiten für ein reibungsloses Lesen und Schreiben eingegangen.
Erforderliche Funktionen für das EEPROM Programmiergerät
Bereitstellen einer Adresse an den Arduino-Pins
Man benötigt eine Funktion setAddress deren Aufgabe es ist, eine vorgegebene Speicheradresse als 16bit Adresse über den Arduino MEGA bereitzustellen. Die Herausforderung besteht nun darin, zu einer gegebenen Adresse Adr (die eine 16 Bit Breite aufweist), die einzelnen Bitwerte an die definierten Adressleitungen überträgt. Dazu muss zuerst eine Aufteilung in die oberen-8 Bit-Werte und die unteren 8-Bit Werte erfolgen:
Mit dem Schiebe-Operator >>8 werden die oberen 8-Bit der Variable wert um 8 Stellen nach rechts verschoben. Die bereits bestehenden Daten gehen dabei verloren. Somit steht in der wert nun nur noch das obere Byte. Um das untere Byte zu identifizieren, wird eine sogenannte Maskierung durchgeführt. Die UND-Verknüfung mit 0x00 übernimmt diese Aufgabe. Anschliessend hat man die 16-Bit breite Variable wert in zwei 8-Bit breite Daten aufgeteilt.
Diese können nun an die Ports übertragen werden. Dazu geht man wie folgt vor:
void setAdress(word adresse){
// Setze 16Bit Adresse über Port-Register F und K
// Bit7|Bit6|Bit5|Bit4|Bit3|Bit2|Bit1|Bit0
// PortF: A7 | A6 | A5 | A4 | A3 | A2 | A1 | A0
// PortK: A15| A14| A13| A12| A11| A10| A9 | A8
// setze Port-Bits auf Ausgang
DDRF = B11111111;
DDRK = B11111111;
// Bestimmung Low- und High-Bytes
byte high_byte = adresse >>8;
byte low_byte = adresse & 0x00ff;
// Übertragung der beiden Bytes auf die PortRegister
PORTF = low_byte;
PORTK = high_byte;
}// end setAdress
Der zentrale Teil der Übertragung der einzelnen Bits an die Ausgabe-Pins kann durch eine einfache Und-Verknüpfung mit den entsprechenden Port-Registern erfolgen.
EEPROM Daten einlesen
Nach dem Setzen des Output_Enable-Bits auf Low (= Daten Lesen) und dem Setzen des Port-registers auf Eingang können die an den Ausgangs-Pins anstehenden EEPROM-Daten einfach in die Variable data eingelesen werden. Die entsprechende Funktion könnte so umgesetzt werden:
byte readEEPROM(int address) {
// setze Output_Enable auf LOW = ermöglicht das Lesen von Daten
digitalWrite(Output_EN, HIGH);
delayMicroseconds(1);
digitalWrite(Output_EN, LOW);
// Setze Portregister C auf Input
DDRC = B00000000;
setAdress(address);
// Lese Daten
byte data = 0;
data=PINC;
return data;
}//end readEEPROM
Formatierte Ausgabe im Monitor-Stil
Aus Remineszenz an die frühen Tage der Computertechnik wo es nur sehr eingeschränkte Ein- und Ausgabe-Möglichkeiten gab, stellten die damaligen Consolen die Inhalte in Form von Hexadezimalen Datenblöcken dar. Es beginnt immer mit der Startadresse gefolgt von 8 aufeinanderfolgenden Daten-Werten ohne explizite Adress-Angabe. Dann folgt ein Zeilenumbruch und es wird die um 8 Stellen erhöhte Adresse-Angabe wieder gefolgt von 8 einzelnen Datenblöcken, zu denen man sich die zugehörige Speicheradresse leicht durch abzählen ermitteln kann.
Wie gesagt, die Idee stammt aus der Vergangenheit aber ist bis heute immer noch ein gutes Instrument in der maschinennahen Programmierung, daher habe ich so eine Funktion zur Datenansicht geschrieben.
void Monitor (int Startadresse, word Endadresse) {
// Ausgabe der Speicherstellen von Startadresse bis Endadresse
Serial.println();
Serial.println("==============================");
Serial.println(" ** Monitor-Programm **");
Serial.println("==============================");
for (int adr=Startadresse;adr< Endadresse;adr++){
randomData = random(255); // Lese Soeicherstelle muss ersetzt werden
if ((adr == 0)||(adr % 8 == 0)) // zeige 8 Stellen in einer Reiche
Serial.print(Int2Hex(adr,4)+": ");
Serial.print(Int2Hex(randomData,2)+" ");
if ((adr+1)%8 == 0) // stelle fest ob Zeilenumbruch erforderlich
Serial.println();
}// end for adr
}// end Monitor
String Int2Hex(int wert, int stellen){
String temp = String(wert,HEX);
String prae = "";
int len = temp.length();
int diff = stellen-len;
for (int i=0;i< diff;i++)
prae = prae +"0";
return prae+temp;
}// end Int2Hex
Beispiel eines sogenannten Hex Dumps
Beispiel EPPROM Programmer Prototyp
Der erste Entwurf für ein komplettes Programm mit dem man EEPROMs lesen und schreiben können kann ist im folgenden dargestellt. Es basiert auf den bereits vorgestellten Teil-Programmen und ist erweitert um die Enable Funktionen für die Output- und die Write-Funktion. Dazu kommt ein Arduino Mega zum Einsatz, da er über genügend IO-Ports verfügt und man so ohne Shift-Register auskommt.
//============================================
// EEPROM Programme
// Monitor: Anzeige von Speichstellen
// SetAdress SetData setzen von Adressen und Daten
// PHOF Nov 2020
//============================================
// ---- Arduino MEGA Portbelegung ----
// Bit7| Bit6| Bit5| Bit4| Bit3| Bit2| Bit1| Bit0
//PortA: P29 | P28 | P27 | P26 | P25 | P24 | P23 | P22
//PortL: P42 | P43 | P44 | P45 | P46 | P47 | P48 | P49
//==============================================================
#define WRITE_EN 12
#define Output_EN 10
// 4-bit hex decoder for common cathode 7-segment display
byte data[] = { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0xff, 0x6f, 0x77, 0x7c, 0x58, 0x5e, 0x79, 0x71 };
void setup() {
DDRA = B11111111; // alle Ports output
DDRC = B11111111;
DDRL = B11111111;
DDRF = B11111111;
DDRK = B11111111;
Serial.begin(9600);
digitalWrite(WRITE_EN, HIGH); pinMode(WRITE_EN, OUTPUT);
digitalWrite(Output_EN, HIGH);pinMode(Output_EN, OUTPUT);
// Test Prozedur
Serial.print("Programming EEPROM");
for (int address = 0; address < sizeof(data); address += 1) {
writeEEPROM(address, data[address]);
Serial.println();Serial.print("Schreibe Daten Byte ... Adresse: ");
Serial.print(address,HEX); Serial.print(" : "); Serial.print(data[address],BIN);Serial.print(" ... ");
Serial.println(data[address],HEX);
if (address % 64 == 0) {
Serial.print(".");
}
delay(500);
}
Serial.println(" done");
}// end setup
void Monitor (int Startadresse, word Endadresse) {
// Ausgabe der Speicherstellen von Startadresse bis Endadresse
Serial.println();
Serial.println("==============================");
Serial.println(" ** Monitor-Programm **");
Serial.println("==============================");
for (int adr=Startadresse;adr< Endadresse;adr++){
// randomData = random(255); // Lese Speicherstelle muss ersetzt werden
if ((adr == 0)||(adr % 8 == 0)) // zeige 8 Stellen in einer Reiche
Serial.print(Int2Hex(adr,4)+": ");
Serial.print(Int2Hex(randomData,2)+" ");
if ((adr+1)%8 == 0) // stelle fest ob Zeilenumbruch erforderlich
Serial.println();
}// ennd for adr
}// end Monitor
String Int2Hex(int wert, int stellen){
String temp = String(wert,HEX);
String prae = "";
int len = temp.length();
int diff = stellen-len;
for (int i=0;i< diff;i++)
prae = prae +"0";
return prae+temp;
}// end Int2Hex
void setAdress(word adresse){
// Setze 16Bit Adresse über Port-Register F und K
// Bit7|Bit6|Bit5|Bit4|Bit3|Bit2|Bit1|Bit0
// PortF: A7 | A6 | A5 | A4 | A3 | A2 | A1 | A0
// PortK: A15| A14| A13| A12| A11| A10| A9 | A8
// setze Port-Bits auf Ausgang
DDRF = B11111111;
DDRK = B11111111;
// Bestimmung Low- und High-Bytes
byte high_byte = adresse >>8; // oder (adresse & 0xff00) >> 8
byte low_byte = adresse & 0x00ff;
// Übertragung der beiden Bytes auf die PortRegister
PORTF = low_byte;
PORTK = high_byte;
}// end setAdress
byte readEEPROM(int address) {
// setze Output_Enable auf LOW = ermöglicht das Lesen von Daten
digitalWrite(Output_EN, HIGH);
delayMicroseconds(1);
digitalWrite(Output_EN, LOW);
// Setze Portregister C auf Input
DDRC = B00000000;
setAdress(address);
// Lese Daten
byte data = 0;
data=PINC;
return data;
}//end readEEPROM
void writeEEPROM(int address, byte data) {
// setze Output_Enable auf HIGH = ermöglicht das Schreiben von Daten
digitalWrite(Output_EN, LOW);
delayMicroseconds(1);
digitalWrite(Output_EN, HIGH);
setAdress(address);
// PortC: P30 | P31 | P32 | P33 | P34 | P35 | P36 | P37
DDRC = B11111111; // setze Port-Bits C auf Ausgang
PORTC = data; // damit stehen die Daten an den Pins von Port C
// set write Enable um die Daten ins EEPROM zu schrieben
digitalWrite(WRITE_EN, LOW);
delayMicroseconds(1);
digitalWrite(WRITE_EN, HIGH);
delay(10);
}//end writeEEPROM
void loop() { }
Dabei handelt es sich um einen ersten Prototypen, realisiert auf einem Steckbrett. Man kann hier nach belieben Erweiterungen wie beispielsweise LED Anzeigen dazu bauen. Oder andere Arduino-PINs verwenden. Wenn dann alles so funktioniert wie ausgedacht, steht einer Version mit einer speziell angefertigten Platine und einem Schnellpannsockel für das unproblematische Einsetzen und Herausnehmen der EEPROMs nichts mehr im Weg.
Ein Timer ist ein Funktionsbaustein eines Mikrocontrollers und kann verwendet werden, um Zeitereignisse zu messen. Vom Aufbau her ist ein Timer ist im Grunde nichts anderes als ein Register im Mikrocontroller, das hardwaregesteuert fortlaufend um 1 erhöht (oder verringert) wird. Dazu wird üblicherweise der Timer mit dem Systemtakt verbunden, um so die Genauigkeit des Quarzes auszunutzen, um ein regelmäßiges und vor allen Dingen vom restlichen Programmfluss unabhängiges Zählen zu ermöglichen.
Um die verschiedenen Timer, die ein Microcontroller zur Verfügung stellt verwenden zu können, müssen bestimmte Eigenschaften konfiguriert werden. Dazu sind jedem Timer bestimmte Register zugeordnet. Zum Teil sind diese Timer spezifisch, es gibt aber auch Register, die von allen Timern gemeinsam genutzt werden. Im nachfolgender Abbildung sind diese Register am Beispiel des 16-Bit Timers1 dargestellt:
Übersicht aller dem Timer1 zugeordneten Register
Neben diesen spezifischen Registern die alle Timer haben, gibt es wie gemeinsam genutzte Register. Dabei handelt es sich zum einen um das sogenannte TIMSK-Register. Das TIMSK-Register definiert, bei welchen Ereignissen welche Art Interrupts ausgelöst werden. Das Register hat für den Timer1 den nachfolgend dargestellten Aufbau.
Dazu kommt noch ein weiteres Register das gemeinsam genutzt wird. Im TIFR- Register wird festgelegt, wie reagiert werden soll, wenn ein Zähler überläuft, bzw. sein Zielwert erreicht hat.
Sind die jeweiligen Interrupts aktiviert, können diese dann über die Interrupt Service Routinen (ISRs) benutzt werden. Dazu müssen die entsprechenden Interruptvektoren an die ISR übergeben werden:
TIMER1_CAPT_vect für Input Capture
TIMER1_COMPA_vect / TIMER1_COMPB_vect für Compare Match
TIMER1_OVF_vect für Timer Overflow
Das ist die Theorie der Timer Programmierung. Für unerfahrene Programmierer sicher alles andere als einfach nachzuvollziehen. Aus diesem Grund soll die Arbeit mit Timern nachfolgend Schritt für Schritt erläutert werden. Dazu ist es wichtig, zu wissen, wie die eben beschriebenen Register im Zusammenhang stehen.
2. Kontrollregister
Um die Timer Funktion zu spezifizieren, werden bestimmte Bits in den für die Timer-Konfiguration vorgesehenen Registern gesetzt. Die wesentlichen Register sind :
TCCRx – Timer/Counter Control Register
TCNTx – Timer/Counter Register
OCRx – Output Compare Register
ICRx – Input Capture Register (only for 16bit timer)
TIMSKx – Timer/Counter Interrupt Mask Register
TIFRx – Timer/Counter Interrupt Flag Register
Im folgenden werden diese Register erklärt und aufgezeigt wie deren innere Bit-Struktur aussieht. Um die Timer zu konfigurieren, müssen diese Register mit entsprechenden Werten gesetzt werden. Das mutet auf den ersten Blick vielleicht etwas verwirrend an, wird am Ende aber hoffentlich transparent.
2.1 Das Timer Control Register
In den Timer Control Registern werden die wesentlichen Einstellungen für das Timer-Verhalten vorgenommen. Dazu gehören u.a.:
Wahl des Wave Form Generation Modes über die WGMx Bits
Festlegung, was bei einem Compare Match passiert (COM xy Bits)
Prescaler bzw. external Clock über die Chip Select Bits CS1x
Im folgenden ist eine Übersicht über Anzahl und den Aufbau sämtlicher Timer Kontroll-Register der Timer des Arduino UNO dargestellt.
Übersicht der Kontrollregister der Timer0, Timer1 und Timer2
2.2 Das Counter Register TCNTx
Das Timer-Register zählt im Systemtakt oder verlangsamt über den gewählten Prescaler. Die untere Grenze wird als Bottom (Null) bezeichnet, die obere Grenze als Top. Top ist, je nach Modus, festgelegt oder kann variabel definiert werden. Für alle Timer gibt es je nach Größe ein oder zwei 8Bit Counter Register. Sie haben die Bezeichnungen: TCNT0, TCNT1H und TCNT1L sowie TCNT2.
2.3 Das Output Compare Register OCRx
In den beiden Output Compare Registern OCR1A und OCR1B kann man Werte definieren, die permanent mit dem Timer Daten Register TCNTx verglichen werden. Je nach Einstellung und Modus löst eine Übereinstimmung (Compare Match) bestimmte Aktionen aus. Die entsprechenden Aktionen werden über die Timer/Counter 1 Control und Status Register konfiguriert.
Der Compare Output Mode ist in alle Timer integriert. Die Bits COMx0 und COMx1 steuern das Verhalten des OCx-Pins (der Ausgabepin des CompareOutput Modes). Wenn beide Bits auf 0 gesetzt sind, behält der OCx-Pin seine Standardfunktion im Mikrocontroller.
Ist das COMx1 Register gesetzt wird der OCx-Pin bei einem Compare Match auf 0 gesetzt (bei PWM wird er bei Erreichen vom maximalen Wert wieder auf 1 gesetzt). Sind beide Register gesetzt ist diese Funktion invertiert (also bei jedem Compare Match wird OCx auf 1 gesetzt).
Output-Compare-Pins
Jedem Timer sind im Timer-Kontroll-Register sogenannte Output-Compare-Pins zugeordnet. Dem Timer1 sind beispielsweise beim Arduino UNO die zwei Output Compare Pins OC1A (Pin 9) und OC1B (Pin 10) zugeordnet. Die Tabelle gibt eine Übersicht der Zuordnung für UNO und MEGA:
Übersicht der OutputCompare-Pins
2.4 Das Input Capture Register ICR1
Dieses Register hat nur der Timer1. Es hat zwei Funktionen: Bei einem Ereignis an ICP1 wird der Zählerstand von TCNT1 in ICR1 geschrieben. Das ICES1 Bit (Bit6 im TCCR1B Kontroll-Register) legt fest, ob dies bei steigender (ICES1 = 1) oder fallender Flanke (ICES1 = 0) passieren soll.
ICR1 dient, wie OCR1A, in einigen WGM1 Modi als Top Wert. In diesen Fällen ist die Input Capture Register Funktion deaktiviert. Im Gegensatz zu OCR1A ist ICR1 nicht gepuffert, sondern wird sofort überschrieben. Welche Folgen das hat, besprechen wir bei den PWM Modi.
2.5 Das TIMSK Register
Dieses Register wird von allen Timern verwendet. Das TIMSK-Register definiert, bei welchen Ereignissen welche Art Interrupts ausgelöst werden. Das Register hat den nachfolgend dargestellten Aufbau. Die Bedeutung der einzelnen Bits wird unten beschrieben:
Struktur des TIMSK Register
OCIE2 (Timer/Counter2 Output Compare Match Interrupt Enable) Wenn dieses Bit gesetzt wird, wird der Timer/Counter2 Compare Match Interrupt aktiviert (vorrausgesetzt die Interrupts sind global aktiviert).
TOIE2 (Timer/Counter2 Overflow Interrupt Enable) Wenn dieses Bit gesetzt wird, wird der Timer/Counter2 Overflow Interrupt aktiviert.
TICIE1 (Timer/Counter1, Input Capture Interrupt Enable) Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Input Capture Interrupt aktiviert.
OCIE1A (Timer/Counter1 Output Compare A Match Interrupt Enable) Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Output Compare A Match Interrupt aktiviert .
OCIE1B (Timer/Counter1 Output Compare B Match Interrupt Enable) Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Output Compare B Match Interrupt aktiviert.
TOIE1 (Timer/Counter1 Overflow Interrupt Enable)
Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Overflow Interrupt aktiviert.
OCIE0 (Timer/Counter0 Output Compare Match Interrupt Enable) Wenn dieses Bit gesetzt wird, so wird der Timer/Counter0 Compare Match Interrupt aktiviert.
TOIE0 (Timer/Counter0 Overflow Interrupt Enable) Wenn dieses Bit gesetzt wird, so wird der Timer/Counter0 Overflow Interrupt aktiviert.
Sind die jeweiligen Interrupts aktiviert, können diese dann über die Interrupt Service Routinen (ISRs) benutzt werden. Dazu müssen die entsprechenden Interruptvektoren an die ISR übergeben werden:
TIMER1_CAPT_vect für Input Capture
TIMER1_COMPA_vect / TIMER1_COMPB_vect für Compare Match
TIMER1_OVF_vect für Timer Overflow
2.6 Das Timer Interrupt Flag Register TIFRx
Die Timer des ATMega16 können verschiedene Interrupts auslösen um auf verschiedene Timerereignisse zu reagieren. Um die Interrupts zu aktivieren, muss das das entsprechende Bit im TIMSK (Timer/Counter Interrupt Mask Register) gesetzt werden. Bei einem Interrupt Request wird das entsprechende Flag (Bit) im TIFR (Timer Interrupt Flag Register) gesetzt, welches dann den Interrupt auslöst. Man kann also auch durch Setzen des entsprechenden Bits im TIFR einen Interrupt „von Hand“ auslösen.
2.6.1 Output Compare Match Interrupt (OCI)
Erreicht das Register TCNTx den Wert, der im Register OCRx voreingestellt wurde, so tritt ein Timer Compare Match ein. Dabei wird ein Output Compare Interrupt ausgelöst. Auf Wunsch wird der Counter dabei zurückgesetzt. Dazu dient das CTC-Bit (Clear Timer on Compare) im TCCRx-Register.
2.6.2 Timer Overflow Interrupt (TOC)
Ist das Register TCNTx voll, so wird ein Timer/Counter Overflow und damit ein Timer/Counter Overflow Interrupt ausgelo ̈st. Der Timer zählt dabei von 0 aufwärts weiter. So kann periodisch eine Interruptroutine ausgeführt werden.
Bei PWM kann bei Bedarf der Timer Overflow Interrupt aktiviert werden um z.B. das OCRx Register zu ändern, wodurch man einen sinusförmigen Ausgang generieren kann.
2.6.3 Timer Input Capture Interrupt (TICI)
Der TICI wird bei einem Input Capture Event ausgelöst, welches eintritt, wenn am ICP1-Pin eine steigende/fallende Flanke (je nach Einstellung) gemessen wird. Es besteht die Möglichkeit einen Noice Canceler einzuschalten, der dafür sorgt, dass der TICI erst ausgelöst wird, wenn das Signal 4 Takte lang stabil anliegt. Dieser Modus kann genutzt werden um Ereignisse zu zählen oder auf sie zu reagieren.
3. Festlegung der Timer-Betriebsart
Timer können in verschiedenen Modi konfiguriert werden. Man unterscheidet dabei zwischen folgenden Modi:
Normal-Mode Der einfachste Betriebsmodus ist der normale Modus. Die Zählrichtung des Timers ist immer aufsteigend, bis zum Überlauf – da fängt der Zähler wieder bei 0 an. Der Überlauf kann einen Interrupt (Timer-Overflow) auslösen. Der Timer kann so konfiguriert werden, dass beim Erreichen dieses Maximums der TIMERn_OVF_vect ausgelöst wird. Die Frequenz, mit der ein Overflow bei Verwendung des Prozessortakts als Taktquelle auftritt ergibt sich mit:
wobei N =256 bei 8Bit Zählern und N=65536 bei 16Bit-Zählern gesetzt wird.
CTC-Mode Hier zählt der Timer nach oben bis zum Erreichen des OCRn Registers. Das Register TCNTn wird beim Erreichen zurückgesetzt. Der Timer kann so konfiguriert werden, dass beim Erreichen des OCRn Wertes der TIMERn_COMPx_vect ausgelöst wird. Die Frequenz, mit der ein Compare Match bei Verwendung des Prozessortakts als Taktquelle auftritt ergibt sich mit:
PWM-Mode Im PWM Modus werden die OCxy Ausgabe-Pins verwendet um PWM Signale zu erzeugen und auszugeben. Die jeweiligen Pins sind genau festgelegt und lassen sich aus der oben stehenden Übersicht entnehmen.
Fast-PWM-Mode Beim Fast PWM zählt der Timer bis zum Maximum seines Zählberreichs. Das Register OCRn dient als Vergleich und abhängig davon, ob TCNTn kleiner oder größer OCRn ist, kann der OCn Pin auf logisch 0 oder 1 gesetzt werden.
Um die beschriebenen Modi zu setzen sind im Timer-Kontroll-Register die WGMx-Bits vorgesehen. der verschiedenen Timer sogenannte Waveform Bits vorgesehen. Für den Timer2 sind das die Bits WGM20 und WGM21. Diese befinden sich im TCCR2A Register an der Position 0 und 1 siehe Abbildung:
Festlegen des Betriebsmodus „Normal“ im Kontroll-Register des Timers 2
Wenn man den Modus bei Timer0 und Timer1 konfigurieren möchte, würde man analog vorgehen, und die entsprechenden WGM0x- und WGM1x-Bits mit den WaveForm-Bit-Kombinationen belegen. Da wie oben erwähnt jeder Timer sein spezifischer zugeordnetes Kontrollregister besitzt.
4. Erstes Beispiel
Als Beispiel soll der Timer 2 so konfiguriert werden, dass eine LED Leuten soll. Der Timer2 soll dabei im Normal-Modus betreiben werden mit einem Prescaler von 1024. Dazu müssen die Parameter in den zughörigen Kontroll-Rister entsprechend gesetzt werden:
Als erstes wird Kontrollregister A des Timer 2, also TCCR2A auf 0 setzen, damit ist der Pin OC2A inaktiv und der Timer aktiviert:
Die für das Beispiel relevanten Kontroll-Register des Timers 2
Anschliessend werden im Kontrollregister TCCR2B alle CS2x Bits auf HIGH gesetzt (Gelb markiert). Der Prescaler ist damit auf 1024 gesetzt.
Da die WGM2x Bits standardmässig auf 0 gesetzt sind, braucht man hier keine weiteren Aktivitäten durchführen, um den Normal-Modus zu konfigurieren.
Als nächstes wird das Bit OIE2 für den Timer Overflow Interrupt gesetzt. Dadurch wird jedesmal, wenn das TCNT2-Register überläuft (> 255), ein Interrupt ausgelöst.
Nun fehlt noch die Festlegung des LED-Pins. Hierzu nutzen wir die Port-Manipulations-Anweisungen, um Pin 7 als Ausgang zu definieren.
Und abschliessend wird noch die Interrupt-Service Routine definiert, die den Timer2 Overflow Interrupt TIMER2_OVF_vect abfängt.
Das Programm ist dann trotz der vielen Vorüberlegungen alles in allem sehr Kompakt. Das liegt natürlich auch daran, dass die ganzen Bezeichnet bereits vordefiniert wurden, und man dadurch sehr effizient auf die Register und die Bits zugreifen kann:
void setup(){
TCCR2A = 0x00; // Set register TCCR2A from Timer2 to LOW;
TCCR2B = (1<<CS22) + (1<<CS21) + (1<<CS20); // prescaler = 1024
TIMSK2 = (1<<TOIE2); // if TCNT2 > 255, wird ein Interrupt ausgelöst
DDRD |= (1<<PD7); // Portmanipulation: replaces pinMode(7, OUTPUT);
}
void loop() {
}
ISR(TIMER2_OVF_vect){
PORTD ^= (1<<PD7); // toggle PD7
}
Nach dem Start des Programms erkennt man, dass die LED sehr schnell leuchtet. Nach der oben genannten Formel lässt sich die Frequenz auch leicht berechnen:
Wenn man einen kleinere Frequenz haben möchte, müsste man weitere Methoden zum verlangsamen, wie beispielsweise einen Scalefaktor, einführen.
4. Timer im CTC Modus
Im Modus Clear-Timer on Compare wird ein Vergleichswert in ein Compare-Register geschrieben. Für den Timer0 wird das Register OCR0 verwendet. Sobald der Timer0 diesen Wert erreicht (TOP-Wert) wird ein Flag gesetzt. Der Timer0 zählt also nicht mehr von 0 bis 255 (MAX-Wert), wie im Normalmodus, sondern kann irgendwo zwischen 0 und 255 zum Zählabbruch gezwungen werden, um dann wieder bei 0 anzufangen. Analog verhält es sich natürlich bei den anderen Timern. Mit dem Unterschied, dass beim 16Bit-Timer1 die Top- und Max-Werte entsprechend größer sind.
Eine zweite Variante im Normalmodus, die noch nicht angesprochen wurde, besteht darin, dass man den Timer nicht bei 0 sondern bei einem Wert k zwischen 0 und 255 starten lässt. Damit lassen sich ebenfalls die Zählzeiten verkürzen.
4. 1 Timer Interrupt Berechnung
Da die Timer wie oben beschrieben, immer an den Systemtakt gekoppelt sind, werden die Zählregister beim Arduino UNO und MEGA mit einer Takt-Frequenz von 16 Mhz erhöht. Dies bedeutet, dass die Incrementierung eines Registers benötigt. Dementsprechend würde ein 8 Bit Register nach 16 Microsekunden überlaufen und ein 16 Bit Register nach 4,9 ms, wie aus den nachfolgenden Rechnungen hervorgeht.
Register Breite
Zeit bis Überlauf
8 Bit Timer Register
256 * 62,5 ns = 16 µs
16 Bit Timer Register
65536 * 62,5 ns = 4,90 ms
Maximale Laufzeiten der Zählregister
Für die meisten Anwendungen ist das natürlich zu schnell. Deshalb gibt es den so genannten Vorteiler oder auch Prescaler genannt, mit dem die Zeitbasis reduziert werden kann.
Ein Prescaler ist einfach ein binärer Teiler für die Taktfrequenz. Mögliche Werte liegen bei 8, 32, 64, 256 oder 1024. Seine Aufgabe ist es, den Systemtakt um den angegebenen Faktor zu teilen. Steht der Vorteiler also auf 1024, so wird nur bei jedem 1024-ten Impuls vom Systemtakt das Timerregister um 1 erhöht. Entsprechend weniger häufig kommen dann natürlich die Overflows. Die allgemeine Formel für die Frequenz f mit der ein Zähler einen Interrupt auslöst lautet:
(1)
In der folgenden Tabelle sind für den Timer1 (16Bit) und Timer 2 (8Bit) mögliche Prescaler-Werte und die daraus folgenden Zeiten für einen Register Überlauf zusammengestellt. Die Idee hinter der Berechnung ist folgende: Bei 16 MHz benötigt ein Zähl-Impuls 62,5 ns. Dementsprechend benötigt ein 8-Bit Register 256 Zeit für einen Überlauf und somit einen Interrupt.
Prescaler
1
8
64
256
1024
8 Bit Reg
16,00 µs
128,00 µs
1,024 ms
4,096 ms
16,38 ms
16 Bit Reg
4,90 ms
32,76 ms
256,14 ms
1,048 s
4,19 s
Zeiten in Abhängigkeit vom prescale Faktor bis Überlauf
4.2 Setzen der Prescalerwerte
Die Prescaler Werte werden über die CSx-Bits im TCCRx Register definiert. Das x in obiger Tabelle steht wie immer für die Timer-Nummer. Für den Timer1 sind folgende Bits definiert: CS12, CS11 und CS10.
Um nun die Zählgeschwindigkeitfestzulegen, ist der jeweilige Pressale-Wert zu wählen und die entsprechende Bit-Kombination in dem Kontrollregister des Timers zu setzen. Ein entsprechender C-Code wie diese Bits konfiguriert werden ist nachfolgend dargestellt:
Das von oben bekannte Beispiel mit dem Timer2, soll nun auf den 16Bit Timer 1 übertragen werden. Nun aber mit der Vorgabe, dass die LED mit einer vorgegebenen Taktzeit von 1,5s aufleuchten soll.
void setup(){
DDRD |= (1<<PD7); // Portmanipulation: replaces pinMode(7, OUTPUT);
TCCR1A = 0; // clear ctrl register A
TCCR1B = 0; // clear ctrl register B
TCCR1B |= (1 << WGM12); // set bit for CTC mode
TCCR1B |= (1 << CS12); // set bit 2 of prescaler for 1024x
TCCR1B |= (1 << CS10); // set bit 0 of prescaler for 1024x
OCR1A = 23437; // set L & H bytes to 23437 (1.5 sec)
TIMSK1 |= (1 << OCIE1A); // enable interrupt on OCR1A
TCNT1 = 0; // reset counter to zero
}
void loop() {
}
ISR(TIMER2_OVF_vect){
PORTD ^= (1<<PD7); // toggle PD7
}
Betrachtet man das Ergebnis des obigen Programms am Oszillographen, so erkennt man dass saubere Flanken in einer Frequenz von rund 30,6 Hz angezeigt werden.
Gemäß obiger Formel
(2)
Möchte man nun genaue Frequenzen einstellen, benötigt man einen weiteren Skalierungsfaktor und einen Startwert, mit denen sich dann folgende Formel ergibt:
(3)
Für eine Wunsch-Frequenz von 1 Hz nutzen wir den Umstand, dass das TCNTx Register auch mit einem Startwert beschrieben werden kann. Es werden dann nur noch Schritte bis zum Überlauf durchgeführt. Wie in der obigen Formel beschrieben, bleibt aber immer noch eine Gleichung mit mehreren Unbekannten:
(4)
(5)
(6)
Wir kennen aber den Systemtakt, die möglichen Prescalewerte und das der Startwert ganzzahlig und kleiner in diesem Beispiel 256 sein muss. Mit nachfolgendem Zusammenhang und etwas rumprobieren erhält man folgende Ergebnisse:
Etwas systematischeres rumprobieren mit Mathemathematica
Aus der Tabelle lassen sich 3 gute Kombinationen entnehmen: Mit einem Prescaler von 256 finden sich die Startwerte 250 und 500. Bei einem Presclaewert von 64 würde nur der Startwert 1000 passen. Bei einem Prescaler von 1024 haben wir nur annähernd passende Werte, die man aufrunden müsste.
Mit und einem 8-Bit Timer ergibt sich dann
(7)
Damit folgt folgendes überarbeitete Programm, dass die Diode mit genau 1 Hz zum leuchten bringt:
Der Legende von Josephus nach, trieben die Römer eine Gruppe von Kriegern in eine Höhle, um diese dann gefangen zu nehmen. Nach der Legende entschieden diese Krieger aber dann sich nicht gefangen nehmen zu lassen, sondern lieber den Heldentod zu sterben d.h. sich gegenseitig umzubringen. In dem jeder seinen nächsten in der Reihe töten sollte. Also der Erste tötet den Zweiten, der Dritte den Vierten usw. Nun stellt sich natürlich die Frage, sind alle Positionen gleich gut oder gibt es eine günstigste Position?
Um diese Frage zu beleuchten, formulieren wir das Problem etwas weniger kriegerisch um: Dazu stellen uns eine Menge n mit Personen vor, die in einer Reihe stehen und definieren zwei Funktionen: RotateLeft: Nimmt die erste Person in der Reihe und stellt diese ans Ende der Reihe Rest: Entfernt die nächste Person in der Reihe
Der Ablauf sieht nun wie folgt aus: Wir starten mit dem Aufruf der Funktion RotateLeft, diese nimmt die erste Person nach dem diese seinen Nachfolger virtuell erschlagen hat von der Liste und setzt sie am Ende ein. Dann folgt der Aufruf der Funktion Rest, diese eliminiert die zweite Person aus der Liste. Dann folgt wieder die Funktion RotateLeft, gefolgt von Rest usw. Dieser Prozess läuft solange bis nur noch eine Person übrig ist.
Mit Hilfe von Mathematica lässt sich dieser Ablauf sehr anschaulich programmieren und nachvollziehbar darstellen. Wir schreiben dazu eine rekursive Funktion mit dem Namen survivor[liste]. Dieser Funktion übergeben wir immer wieder die aktualisierte Liste der Personen.