Hallo Forum,
diese Frage stelle ich mir im Zusammenhang mit einem LPC824-Controller
von NXP, allerdings ist das für die Frage weitgehend irrelevant.
Normalerweise schreibe ich für jedes logische Hardware-Modul einen
low-level-Treiber, z.B. für UART, I2C, SPI-Master. Diese Treiber stellen
alle nötigen Funktionen bereit, die die restliche Software braucht,
sodass außer diesen Treiber-Funktionen niemand anders mit der Hardware
in Berührung kommt. Das kommt einem auch bei einem Wechsel der
Mikrocontroller-Architektur zugute, da nur die low-level Treiber
angepasst werden müssen. Sämtliche andere Funktionalität kann übernommen
werden (soweit zumindest die Theorie :-) ).
Allgemein sieht das dann zum Beispiel UART so aus:
1
voiduart0_init(uint32_tbaudrate);
2
// operiert auf einen Ring-Puffer
3
// blockiert nur, wenn der Puffer voll ist
4
voiduart0_putc(char);
5
voiduart0_puts(constchar*);
6
booluart0_has_data(void);
7
// operiert auf einen Ring-Puffer
8
// blockiert nur, wenn der Puffer leer ist
9
charuart0_getc(void);
10
//...
Darauf kann man dann weitere Funktionalität aufbauen, wie z.B. das
Senden und Empfangen von ganzen Frames mit Checksumme usw. (also so wie
im Schichtenmodell).
Die Anwendung wird aber nicht vom Treiber festgelegt, dieser ist
lediglich für das Übertragen von einzelnen Bytes verantwortlich.
So, nun zur eigentlichen Frage:
Wie designe ich so einen universellen Treiber für einen SPI-Slave?
Alles, was ich bisher geschrieben habe, war massiv von der Anwendung
abhängig.
Konkret möchte ich so was:
Dafür müsste ich dann natürlich einen groß genugen Buffer im Treiber
anlegen, in den dann die Daten von spi_slave_set_data() hineinkopiert
werden. Eine Möglichkeit wäre natürlich, die Größe des Buffers der
spi_slave_init()-Methode mitzugeben, aber dass läuft dann auf
dynamischen Speicher raus.
Während dem Kopieren müsste zumindest der SPI-Interrupt gesperrt werden,
um konsistente Daten zu haben (sonst wird eventuell die Hälfte von den
neuen und die Hälfte der alten Daten übertragen). Wobei das
wahrscheinlich keine so gute Idee ist, wenn man den Interrupt sperrt,
evtl. wären zwei Buffer, zwischen denen dann immer gewechselt wird
besser...
Des weiteren möchte man eventuell DMA (soweit auf dem Controller
verfügbar) verwenden. Wie implementiert man das in dem Treiber? Oder
würde man dort einen extra Treiber schreiben, der dann ausschließlich
mit DMA operiert?
Fragen über Fragen also.
Also nochmal in aller Kürze:
Wie würdet ihr einen universellen SPI-Slave-Treiber programmieren?
Es geht bei dieser Frage nicht um die Implementierung an sich, sondern
um das Design der "API", also wie die Funktionen im Header-File
aussehen.
Mit freundlichen Grüßen,
N.G.
Meiner Meinung nach stellen sich die Entwickler von STM Cube da ganz
geschickt an. Die HAL ist zwar nicht Mega effizient aber einfach zu
bedienen.
Ganz wichtig für eine gute API sind vor allem aber gute Dokumentation
und Beispiele, damit es dem Anwender auch leicht fällt.
> Dafür müsste ich dann natürlich einen groß genugen Buffer im Treiber> anlegen, in den dann die Daten von spi_slave_set_data() hineinkopiert> werden. Eine Möglichkeit wäre natürlich, die Größe des Buffers der> spi_slave_init()-Methode mitzugeben, aber dass läuft dann auf> dynamischen Speicher raus.> Während dem Kopieren müsste zumindest der SPI-Interrupt gesperrt werden,
Ich mache sowas immer so das ich dem Treiber den Puffer über einen
Zeiger (incl. Puffergröße) übergebe. Dann muss ich im Treiber gar nichts
reservieren. Wenn ich den Puffer dann auslesen will, setze ich im
Treiber ein Flag, welches den Schreibzugriff des Treibers auf den Puffer
"sperrt" Dann kann zwar der Interrupt kommen, aber im Interrupt werte
ich das Flag aus und weiß ob ich die Empfangen Daten in den Puffer
schreiben darf oder nicht.
Wenn die Zeit zwischen zwei Telegrammen sehr kurz ist, kann man auch mit
zwei alternierenden Puffern arbeiten, welche wechselseitig beschrieben
und ausgelesen werden. So kannst du dir mit dem Auslesen des einen
Puffers Zeit lassen, während der andere beschreiben wird.
Ich mache da kein extra großes Brimborium um das SPI, sondern füge das
bisschen SPI-Code direkt in den entsprechenden HW-Treiber ein.
Hier mal ein Beispiel für den DAC8532 auf dem AVR:
N. G. schrieb:> void spi_slave_set_data(const uint8_t*, size_t length);
Vielleicht besser spi_read_write ? Und dann überlegen, was in Deinem
Fall besser funktioniert: Lese- und Schreibpuffer ins gleiche oder in
zwei unterschiedliche Arrays.
Ich implementiere solche Funktionen immer über einen Ringpuffer mit
Schreibe- und Lese-Pointer.
Ein ISR schreibt in den Ringpuffer und inkrementiert einen
32Bit-Counter. Der Zugriff auf das Array wird dann ´ala
1
voidISR_xyz(){
2
rxdata[wrptr%RXBUFSIZE]=...
3
wrptr++;
4
}
Dort, wo gelesen wird, gibt es dann sowas wie
1
for(;rdptr<wrptr;rdptr++){
2
uint8_tdata=rxdata[rdptr%RXBUFSIZE];
3
...
4
}
Imho ist es vorteilhaft, einen 32Bit-Zähler kontinuierlich zu
inkrementieren anstatt den Zähler schon im Kreis laufen zu lassen, da
man dann einfache Vergleiche der rd- und wr-Zähler machen kann.
Ein einmaliger malloc macht ja nichts kaputt ... Dynamische
Speicherverwaltung ist es imho erst dann, wenn man auch Speicher wieder
frei gibt und diesen dann neu nutzen möchte.
Bei I2C Treibern kenne ich das so, dass man eine Funktion zum
Datentransfer aufruft, die einen Puffer sendet und in einen anderen
Empfängt. Als Parameter gibt man zwei Zeiger auf die Puffer an, und zwei
Längen-Angaben.
N. G. schrieb:> Normalerweise schreibe ich für jedes logische Hardware-Modul einen> low-level-Treiber, z.B. für UART, I2C, SPI-Master.
Das ist auch gut so. Die Gründe hast du ja selbst genannt. Bei UART ist
das relativ leicht, weil sich dort eigentlich immer zwei gleichrangige
Partner am Interface gegenüber stehen.
Bei den übrigen Interfaces im MASTER Mode sieht das nicht ganz so gut
aus.
Bei I2C müßte man die folgenden Funktionen implementieren:
1
voidInitI2C(void);
2
boolOpenSlave(byteAdresse,boolRW);
3
boolWriteToSlave(bytewas);
4
byteReadFromSlave(boolACK);
5
voidCloseSlave(void);
Dieses Schema ist allerdings bei einigen µC nur schwer hinzukriegen,
weil nach meiner Erfahrung die I2C-Peripheriecores gar mancher µC
ziemlich verkorkst sind. So hab ich das (wenn ich mich recht entsinne)
bei einigen STM32 so erlebt, daß man dort bereits beim OpenSlave
festlegen muß, wieviel Bytes man später per WriteToSlave oder
ReadFromSlave zu übertragen gedenkt. Das ist natürlich ein häßliches
Unding, was einen extrem behindert.
Bei SPI-Master geht das ganze viel einfacher, weil man ja weiß, was man
übertragen will. Dort fallen dann bei vielen Cores ReadfromSlave und
WriteToSlave zusammen, man hat nur noch die Unterscheidung nach
tatsächlicher Datenbreite bei den Übertragungsfunktionen.
Aber eines ist gewiß: Das Modell Algorithmen oder höhere Treiber auf
höherer Ebene und lowlevel-Treiber darunter ist besser, als alles
zusammen zu ziehen und miteinander zu vermischen wie es Peter macht.
So: Slaves:
Bei I2C Slave und bei SPI-Slave sieht das Ganze ein bissel wurschtliger
aus.
N. G. schrieb:> Konkret möchte ich so was:> enum spi_data_order {LSB_FISRT, MSB_FIRST};> enum spi_mode {SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3};> void spi_slave_init(enum spi_data, enum spi_mode);> void spi_slave_set_data(const uint8_t*, size_t length);> Dafür müsste ich dann natürlich einen groß genugen Buffer im Treiber> anlegen,
Daten in Blöcken setzen? lieber nicht so, sondern einzeln in nen
Ringpuffer.
Naja, ale Slave muß man sich schon dazu durchringen, schon beim
Initialisieren festzulegen, wie man sich verhalten will. Also welche
Datenbreite man akzeptiert, was man sendet, wenn es nix zu senden gibt
(alles Einsen oder so), welche Endianess man haben will (falls das im
Core drin ist). Dann ist das Empfangen und Abgefragtwerden ja nicht von
einem selbst gesteuert, weswegen man notgedrungenerweise mit
Zwischenpuffern und mit engem Polling oder mit Events im ganzen System
arbeiten muß.
Das sieht dann nicht so aus, wie du es mit spi_set_data usw. skizziert
hast, sondern eher wie die Verhältnisse beim UART. Also Sende- und
Empfangs-Ringpuffer und Einzelzeichen-I/O, dazu entweder Polling in der
Grundschleife in main oder eben Events (sowas wie "evSPIreceived") in
einer Event-Warteschlange und wiederum Auswertung in der Grundschleife.
W.S.
Stefan U. schrieb:> Bei I2C Treibern kenne ich das so, dass man eine Funktion zum> Datentransfer aufruft, die einen Puffer sendet und in einen anderen> Empfängt.
Nö, I2C kann nicht gleichzeitig senden und empfangen.
Man braucht 2 Funktionen i2c_read() und i2c_write().
Separate Puffer sind also nur verschwendeter RAM.
Aber auch bei SPI interressieren die Sendedaten in der Regel nicht mehr.
Man kann also ruhig die empfangenen Daten im selben Puffer ablegen.
In der Regel bestimmt außerdem ein Command-Byte, ob nachfolgend Daten
gelesen oder geschrieben werden sollen. Daher hat man üblicher Weise
auch fürs SPI separate Funktionen spi_read() und spi_write(), sofern die
SPI-Peripherie überhaupt einen Blocktransfer unterstützt (z.B. EEPROM).
N. G. schrieb:> diese Frage stelle ich mir im Zusammenhang mit einem LPC824-Controller> von NXP,
Dieser µC hat sogar schon Treiber für SPI/I2C und weitere im ROM, incl.
DMA. Habe die aber selber noch nicht benutzt.
W.S. schrieb:> Aber eines ist gewiß: Das Modell Algorithmen oder höhere Treiber auf> höherer Ebene und lowlevel-Treiber darunter ist besser, als alles> zusammen zu ziehen und miteinander zu vermischen wie es Peter macht.
Recht unübersichtlich wird es aber, wenn man über den extra SPI Treiber
andere Interfaces tunneln will, z.B. UART (MAX3100), CAN (MCP2515), PIO
(MCP23S17) usw.
Für mich ist SPI kein eigenständiger Bus, sondern nur ein Werkzeug, um
externe Peripherie anzubinden.
Ansonsten müßte man ja auch fürs MMIO-Interface erstmal einen Treiber
schreiben.
Ich bin zwar für Modularisierung, aber zu kleine Häppchen verwirren
eher, als daß sie vereinfachen.
Peter D. schrieb:> Daher hat man üblicher Weise> auch fürs SPI separate Funktionen spi_read() und spi_write(), sofern die> SPI-Peripherie überhaupt einen Blocktransfer unterstützt (z.B. EEPROM).
Also mein SPI Treiber hat nur eine Funktion, uint8_t SPI_Send(uint8_t
ucVal);
Wenn ich nichts lesen will, dann ignoriere ich den Rückgabewert, wenn
ich nur lesen will, sende ich eine "0". Aber da bei SPI soweit ich weiß
lesen und schreiben immer nur gleichzeitig stattfinden kann, gibt es für
mich auch nur eine Funktion. Ich schiebe was raus und kriege was rein.
> Nö, I2C kann nicht gleichzeitig senden und empfangen.
Ich wollte nur vorschlagen, das Senden und das Empfangen nicht als
sequentiell nacheinander auszuführende Operationen zu betrachten,
sondern als eine Einheit.
I2C sendet eine Registeradresse oder ein Kommando (optional gefolgt von
Daten), um danach Daten zu empfangen. Beide passiert in einer
zusammenhängenden Transaktion. Bei SPI können Sendung und Empfang
zeitlich überlappend stattfinden. Auch dann kann man sagen, dass Sendung
und Empfang zusammen eine Transaktion bilden.
Deswegen halte ich eine Funktion für Sinnvoll, der man sowohl einen
Sendepuffer als auch einen Empfangspuffer übergibt.
SPI Funktionen, die nur einzelne Bytes senden und/oder empfangen passen
nicht zu jeder Hardware. Ein DMA Controller wäre damit z.B. völlig
nutzlose.
Peter D. schrieb:> Für mich ist SPI kein eigenständiger Bus, sondern nur ein Werkzeug, um> externe Peripherie anzubinden.
Also erstmal, SPI ist ja gar kein Bus. Die Sache mit dem /SSEL sollte
man nicht als wirkliche Adressierung ansehen.
Peter D. schrieb:> Recht unübersichtlich wird es aber, wenn man über den extra SPI Treiber> andere Interfaces tunneln will
Das sehe ich aber garnicht so. Erstens ist sowas auf der Master-Seite
sehr gut getrennt und damit übersichtlich zu handhaben und zweitens ist
man auf der Slave-Seite (was ja das Anliegen des TO war) ja gar keiner
der von dir genannten Chips, womit das genannte Tunneln slaveseitig
ersatzlos entfällt.
W.S.
Stefan U. schrieb:> SPI Funktionen, die nur einzelne Bytes senden und/oder empfangen passen> nicht zu jeder Hardware. Ein DMA Controller wäre damit z.B. völlig> nutzlose.
Nun ja, DMA ist in sehr vielen Fällen wirklich etwas ganz nutzloses.
Eben deshalb, weil DMA ja keinerlei Verarbeitungs- oder
Organisationsfunktionen besitzt, sondern eben nur Daten von A nach B
schaufeln kann.
Die einzige Ecke, wo ich DMA als nötig sehe, ist I2S bei den STM32Fxxx
Controllern - weil dort die 64 bit breiten Samples (2x 32 Bit als R/L)
eben aufgrund der mickrigen 16 Bit Peripherie nicht wohlgepuffert in
echten Samples zu gepackten und per FIFO gepufferten 64 Bit ankommen,
sondern zu 4 Häppchen a 16 Bit. Wenn man sich vorstellt, bei 192 kHz
Samplerate pro Sample jedesmal 4 Interrupts ertragen zu müssen, wird mir
übel davon. Daher DMA, aber das ist nicht wegen eines Vorteils von DMA
als solchem, sondern weil diese Peripherie in den STM32 so
grottenmiserabel ist.
W.S.
W.S. schrieb:> Slave-Seite (was ja das Anliegen des TO war)
Ein SPI-Slave ist wirklich sehr tricky, man braucht in jedem Fall
erstmal ein Protokoll. Ich würde zusätzlich noch mit einer CRC eine
minimale Absicherung vornehmen.
Ein Protokoll könnte so aussehen, daß der Master erstmal ein Commandbyte
schickt, was er vom Slave überhaupt will, denn der kann ja nicht
hellsehen. Dann muß der Master noch ein Dummybyte senden, damit der
Slave Zeit hat, das Kommando zu parsen und z.B. das erste Byte in den
Sendepuffer zu stellen. Erst danach kann man Datenbytes lesen oder
schreiben. Damit kein Transfer verloren geht, braucht das Slave-SPI auch
die höchste Interruptpriorität.
Wegen des hohen Aufwandes (Interrupt), der fehlenden Absicherung (kein
Acknowledge) und der hohen Leitungszahl kann ich Slave-SPI nicht
wirklich empfehlen. Es wird Ärger bereiten.
Was soll eine CRC auf SPI ? Angst dass auf den 3cm Leitung ein Bit
verloren geht ?
Zum synchronisieren hat man ja den CS. Es geht erst mit CS bei Zustand
Null los.
Aber ja. SPI Slave sollten in Hardware sein. zB als Schieberegister oder
mit Schieberegister Eingang, ADC, DAC, Flasch, ..
.. zwischen 2 Controllern ohne Buffer muss der Slave immer pollen.
Ausser man denkt sich etwas Starres als Protokoll aus.
Sabberalot W. schrieb:> Was soll eine CRC auf SPI ? Angst dass auf den 3cm Leitung ein Bit> verloren geht ?
Ja natürlich.
Wenn der Slave mal etwas beschäftig ist und nicht schnell genug das Byte
in den Sendepuffer stellen kann, dann liest der Master Mumpitz und
kriegt es nicht mit. Daher die CRC als Notbehelf.
Einen dummen SPI-Flash kannst Du mit 90MHz auslesen. Aber bei einem MC
als SPI-Slave kann das in die Hose gehen. Nicht alles, was ein MC kann,
kann man auch sinnvoll anwenden.
Für 3cm Leitung würde ich auch keinen 2. MC anbinden, sondern einen
etwas dickeren nehmen.
also ich mach das mit dem SPI-Slave ausschließlich in DMA. Wie Peter
schon sagt, ist es wahrscheinlich dass Interrupt zu spät kommt. Als
Slave hat man keine Macht darüber, wie der Master seinen Clock sendet.
Außerdem gibt es keine Flußkontrolle (Busy). Das folgende Byte muss
immer schon vorher bereitliegen.
Das in Kombination mit einem passenden Protokoll geht das sehr gut.
(Erstes Byte sagt wieviele Daten folgen). Die DMA antwortet im
normalfall immer mit 0 (DMA-Zeiger auf eine 0). DMA zyklisch, länge 1.
Wenn was gesendet werden soll wird ein Puffer angelegt, mit dem 1.Byte
länge + Daten + 0. Dann wird die DMA darauf zeigen lassen, Länge um 2
höher. Somit ist am ende immer wieder eine 0 im Puffer.
Ein weiteres Problem ist das Buffering in der SPI und in der DMA. Beim
STM vergehen 2 zyklen, bis ein neues Byte rauskommt. Also wenn man die
DMA verschiebt, kommen vorher noch 2 alte Bytes raus, bevor der Inhalt
des neuen Buffers kommt.
Das alles macht es sehr schwer einen passenden Treiber für alles bereit
zustellen.
Hallo an alle,
zur Zeit befinde ich mich grad im Prüfungs-Stress, wodurch ich mir für
den Rest der Woche leider keine Zeit für dieses Problem nehmen kann.
Ich werde allerdings alle eure Antworten lesen und später darauf
eingehen.
Nochmal um es klarzustellen:
Ich bin primär an einem SPI-Slave interessiert. Und vendor-libs kommen
nicht in Frage, da diese nicht austauschbar sind (vendor-lock), damit
fliegen Dinge wie die HAL oder lpcopen raus.
Ich freue mich aber auf weitere Antworten.
Mit freundlichen Grüßen,
N.G.
Hallo an alle,
Weihnachten und vor allem die Klausuren sind rum :-)
Das bedeutet, dass es hier jetzt weiter gehen kann:
Ich würde schon gerne einen low-level-Treiber schreiben (und eben nicht
die Register in einem "Higher-level"-Treiber beschreiben).
Klar, SPI ist nicht wirklich mit anderen Bussen vergleichbar.
Effektiv wird es also wahrscheinlich auf eine Implementierung mit einem
Sende- und einem Empfangspuffer hinauslaufen.
Ich denke, es wäre besser, wenn ihr einmal etwas über die aktuelle
Aufgabe erfahrt:
Der Slave (also der zu programmierende Prozessor) soll nur Daten zum
Master senden. Ein Paket besteht aus 48bit. Da die meisten SPI-Module
jeweils 8bit auf einmal schicken könnten wären es also 6 einzelne
Übertragungen pro Paket. Die Daten des Masters können verworfen werden.
Wenn keine neuen Daten vorliegen, dann dürfen die alten erneut gesendet
werden. Alle Bits 0 oder 1 ist nicht erwünscht!
Theoretisch würde ich das so umsetzen:
2 Puffer, einer fürs Senden und einer fürs Empfangen, wobei die
empfangenen Daten zwar in den Puffer geschrieben werden (kompatibilität)
aber einfach nicht ausgelesen werden.
Der Puffer für die zu sendenden Daten wird bei jedem neuen "Zyklus" zum
SPI-Modul kopiert (am besten per DMA, sonst per Interrupt).
So wären die Forderungen von oben erfüllt.
Gibt es dazu irgendwelche Einwände? ;-)
Mit freundlichen Grüßen,
N.G.