Die Steuerung eines Prozesses kann mechanisch, elektrisch, elektronisch (digital) oder als eine Kombination davon erfolgen. Desweiteren unterscheidet man zwischen Steuerung mit und Steuerung ohne Rückkopplung (Feedback). Rückkopplung bedeutet, dass ständig kontrolliert wird, ob die Steuergrösse (Eingabe- oder Sollgrösse der Steuerung) auch dem realen, momentanen Zustand des Systems entspricht. Die Differenz, die sich bei dieser Kontrolle ergibt, ist dann wieder Teil der Eingangsgrössen der Steuerung. Man spricht dann von einem Regelkreis. Typischerweise wird ein Regelkreis wie in Abb. 3 dargestellt.

Abbildung 3: Typisches Modell eines Regelkreises
Im folgenden wollen wir uns auf digitale Steuerungen beziehen. Ausserhalb des gesteuerten Prozesses gibt es also nur Bits und Bytes und Berechnungen auf diesen.
R(t) ist der Sollwert (als digitale Information) der Stellgrösse
zum Zeitpunkt t.
B(t) ist der tatsächliche Wert, in gewandelter und evtl. transponierter
Form (z.B. Temperatur
digitale Information).
E(t) ist einfach R(t) - B(t).
G(t) ist die Transferfunktion, die zentrale Komponente der Steuerung,
in ihr wird mit E(t) als Eingabe der neue Steuerwert C(t) berechnet.
Dieser soll den Prozess derart manipulieren, da\3 der Fehler E(t) möglichst
klein wird, also der Proze\3 sich so verhält, wie er eben soll (
steuern, regeln).
Historisch, vor dem Durchbruch der Elektronik, wurden zum Steuern analoge
sogenannte PID-Regler (Proportionalglied, Integralglied und
Differenzialglied) verwendet. Deren Transferfunktion hatte folgende
Gestalt:
![]()
Dabei sind
geeignet gewählte Konstanten. Die PID-Funktion war auch Ausgangspunkt
beim Suchen der geeigneten Transferfunktion für unser Fahrzeug.
Auf unserem Fahrzeug befinden sich zwei Regelkreise, die jedoch miteinander
gekoppelt sind. Zunächst möchten wir feststellen, wie man am
besten für ein einzelnes Rad eine Steuerung programmiert, die zugleich
schnell auf Störungen reagiert, d.h. die Drehzahl wieder schnell auf
konstant
regelt, als auch gleichzeitig ein stetiges, ruckelfreies Laufverhalten
gewährleistet. Danach wollen wir uns dem Problem widmen, wie man die
beiden Regelkreise am besten zu einem einzigen vereinigt.
Der Regelkreises sieht hier dem allgemeinen Schema ganz ähnlich:

