Version Anpassungen
1.0 (2011-12-30) Initiale Version
2.0 (2013-12-08) Deutlich erweiterte Version
3.0 (2022-11-04) Zur Benutzung mit Arduino überarbeitet. Ebenfalls konnte vieles vereinfacht werden, weil vieles durch die Kompiler-Optimierung der modernen GCC-Version unnötig wurde. Für Details siehe Kommentare in Modifications.txt.
4.0 (2022-11-13) Fehler beim Erkennen der Startbedingung behoben: SCL wurde nicht immer frei gegeben.

Motivation

Die Idee ist, intelligente Sensoren und Aktoren durch Einsatz externer Mikroprozessoren zu realisieren. Die Kommunikation soll per I²C stattfinden.

Meine Wahl fiel auf den ATtiny85. Dieser Chip neben dem USI (Universal Serial Interface), mit dem man recht einfach eine interrupt-gesteuerte I²C-Kommunikation implementieren kann. Neben den für den I²C-Bus benötigten beiden Leitungen stehen vier  freie I/O-Leitungen, die als digitale I/O-Kanäle oder als analoge Eingänge (ADC) betrieben werden können, zur Verfügung. Zwei Timer, über die u.a. eine PWM-Steuerung eingerichtet werden kann, runden die Möglichkeiten dieses Chips ab. Außerdem ist er in einer DIL-8-Version erhältlich. 
        DIL-8:     

Damit ist er einerseits groß genug, dass man ihn mit hobbymäßigen Werkzeug anschließen kann, andererseits klein genug, um in ein akzeptables Sensorgehäuse zu passen.

Benötigt man mehr Pins, bieten andere ATtiny-Versionen mit USI-Komponente an (ATtiny25, ATtiny45, ATtiny85, ATtiny24, ATtiny44, ATtiny84, ATtiny2313, ATtiny4313).

ATtinys programmieren mit dem Arduino-Overhead? Ja, es geht! Der Compiler filtert eine Menge unnützer Dinge heraus und optimiert den Rest recht gut. Die Binaries sind erstaunlich klein.


In­halts­ver­zeich­nis

Download

Verwendung

Funktionsweise

Adressierung

Pinbelegung

API

Geräte-Adresse

Enumeration TwiTransferType

Betriebs-Modi

Betriebsmodus STANDARD

Beispiel

Betriebsmodus SIMPLE

Beispiel

Betriebs-Modus REGISTER

Beispiel


Download

Download der Quellen

Das ZIP-Archiv für Bibliothek UrsUsiTwiSlave zum Download. Die entpackten Dateien ins Verzeichnis <user>\Documents\Arduino\libraries kopieren (siehe Installing Additional Arduino Libraries).

Das Archiv enthält die Bibliotheksdateien

UrsUsiTwiSlave.tmp.h ist eine Vorlage für eine Konfigurationsdatei. Diese muss ins Projektverzeichnis kopiert und in UrsUsiTwiSlave.cfg.h umbenannt werden. Dann werden die notwendigen Anpassungen in der kopierten Datei vorgenommen.

UrsUsiTwiSlave.cfg.h wird von UrsUsiTwiSlave.h eingebunden. UrsUsiTwiSlave.h befindet sich im Bibliotheksverzeichnis. In der Arduino-IDE und im Visual-Studio-Plugin Visual Micro ist das Projektverzeichnis nicht in den Include-Pfaden eingebunden. Als Konsequenz lassen sich Dateien aus dem Projektverzeichnis nicht in Bibliotheksfunktionen einbinden sad.

Arduino-IDE: Man kopiert alle Quelldateien ins Projektverzeichnis.

Visual Micro: Bei Projekteigenschaften gibt es die Option Extra Flags. Dort kann man einen Verweis auf das Projektverzeichnis anbringen, z.B. -I"C:\Users\Ulli\Documents\Arduino\i2c-slave\i2c-slave".

Die Beispiele enthalten komplette Visual-Micro-Projekte. Arduino-IDE-Nutzer verwenden einfach nur die .ino-Datei.

Beispiel


Die erste Version eines USI-Slave-Bibliothek recht einfach aufgebaut. Die neue Version v2 bietet einen deutlich höheren Komfort.

Verwendung

Funktionsweise

Diese Bibliothek basiert auf der Atmel AppNote AVR312 - Using the USI module as a TWI slave. Sie stellt ein I²C-Framework auf Basis einer USI zu Verfügung, das über eine Konfigurationsdatei verschiedenen Projektbedürfnissen angepasst werden kann.

