Übersicht
Projektarchitektur · Hardware-Belegung · Datenfluss
Das Projekt ist ein joystickgesteuerter Wellenform- und Ton-Generator auf einem ATmega32 (8 MHz Takt). Mit dem Joystick stellst du Tonhöhe, Lautstärke und Wellenform ein, siehst die Wellenform live auf einem OLED-Display (SH1106) und hörst den passenden Ton über zwei Lautsprecher.
| Bauteil | ATmega32-Pin | Funktion |
|---|---|---|
| Joystick X-Achse | PA0 / ADC0 | analoge Spannung 0–5 V |
| Joystick Y-Achse | PA1 / ADC1 | analoge Spannung 0–5 V |
| Joystick-Taster (SW) | PA2 | digital, Low-aktiv (interner Pull-up) |
| OLED RES | PB3 | Reset des Display-Controllers |
| OLED D/C | PB4 | Data/Command-Umschaltung |
| OLED DIN (MOSI) | PB5 | SPI-Daten |
| OLED CLK (SCK) | PB7 | SPI-Takt |
| OLED CS | GND | fest auf Masse, kein eigener Pin |
| Lautsprecher 1 | PD5 / OC1A | Timer1-Ausgang (“Master”) |
| Lautsprecher 2 | PD4 / OC1B | Timer1-Ausgang (parallel, “Mirror”) |
| USART TX | — | Debug-Ausgabe 9600 Baud, 8N1 |
frequency[Frequenz][Amplitude][Wellenform]
ist ungleich 0 („One-Hot“).
Ihr Wert ist die Tonhöhe in 1/10 Hz
(z. B. 4400 = 440,0 Hz).
Die drei Array-Indizes codieren
Tonhöhen-Slot (0–6),
Lautstärke (0–10) und
Wellenform (0 = Sinus, 1 = Rechteck, 2 = Sägezahn).
main.c — Programmstart & Hauptschleife
Einstiegspunkt · Setup-Aufrufe · 100-ms-Loop · Änderungserkennung
Wozu dient tmpcontroller (Startwert −1) und was bewirkt die Änderungserkennung?
controller-Wert davon abweicht, werden update_display/update_audio aufgerufen — sonst nur 100 ms warten. −1 ist kein gültiger Wert (gültig 0–5) und erzwingt so das erste Update.Warum wird update_display vor update_audio aufgerufen?
frequency-Array: Display schreibt den neuen Zustand hinein, Audio liest ihn. Umgekehrt spielte der Ton den alten Zustand.Bekommen die Module das frequency-Array als Kopie oder als Original?
update_display ändert damit direkt das Array aus main(), es wird nichts kopiert.Diese Datei ist der Dirigent des Projekts. Sie schreibt selbst fast keine Hardware an, sondern ruft die drei Module in der richtigen Reihenfolge auf und sorgt mit einer Endlosschleife dafür, dass das System dauerhaft auf den Joystick reagiert.
Hardware-Kontext: ATmega32, 8 MHz. Ziel: Du sollst nicht nur wissen dass die Schleife läuft, sondern warum sie genau so aufgebaut ist — besonders die Änderungserkennung.
1. Die Grundidee in einem Satz
main.c ist Drumherum (Setup + eine Debug-Ausgabe).
2. Includes und globale Variablen (Kopf der Datei)
#define DELAY 100
int tmpcontroller = -1;
int frequency[AUDIO_NUM_FREQ][AUDIO_NUM_AMP][AUDIO_NUM_WAVE];| Element | Bedeutung |
|---|---|
DELAY 100 | Die Schleife wartet am Ende 100 ms. Das ergibt eine Abtastrate von ca. 10×/Sekunde — schnell genug, dass sich der Joystick „sofort" anfühlt, langsam genug, dass das Display nicht flackert. |
tmpcontroller = -1 | Merkt sich die zuletzt erkannte Joystick-Richtung. Startwert −1, weil controller_status() nur Werte 0–5 liefert. So ist die allererste Messung garantiert „anders" → das System initialisiert sich im ersten Durchlauf selbst. |
frequency[7][11][3] | Das gemeinsame Daten-Array (global, daher automatisch mit 0 vorbelegt). Es ist die „Nachricht" von Display an Audio. Genau eine Zelle ist ≠ 0 (One-Hot); ihr Wert ist die Tonhöhe in 1/10 Hz. |
update_display() (schreibt) als auch update_audio() (liest) brauchen dasselbe Array. Es einmal global anzulegen ist der einfachste Weg, es zwischen den Modulen zu teilen.
3. main() — Setup-Phase
int main() {
setup_audio();
setup_controller();
setup_display();
...Drei einmalige Initialisierungen, jede in ihrem eigenen Modul:
1. setup_audio() — Lautsprecher-Pins als Ausgang, Timer1 für die Tonerzeugung vorbereiten.
2. setup_controller() — USART (Debug), ADC im Free-Running-Modus, Joystick-Taster, und sei() (globale Interrupts ein). Ab hier laufen die ADC- und USART-Interrupts.
3. setup_display() — SPI + OLED initialisieren und das Startbild zeichnen.
setup_controller() per sei() freigegeben. Ein zusätzlicher usartSetup()-Aufruf in main() wäre überflüssig — USART wird bereits in setup_controller() eingerichtet. Die Reihenfolge der drei Setups ist unkritisch, solange alle vor der Schleife laufen.
4. Die Hauptschleife while(1) — Schritt für Schritt
4.1 Diagnose-Werte lesen (Debug)
int8_t x = adcLastRead(0); // X-Achse, skaliert [-16, +16]
int8_t y = adcLastRead(1); // Y-Achse, skaliert [-16, +16]
int8_t z = (PINA & (1 << 2)) ? 0 : 1; // Taster: PA2, Low-aktiv
int controller = controller_status(); // fertige Richtung 0–5x, y, z werden hier nur für die Debug-Ausgabe roh ausgelesen. Die eigentliche Auswertung steckt in controller_status(), das daraus eine einzelne Zahl macht:
0 = Mitte · 1 = Oben · 2 = Unten · 3 = Links · 4 = Rechts · 5 = Taster
z = (PINA & (1<<2)) ? 0 : 1 ist Low-aktiv: Ist Bit 2 (PA2) gesetzt (5 V, Pull-up) → Taster nicht gedrückt → z=0. Liegt PA2 auf 0 V (gedrückt) → z=1.
4.2 Debug-Zeile über die serielle Schnittstelle senden
char buf[40];
snprintf(buf, sizeof(buf), "x=%d y=%d z=%d s=%d\r\n", x, y, z, controller);
usartWriteString(buf);snprintf baut sicher (mit Längenbegrenzung) einen String wie x=3 y=-12 z=0 s=2 und schickt ihn per USART (9600 Baud) an den PC. Das ist reines Werkzeug zum Fehlersuchen.
usartWriteString() einfach 0 zurück und die Zeile wird verworfen. Für Debug-Zwecke ist das egal — main() prüft den Rückgabewert bewusst nicht.
4.3 Das Herzstück: Änderungserkennung (tmpcontroller)
if (controller == tmpcontroller) {
_delay_ms(DELAY);
continue; // nichts hat sich geändert -> nichts tun
} else {
tmpcontroller = controller;
update_display(controller, frequency); // erst Display + Array
update_audio(frequency); // dann Ton
}
_delay_ms(DELAY);Das ist die wichtigste Stelle der Datei. Sie vergleicht die aktuelle Richtung mit der vom letzten Durchlauf:
— Gleich (controller == tmpcontroller) → continue: 100 ms warten und von vorne. Display und Audio werden nicht angefasst.
— Verschieden → neue Richtung merken, dann update_display(...) und update_audio(...).
update_display() schreibt in frequency (es baut die eine aktive Zelle). update_audio() liest frequency und stellt den Timer ein. Würde man die Reihenfolge tauschen, spielte der Ton noch den alten Zustand. Display → Audio ist also Pflicht.
frequency ohne & übergeben? In C „zerfällt" ein Array beim Funktionsaufruf zu einem Zeiger auf sein erstes Element. update_display(controller, frequency) übergibt also die Adresse des globalen Arrays — die Funktion verändert wirklich das Original in main.c, keine Kopie.
5. Die zwei Ebenen der Flankenerkennung (Prüfungsklassiker!)
Es gibt im Projekt zwei unabhängige Mechanismen, die verhindern, dass ein gehaltener Joystick endlos weiterzählt.
| Ebene | Wo | Was sie tut |
|---|---|---|
tmpcontroller | main.c | Tor vor den Aufrufen. update_display()/update_audio() laufen nur, wenn sich die Richtung gegenüber dem letzten Tick geändert hat. Ein konstant gehaltener Joystick löst also genau eine Aktion aus. |
last_input | display.c | Schutz im Modul selbst. Innerhalb von update_display() wird ein Schritt nur ausgeführt, wenn die Eingabe nicht der zuletzt verarbeiteten entspricht. So bleibt das Display korrekt, egal wie oft update_display() aufgerufen wird. |
tmpcontroller das primäre Tor. last_input ist die zweite Sicherung: Sie macht das Display-Modul für sich allein robust. Würde man das main-Tor entfernen, sorgt last_input weiterhin dafür, dass eine Auslenkung = ein Schritt ist. Zwei voneinander unabhängige Schichten → doppelt abgesichert.
update_display(1) läuft, Lautstärke +1. Tick 2: Richtung ist immer noch 1 → tmpcontroller-Tor blockt, nichts passiert. Erst Loslassen (→0) und erneut Hochdrücken (→1) zählt wieder. So wird aus „Dauerdruck" ein sauberer Einzelschritt.
6. Ablauf als Bild
┌─────────────────────────────────────────────────────────────┐
│ setup_audio() setup_controller() setup_display() │ einmalig
└─────────────────────────────────────────────────────────────┘
│
┌───────────▼───────────┐
│ while(1) (alle 100ms) │
└───────────┬───────────┘
▼
controller = controller_status() (0–5)
│
Debug: "x=.. y=.. z=.. s=.." → USART
│
controller == tmpcontroller ?
┌──── ja ──┴── nein ────┐
▼ ▼
100 ms warten tmpcontroller = controller
(nichts tun) update_display(controller, frequency)
│ update_audio(frequency)
└───────────┬───────────────────────┘
▼
100 ms warten → zurück nach oben
7. Typische Prüfungsfragen (Selbsttest)
Wofür steht tmpcontroller und warum Startwert −1?
Warum wird update_display vor update_audio aufgerufen?
frequency-Array, Audio liest es. Sonst spielt der Ton den alten Zustand.Wieso verändert update_display das Array aus main wirklich (keine Kopie)?
Was macht die snprintf/usartWriteString-Zeile?
Warum zählt ein gehaltener Joystick nicht endlos hoch?
tmpcontroller (main) ruft nur bei Änderung auf; last_input (display) sichert das Modul zusätzlich ab.Wie schnell wird abgetastet?
_delay_ms(100)).controller.c — Joystick & Taster
ADC-Auswertung · Richtungserkennung · Schwellwert-Logik
Wie wird aus X/Y genau eine Richtung bestimmt — wozu die Schwellen ±10 und ±6?
Warum ist der Taster „low-aktiv" und wozu PORTA |= Bit(2)?
DDRA &= ~Bit(2)) mit internem Pull-up (PORTA |= Bit(2)), liegt also im Ruhezustand auf high. Drücken verbindet ihn mit GND → 0; der Code wertet das als „gedrückt" und liefert 5 (Vorrang vor jeder Richtung).Was bedeuten die Rückgabewerte und wozu sei() in setup_controller?
sei() gibt die globalen Interrupts frei — nötig, weil ADC (Free-Running) und USART per Interrupt arbeiten.Diese Datei erklärt den Teil des Projekts, der den Joystick und den Taster ausliest:
src/controller.c, include/controller.h sowie die ADC-Hilfsfunktionen in src/adc.c / include/adc.h.
Was macht das Controller-Modul?
Der Joystick gibt zwei analoge Spannungen aus (X-Achse und Y-Achse) und hat zusätzlich einen digitalen Taster (Z-Achse, „drücken"). Das Modul liest diese drei Eingaben aus und liefert einen einzigen Zahlenwert (0–5), der angibt, was der Benutzer gerade tut:
| Rückgabewert | Bedeutung |
|---|---|
0 | Mitte / keine Bewegung |
1 | Joystick nach oben |
2 | Joystick nach unten |
3 | Joystick nach links |
4 | Joystick nach rechts |
5 | Taster gedrückt |
Hardware-Anschlüsse
Joystick-Pin → ATmega32-Pin Funktion ───────────────────────────────────────── VRx (X-Achse) → PA0 / ADC0 Analoge Spannung 0–5 V VRy (Y-Achse) → PA1 / ADC1 Analoge Spannung 0–5 V SW (Taster) → PA2 Digitaler Eingang (Low-aktiv)
ADC — Was ist das und wie funktioniert Free-Running?
Der ADC (Analog-Digital-Converter) wandelt eine Spannung (analog, 0–5 V) in eine Zahl um. Beim ATmega32 liefert er Werte von 0 bis 1023 (10 Bit):
0 V → 0 2,5 V → 511 5 V → 1023
Im Free-Running-Modus läuft der ADC kontinuierlich: Er wandelt Kanal 0 (X), dann Kanal 1 (Y),
dann wieder Kanal 0, usw. – automatisch und im Hintergrund. Sobald eine Wandlung fertig ist,
springt der Prozessor kurz in die ISR (ISR(ADC_vect) in adc.c), speichert den Wert,
und kehrt dann zum Hauptprogramm zurück.
Warum Prescaler /32?
Der ADC-Takt muss laut Datenblatt zwischen 50 kHz und 200 kHz liegen für volle 10-Bit-Genauigkeit. Da wir nur 33 Stufen (−16 bis +16, also ~5 Bit) brauchen, darf der Takt höher sein.
CPU-Takt: 8 MHz Prescaler /32 → ADC-Takt: 250 kHz → eine Wandlung dauert ~52 µs
Kalibrierung: ADC_X_CENTER_RAW und ADC_Y_CENTER_RAW
Im Ruhezustand sollte der ADC theoretisch 512 liefern (Mitte von 0–1023). In der Praxis weicht das ab, weil der Joystick mechanisch nicht perfekt mittig ist.
Durch Messen wurde festgestellt:
— X-Achse im Ruhezustand: ca. 687 statt 512
— Y-Achse im Ruhezustand: ca. 495 statt 512
#define ADC_X_CENTER_RAW 687
#define ADC_Y_CENTER_RAW 495x=%d y=%d) zeigt die skalierten Werte — der Rohwert ist über adcLastReadRaw() erreichbar.
adcScale() — Rohwert auf [−16, +16] umrechnen
Die Funktion adcScale(raw, center) in adc.c rechnet den ADC-Rohwert (0–1023) in einen
skalierten Wert (−16 bis +16) um, wobei der gemessene Ruhewert (center) als Nullpunkt gilt.
raw < center → linke Seite: -(center - raw) × 16 / center raw > center → rechte Seite: (raw - center) × 16 / (1023 - center)
Beispiel für X-Achse (center = 687):
raw = 0 → -(687 - 0) × 16 / 687 = -16 (voller Ausschlag links) raw = 687 → 0 (Ruhelage) raw = 1023 → (1023 - 687) × 16 / 336 = +16 (voller Ausschlag rechts)
setup_controller() — Initialisierung
void setup_controller(void)Diese Funktion wird einmal beim Start in main.c aufgerufen. Sie:
1. Richtet die USART-Schnittstelle ein (9600 Baud, 8N1) → für Debug-Ausgaben
2. Startet den ADC im Free-Running-Modus → X/Y werden ab jetzt dauerhaft gemessen
3. Konfiguriert PA2 als Eingang mit internem Pull-up → für den Joystick-Taster
4. Schaltet mit sei() die globalen Interrupts frei → ISRs können jetzt laufen
controller_status() — Joystick auswerten
int controller_status(void)Diese Funktion wird im Hauptprogramm regelmäßig aufgerufen (alle 100 ms).
Schritt 1: Werte lesen
int8_t x = adcLastRead(0); // X-Achse: -16 (links) bis +16 (rechts)
int8_t y = adcLastRead(1); // Y-Achse: -16 (unten) bis +16 (oben)
int8_t z = (PINA & Bit(2)) ? 0 : 1; // Taster: 0 = offen, 1 = gedrücktSchritt 2: Taster hat Vorrang
if (z == 1) return 5;Schritt 3: Richtungserkennung
Die Schwelle von ±10 sorgt dafür, dass kleine Wackler in der Mitte nicht als Richtung gezählt werden. Die Bedingung auf der Gegenachse (±6) verhindert, dass Diagonalbewegungen als zwei Richtungen erkannt werden:
x ≥ +10 und |y| ≤ 6 → Rechts (4) x ≤ -10 und |y| ≤ 6 → Links (3) y ≤ -10 und |x| ≤ 6 → Unten (2) y ≥ +10 und |x| ≤ 6 → Oben (1) sonst → Mitte (0)
Visualisierung der Schwellwerte:
y ≥ +10
↑ Oben (1)
|
x ≤ -10 ← ·Mitte· → x ≥ +10
Links(3) | Rechts(4)
↓
y ≤ -10
Unten (2)
Zusammenspiel mit dem Rest des Projekts
main.c
│
├── setup_controller() → initialisiert ADC + USART + Taster
│
└── (Loop alle 100 ms)
│
├── controller_status() → liefert 0–5
│
├── update_display(status, ...) → display.c: ändert Frequenz/Amplitude/Waveform
└── update_audio(frequency[...]) → audio.c: spielt Ton ab
audio.c — Tonerzeugung via Timer1
Hardware-PWM · Fast-PWM (Modus 14) · ICR1 = TOP/Frequenz · OCR1A/OCR1B = Tastverhältnis · Lautstärke-Stufen
Wie entsteht der Ton und warum muss die CPU im Betrieb nichts tun?
ICR1 ist der TOP-Wert und bestimmt die Frequenz, OCR1A/OCR1B sind die Compare-Werte und bestimmen das Tastverhältnis (die Pulsweite). Sobald die Pins über COM1A1/COM1B1 an den Timer angeschlossen sind, läuft das Pulssignal autonom in Hardware. Die CPU stellt ICR1/OCR1A/OCR1B nur einmal bei einer Änderung neu ein.Erkläre die ICR1-Formel — warum KEIN Faktor 2 und warum 32-Bit (UL/uint32_t)?
ICR1 = F_CPU·10 / (PRESCALER·note_tenth_hz) − 1. Kein Faktor 2, weil im Fast-PWM-Modus ein kompletter Zähldurchlauf 0→TOP→0 bereits genau einer vollen Signalperiode entspricht (kein Toggle-Paar wie früher im CTC-Modus). Mit Faktor 2 klänge jeder Ton eine Oktave zu hoch. 32-Bit, weil F_CPU·10 = 80 Mio. 16 Bit überläuft.Warum klingen Sinus/Rechteck/Sägezahn jetzt unterschiedlich und wie wird die Lautstärke gemacht?
duty_shift ein anderes Tastverhältnis: Sin 50 %, Square 25 %, Saw 12,5 % von (ICR1+1). Unterschiedliche Tastverhältnisse haben unterschiedlichen Obertongehalt → hörbar andere Klangfarbe am Buzzer, auch ohne DAC. Lautstärke bleibt weiterhin grob über die Zahl aktiver Lautsprecher gestuft (amp 1–5: einer, 6–10: beide).Dieses Dokument erklärt die audio.c Zeile für Zeile bzw. abschnittsweise.
Hardware-Kontext: ATmega32, 8 MHz Takt. Tonerzeugung läuft komplett über
Timer1 als Hardware-PWM. Die CPU muss im laufenden Betrieb nichts tun — der
Timer schaltet die Lautsprecher-Pins selbstständig im richtigen Takt.
1. Die Grundidee
Ein Lautsprecher macht ein Geräusch, wenn sich die Spannung an ihm schnell ändert. Schalten wir einen Pin sehr schnell zwischen high (5 V) und low (0 V) um, entsteht ein Pulssignal:
Spannung
5V ┌────┐ ┌────┐ ┌────┐
│ │ │ │ │ │
0V ─┘ └────┘ └────┘ └── → Zeit
<-T-> = eine Periode = 1 / Frequenz
— Schnelles Umschalten → hoher Ton.
— Langsames Umschalten → tiefer Ton.
Das ist die erste Stellschraube: die Frequenz. Die zweite Stellschraube ist die Pulsweite (das Tastverhältnis) — wie lange der Pin pro Periode high gegenüber low bleibt. Sie ändert nicht die Tonhöhe, sondern den Obertongehalt und damit die Klangfarbe.
2. Die #define-Konstanten
#define F_CPU 8000000UL
#define PRESCALER 8UL| Konstante | Bedeutung |
|---|---|
F_CPU | Taktfrequenz der CPU = 8 MHz = 8 000 000 Hz. |
PRESCALER | Vorteiler. Der CPU-Takt wird vor dem Timer durch 8 geteilt. Timer1 zählt also mit 8 MHz / 8 = 1 MHz (1 Mio. Schritte/Sekunde). |
PRESCALER (Wert 8) muss zum gesetzten Prescaler-Bit im Timer passen. Im Code wird CS11 gesetzt = Vorteiler /8. Würde man eins ändern und das andere vergessen, stimmt jede berechnete Frequenz nicht mehr.
#include "note.h" // liefert NOTE_NONE = 5000Ein Sondersignal: Wenn in einer Zelle der Wert 5000 steht, bedeutet das „kein Ton / Pause". Das UL hinter Zahlen in den Formeln weiter unten heißt Unsigned Long — es zwingt den Compiler, mit großen vorzeichenlosen 32-Bit-Zahlen zu rechnen.
note.h — vom Labor bereitgestellter Header
Definiert ein enum Note mit allen Klaviernoten in 1/10 Hz (scientific pitch notation), von NOTE_B0 = 309 bis NOTE_DS8 = 49780 — z. B. NOTE_C4 = 2616 oder NOTE_A4 = 4400 (Kammerton a'), NOTE_B4 = 4939. Dazu NOTE_NONE = 5000 als Marker für „kein Ton / Pause". Spart in audio.c die eigene Magic-Zahl AUDIO_NOTE_NONE_TENTH_HZ.
3. Das Datenmodell (frequency[7][11][3])
int frequency[AUDIO_NUM_FREQ][AUDIO_NUM_AMP][AUDIO_NUM_WAVE];
// 7 11 3| Index | Name | Bereich | Bedeutung |
|---|---|---|---|
f | Frequenz | 0..6 | Tonhöhen-Slot |
a | Amplitude | 0..10 | Lautstärke (0 = stumm) |
w | Wave | 0..2 | Wellenform: 0=Sin, 1=Rechteck, 2=Säge |
4400 = 440,0 Hz = Kammerton a').
4. setup_audio() — die einmalige Einrichtung
4.1 Pins auf Ausgang schalten
AUDIO_SPK1_DDR |= (1 << AUDIO_SPK1_PIN);
AUDIO_SPK2_DDR |= (1 << AUDIO_SPK2_PIN);DDRx = Data Direction Register. Ein Bit auf 1 = der Pin ist ein Ausgang. (1 << PIN) erzeugt eine Maske mit genau dem einen gesetzten Bit, |= setzt dieses Bit ohne die anderen Pins zu verändern.
4.2 Timer1 konfigurieren
TCCR1A = (1 << WGM11);
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS11);| Bit | Register | Wirkung |
|---|---|---|
WGM11 | TCCR1A | Teil des Wellenform-Modus |
WGM13, WGM12 | TCCR1B | zusammen mit WGM11: WGM13:10 = 1110 → Fast-PWM, Modus 14, TOP = ICR1 |
CS11 | TCCR1B | Clock Select: Prescaler /8 |
ICR1 (TOP) hoch und springt dann sofort wieder auf 0 — ein kompletter Durchlauf entspricht genau einer vollen Signalperiode. ICR1 bestimmt damit die Tonhöhe.
COM1A1/COM1B1), die die Pins an den Timer anschließen, werden in setup_audio() bewusst noch nicht gesetzt. Solange sie 0 sind, bleiben die Pins normale GPIO-Ausgänge und werden fest auf low gezogen → sauberer, garantiert stummer Start. Erst update_audio() übergibt die Pins beim Spielen an den Timer und nimmt sie beim Stummschalten wieder weg.
5. update_audio() — der eigentliche Ablauf
Diese Funktion arbeitet in 5 logischen Schritten:
Schritt 1 — Aktive Zelle suchen
for (uint8_t f = 0; f < AUDIO_NUM_FREQ; f++)
for (uint8_t a = 0; a < AUDIO_NUM_AMP; a++)
for (uint8_t w = 0; w < AUDIO_NUM_WAVE; w++)
if (frequency[f][a][w] != 0) {
active_f = f; active_a = a; active_w = w;
note_tenth_hz = frequency[f][a][w];
goto cell_found;
}
cell_found:goto?
Ein break würde nur die innerste Schleife verlassen. Um aus allen drei Schleifen auf einmal herauszuspringen, ist ein goto an ein Label hier der sauberste und gut lesbare Weg.
Schritt 2 — Stumm-Fälle abfangen
if (note_tenth_hz == 0
|| active_a == 0
|| note_tenth_hz == NOTE_NONE) {
TCCR1A &= ~((1 << COM1A1) | (1 << COM1B1));
AUDIO_SPK1_PORT &= ~(1 << AUDIO_SPK1_PIN);
AUDIO_SPK2_PORT &= ~(1 << AUDIO_SPK2_PIN);
return;
}In drei Fällen soll es still bleiben: kein aktive Zelle gefunden, Amplitude ist null, oder das „kein Ton"-Sondersignal (5000).
Schritt 3 — Tonhöhe → TOP-Wert ICR1
uint16_t top = (uint16_t)((F_CPU * 10UL)
/ (PRESCALER * (uint32_t)note_tenth_hz) - 1UL);
ICR1 = top;Grundformel mit echter Frequenz f:
F_CPU
ICR1 = ───────────── − 1
PRESCALER · f
Setzt man f = note_tenth_hz / 10 ein, wandert die 10 in den Zähler:
F_CPU · 10
ICR1 = ───────────────────────── − 1
PRESCALER · note_tenth_hz
Rechenbeispiel für 440,0 Hz (note_tenth_hz = 4400):
ICR1 = (8 000 000 · 10) / (8 · 4400) − 1
= 80 000 000 / 35 200 − 1
≈ 2272 − 1 = 2271
UL / (uint32_t)?
Der Zwischenwert 80 000 000 passt nicht in 16 Bit (max. 65 535). Ohne 32-Bit-Rechnung gäbe es einen Überlauf und einen völlig falschen Ton. Das Ergebnis wird erst am Ende auf uint16_t zurückgecastet, weil ICR1 ein 16-Bit-Register ist.
Schritt 4 — Wellenform
uint8_t duty_shift;
switch (active_w) {
case AUDIO_WAVE_SIN: duty_shift = 1; break; // 50%
case AUDIO_WAVE_SQUARE: duty_shift = 2; break; // 25%
case AUDIO_WAVE_SAW: duty_shift = 3; break; // 12,5%
default: duty_shift = 1; break;
}
uint16_t duty = (uint16_t)((top + 1U) >> duty_shift);
OCR1A = duty;
OCR1B = duty;
TCCR1A |= (1 << COM1A1);switch wirklich etwas Unterschiedliches: jede Wellenform bekommt ein anderes Tastverhältnis (Bruchteil von top + 1): Sin = 50 % (obertonärmster, „rundester" Puls), Square = 25 %, Saw = 12,5 % (obertonreichster, „schärfster" Puls). Unterschiedlicher Obertongehalt heißt unterschiedliche Klangfarbe — die drei Wellenformen klingen am Buzzer jetzt tatsächlich hörbar verschieden.
Schritt 5 — Lautstärke über Anzahl der Lautsprecher
if (active_a >= 6) {
TCCR1A |= (1 << COM1B1); // Speaker 2 dazuschalten
} else {
TCCR1A &= ~(1 << COM1B1); // Speaker 2 aus
AUDIO_SPK2_PORT &= ~(1 << AUDIO_SPK2_PIN);
}Amplitude a | Verhalten |
|---|---|
0 | stumm (schon in Schritt 2 erledigt) |
1 – 5 | nur Speaker 1 (Speaker 2 hart auf low) |
6 – 10 | beide Speaker parallel (COM1B1 zusätzlich an) |
6. Zusammenfassung des Datenflusses
3D-Array frequency[f][a][w] (genau eine Zelle ≠ 0)
│
▼
update_audio()
├─ 1. aktive Zelle suchen → note_tenth_hz, active_a, active_w
├─ 2. stumm? ── ja ──► Timer-Ausgänge aus, Pins low, return
│ │ nein
├─ 3. ICR1 = Frequenz (TOP) → bestimmt Tonhöhe
├─ 4. switch(wave) → OCR1A/OCR1B = Tastverhältnis (Duty)
└─ 5. amplitude ≥ 6 ? → Speaker 2 dazu (lauter)
│
▼
Timer1 erzeugt Pulssignal an PD5 (+ ggf. PD4) autonom → hörbarer Ton
7. Typische Prüfungsfragen (Selbsttest)
Warum hängen beide Lautsprecher an Timer1?
Was passiert im Fast-PWM-Modus genau?
ICR1 (TOP), springt dann auf 0 zurück und startet neu — ein Durchlauf entspricht genau einer vollen Signalperiode.Warum hat die ICR1-Formel KEINEN Faktor 2?
Warum UL und (uint32_t) in der Formel?
F_CPU * 10 überläuft 16 Bit; man braucht 32-Bit-Rechnung.Warum goto statt break?
break verlässt nur die innerste Schleife; goto springt aus allen dreien.Warum klingen Sin/Square/Saw jetzt unterschiedlich?
duty_shift ein anderes Tastverhältnis (50 % / 25 % / 12,5 % von ICR1+1) in OCR1A/OCR1B — unterschiedliches Tastverhältnis bedeutet unterschiedlichen Obertongehalt und damit hörbar andere Klangfarbe.Wie wird die Lautstärke realisiert?
display.c — OLED-Rendering & Array-Aufbau
SH1106 SPI · Framebuffer · Wellenform-Pixel · One-Hot-Befüllung
Wozu der Framebuffer im RAM statt direkt aufs Display zu schreiben?
Was bedeutet „One-Hot" beim frequency-Array und wie hängt display.c mit audio.c zusammen?
display.c schreibt diese Zelle, update_audio() liest sie.Warum beginnt das Schreiben bei Spalte 2 und wie funktioniert der Byte-Trick (Little-Endian)?
uint64_t-Spalte wird als 8 Bytes betrachtet; Byte page = die 8 Pixel dieser Page — was nur funktioniert, weil der AVR Little-Endian ist.Dieses Modul ist das größte des Projekts. Es hat zwei Jobs:
1. Das OLED ansteuern — einen SH1106-Controller über SPI mit Bildinhalt füttern.
2. Den App-Zustand verwalten — sich merken, welche Tonhöhe/Lautstärke/Wellenform eingestellt ist, diesen Zustand auf Joystick-Eingaben verändern und ihn in das gemeinsame frequency-Array für audio.c schreiben.
Hardware: ATmega32, 8 MHz, OLED-Controller SH1106, 128×64 Pixel.
1. Pin-Belegung
#define DISPLAY_RESET_PIN PB3 // RES -> Display sauber zurücksetzen
#define DISPLAY_DATA_CMD_PIN PB4 // D/C -> LOW=Befehl, HIGH=Pixeldaten
#define DISPLAY_SPI_MOSI_PIN PB5 // DIN -> SPI-Daten (Master Out)
#define DISPLAY_SPI_MISO_PIN PB6 // (unbenutzt, nur Eingang)
#define DISPLAY_SPI_SCK_PIN PB7 // CLK -> SPI-Takt
// CS liegt fest auf GND -> kein eigener Pin nötig2. SPI — wie ein Byte zum Display kommt
static void display_spi_setup(void) {
DDRB |= (1 << MOSI) | (1 << SCK); // MOSI + SCK = Ausgang
DDRB &= ~(1 << MISO); // MISO = Eingang
SPCR = (1 << SPE) | (1 << MSTR); // SPI an, Master
SPSR = (1 << SPI2X); // doppelte Geschwindigkeit -> 4 MHz
}SPI schiebt Daten bitweise synchron zum Takt SCK hinaus. Bei F_CPU = 8 MHz und SPI2X ergibt sich 4 MHz SPI-Takt (Mode 0, MSB first).
static uint8_t display_spi_transfer_byte(uint8_t data) {
SPDR = data; // Schreiben startet die Übertragung
while (!(SPSR & (1 << SPIF))) { } // warten bis Hardware fertig (SPIF)
return SPDR;
}3. Der SH1106 in „Pages" — wie das Display organisiert ist
Eine Page = 8 Pixel HOCH, 132 Spalten breit. page 0 -> y 0..7 page 4 -> y 32..39 page 1 -> y 8..15 page 5 -> y 40..47 page 2 -> y 16..23 page 6 -> y 48..55 page 3 -> y 24..31 page 7 -> y 56..63
Ein Datenbyte beschreibt 8 senkrecht übereinanderliegende Pixel einer Page (Bit 0 oben, Bit 7 unten).
DISPLAY_NONVISIBLE_BORDER_OFFSET), sonst wäre das Bild um 2 Pixel verschoben.
4. Der Framebuffer — und der clevere Bit-Trick (Prüfungs-Highlight!)
static uint64_t display_frame_buffer[DISPLAY_PIXEL_WIDTH]; // 128 SpaltenWir zeichnen nicht direkt aufs Display, sondern erst in diesen RAM-Puffer:
— Ein uint64_t = eine komplette senkrechte Spalte mit 64 Pixeln.
— Bit y = Pixel (x, y). Also: display_frame_buffer[x] |= 1ULL << y; setzt den Pixel bei Spalte x, Zeile y.
Wie wird daraus eine Page für den SH1106?
uint8_t *first_page_column = ((uint8_t *)display_frame_buffer) + page;
for (uint8_t x = 0; x < DISPLAY_PIXEL_WIDTH; x++) {
display_spi_transfer_byte(*(first_page_column + DISPLAY_PAGES * x));
}Der Puffer ist uint64_t[128] — wir betrachten ihn als Byte-Array ((uint8_t *)). Jede Spalte belegt 8 Bytes. Für Spalte x liegen die Bytes 8·x … 8·x+7. Der Code liest für eine bestimmte Page das Byte an Index 8·x + page.
uint64_t einer Spalte x: Bits 0..7 8..15 16..23 ... 56..63
└ Byte 0 ┘ └ Byte 1 ┘ ... └ Byte 7 ┘
y-Pixel: y0..y7 y8..y15 ... y56..y63
SH1106-Page: Page 0 Page 1 ... Page 7
5. Zeichen-Primitive
display_draw_pixel(x, y); // Bit in fb[x] setzen (+ Bereichscheck)
display_draw_vertical_line(x, y1, y2); // y1,y2 sortieren, dann Bits setzen
display_draw_horizontal_line(x1, x2, y); // x1,x2 sortieren, dann zeichnen
display_clear_buffer(); // alle 128 Spalten = 0Alle Zeichenoperationen verändern nur den RAM-Puffer. Erst display_update_hardware() macht sie auf dem echten OLED sichtbar.
6. Die Wellenform zeichnen
6.1 Sample → Y-Pixel
static uint8_t display_sample_to_y(int16_t sample) { // sample: -127..+127
int16_t pixel_amplitude =
(DISPLAY_MAX_WAVE_AMPLITUDE * display_state.amplitude_index) / DISPLAY_MAX_AMPLITUDE_INDEX;
int16_t y = DISPLAY_CENTER_Y - ((sample * pixel_amplitude) / 127);
return display_clamp_y(y); // auf [WAVE_TOP=16 .. WAVE_BOTTOM=60] begrenzen
}Wichtige Konstanten: CENTER_Y = (16+60)/2 = 38, MAX_WAVE_AMPLITUDE = (60−16)/2 = 22, MAX_AMPLITUDE_INDEX = 10.
CENTER_Y − … (Subtraktion). display_clamp_y() hält die Kurve im Bereich 16..60.
6.2 Die drei Wellenformen (rein optisch)
case AUDIO_WAVE_SIN: sample = display_sine_lut[x % 32]; break;
case AUDIO_WAVE_SQUARE: sample = ((x % 32) < 16) ? 127 : -127; break;
case AUDIO_WAVE_SAW: sample = ((int16_t)((x % 32) * 255) / 31) - 127; break;audio.c) immer ein Rechteck. Sinus/Sägezahn sieht man also, hört sie aber nicht als solche.
7. Die Mini-Schrift (3×5-Font) für die Statuszeile
Oben am Display steht z.B. SIN A:5 F:349. Dafür gibt es einen winzigen 3 Pixel breiten, 5 Pixel hohen Font. display_draw_uint() zerlegt eine Zahl rückwärts in Ziffern (über % 10 und / 10) und zeichnet sie dann wieder vorwärts.
/ 10 → 349.
8. Der komplette Bildaufbau
static void display_render_current_state(void) {
display_generate_waveform_pixels(); // 1. Y-Werte der Kurve berechnen
display_clear_buffer(); // 2. Puffer leeren
display_draw_status(); // 3. Statuszeile oben
display_draw_horizontal_line(0, 127, DISPLAY_STATUS_HEIGHT); // 4. Trennlinie bei y=12
display_draw_waveform(); // 5. Kurve
display_update_hardware(); // 6. alles ans OLED schicken
}Jedes Bild wird komplett neu gebaut (kein teilweises Update). Bei 128×64 Pixeln ist das schnell genug.
9. Zustand & Eingabe-Logik
9.1 Der gemerkte Zustand
typedef struct {
uint8_t frequency_index; // 0..6 -> welche Tonhöhe aus display_notes_tenth_hz[]
uint8_t amplitude_index; // 0..10 -> Lautstärke (0 = stumm)
uint8_t waveform_index; // 0=Sin, 1=Rechteck, 2=Säge
} DisplayState;
// Startwerte: frequency_index=3, amplitude_index=5, waveform_index=0 (Sinus)Die 7 Tonhöhen (in 1/10 Hz):
display_notes_tenth_hz[7] = { 2616, 2937, 3296, 3492, 3920, 4400, 4939 };
// Index: C4 D4 E4 F4 G4 A4 B49.2 Eingabe → Zustandsänderung (display_step_state())
case UP: if (amplitude_index < 10) amplitude_index++; break; // lauter
case DOWN: if (amplitude_index > 0) amplitude_index--; break; // leiser (0=stumm)
case LEFT: frequency_index = (frequency_index==0) ? 6 : frequency_index-1; break;
case RIGHT: frequency_index++; if (frequency_index>=7) frequency_index=0; break;
case SELECT:waveform_index++; if (waveform_index>=3) waveform_index=0; break;— Hoch/Runter → Lautstärke (mit Grenzen 0 und 10, kein Umlauf).
— Links/Rechts → Tonhöhe (mit Umlauf: nach dem letzten Slot wieder zum ersten).
— Taster (SELECT) → Wellenform durchschalten (0→1→2→0).
9.3 update_display() — der Einstieg aus main.c
void update_display(int controller, int frequency[7][11][3]) {
DisplayInput input = display_is_valid_input(controller) ? (DisplayInput)controller : CENTER;
if (input != CENTER && input != last_input) // Flankenerkennung im Modul
display_step_state(input);
last_input = input;
display_apply_state(frequency); // Zustand -> One-Hot-Array (Nachricht an Audio)
display_render_current_state(); // OLED neu zeichnen
}last_input-Flankenerkennung
Nur reagieren, wenn die Eingabe neu und nicht Mitte ist. Ein gehaltener Joystick schaltet pro Auslenkung nur einmal weiter. Das ist die zweite Sicherungsebene zur tmpcontroller-Prüfung in main.c.
9.4 Das One-Hot-Array bauen
static void display_apply_state(DisplayFrequencyTable frequency) {
display_clear_frequency(frequency); // ALLES auf 0
frequency[frequency_index][amplitude_index][waveform_index]
= display_notes_tenth_hz[frequency_index]; // genau EINE Zelle setzen
}frequency[7][11][3] wird zuerst komplett gelöscht und dann wird genau eine Zelle gesetzt. Diese Vereinbarung mit audio.c: Indizes = Slots, Wert = Tonhöhe in 1/10 Hz.
10. Initialisierung setup_display()
display_spi_setup();
DDRB |= (1<<RESET) | (1<<DATA_CMD);
display_reset();
display_send_command(SET_DISPLAY_OFF);
display_set_contrast(128);
display_send_command(CHARGEPUMP); display_send_command(0x14); // interne Hochspannung AN
display_clear_buffer(); display_update_hardware();
display_send_command(SET_DISPLAY_ON);12. Typische Prüfungsfragen (Selbsttest)
Wozu der Framebuffer im RAM statt direkt aufs Display?
Erkläre den Byte-Trick in display_update_hardware().
uint64_t-Spalte als 8 Bytes betrachtet; Byte page einer Spalte = die 8 Pixel dieser Page. Funktioniert, weil AVR Little-Endian ist (Byte 0 = niederwertigste Bits = oberste Page).Warum wird beim Schreiben Spalte 2 als Start genommen?
Warum 1/10 Hz in der Notentabelle und /10 für die Anzeige?
Wie wird ein gehaltener Joystick „entprellt"?
last_input-Flankenerkennung: Schritt nur, wenn die Eingabe neu (≠ letzte) und ≠ Mitte ist.Was bedeutet „One-Hot" beim frequency-Array und wer liest es?
update_audio() liest es.