Browse Author: profhof

Thermodrucker mit dem Arduino ansteuern

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() {}

Eventzähler

In Bearbeitung

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
 } 
}

Kapazitätsmesser

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:

    \[T = t_i + t_p\]

    \[t_i = \ln(2) \cdot (R_1+R_2) \cdot C_1\]

    \[t_p = \ln(2) \cdot R_2 \cdot C_1\]

Somit ergibt sich für die Periodendauer folgende Gleichung:

    \[T = \ln(2) \cdot (R_1+ 2\cdot R_2) \cdot C_1\]

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:

    \[C= \frac{1} {\ln(2) \cdot (R_1+ 2\cdot R_2) \cdot f}\]

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.

Arduino steuert MOSFETs

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.

Frequent f = 490 Hz
T = 1/f  : T= 1/490 Hz = 2,041 ms
pwm_value 255 : tL = T = 2041 µs
pwm_value 1   bedeutet T/256 == 7,97 µs

pwm_value 127 bedeutet T/256 * 127 = 7,97 µs * 255 = tH = 1/2T
Tastgrad g = (1/2T)/T= 1/2 =50%

pwm_value 255 bedeutet T/256 * 255 = 7,97 µs * 255 = tH = T
Tastgrad g = T/T= 1 = 100%

Damit wird deutlich, dass der Parameter value in der Funktion analogWrite() im direkten Zusammenhang mit der festgelegten Frequenz und der Zeit t_H steht. Über den in der Abbildung beschriebenen Zusammenhang

    \[g =\frac{T}{t_H+t_L} = \frac{\frac{1}{f}}{t_H+t_L}\]

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

// Lüftersteuerung
// PHOF Jan 2021
  const int motorpin = 3;
  int motor_speed = 0; 

void setup(){
  Serial.begin(57600);
  pinMode(motorpin, OUTPUT); 
}

void loop() {
  for (motor_speed = 0; motor_speed <= 255; motor_speed += 5) {
    printSpeed(motor_speed); 
    analogWrite(motorpin, motor_speed); 
    delay(250);
  }
  delay(5000); 
}

void printSpeed(int motor_speed) {
   Serial.print("Current Speed: ");
   Serial.println(motor_speed); 
}

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

Mathematica Essenzen

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.

NSolve[f[x] == 0]   ==>  {{x -> -3.24655}, {x -> 0.576888}, {x -> 2.66966}}

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:

Lösungen = {x, f[x]} /. NSolve[f[x] == 0]
{{-3.24655,0.}, {0.576888, -8.88178*10^-16}, {2.66966, -3.55271*10^-15}}

Extrema = {x, f[x]} /. NSolve[f'[x] == 0]
{{-1.73205, 15.3923}, {1.73205, -5.3923}}

Plot[f[x], {x, -4, 4}, 
 Epilog -> {{PointSize[0.02], Red, Map[Point, Lösungen],
    PointSize[0.02], Magenta, Map[Point, Extrema]}}]

Anschliessend sorgt die Plot-Funktion für die visuelle Darstellung:

Gleitende Mittelwerte

Arithmetischer Mittelwert

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.

RandomInteger[{1, 10}, 20] = {9, 8, 8, 7, 1, 7, 2, 4, 1, 5, 9, 3, 5, 4, 4, 5, 8, 10, 3, 9}

Mean[{9, 8, 8, 7, 1, 7, 2, 4, 1, 5, 9, 3, 5, 4, 4, 5, 8, 10, 3, 9}] ==> 28/5 = 5.6

Median

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.

Median[{1, 2, 3, 4, 5, 6, 7}] ==> 4
oder
Median[{1, 2, 3, 4, 5, 6, 7, 8}] ==> 9/2
oder
Median[{9, 8, 8, 7, 1, 7, 2, 4, 1, 5, 9, 3, 5, 4, 4, 5, 8, 10, 3, 9}] ==> 5

Gleitender Mittelwert

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
}

EEPROM Programmierung

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:

• Spannungsversorgungs Anschlüsse (GND, Vcc) • Adress-Leitungen (A0…Ax)
• Daten-Leitungen (D0-D7)
• Steuer-Leitungen (CE, OE und WE)

Pin Belegung gängiger EEPROM Bausteine
Pin Belegung gängiger EEPROM Bausteine

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:

  1. 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.
  2. Setze OutputEnable OE auf LOW, d.h. Daten werden ausgegeben.
  3. Lege die Adresse an, die ausgelesen werden soll
  4. 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 A_0,.... bis A_{14} ü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.