Das I²C-Protokoll / die I²C-Datenübertragung besteht aus folgenden Komponenten:

Adressierung

I²C-Geräte haben üblicherweise eine feste Bus-Adresse. Um mehrere gleichartige Geräte am gleichen Bus betreiben zu können, müssen sie unterschiedliche Adressen besitzen. Dies geschieht meist, indem einige Pins des ICs zur Modifikation der Adresse benutzt werden. Der μC muss dazu die Pins auslesen, die Adresse berechnen und der Bibliothek bei der Initialisierung (TwiInitialize) oder später (TwiSetAddress) zur Verfügung stellen.

Pins sind jedoch auf vielen ATtinys knapp. Deshalb besteht die Möglichkeit mit festen Adressen zu arbeiten. Dies spart etwas von dem geringen Speicher der ATtinys und ist auch ein wenig schneller. Der Nachteil ist, dass man für jedes Gerät ein eigenes Binary benötig. Und, wenn man nicht sorgfältig dokumentiert hat, welche Adresse man einprogrammiert hat, ist das Gerät meist wertlos.

Die Bibliothek bietet eine dritte Möglichkeit. Man kann sie so konfigurieren, dass man auch über die Broadcast-Adresse 0 mit dem Gerät kommunizieren kann. Die Callback-Funktion TwiTransmissionStart meldet sowohl die Datenrichtung (Lesen/Schreiben) als auch ob die Broadcast-Adresse verwendet wurde. Wird die Datenübertragung über die Broadcast-Adresse betrieben, kann beim Lesen das Gerät die eingestellte Adresse zurück liefern und beim Schreiben die Geräteadresse entsprechend einstellen und z.B. im EEPROM ablegen. Das folgende Code-Snippet verdeutlicht dieses Vorgegen:

TwiTransferType currentTransferType; // Speicher für Übertragungsmodus
uint8_t myI2cAddress; // Aktuelle Geräte-Adresse

void setup() {
   myI2cAddress = EEPROM.read(0); // Geräte-Adresse aus dem EEPROM lesen
   TwiInitialize(myI2cAddress);   // Bibliothek mit dieser Adresse initialisieren

   // ...
}

void TwiTransmissionStart(TwiTransferType rw) {
   currentTransferType = rw; // Übertragungsmodus speichern
}

uint8_t TwiDataRequest() {
    if (TwiIsBroadcast(currentTransferType))
      return myI2cAddress;  // Geräte-Adresse zurück liefern

   // ...
}

void TwiDataReceived(uint8_t data) {
    if (TwiIsBroadcast(currentTransferType)) {
      myI2cAddress = data; // Neue Geräte-Adresse benutzen
      EEPROM.write(0, data);
      TwiSetAddress(data);
      return;
   }

   // ...
}

Pinbelegung

Die Pins sind durch die Beschaltung der USI (Universal Serial Interface) festgelegt (in Klammern die Pin-Nummern im DIL-Gehäuse):

MCU SDA SCL
ATtiny24, 44, 84 PA6 (7) PA4 (9)
ATtiny25, 45, 85 PB0 (5) PB2 (7)
ATtiny26 PB0 (1) PB2 (3)
ATtiny261, 461, 861 PB0 (1) PB2 (3)
ATtiny2313, ATtiny4313 PB5 (17) PB7 (19)
ATmega165, ATmega325, ATmega3250,
ATmega645, ATmega6450,
ATmega329, ATmega3290, ATmega169
PE5 PE4

API

Geräte-Adresse

Das Modul besitzt zwei Adress-Modi. Über die Konstanten TWI_FIXED_TWI_ADDRESS kann diese konfiguriert werden:

Adress-Modi
NONE Die Präprozessor-Konstante TWI_FIXED_TWI_ADDRESS hat die Ausprägung NONE. Die Geräte-Adresse wird bei der Modul-Initialisierung über void TwiInitialize(uint8_t TwiAddress) festgelegt und kann zu einem späteren Zeitpunkt über void TwiSetAddress(uint8_t TwiAddress) geändert werden.
FIXED Die Geräte-Adresse wird über die Präprozessor-Konstante TWI_FIXED_TWI_ADDRESS vorgegeben und beim Kompilieren fest eingestellt. Die Initialisierung erfolgt über void TwiInitialize (void) deklariert. TwiSetAddress steht nicht zur Verfügung. Mögliche Werte für TWI_FIXED_TWI_ADDRESS sind 1..127.

