Viele Taster an MCP23017s und dem Arduino Mega - nun fehlt die Strategie!

Hallo in die Runde,

als erstes gebührt es die Höflichkeit, dass ich mich kurz vorstelle. Als ambitionierter Bastler in den 30er Jahren bin ich vor ca. zwei Jahren zum Arduino gekommen. Ich habe auch bereits einige kleine Projekte erfolgreich umgesetzt, was mir aufgrund meiner Arbeit in der IT nicht sonderlich schwer von der Hand ging. Sehr geholfen hat mir dabei euer Forum, in dem ich bisher nur lesend unterwegs war.

Nun aber stehe ich vor einem Problem - und wie so oft im Leben ist die Richtung des ersten Schrittes wichtiger als die Weite. Daher möchte ich euch gerne um euren Rat fragen, eventuell könnt ihr mich in die richtige Richtung "schubsen". :)

Ich stehe vor der Aufgabe, ein komplettes Fahrzeug mit einem Arduino zu steuern. Zielsetzung ist es für den Anfang, alle Funktionen über diverse Taster im Fahrzeug betätigen zu können, langfristig soll auch eine Schaltung über Apps, etc. möglich sein. Doch für den Anfang halte ich den Umfang mal so klein wie es geht.

Bisher habe ich die Strategie entwickelt, dass alle Taster über MCP23017-Port-Expander mit dem Arduino verbunden werden. Bei ca. 60 Tastern komme ich damit auf 4-5 Port-Expander. Jeder dieser Eingänge muss einer bestimmten Funktion des Fahrzeugs zugeordnet werden, z.B. die Hupe während dem Tastendruck zu aktivieren oder das Abblendlicht mit einem Tastendruck zu aktivieren und mit dem anderen Tastendruck zu deaktivieren. Für die Ausgänge werden die regulären Pins eines Arduino-Mega verwendet, die über Relaiskarten mit den Fahrzeugfunktionen verbunden sind. Alle Fahrzeugfunktionen, also die Schnittstellen für die Steuerung über den Arduino, sind bereits fertiggestellt.

Der I2C-Bus ist aufgrund der MCP23017-Expander und der Tatsache, dass ich möglichst wenige Leitungen durch das Fahrzeug ziehen möchte, gesetzt. Die Notwendigkeit, tastende und schaltende Funktionen des Fahrzeugs zu unterscheiden, ist ebenfalls gesetzt.

Ich überlege nun wie ich die Logik aufbaue. Bei mir sorgt der Umfang der Taster und Funktionen für gehörig Respekt und mir ist nicht klar wie ich Tastendruck, Erkennung der dazugehörigen Funktion und Umsetzung unter einen Hut bekomme. In meiner fast noch jugendlichen Naivität dachte ich an eine „Map“, in der alle Port-Expander und deren Bits verzeichnet sind - der Arduino überwacht alle Eingänge fortlaufend und steuert, sollte es notwendig sein. Ich habe nur Bedenken hinsichtlich der Performance.

In der „perfekten Welt“ kriegt der Arduino wohl über einen Interrupt eines Port-Expanders einen Impuls, dass etwas passiert ist. Er fragt dann konkret ab, was passiert ist und steuert entsprechend. Ergo: Nicht alle verfügbaren Taster werden fortlaufend auf Änderung überprüft.

Wie würdet ihr hier vorgehen? Habt ihr eventuell einen guten Tipp, denn ich übersehe? Ich kann z.B. sicherstellen, dass alle Taster gegen HIGH oder LOW schalten - ich muss hier keine wechselnden Anforderungen betrachten. Weitere Arbeiten als die Steuerung der Funktionen hat der Arduino auch nicht zu erledigen.

Zusammengefasst: Der Arduino überwacht die Eingänge, erkennt einen Tastendruck und führt die eindeutige, damit verbundene Aktion aus. Diese können tastend oder schaltend sein.

Ich hoffe meine Ausführungen sind nicht zu verwirrend - in jedem Falle danke ich euch allen recht herzlich für eure Zeit!

Hallo, kennst Du die Bibliothek von Adafruit zum MCP23017? Da geht dann mcp1.digitalRead(E5);

Damit willkommen im Forum!

