ATTiny als I²C Slave mit USI – GREETBoard I2C Slave

Im letzten Artikel „Der ATTiny2313 als Sub-Controller“ stellte ich das GREETBoard I²C Slave vor, welches einen ATTiny2313 nutzt. Dieses Zusatzmodul soll vor allem als Sub-Controller verwendet werden, um bestimmte Funktionen auslagern zu können. Somit kann sich das Hauptmodul, des GREETBoard ATMega128, um andere Dinge kümmern. Die Kommunikation mit dem Hauptmodul erfolgt über den I²C-Bus. Da der ATTiny2313 ein vollwertiger (wenn auch kleiner) Mikrocontroller ist, sind sogar Projekte möglich, in dem das Zusatzmodul als Hauptmodul agiert. Es muss lediglich eine Spannungsversorgung von 5V von außen erfolgen.

Die USI-Schnittstelle

Bei den großen Geschwistern des ATTiny2313, z.B. einem ATMega16, sind die Schnittstellen separat auf die Pins herausgeführt. So hat die USART-Schnittstelle eigene Anschlüsse, die sich mit keinem der anderen Schnittstellen (wie z.B. I²C, SPI, etc.) überschneiden. Bedingt durch die geringe Anzahl von Anschlüssen der ATTiny-Controller (der ATiny2313 hat gerade einmal 20 Beinchen), hat Atmel hier ein anderes Konzept gewählt – das Universal Serial Interface, kurz USI. Mit einfachen Worten erklärt, ist USI eine Zusammenfassung der I²C- und der SPI-Schnittstelle. Das schöne ist, dass die USI-Pins einfach mit beiden Anschlüssen verbunden werden. Solange die beiden Schnittstellen nicht gleichzeitig in Betrieb gehen, kommt es auch nicht zu gegenseitigen Störungen, trotz der gemeinsamen Nutzung.

Im folgenden Schaltplan habe ich die USI-Schnittstelle am ATTiny2313 (Nr. 1) rot umrandet. An den Pins 17-19 befinden sich die entsprechenden Anschlüsse. Die Programmierbuchse X1 (Nr. 2) und auch die I2C-Schnittstellen X2 und X5 (Nr. 3) greifen auf exakt die gleichen Anschlüsse zu. Natürlich ist somit ein Programmieren, während der I²C-Bus in Betrieb ist, nicht möglich. Aber ein kurzes Abkoppeln reicht, um den ATTiny zu programmieren. Danach kann das Modul wieder in den I²C-Bus integriert werden.

GREETBoard I2C Slave - USI Schnittstelle

GREETBoard I2C Slave – USI Schnittstelle

Der Master

In einem I²C-Bus gibt es immer nur einen Master, der alle anderen Busteilnehmer anspricht um sie auszulesen oder zu beschreiben. Die dafür notwendige Software ist exakt die, die ich für jeden Bus-Master nutze, nämlich die twimaster Bibliothek von Peter Fleury. Wie eine Bibliothek eingebunden wird, habe ich im Artikel „Library Dateien in AVR Studio 5.1 einbinden“ beschrieben. Die Erklärung gilt auch für AVR Studio 6.0 und 6.1.

Das Programm in kleinen Schritten

Wie bei allen Programmen auf timogruss.de erkläre ich Schritt-für-Schritt, wie diese funktionieren. Dabei möchte ich aber nicht unerwähnt lassen, dass das ursprüngliche Programm das „Slaves“ nicht aus meiner Feder, sondern von Martin Junghans stammt und nach „GNU General Public License“ lizenziert wurde. Daher ist auch meine geänderte Version ebenso lizenziert.

Die Headerfiles usiTwiSlave.h und usiTwiSlave.c werden unverändert übernommen.

Die Header-Dateien

Zu Beginn erfolgt, wie immer, die Einbindung benötigter Header-Dateien. Hervorzuheben ist hierbei die letzt Datei „usiTwiSlave.h“, die die Funktionen für die Kommunikation über den I²C-Bus bereitstellt.

#include <stdlib.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include "usiTwiSlave.h"

Definitionen

