Kitabı oku: «Mit Arduino die elektronische Welt entdecken», sayfa 14
Was wir brauchen
Für dieses Bastelprojekt benötigen wir folgende Bauteile:
Tabelle 2: Bauteilliste | |
Bauteil | Bild |
---|---|
Schieberegister 74HC595 1x | ![]() |
LED rot 8x | ![]() |
Widerstand 330Ω 8x | ![]() |
Widerstand 10KΩ 1x | ![]() |
Mikrotaster 1x | ![]() |
Der Schaltplan
Der Schaltplan zeigt uns die einzelnen LEDs mit ihren 330-Ohm-Widerständen, die durch das Schieberegister 74HC595 angesteuert werden. Der Master-Reset-Eingang des Chips liegt über dem Pullup-Widerstand an der +5V-Betriebsspannung, so dass bei nicht gedrücktem Taster der Reset nicht ausgelöst wird, da der MR-Eingang LOW-aktiv ist. Das erkennst du daran, dass sich über dem MR – ich erwähnte es bereits – ein waagerechter Strich befindet, was eine Negation bedeutet. Der Eingang Output-Enabled ist ebenfalls LOW-aktiv und liegt fest verdrahtet auf Masse, denn die Ausgänge sollen immer freigeschaltet sein. Die Ansteuerung des Schieberegisters erfolgt über die Arduino-Pins 8, 9 und 10 mit den oben beschriebenen Funktionen.
Hast du den Sketch, den ich gleich zeigen werde, gestartet, wird sofort die erste LED an Ausgang QA leuchten, da du lediglich einmal eine 1 ins Schieberegister geschoben hast. Für einen Reset musst du sowohl den Taster der Schaltung als auch den Reset-Taster auf dem Arduino-Board betätigen.

Abb. 7: Der Arduino steuert über drei Leitungen das Schieberegister 74HC595 an
Der Schaltungsaufbau
Wenn du dir die Schaltung auf dem Arduino Discoveryboard aufbaust, achte genau auf die korrekte Verkabelung, denn die Kabeldichte hat etwas zugenommen und verlangt ein wenig Konzentration.

Abb. 8: Der Schaltungsaufbau mit Schieberegister 74HC595
Der Arduino-Sketch
Es folgt der Sketch-Code zur Ansteuerung des Schieberegisters 74HC595 über drei Leitungen der digitalen Ausgänge. Die benötigten Pins am Schieberegister sind:
SH_CP (Schieberegister Takteingang)
ST_CP (Speicherregister Takteingang)
DS (Serieller Eingang für die Daten)
Die drei Datenleitungen werden Variablen zugewiesen, die ich folgendermaßen benannt habe:
SH_CP wird taktPin genannt.
ST_CP wird speicherPin genannt.
DS wird datenPin genannt.
Dieser Sketch soll eine Bit-Kombination, die in einem Array gespeichert ist, an das Schieberegister versenden.
int taktPin = 8; // SH_CP int speicherPin = 9; // ST_CP int datenPin = 10; // DS byte datenArray[] = {1, 0, 1, 0, 1, 1, 0, 1}; // Daten-Array void setup(){ pinMode(taktPin, OUTPUT); pinMode(speicherPin, OUTPUT); pinMode(datenPin, OUTPUT); setzePins(datenArray); // Setze Pins über das Daten-Array } void loop(){ /* leer */ } void setzePins(byte daten[]){ // Speicheranzeige verhindern digitalWrite(speicherPin, LOW); // ST_CP for(byte i = 0; i < 8; i++){ // Daten versenden digitalWrite(datenPin, daten[i]); delay(20); // Takt generieren LOW->HIGH digitalWrite(taktPin, LOW); delay(20); digitalWrite(taktPin, HIGH); } // Speicher anzeigen digitalWrite(speicherPin, HIGH); // ST_CP }
Wie ich oben schon erwähnte, leuchtet nach dem Übertragen des Sketches lediglich der Ausgang QA, was unspektakulär ist, doch später wird es noch spannender. Versprochen.
Den Code verstehen
Zu Beginn werden die Variablen mit den benötigten Pin-Informationen versorgt und am Anfang der setup-Funktion alle Pins als Ausgänge programmiert. Du wirst in diesem Bastelprojekt das erste Mal eine Funktion selbst schreiben. Eine Funktion ist ja nichts Neues mehr für dich, denn setup und loop fallen in eben diese Kategorie der programmtechnischen Konstrukte. Ich möchte hier dennoch etwas genauer auf diese Thematik eingehen, damit der Sinn deutlicher wird. Eine Funktion kann als eine Art Unterprogramm betrachtet werden, das innerhalb des normal ablaufenden Sketches immer wieder aufgerufen werden kann. Sie wird dabei über ihren Namen aufgerufen und kann sowohl einen Wert an den Aufrufer zurückliefern als auch mehrere Übergabewerte aufnehmen, die sie zur Berechnung und Verarbeitung benötigt. Die formale Struktur einer Funktion sieht wie in Abbildung 9 aus.

