Forum: Mikrocontroller und Digitale Elektronik ATtinyX5 und SPI Daisy-Chain


von Martin K. (grisuderdrache)


Angehängte Dateien:

Lesenswert?

Hallo zusammen,

ich versuche gerade für eine größere Projekt-Idee ein SPI-Daisy-Chain 
mit ATTinyX5 (also 25/45/85, je nach entgültiger Codegröße aufzubauen). 
Dabei sollen zum Schluss ca. 140 Bausteine in Reihe geschalten werden. 
Dabei Benötige ich PB3 als Ausgang und PB4 als analogen Eingang für mein 
Projekt und habe somit alle 3 Pins der USI Schnittstelle frei für die 
Kommunikation. Daher hatte ich folgende Idee:

 * Dass CLK Signal wird vom SPI Master ( Arduino ) an alle Slaves 
verteilt

 * Jeder Slave ist DO -> DI zu nächsten verbunden
 * Der SPI-Master ist MOSI -> DI vom ersten Slave und DO von letzten 
Slave geht zu MISO vom SPI-Master

Dazu kommt, dass jedes Datenpaket was übertragen wird, aus 4 bytes 
besteht. D.h. das SPI-Schieberegister in jedem Attiny muss 3 bytes 
zwischenbuffern, bevor es wieder auf den Ausgang geht. Des weiteren habe 
ich keine Resourcen für eine Chip-Select Leitung, weshalb ich mir 
folgende Idee überlegt habe. Ich nutze TIMER1 des AVR mit einem Überlauf 
aller ca. 1ms um die Daten aus dem SPI-Buffer zunehmen und neue 
einfügen. Falls in der Zwischenzeit ein SPI Interrupt erfolgt wird der 
Counter des Timers auf 0 gesetzt, was den Interrupt verhindert. So 
werden 1ms nach dem das letzte Datenpaket versendet wurde, automatisch 
die Daten  übernommen und neue bereitgestellt.  So weit die Idee. Ich 
habe das nun mit 3 ATtiny45 die mit der 16MHz PLL Clock laufen und einem 
Ardunino Uno als Master mit den angehängten Codes ausprobiert. Der 
Master wartet nach dem er 12 Byte ( 3 AVRs je 4 Byte ) immer 40ms.


Zu erwarten wäre dass immer die Ausgabe:
1
    11 22 33 44 | 11 22 33 44 | 11 22 33 44

erfolgt. Jedoch schleichen sich relativ oft (geschätzt 10% der fälle) 
Fehler ein und ich erhalte zum Beipiel:
1
    11 22 33 44 | 11 22 33 44 | 11 22 33 44 
2
    11 22 33 44 | 11 22 33 44 | 11 22 33 44 
3
    11 22 33 44 | 10 89 11 9A | 25 22 33 40 
4
    11 22 33 44 | 08 91 19 A2 | 11 22 33 44 
5
    11 22 33 44 | 04 44 46 68 | 91 22 33 44 
6
    11 22 33 44 | 11 22 33 44 | 11 22 33 44 
7
    11 22 33 44 | 11 22 33 44 | 11 22 33 44
Damit kann ich auch davon ausgehen, dass die je 4 Byte vom Master auch 
fehlerhaft an den Slaves ankommen. Ich hab das auch mit nur einem AVR 
ausprobiert und da fielen mir keine Datenfehler auf. Ab 2 gibt es 
Probleme.

Nach dem ich da nun schon seit Weihnachten dran frickel und diese Fehler 
nicht los werde, hoffe ich dass mir hier jemand helfen kann.

von S. Landolt (Gast)


Lesenswert?

Wäre es nicht einfacher, auf dieses USI und damit das Umspeichern der 4 
Bytes zu verzichten und stattdessen alles in Software zu machen? 
32-bit-Variable, auf einer Seite rein-, der anderen rausschieben, 
getaktet durch INT0? Noch irgendwie auf 32 zählen und, ähnlich wie 
bereits vorhanden, durch Timeout überwachen.

von Martin K. (grisuderdrache)


Lesenswert?

