PCF8574

Aus TippvomTibb
Zur Navigation springen Zur Suche springen

Allgemeines

Als Porterweiterung für Microcontroller ist der PCF8574 mir seiner Anbindung ueber I2C schon ein betagter Geselle (80er). Fuer Arduino&Co aber nach wie vor sehr nuetzlich. Diese Seite dient mir dazu, mir nochmal ueber die Anbindung von Tastaturen, Displays oder auch Relais Klarheit zu verschaffen. Der nachfolgende Artikel aus den 8zigern von ELV diente mir damals dazu mich genauer mit dem I2C-Busprotokoll auseinanderzusetzen. Heute liegt mein Schwerpunkt eher auf der Sondierung der ueber 48 gezaehlten Libraries mit PCF8574-Unterstuetzung des Arduino-Frameworks. Bevor man jedoch die Software beurteilen kann, sollte man sich ueber die Hardwaremoeglichkeiten im Klaren sein. Was bei dem PCF8574 noch recht einfach ist. Ein entscheidendes Kriterium zur Auswahl einer Library ist neben der Groesze, der Kompatibilitaet vor allem die Unterstuetzung der Interrupt-Faehigkeit (/INT).

ELV PCF8574P.jpg

Das Modul war ein Teil dieses Bausatzes

Hardware

Den Einstieg mache ich mit der Portimplementierung, die sich ein wenig von der von Mikrocontrollernbekannten unterscheidet.

Laut Datenblatt betraegt die maximale I²C Taktrate 100kHz fuer den PCF und 400 kHz fuer den PCA, was dem Standard entspricht.

Die Spannungsversorgung kann zwischen 2,5 V und 6 V liegen, was den Betrieb z. B. am Raspi mit seinen 3,3V direkt ohne Level-Shifter moeglich macht.

Was mir auch aufgefallen ist betrifft den Umstand, dass die wenigstens Implementationen den /INT-Pin (active LOW open-drain interrupt output) benutzen/auswerten. Ein Interrupt wird ausgeloest sobald der Status am Eingangspin sich vom Inputport Register Status unterscheidet.

Die Port-Pins sind zustandsgesteuerte Flipflops (Latches) und man kann an sie eine LED z.B. direkt anschlieszen. Die Gesamtstrom-"Belastung" darf dabei aber 80 mA nicht ueberschreiten.

Kapitel 8 des Datenblattes gibt folgendes an:

8 I/O programming

8.1 Quasi-bidirectional I/Os

Quasi weist auf den Umstand hin, dass die Ports zwar als Eingang und Ausgang dienen können , es aber Einschränkungen/Besonderheiten zu beachten gilt.

Es gibt z.B. nicht wie von Mikrocontrollern gewohnt, ein Direction Control Register (DCR). Bei einem Lesezugriff auf einen Port (Anm.: Mit Port ist zwar hier ein Port-Pin gemeint, eigentlich liest oder schreibt man aber immer alle 8 Port-Pins zusammen.) erhaelt man den Status des Eingangs-FlipFlops.

Eine weitere wichtige Information betrifft das Einschaltverhalten. Sobald der IS mit Spannung versorgt wird gehen alle Ports auf HIGH und als INPUTS und die 100uA Stromquelle ist eingeschaltet.

PCF8574 Port intern.png

Das obere FLipFlop uebernimmt und speichert den Zustand im SHift-Register bei einem Schreibzugriff.

Das untere FlipFlop uebernimmt und speicher den Zustand des Port-Pins bei einem Schreib- oder Lesezugriff!

Nach einem Reset (Power on) sind beide FlipFlops im Zustand Q = 1.

Die Anreicherungstypen (MOSFETs) sind bei UGS = 0 V gesperrt (selbstsperrend) ID = 0 A.

Das heiszt der obere pMOSFET (Itrt(pu)) ist bei einem Schreibimpuls und 1-Zustand des Output-FF gesperrt. In allen anderen Faellen ist er leitend.

Ist das Ausgangs-FF im 1-Zustand sind die Ausgangs-MOSFET gesperrt.