EEPROM Programmer Prototyp Aufbau

Timer Programmierung

Beitrag befindet sich in Bearbeitung

1. Grundlagen der Timer-Programmierung

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:

        \[f=\frac{Systemtakt}{Prescaler\cdot N}\]


    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:

        \[f=\frac{Systemtakt}{Prescaler\cdot OCR_n}\]

  • 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:

    \[f=\frac{Systemtakt}{1024\cdot 256} = 61,03 \,Hz\]

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 T=\frac{1}{16\cdot 10^6}=62,5  ns 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 BreiteZeit bis Überlauf
8 Bit Timer Register256 * 62,5 ns = 16 µs
16 Bit Timer Register65536 * 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)   \begin{equation*} f=\frac{Systemtakt}{2^{TimerBit}\cdot Prescaler}\end{equation*}

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 t_T = 62,5 ns. Dementsprechend benötigt ein 8-Bit Register 256\cdot t_T Zeit für einen Überlauf und somit einen Interrupt.

Prescaler18642561024
8 Bit Reg16,00 µs128,00 µs1,024 ms4,096 ms16,38 ms
16 Bit Reg4,90 ms32,76 ms256,14 ms1,048 s4,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:

TCCR1B | = (0 << CS12) | (0 << CS11) | (1 >> CS10); //kein Prescale
TCCR1B | = (0 << CS12) | (1 << CS11) | (0 >> CS10); //Prescale auf 8
TCCR1B | = (0 << CS12) | (1 << CS11) | (1 >> CS10); //Prescale auf 64
TCCR1B | = (1 << CS12) | (0 << CS11) | (0 >> CS10); //Prescale auf 256
TCCR1B | = (1 << CS12) | (0 << CS11) | (1 >> CS10); //Prescale auf 1024

Weiteres Beispiel

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)   \begin{equation*} f=\frac{Systemtakt}{2^{TimerBit}\cdot Prescaler} = \frac{16 000000}{2^8\cdot 1024} = 61.0352  Hz\end{equation*}

Möchte man nun genaue Frequenzen einstellen, benötigt man einen weiteren Skalierungsfaktor und einen Startwert, mit denen sich dann folgende Formel ergibt:

 

(3)   \begin{equation*} f_{wunsch} \cdot ScaleFaktor =\frac{Systemtakt}{(2^{TimerBit}-Startwert)\cdot Prescaler} \end{equation*}

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 (2^{TimerBit}-Startwert) Schritte bis zum Überlauf durchgeführt. Wie in der obigen Formel beschrieben, bleibt aber immer noch eine Gleichung mit mehreren Unbekannten:

 

(4)   \begin{equation*} f_w \cdot ScaleFaktor\cdot (2^{TimerBit}-Startwert)\cdot Prescaler=Systemtakt\end{equation*}

 

(5)   \begin{equation*} (2^{TimerBit}-Startwert)=\frac{Systemtakt}{f_w \cdot ScaleFaktor\cdot Prescaler}\end{equation*}

 

(6)   \begin{equation*}   Startwert = 2^{TimerBit} -\frac{Systemtakt}{f_w \cdot ScaleFaktor\cdot Prescaler}\end{equation*}

Wir kennen aber den Systemtakt, die möglichen Prescalewerte und das der Startwert ganzzahlig und kleiner 2^{TimerBit} 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 f_{wunsch} :=1 und einem 8-Bit Timer ergibt sich dann

 

(7)   \begin{equation*}Startwert = 256 -\frac{16 000000}{500\cdot 256}=131\end{equation*}

Damit folgt folgendes überarbeitete Programm, dass die Diode mit genau 1 Hz zum leuchten bringt:

byte counterStart = 131;  
unsigned int scaleFactor = 500; 

void setup(){ 
  TCCR2A = 0x00; 
  TCCR2B = (1<<CS22) + (1<<CS21); // prescaler = 256
  TIMSK2 = (1<<TOIE2); // interrupt when TCNT2 is overflowed
  TCNT2 = counterStart;
  DDRD |= (1<<PD7);
} 

void loop() { 
}

ISR(TIMER2_OVF_vect){
  static int counter = 0;
  TCNT2 = counterStart;
  counter++;
  if(counter==scaleFactor){
    PORTD ^= (1<<PD7);
    counter = 0; 
  }
}
Programm-Sequenz um Timer zu programmieren
// Timmer aktivieren
TCCR0A |= (1 <<  WGM01);   // enable timer0 CTC mode
TIMSK0 |= (1 <<  OCIE0A);  // enable timer0 compare interrupt