Es empfängt jeder AVR 4 Byte und sendet 4 Byte. Also müssen insgesamt n 
x 4 Byte vom Master gesendet wurden und gleichzeitig empfangen werden. 
Daher hatte sich die Sache mit USI/SPI ganz gut ins Bild der Anwendung 
gepasst. Ich hatte schon mal versucht Teile in Software zu 
reimplementieren, aber das wird m.M.n zu langsam, da ich das ganze 
relativ oft aktualisieren will.

: Bearbeitet durch User
von S. Landolt (Gast)


Lesenswert?

> ... zu langsam ...

Mir scheint, genau das ist beim aktuellen Programm der Fall, offenbar 
kommt der 1-ms-Interrupt dem USI dazwischen und verschiebt dessen Bits.
  Ich bin der Meinung, ein reines 32-bit-Software-SPI ist auf jeden Fall 
schnell genug (allerdings ohne heute abend den Beweis antreten zu 
wollen).

von S. Landolt (Gast)


Lesenswert?

PS:
Kann ein Hardwareproblem, d.h. Wackelkontakt, ausgeschlossen werden?

von Martin K. (grisuderdrache)


Lesenswert?

Ich hab den Interrupt auch auf 2ms erhöht und weiter und das hat nichts 
an dem Problem geändert. Den Wackelkontakt kann ich Ausschliessen, da 
alles verlötet ist und ich nach dem feststellen des Problems, alle 
Lötstellen noch mal kontrolliert habe.

von S. Landolt (Gast)


Lesenswert?

Dann weiß ich erstmal auch nicht weiter.
Ich habe versucht, das Ganze nachzubilden, allerdings mangels Arduino 
wie folgt: 3 ATtiny85 mit Ihrem Original-C-Programm, als Master ein 
ATmega in Assembler programmiert - ich sehe hier keine Fehler.

von c-hater (Gast)


Lesenswert?

Martin K. schrieb:

> Dazu kommt, dass jedes Datenpaket was übertragen wird, aus 4 bytes
> besteht. D.h. das SPI-Schieberegister in jedem Attiny muss 3 bytes
> zwischenbuffern, bevor es wieder auf den Ausgang geht.

Du hast ganz klar die Funktionsweise von SPI im allgemeinen und des USI 
als SPI-Interface im Besonderen nicht wirklich verstanden.

Woran merke ich das? Daran:

> D.h. das SPI-Schieberegister in jedem Attiny muss 3 bytes
> zwischenbuffern, bevor es wieder auf den Ausgang geht.

Erstens kann (und braucht) das SPI-Schieberegister (fast) nix puffern. 
Der einzige nutzbare Puffer ist eine Kopie des zuletzt empfangenen 
Bytes. Mehr Support gibt's nicht in der USI-Hardware.
Zweitens braucht nix "auf Ausgang gehen", denn das Schiebergister ist 
die ganze Zeit gleichzeitig Eingang und Ausgang. Wie das für 
Schieberegister ganz allgemein so üblich ist.
Die einzige Eingriffsmöglichkeit in das Schieberegister ist, innerhalb 
von knapp einem halben SPI-Takt das empfangene Byte im Schieberegister 
durch irgendwas anderes zu ersetzen.

Wie kritisch das ist, hängt nur von einer Sache ab: Welche Zahl kommt 
raus, wenn du den Systemtakt der Clients durch den SPI-Takt dividierst?

von S. Landolt (Gast)


Lesenswert?

Vermutlich sendet der ATmega zu langsam für den 1-ms-Interrupt in den 
ATtinys, mal das 'SPI.setClockDivider(SPI_CLOCK_DIV64);' auf '...DIV16' 
setzen.

von S. Landolt (Gast)


Lesenswert?

Okay, ich sehe jetzt auch sporadisch Fehler.
  Das Problem liegt in diesem 1-ms-Interrupt: füge ich im ATmega nach 
jedem 1-Byte-SPI-Transfer ein Warten von 500 us ein, treten keine Fehler 
mehr auf.

Gute Nacht & schönen Sonntag.

von Martin K. (grisuderdrache)


Lesenswert?

@c-hater