Vermutlich hast du kein Gefühl dafür, wie langsam Taster sind. (Relais übrigens auch)
Das mechanische Prellen hast du automatisch im Griff, wenn du die Schalter/Taster betont langsam (z.B. nur alle 5 ms abfragst).

In 1 ms die 16 Bit eines Portexpanders auswerten, sollte eigentlich kein Problem sein.

Halt uns auf dem Laufenden
und lass dich nicht erwischen, wenn du das Fahrzeug auf der Straße mit dem Handy steuerst.

In einer idealen Welt würde der Arduino eine Liste aller aufgetretenen Änderungen erstellen, und diese dann abarbeiten. Möglichst noch nach Prioritäten geordnet. Dazu reicht dann ein switch(nächsteÄnderung) ...

Mit Apps ergibt sich so eine Liste automatisch, wenn jede Änderung (Tastendruck) per Telegramm gemeldet wird.

Später könnte es etwas komplizierter werden, wenn der Arduino z.B. das Blinken, Warnblinker oder den Intervall-Scheibenwischer implementieren soll. Dann werden solche Steuerungen als Automaten (state machine) implementiert, oder als Tasks, die nicht einfach nur ein- und ausgeschaltet werden, sondern ggf. noch auf die Beendigung eines laufenden Vorgangs warten müssen.

Wow, herzlichen Dank für euer Feedback! Ich gehe mal der Reihe nach durch... :)

@agmue:

Bisher noch nicht, jetzt schon. Vielen Dank für den Tipp, dass sieht sehr gut aus und werde ich garantiert gebrauchen können!

@michael_x:

Da hast du Recht. Mir fehlt auch ehrlich gesagt noch das Gefühl, was ich dem Arduino zumuten kann. Wenn ich dich richtig verstehe, würdest du also alle Port-Expander im Loop der Reihe nach prüfen und, sollte was passieren, entsprechende Maßnahmen ergreifen?

Übrigens werde ich das Auto natürlich nicht mit dem Handy steuern (steuern im Sinne von "fahren"). Es geht lediglich darum, die Funktionen (Licht, Hupe, etc.) steuern zu können - ich möchte das Auto natürlich nicht fernbedient fahren, dass wäre mir viel zu riskant! :)

@DrDiettrich:

Ich habe mich gestern Abend noch ein wenig in die "Finite State Machine" eingelesen. Das Konzept an sich ist mir klar, aber ich habe noch kein Gesamtbild davon. Du würdest die einzelnen Funktionen (bleiben wir beispielhaft mal bei der Hupe [tastend] und beim Licht [schaltend]) jeweils als einzelne Automaten umsetzen? Wie erfolgt dann aber das ganze drum herum, also die Erkennung des Tastendrucks?

Macht es vielleicht Sinn, dass ich das, was ich bisher habe (nicht viel) einmal beispielhaft poste?

Ich danke euch in jedem Fall, auch für die freundliche Aufnahme in diesem Forum!

Ein Automat prüft in jedem Zustand die Bedingungen (Eingänge…), die einen Übergang in einen anderen Zustand erfordern könnten, und wechselt entsprechend in den nächsten Zustand. Beim Wechsel wird noch das ausgeführt, was bei diesem Wechsel einmalig passieren soll. Damit unterscheiden sich Hupe und Licht kaum. Wenn ich Deinen Ansatz richtig verstehe, dann bleibt die Hupe eingeschaltet so lange der Knopf gedrückt ist, während das Licht mit einem Knopf ein und mit einem anderen wieder ausgeschaltet wird, bzw. zwischen Fern- und Abblendlicht umgeschaltet wird.

Zeig mal, was Du schon hast. Benutze bitte die Code Tags </>.

#include <Wire.h>


// Configuration
// ---------------------------------------------------------------------

// Pin & I2C Assignments
#define PIN_MCP_STATE_LED      13
#define I2C_GATEKEEPER_ADDRESS 1
#define EXP_LOWER_1            0x20
#define EXP_LOWER_2            0x21

// Global State Variables
bool mcpState = false;
bool apiState = false;


// Main Application
// ---------------------------------------------------------------------

void setup()
{
  Serial.begin(9600);
  while (!Serial) {
    ;
  }
  Wire.begin();

  Serial.println("[NOTICE] Setup complete");
}

