InfTech Tutorium 4 Zusatzmaterial

Highlight dieser Woche: Zweidimensionale Arrays und Displays

+MP3 Player mit Arduino und Vintage-VFD Display

service_cut.jpg

Was haben Arrays und Pointer mit Displays zu tun? Die Antwort ist sehr sehr viel. Unsere modernen Displays, sowohl auf dem großen Desktop, als auch im Smartphone in der Tasche haben Millionen von Bildpunkten, aka. Pixel. Jedes einzelne Pixel präzise in ihrer Farbe gesteuert ergibt ein prachtvolles Gesamtbild, welches wir zur Darstellung von Bilder, Filmen und Fehlermeldungen beispielsweise verwenden.

Genug von der stimmungsvollen Intro? Na gut... Let's get right into it. Wir stellen uns mal vor, wir zerlegen unser Display in Zeilen und Spalten. Ein 1920 x 1080 Full-HD Display hätte demnach 1080 Zeilen und 1920 Spalten und wir würden jeden einzelnen Pixel allein durch die Zeile und Spalte ansprechen können. Würden wir also irgendwo mittig eine horizontale schwarze Linie zeichnen wollen, dann würden wir sagen 'hey, setze mal den gesamten Inhalt in Zeile 500 auf schwarz'. Wie würde das dann im (Pseudo-)C-Code aussehen?

for(int i = 0; i < 1920; i++){
  displayArray[499][i] = SCHWARZ;
}

Ebenfalls nehmen wir an, dass sobald wir in dieses Array schreiben, das Display beim nächsten Refresh 'sieht', dass sich die Farbe geändert hat. Was wir also tun ist, wir schreiben mit unserem Code direkt in das Display-Register rein.

Während bei einem modernen Display solche Abläufe vom Betriebssystem geregelt werden* (man stelle sich z.B. die Frage was alles passieren muss, damit unser

printf("Hello World"); 

tatsächlich auf dem Bildschirm in perfekter Schriftart ausgegeben wird) und man dadurch nicht so einfach direkten Einfluss auf die einzelne Pixelansteuerung hat, kann man bei einem Mini-Display mit wenigen, vorgegebenen Zeichen sehr wohl die Ansteuerung 'by-oneself-from-scratch' schreiben. Wie sieht so ein 'einfaches' Display aus?

vfdDisplay_smvar.jpg

So zum Beispiel. Was hier zu sehen ist ein VFD-Dipslay (auf deutsch: Vakuum-Fluorescenz-Anzeige), welches in den Sechzigern entwickelt worden ist. Man kennt sie aus Kassenanzeigen, Autoradios, CD-Player. Dieses dargestellte Exemplar gehörte tatsächlich einem CD-Player wenn man genau hinschaut: Play, Pause, Minutenanzeige, Titelnummer! Da wir alle Elektrotechnik lieben, erkläre ich auch wie das Display angesteuert wird. Kurz gefasst bringen wir eines dieser Segmente zum Leuchten, wenn wir eine hohe Spannung (30V zum Beispiel) an einem Pin anlegen. Durch gezielte Wahl von angeschalteten Segmenten lassen sich Buchstaben, Zahlen darstellen.

Beleuchtet, MP3-Player in Action

Im Dunkeln

Verdrahtung Rückseite

Schnell mal mit einem MP3-Modul und Arduino-AVR-Chip zusammengelötet, haben wir die CD-Anzeige upcyclet und umfunktioniert zu einem mehr oder weniger portablen MP3-Player. Interessant ist für uns aber, wie genau der C-Code denn aussieht, mit der die Anzeige angesprochen wird.

Dazu muss ich ein bisschen ausholen, denn gleich zu Beginn stehen wir vor einer Herausforderung. Welche Herausforderung? Wir finden genau 22 silberne Beinchen (Pins), die die Funktionalität besitzen, mit der Außenwelt zu kommunizieren, bzw. Spannungssignale entgegenzunehmen. Wait a second. Das ist doch viel zu wenig wenn wir jedes einzelne Segment einzeln ansteuern wollen? Dazu haben sich Ingenieure des Displays was besonders cleveres einfallen lassen: Zeitmultiplexing. Zeitmultiplexing funktioniert hier so, dass ein Teil der Pins nur dafür zuständig sind, Teile des Displays ein und auszuschalten. Ist ein Teil an, ist es möglich, Segmente darauf mit den restlichen Pins zum Leuchten zu bringen. Das ganze passiert abwechselnd so dass zu jedem Zeitpunkt ein unterschiedliches Anzeigenteil an ist. Wiederholt sich dieser Rhythmus so schnell in einer Sekunde, nehmen wir es so wahr als würde es die ganze Zeit an sein.