Abbildung 4: Regelkreis für ein einzelnes Rad
Eine momentane Geschwindigkeit, wie sie in Abb.4 vorkommt, mu\3 erst einmal berechnet werden, dazu haben wir zwei Möglichkeiten getestet:
In beiden Fällen gehen wir davon aus, dass die Drehgeschwindigkeit
von Motor und Fahrzeug stets im gleichen Verhältnis zueinander stehen,
d.h. da\3 die Räder stets festen Kontakt zum Boden haben, es also
keinen Schlupf gibt.
Nun ergibt sich das Problem des Timer-Überlaufes. Man kann, wie schon erwähnt, den Timer mit unterschiedlicher Geschwindigkeit hochzählen lassen (19 kHz bis 2,5 MHz). Bei einem 16-Bit Zähler ergeben sich dann alle 26 ms bis 3,36 s Überlaufe. Wir müssen nun einen vernünftigen Wert für die Timergeschwindigkeit herausfinden. Einerseits soll die Zeitmessung feinkörnig sein, um die Geschwindigkeit auch exakt zu erfassen und regeln zu können, auf der anderen Seite sollte der Timer nicht zu schnell sein und nicht zwischen zwei Lichtschrankenimpulsen mehr als einmal überlaufen.
//Diese Interruptroutine wird bei einem Timerueberlauf
// (des Timer T1) aufgerufen . Sie merkt sich die Zeit, die
// bis zum aktuellen Ueberlauf schon vergangen war. Diese wird
// benoetigt, um beim naechsten Lichtschrankenimpuls die
// Momentangeschwindigkeit korrekt zu berechnen. Bei jedem
// folgenden Lichtschrankenimpuls wird das flag overflowR/L
// dann wieder auf 0 gesetzt.
void time_handlert1(void){
if( overflowR ==0){
merkeCCR = 0xffff - merkeCCR;
overflowR = 1;
}
else
merkeCCR = 0xffff; // bei erneutem Ueberlauf $\rightarrow$ Maximalwert
//(Rad wahrscheinlich still)
if( overflowL == 0){
merkeCCL = 0xffff - merkeCCL;
overflowL = 1;
}
else
merkeCCL = 0xffff;
};
// Interruptroutine fuer Impulse der rechten Lichtschranke
// Zur Bestimmung der bisher gefahrenen Strecke werden
// die Anzahl der Impulse hochgezaehlt (in segmentcounterR).
// Ausserdem wird die Momentangeschwindigkeit (speedR) bestimmt.
void segment_handlerR(void){
segmentcounterR++;
if(ueberlaufR == 0){
speedR = *CCR - merkeCCR;
/*die Geschwindigkeit wird nach unten begrenzt: */
if(speedR >=MINSPEED)
speedR = MINSPEED;
}
else{
if(ueberlaufR == 1){ /* Behandlung von Timerueberlauf */
if(*CCR < (MINSPEED - merkeCCR))
speedR = *CCR + merkeCCR;
else
speedR = MINSPEED;
}
if(ueberlaufR > 1 )
speedR = MINSPEED;
ueberlaufR = 0;
}
merkeCCR = *CCR;
};
In dem Steuerungsprogramm verwenden wir zwei Vereinfachungen:
Im Gegensatz zu analogen Steuerungssystemen erfolgt in digitalen Systemen
keine kontinuierliche Korrektur ( d.h. in kontinuierlich werden kontinuierliche
Werte ein- und ausgegeben) sondern zu diskreten Zeiten werden Werte eingelesen
und ausgegeben. In der Zwischenzeit werden sie von einem Halteglied konstant
gehalten. Erst dadurch, da\3 man die Periodenzeit sehr klein macht, ergibt
sich der Eindruck einer kontinuierlichen Regelung.
In unserem Regelkreis sind die Zeitpunkte der Ausgabewertänderung durch periodische Aufrufe der Funktion regelung() gegeben. Ein Ausgabewert kann dabei, bedingt durch den 8-Bit D/A- Wandler, nur einen von 256 möglichen Zuständen annehmen.
Erst dadurch, dass man die Periodenzeit sehr klein macht, ergibt sich
der Eindruck einer kontinuierlichen Regelung.
Mit welcher Frequenz sollte in unserem Fall eine Wertänderung vorgenommen
werden? Dazu muss man einen Blick auf die Frequenzen legen, mit der externe
Ereignisse, also Veränderungen des Prozesses, an die Steuerung herantreten
können. Wir können die Anzahl der Lichtschrankenimpulse pro Rad
und Sekunde nach oben auf etwa 50 abschätzen. Bei zwei Rädern
ergibt sich so maximal 100 mal pro Sekunde die Möglichkeit eine Geschwindigkeitsänderung
zu registrieren. Es zeigte sich auch experimentell, dass es sinnvoll ist,
die Steuerfunktion mit eben dieser Frequenz von 100 Hz zu beteiben.
Es zeigte sich ebenfalls, dass die einfachste Steuerfunktion die beste
ist. Dabei berücksichtigen wir nur das erste Glied der PID-Funktion.
Konkret sieht dies so aus:
neuer-Steuerwert := alter-Steuerwert - FAKTOR * akuteller-Fehler} wobei aktueller-Fehler = sollgeschwindigkeit - aktuelle-geschwindigkeit
Dabei gibt es folgende Entsprechnungen mit der PID-Funktion:
C(t) neuer-Steuerwert
E(t) akuteller-Fehler
R(t) sollgeschwindigkeit
B(t) aktuelle-geschwindigkeit
FAKTOR
Der Bestimmung der Konstanten FAKTOR (entspr
) kommt dabei die entscheidende Bedeutung zu. Wählt man FAKTOR zu
gross, so wird die Regelung unruhig, sie überkorrigiert, die Geschwindigkeit
alterniert dabei in immer grösseren Amplituden um die Sollgeschwindigkeit.
Wählt man FAKTOR hingegen zu klein, dann ist die Regelung zu langsam.
Zwar ist der Lauf gleichmässig stetig, doch es dauert zu lange, bis
die Sollgeschwindigkeit erreicht wird.
Experimente mit Transferfunktionen, die das Integral- und/oder Differenzialglied
enthalten, zeigten alle chaotische Erbegnisse, so fiel es leicht sich für
die einfache Steuerfunktion zu entscheiden.
Nachdem wir nun in der Lage waren, ein einzelnes Rad auch bei wechselndem
Drehwiderstand mit einer vorgegebenen Geschwindigkeit drehen zu lassen,
mussten wir uns überlegen, wie wir die beiden bis jetzt unabhängigen
Regelkreise zu einem einzigen zusammenfassen. Die Fahrtrichtung des Fahrzeugs
ergibt sich, wie bereits angedeutet, aus dem Geschwindigkeitsverhältnis
der beiden Antriebsräder. Bezogen auf einen Streckenabschnitt (Kreisausschnitt
oder Gerade) ist aber das Verhältnis der Geschwindigkeiten gleich
dem Verhältnis der zurückgelegten Wegstrecke. Die Wegstrecke
ist dabei gerade das Integral der Geschwindigkeit. Die Variable segmentcounterR/L
enthält die Anzahl der in einem Streckenabschnitt erhaltenen Lichtschrankenimpulse.
Diese lä\3t sich, da wir von Schlupf abstrahieren, als Ma\3 für
die bisher zurückgelegte Strecke (bezüglich eines Streckenabschnitts)
interpretieren. Daraus ergibt sich für einen Streckenabschnitt:
![]()
Da wir nur Integerzahlen innerhalb unserer Steuerug kennen, schreiben wir
für c
.
Diese Zahl (Bruch) schreibt also das Sollverhältnis der Geschwindigkeiten
von rechtem Rad zu linkem Rad vor.
Die Idee bei der Richtungssteuerung ist, da\3 ständig überwacht
wird, ob das Verhältnis
gleich
ist. Ein (Richtungs-)Fehler ergibt sich dann folgenderma\3en:
![]()
Wenn der Richtungsfehler stets sehr klein ist, können wir davon ausgehen,
da\3 unser Fahrzeug recht genau die richtige Bahn beschreibt. Dem wollen
wir oberste Priorität geben, ob dabei die korrekte Geschwindigkeiten
gefahren wird hat zweite Priorität. Aus
und der Zahl
lassen sich die Sollgeschwindigkeiten von linkem und rechtem Rad (sollspeedR/L)
berechnen. Daraus ergibt sich dann ein Geschwinigkeitsfehler für jedes
Rad (fehlerR = sollspeedR - speedR). Nun die Transferfunktion für
das rechte Rad:
![]()
und fürs linke Rad:
![]()
Bemerkungen:
Nun möchten wir Aussagen darüber treffen, ob unser Realzeitsystem auch wirklich zuverlässig ist. Auf der einen Seite stehen da natürlich praktische Tests, auf der anderen Seite versuchen wir durch eine mehr theoretische Herangehensweise auszuschlie\3en, da\3 es zu einer Situation kommen kann, in der unsere Steuerung versagt. Wir beschränken uns dabei auf das Betrachten des zeitlichen Verhaltens der Steuerung. Einen Beweis der Korrektheit im klassischen Sinne der Informatik, also ob Ergebnisse vom Inhalt her korrekt berechnet werden, wird es nicht geben. Interessant ist vor allem Extremsituationen zu betrachten, in denen das System unter hoher Last steht, d.h. innerhalb kurzer Zeit viele Eingaben bzw. Unterbrechungen anfallen. Wir wollen die Frage beantworten, ob es sein kann, da\3 unser System bei Maximallast, d.h. bei maximaler Frequenz aller Interruptquellen, die Steuerungsaufgabe nicht mehr erfüllen kann. Wir versuchen die Situation auszuschlie\3en, da\3 ein Routine, die den Interrupt A bedient, noch bevor sie vollständig abgearbeitet ist, erneut durch den Interrupt A (verursacht durch externe (Lichtschranken) oder auch interne Geräte (Timer)) aufgerufen wird. Hierzu mu\3 man in Erwägung ziehen, da\3 die Ausführung einer Routine in die Länge gezogen wird, falls sie von Routinen, welcher Interrupts höherer Priorität bedienen, unterbrochen wird. Die Routine (Funktion) regelung() wird beispielsweise bei Eintreffen einen Lichtschrankeninterrupts von den Routinen segment-handlerR/L() unterbrochen.
Da uns keine entsprechenden Werkzeuge zur Verfügung stehen, müssen wir die Analyse des zeitlichen Verhaltens vollständig von Hand unternehmen. Zunächst wollen wir wissen, wieviel Rechenzeit die einzelnen Routinen maximal verbrauchen. Dazu müssen wir auch einen Blick auf die Art und Weise der Übersetzung des C Compilers legen, denn nur von den Assemblerbefehlen kennen wir die genauen Ausführungszeiten.
Wir schreiben also ein paar einfache Testprogramme, um herauszufinden,
wie der C Compiler einzelne C-Instruktionen in Assemblercode übersetzt:
Seien a,b integer (16 Bit)