void loop()
{
  requestGatekeeperStatesByI2C();
  requestInputsByI2C();
}

void requestGatekeeperStatesByI2C()
{
  Wire.requestFrom(I2C_GATEKEEPER_ADDRESS, 1);

  while(Wire.available()) {
    byte gatekeeperFeedback = Wire.read();
    updateMcpState(bitRead(gatekeeperFeedback, 0));
    updateApiState(bitRead(gatekeeperFeedback, 1));

    // TODO: Is it necessary to add a kind of "debounce delay" here?
  }
}

/**
 * I2C: Requests inputs from port expanders
 */
void requestInputsByI2C()
{
  // How to:
  // * Check both Port Expanders
  // * Detect if there any actions to do
  // * Call the functions, if necessary
  // * Avoid "blocking" of other functions
}

void updateMcpState(bool newMcpState)
{
  if (newMcpState != mcpState) {
    mcpState = newMcpState;
    digitalWrite(PIN_MCP_STATE_LED, (mcpState) ? HIGH : LOW);
  }
}

void updateApiState(bool newApiState)
{
  if (newApiState != apiState) {
    apiState = newApiState;
  }
}


// Vehicle Functions
// ---------------------------------------------------------------------

void operateHorn()
{
  // TODO
  Serial.println("[DEBUG] Operate Horn");
}

void toggleLowBeam()
{
  // TODO
  Serial.println("[DEBUG] Toggle Low Beam");
}

void operateWindowLeftDown()
{
  // TODO
  Serial.println("[DEBUG] Operate Window Left Down");
}

void operateWindowRightUp()
{
  // TODO
  Serial.println("[DEBUG] Operate Window Left Up");
}

/* Alternative solution: A vehicle function with "options".
 *
 * Would be the "cleaner" solution, because it reduces the
 * amount of functions, but would add another layer of
 * complexity.
 */
void operateWindow(string side, string direction)
{
  // TODO
  Serial.println("[DEBUG] Operate Window");
}

Ich sage ja, mega übersichtlich aktuell. “MCP” im Code bezieht sich übrigens auf den Namen des Programms, nicht auf Port-Expander vom Typ “MCP23017”. Und Gatekeeper ist eine bereits fertiggestellte Komponente, die den Zugriff überhaupt erst ermöglicht - das muss hier aber nicht beachtet werden.

Aktuell geht es nur um die Machbarkeitsstudie - da ich im echten Leben PHP-Entwickler bin, gefällt es mir nicht, alles in eine Datei zu packen - saubere Klassenstrukturen wären mir hier lieber, aber alles der Reihe nach. :wink: Die gesamte I2C-Abfrage der Port-Expander fehlt auch wieder - die habe ich entfernt. Da ich mir agmue’s Library angeschaut habe, diese bisher aber noch nicht integriert habe.

Um einen Prototyp zu entwickeln und diesen auch “reell” testen zu können, gehe ich von zwei Port-Expandern und vier möglichen Funktionen aus: Hupe, Abblendlicht, Fensterheber hoch, Fensterheber runter). Wenn diese Funktionen integriert sind, wird der Rest analog funktionieren.

Edit:

/**
 * The loop.
 */
void loop()
{
  static short state = STATE_IDLE;
  static unsigned long ts;

  // At the moment, there is only one state: Polling
  // the Gatekeeper status and checking if there are
  // any inputs made by the port expanders.
  switch (state) {
    case STATE_IDLE:
      ts = millis();
      if (millis() > ts + POLLING_INTERVAL_GATEKEEPER) {
        requestGatekeeperStatesByI2C();
      }
      if (millis() > ts + POLLING_INTERVAL_GATEKEEPER) {
        requestInputsByI2C();
      }
      break;
  }
}

Ein alternativer Loop, in diese Richtung würde ich jetzt weiterentwickeln. Er prüft alle 250 ms den Status des Gatekeepers und alle 50 ms den Status aller Port-Expander. Sollten sich hier Eingaben ergeben, würde der State auf “Erkennung” wechseln und er würde die notwendige Aufgabe ermitteln. Im nächsten Status, “Verarbeitung” würde er diese dann verarbeiten, dann zurück auf Anfang (IDLE).