Ist das Ausgangs-FF im 0-Zustand sind die Ausgangs-MOSFET beide leitend. Ein (/INT) Interrupt wird ausgeloest wenn wenn sich der Zusatnd am Port-Pin vom Zusatnd des Eingangs-FF unterscheidet, d.h. der Zustand wurde noch nicht per Lesezugriff ins Eingangs-FF uebernommen. Der Interrupt wird also durch einen Lese- oder Schreibzugriff zurueckgesetzt. Kehrt der Zustand des Port-Pins zum "alten" bekannten Zustand zurueck bevor der Mikrocontroller reagiert, geht der Interrupt auch wieder in den unausgeloesten Zustand zurueck.

An einem Port-Pin kann man 4 verschiedene Zustaende unterscheiden:

INPUT HIGH/LOW: Man muss eine 1 an den Port-Pin uebertragen. Das ist der "Normalzustand" nach einem PowerOn (Reset). MOSFETs gesperrt.
Durch einen Lesezugriff wird der Zustand (VDD oder VSS) des Port-Pins gelesen.
Ensure a logic 1 is written for any port that is being used as an input to ensure the strong external pull-down is turned off.
Die Zuordnung von LOW und HIGH geschieht LOW < 0,3 * VDD und HIGH > 0,7 * VDD.
VDD = 5 V    LOW < 1,5 V HIGH > 3,5 V
VDD = 3,3 V  LOW < 0,99 V HIGH > 2,31 V
OUTPUT HIGH: Man muss eine 1 an den Port-Pin uebertragen.
OUTPUT LOW: Man muss eine 0 an den Port-Pin uebertragen. Hierbei ist zu beachten, dass nicht gleichzeitig der PIN auf VDD als INPUT gelegt wird.
If a LOW is written, the strong pull-down turns on and stays on. If a 
HIGH is written, the strong pull-up turns on for 1⁄2 of the clock cycle, then the line is held 
HIGH by the weak current source.

Programmierung

C

An der Stelle nutze ich die Gelegenheit mir nochmal die geliebten Zeiger(-Operationen) zu vergegenwaertigen. Eigentlich will ich mir nur noch mal im Klaren werden, ob in main() char *argv[] oder char **argv 'besser' ist.

Ein Zeiger ist eine Variable, in der sich die Adresse eines Datenobjektes oder einer Funktion befindet.

Um die Adresslaenge (8, 16, 32, 64, 96) herauszubekommen dient z. B. folgendes kleine Programm.

 1 #include "stdio.h"
 2 int main()
 3 {
 4    int array[5];
 5   
 6    /* If %p is new to you, you can use %d as well */
 7    printf("array=%p : &array=%p\n", array, &array); 
 8   
 9    printf("array+1 = %p : &array + 1 = %p\n", array+1, &array+1);
10   
11    return 0;
12 }

Der Pointer array zeigt auf das erste Element des Integer-Arrays. Somit haben &array[0] und array den gleichen Inhalt.

Auf dem Raspi ergibt sich eine Integerlaenge zu 4 Byte.

Die Adresse von array (&array) zeigt auf das ganze Array (5 x 4 Byte).

Die Notation Datentyp* zeiger finde ich eingaengiger als Datentyp *zeiger. Da beides zulaessig ist bevorzuge ich das Sternchen beim Datentyp.

Das folgende Beispiel zeigt warum:

1 int x, *px = &x;
2 int *py; py = px;

Hier funktioniert * als "Inhalt von" und & als "Adresse von" zu lesen irgendwie nicht mehr so richtig. Die weiteren Dereferenzierungsoperatoren (. und ->) lasse ich hier mal auszer Acht.

Wenn ich es wie foglt schreibe dagegen schon:

1 int x
2 int* px = &x;
3 int* py; 
4 py = px;

Der Progammcode ist identisch, die zweite Variante halte ich allerdings fuer besser fassbar.

Zeiger sind vom gleichen Datentyp wie der Datentyp auf den sie zeigen.  
1 const int* pointertoconst  // Zeiger auf const int; der Zeiger kann veraendert werden seine Objekte nicht
2 int* const constpointer // const Zeiger auf int; *constpointer=10; erlaubt, aber constpointer = &intvariable nicht

