Für ein neues Projekt suchte ich nach einer Möglichkeit, einen Atmega8
mit zwei ATtiny2313 per SPI zu koppeln. In den Foren finden sich viele
Anregungen, aber die vollständige Lösung war nicht dabei.
Hier deshalb mal mein Testaufbau mit Schaltung und Source für die MCUs
in Assembler.
Der Aufbau ist getestet und funktioniert.
Bezüglich der Beschaltung der MCUs bin ich nicht ganz sicher, ob hier
alles optimal gewählt ist. Für Hinweise bin ich dankbar.
Huldreich
So einfach war es denn doch nicht, so hatte ich viele Wochen zu tun, bis
der Versuchsaufbau wirklich stand.
Da ATTiny kein SPI hat, ist USI zu verwenden. Dies funktionierte beim
ersten Byte auch gut, ab dem zweiten kam es zu Fehlern. Deswegen habe
ich mir eine Art Handshake ausgedacht. Zur Kontrolle habe ich zunächst
zwei LCD-Displays angeschlossen.
ATMega steuert die Slaves per Slaveselect (PortC1 oder C2) und erhält
die Antwort als Pegelwechsel über PINC3.
Die vollständige Source habe ich angehängt, ist aber noch nicht geordnet
und daher mühsam zu lesen, deswegen habe ich die entscheidenden Passagen
hier herausgenommen, zunächst der ATMega8
1
; Pulsdauer für ein Servo übertragen, die Daten liegen in R15 (Servonummer), R24 (Lowbyte) und R25 (Highbyte)
andi r16,0b00001111 ; oberes Nibbel ausblenden, es verbleibt das Highbyte der Servopulsdauer
11
andi r17,0b00001111 ; vertauschtes oberes Nibbel ausblenden, es verbleibt die Servonummer
12
mov r1,r17 ; Servonummer
13
mov r2,r16 ; Highbyte der Servopulsdauer
14
15
sbi PORTB,1 ; Pegeländerung: fertig
16
rcall SPI_Start
17
cbi PORTB,1 ; Pegeländerung an B1 zeigt dem Master die Bereitschaft an
18
rcall SPI_Empfang
19
20
; Auslassung: die Zeilen dienen dem Speichern und Anzeigen der empfangenen Daten
21
22
sbi PORTB,1 ; Pegeländerung: fertig
23
cbi DDRB,1 ; B1 wieder als Eingang
24
rjmp mainloop
Eigentlich hätte das sofort funktionieren müssen; es geht jedoch nur mit
der Warteroutine 100µs für den ATMega, sonst wird ab dem zweiten Byte
nur Müll oder gar nichts empfangen.
Wer kann das erklären?
Viele Grüße
Huldreich
Hallo Huldreich
Einen tiny2313 für eine vollständige SPI Kommunikation zu verwenden ist
keine so gute Idee. Wenn du allerdings auf das zurücksenden von Daten
per 'DO' verzichten kannst, dann hab ich da was für dich. Ich verwende
es in einer Positionsanzeige mit 3x8 7-Segmentanzeigen. Der tiny2313
arbeitet hierbei als Slave und nimmt die Daten vom SPI entgegen.
Momentan läuft der SPI Takt des Masters mit 16Mhz/64 = 250kHz. Der SPI
Slave ala tiny2313 hat einen /CS Eingang. Die SPI Auswertung erfolgt im
Interrupt. Und wenn alle Bytes empfangen wurden, wird ein Flag im
GPIOR0-Register gesetzt welches man in der Hauptschleife abfragen kann.
Der Vorteil der Interruptbehandlung des SPI liegt darin, dass man wärend
des SPI-Transfers weiterhin andere Aufgaben erledigen kann.
Hier die Defines:
1
;***** Register-Variablen
2
.def SREG_SAVE = r0 ; Sicherungskopie des SREG-Registers im Interrupt
3
4
; Konstanten
5
.equ SPI_RD_BYTES = 12 ; zu empangene Bytes per SPI
6
7
; Flags in GPOIR0
8
.equ SPI_ON = 2 ; 0= SPI Off | 1= SPI On
9
.equ SPI_OVERRUN = 3 ; 0= kein SPI Data OverRun | 1= SPI Data OverRun
10
.equ SPI_UNDERRUN = 4 ; 0= kein SPI Data UnderRun | 1= SPI Data UnderRun
11
.equ NEW_SPI_DATA = 7 ; Flag zeigt an, dass neue Daten von dem USI-SPI zur Verfuegung stehen
12
13
; Port defines
14
.equ USI_SPI_PORT = PORTB ; fest vorgeschrieben
15
.equ usi_SPI_SCK = 7 ; fest vorgeschrieben
16
.equ usi_SPI_DO = 6 ; fest vorgeschrieben
17
.equ usi_SPI_DI = 5 ; fest vorgeschrieben
18
.equ usi_SPI_CS = 4 ; 0..4 von PORTB frei waehlbar
Wenn die /CS Leitung vom Master auf Low gezogen wurde wird dies erkannt
und dem SPI Interrupt das abspeichern der empfangenen Bytes angewiesen.
Im SPI Interrupt wird geprüft, dass nicht zuviel Bytes gesendet werden.
(festgelegte Anzahl zu empfangender Bytes per Frame in 'SPI_RD_BYTES')
Andernfalls wird das 'OVERRUN'-Flag gesetzt und das SPI Frame verworfen.
Sollte nun vom Master die /CS Leitung wieder auf High gezogen, dann wird
dies wieder im Pin-Change-Interrupt erkannt und geprüft, ob auch schon
alle Bytes eines SPI-Frames empfangen wurde. Sollte das der Fall sein,
wird das 'NEW_SPI_DATA'-Flag im GPOIR0 gesetzt und damit angezeigt, dass
ein neuer Datensatz im 'SPI_RD_BUFFER' im SRAM liegt. Darauf kann dann
die Hauptschleife reagieren.
Hier der Pin Change Interrupt der /CS Leitung:
Jetzt noch das Unterprogramm 'memcpy' zur Speicherkopie. Die sollte man
machen, da ja der 'SPI_RD_BUFFER' in einem weiteren SPI-Interrupt wieder
verändert werden kann. So kann man mit der Kopie erstmal ganz beruhigt
arbeiten.
1
memcpy:
2
ldi r17,SPI_RD_BYTES
3
load_p X,(SPI_RD_BUFFER)
4
load_p Y,(MEM_BUFFER)
5
cli ; Atomarer Zugriff begin
6
memcpy_loop:
7
ld r16,X+
8
st Y+,r16
9
dec r17
10
brne memcpy_loop
11
sei ; Atomarer Zugriff ende
12
cbi GPIOR0,NEW_SPI_DATA
13
ret
Und so kommt man nun an die SPI Daten ran:
1
; RESET = Programmbeginn
2
RESET:
3
rcall init_usi_spi ; USI SPI initialisieren (PORTs, INTs und SRAM)
4
; Hauptschleife
5
main:
6
sbic GPIOR0,NEW_SPI_DATA
7
rcall new_spi_data_receive
8
; .....
9
; ..... tue irgendwas in der Hauptschleife
10
; .....
11
rjmp main
12
13
new_spi_data_receive:
14
rcall memcpy ; ab hier stehen die SPI Data Bytes im 'MEM_BUFFER'
15
; ...... ; zur weiteren Verarbeitung bereit
16
17
ret
Ich glaub dass ist jetzt ganz schön lang geworden. Wenn noch Fragen sind
oder Ihr Anregungen/Verbesserungen habt, nur her damit.
Grüße Steffen
Hallo Steffen,
vielen Dank für Code und Erläuterung.
Da mein Programm eigentlich funktioniert und dann -Frust- eben doch
wieder nicht ganz richtig, hatte ich bereits die Idee einer
vollständigen Interruptsteuerung, diese jedoch nicht weiter verfolgt und
dann das Projekt vorerst ganz zurückgestellt.
Deine Lösung, die Daten zwischenzupeichern, ist eleganter als meine.
Ich werde auf jeden Fall mein Programm nach Deiner Anregung umschreiben
und dann berichten. (Kann ein paar Wochen dauern)
Viele Grüße
Huldreich
Hallo Steffen,
das Studium Deines Schreibens bringt einen autodidaktischen Hobbyisten
wirklich weiter; vielleicht kannst Du mir einige Fragen beantworten:
1. Lt. Datenblatt kann GPOIR0 für globale Variablen genutzt werden. Hast
Du Deine Festlegungen (.equ SPI_ON = 2 ........) selbst definiert
oder irgendwo abgeleitet. Wird dieses Register wirklich nicht intern von
der MCU benutzt?
2. Warum hast Du allgemeine Formulierungen benutzt (z.B. USI_SPI_PORT-2
anstelle von PINB)? Daran habe ich einige Zeit gerätselt, da mir diese
Schreibweise unbekannt war.
3. Warum wird in:
usi_spi_transfer_start:
sbi GPIOR0,SPI_ON ; SPI Auswertung einschalten
in r16,USI_SPI_PORT-1
sbr r16,(1<<usi_SPI_DO)
out USI_SPI_PORT-1,r16
auf usi_SPI_DO zugegriffen. Du hattest doch oben geschrieben, dass der
ATTiny nur Daten empfangen aber nicht senden soll.
4. Warum ist am Ende von usi_spi_transfer_complied:
der Sprungbefehl rjmp usi_spi_end eingefügt? Das ist doch die nächste
Anweisung.
5. Warum muss in der ISR für USI_OVERFLOW: in der zweiten Zeile das Flag
gelöscht werden. Geht das nicht automatisch?
6. Warum wird unter init_usi_spi: in der 8. Zeile usi_SPI_DO als Input
initialisiert? Das sollte doch eigentlich gar nicht benutzt werden,
s.o..
7. Reine Verständnisfrage: Drei Zeilen weiter unten: damit wird der
Pullupwiderstand (usi_SPI_CS) aktiviert?
8. Unter ; USI SPI einschalten:
sbr r16, (1<<USIOIE)|(1<<USIWM0)|(1<<USICS1)
cbr r16, (1<<USIWM1)|(1<<USICS0)|(1<<USICLK)
out USICR, r16
lautet meine Formulierung
ldi R16,0b00011000 ; (1<<USIWM0)|(1<<USICS1)
out USICR,R16
Sollte bis auf Bit 6 identisch sein. Kannst Du das für mich prüfen? Ich
bin bei Adressierung von Bits und Masken immer unsicher.
Viele Grüße
U.
Hallo
Huldreich schrieb:> 1. Lt. Datenblatt kann GPOIR0 für globale Variablen genutzt werden. Hast> Du Deine Festlegungen (.equ SPI_ON = 2 ........) selbst definiert> oder irgendwo abgeleitet. Wird dieses Register wirklich nicht intern von> der MCU benutzt?
Über GPIOR0 (0x13) kannst man frei verfügen und dieses wird nicht von
der CPU benutzt. Da beim ATtiny2313 die Adresse für dieses Register
innerhalb der Adessen 0x01..0x1F liegt (nicht MEMORY MAPPED) kann man
einzelne Bits ganz einfach mit sbi oder cbi verändern und die
Skipbefehle sbis oder sbic verwenden.
1
.equ SPI_ON = 2
..habe ich selber definiert. Damit kann man im Programm besser sehen,
was mit dem Bit2 gemeint ist.
Huldreich schrieb:> 2. Warum hast Du allgemeine Formulierungen benutzt (z.B. USI_SPI_PORT-2> anstelle von PINB)? Daran habe ich einige Zeit gerätselt, da mir diese> Schreibweise unbekannt war.
Es ist doch einfacher in der Zuweisung deines Ports nur den Port
anzugeben und das/die Bits der entsprechenden Signale als immer noch
DDRx und PINx. Schau mal in deine 'tn2313.def'. Da sieht man schön, wie
die Adressvergabe des PORT/DDR/PIN bei allen ATMEL AVR's immer gleich
ist.
Deswegen kann man einfach nur den PORT angeben und wenn man das
Dir-Register zieht man einfach von der PORT-Adresse 1 ab, bzw. bei der
PIN-Adresse halt 2 abziehen.
Huldreich schrieb:> 3. Warum wird in:> usi_spi_transfer_start:> sbi GPIOR0,SPI_ON ; SPI Auswertung einschalten> in r16,USI_SPI_PORT-1> sbr r16,(1<<usi_SPI_DO)> out USI_SPI_PORT-1,r16> auf usi_SPI_DO zugegriffen. Du hattest doch oben geschrieben, dass der> ATTiny nur Daten empfangen aber nicht senden soll.
Ich hatte es mal vorgesehen, allerdings wieder verworfen. Denn es muss
garantiert werden, dass eine bestimmte Zeit eingehalten werden muss,
wenn die /CS-Flanke von 1->0 geht bis der erste CLK des SPI kommt. Denn
in dieser Zeit muss der PCINT Interrupt eimal ausgeführt worden sein
um das SPDR mit dem Wert zu füllen, der zurückgesendet werden soll. Hier
wird nur einfach dafür gesorgt, dass DO ein Eingang bleibt.
Huldreich schrieb:> 4. Warum ist am Ende von usi_spi_transfer_complied:> der Sprungbefehl rjmp usi_spi_end eingefügt? Das ist doch die nächste> Anweisung.
1
..
2
brlo usi_spi_end
3
sbi GPIOR0,NEW_SPI_DATA
4
rjmp usi_spi_end ; <<---- Der hier?
5
usi_spi_end:
6
pop XH
7
..
Der rjmp *usi_spi_end* kann entfernt werden. Der ist wirklich
überflüssig. Danke für den Hinweis!
Huldreich schrieb:> 5. Warum muss in der ISR für USI_OVERFLOW: in der zweiten Zeile das Flag> gelöscht werden. Geht das nicht automatisch?
Nein, leider nicht. Das muss so sein. Steht auch so im Datasheet. Damit
wird der USI-Counter wieder auf NULL gesetzt.
Huldreich schrieb:> 6. Warum wird unter init_usi_spi: in der 8. Zeile usi_SPI_DO als Input> initialisiert? Das sollte doch eigentlich gar nicht benutzt werden,> s.o..
War aber mal so vorgesehen. Wenn man den PIN für DO anderweilig
braucht kann man alles was mit DO zu tun hat einfach löschen oder
auskommentieren.
Huldreich schrieb:> 7. Reine Verständnisfrage: Drei Zeilen weiter unten: damit wird der> Pullupwiderstand (usi_SPI_CS) aktiviert?
Ja, ich schalte für den /CS-Eingang den Pullup ein. Das kommt noch vom
debuggen mit dem Logic-Analizer. Und ich glaube auch deswegen, damit man
die SPI-Verbindung während einer Kommunikation an/ab stöpseln kann.
Deswegen auch die Abfragen auf OVER-/UNDERRUN.
Huldreich schrieb:> 8. Unter ; USI SPI einschalten:> sbr r16, (1<<USIOIE)|(1<<USIWM0)|(1<<USICS1)> cbr r16, (1<<USIWM1)|(1<<USICS0)|(1<<USICLK)> out USICR, r16> lautet meine Formulierung> ldi R16,0b00011000 ; (1<<USIWM0)|(1<<USICS1)> out USICR,R16> Sollte bis auf Bit 6 identisch sein. Kannst Du das für mich prüfen? Ich> bin bei Adressierung von Bits und Masken immer unsicher.
Das ist schon korrekt so. Ich setze ja nurnoch das Interrupt-Bit, um
einen Interrupt beim Überlauf des USI-Zählers zu erreichen.
Ich hoffe ich konnte weiter helfen.
Gruß Steffen
Hallo
Was genau willst du denn umsetzen? 6x PWM für Modellbau-Servos die per
SPI gesteuert werden? Wenn ja, welche auflösung soll das PWM-Signal denn
haben (16,10 oder 8Bit)?
Hallo
So, ich hatte mal ein wenig Zeit und hier mal meine Version von einem
6-Kanal SERVO-PWM Treiber der via 2Byte SPI-Telegramm angesteuert wird.
Kann ihn leider noch nicht testen, da ich keinen SPI-Master zur
Verfügung habe. Die PWM für die Servos jedenfalls steht aal glatt an den
Ausgängen an.
Wenn es mal jemand testen könnte und mir ein positives Feedback gibt,
werde ich es in die Codesammlung einstellen.
Vor allem müsste man mal testen, wie hoch hier der maximale SPI-Takt
sein darf. Der tiny2313 arbeitet mit dem internen 8Mhz Osszilator. Der
ist allerdings nicht so Temperatur/Spannungs stabil wie ein externer
Quarz, aber wenn man ihn ein wenig trimmt
Sagenhaft - ich bin geplättet.
So ähnlich habe ich mir das vorgestellt, jedoch bin ich noch lange nicht
so weit.
Zum tiny2313 - interne 8 Mhz: bei unveränderten Lockbits wird, wenn ich
das richtig verstanden habe, der Takt durch 8 geteilt, d.h. wir haben
dann noch 1 MHz. Soll das so sein?
Viele Grüße
U.
Huldreich schrieb:> welches Oszilloscop (Hardware, Software) hast Du für die Darstellung> benutzt?
Das ist ein Logic Analyzer mit zugehöriger Software.
http://gadgetforge.gadgetfactory.net/gf/project/butterflylogic/
Eine sehr nützliche Anschaffung.
Huldreich schrieb:> Zum tiny2313 - interne 8 Mhz: bei unveränderten Lockbits wird, wenn ich> das richtig verstanden habe, der Takt durch 8 geteilt, d.h. wir haben> dann noch 1 MHz. Soll das so sein?
Nein. Man muss natürlich die CKDIV8 Fuse auschalten (CKDIV8=0). So hat
man die internen 8Mhz.
Hat denn den jemand mal den Code testen können?
Gruß Steffen
Huldreich schrieb:> In den Foren finden sich viele> Anregungen, aber die vollständige Lösung war nicht dabei.
Das liegt daran, daß das AVR-SPI eine Mogelpackung ist.
Man hat keinen Sendepuffer und somit hat der SPI-Slave nur einen halben
SCK-Takt Zeit, in den Interrupt zu springen und das nächste Byte zu
schreiben, also ein Ding der Unmöglichkeit.
Als Master ist das SPI nutzbar, aber als Slave ist es ne Katastrophe.
Der Master muß nach jedem Byte lange Gedenkpausen einlegen.
Bei den neueren AVRs hat man etwas nachgebessert und der UART einen
gepufferten SPI-Modus verpaßt. Allerdings hat man den Slave-Mode
vergessen, also auch ne Sackgasse.
Bei der 8051-Fraktion war man schlauer. Z.B. der AT89LP4052 hat ein
gepuffertes SPI. Der kann also SPI-Slave sein, ohne den Master
haufenweise CPU-Zeit verschwenden zu lassen. Der Master kann
ununterbrochen senden und empfangen.
Man kann sagen, die AVR-Leute haben gründlich Mist gebaut.
Und deshalb gibt es auch keine brauchbaren Lösungen.
Huldreich schrieb:> Eigentlich hätte das sofort funktionieren müssen; es geht jedoch nur mit> der Warteroutine 100µs für den ATMega,
Die Wartezeit muß so lang sein, wie die längst mögliche Zeit, die der
Slave braucht. Hat der Slave noch andere Interrupts zu behandeln, kann
das also richtig lange dauern und die 100µs noch zu kurz sein.
Peter
Peter Dannegger schrieb:> Und deshalb gibt es auch keine brauchbaren Lösungen
@ Peter: Vielen Dank, das baut mich wieder auf, dachte es liegt an mir.
@ Steffen: Habe Deinen Code teilweise analysiert, verstehe aber nicht
alles.
1
in SREG_SAVE,TCNT1L ; compensate var. irq delay
2
sbrs SREG_SAVE,0 ; odd: +2 clk; even: +3 clk
3
ld SREG_SAVE,Z ; Zeit kompensieren
4
in SREG_SAVE,SREG
Was wird kompensiert; ist damit gemeint eine evtl. vorher ablaufende
ISR?
Wozu dienen weiter unten die NOPs?
Könnte man evtl. vielleicht die USI-Abfrage in den Mainloop per polling
integrieren. Dann hätte man sehr kurze ISRs ohne irgendwelche
Kompensationen. Die USI-Abfrage müsste dann per Handshake stattfinden,
z.B. Master sendet Slaveselect (low), wartet auf Slaveantwort (low),
sendet erst dann nur ein Byte, legt Slaveselect auf high, wartet bis
Slave mit high Datenempfang und -verarbeitung quittiert, dann folgt das
nächste Byte. Ist dann nicht so elegant wie eine reine
Interruptsteuerung aber vielleicht einfacher.
Alternativ könnte ich mir vorstellen, dass man einfache (kurze) ISRs für
alle Interrupts erstellt und dann einen einmaligen kurzen Fehler
(Verlängerung) für die Pulsdauer akzeptiert; das dürfte bei 50Hz
Wiederholfrequenz nicht ins Gewicht fallen.
U.
Huldreich schrieb:> as wird kompensiert; ist damit gemeint eine evtl. vorher ablaufende> ISR?> Wozu dienen weiter unten die NOPs?
Wenn die ISR angesprungen wird, dann weiß man ja nicht, ob vorher ein
1Byte oder 2Byte Befehl abgearbeitet worden ist. Wenn es ein
2Byte-Befehl war, dann ist schon wieder 1Systemtakt flöten gegangen.
Deshalb diese Abfrage am Anfang der ISR.
Desweiteren wird ja der System-Clock für den Timer durch 8 geteilt. Das
heißt ja, dass der Timer aller 8 sys-clk wieder eins hochgezählt wird.
Diese nop's sind eigentlich nur zur kompensation dieser 8 Takte drin.
Macht man das nicht, dann hat man um maximal 7 Takte verschobene
Compare-Zeiten der Impulseausgabe.
Wenn man es nicht so genau braucht, dann kann man sich das alles auch
sparen.
Huldreich schrieb:> Master sendet Slaveselect (low), wartet auf Slaveantwort (low),> sendet erst dann nur ein Byte, legt Slaveselect auf high, wartet bis> Slave mit high Datenempfang und -verarbeitung quittiert, dann folgt das> nächste Byte. Ist dann nicht so elegant wie eine reine> Interruptsteuerung aber vielleicht einfacher.
Das ist dann aber kein SPI mehr. Und ausserdem hat man dadurch bei jedem
Slave den man am Master hat noch eine Signalleitung mehr drin.
Huldreich schrieb:> Alternativ könnte ich mir vorstellen, dass man einfache (kurze) ISRs für> alle Interrupts erstellt und dann einen einmaligen kurzen Fehler> (Verlängerung) für die Pulsdauer akzeptiert; das dürfte bei 50Hz> Wiederholfrequenz nicht ins Gewicht fallen.
Wie gesagt, wenn man die Impulse nicht auf den Takt genau braucht, dann
kann man die ganze Taktkompensation wegfallen lassen. Bei 8Mhz ist ein
Takt 125ns lang. Macht bei eventuellen 7 Takten 875ns. Muss man mal
ausprobieren, ob die Servos da eventuell zu zittern oder knurren
anfangen.
Ich kann es leider nicht testen. Hast du es denn schonmal testen können?
Gruß Steffen
Hallo Steffen,
momentan habe ich keinen Versuchsaufbau; deswegen kann ich Dein Programm
nicht testen.
Peter Dannegger schrieb:> Man hat keinen Sendepuffer und somit hat der SPI-Slave nur einen halben> SCK-Takt Zeit, in den Interrupt zu springen und das nächste Byte zu> schreiben, also ein Ding der Unmöglichkeit.
Wenn dem so ist und außerdem saubere Servoimpulse generiert werden
sollen, gibt es nur noch zwei Interrupts, die die Pulsdauern festlegen.
Die Kommunikation mit dem Master kann nur per Polling mit Handshake
erfolgen.
Mal probieren ob das dann funktioniert.
Mit bestem Gruß
U.
Peter Dannegger schrieb:> Man hat keinen Sendepuffer und somit hat der SPI-Slave nur einen halben> SCK-Takt Zeit, in den Interrupt zu springen und das nächste Byte zu> schreiben, also ein Ding der Unmöglichkeit.
Damit meint der Peter aber das zurücksenden von Daten als Slave. Als
Slave Daten empfangen klappt schon ganz gut.
Steffen
Hallo Steffen,
langsam komme ich weiter.
Per Handshake ist mir eine einwandfreie Datenübertragung gelungen.
Die beiden MCUs müssen dann eben immer kurz aufeinander warten;
allerdings dürften dann auch bei längeren ISRs keine Daten verloren
gehen.
Die Programmcodes sind nicht gekürzt, vielleicht hat später mal jemand
Interesse daran.
Die wichtigen Routinen heißen beim Master Bytetransfer und
Daten_an_Slave, beim Slave mainloop und ml2.
Viele Grüße
U.