Mir ist durchaus bewusst, wie ein Schieberegister und der SPI-Bus 
funktionieren, das Problem ist halt, dass ich im Daisy-Chain Betrieb ist 
und wenn jeder Slave n-Byte am stück empfangen soll, so muss sobald das 
SPI  Register voll ist, dieses mit einen internen Buffer/Queue getauscht 
werden, sodass beim nächsten CLK Impuls die Bytes passend weiter 
gereicht werden.

@S. Landolt
Ich habe das ganze bis 350µs und _DIV4 zum laufen bekommen, das Problem 
dabei ist, dass die künstliche Pause die effektive Datentransferrate 
sehr weit senkt und ich damit für das wo ich hin will (ca. 130 Module in 
Reihe) 182ms mit Warten verbringe. Inklusive der Transfers komme ich 
damit noch auf maximal 5 komplette Durchläufe beim Datenabrufen.  Das 
DIV64 hatte ich genutzt, da ich im objdump ausgezählt hatte wieviele 
Zyklen der USI Interupt läuft und so fertig ist, bevor das nächste Byte 
da ist. Damit wurde _DIV32 sehr knapp und somit habe ich mit DIV64 und 
DIV128 probiert.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Ich würde einfach immer 3 Byte am Stück senden und dann eine Pause 
machen. Die Slaves erkennen per Timerinterrupt, daß auf SCK nichts los 
ist und lesen dann jeder sein Byte. Das Ganze dann 4-mal und eine 
größere Pause, damit die Slaves erkennen, daß ein neuer Frame beginnt.
Ob auf SCK seit dem letzten Timerinterrupt Ruhe war, kann man mit dem 
Pin-Change Flag erkennen.

von Martin K. (grisuderdrache)


Lesenswert?

Peter D. schrieb:
> Ich würde einfach immer 3 Byte am Stück senden und dann eine Pause
> machen. Die Slaves erkennen per Timerinterrupt, daß auf SCK nichts los
> ist und lesen dann jeder sein Byte. Das Ganze dann 4-mal und eine
> größere Pause, damit die Slaves erkennen, daß ein neuer Frame beginnt.
> Ob auf SCK seit dem letzten Timerinterrupt Ruhe war, kann man mit dem
> Pin-Change Flag erkennen.

Das klingt auch nach möglichen Option. Da werde ich morgen mal schauen, 
wie/ob ich das irgendwie umgesetzt bekomme.
Also den USI Overflow interrupt nicht benutzen, da der PCINT ja eine 
höher Priorität hat als der USI und somit der PCINT auf SCK den Overflow 
vom USI blocken würde?

von Matthias S. (Firma: matzetronics) (mschoeldgen)


Lesenswert?

Du baust da m.M.n. ein Synchronclock/Ripple Data Schieberegister. Die 
Clock führst du ja auf alle parallel, aber die Daten werden von Tiny zu 
Tiny durchgereicht und sind bald (nach ein oder 2 Tinys) nicht mehr 
synchron zur Clock.
Ich würde da vermutlich ähnliches wie I2C benutzen(oder SPI mit 
Geräteadresse) und SPI Clock und Data parallel an alle Tinys legen.

: Bearbeitet durch User
von S. Landolt (Gast)


Lesenswert?

> ... bis 350µs ...das Problem dabei ...

Meine 500 us sollten nur klarstellen, dass das Problem in dem 
Timer-Interrupt liegt, welcher in eigenem Rhythmus den USI-Interrupt 
sperrt/freigibt, ohne zum Master synchron zu sein. Es fehlt eben eine 
CS- bzw. SS-Leitung zur Synchronisierung, einen Ausweg hat Peter 
Dannegger aufgezeigt.

von S. Landolt (Gast)


Lesenswert?

PS:
Diese 350 bzw. 500 us waren schon so eine Art Synchronisierung, aber 
eben nur schlecht, von Verarbeitungszeiten im Mega und den Tinys 
abhängig; das muss noch auf die Füsse gestellt werden.

von S. Landolt (Gast)


Angehängte Dateien:

Lesenswert?

Versuch eines C-Laien.

von Martin K. (grisuderdrache)


Angehängte Dateien:

Lesenswert?

