Mit dem 74HC165-Schieberegister Eingabe-Pins und Leitungen einsparen

In meinem Artikel Mit dem 74HC595 Steuerleitungen einsparen hatte ich erklärt, wie man den 74HC595-Schieberegister-Chip benutzt, um mit nur drei Leitungen bzw. Output-Pins die 8 LEDs einer 7-Segment-Anzeige zu steuern.

Der 74HC595 wird verwendet, wenn man Output-Pins einparen will. Er wird auch Serial In Parallel Out (kurz SIPO) Shift Register genannt. Das heißt, wir schieben pro Chip (man kann diese kaskadieren) 8 Bits über 3 Leitungen (Data in, Shift, Store) hinein, um zum Beispiel 8 an den Chip angeschlossene LEDs einzeln anzusteuern. Die einzelnen Bits geben an, welche LED auf High oder Low gehen (eingeschaltet werden) sollen.

Der 74HC165 ist das Gegenstück zum 74HC595. Er wird nämlich verwendet, wenn man Input-Pins einsparen will. Dementsprechend heißt er auch Parallel In Serial Out (kurz PISO) Shift Register. Hier schließen wir z. B. acht Taster an den Chip an und erhalten dann mit vier Leitungen eine Bit-Sequenz, die angibt, ob die Taster auf Low oder High (sprich gedrückt) sind.

Wo wir dem 74HC595 eine Bit-Sequenz geschickt haben, spielen wir diesmal die andere, die Empfangsseite und bekommen vom 74HC165 eine Bit-Sequenz.

Dazu steuern wir den 74HC165 mit den Steuerleitungen SH/LD, CLK und CLK INH in einem bestimmten Protokoll. Doch schauen wir uns erst einmal die Pinblegung des 74HC165N näher an.

74HC165N Pinout

Pin Nr. I/O Beschreibung A 11 I Parallel Input B 12 I Parallel Input C 13 I Parallel Input D 14 I Parallel Input E 3 I Parallel Input F 4 I Parallel Input G 5 I Parallel Input H 6 I Parallel Input CLK 2 I Clock input CLK INH 15 I Clock Inhibit, when High No change in output QH 9 O Serial Output QH 7 O Complementary Serial Output SER 10 I Serial Input SH/LD 1 I Shift or Load input. When High Data, shifted. When Low data is loaded from parallel inputs. VCC 16 — Power Pin (2-7 V DC) GND 8 — Ground Pin An A bis H schließen wir unsere (bis zu) 8 Taster an. SH/LD, CLK und CLK INH sind Eingänge für den HC165 und Ausgänge am Microcontoller, mit dem wir den HC165 steuern. Der gibt uns dann über seinen Ausgang QH eine serielle Sequenz der Schaltzustände an A bis H zurück:



74HC165 Protokoll

Wie die Steuerleitungen im einzelnen gesetzt und die Bits abgerufen werden müssen, regelt wieder ein Protokoll, das dem Datenblatt zu entnehmen ist:



Im Anfangszustand ist CLK INH auf High und Ser auf Low. SH/LD wird einmal auf Low, High gesetzt. Clock wird in regelmäßigen Abständen abwechselnd auf High und Low geschaltet. Danach werden dann die einzelnen Bits ausgelesen.

Das resultiert dann zu folgendem Code digitalWrite(PinShift, LOW); delayMicroseconds(HC165_pulseLength); digitalWrite(PinShift, HIGH); delayMicroseconds(HC165_pulseLength); // Anfangs-Status lt. Datenblatt digitalWrite(PinClockInh, HIGH); digitalWrite(PinClock, LOW); // Clock ein // die 8 Zustände A...H einlesen byte states = shiftIn(PinData, PinClockInh, MSBFIRST); digitalWrite(PinClock, HIGH); // Clock aus Um das genaue Timing beim Auslesen der einzelnen Bits brauchen wir uns nicht selbst zu kümmern, wie wir es beim HC595 auf dem Raspberry Pi noch getan hatten. Denn auf dem Arduino gibt es dafür den Befehl shiftIn.

Arduinon Befehl ShiftIn