Die Library beinhaltet ein paar interessante Zusatzfunktionen, die das Auslesen und Manipulieren von einzelnen Bits deutlich erleichtern. Diese werde ich erklären, nachdem die Adresse des ATTiny im I²C-Bus bestimmt wurde. In diesem Beispiel ist es die 0x50 (Hex), bzw. 00110100. Dabei ist zu beachten, dass das niedrigste Bit (das ganz rechts) je nachdem ob gelesen oder geschrieben wird, 1 oder 0 ist. Anschließend wird der Quarztakt definiert (hier 20MHz)

#define SLAVE_ADDR_ATTINY 0x50 //0b00110100

#ifndef F_CPU
  #define F_CPU 20000000UL
#endif

Die nächsten Definitionen beinhalten gleichzeitig je eine Funktion (die zuvor erwähnten Zusatzfunktionen), die je einen Rückgabewert erzeugt.

UNIQ fügt zwei 8-Bit Binärzahlen zu einer 16-Bit Binärzahl zusammen. Das ist sehr nützlich, wenn eine Berechnung mit einer 16-Bit Binärzahl erfolgt und diese über den Bus empfangen wird. Dieser sendet ausschließlich ganze Bytes, also 8-Bit Zahlen, sodass eine 16-Bit aus zwei 8-Bit Zahlen zusammengesetzt werden muss. Dabei steht LOW für das niederwertige Byte (also die rechten acht Stellen) und HIGH für das Höherwertige (die linken acht Zahlen).

LOW_BYTE und HIGH_BYTE machen das genaue Gegenteil, sie lesen aus einer 16-Bit Binärzahl das untere (LOW), bzw. das obere (HIGH) Byte heraus.

#define UNIQ(LOW,HEIGHT) ((HEIGHT << 8)|LOW) // Erstelle 16 Bit aus zwei Bytes
#define LOW_BYTE(x) (x & 0xff)               // Hole Low-Byte von 16 Bit-Zahl
#define HIGH_BYTE(x) ((x >> 8) & 0xff)       // Hole High-Byte von 16 Bit-Zahl

Wurden zuvor ganze Bytes auseinander genommen und wieder zusammengesetzt, so sind die nächsten drei Funktionen für die Manipulation einzelner Bits zuständig. Alle drei Funktionen erwarten eine Variable mit dem Namen ADDRESS und BIT. Hinter ADDRESS verbirgt sich das zu ändernde Byte, bzw. die Variable, die das entsprechende Byte enthält. Mit BIT wird die zu ändernde Stelle im Byte angegeben. Hier ein Beispiel: Wird als ADDRESS ein 0x4E (also 01001110) und als BIT eine 4 übergeben, dann würde das Bit an vierter Stelle von rechts verändert. Sbi setzt es (1), cbi löscht es (0) und toggle „schaltet“ das Bit um, sodass aus einer 1 eine 0, bzw. aus einer 0 eine 1 wird. Sehr praktisch um einzelne Bit als „Schalter“ zu verwenden

#define sbi(ADDRESS,BIT) ((ADDRESS) |= (1<<(BIT)))  // Setzte Bit
#define cbi(ADDRESS,BIT) ((ADDRESS) &= ~(1<<(BIT))) // Lösche Bit
#define toggle(ADDRESS,BIT) ((ADDRESS) ^= (1<<BIT)) // Schalte Bit um

Im Kapitel der Bitmanipulation habe ich bereits erklärt, wie einzelne Bits ausgelesen werden können. So ist es möglich auf veränderte Bits zu reagieren, ganz so, als wenn ein Schalter ein- oder ausgeschaltet wird. Mit den folgenden beiden Funktionen ist diese Abfrage viel einfacher möglich. ADDRESS und BIT werden auch hier verwendet, um das gewünschte Bit auszuwählen.

#define bis(ADDRESS,BIT) (ADDRESS & (1<<BIT))    // Is bit set?
#define bic(ADDRESS,BIT) (!(ADDRESS & (1<<BIT))) // Is bit clear?

Ich möchte noch kurz darauf hinweisen, dass einige der Definitionen/Funktionen nicht im Programm verwendet werden. Sie sind für die Bus-Verbindung nicht nötig, aber für eventuelle Erweiterungen sehr praktisch. 🙂

Variablen

Im weiteren Verlauf benötigt das Programm ein paar Variablen, die vorab definiert werden müssen. Dabei sind word und buffer für die 16-Bit- und die restlichen Variablen für die 8-Bit Binärzahlen zuständig. Eigentlich gibt es hier nichts wirklich Besonderes zu beachten oder zu erklären.