Weil ich gerade so am Probieren war hab ich das Testprogramm noch ein wenig erweitert.

 1 #include "stdio.h"
 2 #include "stdint.h" // wird fuer die Extended Variable Types gebraucht
 3 
 4 int main()
 5 {
 6 //   int array[5] = {0};                        // alle Werte werden zu 0 initialisiert
 7    int array[5]={0,1,2};                        // erlaubt; der Rest der Elemente wird mit 0 initialisiert
 8 
 9 
10    uint8_t i;                                   // Extended Variable Type; liest sich logischer als unsigend char i
11 
12    int len = sizeof(array) / sizeof(array[0]);  // Anzahl der Elemente eines Array ermittlen
13 
14    #define NUM(a) (sizeof(a) / sizeof(*a))      // eleganter Weg die Anzahl der Elemente eines Array zu ermittlen
15                                                 // damit koennte man in der for-Schleife anstelle von len schoener NUM(array) schreiben
16 
17    /* If %p is new to you, you can use %d as well */
18    printf("array=%p : &array=%p\n", array, &array);
19 
20    printf("array=%x : &array=%x\n", array, &array);
21 
22    printf("array+1 = %p : &array + 1 = %p\n", array+1, &array+1);
23 
24    printf("array+1 = %x : &array + 1 = %x\n", array+1, &array+1);
25 
26    // index bei 5 Elementen von 0 bis 4
27 
28    for (i=0;i<len;i++) {
29         printf("%x : %p\n",array[i],&array[i]);
30    }
31    return 0;
32 }

Nun zur eigentlichen Herausforderung, die Bedeutung der doppelten Dereferenzierung:

char** array;

Eine haeufig angewendete Naeherung geschieht ueber ein String array D‑':

1 char* myStrings[] = {
2     "Hello",
3     "World"
4 };

Der Grund ist naheliegend. Es gibt keinen Datentyp String in C. Stattdessen arbeitet man mit NULL-terminierten Character-Ketten.

Den gleichen Effekt wie das voran gegangene Beispiel erhaelt man auch mit:

 1 #include "stdio.h"
 2 
 3 int main(){
 4 
 5 char hello[]={'H','e','l','l','o','\0'};
 6 char world[]={'W','o','r','l','d','\0'};
 7 
 8 char* myStrings[]={hello,world};
 9 
10 // oder !!!
11 
12 char* myStrings[2];
13 
14 myStrings[0] = &hello[0]; // hier koente man auch hello schreiben
15 myStrings[1] = &world[0];
16 
17 printf("%s %s\n",myStrings[0],myStrings[1]);
18 
19 return 0;
20 }

Vielleicht ist das ein Grund warum recht viele bei C den Riemen runterwerfen. Ich halte C allerdings, abgesehen von Assembler, fuer einen Informatiker mit physikalisch-elektrischen Wurzeln ;-) und Anwendungsfaellen, nach wie vor fuer die beste Wahl.

Die main Funktion erhaelt vom Betriebssystem zwei Parameter uebergeben. Der erste (argc) enthaelt die Anzahl der Argumente, die immer groeszer gleich 1 ist. Bei der Ueberbgabe befinden sich alle Eintrage der Kommandozeile im zweiten Parameter (argv). Da der Programmname zu Beginn der Zeichenfolge steht bildet er auch das erste Element (argc=1). Die weiteren Aufrufoptionen sind durch Leerzeichen getrennte Zeichenketten, die als Strings an main() uebergeben werden. Wie in den Beispielen voran zu sehen, ist argv ein array von Strings also ein array von character-Ketten, die wiederum arrays sind. Wenn man also die String, bzs. Character-Ketten, als char-arrays begreift ist also argv ein array von Zeigern auf diese char-arrays. Also

argv: char* argv[]

Jetzt wird's richtig uebel.

 1 #include "stdio.h"
 2 
 3 int main(int argc, char* argv[]){
 4 
 5 printf("%d : %s : %c %c %c %c %c %c %c %c ...\n", argc, argv[0],*argv[0],*(argv[0]+1),*(argv[0]+2),*(argv[0]+3),*(argv+4),*(argv+5),*(argv+6),*(argv+7));
 6 
 7 printf("%x %x %x %x\n",argv,argv[0],argv+1,argv[0]+1);
 8 
 9 return 0;
10 }