Bei unserem VFD-Display haben wir genau 10 Pins zum Ein-und Ausschalten von Displayteilen. Was wir jetzt also im C-Code umsetzen müssen ist ein Array, welches zwölf Elemente beinhaltet. Wie würde jetzt Multiplexing funktionieren? Nun, wir lassen es im Zyklus ausgeben. Zu jedem Zeitpunkt ist gerade eines der 10 Arrays "ausgabeaktiv" und in diesem Array befinden sich die Ein-und Ausschalt-Informationen einzelner Segmente:

#define DISPROWS 10
#define DISPBYTES 2

/* Display Register mapping
  15    byte1     11     byte1     7     byte0     3     byte0    0
  | 0 | 0 | 0 | 0 | e | f | g | sp | a | b | c | d | sp | sp | sp | sp |  dispIdx = 0
  | 0 | 0 | 0 | 0 | e | f | g | sp | a | b | c | d | sp | sp | sp | sp |  dispIdx = 1
  | 0 | 0 | 0 | 0 | e | f | g | sp | a | b | c | d | sp | sp | sp | sp |  dispIdx = 2
  ...                              ...                                      ...
  | 0 | 0 | 0 | 0 | e | f | g | sp | a | b | c | d | sp | sp | sp | sp |  dispIdx = 10
  15              11               7               3              0
*/
uint8_t** displayRegister;

Zunächst, warum jetzt ein zweidimensionales Array? Völlig berechtigt die Frage, denn man könnte auch ein 16 Bit Integer für die 12 Segmente des Segmentregisters verwenden. Da die finale Hardware-Ausgabe aber (minimal) praktischer mit 8 Bit funktioniert, fiel der Entschulss auf zwei 8 Bit Integer (uint8_t ist nichts anderes als ein unsigned char, aber in einer konsequenteren und eindeutigeren Schreibweise).

Im Bild rechts sieht man die exakte Zeichenmatrix dieser Anzeige. Da es keine Online-Dokumentation zum Display gibt, habe ich einfach selbst eine geschrieben - Ergebnis einer Session Herumprobieren. Die sogenannten Grids ('Gitter', g0 bis g9) aktivieren Teile des Displays, Segmente werden durch die Anoden (a0 bis a11) gesteuert.

Wir wollen uns nun anschauen, wie es funktioniert, wenn wir in dieses Register schreiben wollen. Für Zeichen (Buchstaben und Zahlen) wollen wir dort auf die Segmente 'a' bis 'g' zugreifen:

Durch ein wenig probieren ergibt sich diese Zeichenmatrix.

void writeChar(uint8_t dispIdx, uint8_t num){
  if((dispIdx < 2) || (dispIdx > 9)) return;
  
  // dispIdx between 2 (leftmost) and 9 (rightmost)
  // Convert to seven segment number
  num = charConvert(num);

  // Low bit segments: Extract segments a, b, c, d; write into display register
  displayRegister[dispIdx][0] |= (((num >> 4) & 0x0F) << 4);   
  // High bit segments: Extract segments e, f, g; write into display register   
  displayRegister[dispIdx][1] |= (((num >> 1) & 0x07) << 1);
}