Die Definition von TWI_FIXED_TWI_ADDRESS und die Belegung mit einem der zuvor genannten Werte ist obligatorisch und wird beim Kompilieren überprüft.

In beiden Adress-Modi kann über TWI_ACCEPT_BROADCAST_MSG festgelegt werden, ob zusätzlich zu der oben festgelegten Adresse auch die I²C-Broadcast-Adresse 0 akzeptiert wird. Mögliche Werte sind TRUE oder FALSE. Die Definition von TWI_ACCEPT_BROADCAST_MSG und die Belegung mit einem der zuvor genannten Werte ist obligatorisch und wird beim Kompilieren überprüft.

Enumeration TwiTransferType

Beim Start einer Datenübertragung wird die Callback-Funktion TwiTransmissionStart(TwiTransferType rw) aufgerufen. rw gibt an, welche Art von Übertragung gestartet wurde:

enum class TwiTransferType {
   TwiRead = 0,    // Der Master will Daten von diesem Slave empfangen
   TwiWrite = 1,    // Der Master sendet Daten an diesen Slave
#if (TWI_ACCEPT_BROADCAST_MSG)
   BroadcastRead = 2,  // Der Master will Daten von allen Slaves empfangen
   BroadcastWrite = 3, // Der Master sendet Daten an alle Slaves
#endif // ACCEPT_BROADCAST_MSG
};

Zur Vereinfachung dient die Methode

Methode Bedeutung
bool TwiIsBroadcast(TwiTransferType rw) Liefert true, wenn die Übertragung über die Broadcast-Adresse 0 gestartet wurde.

Betriebs-Modi

Es stehen drei Modi zur Auswahl:

Betriebs-Modi
STANDARD Synchronbetrieb. Es gibt keine Unterstützung durch die Bibliothek. Zu sämtlichen relevanten Ereignissen werden Callback-Funktionen aufgerufen, die von der Applikation implementiert werden müssen.
SIMPLE Asynchronbetrieb. Es wird jeweils nur ein einzelnes Byte zur Verfügung gestellt. Lese- und Schreibzugriffe des Masters werden vollkommen über interne Funktionen bedient. Der Datenaustausch erfolgt über die Variable TwiData.
REGISTER Synchronbetrieb. Die Daten werden in einem strukturierten und adressierbaren I²C-Register zur Verfügung gestellt bzw. dort abgelegt.  Lese- und Schreibzugriffe des Masters werden vollkommen über interne Funktionen bedient und können durch zusätzliche Callback-Funktionen überwacht werden.

Der Modus wird über die Präprozessor-Konstante TWI_OPERATION_MODE eingestellt. Mögliche Werte sind TWI_MODE_STANDARD, TWI_MODE_SIMPLE oder TWI_MODE_REGISTER. Die Definition von TWI_OPERATION_MODE und die Belegung mit einem der zuvor genannten Werte ist obligatorisch und wird beim Kompilieren überprüft.

Betriebsmodus STANDARD

Es werden keine Automatismen implementiert. Die Callback-Funktionen

Methode Bedeutung
void TwiTransmissionStart(TwiTransferType rw) Eine neue Datenübertagung hat begonnen (Start-Condition erkannt und übertragene Geräteadresse passt). Zu rw s.o.
void TwiTransmissionStop() Die Datenübertragung ist beendet (Stop- oder Repeated-Start-Condition erkannt).
uint8_t TwiDataRequest() Der Master fordert Daten an. TwiDataRequest liefert jeweils das nächste Byte.
void TwiDataReceived(uint8_t data) Der Master sendet Daten, data ist das gesendete Byte.

müssen von der Applikation implementiert werden.

Wird eine der Methoden von der Applikation nicht benötigt, kann sie als static inline in UrsUsiTwiSlave.cfg.h hinterlegt werden. z.B.:

static inline uint8_t TwiDataRequest() {
      return 0; // Immer 0 senden
}

Der Compiler wird sie heraus optimieren.

Beispiel

Das folgende Beispiel zeigt die Verwendung dieses Modus.

Die Konfigurationsdatei für dieses Beispiel:

#pragma once

/*
 * UsiTwiSlave.cfg.h
 */

// Diese Datei enthält projektspezifische Einstellungen für das USI-TWI-Slave-Modul. 

// --- Adress-Modus -----------------------------------------------------------------------------------------------------
#define TWI_FIXED_TWI_ADDRESS     0x30    // Feste Bus-Adresse des TWI-Slaves.

