Die Idee ist, intelligente Sensoren und Aktoren durch Einsatz externerMicroprozessoren zu realisieren. Die Kommunikation mit dem NXT soll per I²C stattfinden.

Meine Wahl fiel auf den ATtiny45. 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 Sensorghäuse zu passen.

Das ATtiny45 Datenblatt kann bei Atmel abgerufen werden. Hier findet man auch bei den Atmel Application Notes unter "AVR312: Using the USI module as a I2C slave" eine Dokumentation und auch Code, wie man mit dem USI-Modul einen I²C-Slave realisieren kann. Der Beispiel-Code hat aber einen gravierenden Fehler (Details siehe hier).

Zu Testzwecken ist es am einfachsten, den I²C-Slave zunächst über den PC anzusteuern. Dazu habe ich einen USB2I2C-Adapter benutzt. Das gleiche Gerät kann man über eine Vielzahl von Anbietern beziehen.

Vorbereitung des ATtiny:
Der Adapter arbeitet mit einer Taktfrequenz von 100kHz und beherrscht wohl kein Clock-Stretching (oder hier nachlesen). Das Datenblatt zum Modul gibt hierzu keine Auskunft.

Damit der µC hier noch mitkommt, muss er auf die höchstmögliche Frequenz gestellt werden. Dazu müssen die CKSEL fuses auf ‘0001’ eingestellt werden. In PonyProg sieht das wie folgt aus:

Wenn der Chip später an den NXT angeschlossen wird, kann man mit niedrigeren Frequenzen arbeiten. Der NXT bedient in der Standardeinstellung den I²C-Bus mit einer Taktfrequenz von nur 9600 Hz.

I²C-Slave-Modul (Download):
Das Beispiel aus der Applikation Note habe ich so umgebaut, dass über vier Interface-Funktionen betrieben werden kann. Im wesentlichen wurde der enthaltene Ringpuffer durch Callback-Funktionen ersetzt. Außerdem habe ich einige Anweisungen umgestellt, damit das ganze etwas weniger zeitkritisch wird.

I²C-Slave-Interface
void I2CSlave_Initialise(unsigned char I2CAddress) Initialisiert die USI-Register für den I2C Modus
void I2CSlave_TransmissionStart(void) Signalisiert, dass ein neuer Übertragungszyklus begonnen hat.
Diese Funktion wird aufgerufen, wenn audem I²C-Bus die Startkonfiguration erkannt wurde.
Wichtig: Die Funktion muss schnell zurückkehren!
unsigned char I2CSlave_DataRequest(void) Liefert die Daten für das I2C-Modul.
Diese Funktion wird aufgerufen, wenn das I2C-Modul aufgefordert wurde, Daten zu liefern (Read-Request des Masters).
Wichtig: Die Funktion muss schnell zurückkehren!
void I2CSlave_DataReceived(unsigned char data) Liefert Daten aus dem I2C-Modul.
Diese Funktion wird aufgerufen, wenn das I2C-Modul Daten erhalten hat (Send-Request des Masters).
Wichtig: Die Funktion muss schnell zurückkehren!
void I2CSlave_SetI2CAddress(unsigned char I2CAddress) Legt eine neue I2C-Adresse fest.

Anmerkungen:
Testprogramme:
Einfaches "Single Byte" Programm
Ein über den I²C-Bus empfangenes Byte wird beim nächsten Lesevorgang zurückgesandt.

#include "I2CSlave.h"
#include <interrupt.h>

int main( void )
{ //*****************************************************
  // Initialize I2C Module
  //*****************************************************
  unsigned char I2C_slaveAddress = 0x10; //Set TWI slave address
  I2CSlave_Initialise(I2C_slaveAddress);
  sei(); // Enable interrupts

  for(;;) // This loop runs forever.
  {
  }
} // main


//*****************************************************
// I2C Callbacks
//*****************************************************
 unsigned char tmp = 0xFF; // Nicer than zero :-)

unsigned char I2CSlave_DataRequest(void)
{ return tmp; // Transmit saved byte
}

void I2CSlave_DataReceived(unsigned char data)
{ tmp = data; // Save received byte
}

void I2CSlave_TransmissionStart(void)
{ //Nothing to do
}


Einfaches "Multi Byte" Programm
Über den I²C-Bus empfangene Bytes werden in einem Array gespeichert. Das Array wird bei jeder Übertragung komplett gefüllt.

#include "I2CSlave.h"
#include <interrupt.h>

int main( void )
{ //*****************************************************
  // Initialize I2C Module
  //*****************************************************
  unsigned char I2C_slaveAddress = 0x10; //Set TWI slave address
  I2CSlave_Initialise(I2C_slaveAddress);
  sei(); // Enable interrupts

  for(;;) // This loop runs forever.
  {
  }
} // main


//*****************************************************
// I2C Callbacks
//*****************************************************
 #define BUFFER_SIZE 4
 unsigned char buffer[BUFFER_SIZE];
 unsigned char ind=0;

// Xmit data from buffer
unsigned char I2CSlave_DataRequest(void)
{ unsigned char tmp = buffer[ind++];
  if (ind >= BUFFER_SIZE) // No buffer overflow
     ind = BUFFER_SIZE -1;
  return tmp; // Rerturn data form buffer
}

// Save data in buffer
void I2CSlave_DataReceived(unsigned char data)
{ buffer[ind++] = data;   // Save received Byte
  if (ind >= BUFFER_SIZE) // No buffer overflow
     ind = BUFFER_SIZE -1;}
}

void I2CSlave_TransmissionStart(void)
{ ind = 0;   // Reset buffer index
}

Anmerkungen
Dies ist ein Testprogramm um die Funktionen zu demonstrieren und nicht für einen realen Einsatz geeignet. Die I²C-Verbindung kann jederzeit gestört werden. Dann hätte man unvollständige Daten im Puffer.
Ebenso könnte während des Auslesens der Daten ein anderer Thread den Puffer modifizieren.

Für den Praxisbetrieb darf nicht direkt in den Puffer geschrieben oder aus ihm gelesen werden. Die Daten müssen zwischengespeichert werden.
  • Beim Empfang wird der Zwischenspeicher erst nach vollständiger Übertragung umkopiert. Dies erfolgt sinnvollerweise in I2CSalve_DataReceived, wenn alle erwarteten Bytes angekommen sind. Die temporären Daten werden verworfen, wenn der Empfang nicht vollständig ist. Dies erfolgt entweder in I2CSlave_TransmissionStart() oder über einen Timeout-Mechanismus.
  • Beim Senden müssen die Daten zunächst in einen Zwischenspeicher übernommen werden. Die Speisung des I²C-Moduls erfolgt dann aus diesem Zwischenspeicher heraus. Zum umkopieren ist wiederum I2CSlave_TransmissionStart() geeignet.
  • Der Zugriff auf den Puffer beim Umkopieren muss über Prozess-Synchronisations-Mechanismen (Mutex, Semaphore, etc.) geregelt werden.
  • Wenn zu viele Operationen in den Callback-Funktionen durchgeführt werden, z.B. die zu kopierenden Datenmengen zu groß werden, geht die Synchronisation mit dem I²C-Bus verloren. Es ist also sinnvoll, nur die ersten notwendigsten Daten zu übertragen und ein Signal zu setzen. Die restliche Übertragung der Daten kann dann über das Hauptprogramm erfolgen, während das I²C-Modul bereits die ersten Daten überträgt oder empfängt.
Sourcecode (Download)