uint16_t word=0;
uint8_t  byte1, byte2;
uint16_t buffer;
uint8_t  high,low = 0;

Das Hauptprogramm

Zu Beginn des Hauptprogramms wird zunächst die I²C-Verbindung initialisiert.

int main(void) {
  cli();                              // Interrupts aus
  usiTwiSlaveInit(SLAVE_ADDR_ATTINY); // TWI slave init
  sei();                              // Interrupts ein

Im nächsten Schritt werden einige Beispieldaten in den Datenausgangs- (tx) und Eingangspuffer (rx) geschrieben, damit ein paar Daten zum Testen verfügbar sind.

  for (int i = 0; i < 10; i++)        //txbuffer[0-9] (Transmission/Senden)
  txbuffer[i] = i + 10;               //[0]=10, [1]=20,..., [9]=100

  for (int i = 0; i < 4; i++)         //rxbuffer[0-3] (Receiving/Empfangen)
  rxbuffer[i] = i + 20;               //[0]=20, [1]=40,..., [3]=80

Natürlich könnten hier auch noch einzelne I/O-Pins konfiguriert werden, sodass ein Empfang über eine LED angezeigt wird oder andere Reaktionen auf empfangene Daten stattfinden. Aber an dieser Stelle geht es lediglich um die grundlegende I²C-Slave-Funktion. Erweiterungen können individuell hinzugefügt werden.

Die Hauptschleife

In der Hauptschleife wird die eigentliche Datenübermittlung zwischen Master und Slave hergestellt. Um Daten an das Programm im Slave (hier: der verwendete ATTiny2313) zu übermitteln, wählt der Master den zu beschreibenden Puffer aus und sendet daraufhin die Daten an diesen. Der ATTiny ließt permanent die Empfangspuffer aus und speichert deren Inhalt in internen Variablen. An dieser Stelle hört der folgende Beispiel-Code auf. Natürlich kann auf die Änderung der Variablen individuell reagiert werden, aber diese Reaktionen sind von Projekt zu Projekt unterschiedlich. Daher gehe ich hier nicht näher darauf ein.

Wie bereits weiter oben erwähnt, werden immer nur acht Bits über den Bus gesendet. Aus diesem Grund weisen auch die Puffer nur acht Bits auf. Im Folgenden werden die ersten beiden Eingangspuffer in den Variablen byte1 und byte2 gespeichert. Dort kann ein eigenes Programm die Daten auslesen und weiterverarbeiten. Die Variablen low und high speichern die Eingangspuffer 2 und 3. Direkt im Anschluss vereinigt die Funktion word diese beide 8-Bit Variablen zu einer 16-Bit Variablen.

  while(1) {
    //############################################ Daten aus dem Eingangspuffer lesen

    // Zustand zum Programmstart: byte1 = 20 und byte2 = 40
    byte1 = rxbuffer[0];
    byte2 = rxbuffer[1];

    // Zustand zum Programmstart: low = 60 und high = 80
    low = rxbuffer[2];
    high = rxbuffer[3];
    word = uniq(low,high);

Der letzte Programmabschnitt behandelt das Senden von Daten, bzw das Bereitstellen von Daten für den Master. Im Normalfall gibt es nur einen Master innerhalb eines I²C-Busses. Dieser sendet Daten an die angeschlossenen Module, die sich alle im Slave-Modus befinden. Ein eigenständiges Senden von Daten erfolgt nicht. Hierfür sendet der Master eine Leseanforderung an den entsprechenden Slave und liest die Daten darauf hin von der gewünschten Adresse aus. Diese Daten befinden sich in diesem Beispiel im sogenannten Sendepuffer txbuffer.

Der folgende Code schreibt zunächst die Variablen byte1 und byte2 in die Sendepuffer txbuffer[0] und txbuffer[1]. Dadurch sind diese Daten für den Master im Register 0x00, bzw. 0x01 verfügbar. Im zweiten Codeabschnitt wird die 16-Bit Variable buffer in sein Low- und High-Byte zerlegt und diese Bytes anschließend in den Sendepuffern [2] und [3] gespeichert.

Die beiden geschweiften Klammern beenden den Code.

    //########################################## Daten in den Sendepuffer legen

    txbuffer[0]= byte1;
    txbuffer[1]= byte2;

    // 16 bit Variable wird in Low- und High-Byte zerlegt
    buffer = word;
    txbuffer[2] = LOW_BYTE(buffer);
    txbuffer[3] = HIGH_BYTE(buffer);

    //############################################################################

  } //end.while
} //end.main