Erstmal vielen Dank für paar Ideen und Denkanstöße wie ich das ganze 
realisieren kann. Ich habe heute mal die Idee von @peda umgesetzt und 
bin zu einem schnellen und (bisher) zufriedenstellenden Ziel gekommen. 
Ich sende nun erst alle ersten Bytes, dann alle zweiten.. usw, und 
zwischen den Bytes brauch ich nur eine Pause von 75µs. D.h. bei der 
Übertraung von 4 Byte an alle Slaves fügt nur noch 225µs "nutzlose" Zeit 
ein. Des weiteren kann ich den SPI-Takt bis auf 4MHz erhöhen.

Anbei der Code für die Slaves. Der Master sendet/empfängt mittels
1
  uint8_t slaves = 3;
2
3
  for ( uint8_t j = 0; j < 4; j++) {
4
     for ( uint8_t i = 0; i < slaves ; i ++) {
5
        SPI.transfer(&recv[i*4+j],1);
6
     }
7
     delayMicroseconds(75);
8
  } 
9
  delayMicroseconds(200);

Und ja ich weiß, wenn ich das recv array noch umsortiere, kann ich mir 
die innere Schleife auch noch schenken ;-) Aber das kommt in der 
weiteren Entwicklung. Jetzt funktioniert erstmal die Datenübertragung 
sauber.

: Bearbeitet durch User
von S. Landolt (Gast)


Lesenswert?

Parbleu - auf den ersten Blick (auch auf den zweiten) hätte ich gesagt, 
dass das gar nicht funktionieren kann.
  Wenn doch - Respekt!

von S. Landolt (Gast)


Lesenswert?

Auch auf den dritten Blick: ohne USI-Interrupt, stattdessen ein 
Timer-Interrupt mit festen 800 Takten? Wobei der Timer ständig per SCK 
resp. INT0 zurückgesetzt wird? Pardon, ich versteh's nicht.

von Peter D. (peda)


Lesenswert?

S. Landolt schrieb:
> ich versteh's nicht

Das Problem ist doch, daß man ganz genau in 0,5 SCK-Takten dem USI das 
Byte unterm Hintern wegändern muß. Also läßt man es ganz sein und den 
Master einfach soviele Bytes rausschieben, wie Slaves dran hängen. In 
der Pause danach kann dann jeder Slave ganz in Ruhe sein Byte lesen bzw. 
schreiben.
Die Slaves müssen nur erkennen, wann die Pause ist und das macht der 
Timerinterrupt. Den USI-Interrupt darf man daher nicht verwenden.
Stell Dir das USI einfach als Schieberegister (z.B. 74HC299) vor.

: Bearbeitet durch User
von S. Landolt (Gast)


Lesenswert?

Danke für die Erklärung (der letzte Satz war entscheidend)!

von Martin K. (grisuderdrache)


Lesenswert?

Was für die Funktionsweise vielleicht noch interessant ist. Der 
INT0-Interrupt muss nicht für jedes übertragene Bit ausgeführt werden. 
Er ist ausreichend, wenn er "oft genug" dass heißt bis spätestens 800 
CPU-Takte rum sind einmal ausgeführt ist. Im Falle von 4 MHz SPI Takt, 
hätte man auf dem 16  MHz AVR eh nur max. 4 Instructions im 
Interrupt-Handler Zeit. Jedoch braucht der Interrupt-Handler für
1
 
2
  TCCR1 = TIMER1_...; 
3
  TCNT1 = 0; 
4
  recv = 1;


schon 7 Takte ( 2x LDI + OUT, 1x LDI+STS). Dazu kommt noch die 
Interrupt-Präambel und das Aufräumen (min. 3 Push und 3 Pop + Clear des 
r0, r1 + Reti) Sind wir bei ca. 20 Takten, was bedeuten würde das der 
Interrupt nur aller etwa 5 Bit auf dem SPI Bus ausgeführt wird. Dies 
genügt um den TIMER oft genug zurück zu setzen und so quasi das 
"Latchen" des Schieberegisters nach 800 Takten zu verhindern.

Ich hatte mich beim Ursprungsplan zu sehr an der Idee immer die 
zusammengehörigen Bytes zu übertragen festgehalten. Aber da hat man wie 
Peter sagte, nur 0,5 SPI Takte Zeit alle Daten in den Slaves zu 
reorganisieren, was zu den niedrigen SPI-Takt führte.

