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:
- Die Initialisierung der Register muss vor der Freigabe der Interrupts erfolgen. Also erst
I2CSlave_Initialise() und danach erst
sei().
- Die Callback-Funktionen sind zeitkritisch. Mehr als die Daten zwischen zu speichern und ein
Signal zu setzen ist nicht möglich. Wird dies nicht beachtet, gerät die Synchronisation
mit dem I²C-Bus aus Takt.
- Das Modul akzeptiert sowohl die vorgesehene Bus-Adresse als auch die Adresse 0. Wenn man also
die Adresse per Software änderbar im EEPROM ablegt , kann man bei unbekannter (fehlgeschlagener
oder vergessener) Bus-Adresse über die Ansprache des Geräts mit der Adresse 0 wieder
definierte Zustände herstellen. Natürlich sollte hierbei das zu konfigurierende Gerät
allein angeschlossen sein.
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)