Hallo zusammen,
für einen Bitbanging-Treiber auf einem STM32F446 und einem STM32F103
habe ich mir mal einen krude Delay-Funktion für Nanosekunden
zusammengebastelt:
Wie man sieht, ist die bei sehr kurzen Verzögerungen Taktgenau
(mindestens aber einen Takt) und bei längeren Verzögerungen auf 5 Takte
genau.
Da ein Bitbanging-Treiber nun wirklich eine Stelle ist, wo mir jeder
eingesparte Taktzyklus nützt, würde ich das jetzt gerne noch ein bischen
genauer bekommen. Perfekt wäre eine Genauigkeit von +0...+1 Takt.
Nun bin ich dummerweise nicht fit genug in Assembler/Inline Assembler,
um zu zählen, wieviele Takte exakt der Ein- und der Rücksprung der
inline-Assembler-Funktion kostet.
An dieser Stelle frage ich tatsächlich zum ersten Mal nicht nach Hilfe
zur Selbsthilfe, sondern in der Richtung "Wer kann mir was machen"?
Einfach deshalb, weil hier "wissen/können und einfach machen" und
"einarbeiten und selbst machen" in so krassem Unterschied stehen.
Vielleicht bist Du der Nachbar/Fremde/Bekannte, dem ich schon ein Brett
gehobelt, das Gartentor geschweißt oder dem Auto Starthilfe gegeben habe
und kannst mir ein paar Zeilen Inline Assembler schenken.
Viele Grüße
W.T.
Nun kann ich da wohl wahrlich nicht helfen, aber mal 3 Gegenfragen:
Macht auf einem 32-bitter mit 64 bit zu rechnen nicht mehr "overhead"
als die wirklich wenigen Takte, die du sparen willst?
Ist DWT_CYCCNT nicht das Einfachste und Genaueste für sowas?
Ist das "harte" Rechnen in einer delay-Funktion nicht kontraproduktiv?
Lutz schrieb:> Macht auf einem 32-bitter mit 64 bit zu rechnen nicht mehr "overhead"> als die wirklich wenigen Takte, die du sparen willst?
Da die Funktion "static inline" ist und der Eingabeparameter schon zur
Compilezeit feststeht: Nein.
Lutz schrieb:> Ist DWT_CYCCNT nicht das Einfachste und Genaueste für sowas?>> Ist das "harte" Rechnen in einer delay-Funktion nicht kontraproduktiv?
Es geht um einen Bitbanging-Treiber (paralleler Bus).
Um ein Gefühl für die Größenordnung zu bekommen: Der längste Wert, mit
dem die Funktion bislang aufgerufen wird, ist 30 ns, d.h. die Funktion
evaluiert zu gerade einmal sechs NOP() (auf dem STM32 mit 168 Mhz), der
häufigste Aufruf erfolgt mit 10 ns, also zwei NOP().
Aber: Diese Funktion brennt im Auge. Ich weiß, daß es besser gehen muß.
Sie hat mich vor allen Dingen viel Zeit bei der Hardware-Fehlersuche
gekostet (Liegt es am Timing? Warum sehe ich nichts am Oszilloskop, wenn
ich von 30 auf 40 ns umstelle ?????)
Nur leider übersteigt "besser machen" hier meine Fähigkeiten.
Lutz schrieb:> Ist DWT_CYCCNT nicht das Einfachste und Genaueste für sowas?
Das macht man üblicherweise mit einer Schleife, und allein schon des
Branchings wegen wird das in diesem Fall kaum möglich sein. Bis etwa
runter auf 1 Mikrosekunde geht das noch, aber wenn hier auch 10 ns
gefordert sind, dann ist DWT_CYCCNT nicht gangbar.
Walter T. schrieb:> Nun bin ich dummerweise nicht fit genug in Assembler/Inline Assembler,> um zu zählen, wieviele Takte exakt der Ein- und der Rücksprung der> inline-Assembler-Funktion kostet.
Naja wenn Du die Funktion static inline machst, dann gibt's keinen
Sprung.
Ich nehme mal an, Du nimmst GCC, oder?
Die grundsäztliche Syntax geht so:
Das Gesaue mit dem attribute ist nötig, weil "inline" nur ein
unverbindlicher Vorschlag für den Compiler ist. Und "inline" selber ist
nötig, weil das attribute alleine nicht bei GCC durchgeht.
Das kannst Du als C-Funktion aufrufen, und die wird vom Compiler
garantiert ge-inlined. Jetzt kannste dann erstmal am Oszi mit
unterschiedlich vielen NOPs messen, wieviel Delay Du da eigentlich
bekommst bei Deinen Controllern, den eingestellten Taktfrequenzen und
DCache/ICache/Prefetch.
Also im C-Code ein IO toggeln und dazwischen die Funktion aufrufen und
erstmal schauen, wo man da eigentlich so landet.
Im nächsten Schritt, wenn Du die Daten hast, kannst Du dann einen
Parameter an die Funktion übergeben, einen uint32_t, der dann in R0
landet. Nur, 10ns, das sind 100 MHz. Da ist keine Zeit mehr für
irgendwelche Abfragen, fürchte ich.
Von daher kann's bei so kurzen Delays das Einfachste sein, eine
delay-10ns-Funktion zu haben, eine für 20, eine für 30, und dann erst
eine, die dynamisch Delay ergibt.
Ach ja, und die GPIOs natürlich nicht über HAL setzen, das gibt keine
aussagefähige Messung, sondern direkt über das betreffende BSRR. Damit
hast dann auch nicht die Arie mit Laden, AND/OR, Speichern.
>Perfekt wäre eine Genauigkeit von +0...+1 Takt.>Nun bin ich dummerweise nicht fit genug in Assembler/Inline Assembler,>um zu zählen, wieviele Takte exakt der Ein- und der Rücksprung der>inline-Assembler-Funktion kostet.
Vergiss es. Jeder Interrupt versaut dir das. Oder DMA Transfers.
Wer sowas will sollte einen CPLD oder FPGA nehmen.
holger schrieb:> Jeder Interrupt versaut dir das.
Das ist ein Assembler-Befehl, und die sind alle abgeschaltet. Sogar der
Systick, obwohl der eine Exception ist.
> Oder DMA Transfers.
Muß man nicht anschalten. Scho gar nicht bei so einem Projekt.
Guten Morgen,
holger schrieb:> Vergiss es. Jeder Interrupt versaut dir das. Oder DMA Transfers.
Wenn ich ISRs und Peripheriebuslast durch DMA-Transfers nicht schon viel
früher berücksichtig hätte als erst zu dem Zeitpunkt, wo ich die
Wartezeiten*) taktgenau bekommen will, wäre das doch grob fahrlässig.
Nop schrieb:> Also im C-Code ein IO toggeln und dazwischen die Funktion aufrufen und> erstmal schauen, wo man da eigentlich so landet.
Das mache ich natürlich. Allerdings ist die Aussagekraft kleiner, als
man glauben mag. In Anbetracht dessen, daß ein MCU-Takt noch schlappe
5,6 ns beträgt, einer Slew-Rate, die in der gleichen Größenordnung liegt
und dem Peripheral Bus, der auch einen gewissen Jitter verursacht, weil
er niedriger als die MCU getaktet ist, ist es schwer bis unmöglich zu
beurteilen, ob jetzt eine Funktion 6 oder 7 Takte gewartet hat.
Die Zeiten für langes warten stimmen schon (im Rahmen meiner
Meßgenauigkeit) in der Funktion, die ich oben gepostet habe. Die für
kurze Wartezeiten bis 6 Takte natürlich auch. Jetzt wäre es noch schön,
wenn der Zwischenbereich auch stimmen würde.
Ich gehe auch davon aus, daß eine Funktion, die das für kurze und lange
Wartezeiten taktgenau hinbekommt, prinzipiell komplett identisch ist: Im
Wesentlichen eine Herunterzählschleife mit bekannter Anzahl an Takten
und ein Jump über mehrere NOPs, um den (Divisions-) Rest an Takten auch
noch zu verbrauchen.
Ich denke, für einen guten Taktzähler ist das eine kleine Fingerübung.
Ich bin nur leider komplett unmusikalisch.
Viele Grüße
W.T.
*) Wartezeiten := Mindest-Wartezeiten
Walter T. schrieb:> Wartezeiten taktgenau hinbekommt, prinzipiell komplett identisch ist: Im> Wesentlichen eine Herunterzählschleife mit bekannter Anzahl an Takten> und ein Jump über mehrere NOPs, um den (Divisions-) Rest an Takten auch> noch zu verbrauchen.
Wenn Du auch 10 ns warten können willst, ein Takt aber schon gut 5 ns
dauern, dann wirst Du da keine Division, kein Herunterzählen, kein
compare und keinen Branch schaffen. Ist einfach so.
Ziemlicher Käse das Ganze. Entweder, man MUSS wirklich taktgenau
arbeiten, dann packt man das als KOMPLETTE Funktion in eine separate
Funktion in einer separaten Assemblerdatei. Damit kann man normal und
ohne Handstände ASM programmieren und bekommt EXAKT was man braucht.
Oder es ist soweit zeitunkritisch, daß man kein ASM und auch keine
ASM-Macros braucht, dann bleibt man bei C und gut.
Aber auf C-Ebene mit solchen Stunts taktgenaues ASM hinzubiegen ist
Murks^3.
Falk B. schrieb:> Aber auf C-Ebene mit solchen Stunts taktgenaues ASM hinzubiegen ist> Murks^3.
Danke sehr!
Manche brauchen das .... im Elfenbeinturm vor sich hinspinnen.
Nop schrieb:> kein Herunterzählen, kein compare und keinen Branch schaffen.
Wobei, das compare braucht es nicht, weil die Statusflags beim
Herunterzählen ohnehin setzbar sind. Ändert nichts daran, daß man mit
zwei Takten nicht den Overhead eines Schleifenkörpers nebst Division
realisiert bekommt.
Zudem hat Falk natürlich auch insofern recht, als daß man die
Delay-Funktion zwar als inline-C-Funktion aufrufen kann, aber keine
Kontrolle hat, was der Compiler dann noch alles tut, beispielsweise
register spill. Besonders, wenn dann auch noch etwas gerechnet werden
soll, also Schleife und Division.
Nop schrieb:> Wenn Du auch 10 ns warten können willst, ein Takt aber schon gut 5 ns> dauern, dann wirst Du da keine Division, kein Herunterzählen, kein> compare und keinen Branch schaffen. Ist einfach so.
Wenn ich 10 ns warten will, mache ich 2 Nops. Oder 1 Nop. Je nach
Target.
Falk B. schrieb:> man MUSS wirklich taktgenau> arbeiten,
Ich muß nicht komplett taktgenau arbeiten. Ich brauche nur
Mindest-Wartezeiten. Und je genauer meine IST-Wartezeiten an den
Mindest-Wartezeiten sind, desto mehr Bus-Durchsatz habe ich.
Ist OK. Ich habe verstanden. Hier finde ich niemanden, der helfen will,
dafür viele, die lästern wollen. Ist auch einfacher. Dafür muß man
nichts können.
Walter T. schrieb:> Und je genauer meine IST-Wartezeiten an den> Mindest-Wartezeiten sind, desto mehr Bus-Durchsatz habe ich.
Ja schon klar. Und dafür muss man dann an den letzten
Nanosekunden herumpfriemeln.
Walter T. schrieb:> Hier finde ich niemanden, der helfen will,
Nein. Richtig ist: Hier findest du niemanden der deine
Krümelkackerei mitmachen will.
Walter T. schrieb:> Wenn ich 10 ns warten will, mache ich 2 Nops. Oder 1 Nop. Je nach> Target.
Eben, Und wenn Du zur Laufzeit rausfinden willst, wieviel Du brauchst,
ist das zuviel Overhead. Deswegen nichts mit Divisionen und Schleifen
für zwei Takte.
Walter T. schrieb:> Ist OK. Ich habe verstanden.
Nein, hast Du nicht. Zudem finde ich es unverschämt von Dir, daß ich Dir
erst eine Assemblerversion zumindest mal zur Messung mundgerecht
serviere und dann sowas kommt:
Walter T. schrieb:> Hier finde ich niemanden, der helfen will
Nop schrieb:> Eben, Und wenn Du zur Laufzeit rausfinden willst, wieviel Du brauchst,> ist das zuviel Overhead.
Niemand hat davon gesprochen, daß das zur Laufzeit berechnet werden
soll. Wird es doch aktuell auch nicht.
Nop schrieb:> Nein, hast Du nicht. Zudem finde ich es unverschämt von Dir, daß ich Dir> erst eine Assemblerversion zumindest mal zur Messung mundgerecht> serviere
Wunderbar. Diese Zeilen Assembler machen genau einmal NOP();. Da ist
mein Gebastel im Eingangsposting ja schon weiter.
Walter T. schrieb:> Wunderbar. Diese Zeilen Assembler machen genau einmal NOP();.
Ich ging davon aus, daß Du in der Lage bist, weitere NOP-Zeilen zu
ergänzen. Editoren haben dafür copy&paste. Dann ging ich davon aus, daß
Du erstmal NACHMISST, was da für ein Timing rauskommt, bevor man das
komplexer macht. Machste alles nicht, weil Du zu faul bist.
> Da ist> mein Gebastel im Eingangsposting ja schon weiter.
Dann noch viel Spaß mit Deiner Switch-Struktur und zwei Takten Delay. Du
machst das schon.
Ist nur eine Frage, da ich selber nicht so der Hirsch bin; aber wie
sieht es denn mit den Waitstates aus, wenn der ART Accelerator™ noch
dreinfunkt beim STM32F446?
Ich glaube nicht, dass das überhaupt "stabil" lösbar ist, sobald
Prefetch und/oder Cache ins Spiel kommen. Je nachdem, wo Deine Loop
liegt, kann sie unterschiedlich schnell sein. Nur mit ASM (und .balign)
sehe ich da überhaupt eine Chance.
Jörg
Es geht mir nur darum, eine Funktion zu haben, die taktgenau eine
bestimmte Anzahl an Takten verbraucht. Die Anzahl an Takten ist zur
Build-Zeit bekannt und minimal 1.
Lassen wir mal Caching- und Bus-Effekte außen vor. Wir können sie eh
nicht vermeiden. Cortex M3 und M4 bieten keine Out-of-Order-Execution -
das heißt das Problem ist prinzipiell lösbar. Wenn eine einzelne
Wartezeit mal einen oder zwei Takte länger dauert, ist das kein
Weltuntergang.
Die C-Funktion im Anhang erfüllt all diese Anforderungen, hat aber
gewisse Nachteile. Ich bin sicher, dass es mit einer oder zwei
Inline-Assembler-Funktionen deutlich eleganter geht.
Selbst bekomme ich eine solche Funktion nicht hin. Der DWT-Zähler ist
zwar super für grobes Profiling, aber eine taktgenaue Aussage, wie lange
jetzt mein Inline-Assembler gebraucht hat, klappt so nicht. Da hilft nur
Zyklen zählen.
Walter T. schrieb:> Es geht mir nur darum, eine Funktion zu haben, die taktgenau eine> bestimmte Anzahl an Takten verbraucht.
Deine gesamte Herangehensweise ist falsch. Das ist es.
Wenn du mal genauer hinschauen würdest, dann sähest du, daß jeder
Interrupt deine ganze Taktgenauigkeit zunichte macht. Und wenn du jetzt
bockig reagierst und dir sagst, 'dann verbiete ich die eben', dann bist
du auf der ollen AVR-Denke, die man besser bleiben lassen sollte.
Also pfeife auf die ganze "Taktgenauigkeit" und nimm für kürzere Zeiten
eine simple Delayschleife OHNE Assemblereinschub und schätze diese
einfach per Oszi ein. Bei Interrupts wird sie länger, also plane das
ein. Und wenn deine Hardware damit nicht klarkommt, dann hast du selbige
falsch entwickelt.
W.S.
W.S. schrieb:> Wenn du mal genauer hinschauen würdest, dann sähest du, daß jeder> Interrupt deine ganze Taktgenauigkeit zunichte macht.
Ich habe doch nie taktgenaue Verzögerung gefordert. Ich will lediglich
eine taktgenaue Auflösung der Verzögerungsfunktion. Mehr nicht.
W.S. schrieb:> und nimm für kürzere Zeiten> eine simple Delayschleif
Schleifen sind mir an einigen Stellen schon viel zu lang. Mindestens 3
Zyklen.
W.S. schrieb:> Bei Interrupts wird sie länger, also plane das> ein. Und wenn deine Hardware damit nicht klarkommt, dann hast du selbige> falsch entwickelt.
Interrupts sind kein Problem, und alles kommt damit klar.
W.S. schrieb:> und schätze diese einfach per Oszi ein
Genau darauf habe ich keine Lust. Für jedes Delay für jedes Target bei
jeder einstellbare Taktkonfiguration von vorn einschätzen - nein.
Die Hardware funktioniert ganz hervorragend, wenn ich die mit dem
Taschenrechner berechnete Anzahl an NOP(); einfüge. Also ist der
sinnvolle nächste Schritt, das Ganze vom Compiler vorberechnen zu
lassen. Bevor bei der nächsten Taktänderung des Targets alles wieder von
vorn in Handarbeit eingepflegt werden muss.
Walter Walter,
Liest du eigentlich, was du da schreibst?
> Es geht mir nur darum, eine Funktion zu haben, die taktgenau eine> bestimmte Anzahl an Takten verbraucht.> Die Anzahl an Takten ist zur Build-Zeit bekannt und minimal 1.
und dann gleich kommt gleich hinterher:
> Wenn eine einzelne Wartezeit mal einen oder zwei Takte länger dauert,> ist das kein Weltuntergang.
Ein oder zwei Takte mehr macht also bis 300% Abweichung. Ist das etwas
"taktgenau"?
Hinzu kommt, dass die Ungenauigkeit tatsächlich sogar größer sein wird.
Da ist nichts mehr genau.
Das Einzige, was realistisch machbar ist, sind mindest-Verzögerungen.
Nenne deine Funktion doch "delay_mindestens_ns()".
Das ist wohl eher ein Problem der Textauslegung: Wenn ich fordere, dass
eine Funktion genau 3 Takte verbraucht, heißt das, dass nach dem Ende
der Funktion mindestens 3 Takte vergangen sein müssen.
Jede Verzögerung ist eine Mindest-Verzögerung. Das ist eine typische
Eigenschaften von Verzögerungen, dass es unter Umständen noch länger
dauern kann.
Walter T. schrieb:> Die Hardware funktioniert ganz hervorragend, wenn ich die mit dem> Taschenrechner berechnete Anzahl an NOP(); einfüge. Also ist der> sinnvolle nächste Schritt, das Ganze vom Compiler vorberechnen zu> lassen. Bevor bei der nächsten Taktänderung des Targets alles wieder von> vorn in Handarbeit eingepflegt werden muss.
Steht die Taktfrequenz sowie die Anzahl der Nanosekunden bei jedem
Delay-Aufruf als Compilezeit-Konstante zur Verfügung?
Nop schrieb:> Steht die Taktfrequenz sowie die Anzahl der Nanosekunden bei jedem> Delay-Aufruf als Compilezeit-Konstante zur Verfügung?
Natürlich. Sonst wäre die Aufgabe nicht lösbar.
Walter T. schrieb:> Natürlich. Sonst wäre die Aufgabe nicht lösbar.
Ich glaub, ich hab ne Idee.. welchen Nanosekundenbereich brauchst Du
dafür? Reicht 0-50?
Nop schrieb:> welchen Nanosekundenbereich brauchst Du dafür?
Gar keinen.
Er spinnt nur herum und meint so etwas zu brauchen.
Und alle die, die krampfhaft eine Lösung finden wollen
sind die Verarschten.
Nop schrieb:> welchen Nanosekundenbereich
Naja, also ob 1...50 Nanosekunden oder 1...12782640 Nanosekunden (bei
168 MHz) sollte bei der Implementierung nicht viel Unterschied machen.
Es wird ja auf das hier hinauslaufen:
Walter T. schrieb:> eine Herunterzählschleife mit bekannter Anzahl an Takten> und ein Jump über mehrere NOPs, um den (Divisions-) Rest an Takten auch> noch zu verbrauchen.
Walter T. schrieb:> Es wird ja auf das hier hinauslaufen:
Nein, ich dachte eher an ein paar üble Makro-Hacks, die ohne Schleife
auskommen. Ich hab aber nicht daran gedacht, daß der Präprozessor keine
Ausdrücke auswertet, sondern erst der Compiler im Schritt danach.
Tja, ansonsten kannst Du das case noch aufbohren, bis die verbleibende
Genauigkeit beim generischen Delay vernachlässigbar wird. Mit
Taktzählung wirst Du hier eh nichts, das sagte Falk ja auch schon, wenn
Du nicht ALLES in Assembler machst, also auch den Rest vom Treiber.
Da hängt zuviel daran, wie der Compiler im konkreten Fall gerade
optimieren kann. Da wird mal ein Register auf den Stack geworfen und
wieder zurückgeholt oder auch nicht, bei minimalen Anpassungen im
Quelltext. Mit LTO nochmal mehr.
Aber die Idee mit den Macros geht doch! Muß ja gar nicht über den
Präprozessor sein, das darf irgendwo in der Buildchain sein.
Im simpelsten Fall schreibst Du Dir ein Programm, dem Du auf der
Kommandozeile die Taktfrequenz des Systems übergibt, und das schmeißt
dann ein .h-File mit Macros für die relevanten Delays raus. Sowas hier:
1
#define DELAY_NS_10 { \
2
asm volatile("nop;"); \
3
asm volatile("nop;"); \
4
} while 0
5
6
#define DELAY_NS_15 { \
7
asm volatile("nop;"); \
8
asm volatile("nop;"); \
9
asm volatile("nop;"); \
10
} while 0
Wieviele Nops da reinkommen, kann sich das Tool ja nach Dreisatz
berechnen mit Auf- oder Abrundung.