Der Reihe nach. In argv steht ein Zeiger, der auf den ersten Zeiger auf das erste Element (Character-Kette). Somit sind argv und &argv[0] identisch. Wenn ich den String bearbeiten moechte benoetige ich den zweiten (besser vielleicht untergeordneten) Zeiger. Will ich an die einzelnen Buchstaben eines jeden String benoetige ich den Inhalt (*) des zweiten Zeigers, also *(argv[0]) usw. oder den InhaltInhalt von argv, also **argv.

 1 #include "stdio.h"
 2 
 3 
 4 int main(int argc, char* argv[]){
 5 
 6 char hello[]={'H','e','l','l','o','\0'};
 7 char world[]={'W','o','r','l','d','\0'};
 8 
 9 //char* myStrings[]={hello,world};
10 
11 // oder !!!
12 
13 char* myStrings[2];
14 
15 myStrings[0] = &hello[0];
16 myStrings[1] = &world[0];
17 
18 printf("%s %s\n",myStrings[0],myStrings[1]);
19 
20 printf("%d : %s : %c %c %c %c %c %c %c %c %c...\n", argc, argv[0],*argv[0],**argv,*(argv[0]+1),*(argv[0]+2),*(argv[0]+3),*(*argv+4),*(*argv+5),**argv+6,**argv+7);
21 
22 printf("%x %x %x %x\n",argv,argv[0],argv+1,argv[0]+1);
23 
24 return 0;
25 }

Die Klammern um die Additionen (Character-Ketten-Zeiger-Manipulation :-)) sind notwendig um die richtige Reihenfolge der Operationen zu gewaehrleisten. +6 und +7 ergeben nicht das richtig Ergibnis.

Eine Array-Defintion ohne Dimensionsangabe oder gleichzeitige Initialisierung funktioniert nicht. Deshalb ist die Schreibweise char* argv[] durchaus irrefuehrend. Hier klappt es nur wegen der byreference-Parameteruebergabe. Deshalb ist eine Defintion char** argv u.U. besser fassbar.

Beide main-Defintionen liefern das gleich Ergebnis, die Lesart der ersten Variante ist etwas eingaengiger, naemlich dass argv ein Zeiger auf ein array ist.

 1 #include "stdio.h"
 2 
 3 //int main(int argc, char* argv[]){
 4 int main(int argc, char** argv){
 5 
 6 printf("%d : %s : %c %c %c %c %c %c %c %c %c...\n", argc, argv[0],*argv[0],**argv,*(argv[0]+1),*(argv[0]+2),*(argv[0]+3),*(*argv+4),*(*argv+5),**argv+6,**argv+7);
 7 
 8 printf("%x %x %x %x\n",argv,argv[0],argv+1,argv[0]+1);
 9 
10 return 0;
11 }
  • Die Schreibweise **argv+i fuehrt nicht zum Ziel.
  • Die Schreibweise *(*argv+i) fuehrt zum Ziel aber funktioniert so nur fuer das erste Element (Programmname)
  • Die Schreibweise *(argv[x]+i) ist am flexibelsten, da man so auch jeden einzelnen Buchstaben der uebergebenen Optionen zugreifen kann

An der Stelle soll's mit einem Abschlussbeispiel mal reichen. Fuer den Anwendungsfall der i2c-Programmierung bleiebn die Kommandozeilen-Optionen ueberschaubar.

 1 #include "stdio.h"
 2 
 3 //int main(int argc, char* argv[]){
 4 int main(int argc, char** argv){
 5 
 6  int i;
 7 
 8  for (i=1;i<argc;i++){
 9         if (*argv[i]=='-'){
10                 switch(*(argv[i]+1)){
11                         case 'h': printf("Help\n");
12                                   return 1;
13                                   break;
14                         default: break;
15                 }
16         }
17  }
18 
19  for(i=0;i<argc;i++){
20         printf("%d: %s\n",i,argv[i]);
21  }
22  return 0;
23 }