von S. Landolt (Gast)


Lesenswert?

Da auf PortB2 auch T0 liegt, könnte man das Zurücksetzen des Timer1 auch 
über Timer0.OC0x realisieren, dann hätten die Tinies während der 
Übertragung noch Zeit, etwas zu tun.

von Peter D. (peda)


Lesenswert?

S. Landolt schrieb:
> dann hätten die Tinies während der
> Übertragung noch Zeit, etwas zu tun.

Der INT0 könnte sich selbst sperren und der T1 gibt ihn nach 10µs wieder 
frei. Dann hat man 160 Zyklen zwischen den Interrupts und die CPU kann 
während der Übertragung noch anderes tun.
Oder der T1 prüft alle 10µs, ob das INTF0 gesetzt ist und löscht es.

von Chris (Gast)


Lesenswert?

Weshalb einen externen int benutzten und nicht den usi overflow 
interrupt? Wenn der interrupt auftritt, in 80 CLK mittels Timer dann den 
Interrupt abarbeiten.
So werden die Bytes ausgeblendet welche für andere UC bestimmt sind und 
nur das letzte byte wird verarbeitet. Der Master muss eine Pause nach 
der Übertragung einführen.

von Martin K. (grisuderdrache)


Lesenswert?

Chris schrieb:
> Weshalb einen externen int benutzten und nicht den usi overflow
> interrupt?

Was mir bei dem USI Interrupt aufgefallen ist, ist, dass der USI 
Interrupt der am niedrigsten priorisierte ist und so bei einem dummen 
Zufall, der TIMER Interrupt dem USI zuvor kommen kann.

von Martin K. (grisuderdrache)


Lesenswert?

Ich bin mittlerweile mit dem, der Frage zu Grunde liegenden Projekt, ein 
ganzes Stück weiter gekommen und hab dabei noch einen Bug im Timer-ISR 
für das Update des SPI Buffers gefunden, den ich hier mal schnell 
dokumentiere, falls es für jemanden anders von Nutzen sein könnte.

Die alte ISR sah wie folgt aus:
1
ISR(TIM1_COMPA_vect) {
2
    if ( recv &&  data_in_ptr < 4){
3
        data_in_buffer[data_in_ptr] = USIDR;
4
        USIDR = data_out_buffer[data_in_ptr+1];
5
        data_in_ptr++;
6
        TCNT1 = 0;
7
    }
8
}
wobei wir recv = 1 annehmen. Diese Variante funktioniert nur solange 
jede SPI Nachricht immer 4 Byte lang ist. Will man jedoch flexible 
Nachrichtenlängen unterstützen so ist die Routine in
1
ISR(TIM1_COMPA_vect) {
2
    if ( (USISR & ( 1 << USIOIF))  &&  data_in_ptr < MAX_BUFFER_LEN){
3
        data_in_buffer[data_in_ptr++] = USIDR;
4
        USIDR = (data_in_ptr < MAX_BUFFER_LEN) ? data_out_buffer[data_in_ptr]:0x00;
5
        TCNT1 = 0;
6
    }
7
}
zu ändern. So wird nur mit dem SPI Buffer interagiert, wenn da auch 
wirklich Daten angekommen sind.

von Veit D. (devil-elec)


Lesenswert?

Hallo,

sollte man nicht besser sie Senderoutine losgelöst vom Timer laufen 
lassen?
Der Timer sollte nur aller x µs oder ms den Anstoss zum senden geben, 
aber nicht selbst senden.

Bei 1 bis 2 Byte könnte man das noch im Timer machen. Erstes Byte wird 
sofort gesendet und das 2. liegt im Sendebuffer. Timer ISR wird also 
noch nicht blockiert. Wenn man aber längere Daten senden möchte, 
blockiert man die Timer ISR, wann man ja generell vermeiden soll.

Idee wäre, Daten in einen Ringbuffer, Timer löst das senden aus, 
Senderoutine bedient sich vom Ringbuffer und liest und sendet bis 
fertig. Mit Ringbuffer können schon neue Sendedaten berechnet werden 
ohne das aktuelle Sendepaket zu verfälschen. Wenn man Zeit hat kann der 
Ringbuffer auch wegfallen.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.