dabei ist KR die aktuelle Kellerrahmenadresse, Rx/y Register der CPU und
A(a) der Offset der Adresse von a bezüglich des Kellerrahmens. Die
mov-Befehle haben die Form mov Ziel,Quelle .
Sämtliche anderen Übersetzungen folgen dem allgemeinen Schema für imperative Programmiersprachen (siehe Buch Übersetzerbau von Wilhelm/Maurer). Insbesondere kommen beim jedem Funktionsaufruf 7 mov-Befehle, 2 Additionsbefehle und ein return-Befehl im Pro- bzw Epilog dazu. Verwenden wir long statt 16 Bit-integer Arithmetik, so besteht wird eine Addition mit 4 mov- und 2 add-Befehlen und ein Multplikation mit 6 mov-, 2 add- und 3 mul-Befehlen übersetzt.
Nun wollen wir wissen, wieviel Zeit die einzelnen Assemblerbefehle benötigen.
Diese wird zunächst angegeben in States, welche abhängig von
der Taktfrequenz des Chips ist. Für 40 MHz gilt:
![]()
Jedem Maschinenbefehl B ist ein Wert T(ROM) (B) zugeordnet. Dieser gibt an, wieviel states ein Befehl zur Abarbeitung benötigt, würde er im ROM stehen. Für die meisten Befehle liegt dieser Wert bei 2 oder 4. T(ROM) (mul) = 10, T(ROM) (div) = 20.
Da der Programmcode jedoch im externen Speicher steht, und dieser nur
über einen 8-Bit Bus mit der CPU verbunden ist gelten folgende Werte:
bei 2-Byte-Befehl:
![]()
bei 4-Byte-Befehl:
![]()
ACT ( ALE (Adress Latch Enable) Cycle Time) gibt Auskunft über die Geschwindigkeit des externen Buszugriffs (Multiplex, Demultiplex). ACT berech- net sich aus verschieden Registerinhalten und beträgt im konkreten Fall 4 states.
Es ergeben sich folgende Zeitwerte für die Maschienenbefehle:


Je nachdem, ob die Bedingungen in den if-Abfragen erfüllt sind oder
nicht, ergibt sich eine Anzahl von minimal 254 bis maximal 512 states (ohne
Pro- Epilog, 352 bis 610 mit)
Das entspricht, samt Pro- Epilog, einer Zeitdauer von
.

Anzahl states: 162 + 98(Pro- Epilog) + 18 (Parameterübergabe) = 278
Zeit für ein ParOutputaufruf: 13,9
s.

Maximale Anzahl der States (mit Pro-Epilog): 2144
Zeit für ein Steuerfunktionsaufruf: 102,3
s.

Zeit: 2*220 +98 = 538, states= 16,9 ms

Wir zeichnen nun eine Zeitachse mit dem Schlechtesten-Fall-Zenario an Interrupteingängen:

Abbildung 5: Schlechtester Fall beim konkreten Fahrzeug. Die Laenge
der Routinen ist um den Faktor 10 zu breit dargestellt.
Die Situation in Abb.5 geht von einer höchsten Drehzahl von 2 Umdrehungen pro Sekunde (also maximal 96 Lichtschrankeninterrupts) aus, was für unser Fahrzeug (leider) vollkommen ausreicht, jedoch nicht mit den geforderten 6 km/h übereinstimmt. Wie würde die Situation dann aussehen: Bei einem Raddurchmesser von 18 cm ergäben sich rund 9 Umdrehungen pro Sekunde. Das sind 216 Impulse pro Rad und Sekunde, also 432 Impulse pro Sekunde insgesamt. Wiederum sollte die Steuerfunktion so schnell laufen, das sie auf jede Geschwindigkeitsänderung sofort reagieren kann. Gehen wir also von einer Frequenz von 400 Hz für die Steuerfunktion aus und konstruieren nochmals obiges Schaubild:

Abbildung 6: Schlechtester Fall beim Fahrzeug der Aufgabenstellung,
die Laenge der Routinen ist ebenfalls um den Faktor 10 zu breit dargestellt
Wie man nun in Abb.6 sieht, kann es nie vorkommen kann , dass die Steuerfunktion regelung(), die ja mit konstant 400 Hz läuft, von hereinbrechenden Interruptanforderungen für Segment-handlerL/R() oder time-handler() so lange unterbrochen wird, dass sie nicht vor ihrem erneuten Aufruf abgearbeitet wäre. Das gleiche gilt (offentsichtlich) für die anderen (wesentlich kürzeren) Routinen auch. Diese Aussagen sind nur möglich, weil wir sinnervollerweise von einer Oberschranke für die Aufruffrequenzen der Interruptroutinen (432 Hz) ausgehen können.