Fazit

Der ATTiny ist eine tolle Alternative zu einem ATMega, wenn es um untergeordnete und/oder unkomplizierte Aufgaben geht. Vor allem durch die Verbindung über den I²C-Bus, steht der Modularisierung von Funktionen Tür und Tor offen. Dabei ist der notwendige Code sehr überschaubar und leicht nachzuvollziehen. Meiner Meinung nach ist der Aufbau und die Einbindung derart einfach, dass sich die Verwendung eines PCF8574 nicht lohnt. Im industriellen Bereich, wo jeder Cent zählt, mag das sicher anders sein.

Ich bin gespannt, in welche zukünftigen Projekte ich das Modul noch einbinden werde!

Alles Gute, Timo

Anhang: Das komplette Programm

#include <stdlib.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include "usiTwiSlave.h"

#define SLAVE_ADDR_ATTINY 0x50 //0b00110100

#ifndef F_CPU
  #define F_CPU 20000000UL
#endif

#define UNIQ(LOW,HEIGHT) ((HEIGHT << 8)|LOW) // Erstelle 16 Bit aus zwei Bytes
#define LOW_BYTE(x) (x & 0xff)               // Hole Low-Byte von 16 Bit-Zahl
#define HIGH_BYTE(x) ((x >> 8) & 0xff)       // Hole High-Byte von 16 Bit-Zahl

#define sbi(ADDRESS,BIT) ((ADDRESS) |= (1<<(BIT)))  // Setzte Bit
#define cbi(ADDRESS,BIT) ((ADDRESS) &= ~(1<<(BIT))) // Lösche Bit
#define toggle(ADDRESS,BIT) ((ADDRESS) ^= (1<<BIT)) // Schalte Bit um

uint16_t word=0;
uint8_t  byte1, byte2;
uint16_t buffer;
uint8_t  high,low = 0;

int main(void) {
  cli();                              // Interrupts aus
  usiTwiSlaveInit(SLAVE_ADDR_ATTINY); // Init TWI slave
  sei();                              // Interrupts ein

  for (int i = 0; i < 10; i++)        // txbuffer[0-9] (Transmission/Senden)
  txbuffer[i] = i + 10;               // [0]=10, [1]=20,..., [9]=100

  for (int i = 0; i < 4; i++)         // rxbuffer[0-3] (Receiving/Empfangen)
  rxbuffer[i] = i + 20;               // [0]=20, [1]=40,..., [3]=80

  while(1) {
    //############################################ Daten aus dem Eingangspuffer lesen

    // Zustand zum Programmstart: byte1 = 20 und byte2 = 40
    byte1 = rxbuffer[0];
    byte2 = rxbuffer[1];

    // Zustand zum Programmstart: low = 60 und high = 80
    low = rxbuffer[2];
    high = rxbuffer[3];
    word = uniq(low,high);

    //########################################## Daten in den Sendepuffer legen

    txbuffer[0]= byte1;
    txbuffer[1]= byte2;

    // 16 bit Variable wird in Low- und High-Byte zerlegt
    buffer = word;
    txbuffer[2] = LOW_BYTE(buffer);
    txbuffer[3] = HIGH_BYTE(buffer);

    //############################################################################

  } //end.while
} //end.main

6 Gedanken zu „ATTiny als I²C Slave mit USI – GREETBoard I2C Slave

  1. Hallo Timo
    super Sache hast du gemacht. Bin am staunen. Kannte es von anderen Teile, aber noch nie angewendet. Mach weiter so
    achim

  2. Hallo Timo
    mir sind ein paar Sachen aufgefallen.
    Im Anhang: komplettes Programm, erste Zeile haben sich ein paar Zeichen eingeschlichen, die nicht funktionieren.
    Das ist das Programm für den ATi 2313. Die Zeichen sendest/empfängst du zum 1284p mit rx und tx. Hast du auf dem 1284p auch eine anderes Programm drauf?
    achim

  3. Hallo Timo
    habe einiges von deinen Sachen nachgebaut und an mein System angepasst. Geht alles super. Es wird Zeit das du wieder was machst.
    achim

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.