#define TWI_ACCEPT_BROADCAST_MSG  FALSE   // Transferanforderung über die Broadcast-Adresse (0) werden nicht akzeptiert.

// --- Betriebs-Modus ---------------------------------------------------------------------------------------------------
#define TWI_OPERATION_MODE TWI_MODE_STANDARD // Einstellung des Betriebs-Modus: Das API meldet I2C-Aktivitäten 
                                             // über die Callback-Funktionen
                                             // TwiTransmissionStart, TwiTransmissionStop, TwiDataReceived, TwiDataRequest.

Der Applikation implementiert die geforderten Methoden. Wird ein Byte empfangen, wird der um 1 erhöhte Wert abgelegt und bei der nächsten Datenanforderung zurück geliefert. Außerdem wird, abhängig vom LSB des empfangenen Bytes Pin 4 (PB4 beim ATtiny85) geschaltet. Eine LED an diesem Pin kann zur Kontrolle des Empfangs dienen.

/*
 Name:	i2c_slave.ino
 Created:	2022-11-05
 Author: 	Ulli
*/

#include "UrsUsiTwiSlave.h"

uint8_t twiData = 0; // Zwischenspeicher für das empfangene Byte

void setup() {
   TwiInitialize();
   pinMode(4, OUTPUT);
}

void loop() {
   // Nothing to do
}

uint8_t TwiDataRequest() {
   return twiData;
}

void TwiDataReceived(uint8_t data) {
   twiData = data + 1;
   digitalWrite(4, data & 0x01);
}

void TwiTransmissionStart(TwiTransferType rw) {} // Leere Funktion
void TwiTransmissionStop() {}  // Leere Funktion

Das Beispiel befindet sich im Ordner examples\i2c-slave-standard.

Betriebsmodus SIMPLE

In diesem Modus wird jeweils nur ein einzelnes Byte übertragen. Dieses Byte wird jeweils in der Variablen TwiData zur Verfügung abgelegt. Über die Variable TwiDataReceivedFlag kann abgefragt werden, ob ein neues Byte empfangen wurde. Nach Auswertung des Flags muss dieses von der Applikation gelöscht werden.

Die Implementierung dieser Variante ist einfacher als die anderen, hat aber den Nachteil, das Daten verloren gehen können.

Beispiel

Das folgende Beispiel zeigt die Verwendung dieses Modus.

Die Konfigurationsdatei für dieses Beispiel:

#pragma once

/*
 * UsiTwiSlave.cfg.h
 */

// Diese Datei enthält projektspezifische Einstellungen für das USI-TWI-Slave-Modul. 

// --- Adress-Modus -----------------------------------------------------------------------------------------------------
#define TWI_FIXED_TWI_ADDRESS     0x30    // Feste Bus-Adresse des TWI-Slaves.

#define TWI_ACCEPT_BROADCAST_MSG  FALSE   // Transferanforderung über die Broadcast-Adresse (0) werden nicht akzeptiert.

// --- Betriebs-Modus ---------------------------------------------------------------------------------------------------
#define TWI_OPERATION_MODE TWI_MODE_SIMPLE // Einstellung des Betriebs-Modus: Das API meldet I2C-Aktivitäten 
                                           // über das Flag TwiDataReceivedFlag und die Variable TwiData.

Der Applikation implementiert die geforderten Methoden. Wird ein Byte empfangen, wird der um 3 erhöhte Wert abgelegt und bei der nächsten Datenanforderung zurück geliefert. Außerdem wird, abhängig vom LSB des empfangenen Bytes Pin 4 (PB4 beim ATtiny85) geschaltet. Eine LED an diesem Pin kann zur Kontrolle des Empfangs dienen.

/*
 Name:		i2c_slave.ino
 Created:	2022-11-05
 Author: 	Ulli
*/

#include "UrsUsiTwiSlave.h"

void setup() {
   TwiInitialize();
   pinMode(4, OUTPUT);
}

void loop() {
   if (TwiDataReceivedFlag) {
      TwiData = TwiData + 3;
      digitalWrite(4, TwiData & 0x01);
      TwiDataReceivedFlag = false;
   }
}

Das Beispiel befindet sich im Ordner examples\i2c-slave-simple.

Betriebs-Modus REGISTER

In diesem Modus werden die Daten in einem strukturierten und adressierbaren I²C-Register zur Verfügung gestellt bzw. dort abgelegt. Jede Schreiboperation des Maters beginnt in diesem Betriebsmodus zunächst mit der Übertragung des Registerindex (ein Byte). Jedes weitere Byte wird dann an der vom Index angegeben Stelle abgelegt und der Index inkrementiert.