Abb. 9: Grundlegender Aufbau einer Funktion
Der umrandete Bereich wird Funktionssignatur genannt und stellt die formale Schnittstelle zur Funktion dar. Eine Funktion ist vergleichbar mit einer Black-Box. Eigentlich musst du gar nicht wissen, wie sie funktioniert. Das Einzige, wovon du Kenntnis haben musst, ist die Struktur der Schnittstelle und in welcher Form ein Wert zurückgegeben wird.
Natürlich programmierst du hier die Funktion selbst und musst deswegen wissen, was du an Logik dort hineinpackst. Doch es gibt auch Funktionen, die du aus dem Internet herunterladen und in dein Projekt einbauen kannst, sofern sie keine lizenzrechtlichen Einschränkungen haben. Wurden sie von anderen programmiert und erfolgreich getestet, kann es dir egal sein, wie sie funktionieren. Die Hauptsache ist, dass sie funktionieren.
Doch zurück zu unserer Funktionsdefinition. Falls sie einen Wert an den Aufrufer zurückliefert, wie das beispielsweise auch digitalRead macht, musst du in deiner Funktion den entsprechenden Datentyp angeben. Angenommen, du möchtest Werte zurückliefern, die allesamt Ganzzahlen sind, dann ist das der Datentyp Integer, der bekanntermaßen mit dem Schlüsselwort int gekennzeichnet wird. Wenn eine Rückgabe jedoch nicht erforderlich ist, musst du das durch das Schlüsselwort void (übersetzt: »leer«) kenntlich machen, das auch bei beiden Hauptfunktionen setup und loop verwendet wird.
Systemfunktionen

Hast du dich eigentlich einmal gefragt, warum die beiden Funktionen setup und loop genau so genannt und wo sie dann aufgerufen werden müssen? Das ist eine durchaus berechtigte Frage und dieser Umstand wird einfach so hingenommen. Bei setup und loop handelt es sich um Systemfunktionen, die implizit aufgerufen werden. Du musst dich, wie du schon gesehen hast, nicht extra darum kümmern. Falls es dich aber dennoch interessiert, kannst du bei Windows im Installationsverzeichnis nachschauen:
C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino
Öffne die Datei main.cpp mit einem Texteditor – nutze dafür zum Beispiel den Editor Notepad++. Du bekommst Folgendes zu sehen:

Die bei der Programmiersprache C++ zu Anfang des Programmstarts aufgerufene Funktion, die du hier siehst, nennt sich main. Es ist quasi der Einstiegspunkt, damit das Programm weiß, womit es beginnen soll. Die Funktion main enthält mehrere Funktionsaufrufe, die nacheinander abgearbeitet werden. Du erkennst die setup-Funktion in Zeile 43 und in einer Endlosschleife, die mit for(;;) definiert wird, den Aufruf der loop-Funktion in Zeile 46. Jetzt erkennst du sicherlich die Abläufe und Zusammenhänge, die im Hintergrund beim Start eines Sketches ablaufen, wenn es um das Aufrufen von setup beziehungsweise loop geht. Wenn du deiner Funktion einen oder mehrere Werte übergeben möchtest, werden sie innerhalb der runden Klammern hinter dem Funktionsnamen durch Kommata getrennt mit ihren entsprechenden Datentypen aufgelistet. Falls du keine Werte übergeben willst, bleibt das runde Klammerpaar einfach leer. Aber weglassen darfst du es nicht! Die Signatur haben wir jetzt so weit abgehandelt, dass nur noch der Funktionsrumpf übrigbleibt, der durch das geschweifte Klammerpaar gebildet wird. Alle Befehle, die sich innerhalb dieser Klammern befinden, gehören zu dieser Funktion und werden beim Funktionsaufruf sequentiell von oben nach unten abgearbeitet.
Doch nun zurück zu unserem Sketch, der im Moment noch eine Zwischenstufe zu einem endgültigen Code darstellt, der für das Versenden von seriellen Daten an das Schieberegister verwendet wird. Dann wollen wir mal sehen, wie der Code seine Arbeit verrichtet. Alles dreht sich hier um das Daten-Array, in dem das Muster hinterlegt ist, wie die einzelnen LEDs anzusteuern sind. Fangen wir mit der Deklaration an, in der die drei Steuer-Pins für das Schieberegister hinterlegt sind:
int taktPin = 8; // SH_CP int speicherPin = 9; // ST_CP int datenPin = 10; // DS
Die Daten sind im folgenden Array zu finden:
int datenArray[] = {1, 0, 1, 0, 1, 1, 0, 1};
Der Code liest die einzelnen Array-Elemente von links nach rechts aus und schiebt die Werte in das Schieberegister. Eine 1 bedeutet LED an, eine 0 LED aus. Die Ansteuerung der LEDs wurde durch die Werte 0 und 1 beschrieben. Funktioniert das denn auch wirklich? Sollte man nicht besser mit den Konstantennamen HIGH und LOW arbeiten? Sicherlich eine berechtigte Frage. Ich habe hier die Werte 1 und 0 verwendet, weil das genau die Werte sind, die sich hinter den Konstanten HIGH und LOW verbergen. Normalerweise bin ich gegen Magic Numbers, doch in diesem Fall dachte ich, ich könnte eine Ausnahme machen. 1 und 0 sind ja auch die logischen Werte, und deswegen bereitet das keine allzu großen Interpretationsprobleme – oder? Du kannst beides schreiben:
int datenArray[] = {1, 0, 1, 0, 1, 1, 0, 1};
oder:
int datenArray[] = {HIGH, LOW, HIGH, LOW, HIGH, HIGH, LOW, HIGH};
Kehren wir zurück zum Code und der Frage, wie er das Array auswertet. Innerhalb der setup-Funktion kommt es natürlich zur Konfiguration der drei Steuer-Pins, die allesamt als Ausgänge arbeiten. Am Ende wird dann einmalig eine selbst definierte Funktion mit dem Namen setzePins aufgerufen, in der die komplette Ansteuerung des Schieberegisters erfolgt:
void setup() { pinMode(taktPin, OUTPUT); pinMode(speicherPin, OUTPUT); pinMode(datenPin, OUTPUT); setzePins(datenArray); // Setze Pins über das Daten-Array }
Die setzePins-Funktion besitzt einen einzigen Parameter, der aber keine normale Variable aufnehmen kann, sondern nur ein ganzes Array. Beim Aufruf übergebe ich einfach das Daten-Array als Argument an die Funktion:
setzePins(datenArray);
Die Funktion wurde dann folgendermaßen definiert:
void setzePins(byte daten[]){ // Speicheranzeige verhindern digitalWrite(speicherPin, LOW); // ST_CP for(byte i = 0; i < 8; i++){ // Daten versenden digitalWrite(datenPin, daten[i]); delay(20); // Takt generieren LOW->HIGH digitalWrite(taktPin, LOW); delay(20); digitalWrite(taktPin, HIGH); } // Speicher anzeigen digitalWrite(speicherPin, HIGH); // ST_CP }
Du siehst, dass in der Signatur der Funktion mithilfe eines eckigen Klammerpaares ein Array des Datentyps int deklariert wurde. Beim Aufruf der Funktion wird das ursprüngliche Array datenArray in das Array daten kopiert, mit dem dann innerhalb der Funktion gearbeitet wird. Zu Beginn wird der Speicher-Pin über folgende Zeile mit einem LOW-Pegel versehen:
digitalWrite(speicherPin, LOW); // ST_CP
Das führt dazu, dass beim Einlesen der einzelnen Bis aus dem Array diese nicht unmittelbar an die Ausgänge beziehungsweise zur Anzeige gelangen. Erst am Ende des Einlesevorgangs erfolgt ein Wechsel dieses Pins auf einen HIGH-Pegel. Nun wird über die for-Schleife jedes einzelne Array-Element angesprochen und mit einer kleinen Pause von 20ms an den seriellen Eingang geschickt:
digitalWrite(datenPin, daten[i]); delay(20);
Es wird über einen Taktimpuls mit dem Pegelwechsel LOW-HIGH und einer Pause von 20ms in das erste interne Register geschoben:

digitalWrite(taktPin, LOW); delay(20); digitalWrite(taktPin, HIGH);
Das Ganze erfolgt achtmal (0 bis 7), wobei die internen Register ihre Werte immer an den Nachfolger weiterreichen. Die folgenden Abbildungen können das noch ein wenig mehr veranschaulichen:

Abb. 10: Das Schieberegister (Phase 1)
Zu Beginn sind die internen Register noch alle leer. Am seriellen Eingang wartet jedoch schon eine 1 auf den Transport in das erste interne Register auf der linken Seite.

Abb. 11: Das Schieberegister (Phase 2)
Die sich am seriellen Eingang befindende 1 wird beim SH_CP-Takt in das erste interne Register geschoben. Die Inhalte aller Register werden um eine Position weiter nach rechts verschoben. Nach dieser Aktion ergeben sich folgende Zustände:

Abb. 12: Das Schieberegister (Phase 3)
Am Eingang befindet sich nun eine 0, die ebenfalls beim nächsten SH_CP-Takt in das erste interne Register geschoben wird. Doch zuvor wandert der Zustand des ersten internen Registers in das zweite, das zweite in das dritte und so weiter. Wir springen jetzt zu dem Zeitpunkt, zu dem alle Werte des Arrays in die internen Register nach dem oben erläuterten Schema geschoben wurden und der ST_CP-Takt die Register zu den Ausgängen durchgeschaltet hat:

Abb. 13: Das Schieberegister (Phase 4)
Nach dem Absetzen des folgenden Befehls liegen die Werte des eingelesenen Arrays an den Ausgängen an:
digitalWrite(speicherPin, HIGH); // ST_CP
Der erste eingeschobene Wert liegt dabei ganz rechts und der letzte ganz links. Gibt es eigentlich eine Möglichkeit, das Verhalten umzukehren? Das ist kein Problem. Das Setzen der Pins geschieht wo? Richtig, innerhalb der setzePins-Funktion. Die for-Schleife steuert die einzelnen Pins an. Wenn du jetzt zuerst den letzten anstatt den ersten Wert abrufst und in das Schieberegister überträgst, wird die Reihenfolge umgekehrt. Hier der modifizierte Code der for-Schleife:
for(int i = 7; i >= 0; i--){ // ... }
Der erweiterte Arduino-Sketch (Teil 2)
Nun habe ich dir so viel Grundwissen über das Schieberegister 74HC595 vermittelt, dass ich dich mit einem speziellen Befehl vertraut machen möchte, der dir ein wenig Arbeit abnimmt. Dieser Befehl lautet shiftOut und ist wirklich einfach anzuwenden.
Die Speicherung von Werten

Wie in Kapitel 3 schon erklärt, möchte ich hier noch einmal Informationen über die Speicherung von Werten im Computer geben, die du brauchst, um die Funktionsweise eines Mikrocontrollers zu verstehen. Für meine Ausführungen ziehe ich den Datentypen byte
heran, der ja eine Datenbreite von 8 Bits besitzt und Werte von 0 bis 255 speichern kann. In der folgenden Abbildung ist der dezimale Wert 157 als binärer Wert 10011101 dargestellt:

Abb. 14: Binärkombination für den Ganzzahlwert 157
Wenn du dir die Potenzen anschaust, wirst du sehen, dass die Basis die Zahl 2 ist. Wir Menschen rechnen mit der Basis 10. Der Wert der einzelnen Stellen einer Zahl ist also – von rechts nach links gesehen – 100, 101, 102 und so weiter. Für die Zahl 157 wäre das Folgendes, was in Summe wieder 157 ergibt:

Da der Mikrocontroller jedoch nur zwei Zustände (HIGH und LOW) speichern kann, liegt dem binären System wie es genannt wird, die Basis 2 zugrunde. Der Wert der einzelnen Stellen einer Zahl ist also – von rechts nach links gesehen – 20, 21, 22 und so weiter. Der dezimale Wert der oben gezeigten Binärkombination errechnet sich demnach wie folgt, wobei meist mit dem niedrigsten Bit angefangen wird:

Was bedeutet eine tiefgestellte Zahl hinter einem Wert?

Zur besseren Übersicht wird bei der Verwendung von Werten unterschiedlicher Zahlensysteme die Basis hinter den Wert geschrieben und tiefgestellt.
Mit einer Datenbreite von 8 Bit (= 1 Byte) kannst du 256 unterschiedliche Werte (0 bis 255) darstellen.
Auf Grundlage dieses Wissens können wir uns dem shiftOut-Befehl zuwenden. Er hat unterschiedliche Parameter, die du hier kennenlernen wirst:

Abb. 15: Der Befehl shiftOut mit seinen zahlreichen Argumenten
Die Argumente datenPin, taktPin und der zu übertragende wert sollten klar sein. Was aber bedeutet die Konstante MSBFIRST? Mit diesem Argument kannst du die Bit-Übertragungsrichtung festlegen. Du weißt ja schon, dass bei einem Byte das Bit mit dem höchsten Wert Most Significant Bit (MSB) und das mit dem niedrigsten Wert Least Significant-Bit (LSB) genannt wird. Du kannst also mit der Verwendung von MSBFIRST und LSBFIRST festlegen, welches Bit zuerst in das Schieberegister transferiert werden soll:


Nachfolgend erhältst du den kompletten Code mit dem Befehl shiftOut. Die Schaltung muss auch hier nicht verändert werden.
int taktPin = 8; // SH_CP int speicherPin = 9; // ST_CP int datenPin = 10; // DS byte wert = 157; // Zu übertragender Wert void setup(){ pinMode(taktPin, OUTPUT); pinMode(speicherPin, OUTPUT); pinMode(datenPin, OUTPUT); } void loop(){ digitalWrite(speicherPin, LOW); shiftOut(datenPin, taktPin, MSBFIRST, wert); digitalWrite(speicherPin, HIGH); delay(20); }
Die Angabe eines binären Wertes

Du kannst anstelle des dezimalen Wertes 157 auch direkt die Binärkombination bei der Initialisierung der Variablen angeben, sodass die Umrechnung entfällt. Schreib einfach B10011101. Das Präfix B ist ein Kennzeichen dafür, dass es sich um eine Binärkombination handelt, mit der die Variable initialisiert werden soll.
Ich habe ein Impulsdiagramm angefertigt, das dir die Pegel der drei Datenleitungen zur Ansteuerung des Schieberegisters übereinander zeigt:

Abb. 16: Impulsdiagramm für den übergebenen Wert 157 (B10011101)
Ganz oben erkennst du das Taktsignal SH_CP zur Übernahme der Daten am seriellen Eingang DS. Nach Abschluss des achten Taktes wird der Pegel von ST_CP von LOW auf HIGH gesetzt und die Daten werden aus den internen Registern an die Ausgänge übertragen. Spiel ein wenig mit unterschiedlichen Werten und Übertragungsrichtungen, um das Verständnis zu vertiefen.
Ein einfacher Binärzähler
Schau dir den folgenden Sketch an und versuche herauszufinden, wie er funktioniert. Du brauchst die anfangs gezeigte Schaltung dafür nicht anzupassen.
int taktPin = 8; // SH_CP int speicherPin = 9; // ST_CP int datenPin = 10; // DS void setup() { // Alle Pins als Ausgänge pinMode(taktPin, OUTPUT); pinMode(speicherPin, OUTPUT); pinMode(datenPin, OUTPUT); } void loop() { for(byte i = 0; i < 256; i++) { // speicherPin=LOW -> keine Anzeige digitalWrite(speicherPin, LOW); shiftOut(datenPin, taktPin, MSBFIRST, i); // speicherPin=HIGH ->Anzeige digitalWrite(speicherPin, HIGH); delay(300); } }
Troubleshooting
Falls die LEDs nicht nacheinander anfangen zu leuchten, trenne das Board sicherheitshalber vom USB-Anschluss und geh folgende Dinge durch:
Überprüf deine Steckverbindungen auf dem Breadboard, ob sie wirklich der Schaltung entsprechen.
Achte auf mögliche Kurzschlüsse.
Sind die LEDs richtig herum eingesteckt worden? Denk an die richtige Polung.
Haben die Widerstände die korrekten Werte?
Hast du das Schieberegister richtig verkabelt? Kontrolliere noch einmal alle Verbindungen, die ja zahlreich sind.
Überprüfe noch einmal den Sketch-Code auf Richtigkeit.
Was haben wir gelernt?
Du hast das Schieberegister vom Typ 74HC595 mit seriellem Eingang und acht Ausgängen kennengelernt.
Du hast im ersten Sketch die Ansteuerung über die drei Datenleitungen SH_CP, ST_CP und DS gesehen. Außerdem hast du gelernt, dass die Taktsignale taktflankengesteuert sind, was bedeutet, dass sie nur auf einen Pegelwechsel von LOW zu HIGH reagieren.
Der Befehl shiftOut bietet eine einfache Möglichkeit, Bit-Kombinationen über Dezimal- oder auch Binärzahlen an das Schieberegister zu versenden.
Du kannst eine Variable vom Datentyp byte mit einem Ganzzahlwert, beispielsweise 157, initialisieren oder mithilfe der entsprechenden Bit-Kombination, der das Präfix B vorangestellt werden muss, also zum Beispiel B10011101.
Wir haben einen einfachen Binärzähler realisiert.