Wo ich den Knoten ins Hirn kriege: Sowas wie die Hupe, die doch während dem Tastendruck funktionieren soll, kann doch hier nicht funktionieren?

Man möge es mir verzeihen, wenn ich Unsinn Rede, aber ich stehe wirklich einfach nur auf dem Schlauch… :slight_smile:

Bleibt die Frage, wie Ausgänge und Eingänge verknüpft werden.

Wie wäre es mit einem Array aus Funktionszeigern für die Eingangsbits ?

void Window(byte param, bool set) {
  // set: true=Taster jetzt betätigt, false=Taster jetzt losgelassen
  bool direction = param & 0x01; // false = AUF, true = ZU
  byte windowID = (param >> 4) - 1 ; // 0 .. 3 : das jeweilige Fenster
  // ...
}

const struct FP { void (*func) (byte, bool) ; byte param; } Verteiler[64] = {
    { Window, 0x10 },  // Bit 0: Fenster li vo AUF
    { Window, 0x11 },  // Bit 1: Fenster li vo ZU
    { Window, 0x20 },  // Bit 2: Fenster re vo AUF
    // ...
};

// check states of one Portexpander and trigger actions according to function  list
void checkChanges(uint16_t newState, uint16_t & State, const struct FP * functions) {
   if (newState == State) return; // shortcut for usual case
   for ( byte i = 0; i < 16; i++) {
      bool n = bitRead(newState, i);
      if ( n != bitRead(State, i) ) {
         void (*f) (byte, bool) = functions[i].func; // der Funktionszeiger
         if (f == NULL ) { Serial.println ("Error "); continue;}
         f(functions[i].param, n); // hier wird z.B. Window(0x10, true) aufgerufen
      }
   }
   State = newState; 
}

const byte EXP_LOWER_1=0x20;
const byte EXP_LOWER_2=0x21;

void setup() {}

uint16_t getState( byte I2cAddress ) { return 0x1234; }  // vorab

void loop() {
   static uint16_t states[4];  // 4*16 bit
   uint16_t newvalues = getState(EXP_LOWER_1); // read the 16 bit of one PortExpander
   checkChanges (newvalues, states[0], Verteiler);  // update states and call functions as necessary
   newvalues = getState(EXP_LOWER_2); // read the 16 bit of one PortExpander
   checkChanges (newvalues, states[1], Verteiler+16);  // update states and call functions as necessary
}

Compiliert, aber ungetestet

Man könnte entweder alle Eingänge auf Zustandswechsel testen, und dann die zugehörigen Handler aufrufen, oder alle Handler (Tasks) aufrufen, die dann ihre Eingänge prüfen. Dazu könnte ein Array aller Eingänge mit den Adressen der Handler benutzt werden, oder ein Array aller Task-Objekte.

... Oder das ganze ist vielleicht eigentlich so trivial, dass man gar nicht auf Änderung überwachen muss, sondern nur eine Logik braucht, die Eingangspins mit Ausgangspins verknüpft.

Sicher findet sich doch noch irgendwo eine State Machine, aber nicht alle brauchen explizite Flanken-Erkennung

Alter Schwede! :)

Schon einmal vielen Dank für den Input und die Mühen! :) Ich muss mich wirklich wieder (mehr) mit der Syntax von C++ auseinandersetzen... ;)

Ich habe gerade 30 Minuten auf den von dir geschriebenen Code geschaut, michael_x, und das meiste ist mir einigermaßen klar. Magst du mir bitte nur die Zeile "param & 0x01" erklären? & ist der Operator "bitweises AND", doch param ist 0x10, 0x11, 0x20 oder 0x21 - mir ist nicht klar, wie hier true oder false herauskommt?

Wie du die WindowID ermittelst ist einfach und genial! Dieses Vorgehen lässt natürlich Raum für die anderen Fahrzeugfunktionen, die ebenfalls "Optionen" besitzen. Nur damit ich dein Codebeispiel richtig verstehe, wenn du 0x10 und 0x11 um 4 Bit nach rechts shiftest, kommt doch 1 (-1 = 0) heraus, bei 0x20 und 0x21 entsprechend 2 (-1 = 1)? windowID ist daher 0-1, nicht 0-3, oder?