Die Arduino Reference zu shiftIn erklärt shiftIn wie folgt:
shiftIn()

Beschreibung
Shiftet ein Byte von Daten, ein Bit pro Zeiteinheit. Beginnt entweder mit dem most significant bit (linkestes Bit) oder dem least significant bit (rechtestes Bit). Für jedes Bit wird der clock pin HIGH geschalten. Anschließend wird das nächste Bit gelesen und der clock pin auf LOW gesetzt.

Wenn du mit einem Gerät arbeitest, das bei steigender Flanke die clock auslöst, musst du darauf achten, dass der clock pin LOW ist, bevor das erste Mal shiftIn() aufgerufen wird. Das ist z.B. möglich mit einem Aufruf von digitalWrite(clockPin, LOW).

Bemerkung: Das ist eine Softwareimplementierung; Arduino bietet auch eine SPI-Bibliothek an, die eine Hardwareimplementierung benutzt. Diese ist zwar schneller, funktioniert aber nur mit spezifischen Pins.

Syntax
byte incoming = shiftIn(dataPin, clockPin, bitOrder)

Parameter
dataPin: Der Pin, auf dem jedes Bit eingegeben wird. Erlaubte Datentypen: int.
clockPin: Der Pin, auf dem das Signal gesetzt wird, um den dataPin zu lesen
bitOrder: Welche Ordnung verwendet werden soll, um zu shiften. Entweder ist dies MSBFIRST oder LSBFIRST. (Most Significant Bit First oder Least Significant Bit First)

Rückgabewert
Den gelesenen Wert. Datentyp: byte.
Das Schalten der High- und Low-Zustände übernimmt also shiftIn für uns. Wir müssen nur die Pins eingeben, von welcher Seite wir die Zustände lesen wollen und bekommen dann ein Byte mit den 8 Zuständen in dessen 8 Bits wieder.

Die Schaltung




Als Mikrocontroller habe ich die BluePill mit STM32 gewählt, denn ich habe vor, die Schaltung später in einem 8fach-7-Segment-Timer aufgehen zu lassen. Der soll neben Stoppuhr und Countdown-Timer vielleicht auch als Uhr dienen und da ist die batteriebufferbare Echtzeituhr der BluePill ganz nützlich - so denn man kein gefälschtes Exemplar angedreht bekommen hat.

Für die Ansteuerung habe ich PA4 bis PA7 gewählt. Eigentlich sind die Pins egal, aber ich hatte vorher die Library MorePins bzw. MoreInputs ausprobiert, die diese Pins wollte und vielversprechend aussah in Hinsicht auf einfache Abfrage der am HC165er hängender Taster. Laut Visual Code / Platform IO sollte sie auch mit dem STM32 kompatibel sein. Ich habe sie aber ums Verrecken nicht kompiliert bekommen. Also bin ich bei der Fehlersuche nochmal auf die Arduino IDE gegangen und habe dort gesehen, dass sie dort auch nicht mit dem STM32 zusammen will. Für den Arduino Nano ließ sie sich hingegen fehlerfrei kompilieren.

Ende vom Lied: viel zum Fenster rausgeworfene zeit, weil ich der Kompatibilitätsangabe vertraut habe. Und dann kam die Erkenntnis, dass ich in der Zeit schon selbst was programmiert hätte und dabei auch noch Spaß gehabt hätte.


Nachdem ich einen Schwung SSD1306-128x64-OLEDs bestellt und bekommen hatte, habe ich viele meiner Entwicklungs-Breadboards mit einem ausgestattet, weil das einfach einfacher ist mit dem Anzeigen von Ergebnissen. Sonst muss man immer auf den Serial Monitor schauen. Auch bei Demo-Videos ist das natürlich von Vorteil, weil man dann nur noch das Breadboard abfilmen muss.

