Arduino IO Expander

Fra Holstebro HTX Wiki
Skift til: navigering, søgning
I2C I/O Expander IC

Modulet er anskaffet i Kina[1] til en pris omkring $0,40 og er dokumenteret i følgende datablad[2].

Modulet tilsluttes GND og +5V samt SCL og SDA som I2C protokollen specificerer (IC'er med 16 ben).

Lignende moduler

Vi har 2 forskellige IC'er, der ligner hinanden en del, men kan expande forskelligt antal bit.

På denne side er både PCF8574 og MCP23017 dokumenteret, software-bibliotekerne kan hentes her:

Der findes også andre kredse med lignende funktionalitet - PCF8575 er en 16 bit version af PCF8574.

Elektrisk tilslutning

IO Expanderen fungerer på I2C, og skal dermed have en adresse at kommunikere på. Denne adresse sættes elektrisk med 3 ben på PCF8574, og ligger i området 0x20 til 0x27.

IC'en skal selvfølgelig have en forsyning, og den kan fungere på både 5V og 3,3V.

Ud over dette skal IC'en have forbindelse til SDA og SCL, der på Arduino UNO er A4 og A5.

Hvis man vil anvende interrupt i PCF8574, så skal dette ben også forbindes, ellers kan det bare svæve.

Herunder ses de grundlæggende forbindelser til PCF8574:

De grundlæggende forbindelse til en PCF8574
De grundlæggende forbindelse til en PCF8574

Ud over at der er flere I/O og to interrupt-ben samt en reset der skal til +5V, så forbindes MCP23017 på helt samme måde som PCF8574

Herunder ses de grundlæggende forbindelser til MCP23017:

De grundlæggende forbindelse til en MCP23017
De grundlæggende forbindelse til en MCP23017


Output overvejelser

På PCF8574 er outputtet asymmetrisk, det vil sige at outputtet er meget bedre til at trække lav (strøm ind i kredsen - typisk 25 mA) end til at trække højt (levere strøm ud af kredsen - typisk kun 100 uA).

Det betyder at hvis man fx vil trække LED'er med kredsen, så skal de aktiveres med et lavt output og forbindes med en seriemodstand op til +5V som vist her:

Forbindelse af en LED til udgangen af en PCF8574
Forbindelse af en LED til udgangen af en PCF8574

Output overvejelser til MCP23017

På en MCP23017 er outputtet stadig asymmetrisk, men slet ikke i samme grad som på PCF8574, så man kan godt bruge outputtet som aktivt høj, bare ikke med så stor en strøm som hvis man trækker lav.

Outputtet kan trække 8 mA lav, men kun levere 3 mA høj.

Til at tænde fx. lysdioder kan det være en god ide at anvende samme forbindelse som ved PCF8574.

Input overvejelser

En PCF8574 har en lidt sjov struktur, hvor man ikke helt har styr på om et ben er input eller output. Årsagen til dette er at man altid skriver de 8 bit samtidigt, fordi de ligger i et register inde i kredsen.

Hvis man vil udnytte en kreds til både at have input og output, så er det en god ide at starte med at sætte alle output til høj (det samme kan gøres ved at sætte benene til INPUT_PULLUP). Dette vil gøre at der er en svag pull-up effekt på benet som trækker ca. 100 uA høj (som den gør ved højt output).

Dette har samtidigt den effekt at man har en intern pull-up inde i kredsen, og dermed ikke behøver at have en pull-up modstand uden for kredsen (man må heller ikke tilslutte en pull-down). Dette gør det nemt, hvis man vil arbejde med kontakter/trykknapper.

Det har så den ulempe, at kontakten skal trække lav, ved at lave forbindelse til GND - altså er indgangen LOW når kontakten er aktiveret og HIGH når der ikke er trykket på kontakten - det betyder at man skal tænke sig om når man programmerer.

Forsøger man sig med at sætte en kontakt til høj, så skal man have en effektiv pull-down modstand (omkring 1k), og hvis man ved et uheld får sat en udgang lav, så vil kredsen trække meget strøm når der trykkes på knappen. Dette kan IKKE anbefales.

En trykknap tilsluttes som vist her:

Forbindelse af en trykknap til indgangen af en PCF8574
Forbindelse af en trykknap til indgangen af en PCF8574

Input overvejelser på MCP23017

Hele registerstrukturen i en MCP23017 er noget mere avanceret, så input-mæssigt kan den fungere ligesom Arduinos input. Det eneste man skal være opmærksom på er at man skal sætte de interne pull-up modstande på en lidt anden måde.

Fordi man har den interne pull-up, så kan man med fordel sætte trykknapper på ligesom illustreret ved PCF8574.

Hardware til Software eksempel

I den beskrevne software anvendes forskellige input og output sluttet til Arduinoen - diagrammet herunder viser hvordan testopstillingen er koblet op.

Diagrammet der anvendes til den beskrevne software
Diagrammet der anvendes til den beskrevne software

S1 er den kontakt der aktiverer interruptet, som inde i softwaren styrer LED2. Den skifter høj/lav for hvert interrupt.

S2 styrer de resterende lysdioder.
LED0 lyser når der ikke er trykket på S2.
LED1 lyser når der er trykket på S2.
LED3 lyser i et halvt sekund efter S2 aktiveres.
LED4 blinker 5 gange med 1 sekund interval når der trykkes på S2.
LED5 blinker hele tiden med 1 sekund interval.

Software

Biblioteket der anvendes ligger på GitHub PCF8574 Library[3]. Til eksemplet med MCP23017 anvendes biblioteket på Github MCP23017 Library[4] Biblioteket downloades og installeres som beskrevet under Arduinos biblioteker.

Der ligger biblioteker til både PCF8574 (8 bit I/O) og PCF8575 (16 bit I/O), så den vil ikke direkte installere biblioteket fra ZIP-filen, men man kan blot kopiere biblioteket med den ønskede kreds ind under Dokumenter\Arduino\libraries, så vil det fungere. På UC Holstebro har vi p.t. (2018) kun PCF8574 og MCP23017 på lager. MCP23017 biblioteket ser ud til at installere på normal vis fra ZIP-filen.

Sammen med biblioteket til PCF8574 kommer et eksempel ButtonBlink, der giver et overblik over en del af funktionerne.

Her gennemgås kun de grundlæggende dele af modulet.

Til MCP23017 er der 3 eksempler, hvor der gennemgås noget fra button og toggle (output), mens interrupt eksemplet ikke gennemgås, det ligner meget eksemplet fra PCF8574.

Anvendelsen af softwaren til PCF8574

Hele den beskrevne software kan hentes i denne ZIP-fil. Filen kræver af PCF8574 biblioteket er installeret.

Som alle andre biblioteker skal der sættes de grundlæggende ting op for at kontakte biblioteket. Der oprettes et objekt expander, der refererer til den enkelte PCF8574-kreds. Hvis man har flere kredse (på hver sin adresse), så skal man også oprette flere objekter. Dette er ikke vist her.

/* Biblioteker */
#include <Wire.h>    // Required for I2C communication
#include "PCF8574.h" // Required for PCF8574

/** PCF8574 instance - det aktuelle objekt knyttet til én kreds */
PCF8574 expander;

Inde i setup() skal der sættes gang i den serielle port til test ved hjælp af begin-metoden, og expander modulet initialiseres med begin på adresse 0x20.

De enkelte I/O sættes op som output og input, og for en sikkerheds skyld sættes alle udgange høje, så LED'erne slukkes fra starten.

/** setup() */
void setup() {

  /* Setup serial for debug */
  Serial.begin(9600);
  
  /* Start I2C bus og PCF8574 instance af objektet på adresse 0x20 */
  expander.begin(0x20);
  
  /* Setup for PCF8574 pins til demo-formål */
  expander.pinMode(0, OUTPUT);
  expander.pinMode(1, OUTPUT);
  expander.pinMode(2, OUTPUT);
  expander.pinMode(3, OUTPUT);
  expander.pinMode(4, OUTPUT);
  expander.pinMode(5, OUTPUT);
  expander.pinMode(6, INPUT_PULLUP);
  expander.pinMode(7, INPUT_PULLUP);
  expander.set();   // For en sikkerheds skyld sættes alle output høje

Ud over dette sættes interruptet på til at fungerer på PCF8574 ben 7 og ind på Arduinoens ben D8

  /* Enable PCF8574 interrupts, use pin D8 as "INT" pin and ISRgateway() as callback function */
  expander.enableInterrupt(8, ISRgateway);
  
  /* Attach a software interrupt on pin 7 of the PCF8574 */
  expander.attachInterrupt(7, ISRdemo, FALLING);
}

Til interruptet anvendes der to service-rutiner som vist her:

/** This function will be called each time the state of a pin of the PCF8574 change */
void ISRgateway() {
  expander.checkForInterrupt();
}

/** This function will be called each time the button on pin 7 is pressed (HIGH-to-LOE transition) */
void ISRdemo() {
  /* Toggle PCF8574 output 2 for demo */
  expander.toggle(2);
}

Den første rutine ISRgateway() kaldes af Arduinoen, hver gang der kommer et interrupt på ben D8 ind på Arduinoen

Den næste rutine ISRdemo() kaldes fra checkForInterrupt() filtreret efter det indput der er angivet i attachInterrupt() (ben 7) og kun på den faldende kant (FALLING).

Funktionen af dette er at output 2 skifter HIGH/LOW hver gang det trykkes på knappen tilsluttet input 7.

I loop() håndteres alle lysdioderne med de forskellige funktioner.

Først læses niveauet på kontakten S2 hvorefter LED0 og LED1 sættes ud fra niveauet. Da inputtet er aktivt lavt, så sættes buttonState til det inverterede:

  buttonState = ! expander.digitalRead(6);
  expander.digitalWrite(0, buttonState);
  expander.digitalWrite(1, ! buttonState);

Når der trykkes på knappen fanges forkanten af trykket, og hvis det er en forkant, nulstilles den timer-counter der anvendes til alle tiderne samt den counter der styrer antal blink.

LED3 tændes også på forkanten, og der holdes styr på niveauet til næste gang i lastButtonState, så man kan fange forkanten:

  if (buttonState) {
    if (! lastButtonState) {
      timeCnt = 0;
      blinkCnt = 10;
      expander.digitalWrite(3, LOW);
    }
    lastButtonState = true;
  } else {
    lastButtonState = false;
  }

Tidsmålingen laves simpelt ved at bruge et delay på 10 ms, og der tælles i en time-counter:

  delay(10);
  timeCnt++;

Når der er gået ca. et halvt sekund, så nulstilles timer-counteren og LED3 slukkes igen (lyser et halvt sekund ved tryk på knappen):

  if (timeCnt == 50) {
    expander.digitalWrite(3, HIGH);
    timeCnt = 0;

Hvis blink-counteren stadig er over 0, så toggles LED4. Ved at blink-counteren starter på 10, så vil LED4 være tændt 5 gange i løbet af ca. 5 sekunder:

    if (blinkCnt > 0) {
      expander.toggle(4);
      blinkCnt--;
    }

Til slut i koden der afvikles hvert halve sekund bliver LED5 togglet, så den blinker konstant:

    expander.toggle(5);

Yderligere muligheder i softwaren til PCF8574

Der ligger yderligere muligheder i biblioteket man kan anvende, hvis man ønsker andre måder at anvende IO Expanderen på.

Man kan skrive alle udgange på en gange ved at skrive en byte ud, og tilsvarende kan man læse alle input i en byte:

  expander.write(byteVal);
  byteVal = Expander.read();

Man kan sætte alle udgange enten lave på en gang eller høje på en gang:

  expnader.clear();
  expander.set();

Der er en blink-funktion, der kan blinke med et af benene et antal gange med en vis varighed. Dette kan ikke anbefales, da funktionen ikke returnerer før den er færdig med at blinke:

  expander.blink(pin, count, duration);

Som vist oppe i eksemplet kan kredsen sættes op til at fungere med interrupt. Til dette skal der sættes flere ting op, som kan gøres med følgende funktioner:

  expander.enableInterrupt(pin, *selfCheckFunction);
  expander.disableInterrupt();

  expander.checkForInterrupt();

  expander.attachInterrupt(pin, *userFunc, mode);
  expander.detachInterrupt(pin);

Anvendelse af softwaren til MCP23017

Hele den beskrevne software kan hentes i denne ZIP-fil. Filen kræver af MCP23017 biblioteket er installeret.

Som alle andre biblioteker skal der sættes de grundlæggende ting op for at kontakte biblioteket. Der oprettes et objekt expander, der refererer til den enkelte MCP23017-kreds. Hvis man har flere kredse (på hver sin adresse), så skal man også oprette flere objekter. Dette er ikke vist her.

#include <Wire.h>
#include "Adafruit_MCP23017.h"

Adafruit_MCP23017 mcp;

Inde i setup() sættes gang i expander modulet ved at det initialiseres med begin på grundadressen 0x20 + 0.

De enkelte I/O sættes op som output og input, og der laves pullup på kontakten til GND, men den slukkes til kontakten der har pulldown elektrisk.

void setup() {  
  mcp.begin(0);      // use default address 0

  mcp.pinMode(7, OUTPUT);
  mcp.pinMode(6, OUTPUT);
  mcp.pinMode(5, OUTPUT);
  mcp.pinMode(4, OUTPUT);
  mcp.pullUp(0, HIGH);  // turn on a 100K pullup internally
  mcp.pinMode(0, INPUT);
  mcp.pinMode(1, INPUT);
  mcp.pullUp(1, LOW);  // turn off a 100K pullup internally
}

I loop() håndteres alle lysdioderne med de forskellige funktioner.

Først er der et delay, der giver blink-funktionen læses niveauerne på det to kontakter hvor lysdioderne styres ud fra niveauet. Inputtet er på ben 0 er aktivt lavt, så det tænder den lysdiode der er aktiv lav. Inputtet på ben 1 er aktivt højt, så det tænder lysdioden på det output der er aktivt højt:

void loop() {
  delay(300);

  mcp.digitalWrite(6, mcp.digitalRead(1)); // LED ON when button pressed
  mcp.digitalWrite(4, mcp.digitalRead(0)); // LED ON when button pressed

Herefter skiftes de to udgange (en aktiv lav og en aktiv høj), der ventes og de sættes til det modsatte, så sammen med delayet i starten kommer de til at blinke regelmæssigt og de to lysdioder i modtakt.

  mcp.digitalWrite(7, HIGH); // Turn LED ON
  mcp.digitalWrite(5, HIGH); // Turn LED OFF

  delay(300);

  mcp.digitalWrite(7, LOW); // Turn LED OFF
  mcp.digitalWrite(5, LOW); // Turn LED ON

Det at der anvendes delay til hele tiden mellem skiftene gør at kontakterne har en langsom reaktion - det skulle omprogrammeres som eksemplet til MCP8574 for at fungere bedre.

Mulighederne til MCP23017

Modulet til MCP23017 er lidt anderledes, og har derfor ikke de samme muligheder som PCF8574, og den skal desuden forholde sig til at den håndterer 16 bit I/O.

Der er to forskellige initialiseringer til modulet, hvor den uden parameter bare sætter den til adresse 0. I den anden kan anvendes adresse 0-7 som beskrevet under hardwaren.

  void begin(addr);
  void begin();

På samme måde som de almindelige ind- og ud-gange, så er der en pinMode metode. Bemærk at den ikke kender INPUT_PULLUP, men kun INPUT og OUTPUT. Den har til gengæld en pullUp metode som kan tænde og slukke kontakten til pull-up modstanden

  void pinMode(pin, mode);   // mode er INPUT eller OUTPUT
  void pullUp(pin, state);   // state er HIGH eller LOW

Som de normale pins har modulet en digitalWrite og en digitalRead metode, som kan styre og læse de enkelte pins

  void digitalWrite(pin, level);  // level er HIGH eller LOW
  uint8_t digitalRead(pin);       // returnerer HIGH eller LOW

Der er rutiner der kan håndtere alle 16 bit på en gang

  void writeGPIOAB(mask16);   // kan skrive alle 16 udgange på en gang fra en 16 bit variabel
  uint16_t readGPIOAB();      // kan returnere alle 16 indgange i en variabel på 16 bit

Der er en læserutine som kan håndtere en af de 8 bits porte

  uint8_t readGPIO(port);   // Port A læses med port = 0 og Port B læses med port = 1

Biblioteket kan sættes op til at håndtere noget der ligner interrupt på pins - se eksemplet interrupt under biblioteket.

  void setupInterrupts(uint8_t mirroring, uint8_t open, uint8_t polarity);
  void setupInterruptPin(uint8_t p, uint8_t mode);
  uint8_t getLastInterruptPin();
  uint8_t getLastInterruptPinValue();

Test

Test af softwaren viser at Den beskrevne software virker efter hensigten.

Det eneste der kan drille er at hvis man genstarter de 5 blink inden den har blinket færdig, så kan den komme til at stå tændt i stedet for slukket.

Test af MCP23017

Der er kun testet de grundlæggende IO-egenskaber, og det viser at de håndteringsmæssigt minder meget om Arduinos digitale I/O. Der er fuldt fleksible og kan sættes op som IO uden problemer.

Interrupt-delen er ikke testet, men det fungerer sandsynligvis på samme måde som PCF8574.

Referencer