Das Lesen erfolgt ebenfalls an der vom Index angegeben Stelle. Durch das Schreiben eines einzelnen Bytes (= Registerindex) vor dem Lesebefehl kann diese Stelle verschoben werden.

Die Aufbau des I²C-Registers wird über den Typ I2CRegister_t festgelegt. Dieser Typ muss in UsiTwiSlave.cfg.h definiert werden. Am sinnvollsten wird er als struct hinterlegt:

typedef struct
{ ...
} I2CRegister_t;

Es sind aber auch andere Darstellungen möglich. Intern wird auf das Register stets mit dem Typ-Konverter ((uint8_t *)&I2CRegister)[] zugegriffen.

Die Verwaltung des Index (RegisterIndex) zum Zugriff auf das I²C-Register erfolgt intern. Wird der Index durch den Master auf einen Wert gesetzt, der größer als die Größe des Registers ist, wird der Index auf das letzte Byte des Registers gesetzt. Ähnliches gilt beim Inkrementieren des Index. Würde er zu Zugriffen außerhalb das Register-Speicherraums führen, wird das Inkrementieren unterdrückt.

Folgende Callback-Methoden müssen von der Applikation implementiert werden:

Methode Bedeutung
void TwiTransmissionStart(TwiTransferType rw) Eine neue Datenübertagung hat begonnen (Start-Condition erkannt und übertragene Geräteadresse passt). Zu rw s.o.
void TwiTransmissionStop() Die Datenübertragung ist beendet (Stop- oder Repeated-Start-Condition erkannt).
bool TwiDataReceived(uint8_t index, uint8_t data) Der Master sendet Daten
index: Registerindex, an den das Byte geschrieben würde.
data ist das gesendete Byte.
Rückgabewert:
true, wenn das Byte bereits ausgewertet wurde und nicht ins Register geschrieben werden soll.
false: Das Byte wird an die Stelle index in das Register geschrieben.
bool TwiDataRequest(uint8_t index, uint8_t* pData) Der Master fordert Daten an.
index: Registerindex, aus dem das Byte gelesen würde.
pData: Pointer zu Übergabe des zu sendenden Bytes.
Rückgabewert:
true: Das Byte, das über pData beschrieben wurde, wird gesendet.
false: Das Byte, auf das index im Register zeigt, wird gesendet.

Wird eine der Methoden von der Applikation nicht benötigt, kann sie als static inline in UrsUsiTwiSlave.cfg.h hinterlegt werden. z.B.:

static inline bool TwiDataReceived(uint8_t index, uint8_t data) {
      return false; // Byte ins Register speichern.
}

Der Compiler wird sie heraus optimieren.

Beispiel

Das Beispiel zu diesem Betriebsmodus zeigt, wie man komplexere Protokolle gestalten kann. Die Geräteadresse des Slaves soll per I²C eingestellt und im EEPROM gespeichert werden können. Das Senden einer neuen und das Abfragen der aktuellen Adresse erfolgt über die Broadcast-Adresse 0. Schreib- und Lese-Operation erfolgen über die reguläre-Adresse in das / aus dem Register. Zur sichtbare Kontrolle wird eine LED nach jedem Übertragungsvorgang umgeschaltet. Das Beispiel funktioniert mit dem I²C-Master, wie er in Arduino Software I2C Master: I²C auf allen Pins, Beispiel 2 beschrieben ist.

Die Konfigurationsdatei für dieses Beispiel:

#pragma once

/*
 * UsiTwiSlave.cfg.h
 */

// Diese Datei enthält projektspezifische Einstellungen für das USI-TWI-Slave-Modul. 

// --- Adress-Modus ------------------------------------------------------------------------------------------------------

#define TWI_FIXED_TWI_ADDRESS     NONE   // Variable Bus-Adresse des TWI-Slaves.

#define TWI_ACCEPT_BROADCAST_MSG  TRUE   // Transferanforderung über die Broadcast-Adresse (0) werden akzeptiert.

// --- Betriebs-Modus ----------------------------------------------------------------------------------------------------

#define TWI_OPERATION_MODE TWI_MODE_REGISTER // Einstellung des Betriebs-Modus

  //******************************************************************
  // Festlegung der I2C-Register-Struktur
  //******************************************************************
typedef struct
{
   uint8_t Version;
   uint8_t Count;
} I2CRegister_t;