Das OLED hängt am I2C-Bus und ist mit 2 Leitungen an PB7 und PB8 angeschlossen. Für eine genaue Anleitung zum Anschluss siehe meinen Artikel OLED-Display (128x64 px, 0.96") an STM32 betreiben. Obwohl ich diesmal nicht die Adafruit-Library verwende und auch nicht die von ThingPulse, wie ich sie letztens am ESP32 benutzte, sondern die U8x8lib von OliKraus. Und zwar, weil diese auch in VS Code/PIO verfügbar ist. Für Textausgabe ist sie sehr gut geeignet und sie bringt nette Fonts mit. Mit Grafik ist allerdings eher mau.


Aus China habe ich für einen Spottpreis - so wie es aussieht - "originale" 74HC165N-Schieberegister von Texas Instruments geschickt bekommen. Aber normalerweise weiß man nicht so genau, was man bekommt, wenn man nicht bei einem großen, vertrauenswürdigen (und meist teureren oder nur für Gewerbekunden zugelassenen) Elektronikhändler bestellt.

Die Verkabelung ist die folgende: Bezeichnung BluePill 74HC165 Farbe Shift PA4 1 SH/LD rot Clock PA5 2 CLK blau Data PA6 7 QH schwarz ClockInh PA7 15 CLK INH weiß 3.3 Volt 3.3V 16 VCC Ground GND 8 GND Taster A rechts A Taster B rechts B Taster C rechts C
Fehlen noch die Taster. Ich verwende eine Pullup-Schaltung, wie ich sie schon in meinem Artikel zum Senso Spiel auf dem Raspberry Pi erklärt habe.

Hier hängen bei einer Pulldown-Schaltung die rechten Anschlüsse der Taster (die beiden rechts sowieso die beiden linken sind kurzgeschlossen) an den Eingängen des 165er und außerdem über einen 10 kΩ-Widerstand an GND. Die linken Anschlüsse hängen alle an 3.3V.

Oder bei einer Pullup-Schaltung hängen die rechten Anschlüsse an den Eingängen des 165er und außerdem über einen 10 kΩ-Widerstand an 3.3V. Die linken Anschlüsse hängen dementsprechend alle an GND.

Welche Pull-Schaltung verwendet wird, bleibt Frage des eigenen Geschmacks oder was sich auf einer Platine einfacher verdrahten lässt. Ein einfaches binäres NOT ändert die Zustandreihe der Bits in der Software. Von daher ist es eigentlich so gut wie egal.

Wichtig ist nur, dass man die Spannung in einen definierten Zustand (eben Low oder High) zieht. Denn ansonsten kippen die Bits in ungedrücktem Zustand fröhlich zwischen Low und High, ähnlich wie einem Fähnchen, das in der Luft hängt und herumflattert.

Das gilt übrigens auch für die ungenutzten Eingänge, in unserer Schaltung D bis H. Aber diese müssen wir nicht auf High oder Low ziehen, weil wir ja wissen, dass wir diese nicht angeschlossen haben und einfach ausblenden. Das geht mit ein paar ShiftLeft und ShiftRight-Befehlen, die eigentlich jeder Prozessor als direkte und damit überaus schnelle Maschinensprachenbefehle im Petto hat.

Die Firmware

Unsere Firmware soll nichts anderes tun, als die Zustände der drei Taster alle paar Millisekunden abzufragen und dann deren Zustände auf dem OLED und über den Serial Monitor sichtbar zu machen.

Habt ihr kein OLED, dann schmeißt die OLED-Befehle einfach raus und benutzt nur die Serial.print-Befehle.

Der Source-Code lässt sich sowohl unter der Arduino IDE als unter der VS Code / Platform IO komplieren und sowohl für Arduino (Uno, Nano), als auch für den STM32.

Für den Arduino müssen in der defines.h halt die Pin-Namen angepasst werden.

defines.h #define PinShift PA4 #define PinClock PA5 #define PinData PA6 #define PinClockInh PA7 #define HC165_pulseLength 5

main.cpp bzw. 74HC165-Demo.ino //////////////////////////////////////////////////////// // (C) 2020 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// #include <Arduino.h> #include <U8x8lib.h> // OLED https://github.com/olikraus/u8g2/wiki/u8x8reference // fontlist: https://github.com/olikraus/u8g2/wiki/fntlist8x8 #include "defines.h" U8X8_SSD1306_128X64_NONAME_HW_I2C oled(/* reset=*/ U8X8_PIN_NONE); void setup() { Serial.begin(115200); pinMode(LED_BUILTIN, OUTPUT); pinMode(PinData, INPUT); pinMode(PinClock, OUTPUT); pinMode(PinClockInh, OUTPUT); pinMode(PinShift, OUTPUT); oled.begin(); // OLED starten oled.clearDisplay(); // OLED löschen oled.setFont(u8x8_font_inb46_4x8_r); // großen Font setzen } void pulseShift() { // führt eine kurzen Pulse vor dem Auslesen der Bit-States des HC165 auf der Shift-Leitung aus digitalWrite(PinShift, LOW); delayMicroseconds(HC165_pulseLength); digitalWrite(PinShift, HIGH); delayMicroseconds(HC165_pulseLength); } void shiftRegisterRead(int anz165er, byte *states) { // anz165er=Anzahl Chips, normal 1, mehr, wenn kaskadiert. pulseShift(); // Anfangs-Status lt. Datenblatt digitalWrite(PinClockInh, HIGH); digitalWrite(PinClock, LOW); // Clock ein for (int i=0; i < anz165er; i++) { // die 8 Zustände A...H einlesen states[i] = shiftIn(PinData, PinClockInh, MSBFIRST); } digitalWrite(PinClock, HIGH); // Clock aus } void loop() { byte states[1]; while (1) { shiftRegisterRead(1, states); // states[0] = ~states[0]; // falls Pulldown: Bits umdrehen: gedrückte Tasten als 1 und ungedrückte als 0 // mich interessieren nur die untersten 3 Bits, die nicht angeschlossenenen 5 flattern in der Luft zwischen High und Low hin und her states[0] <<= 5; states[0] >>= 5; Serial.println(states[0], BIN); oled.drawString(0,0, states[0] & 4 ? "X":"_"); oled.drawString(6,0, states[0] & 2 ? "X":"_"); oled.drawString(12,0, states[0] & 1 ? "X":"_"); digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN)); delay (50); } } Die Funktion shiftRegisterRead ist schon darauf vorbereitet, mit kaskadierten HC165ern zusammenzuarbeiten. In meinem Timer-Projekt sollen 24 Taster und somit drei 165er zum Einsatz kommen (acht über und acht unter den Siebensegmentanzeigen zum Einstellen des Countdowns und 8 weitere Funktionstasten) und ich möchte diese alle über die vier Leitungen abfragen und nicht noch extra Leitungen stecken müssen. Wenn ich dann 3 als Parameter übergeben (und natürlich ein entsprechend größeres Array habe), dann landen in states[0] bis states[2] die 24 Zustände. Denke ich zumindest, denn ausgetestet habe ich das noch nicht. Aber jetzt will ich erst einmal auf einer Lochrasterplatine weitermachen, die Software lässt sich dann ja immer noch anpassen.

Der Rest des Codes wurde bereits erklärt oder erklärt sich durch die eingefügten Kommentare von selbst.

Demonstrations-Video

Wie das Ganze aussieht und funktioniert, sieht man in diesem Video. Außerdem verliere ich noch ein paar Worte zu der Schaltung:



Das Projekt werde ich dann, wie gesagt zu einem Timer mit noch viel mehr Tastern ausbauen.

Man sieht, mit den günstigen Schieberegistern kann man auf einfache und günstige Weise viele Leitungen einsparen, denn davon sind auch auf dem besten Mikrocontroller nur begrenzt viele vorhanden. Mit dem 74HC595N steuert man pro Chip bis zu 8 LEDs an und mit dem 74HC165N fragt man pro Chip bis zu 8 Taster ab. Zudem lassen sich die Chips eines Types hintereinander hängen (kaskadieren) und so noch mehr Komponenten steuern, ohne mehr Leitungen zu benötigen.

74HC595 Wie man mehrere 165er hintereinander schaltet, um mehr als 8 Taster zu verwalten, erkläre ich euch in meinem Artikel 74HC165-Schieberegister kaskadieren und mehr als 8 Taster verwalten