Einfach zusammengefasst prüft der Code zuerst, ob wir in einen gültigen Bereich schreiben und setzen mit den ominösen '&' (bitweise UND), '|' (bitweise ODER), '<<' (logischer Links-Shift) und '>>' (logischer Rechts-Shift) die einzelnen Segment-Repräsentanten im Array. Die einzelnen Bedeutungen der Operatoren werden im Rechneraufbau-Teil gezeigt sowie in der weiterführenden Veranstaltung Mikroprozessortechnik. Die Funktion charConvert zerlegt ein Zeichen in dessen Siebensegmentdarstellung (hier weiterlesen: https://de.wikipedia.org/wiki/Segmentanzeige).

Damit wären wir also in der Lage beispielsweise die Spielzeit in Minuten auszugeben. Wie würde der Code dazu aussehen?

writeChar(2, minutes / 10);  // Minute decimals
writeChar(3, minutes % 10);  // Least significant minute digit
writeChar(4, seconds / 10);  // Second decimals
writeChar(5, seconds % 10);  // Least significant second digit

Noch im Kopf was der Modulo-Operator macht? Hier filtert es aus einer ein oder zweistelligen Zahl stets die hinterste Ziffer heraus. Dieser kommt auch in der ein wenig länger erscheinenden Funktion vor, die Titelnummer (das sind die fixen Zahlen rechts vom VFD-Display) auf dem Display anzeigen lässt:

// Display a song number (between 1 and 20)
void writeSongNum(uint8_t num){
  if((num < 1) || (num > 20)) return; // Cancel if out of range
  uint8_t dispIdx = num % 5;          // Get column position (dispIdx)
  if(dispIdx == 0) dispIdx = 5;       // Set column position to 5 if (% 5) == 0
  uint8_t bitIdx = num / 5;           // Get row position (bitIdx)
  // Special row case: If num dividable by five, subtract by 1 to get actual position
  if(dispIdx == 5) bitIdx -= 1;       

  displayRegister[dispIdx][0] |= (1 << (3 - bitIdx)); // Write into display register
}

Wir haben vier Zeilen und fünf Spalten und gehen im Abstand von 5 Songs stets eine Zeile weiter 'nach unten' (bitIdx). Welcher Song genau wird vom Modulo-Operator lokalisiert. Und weiter? Natürlich muss abschließend Display-Register geschrieben werden, damit beim nächsten Update die Ausgabe erfolgt.

Nun... könnte nun einfach weiter Funktionen vorstellen. Wie man Textnachrichten ausgeben kann... Mag aber auf Dauer für beide Seiten unspektakulär zu lesen sein. Umso interessanter wie die Funktion aussieht, die den Inhalt des Display-Registers tatsächlich auf das Display überträgt? Das ist auch die Funktion, die Zeitmultiplexing übernimmt. Ich stelle vor:

void displayUpdate(){
  mpxCounter++;
  uint16_t mpxBitPos;
  if(mpxCounter == 10){
    mpxCounter = 0;
    mpxBitPos = 1;
  }
  mpxBitPos = (1 << mpxCounter);                                    // Next display
  
  if(displayRegister[mpxCounter][0] || displayRegister[mpxCounter][1]){
    digitalWrite(LATCH_PIN, LOW);                                       
    shiftOut(DATA_PIN, CLOCK_PIN, LSBFIRST, (mpxBitPos & 0xFF));
    shiftOut(DATA_PIN, CLOCK_PIN, LSBFIRST, ((mpxBitPos >> 8) & 0x0F) | (displayRegister[mpxCounter][0] << 2));
    shiftOut(DATA_PIN, CLOCK_PIN, LSBFIRST, (displayRegister[mpxCounter][0] >> 6) | (displayRegister[mpxCounter][1] << 2));
    digitalWrite(LATCH_PIN, HIGH);
  }
}

Huch... Isses jetzt etwa wie in InfTech - so viel Code auf einmal und auch noch in übersichtlich? Tatsächlich. Aber wie der Code Bit für Bit funktioniert ist eigentlich recht unwichtig. Ganz interessant nur zu sehen ist wie hier gemultiplext wird. Im letzten Zusatzmaterial habe ich kurz die Arduino-Umgebung vorgestellt mit den Funktionen setup() und loop(). In Loop wird der Inhalt unendlich oft ausgeführt (bis eben der Stecker gezogen wird). Wir rufen jedes Mal die Funktion displayUpdate, also unsere Ausgabe-Funktion auf, und bei jedem Durchlauf wird ein anderer Teil vom Display angesprochen (mpxCounter wird erhöht, dadurch auch mpxBitPos, welches die tatsächliche Position in Binär angibt). Ein anschließendes Schreiben auf die Shift-Register, die über Treiber-Transistoren (NPN-PNP-Paar) mit dem Display verbunden sind aktiviert den Teil des Displays. Da Loop hunderte Male in einer Sekunde ausgeführt wird, nehmen wir an, dass jeder aktive Displayteil zu jeder Zeit an ist.

Schon Lust bekommen, selbst einen Arduino-MP3-Player zu bauen? Den vollständigen Code kann man sich übrigens hier herunterladen und begutachten:

Die Theorie zur Beschaltung eines VFD-Displays findet sich auch hier auf der Website (englisch). Die restliche Beschaltung kann aus dem Code entnommen werden.

Anm.: Dieser wurde nicht optimiert (unsauber geschrieben, bloß nicht als Vorbild nehmen ^^) und soll nur eine grundlegende Demonstration zur Funktionalität sein, dass es möglich ist, mit dem Arduino und DFPlayer Mini einen MP3-Player mit VFD-Display zu bauen. Die finale Verdrahtung zum Display ist ebenfalls willkürlich gewählt; Schaltplan muss auch noch geschrieben werden, immerhin ist der Schaltplan für Power-Management fertig:

Power Management Circuit

MP3 Player von hinten.

Frank F. Zheng

Hi there, it's Frank from THE VFD COLLECTIVE.

I'm a 22 year old electrical engineer student currently living in Berlin, Germany. This page, THE VFD COLLECTIVE was brought into life to promote my VFD tube digital clock (Project OpenVFD) and let everyone who enjoy doing creative nerd stuff with VFD displays and tubes to share their work.

In my free time, I'm in love with traveling, photography, hiking in the mountains, vegan food and great coffee. At home, you'll either find me learning for college or making music. Playing the piano, guitar and singing is so much fun.

Peace :)