Mikrocontroller-Programmierung: Wiederholung und Übungsaufgaben
https://bildung.social/@oerinformatik/
https://oer-informatik.de/mikrocontroller-programmierung-uebungsaufgaben
Mikrocontroller-Programmierung, C, Datentypen, Bitoperationen, Port-Register, Programmablaufplan, Struktogramm, Interrupt, Zustandsautomat, Übungsaufgaben, Prüfungsvorbereitung
tl/dr; (ca. 71 min Lese- und Bearbeitungszeit): Wiederholung und Übungsaufgaben zur Mikrocontroller-Programmierung in C – von Datentypen und Bitoperationen über Port-Register, Schleifen, PAP und Struktogramm bis zu Interrupts, Zustandsautomat und Fehlersuche. Du rechnest mit Datentypen und Bitmasken, führst Schreibtischtests durch, setzt einen Programmablaufplan und ein Struktogramm in C um, ergänzt den Zustandsautomaten einer Torsteuerung und findest fünf Fehler in einem kurzen Programm. Zu jeder der 19 Aufgaben lässt sich eine Beispiellösung einblenden [AI-Assisted]. (Zuletzt geändert am 13.11.2026)
Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.
Als Programmier-Grundlagen kann ich nach Durcharbeiten des Artikels…
… passende Datentypen wählen und Überläufe erkennen.
… einzelne Bits mit Masken setzen, löschen, umschalten und abfragen.
… Pins über DDR-, PORT- und PIN-Register als Ein- oder Ausgang konfigurieren.
… Schleifen und Verzweigungen mit einem Schreibtischtest nachvollziehen.
… Programmablaufpläne und Struktogramme lesen und in C umsetzen.
Darüber hinaus kann ich als Programmier-Expertenwissen…
… Interrupt-Service-Routinen schreiben und
volatilesowie atomaren Zugriff begründen.… blockierende und nicht blockierende Zeitsteuerung unterscheiden.
… einen Zustandsautomaten aus einem Zustandsdiagramm programmieren.
… typische Fehler in C-Programmen finden und korrigieren.
In der Geräte- und Systemtechnik gehört es zum Kernwissen, kurze C-Programme zu lesen, zu ergänzen und auf Fehler zu prüfen: Datentypen und Überläufe, Bitoperationen, Port-Register, Kontrollstrukturen, Programmablaufpläne und Struktogramme, Interrupts, Zeitsteuerung und Zustandsautomaten. Die Hardware rund um den Mikrocontroller steht im Artikel Mikrocontroller: Wiederholung und Übungsaufgaben.
Dieser Artikel ist in acht Schritten aufgebaut. Schritt 8 ist eine Fehlersuche in einem kompletten Programm. Jeder Schritt beginnt mit einer kurzen Wiederholung, danach folgen Aufgaben:
gebundene Aufgaben (Auswahlaufgaben): Genau eine der fünf Antworten ist richtig.
ungebundene Aufgaben: Lösungsweg mit Ansatz und Ergebnis.
Hinweise zur Bearbeitung: Die Beispiellösung lässt sich unter jeder Aufgabe einblenden. Probiere es aber erst selbst. Die Beispiellösungen sind immer gleich aufgebaut: Oben steht, was gegeben ist (Geg.), bei jeder Teilaufgabe, was gesucht ist (Ges.). Bei Programmieraufgaben ist der Code das Ergebnis; andere gleichwertige Lösungen sind ebenfalls richtig.
Die Programme sind in C für einen 8-Bit-Mikrocontroller geschrieben (Registernamen wie beim ATmega328P, Datentypen aus stdint.h).
Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?
Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.
Erstelle neue Übungsaufgaben zum Thema „Mikrocontroller-Programmierung“.
Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.
Fachliche Grundlage:
- https://oer-informatik.de/mikrocontroller-programmierung-uebungsaufgaben
- Stilbeispiel: https://oer-informatik.de/mikrocontroller-programmierung-uebungsaufgaben
Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Mikrocontroller-Programmierung
- C
- Datentypen
- Bitoperationen
- Port-Register
- Programmablaufplan
- Struktogramm
- Interrupt
- Zustandsautomat
Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan
Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
danach frage.
1 Datentypen und Zahlenbereiche
Grundlagen
| Datentyp | Größe | Wertebereich |
|---|---|---|
uint8_t |
8 Bit | 0 … 255 |
int8_t |
8 Bit | -128 … 127 |
uint16_t |
16 Bit | 0 … 65 535 |
int16_t |
16 Bit | -32\,768 … 32 767 |
uint32_t |
32 Bit | 0 … 4 294 967 295 |
float |
32 Bit | Gleitkommazahl, ca. 7 signifikante Stellen |
Ein Überlauf entsteht, wenn ein Ergebnis nicht in den Datentyp passt. Bei vorzeichenlosen Typen wird modulo 2^n weitergezählt: 255 + 1 ergibt nach der Zuweisung an eine
uint8_t-Variable 0.Rechnungen werden in C mindestens im Typ
intbzw.unsigned intausgeführt; auf 8-Bit-Mikrocontrollern haben diese nur 16 Bit. Zwischenergebnisse können deshalb überlaufen, obwohl das Endergebnis passt. Abhilfe: einen Operanden vorher in einen größeren Typ umwandeln (Typumwandlung, cast), z. B.(uint32_t)a * b.Die Division ganzer Zahlen schneidet die Nachkommastellen ab:
7 / 2ergibt 3,7 % 2(Rest) ergibt 1.Auf Mikrocontrollern ohne Gleitkommaeinheit ist
floatlangsam und speicherhungrig; man rechnet deshalb oft mit ganzen Zahlen in kleinen Einheiten (mV statt V).
Aufgabe 1.1 (gebunden)
Ein 10-Bit-AD-Umsetzer liefert Werte von 0 bis 1023. Welcher Datentyp speichert den Wert mit dem geringsten Speicherbedarf?
uint8_tuint16_tint8_tfloatuint32_t
Richtige Antwort: 2
Geg.: Wertebereich 0 … 1023
Ges.: Datentyp mit geringstem Speicherbedarf
1023 passt nicht in 8 Bit (höchstens 255), aber in 16 Bit: \underline{\underline{\texttt{uint16\_t}}}, Antwort 2. uint8_t (Antwort 1) und int8_t (Antwort 3, höchstens 127) sind zu klein. float (Antwort 4) und uint32_t (Antwort 5) brauchen 4 Byte statt 2.
Aufgabe 1.2 (gebunden)
Welchen Wert hat z nach den folgenden Zeilen?
uint8_t z = 250;
z = z + 10;260
255
4
0
-6
Richtige Antwort: 3
Geg.: uint8_t z = 250; z = z + 10;
Ges.: Wert von z
250 + 10 = 260 passt nicht in 8 Bit. Es bleibt der Rest modulo 256:
260 - 256 = \underline{\underline{4}}
Antwort 3. 260 (Antwort 1) kann uint8_t nicht speichern. Eine Begrenzung auf 255 (Antwort 2) findet nicht statt. 0 (Antwort 4) wäre das Ergebnis von 256. -6 (Antwort 5) ist das Bitmuster des Ausgangswerts 250 (\mathrm{FA}_\mathrm{h}), als int8_t gelesen – vorzeichenlose Typen haben keine negativen Werte.
Aufgabe 1.3 (ungebunden, 6 Punkte)
uint16_t adc = 1000; // Messwert, 0 ... 1023
uint16_t mv = adc * 5000 / 1023; // Spannung in mVErkläre, warum
mvnicht den erwarteten Wert enthält, wennint16 Bit groß ist. (2 Punkte)Korrigiere die zweite Zeile und berechne das Ergebnis. (2 Punkte)
Gib die Werte von
17 / 5und17 % 5an. (2 Punkte)
Geg.: adc = 1000 (uint16_t); int hat 16 Bit; Formel adc * 5000 / 1023
a) Ges.: Ursache des falschen Ergebnisses
Die Multiplikation wird zuerst ausgeführt, im 16-Bit-Typ:
1000 \cdot 5000 = 5\,000\,000 > 65\,535
Das Zwischenergebnis \underline{\underline{\text{läuft über}}}: adc und 5000 werden als unsigned int (16 Bit) multipliziert, es bleibt 5\,000\,000 \bmod 65\,536 = 19\,264. Danach ergibt 19\,264 : 1023 ganzzahlig nur mv = 18. Das richtige Endergebnis (etwa 4888) würde dagegen in 16 Bit passen.
b) Ges.: korrigierte Zeile, Ergebnis
uint16_t mv = (uint32_t)adc * 5000 / 1023;\frac{1000 \cdot 5000}{1023} = \frac{5\,000\,000}{1023} \approx 4887{,}6 \quad\Rightarrow\quad \texttt{mv} = \underline{\underline{4887}}
Die Ganzzahldivision schneidet die Nachkommastellen ab.
c) Ges.: 17 / 5 und 17 % 5
17 = 3 \cdot 5 + 2: 17 / 5 ergibt \underline{\underline{3}}, 17 % 5 ergibt \underline{\underline{2}}.
2 Bitoperationen
Grundlagen
Bitoperatoren (Beispiel mit x = 0b1100):
&bitweises UND:x & 0b1010ergibt0b1000|bitweises ODER:x | 0b0011ergibt0b1111^bitweises XOR:x ^ 0b0110ergibt0b1010~bitweise Negation:~xinvertiert alle Bits<<,>>Schieben nach links bzw. rechts:1 << 3ergibt0b1000
Mit einer Maske (1 << n) wird gezielt Bit n bearbeitet, ohne die anderen Bits zu verändern:
Bit setzen:
reg |= (1 << n);Bit löschen:
reg &= ~(1 << n);Bit umschalten:
reg ^= (1 << n);Bit abfragen:
if (reg & (1 << n)) { ... }
Logische Operatoren (&&, ||, !) liefern dagegen nur „wahr“ (1) oder „falsch“ (0). Die Schreibweise 0b… ist eine Erweiterung des AVR-Compilers (GCC).
Aufgabe 2.1 (gebunden)
Welche Anweisung setzt Bit 5 von PORTB auf 1, ohne die übrigen Bits zu verändern?
PORTB = (1 << 5);PORTB &= (1 << 5);PORTB ^= (1 << 5);PORTB |= (1 << 5);PORTB |= 5;
Richtige Antwort: 4
Geg.: Bit 5 von PORTB setzen, übrige Bits unverändert
Ges.: Anweisung
Die ODER-Verknüpfung mit der Maske 0010\,0000_2 setzt Bit 5 und lässt alle anderen Bits unverändert (x \vee 0 = x): PORTB |= (1 << 5);, Antwort 4. Antwort 1 löscht alle anderen Bits. Antwort 2 löscht alle anderen Bits und lässt Bit 5 nur stehen, wenn es schon 1 war. Antwort 3 schaltet Bit 5 um, setzt es also nur, wenn es vorher 0 war. Antwort 5 setzt mit 5 = 0000\,0101_2 die Bits 0 und 2.
Aufgabe 2.2 (gebunden)
Welchen Wert hat y?
uint8_t x = 0b10110110;
uint8_t y = x & 0x0F;0xB60x060xB00x0B0xF6
Richtige Antwort: 2
Geg.: x = 0b10110110, Maske 0x0F
Ges.: y = x & 0x0F
| Bit 7 … 4 | Bit 3 … 0 | |
|---|---|---|
x |
1011 | 0110 |
0x0F |
0000 | 1111 |
x & 0x0F |
0000 | 0110 |
y = 0000\,0110_2 = \underline{\underline{\texttt{0x06}}}, Antwort 2. Die Maske behält das untere Halbbyte (Nibble). Antwort 1 ist x selbst, Antwort 3 das obere Halbbyte an seiner Stelle (x & 0xF0), Antwort 4 das obere Halbbyte nach rechts geschoben (x >> 4), Antwort 5 ist x | 0xF0.
Aufgabe 2.3 (ungebunden, 10 Punkte)
Ein Register enthält r = 0xA6. Gib jeweils das Ergebnis dual und hexadezimal an. Jede Teilaufgabe geht von r = 0xA6 aus.
r | 0x09(2 Punkte)r & ~0x06(2 Punkte)r ^ 0xFF(2 Punkte)r >> 4(2 Punkte)Prüfe, ob die Bedingung
(r & (1 << 2))erfüllt ist. (2 Punkte)
Geg.: r = 0xA6 = 1010\,0110_2
a) Ges.: r | 0x09
1010\,0110_2 \vee 0000\,1001_2 = \underline{\underline{1010\,1111_2 = \texttt{0xAF}}}
b) Ges.: r & ~0x06
~0x06 = 1111\,1001_2; 1010\,0110_2 \wedge 1111\,1001_2 = \underline{\underline{1010\,0000_2 = \texttt{0xA0}}} (Bits 1 und 2 gelöscht)
c) Ges.: r ^ 0xFF
1010\,0110_2 \oplus 1111\,1111_2 = \underline{\underline{0101\,1001_2 = \texttt{0x59}}} (alle Bits invertiert)
d) Ges.: r >> 4
Vier Stellen nach rechts, links werden Nullen nachgeschoben: \underline{\underline{0000\,1010_2 = \texttt{0x0A}}}
e) Ges.: Bedingung (r & (1 << 2))
1010\,0110_2 \wedge 0000\,0100_2 = 0000\,0100_2 \neq 0: Bit 2 ist gesetzt, die Bedingung ist \underline{\underline{\text{erfüllt}}}.
3 Port-Register
Grundlagen
Bei vielen 8-Bit-Mikrocontrollern hat jeder Port drei Register:
DDRx(Data Direction Register): Bit = 1 → Pin ist Ausgang, Bit = 0 → Pin ist Eingang.PORTx: Bei einem Ausgang bestimmt das Bit den Pegel. Bei einem Eingang schaltet ein 1-Bit den internen Pull-up-Widerstand ein.PINx: Lesen des aktuellen Pegels an den Pins.Nach dem Reset sind
DDRxundPORTx0: Alle Pins sind Eingänge ohne Pull-up.
Aufgabe 3.1 (gebunden)
Für Pin PD2 gilt: Bit 2 in DDRD ist 0, Bit 2 in PORTD ist 1. Wie arbeitet der Pin?
als Ausgang mit High-Pegel
als Ausgang mit Low-Pegel
als Eingang ohne Pull-up
als Analogeingang
als Eingang mit internem Pull-up
Richtige Antwort: 5
Geg.: DDRD Bit 2 = 0, PORTD Bit 2 = 1
Ges.: Arbeitsweise von PD2
DDRD = 0 macht den Pin zum Eingang; ein 1-Bit in PORTD schaltet bei einem Eingang den Pull-up ein: \underline{\underline{\text{Eingang mit internem Pull-up}}}, Antwort 5. Ausgänge (Antworten 1 und 2) hätten DDRD = 1. Ein Eingang ohne Pull-up (Antwort 3) hätte PORTD = 0. Der Analogeingang (Antwort 4) wird über die ADC-Register ausgewählt, nicht über diese Bits.
Aufgabe 3.2 (ungebunden, 8 Punkte)
An PB0 bis PB3 sind LEDs (high-aktiv) angeschlossen, an PD2 ein Taster gegen 0 V.
Schreibe die Anweisungen, die PB0 bis PB3 als Ausgänge einstellen, ohne die übrigen Pins von Port B zu verändern. (2 Punkte)
Schreibe die Anweisungen, die PD2 als Eingang mit Pull-up einstellen. (2 Punkte)
Schreibe eine Abfrage, die die LED an PB1 einschaltet, solange der Taster gedrückt ist, und sonst ausschaltet. (4 Punkte)
Geg.: LEDs an PB0 … PB3 (high-aktiv); Taster an PD2 gegen 0 V
a) Ges.: PB0 … PB3 als Ausgänge
DDRB |= (1 << PB0) | (1 << PB1) | (1 << PB2) | (1 << PB3); // oder: DDRB |= 0x0F;b) Ges.: PD2 als Eingang mit Pull-up
DDRD &= ~(1 << PD2); // Eingang
PORTD |= (1 << PD2); // Pull-up einc) Ges.: LED an PB1 bei gedrücktem Taster
Gedrückt verbindet der Taster PD2 mit 0 V; das Bit in PIND ist dann 0.
if (!(PIND & (1 << PD2))) { // Taster gedrückt (Low)
PORTB |= (1 << PB1); // LED an
} else {
PORTB &= ~(1 << PB1); // LED aus
}Die Abfrage steht in der Hauptschleife while (1).
4 Kontrollstrukturen
Grundlagen
| Struktur | C-Syntax | Hinweis |
|---|---|---|
| Verzweigung | if (b) { ... } else { ... } |
Vergleich mit ==, nicht = |
| Mehrfachauswahl | switch (x) { case 1: ...; break; default: ...; } |
ohne break läuft das Programm in den nächsten case weiter |
| Zählschleife | for (i = 0; i < n; i++) { ... } |
kopfgesteuert |
| kopfgesteuerte Schleife | while (b) { ... } |
läuft eventuell gar nicht |
| fußgesteuerte Schleife | do { ... } while (b); |
läuft mindestens einmal |
| Endlosschleife | while (1) { ... } |
Hauptschleife jedes Mikrocontrollerprogramms |
Ein Schreibtischtest verfolgt die Werte aller Variablen Schritt für Schritt in einer Tabelle.
Aufgabe 4.1 (gebunden)
Wie oft wird der Schleifenrumpf ausgeführt?
for (uint8_t i = 0; i < 10; i += 3) { ... }4-mal
3-mal
10-mal
9-mal
0-mal
Richtige Antwort: 1
Geg.: for (i = 0; i < 10; i += 3)
Ges.: Zahl der Durchläufe
i nimmt die Werte 0, 3, 6, 9 an; bei 12 ist die Bedingung falsch. Der Rumpf läuft \underline{\underline{4\text{-mal}}}: Antwort 1. Antwort 2 vergisst den Durchlauf mit 0, Antwort 3 übersieht die Schrittweite 3, Antwort 4 nimmt den letzten Wert von i als Anzahl. 0-mal (Antwort 5) gilt nur, wenn die Bedingung schon zu Beginn falsch ist.
Aufgabe 4.2 (gebunden)
Welchen Wert hat n nach der Schleife?
uint8_t x = 20, n = 0;
while (x > 0) {
x = x / 2;
n++;
}10
4
20
5
0
Richtige Antwort: 4
Geg.: x = 20, n = 0; x = x / 2 je Durchlauf
Ges.: n nach der Schleife
| Durchlauf | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
x danach |
10 | 5 | 2 | 1 | 0 |
n |
1 | 2 | 3 | 4 | 5 |
Nach dem 5. Durchlauf ist x = 0, die Schleife endet: \underline{\underline{n = 5}}, Antwort 4. Antwort 2 übersieht den letzten Schritt 1 / 2 = 0 (ganzzahlig). Antwort 1 und 3 verwechseln n mit Werten von x; 0 (Antwort 5) ist der Endwert von x, nicht von n.
Aufgabe 4.3 (ungebunden, 8 Punkte)
uint8_t a = 3, b = 0;
for (uint8_t i = 1; i <= 4; i++) {
if (i % 2 == 0) {
b = b + a;
} else {
a = a + i;
}
}Führe einen Schreibtischtest durch und gib
aundbnach der Schleife an. (5 Punkte)Im folgenden Ausschnitt fehlt ein
break. Gib an, welche Funktionen beimodus = 1aufgerufen werden. (3 Punkte)
switch (modus) {
case 1: led_gruen();
case 2: led_gelb();
break;
default: led_rot();
}Geg.: a = 3, b = 0; Schleife i = 1 … 4
a) Ges.: Schreibtischtest; a und b am Ende
i |
i % 2 == 0? |
Anweisung | a |
b |
|---|---|---|---|---|
| Start | – | – | 3 | 0 |
| 1 | nein | a = 3 + 1 |
4 | 0 |
| 2 | ja | b = 0 + 4 |
4 | 4 |
| 3 | nein | a = 4 + 3 |
7 | 4 |
| 4 | ja | b = 4 + 7 |
7 | 11 |
Ergebnis: \underline{\underline{a = 7,\ b = 11}}.
b) Ges.: aufgerufene Funktionen bei modus = 1
Ohne break nach case 1 läuft das Programm in case 2 weiter, bis zum nächsten break. Es werden \underline{\underline{\texttt{led\_gruen()} \text{ und } \texttt{led\_gelb()}}} aufgerufen, led_rot() nicht.
5 Programmablaufplan und Struktogramm
Grundlagen
Der Programmablaufplan (PAP, DIN 66001) zeigt den Ablauf mit Sinnbildern: abgerundetes Rechteck für Start und Ende, Rechteck für Verarbeitung, Parallelogramm für Ein- und Ausgabe, Raute für Verzweigung; die Pfeile geben die Richtung an.
Das Struktogramm (Nassi-Shneiderman, DIN 66261) stellt nur strukturierte Abläufe dar: Folge, Verzweigung (Dreieck mit ja/nein), Schleifen. Bei einer kopf- oder zählgesteuerten Schleife steht die Bedingung oben, der Rumpf ist eingerückt; bei einer fußgesteuerten Schleife steht sie unten.
Eine Hysterese (zwei verschiedene Schaltschwellen) verhindert, dass ein Ausgang an einer Schwelle ständig ein- und ausschaltet.
Aufgabe 5.1 (gebunden)
Welche Schleifenart zeigt das Struktogramm zur Mittelwertbildung?
fußgesteuerte Schleife
Zählschleife (kopfgesteuert)
Endlosschleife
Verzweigung
Mehrfachauswahl
Richtige Antwort: 2
Geg.: Struktogramm: Bedingung „für i von 0 bis 7“ oben, Rumpf eingerückt
Ges.: Schleifenart
Die Bedingung steht oben und gibt eine feste Zahl von Durchläufen vor: \underline{\underline{\text{Zählschleife}}} (kopfgesteuert), Antwort 2. Bei einer fußgesteuerten Schleife (Antwort 1) stünde die Bedingung unten. Eine Endlosschleife (Antwort 3) hat keine Abbruchbedingung. Verzweigung (Antwort 4) und Mehrfachauswahl (Antwort 5) werden mit Dreieck bzw. Spalten dargestellt.
Aufgabe 5.2 (ungebunden, 10 Punkte)
Verwende den Programmablaufplan der Pumpensteuerung. Es stehen die Funktionen fuellstand() (liefert uint16_t), pumpe_ein(), pumpe_aus() und _delay_ms(ms) zur Verfügung.
Setze den Ablauf in ein C-Programm um. (5 Punkte)
Gib an, was bei einem Füllstand von 500 geschieht, und erkläre den Zweck der beiden Schwellen. (3 Punkte)
Begründe, warum die Rücksprunglinie oberhalb von „Füllstand lesen“ und nicht vor „Pumpe aus“ endet. (2 Punkte)
Geg.: PAP der Pumpensteuerung; Schwellen 200 und 800; Funktionen fuellstand(), pumpe_ein(), pumpe_aus(), _delay_ms()
a) Ges.: C-Programm
int main(void) {
uint16_t f;
pumpe_aus(); // Initialisierung
while (1) {
f = fuellstand();
if (f < 200) {
pumpe_ein();
} else if (f > 800) {
pumpe_aus();
}
_delay_ms(100);
}
}b) Ges.: Verhalten bei 500; Zweck der Schwellen
Bei 500 ist keine Bedingung erfüllt; die Pumpe \underline{\underline{\text{behält ihren Zustand}}}. Läuft sie, füllt sie weiter bis über 800; steht sie, bleibt sie aus, bis der Füllstand unter 200 fällt.
Die beiden Schwellen bilden eine Hysterese. Mit nur einer Schwelle würde die Pumpe bei kleinen Schwankungen des Füllstands ständig ein- und ausschalten; das belastet Motor und Schaltelemente.
c) Ges.: Begründung Rücksprung
„Pumpe aus“ ist die Initialisierung beim Start. Würde der Rücksprung davor enden, schaltete das Programm die Pumpe in jedem Durchlauf aus; die Hysterese ginge verloren und die Pumpe würde nie dauerhaft laufen. Der Rücksprung bildet deshalb die \underline{\underline{\text{Endlosschleife ohne Initialisierung}}}.
Aufgabe 5.3 (ungebunden, 6 Punkte)
Verwende das Struktogramm zur Mittelwertbildung. Die Funktion uint16_t adc_lesen(uint8_t kanal) liefert 0 … 1023, die Funktion ausgeben() zeigt einen Wert an.
Setze das Struktogramm in C um. (3 Punkte)
Wähle einen geeigneten Datentyp für
summeund begründe die Wahl. (2 Punkte)Ersetze die Division durch eine Schiebeoperation. (1 Punkt)
Geg.: Struktogramm Mittelwert aus 8 Messwerten; adc_lesen() liefert 0 … 1023
a) Ges.: C-Code
uint16_t summe = 0;
uint16_t mittel;
for (uint8_t i = 0; i <= 7; i++) {
uint16_t wert = adc_lesen(0);
summe = summe + wert;
}
mittel = summe / 8;
ausgeben(mittel);b) Ges.: Datentyp für summe
\text{summe}_\mathrm{max} = 8 \cdot 1023 = 8184
\underline{\underline{\texttt{uint16\_t}}} (bis 65 535) reicht; uint8_t (bis 255) wäre zu klein, uint32_t verschwendet Speicher und Rechenzeit.
c) Ges.: Division durch Schieben
8 = 2^3, also drei Stellen nach rechts schieben:
mittel = summe >> 3;6 Funktionen, Interrupts und volatile
Grundlagen
Eine Funktion kapselt eine Teilaufgabe, erhält Parameter und gibt einen Wert zurück (
uint16_t messen(uint8_t kanal));voidbedeutet „kein Rückgabewert“.Lokale Variablen gelten nur in ihrer Funktion; globale Variablen im ganzen Programm.
Eine Interrupt-Service-Routine (ISR) wird bei einem Ereignis automatisch aufgerufen. Sie soll kurz sein (keine Wartezeiten, keine langen Berechnungen).
Variablen, die in einer ISR und im Hauptprogramm verwendet werden, werden als
volatiledeklariert. Der Compiler liest sie dann bei jedem Zugriff aus dem Speicher, statt einen in einem Register zwischengespeicherten Wert zu verwenden.Auf einem 8-Bit-Mikrocontroller wird eine 16-Bit-Variable in zwei Schritten gelesen. Ändert eine ISR sie dazwischen, entsteht ein falscher Wert. Abhilfe: Interrupts während des Zugriffs kurz sperren (
cli(), danachsei()).
Aufgabe 6.1 (gebunden)
Warum wird eine globale Variable, die in einer ISR verändert wird, als volatile deklariert?
Damit sie im EEPROM gespeichert wird.
Damit sie nach dem Abschalten erhalten bleibt.
Damit der Compiler sie bei jedem Zugriff neu aus dem Speicher liest.
Damit die ISR schneller ausgeführt wird.
Damit die Variable nur in der ISR verwendet werden kann.
Richtige Antwort: 3
Geg.: globale Variable, in ISR verändert
Ges.: Grund für volatile
volatile zwingt den Compiler, die Variable \underline{\underline{\text{bei jedem Zugriff neu aus dem Speicher zu lesen}}}. Sonst könnte er sie im Hauptprogramm in einem Register zwischenspeichern und Änderungen der ISR nicht bemerken: Antwort 3. EEPROM (Antwort 1) und Erhalt nach dem Abschalten (Antwort 2) erfordern eigene Schreibbefehle. Die ISR wird nicht schneller (Antwort 4), eher minimal langsamer. Die Sichtbarkeit (Antwort 5) hängt nicht von volatile ab.
Aufgabe 6.2 (ungebunden, 10 Punkte)
Ein Programm misst eine Frequenz. Jede steigende Flanke an INT0 löst einen Interrupt aus.
volatile uint16_t impulse = 0;
ISR(INT0_vect) {
impulse++;
}
int main(void) {
uint16_t frequenz;
/* Initialisierung von INT0 ... */
sei();
while (1) {
_delay_ms(1000);
frequenz = impulse;
impulse = 0;
anzeigen(frequenz);
}
}Nach einer Torzeit von 1 s wurden 437 Impulse gezählt. Gib die Frequenz an und berechne die höchste Frequenz, die die Variable ohne Überlauf erfassen kann. (3 Punkte)
Erkläre, warum in der ISR kein
_delay_ms()stehen darf. (2 Punkte)Beschreibe den Fehler, der beim Kopieren von
impulseauftreten kann, und ergänze das Programm so, dass er nicht auftritt. (3 Punkte)Nenne einen Grund, warum die Torzeit mit
_delay_ms()ungenau ist. (2 Punkte)
Geg.: Torzeit t_\mathrm{T} = 1\ \text{s}; gezählt N = 437; Zähler uint16_t
a) Ges.: f, f_\mathrm{max}
f = \frac{N}{t_\mathrm{T}} = \frac{437}{1\ \text{s}} = \underline{\underline{437\ \text{Hz}}} \qquad f_\mathrm{max} = \frac{N_\mathrm{max}}{t_\mathrm{T}} = \frac{2^{16} - 1}{1\ \text{s}} = \underline{\underline{65\,535\ \text{Hz}}}
b) Ges.: Begründung kein _delay_ms() in der ISR
Während die ISR läuft, ist das Hauptprogramm angehalten, und weitere Interrupts sind gesperrt. Eine Wartezeit würde \underline{\underline{\text{Impulse verloren gehen lassen}}} und das ganze System blockieren. ISR sollen deshalb nur kurz Werte übernehmen oder Merker setzen.
c) Ges.: Fehler beim Kopieren; Korrektur
impulse hat 16 Bit; ein 8-Bit-Mikrocontroller liest es in zwei Schritten. Kommt zwischen den beiden Bytes ein Interrupt (z. B. Übergang von 255 auf 256), entsteht ein falscher Wert. Außerdem gehen Impulse verloren, die zwischen dem Kopieren und dem Nullsetzen eintreffen. Abhilfe: \underline{\underline{\text{Interrupts kurz sperren}}}.
cli(); // Interrupts sperren
frequenz = impulse;
impulse = 0;
sei(); // Interrupts freigebenAlternativ: ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ... } aus <util/atomic.h>.
d) Ges.: Grund für die ungenaue Torzeit
Zu den 1000 ms kommt die Laufzeit der übrigen Befehle in der Schleife (vor allem anzeigen()) hinzu, und die ISR verlängert die Wartezeit von _delay_ms(), weil sie die Warteschleife unterbricht. Die tatsächliche Torzeit ist \underline{\underline{\text{länger als 1 s}}}. Genauer ist eine Torzeit aus einem Timer-Interrupt oder die Zählung mit einem Hardware-Zähler.
7 Zeitsteuerung und Zustandsautomat
Grundlagen
_delay_ms()blockiert: Während der Wartezeit kann das Programm nichts anderes tun (z. B. keine Taster abfragen).Nicht blockierend arbeitet man mit einem Zeitzähler, den ein Timer-Interrupt hochzählt (z. B. jede Millisekunde). Das Hauptprogramm prüft nur, ob seit dem letzten Ereignis genug Zeit vergangen ist.
Ein Zustandsautomat beschreibt ein System durch Zustände und Übergänge. Im Programm speichert eine Variable den aktuellen Zustand; eine
switch-Anweisung prüft in jedem Durchlauf der Hauptschleife die Übergangsbedingungen des aktuellen Zustands.
Aufgabe 7.1 (gebunden)
Warum sollte in der Hauptschleife einer Torsteuerung kein _delay_ms(500) verwendet werden?
Weil
_delay_ms()nur in Interrupts erlaubt ist.Weil die Funktion den Timer zerstört.
Weil 500 ms nicht in einen
uint8_tpassen.Weil der Watchdog dadurch abgeschaltet wird.
Weil das Programm während der Wartezeit nicht auf Endschalter oder Lichtschranke reagiert.
Richtige Antwort: 5
Geg.: _delay_ms(500) in der Hauptschleife einer Torsteuerung
Ges.: Grund für den Verzicht
Während der Wartezeit \underline{\underline{\text{reagiert das Programm nicht}}} auf Endschalter oder Lichtschranke. Das Tor könnte bis zu 500 ms über die Endlage hinaus fahren oder ein Hindernis zu spät erkennen: Antwort 5. _delay_ms() ist gerade in Interrupts zu vermeiden (Antwort 1). Die Funktion verwendet eine Warteschleife und verändert keinen Timer (Antwort 2). Der Parameter ist kein uint8_t (Antwort 3). Der Watchdog wird nicht abgeschaltet (Antwort 4), er kann aber ablaufen, wenn lange gewartet wird.
Aufgabe 7.2 (ungebunden, 10 Punkte)
Die Torsteuerung wird nach dem Zustandsdiagramm programmiert. Es gibt die Funktionen taster(), end_oben(), end_unten(), lichtschranke() (liefern 1, wenn betätigt bzw. unterbrochen) sowie motor_auf(), motor_zu(), motor_aus().
enum zustand { ZU, OEFFNET, OFFEN, SCHLIESST };
enum zustand z = ZU;
while (1) {
switch (z) {
case ZU:
if (taster()) { motor_auf(); z = OEFFNET; }
break;
case OEFFNET:
if (end_oben()) { motor_aus(); z = OFFEN; }
break;
case OFFEN:
if (taster()) { motor_zu(); z = SCHLIESST; }
break;
case SCHLIESST:
/* ergänzen */
break;
}
}Gib alle Übergänge aus dem Zustand SCHLIESST mit Bedingung und Folgezustand an. (2 Punkte)
Ergänze den Programmteil für
case SCHLIESST. Die Lichtschranke soll Vorrang vor dem Endschalter haben. (5 Punkte)Ein Tastendruck dauert einige hundert Millisekunden, die Hauptschleife läuft in wenigen Mikrosekunden durch. Beschreibe das Problem beim Übergang von OFFEN nach SCHLIESST und eine Lösung. (3 Punkte)
Geg.: Zustandsdiagramm; Funktionen taster(), end_oben(), end_unten(), lichtschranke(), motor_auf(), motor_zu(), motor_aus()
a) Ges.: Übergänge aus SCHLIESST
| Bedingung | Folgezustand |
|---|---|
| LS oder T | \underline{\underline{\text{ÖFFNET}}} (Motor auf) |
| EU | \underline{\underline{\text{ZU}}} (Motor aus) |
b) Ges.: Programmteil case SCHLIESST
case SCHLIESST:
if (lichtschranke() || taster()) { // Hindernis oder Taster: Vorrang
motor_auf();
z = OEFFNET;
} else if (end_unten()) {
motor_aus();
z = ZU;
}
break;Die Lichtschranke wird zuerst geprüft; ist sie unterbrochen, öffnet das Tor auch dann, wenn gleichzeitig der Endschalter meldet.
c) Ges.: Problem beim Übergang OFFEN → SCHLIESST; Lösung
Beim Tastendruck wechselt der Automat nach SCHLIESST. Der Taster ist im nächsten Schleifendurchlauf (wenige Mikrosekunden später) noch gedrückt; in SCHLIESST gilt T als Bedingung zum Öffnen. Das Tor \underline{\underline{\text{schaltet sofort wieder auf Öffnen}}}. In ÖFFNET ist der obere Endschalter noch betätigt, es folgt sofort OFFEN, dann über T wieder SCHLIESST usw.: Der Automat pendelt, solange der Taster gedrückt ist.
Lösung: Nur die Flanke des Tasters auswerten (Wechsel von „nicht gedrückt“ auf „gedrückt“, mit einem Merker für den vorherigen Zustand) und den Taster entprellen.
8 Fehlersuche
Grundlagen
Typische Fehler in kurzen C-Programmen:
Zuweisung
=statt Vergleich==in einer Bedingung,falsche Maske beim Löschen (
&= (1 << n)statt&= ~(1 << n)),Semikolon direkt hinter
if (...),for (...)oderwhile (...),fehlendes
breakinswitch,zu kleiner Datentyp (Überlauf),
Lesen von
PORTxstattPINxbei einem Eingang.
Aufgabe 8.1 (ungebunden, 10 Punkte)
Das folgende Programm soll die LED an PB3 einschalten, solange der Taster an PD2 (gegen 0 V, interner Pull-up) gedrückt ist. Eine Variable zählt die Schleifendurchläufe mit gedrücktem Taster bis 300 und beginnt dann wieder bei 0. Das Programm enthält fünf Fehler.
int main(void) {
uint8_t zaehler = 0; // Zeile 2
DDRB |= (1 << PB3); // Zeile 3
PORTD |= (1 << PD2); // Zeile 4
while (1) {
if (PORTD & (1 << PD2)) ; // Zeile 6: nicht gedrückt?
{
PORTB &= (1 << PB3); // Zeile 8: LED aus
} else {
PORTB |= (1 << PB3); // Zeile 10: LED an
zaehler++; // Zeile 11
}
if (zaehler = 300) { // Zeile 13
zaehler = 0;
}
}
}Gib zu jedem Fehler die Zeile an, beschreibe den Fehler und korrigiere ihn; je Fehler gibt es 2 Punkte. (10 Punkte)
Geg.: Programm mit fünf Fehlern; LED an PB3, Taster an PD2 gegen 0 V mit Pull-up; Zähler bis 300
Ges.: Fehler, Beschreibung, Korrektur
| Zeile | Fehler | Korrektur |
|---|---|---|
| 2 | uint8_t reicht nur bis 255; der Wert 300 wird nie erreicht. |
uint16_t zaehler = 0; |
| 6 | Ein Eingang wird über PIND gelesen; PORTD liefert nur den Pull-up-Zustand. |
if (PIND & (1 << PD2)) |
| 6 | Das Semikolon beendet die if-Anweisung; der Block danach gehört nicht dazu, das else hat kein if (Übersetzungsfehler). |
Semikolon entfernen |
| 8 | &= (1 << PB3) löscht alle anderen Bits und lässt PB3 unverändert. |
PORTB &= ~(1 << PB3); |
| 13 | = ist eine Zuweisung; die Bedingung ist immer wahr, der Zähler wird ständig auf 0 gesetzt. |
if (zaehler == 300) |
Korrigiertes Programm:
int main(void) {
uint16_t zaehler = 0;
DDRB |= (1 << PB3);
PORTD |= (1 << PD2);
while (1) {
if (PIND & (1 << PD2)) { // nicht gedrückt
PORTB &= ~(1 << PB3); // LED aus
} else {
PORTB |= (1 << PB3); // LED an
zaehler++;
}
if (zaehler == 300) {
zaehler = 0;
}
}
}Leitfragen
- Welcher Datentyp passt zu welchem Wertebereich, und wann entsteht ein Überlauf?
uint8_t reicht von 0 bis 255, int8_t von −128 bis 127, uint16_t bis 65 535, int16_t von −32 768 bis 32 767 und uint32_t bis etwa 4,29 Milliarden – ein 10-Bit-ADC-Wert braucht also uint16_t. Ein Überlauf entsteht, wenn ein Ergebnis nicht in den Typ passt; vorzeichenlose Typen zählen dann modulo 2^n weiter (250 + 10 ergibt in uint8_t 4), und auch Zwischenergebnisse im 16-Bit-int können überlaufen, was ein Cast wie (uint32_t)a * b verhindert – siehe Abschnitt „1 Datentypen und Zahlenbereiche“ und Aufgabe 1.3.
- Wie setzt, löscht, schaltet und prüft man ein einzelnes Bit?
Du arbeitest mit der Maske (1 << n): Setzen mit reg |= (1 << n);, Löschen mit reg &= ~(1 << n);, Umschalten mit reg ^= (1 << n); und Abfragen mit if (reg & (1 << n)). So bleiben alle anderen Bits unverändert – siehe Abschnitt „2 Bitoperationen“ und Aufgabe 2.1.
- Welche Aufgabe haben die Register DDRx, PORTx und PINx?
DDRx legt die Richtung fest (Bit 1 = Ausgang, Bit 0 = Eingang). PORTx bestimmt bei einem Ausgang den Pegel und schaltet bei einem Eingang mit einem 1-Bit den internen Pull-up ein; über PINx liest Du den aktuellen Pegel an den Pins. Nach dem Reset sind alle Pins Eingänge ohne Pull-up – siehe Abschnitt „3 Port-Register“ und Aufgabe 3.1.
- Wie führt man einen Schreibtischtest durch?
Beim Schreibtischtest verfolgst Du die Werte aller Variablen Schritt für Schritt in einer Tabelle, mit einer Zeile je Schleifendurchlauf und Spalten für Bedingung, ausgeführte Anweisung und Variablenwerte. So erkennst Du etwa, dass die Schleife mit x = x / 2 ab 20 genau fünfmal läuft – siehe Abschnitt „4 Kontrollstrukturen“ und Aufgabe 4.3.
- Wie liest man einen Programmablaufplan oder ein Struktogramm und setzt es in C um?
Im PAP nach DIN 66001 stehen abgerundete Rechtecke für Start/Ende, Rechtecke für Verarbeitung, Parallelogramme für Ein-/Ausgabe und Rauten für Verzweigungen; im Struktogramm nach DIN 66261 steht die Schleifenbedingung oben (kopf- oder zählgesteuert) oder unten (fußgesteuert) und Verzweigungen als Dreieck. Du übersetzt die Elemente in if/else, for, while und die Endlosschleife while (1) und achtest darauf, wohin Rücksprünge führen (z. B. nicht vor die Initialisierung) – siehe Abschnitt „5 Programmablaufplan und Struktogramm“ und Aufgabe 5.2.
- Warum muss eine Variable, die eine ISR verändert,
volatilesein, und was bedeutet atomarer Zugriff?
Mit volatile liest der Compiler die Variable bei jedem Zugriff neu aus dem Speicher, statt einen im Register zwischengespeicherten Wert zu verwenden, der die Änderungen der ISR nicht kennt. Atomar heißt, dass ein Zugriff nicht unterbrochen werden kann: Eine 16-Bit-Variable wird auf einem 8-Bit-Mikrocontroller in zwei Schritten gelesen, deshalb sperrst Du währenddessen kurz die Interrupts mit cli() und sei() (oder ATOMIC_BLOCK) – siehe Abschnitt „6 Funktionen, Interrupts und volatile“ und Aufgabe 6.2.
- Warum blockiert
_delay_ms(), und wie programmiert man einen Zustandsautomaten?
_delay_ms() ist eine Warteschleife, während der das Programm nichts anderes tun kann, also z. B. nicht auf Endschalter oder Lichtschranke reagiert; nicht blockierend arbeitest Du mit einem Zeitzähler aus einem Timer-Interrupt. Für einen Zustandsautomaten speichert eine Variable (z. B. ein enum) den aktuellen Zustand, und eine switch-Anweisung prüft in jedem Durchlauf der Hauptschleife die Übergangsbedingungen dieses Zustands – siehe Abschnitt „7 Zeitsteuerung und Zustandsautomat“ und Aufgabe 7.2.
- Welche typischen Fehler kommen in C-Programmen vor?
Typisch sind = statt == in Bedingungen, die falsche Löschmaske &= (1 << n) statt &= ~(1 << n), ein Semikolon direkt hinter if, for oder while, ein fehlendes break im switch, ein zu kleiner Datentyp und das Lesen von PORTx statt PINx bei einem Eingang – siehe Abschnitt „8 Fehlersuche“ und Aufgabe 8.1.
Links und weitere Informationen
Hinweis zur Nachnutzung als Open Educational Resource (OER)
Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Mikrocontroller-Programmierung: Wiederholung und Übungsaufgaben” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/mikrocontroller-programmierung-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/mcu/arduino-esp. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 13.11.2026.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]