TCCR1B |= (1  <<  WGM12);   // enable timer1 CTC mode
TIMSK1 |= (1  <<  OCIE1A);  // enable timer1 compare interrupt

TCCR2A |= (1  <<  WGM21);   // enable timer2 CTC mode
TIMSK2 |= (1  <<  OCIE2A);  // enable timer2 compare interrupt

// Timmer Rücksetzen
// reset a timer unit (replace X by timer number)
TCCRXA = 0;  // set TCCRXA register to 0
TCCRXB = 0;  // set TCCRXB register to 0
TCNTX  = 0;  // reset counter value

//Timer Vergleichswert setzen
OCR0A = 124;   // set compare match register of timer 0 (max. value: 255 = 2^8 - 1)
OCR1A = 20233; // set compare match register of timer 1 (max. value: 65535 = 2^16 - 1)
OCR2A = 20;    // set compare match register of timer 2 (max. value: 255 = 2^8 - 1)

//Prescaler Werte setzen 
//am Beispiel Timer1
TCCR1B | = (0 << CS12) | (0 << CS11) | (1 >> CS10); //kein Prescale
TCCR1B | = (0 << CS12) | (1 << CS11) | (0 >> CS10); //Prescale auf 8
TCCR1B | = (0 << CS12) | (1 << CS11) | (1 >> CS10); //Prescale auf 64
TCCR1B | = (1 << CS12) | (0 << CS11) | (0 >> CS10); //Prescale auf 256
TCCR1B | = (1 << CS12) | (0 << CS11) | (1 >> CS10); //Prescale auf 1024

TCCR0B |= (1  <<  CS00);  // no prescaling for timer0

TCCR2B |= (1  <<  CS22) | (1  <<  CS20);  //Prescale auf 1024 for timer2
void setup(){
//set pins as outputs
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(13, OUTPUT);

cli();//stop interrupts

//set timer0 interrupt at 2kHz
  TCCR0A = 0;// set entire TCCR2A register to 0
  TCCR0B = 0;// same for TCCR2B
  TCNT0  = 0;//initialize counter value to 0

// set compare match register for 2khz increments
  OCR0A = 124;// = (16*10^6) / (2000*64) - 1 (must be <256)
  // turn on CTC mode
  TCCR0A |= (1 << WGM01);
  // Set CS01 and CS00 bits for 64 prescaler
  TCCR0B |= (1 << CS01) | (1 << CS00);   
  // enable timer compare interrupt
  TIMSK0 |= (1 << OCIE0A);

//set timer1 interrupt at 1Hz
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
  OCR1A = 15624;// = (16*10^6) / (1*1024) - 1 (must be <65536)
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS12 and CS10 bits for 1024 prescaler
  TCCR1B |= (1 << CS12) | (1 << CS10);  
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);

//set timer2 interrupt at 8kHz
  TCCR2A = 0;// set entire TCCR2A register to 0
  TCCR2B = 0;// same for TCCR2B
  TCNT2  = 0;//initialize counter value to 0
  // set compare match register for 8khz increments
  OCR2A = 249;// = (16*10^6) / (8000*8) - 1 (must be <256)
  // turn on CTC mode
  TCCR2A |= (1 << WGM21);
  // Set CS21 bit for 8 prescaler
  TCCR2B |= (1 << CS21);   
  // enable timer compare interrupt
  TIMSK2 |= (1 << OCIE2A);


sei();//allow interrupts

}//end setup

// Interrupt Service Routine 

ISR(TIMER0_COMPA_vect){
//timer0 interrupt 2kHz toggles pin 8
}

ISR(TIMER1_COMPA_vect){
//timer1 interrupt 
}
  
ISR(TIMER2_COMPA_vect){
//timer1 interrupt 8kHz toggles pin 9
}


void loop(){
  //do other things here
}

Quellen

  1. AVR-GCC-Tutorial/Die Timer und Zähler des AVR https://www.mikrocontroller.net/articles/AVR-GCC-Tutorial/Die_Timer_und_Zähler_des_AVR
  2. Die Timer/Counter des AVR (Uni Regensburg)
  3. ATMega Datenblatt (deutsche Version)
  4. Timer Teil1 und Teil2 Wolles Elektronik Kiste https://wolles-elektronikkiste.de/tag/timer
  5. und die vielen übrigen Beiträge in Foren

Das Josephus Problem

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.