#define I2C_REGISTER_DEFAULT 13, 0 // Vorbelegung des Registers. Wird so benutzt: I2CRegister = {I2C_REGISTER_DEFAULT};
#define TWI_WRITE_PERMIT     1     // Schreiben ab dieser Position erlauben

Das I²C-Register enthält zwei Byte. Das erste Byte ist eine Versionsnummer, die nicht überschrieben werden soll (TWI_WRITE_PERMIT 1). Die Vorbelegung ist 13. Das zweite Feld count wird mit 0 vorbelegt (I2C_REGISTER_DEFAULT 13, 0).

Der Programmcode des Slaves:

/*
 Name:	 i2c_slave.ino
 Created: 2022-11-07
 Author:  Ulli
*/

#include <EEPROM.h>
#include "UrsUsiTwiSlave.h"

TwiTransferType currentTransferType; // Speicher für Übertragungsmodus
uint8_t myI2cAddress;                // Aktuelle Geräte-Adresse

bool ledState = false;               

void setup() {
   myI2cAddress = EEPROM.read(0); // Geräte-Adresse aus dem EEPROM lesen
   if (myI2cAddress == 0xFF)      // EEPROM nicht belegt
      myI2cAddress = 0x30;        // 0x30 ist die Voreinstellung
   TwiInitialize(myI2cAddress);   // Bibliothek mit dieser Adresse initialisieren

   pinMode(4, OUTPUT);
}

void loop() {
   // Nichts zu tun
}

void TwiTransmissionStart(TwiTransferType rw) {
   currentTransferType = rw; // Übertragungsmodus speichern
}

void TwiTransmissionStop() { // Übertragung beendet
   // Hier erfolgt die Reaktion auf die Änderung der Registerinhalte
   ledState = !ledState;
   digitalWrite(4, ledState);
}

bool TwiDataRequest(uint8_t index, uint8_t* pData) {
   if (TwiIsBroadcast(currentTransferType)) {
      *pData = myI2cAddress;  // Geräte-Adresse zurück liefern
      return true;
   }
   else
      return false; // Byte aus dem Register lesen.
}

bool TwiDataReceived(uint8_t index, uint8_t data) {
   if (TwiIsBroadcast(currentTransferType)) {
      myI2cAddress = data; // Neue Geräte-Adresse benutzen
      EEPROM.write(0, data);
      TwiSetAddress(data);
      return true; // Auswertung des Byte abgeschlossen, nicht ins Register speichern.
   }

   // Hier erfolgt ggf. die Auswertung des gesendeten Bytes.

   return false; // Byte ins Register speichern.
}

Der Programmcode des Masters:

/*
 Name:    ArduinoSoftI2c-Test
 Created: 2022-11-07
 Author:  Ulli (https://ullisroboterseite.de)
 Docs:    https://ullisroboterseite.de/arduino-soft-i2c.html
*/


#include <UrsArduinoSofti2c.h>

const uint8_t sda = 8; // SDA pin is pin 8
const uint8_t scl = 9; // SCL pin is pin 9
uint8_t devAdr = 0x30; // Device address

uint8_t readBuffer[] = { 0, 0 };
uint8_t zeroRegAdr = 0;
uint8_t cnt = 0;


UrsArduinoSoftI2cClass& i2c = SoftI2c; 

void setup() {
   Serial.begin(9600);
   delay(10);
   Serial.println("I2C-Test");

   i2c.begin(sda, scl);

   uint8_t rtc = i2c.readPacket(0, readBuffer, 1); // read address
   if (rtc == 1) {
      devAdr = readBuffer[0];
      Serial.print("Device address: "); Serial.println(readBuffer[0]);
   }
   else {
      Serial.println("Device address cannot be determined");
      while (true); // ... and stopp
   }
}



void loop() {
   cnt ++;

   // Registerfeld 'count' (Index 1) beschreiben
   uint8_t rtc = i2c.writeToRegister(devAdr, 1, &cnt, 1);
   if (rtc == 1) {
      Serial.println("Write ok");
   }
   else
      Serial.println("Write error");

   // Felder 'version' (index 0) und 'count' (index 1) auslesen
   rtc = i2c.readFromRegister(devAdr, 0, readBuffer, sizeof(readBuffer));
   if (rtc == sizeof(readBuffer)) {
      Serial.print("read ok: "); Serial.print(readBuffer[0]); Serial.print(" "); Serial.println(readBuffer[1]);
   }
   else
      Serial.println("read error");

   Serial.println();
   delay(1000);
}