Grundlagen der Digitaltechnik am Beispiel des Schieberegisters SN74HC595N
Einer der blödesten Sprüche aus meiner Jugend war: Lehre nicht andere, bis du nicht selbst gelehret bist. So ein Schwachsinn. Wer weiß schon alles? Und stehen wir nicht in der Pflicht, jungen Menschen das Wissen, was wir uns in den vergangenen Jahrzehnten angeeignet haben, weiterzugeben, sofern es noch relevant ist? Also werde ich im Folgenden mit meinen Worten versuchen, Grundlagen über die Arbeitsweise unserer Micro Controller, mit Schwerpunkt auf der Logik zu vermitteln, obwohl ich wahrhaftig nicht alles darüber weiß.
Die erste Hürde stellen für viele Anfänger die unterschiedlichen Zahlensysteme dar. Wer zehn Finger hat, rechnet im Jahre 2022 am besten im Dezimalsystem. Das römisch-lateinische Zahlensystem, nachdem wir im Jahre MMXXII leben, hat sich nicht bewährt. Aber unser Computer hat keine zehn Finger; er kennt nur 0 oder 1, Licht aus oder Licht an. Jede Speicheradresse und jeder Registerinhalt sind nur eine Folge von Nullen und Einsen. Die Einheit für diesen Speicher, der nur diese zwei Zustände annehmen kann, nennen wir bitte ein Bit. (Keine Schleichwerbung!). Wegen dieser Erkenntnis wird Konrad Zuse zu Recht als Erfinder des Computers bezeichnet, auch wenn sein Z2 noch mit Telefonrelais arbeitete, weil der Transistor noch nicht erfunden war.
Während wir also mit zehn verschiedenen Ziffern unsere Zahlen darstellen, begnügt sich die Elektronik mit einem Zweiersystem, auch Dualsystem oder Binärsystem genannt. Um den Überblick zu behalten, haben die Programmierer ganz schnell Leerzeichen in die schier unendliche Abfolge von Nullen und Einsen eingefügt, und zwar nach jedem vierten Zeichen. So eine Viererfolge hat man ein Nibble genannt. Für Prozessoren und Datenbusse hat man zunächst 8-Bit für die Verarbeitung ausgewählt und diese 8 Bits hat man ein Byte genannt. Damit arbeitet unser Mikrocontroller noch heute, während unsere PCs bei einer Datenbreite von 64 Bit angekommen sind. Die einzelnen Schritte dieser Evolution folgen (wie wir sehen werden logischerweise) den Zweierpotenzen.
Das Leerzeichen nach jeweils vier Bits erleichtert z.B. das Abtippen längerer Folgen, aber eine kürzere Schreibweise wäre doch wünschenswert. Deshalb haben die Programmierer ein weiteres Zahlensystem eingeführt: das Hexadezimalsystem. Welch ein Unwort: der erste Teil des Namens Hexa kommt aus dem Griechischen (für 6) , der zweite Teil aus dem Lateinischen (decem=10).
Mit vier Zeichen im Dualsystem kann man grundsätzlich 16 verschiedene Zahlen beschreiben. Da die Zählung bei Null beginnt, ist die größte Zahl zunächst 15. Das Hexadezimalsystem bedient sich zunächst der gleichen Ziffern wie das Dezimalsystem, als 0 bis 9. Danach werden Buchstaben verwendet: A=10, B=11, C=12, D=13, E=14 und F=15. Vielleicht hilft diese kleine Eselsbrücke: …, Cwölf, Dreizehn, …, Fünfzehn
Um bei Mehrdeutigkeit, z.B. 11, zu erkennen, um welches Zahlensystem es sich handelt, werden die Zahlen gekennzeichnet, z.B. wird BIN, DEZ oder HEX nachgestellt. In vielen Programmiersprachen erfolgt die Kennzeichnung der Binärzahlen durch ein vorangestelltes 0b (Null b), der Hexadezimalzahlen durch 0x (Null x).
Vergleich der Zahlensysteme:
DEZ |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
BIN |
0000 |
0001 |
0010 |
0011 |
0100 |
0101 |
0110 |
0111 |
HEX |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
DEZ |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
BIN |
1000 |
1001 |
1010 |
1011 |
1100 |
1101 |
1110 |
1111 |
HEX |
8 |
9 |
A |
B |
C |
D |
E |
F |
Stellt man die 8 Bit als Binärzahl in einer Reihe dar, steigt die Wertigkeit jedes Bits von einer Seite zur anderen um den Faktor 2.
Beispiel zur Umrechnung:
Wertigkeit dezimal |
128 |
64 |
32 |
16 |
8 |
4 |
2 |
1 |
Umgerechnete Dezimalzahl |
binär |
0 |
1 |
0 |
0 |
1 |
1 |
0 |
1 |
1+4+8+64 = 77 |
binär |
1 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
128+8+2 = 138 |
binär |
0 |
0 |
1 |
1 |
0 |
1 |
1 |
1 |
32+16+4+2+1 = 55 |
Man addiert einfach die Wertigkeit der Stellen, die mit 1 „gesetzt“ sind. So kann man sehr schnell von Binär in Dezimal umrechnen. Außerdem kann man hier bereits erkennen, ob es sich um eine gerade oder ungerade Dezimalzahl handelt, nämlich wenn das Bit rechts außen mit der Wertigkeit 1 gesetzt ist.
Wie wir gesehen haben, steigt die dezimale Wertigkeit einer Ziffer, je weiter links sie in der Ziffernfolge steht, um den Faktor 2, bei Hexadezimalzahlen um den Faktor 16. Die höchste Zahl, die man mit einem Byte darstellen kann, lautet 1111 1111 oder in Hexadezimal FF, umgerechnet mit F=15 ergibt das 16*15 + 1*15 = 255. Programmiererwitz mit Bart: Weihnachten ist 19 HEX (=25 DEZ).
Für die von uns meistgenutzte 16 Bit-Integerzahl werden zwei Bytes reserviert. Die höchste Zahl ist somit FF FF HEX = 16hoch3 * 15 + 16hoch2 * 15 + 16hoch1* 15 + 16hoch0 * 15 = 2hoch16 -1 = 65.535. Anstelle des Zahlenbereichs von 0 bis 65.535 wird bei der signed int-Zahl der Bereich von -32.768 bis + 32.767 verwendet.
Das ganz rechte Bit wird auch LSB = least significant bit genannt. Seine Wertigkeit beträgt 2 hoch 0 = 1. Wir merken uns: Gezählt werden die Bits üblicherweise (siehe folgende zwei Absätze) von rechts nach links und die Zählung beginnt bei 0. Das siebte Bit ist dann das ganz linke Bit, genannt MSB = most significant bit) und hat die Wertigkeit 2 hoch 7 = 128.
Wenn Sie schon über die Begriffe Big-Endian und Little-Endian gestolpert sind, hier kurz eine Erklärung: Im Wesentlichen geht es dabei um die serielle (bitweise) Übertragung von Daten.
Man spricht von Big-Endian, wenn das hochwertigste Bit eines Bytes zuerst übertragen wird (z.B. bei I²C), und von Little-Endian, wenn das niederwertigste Bit eines Bytes zuerst übertragen wird (z.B. bei RS-232).
Nun noch einmal zu dem Tipp, den ich im Blog-Beitrag zum Logic Analyzer schon gezeigt habe: die Verwendung des Windows-Rechners für die Umrechnung zwischen den Zahlensystemen. Man wählt links neben dem Wort Standard durch Klicken auf die drei waagerechten Streifen den Modus Programmierer und erhält das folgende Bild, bei dem die Dezimalzahl 255 zugleich in den anderen Zahlenformaten gezeigt wird.
Aber der Rechner kann noch mehr, als nur Zahlen in die verschiedenen Zahlensysteme umzurechnen: Der Programmierer möchte die Zahlen ggf. „manipulieren“, d.h. Rechenoperationen durchführen, um eine bestimmte Wirkung zu erzielen. Beispiele dazu werden wir später sehen.
Wenn man auf die Schaltfläche „Bitweise“ drückt, erhält man Zugang zu fünf Funktionen, mit denen man zwei Zahlen verknüpfen kann, sowie als sechsten Operator das NOT, mit dem eine Zahl bitweise negiert wird.
Die logischen Funktionen sind Bitweise AND, OR, NOT, NAND, NOR und XOR. Am besten nachvollziehbar ist die jeweilige Funktionsweise tatsächlich im Binärformat, da dort jedes Bit angezeigt wird. Nach jedem Beispiel werde ich die jeweilige C/C++-Funktion zeigen, die wir für das Gleiche in der Arduino IDE benutzen würden:
AND: Die Und-Funktion ergibt immer dann 1, wenn beide Zahlen an der Stelle eine 1 hatten.
Logik-Tabelle:
A |
B |
A & B |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
Beispiel: Ein beliebiger Registerinhalt soll so verändert werden, dass das fünfte Bit (von hinten gezählt) zu Null gesetzt wird. Dazu nutze ich die Und-Verknüpfung:
0111 1110 & 1110 1111 = 0110 1110
Quelle |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
& |
||||||||
Maske |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
Ergebnis |
0 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
Das nennt man im Sprachgebrauch „Bitmaske“. Auf diese Weise lassen sich einzelne Bits „manipulieren“, ohne die restlichen Bits zu verändern.
Das Zeichen in der Arduino IDE (C++) ist das kaufmännische Und-Zeichen (Ampersand, &)
Beispiel aus der Arduino-Hilfe für die direkte Portmanipulation, um die GPIOS per Register ohne Funktionsaufrufe zu konfigurieren:
PORTD = PORTD & B00000011; // Löschen der Bits 2 - 7 und Pins PD0 und PD1 (xx & 11 == xx) nicht ändern.
Sie kannten bisher das doppelte Zeichen &&? Das ist nicht die bitweise UND-Verknüpfung, sondern die Verknüpfung zweier logischer Ausdrücke, also das Logische UND aus der Booleschen Algebra, z.B. wenn gefragt wird, ob eine Zahl größer oder gleich Null ist UND kleiner als 10.
Codeschnipsel: if (zahl>=0 && zahl<10)
OR: Die Oder-Funktion ergibt immer dann 1, wenn eine der beiden Zahlen an der Stelle eine 1 hat.
Logik-Tabelle:
A |
B |
A | B |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
Beispiel: Ein beliebiger Registerinhalt soll so verändert werden, dass nur das fünfte Bit (von hinten gezählt) auf Eins gesetzt wird. Dazu nutze ich die Oder-Verknüpfung:
0110 1110 | 0001 0000 = 0111 1110
Quelle |
0 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
| |
||||||||
Maske |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
0 |
Ergebnis |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
Das Zeichen für den bitweise OR-Operator in C ++ ist das vertikale Balkensymbol |.
Auch dieses Zeichen kennen Sie ggf. schon in doppelter Ausführung ||. Erneut nicht der bitweise Vergleich, sondern die Oder-Verknüpfung zweier logischer Ausdrücke.
NOT: Die NOT-Funktion negiert den Wert, also aus 1 wird 0 und aus 0 wird 1, bestes Beispiel ist das zyklische Ein- und Ausschalten einer LED, indem man den aktuellen Zustand negiert. Aber hier bezieht es sich auf jedes einzelne Bit.
Der bitweise NOT-Operator in C ++ ist das Tilde-Zeichen ~. Im Gegensatz zu & und | wird der bitweise NOT-Operator auf einen einzelnen Operanden rechts davon angewendet.
Achtung: Bei dieser Funktion kann man abstruse Lösungen erhalten, denn je nach Länge des Eingabewertes werden die vorangestellten und nicht angezeigten Nullen zur Eins.
Im Windows-Rechner deshalb zunächst durch dreifachen Mausklick auf QWORD (es erscheint zunächst DWORD, dann WORD) auf Byte wechseln. Dann erhält man „nur“ acht Zeichen angezeigt, auch wenn man eine kleinere Anzahl eingegeben hat, siehe nachfolgendes Bild:
Das Beispiel auf der Arduino-Hilfe zeigt eine Integerzahl, bestehend aus zwei Bytes:
int a = 103; // Binär: 0000000001100111
int b = ~a; // Binär: 1111111110011000 = -104
Das höchste Bit in einer signed int-Variablen ist das sogenannte Vorzeichenbit. Wenn das höchste Bit 1 ist, wird die Zahl als negativ interpretiert. Diese Kodierung von positiven und negativen Zahlen wird als Zweierkomplement bezeichnet.
Um den Unterschied zwischen der bitweisen Negation zur Booleschen Algebra noch einmal hervorzuheben, hier der Vollständigkeit halber die Negation eines logischen Ausdrucks: Diese erfolgt in C++ mit dem Ausrufezeichen ! Beispielcode: if (!Bedingung) bedeutet: wenn die Bedingung nicht erfüllt ist.
NAND und NOR sind schnell erklärt: Am Ende der Und- bzw. Oder-Funktion werden die Werte negiert, also NAND = NOT AND und NOR = NOT OR.
XOR, das EXCLUSIVE ODER, ist noch einmal ein interessanter Operator: Eine bitweise XOR-Operation führt nur dann zu einer 1, wenn die Eingangsbits unterschiedlich sind. Andernfalls ergibt sich eine 0.
In C++ wird der bitweise XOR-Operator mit dem Caret-Symbol ^ (auch Zirkumflex genannt) geschrieben.
Logik-Tabelle:
A |
B |
A ^ B |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
Bitverschiebung: Die Shift-Operatoren bewirken, dass die Bits entweder nach links oder rechts verschoben werden. Der linke Operator ist dabei die Quellvariable, der rechte Operator gibt die Weite an, um wie viele Stellen verschoben wird. Im Windows-Rechner erfolgt die Verschiebung durch mehrfaches Drücken der entsprechenden Taste.
Im nachfolgenden Bild wird im Windows-Rechner zunächst die zyklische Verschiebung, also die Rotation sämtlicher acht Bits (auch der nicht angezeigten voranstehenden Nullen) angezeigt. Im rechten Bild hatte ich die Binärzahl 0001 1001 (=25 DEZ = 19 HEX) eingegeben und einmal die Verschiebung nach links << angeklickt.
Wichtiger Hinweis: Diese Verschiebung um ein Bit nach links entspricht der Multiplikation mit 2
Interessanterweise gibt es weitere Funktionen für Bitoperationen, die in der Arduino-Referenz angezeigt werden:
Bits und Bytes
bit() Berechnet den Wert des angegebenen Bits (Bit 0 ist 1, Bit 1 ist 2, Bit 2 ist 4, etc.).
Syntax bit(n)
bitClear() Löscht den Wert (Schreibt 0) eines Bits einer Zahlen-Variable.
Syntax bitClear(x, n)
bitRead() Liest ein Bit aus einer Zahlenvariable.
Syntax bitRead(x, n)
bitSet() Setzt (Schreibt eine 1) ein Bit einer Zahlenvariable.
Syntax bitSet(x, n)
bitWrite() Schreibt ein Bit (0 oder 1) einer Zahlenvariable an der n-ten Stelle.
Syntax bitWrite(x, n, b)
highByte() Liest das most-significant (linke) Byte eines Datenwortes (oder das zweitkleinste Byte eines größeren Datentypes).
Syntax highByte(x)
lowByte() Liest das least-significant (rechte) Byte eines Datenwortes oder größeren Datentypes. Syntax lowByte(x)
Als zusammenfassendes Beispiel möchte ich die Funktionsweise des ICs SN74HC595N erklären, der besser bekannt ist als „Schieberegister“.
In einem Beispiel im Internet wird gezeigt, wie acht LEDs individuell angesteuert werden, ohne dafür auch acht Ausgänge des Micro Controllers zu benutzen, sondern lediglich drei. Und das Schöne ist: Man kann diesen Baustein kaskadieren, d.h. hintereinanderschalten, und benötigt weiterhin nur die drei Anschlüsse. Man erhält auf diese Weise also 16 oder sogar 24 Ausgänge. Die Anzahl der hintereinander geschalteten ICs wird nur limitiert durch Übertragungsrate und Taktfrequenz.
Die drei wesentlichen Eingänge am IC heißen meist DS (=Data Serial), SHCP (=Shift Register Clock Pin) und STCP (=STORE Register Clock Pin), doch bevor ich die Funktionen dieser drei Pins erkläre, hier zunächst das Schaltbild des DIP-16 ICs und die abweichenden Bezeichnungen gemäß Datenblatt:
Herzstück des ICs sind zwei Register (=Speicherplätze), das Schieberegister (shift register), das die 8 Bits Daten seriell empfängt, und das Speicherregister (storage register), das die Daten übernimmt und an den Ausgängen bereitstellt. Die Datenübergabe vom Micro Controller an den SN74HC595 erfolgt wie gesagt seriell am Pin DS, getaktet bei jeder positiven Flanke der Shift Register Clock an SRCP. Wenn alle Bits in das Schiebe- oder Shift Register übertragen sind, sorgt eine positive Flanke an der Storage Register Clock (STCP) dafür, dass alle Bits gleichzeitig an das Speicherregister übertragen werden.
Wenn der Anschluss OE (=Output Enable) – der Querstrich oberhalb der Buchstaben steht für die negative Logik – an Masse liegt, werden die Daten ausgegeben, wenn HIGH an OE anliegt, sind die Ausgänge im „high-impedance state“. (Anmerkung: Der hochohmige Zustand wird von einem Bauelement ausgegeben, wenn dieses keine aktive Eingabe hat. Bei digitalen Schaltungen bedeutet dies, dass das Ausgangssignal weder logisch 0 noch 1 ist, sondern hochohmig. Ein solches Signal sorgt dafür, dass sich das Bauelement verhält, als wäre sein Ausgang temporär von der Schaltung abgetrennt.) Für die normale Ausgabe legen wir diesen Pin dauerhaft an GND.
Auch der Pin 10 MR=Master Reset (SRCLR=Shift Register Clear) hat negative Logik. Deshalb legen wir diesen Anschluss auf VCC, um ein Reset auszuschließen.
Die Spannungsversorgung erfolgt an Pin 16 für VCC (3,3 oder 5V) und an Pin 8 für GND.
Die Ausgänge heißen Q0 bis Q7 (Pins 15 und 1 bis 7).
Der letzte noch nicht angesprochene Pin 9 (Q7S) ist der Ausgang für die Kaskadierung. Hier wird DS des nächsten ICs angeschlossen.
Mit der folgenden Schaltung und dem zugehörigen Sketch wird eine im Serial Monitor eingegebene Integerzahl (signed int, zwischen -32768 und +32767) mit Hilfe von 16 LEDs als Dualzahl angezeigt:
Aus Gründen der Übersichtlichkeit haben ich die Verbindungen der Ausgänge zu den Anoden der LEDs weggelassen. Bitte verbinden Sie die LEDs von links nach rechts mit Q0 bis Q7
Explizit möchte ich noch einmal auf die grüne Leitung vom linken Ausgang Q7S zum rechten Eingang DS hinweisen. Hier erfolgt die Kaskadierung.
// This sketch displays a given decimal number (Integer)
// in binary format in 16 LEDs connected to two SN74HC595N
// Arduino-Pin connected to SHCP of 74HC595
int shiftPin = 8;
// Arduino-Pin connected to STCP of 74HC595
int storePin = 9;
// Arduino-Pin connected to DS of 74HC595
int dataPin = 10;
// Diese Zahl soll eingelesen und als Dualzahl ausgegeben werden
int decimal;
void setup() {
// initialize serial communication at 9600 bits per second:
Serial.begin(9600);
// define Pins 8,9,10 as outputs
pinMode(storePin, OUTPUT);
pinMode(shiftPin, OUTPUT);
pinMode(dataPin, OUTPUT);
}
void loop () {
// important note: select "no line ending" = "kein Zeilenende"
// at the bottom of the Serial Monitor.
// Enter your number in the top line of the Serial Monitor
if (Serial.available() > 0) {
// read the incoming byte:
decimal=Serial.parseInt();
// uncomment for display in the Serial Monitor
// Serial.print(decimal);
// Serial.println();
// for (int i=0; i<16;i++) {
// Serial.print(bitRead(decimal,i));
}
// storePin to LOW
digitalWrite(storePin, LOW);
for (int i=0; i<16; i++) {
// start with all 3 Pins LOW
// data transfer (1 bit) at the change from LOW to HIGH (positive-edge triggered)
digitalWrite(shiftPin, LOW);
// write value of the respective bit to DS pin
digitalWrite(dataPin, bitRead(decimal,i));
// ShiftPin SHCP from LOW to HIGH for data transfer to the sift register
digitalWrite(shiftPin, HIGH);
}
// When all 16 bis are in shift registers put the StorePin STCP
// from LOW to HIGH, in order to copy all 18 bits
// from the shift registers to the storage registers for output
digitalWrite(storePin, HIGH);
}
Geben Sie die Dezimalzahl, die Sie als Dualzahl angezeigt bekommen möchten, in der obersten Zeile des Seriellen Monitors ein und drücken Sie Enter oder klicken auf Senden. Nach kurzer Pause sehen Sie die Anzeige an den LEDs. Probieren Sie auch einmal negative Zahlen.
Dieser IC oder funktionsgleiche Integrierte Schaltungen kommen häufig vor, wenn Daten seriell übertragen und parallel (zeitgleich) ausgegeben werden sollen, z.B. bei den LED- oder LCD-Displays. Außerdem wird der SN74HC595N beim Motor-Controller-Shield benutzt, um trotz begrenzter Anzahl an Ausgängen des Micro Controllers zwei Motor-ICs L293D und damit vier Motoren gleichzeitig zu betreiben.
Ein anderer IC, an dem man das Auslesen, Beschreiben und Manipulieren von Registerinhalten gut üben kann, ist der Port Expander MCP23017, zu dem ich im Sommer 2020 einen dreiteiligen Blog geschrieben hatte (Teil 1, Teil 2, Teil 3).
4 commentaires
Andreas Wolter
@Bernd-Steffen Großmann: vielen Dank für diesen Hinweis. Wir haben die Abbildung des Schaltplans ausgetauscht.
Grüße,
Andreas Wolter
AZ-Delivery Blog
Bernd-Steffen Großmann
Hallo Herr Wolter, Sie haben uns da eine ziemliche Nuss zu knacken gegeben! Der Versuchsaufbau machte alles möglichen, nur nicht was er sollte! Ich hab zuerst an mir und dann am IC 74HC595 gezweifelt bzw. am Fernost-Lieferanten. Sie haben im Sketch den Schiebe-Takt (SHCP=Shift Register Clock Pin) an Pin 8 und Dateneingang an Pin 10 vom Arduino festgelegt. In der Schaltung ist es genau anders herum gezeichnet! Böse Falle…
Viele Grüße, Bernd-Steffen Großmann
Andreas Wolter
@Frant: das ist kein Fehler, denn die Wertigkeit steigt von rechts nach links pro Ziffer um den Faktor 2, also jedes mal verdoppelt. Faktor 10 wäre pro Stelle mit 10 multipliziert, was eine Folge von 1, 10, 100, 1000, … etc. ergeben würde. Ich habe die Wortwahl trotzdem etwas angepasst, um Missverständnisse zu vermeiden.
Grüße,
Andreas Wolter
AZ-Delivery Blog
Frant
!!! Fehler!!!
Wie wir gesehen haben, steigt bei den Dezimalzahlen die Wertigkeit einer Ziffer, je weiter links sie in der Ziffernfolge steht, um den Faktor 2, bei Hexadezimalzahlen um den Faktor 16.
richtig ( Faktor 10 statt 2)
Wie wir gesehen haben, steigt bei den Dezimalzahlen die Wertigkeit einer Ziffer, je weiter links sie in der Ziffernfolge steht, um den Faktor 10, bei Hexadezimalzahlen um den Faktor 16.