Survivor[liste_] := 
 Nest[(Rest[RotateLeft[#]]) &, liste, Length[liste] - 1]

Mit Hilfe der Trace-Funktion die wir auf die RotateLeft Funktion anwenden, wird der rekursive Programm-Ablauf deutlicher:

TracePrint[Survivor[Range[20]], RotateLeft]

    RotateLeft
   {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,1}
    RotateLeft
   {4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,1,3}
    RotateLeft
   {6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,1,3,5}
    RotateLeft
   {8,9,10,11,12,13,14,15,16,17,18,19,20,1,3,5,7}
    RotateLeft
   {10,11,12,13,14,15,16,17,18,19,20,1,3,5,7,9}
    RotateLeft
   {12,13,14,15,16,17,18,19,20,1,3,5,7,9,11}
    RotateLeft
   {14,15,16,17,18,19,20,1,3,5,7,9,11,13}
    RotateLeft
   {16,17,18,19,20,1,3,5,7,9,11,13,15}
    RotateLeft
   {18,19,20,1,3,5,7,9,11,13,15,17}
    RotateLeft
   {20,1,3,5,7,9,11,13,15,17,19}
    RotateLeft
   {3,5,7,9,11,13,15,17,19,1}
    RotateLeft
   {7,9,11,13,15,17,19,1,5}
    RotateLeft
   {11,13,15,17,19,1,5,9}
    RotateLeft
   {15,17,19,1,5,9,13}
    RotateLeft
   {19,1,5,9,13,17}
    RotateLeft
   {5,9,13,17,1}
    RotateLeft
   {13,17,1,9}
    RotateLeft
   {1,9,17}
    RotateLeft
   {17,9}
  RotateLeft
   {9}

An diesem Beispiel wird deutlich, was für ein mächtiges Werkzeug Mathematica ist, um sehr komplexe Fragestellungen zu untersuchen.

Bit-Manipulations Mechanismen

Das Dualsystem spielt in der Welt der Steuerungstechnik eine fundamentale Rolle. Daher ist es wichtig zu wissen, wie man mit Binären Zahlen und Logischen Operationen umgehen kann. Hier sollen die wesentlichen  Bit-Operationen vorgestellt werden. Im Folgenden werden die Bit-Operatoren der Sprache C vorgestellt.

Logische Operationen

UND Verknüpfung

Die UND-Verknüpfung wird in C mit dem Operator & durchgeführt. Bei der bitweisen UND-Verknüpfung hat das Ergebnis an den Stellen eine 1, an denen beide Vergleichswerte eine 1 besitzen. 

ODER Verknüpfung

Bei der bitweisen ODER-Verknüpfung hat das Ergebnis an den Stellen eine 1, an denen mindestens einer der beiden Vergleichswerte eine 1 besitzt. Das Operatorzeichen dafür ist das einfache Pipe-Zeichen |.

Exklusiv-Oder Verknüpfung

Bei der bitweisen XOR Verknüpfung hat das Ergebnis an den Stellen eine 1, an denen entweder der eine oder der andere Vergleichswert eine 1 besitzt. Das Operatorzeichen dafür ist das Dach-Zeichen ^.

NICHT Verknüpfung

Bei der bitweisen Negation wird jedes Bit umgekehrt: aus 0 wird 1 und aus 1 wird 0. Das Operator-Zeichen dafür ist die Tilde ~.

Beispiel-Programm

Im folgenden sollen die oben vorgestellten Verknüpfungen innerhalb eines Beispielprogramms umgesetzt werden:

    int x = 5;       // binary: 101
    int y = x & 1;   // now y == 1
    x = 4;           // binary: 100
    y = x & 1;       // now y == 0
    y = x | 2;       // now y == 110

    int x = 12;     // binary: 1100
    int y = 10;     // binary: 1010
    int z = x ^ y;  // binary: 0110, or decimal 6

    int a = 103;    // binary:  0000000001100111
    int b = ~a;     // binary:  1111111110011000 = -104

Schiebe-Operationen

Durch Verschieben, nach links mit << und nach rechts mit >>, wird ein Binärwert um eine bestimmte Anzahl von Bits nach links oder rechts verschoben. Die durch die Verschiebung freiwerdenden Stellen werden mit 0 gefüllt.

01101011 << 2 ergibt: 10101100
01101011 >> 4 ergibt: 00000110

Bit-Manipulation

In den folgenden Abschnitten wird gezeigt, wie man in einer Bitfolge gezielt einzelne Bits setzt, löscht oder umkehrt.

Setzen eines Bits

Um ein bestimmtes Bit innerhalb eines Bytes zu setzen kann man entweder eine Kombination aus einer Shift- und einer ODER-Operation verwenden oder den Maskierungsansatz verwenden.

Um das n-te Bit einer Variable A zu setzen wird folgende Formel verwendet:

Setzen von Bit 0:
0100 |= (1 << 0) ergibt: 0100 | 0001 ergibt: 01001
Setzen von Bit 3:
0100 |= (1 << 2) ergibt: 0100 | 0100 ergibt: 0100 (keine Veränderung)

Alternativ lässt sich ein Bit auch direkt über eine Maske setzen. Dazu verknüpft man den zu prüfenden Wert mit einer sogenannten Maske, in der genau das Bit (und nur das Bit) gesetzt ist, nach dem man setzen bzw. das man prüfen möchte. Der einzige Unterschied zur vorherigen Methode ist, dass die Maske manuell erstellt wird und nicht durch eine shift-Operation generiert wird.

Überprüfung ob ein BIT gesetzt ist
// Bit Operationen - Beispiel

  int Wert  = 0b01001111;
  int Maske = 0b00001000;
  int Pin = 9;
  
void setup() {
  Serial.begin(9600);
  pinMode(Pin,OUTPUT);

  // wenn Bit gesetzt leuchtet LED
  digitalWrite(Pin, (Wert & Maske) == Maske);
  
   if ( (Wert & Maske) == Maske) {
       Serial.println("Bit ist gesetzt");
    }
}

void loop(){
}

Löschen eines Bits

Das Löschen eines Bits kann durch die Kombination aus einem Shift, dem Komplement und der UND-Verknüpfung realisiert werden. Auch hier besteht die Möglichkeit anstelle des Verschiebens wieder Manuell eine Bit-Maske zu erzeugen und einzusetzen. Um das n-te Bit in der Variable A zu löschen (von 0 an gezählt, also vom niederwertigsten Bit) wird folgender Ausdruck verwendet:

Beispiel wie das Bit 0 der Variable A gelöscht (auf LOW) gesetzt werden kann:

A =  65  ==> 1000001
A &= ~ (1 << 0)   ==> 1001000 // lösche Bit 0

Togeln eines Bits

Das Wechseln eines Bits geschieht durch die Kombination der Shift-Funktion und der Exklusiv-ODER-Verknüpfung. Dabei wird das entsprechende Bit von 0 auf 1 oder von 1 auf 0 gesetzt. Um den Zustand des n-ten Bits in der Variable A zu wechseln (von 0 an gezählt) wird folgender Ausdruck verwendet:

byte A =  65;       //  ==> 1000001 
Serial.println(A);  // A=65
// toggle Bit 0
A = (A^=(1 << 0));  // ==> 1001000 
Serial.println(A);  // A=64

Bestimmung des Low-Bytes und High-Bytes

In verschiedenen Fällen benötigt man von einer 16-Bit-Variable die unteren 8-Bit und die oberen 8 Bit separiert. Hierzu können die oben beschriebenen Mechanismen verwendet werden:

 word wert = 27543;
  byte high_byte = wert >> 8; // oder (wert & 0xff00) >> 8
  byte low_byte  = wert & 0x00ff;

Arduino Funktionen

Da die oben beschriebenen Bit-Manipulationen relativ häufig vorkommen, und nicht immer jederzeit von weniger geübten programmieren abgerufen werden können, haben sich die Entwickler entschlossen, die häufig benötigten Funktionen in den Sprach-Umfang zu integrieren:

FunktionErklärung
bit(n)Berechnet den Wert des angegebenen Bits (Bit 0 ist 1, Bit 1 ist 2, Bit 2 ist 4, etc.).
bitClear(var,n)var: Die Zahlenvariable, deren Wert manipuliert werden soll.
n: Bit gelöscht werden soll; Startet bei 0 für das least-significant (rechteste) Bit.
bitRead(var,n)Liest n-tes Bit aus der Variable var
bitSet(var,n)Setze Bit n in der Variable var
highByte(var)Liest das most-significant (linkeste) Bit eines Wortes (oder das zweitkleinste Bit eines größeren Datentypes).
lowByte(var)Liest das least-significant (rechteste) Bit eines Wortes oder größeren Datentypes.
Vordefinierte Bit-Manipulations-Funktionen beim Arduino

Weitere Details finden sich in der Arduino-Referenz Dokumentation.