Generell war es ja mein erster Gedanke, eine "Map" zu verwenden, also eine Auflistung aller Funktionen zur Ansteuerung - daher gefällt mir dieser Ansatz sehr gut (und zeigt mir, dass ich nicht völlig falsch lag ;))! Bei Fahrzeugfunktionen ohne Optionen (param), wie z.B. der Hupe, wäre der Parameter entsprechend 0x00 und würde ignoriert.

Im aktuellen Falle wäre der Ablauf also so, dass der Arduino alle vorhandenen Port-Expander der Reihe nach prüft. Erkennt er eine Änderung des Zustandes, ruft er die entsprechende Funktion auf. Innerhalb der Funktion, z.B. "Window", ist zentralisiert definiert was überhaupt zu tun ist (Welcher Ausgangs-Pin angesteuert wird, etc.). Auch wäre es möglich, über dieses Vorgehen andere Funktionen zu beeinflussen (z.B. Anschalten des Aufblendlichts forciert ein Ausschalten der Nebelscheinwerfer, etc.)

Wie ihr oben richtig gesagt habt, fehlt mir noch das richtige Gefühl für den Arduino - also ob er dies auch performant packt und z.B. einen Tastendruck auch zielsicher und schnell erkennt und verarbeitet. Daher stellte ich diesen Ansatz in Frage - aber ich fasse dank euch Vertrauen, dass in dieser Form einmal umzusetzen, dafür danke ich euch wirklich herzlich!

Habs jetzt nicht getestet, und die Fensternummer "nur zum Spass" als 0x10, 0x20 .. codiert und hinterher auf 0, 1, ... umgerechnet:

(0x10 >>4) -1 = 0
(0x20 >>4) -1 = 1

das -1 kann man sich sparen, wenn die konstante Parameterliste gleich passend aufgebaut wird.

( 0x11 & 1 ) == true C ist da nicht so pingelig, 1 und true sind sehr ähnlich. Oder: das Ergebnis einer arithmetischen Berechnung kann als bool weiterverwendet werden. Alles ausser 0 ist true

Das langsamste ist der I2C Bus, das bisschen ( x>>4 - 1 ) usw. kannst du komplett vergessen.
Der Takt eines atmega328 (unvorstellbare 16 MHz) sind 62,5 Nanosekunden.
In einer µs hast du also 16 Takte ( soviel braucht die Formel oben nicht )
Und eine ms hat 1000 µs.

Wie gesagt, damit du dich nicht um Entprellen kümmern musst, empfehle ich, jeden PortExpander nur alle paar ms abzufragen…
Du willst ja keinen Pianisten samplen, sondern max. zwischen kurzen (80 … 300 ms) und langen Tastendrücken unterscheiden.

Die Komfort-Funktionen der Arduino IDE (digitalWrite) haben ihren Sinn, weil meist die Zeit reichlich ist. Bieten also noch Optimierungspotential, wenn es erforderlich würde.

Auch könntest du zur Not einen schnellen Expander für Taster und 3 langsame für Schalter nehmen und die 1-2-1-3-1-4- abfragen …
Das haben wir gemacht als Daten mit 300 Bd übertragen wurden

Also, ich kann dir ja gar nicht genug danken… aber ich habe einen “Prototypen” am laufen, diesen auch hier in der Praxis getestet. :wink:

Natürlich ist da noch einiges zu tun, aber ich denke es geht in die richtige Richtung - du wirst einiges von dir wieder erkennen! :wink:

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


// Configuration
// ---------------------------------------------------------------------

// IC2 Addresses
const byte I2C_GATEKEEPER_ADDRESS = 0x01;
const byte PORT_EXP_1_ADDRESS     = 0x20;

// Global State Variables
bool mcpState = false;
bool apiState = false;

// Global Declarations
Adafruit_MCP23017 mcp1;


// Vehicle Functions
// ---------------------------------------------------------------------

void horn(byte param, bool set)
{
  // TODO
  Serial.println("[DEBUG] Horn");

  // Turn on internal LED for debug purposes
  if (set) {
    digitalWrite(LED_BUILTIN, HIGH);
  } else {
    digitalWrite(LED_BUILTIN, LOW);
  }
}

void lowBeam(byte param, bool set)
{
  // TODO
  Serial.println("[DEBUG] Low Beam");
}

void window(byte param, bool set)
{
  // TODO
  Serial.println("[DEBUG] Window");
}


// Vehicle Functions Mapping
// ---------------------------------------------------------------------

const struct FP { void (*func) (byte, bool) ; byte param; } Mapping[] =
{
    { horn,    0x00 }, // Expander 1, Bit 0: Horn
    { lowBeam, 0x00 }, // Expander 1, Bit 1: Low Beam
    { window,  0x10 }, // Expander 1, Bit 2: Window Left Down
    { window,  0x11 }, // Expander 1, Bit 3: Window Left Up
    { window,  0x20 }, // Expander 1, Bit 4: Window Right Down
    { window,  0x21 }, // Expander 1, Bit 5: Window Right Up
};



// Application Methods
// ---------------------------------------------------------------------

void requestGatekeeperStatesByI2C()
{
  Wire.requestFrom(I2C_GATEKEEPER_ADDRESS, 1);

  while(Wire.available()) {
    byte gatekeeperFeedback = Wire.read();
    updateMcpState(bitRead(gatekeeperFeedback, 0));
    updateApiState(bitRead(gatekeeperFeedback, 1));
  }
}

uint16_t requestInputsByI2C(Adafruit_MCP23017 mcp)
{
  uint16_t inputValues = mcp.readGPIOAB();

  // Invert values because the use of pull-up resistors
  return ~inputValues;
}

void checkForUpdates(
  uint16_t newState,
  uint16_t &currentState,
  const struct FP *functions
) {
  if (newState == currentState) {
    return;
  }
  for (byte i = 0; i < 16; i++) {
    bool n = bitRead(newState, i);
    if (n != bitRead(currentState, i)) {
      void (*f) (byte, bool) = functions[i].func;
      if (f == NULL) {
        Serial.println ("[ERROR] Invalid function call");
        continue;
      }
      f(functions[i].param, n);
    }
  }

  currentState = newState;
}

void updateApiState(bool newApiState)
{
  if (newApiState != apiState) {
    apiState = newApiState;
  }
}

void updateMcpState(bool newMcpState)
{
  if (newMcpState != mcpState) {
    mcpState = newMcpState;
    digitalWrite(LED_BUILTIN, (mcpState) ? HIGH : LOW);
  }
}


// Main Application
// ---------------------------------------------------------------------

void setup()
{
  Serial.begin(9600);
  while (!Serial) {
    ;
  }

  pinMode(LED_BUILTIN, OUTPUT);

  // TODO: Add a list of port expanders to initialize
  mcp1.begin();
  for (short i = 0; i < 16; i++) {
    mcp1.pinMode(i, INPUT);
    mcp1.pullUp(i, HIGH);
  }

  Serial.println("[NOTICE] Setup complete");
}

void loop()
{
  static uint16_t states[4];

  // TODO: Add a list of port expanders to check for updates
  uint16_t newValues = requestInputsByI2C(mcp1);
  checkForUpdates(newValues, states[0], Mapping);
}

Offen wäre nun noch die möglichst effektive Initialisierung mehrerer Port-Expander und die Anbindung an das Mapping - ich möchte mehrere Mapping-Variablen für mehrere Port-Expander vermeiden, sondern diese einfach der Reihe nach durch definieren. Ich stelle mir also ein Array von Port-Expandern vor, die in einer Schleife sowohl initialisiert (setup()), als auch geprüft (loop()) werden.

Verrate mir noch ein Geheimnis: Für was steht in deinem Codebeispiel die Abkürzung FP des Structs? :wink:

Allerbesten Dank, morgen schaue ich direkt nochmals drauf und versuche es auf diesem Fundament weiter zu bauen! :slight_smile:

“struct FP” ist der Typname, und “Mapping” der Name der Variablen.

Zu Deinem Wunsch nach einer Aufteilung in mehrere Dateien:

In C können Dateien mit #include eingebunden werden. Da diese Dateien bei jedem #include neu übersetzt werden, sollten sie keine Definitionen enthalten, die initialisierte Variablen oder ausführbaren Code erzeugen, die zuletzt mehrfach im Speicher landen könnten. Deshalb teilt man ein Modul oft auf in eine Header-Datei (.h), mit allen Definitionen und eine gleichnamige Implementierungs-Datei (.cpp) mit dem zugehörigen Code. Die IDE sorgt dann dafür, daß alle *.cpp Dateien im Projektverzeichnis compiliert und zum fertigen Programm dazugebunden werden.

Ich hab’s eben mal ausprobiert, die Verwaltung mehrerer Dateien ist etwas gewöhnungsbedürftig.
Zuerst den Sketch abspeichern, damit ein Projektverzeichnis angelegt wird.
Dann mit dem Drop-Down Button ganz rechts “New Tab” auswählen und den Dateinamen (z.B. test.cpp) eingeben.
Im einfachsten Fall mit den Funktionszeigern reicht eine *.cpp Datei, eine *.h Datei ist nicht notwendig wenn man im Sketch die Deklarationen der Funktionen direkt einfügt.

Als Beispiel die Hupe:
Sketch

//Deklarationen für alle externen Funktionen
void hupe(bool On);

const int pinHupe = 5;

void setup() {
 Serial.begin(9600);
}

void loop() {
 hupe(digitalRead(pinHupe));
}

Hupe.cpp

#include <Arduino.h>

void hupe(bool On) {
 if (On)
   Serial.println("tuuut");
}

Besten Dank, ich habe die Fahrzeug-Funktionen bereits ausgelagert in eine separate Datei. Als Fan der objektorientierten Programmierung bin ich immer verleitet, mit Klassen zu arbeiten, aber ich halte es mal so einfach wie möglich… :wink:

Ich habe den Arduino gerade nicht hier, aber ich habe einmal die Initialisierung und Prüfung aller verfügbaren Port-Expander in Schleifen gepackt:

Erstellung:

Adafruit_MCP23017 portExpander1;
Adafruit_MCP23017 portExpander2;
Adafruit_MCP23017 portExpander3;

const struct PortExpander
{
  byte address;
  Adafruit_MCP23017 portExpander;
}
portExpanders[5] = {
  { 0x20, portExpander1 },
  { 0x21, portExpander2 },
  { 0x22, portExpander3 },
};

const int numPortExpanders = sizeof(portExpanders) / sizeof(portExpanders[0]);

Konfiguration (in setup()) und Abfrage (in loop()):

// setup
for (int i = 0; i < numPortExpanders; ++i) {
  portExpanders[i].portExpander.begin(portExpanders[i].address);
  for (short i = 0; i < 16; ++i) {
    portExpanders[i].portExpander.pinMode(i, INPUT);
    portExpanders[i].portExpander.pullUp(i, HIGH);
  }
}

// loop
for (int i = 0; i < numPortExpanders; ++i) {
  uint16_t newValues = requestInputsByI2C(portExpanders[i].portExpander);
  checkForUpdates(newValues, states[0], mapping);
}

Ich kann es ohne Arduino nicht testen, aber das scheint mir schlüssig und kompiliert ohne Probleme. Hast du eventuell noch einen Tipp, wie ich

Adafruit_MCP23017 portExpander1;
Adafruit_MCP23017 portExpander2;
Adafruit_MCP23017 portExpander3;

vermeiden kann? Ich erstelle doch hier nur Objekte, die ich in das strukturierte Array packe - kann ich nicht in

portExpanders[5] = {
** { 0x20, portExpander1 },**
** …**
};

direkt ein neues Objekt erzeugen?

Ich wiederhole mich, aber besten Dank! Ihr habt mich auf einen guten Weg gebracht! :slight_smile:

Fehlerbehandlung kommt in Demo-Code immer zu kurz, da die Klarheit etwas leidet, aber wenn du explizit ein Array mit 5 Elementen definierst und nur 3 füllst, solltest du zur Laufzeit prüfen ob du daneben liegst. Zum Glück sind beim Arduino alle nicht initialisierten globalen Variablen immer 0, und das ist z.B. keine gültige I²C Adresse.

Auch würde ich eine Referenz statt einer Kopie verwenden

struct PortExpander
{
  byte address;
  Adafruit_MCP23017 & portExpander;
} portExpanders[] = {
    { 0x20, portExpander1 }
   };