Bin gerade drüber gestolpert, dass man halt hin und wieder schon mal
ein paar simple "spin-loop" Delays benötigt, für die es aus diesem
oder jenem Grund gerade nicht lohnt, erst großartig die Timerhardware
in Gang zu setzen.
Solange man konstante Ausführungszeiten hat (also keinen Cache und
Ausführung der Befehle mit CPU-Frequenz), geht sowas durchaus auch
auf einem ARM zu implementieren. Vorbild dafür waren die seit langem
in der avr-libc vorhandenen Implementierungen, allerdings wurde der
führende Unterstrich von den Namen gestrichen, denn das hier ist halt
nicht die Systembibliothek, sondern x-beliebiger Userland-Code.
Ich möchte Dein Thread nicht mit langen Diskussionen über RTOS ja/nein,
Sinn von längeren Spin-Loops etc. zumüllen. Nur wer über die Suche
Deinen Code hier findet, sollte sich überlegen, ob er wirklich einen
solchen "AVR-Style" Spin-Delay möchte.
Eine Alternative wäre ein kleines RTOS mit wunderschön bequemen
Sleep-Funktionen und noch ein paar hübschen Sachen mehr. Eben
"CortexM-Style".
Auch für die kleinsten Cortex M0 (STM32F030) verwende ich mittlerweile
eigentlich immer ChibiOS, hier dann in der reduzierten NIL-Variante:
http://chibios.org
Sleep sieht dann so aus:
1
chThdSleepMicroseconds(100);
Und ein Sleep, der gleichzeitig noch auf Events (z.B. aus anderen
Threads oder IRQs) wartet:
Gerd E. schrieb:> Eine Alternative wäre ein kleines RTOS
Nee, du hast den Sinn von sowas nicht verstanden.
Um einen 500-ns-E-Impuls auf einem HD44780 zu generieren, hilft
mir ein RTOS rein gar nichts. Solange braucht das schon für den
Taskwechsel.
Dass Spinloop-Delays keine richtigen Timer ersetzen können, ist
(völlig unabhängig vom Prozessor) sonnenklar. Aber sie ergänzen
sich – je nach Situation.
Ingo Less schrieb:> Nette Sache, aber wer auf nem Coretex den Systick-Timer nicht> standardmäßig mitlaufen lässt
Der läuft, was genau hülfe der mir hier?
Solange ich während des Delays sowieso nichts anderes tun kann,
ist es auch so ziemlich egal, ob ich nun via SysTick eine Millisekunde
warte oder via Delay. (Nur so: ich hatte das vorher auf einem Timer
aufgesetzt, aber der ist mir zu schade für dieses bisschen Gepopel,
der soll mal bessere Dinge zu tun bekommen können.)
Jörg W. schrieb:> Um einen 500-ns-E-Impuls auf einem HD44780 zu generieren, hilft> mir ein RTOS rein gar nichts. Solange braucht das schon für den> Taskwechsel.
Das stimmt, der Taskwechsel liegt irgendwo so zwischen 1 und 2 µs
(@48MHz). Wenn es aber mehr als vielleicht 10µs werden, dann sieht die
ganze Sache anders aus, dann kannst Du die Zeit sinnvoll für was anderes
nutzen.
Auch musst Du in vielen Fällen damit rechnen, daß Dein Wartecode von was
anderem, viel wichtigerem (IRQ, High-Prio-Thread,...) unterbrochen
wurde. Deine hart gecodete Warteschleife wartet jetzt einfach noch
länger.
Da finde ich Ingos Vorschlag, den normal sowieso mitlaufenden
Systick-Timer abzufragen, gar keine so dumme Idee.
> (Nur so: ich hatte das vorher auf einem Timer> aufgesetzt, aber der ist mir zu schade für dieses bisschen Gepopel,> der soll mal bessere Dinge zu tun bekommen können.)
Ingo meint keinen der wertvollen Timer aus der Peripherie mit PWM etc.,
sondern den speziellen Systick-Timer des Cortex-M-Kerns. Der ist
eigentlich genau für solche Aufgaben gedacht.
Gerd E. schrieb:> Ingo meint keinen der wertvollen Timer aus der Peripherie mit PWM etc.,> sondern den speziellen Systick-Timer des Cortex-M-Kerns.
Ja, ist mir schon klar. Ich schrieb ja auch, dass der
selbstverstänlich auch bei mir läuft (war das erste, was ich in
Betrieb genommen habe auf meinem kleinen Experimentierboard).
> zwischen 1 und 2 µs (@48MHz)
Klar, aber danach hast du noch kein Stück nützlichen Code in der
neuen Task abgearbeitet – dann müsstest du schon wieder zurückschalten.
Dass Delays nicht das Allseligmachende sind, ist auch auf dem AVR
nicht anders. Es gibt aber Situationen, in denen man in der
Zwischenzeit ohnehin nichts anderes tun kann, genau dafür sind sie
gut. Der einzige Sinn und Zweck des geposteten Codes ist es, dass
der Compiler sich die Anzahl der zu verplempernden Taktzyklen selbst
ausrechen kann, sodass man als Programmierer eben für den E-Impuls
einfach schreiben kann "delay_us(0.5);".
(Es sollte übrigens "avr-libc style" heißen, ich korrigiere das mal.
Es geht dabei nur um die API-Kompatibilität, nicht um grundlegende
Konzepte, wo man besser einen Timer benutzen würde.)
Es ist zwar richtig, dass eine hardwaregestütze Delay-Funktion besser
ist, hin und wieder will man aber mal was austesten oder nur eine kurze
Verzögerung haben. Dann ist ein simples Delay nicht die schlechteste
Lösung. Um wenigstens einigermaßen reproduzierbare Zeiten zu bekommen
sollte die innere Schleife aber in assembler programmiert werden, damit
es nicht im Debug- oder Releasemodus zu extremen Unterschieden in der
Laufzeit kommt. Dazu verwende ich folgenden Codeschnipsel. Den
Grundgedanken hatte ich mal irgendwo her, wo weiss ich aber nicht mehr.
Bei meinen Tests ursprünglich auf einem LPC1769 musste ich feststellen,
dass es immer wieder extreme Unterschiede in der Laufzeit bei der
kleinsten Codeänderung in anderen Programmteilen gab. Bei den LPC11xx
oder LPC17xx liegt das im Flashcaching begründet. Das kann bei anderen
Controllern anders aussehen. Mir hat jedenfalls geholfen die innerste
Schleife mit align4 immer für den cache gleich auszurichten.
Sollte das ganze für einen M3/M4 verwendet werden, ist "subs" an Stelle
für "sub" zu verwenden.
Ich habe das bei mehreren Controllern am Laufen. Wenn ich einen neuen
Typ in die Finger kriege, messe ich das ganze mit einem LA mal aus und
tune das fein. Auf große Genauigkeit kommt es da nicht an.
Das Register SystemCoreClock wird eigentlich immer vom CMSIS gesetzt.
Wenn nicht, dann muss da der Systemtakt rein.
1
// Microsecond delay loop-
2
externuint32_tSystemCoreClock;
3
voidDelayuS(uint32_tuS)
4
{
5
uint32_tCyclestoLoops;
6
7
CyclestoLoops=SystemCoreClock<<1;
8
if(CyclestoLoops>=2000000)
9
{
10
CyclestoLoops/=1000000;
11
CyclestoLoops*=uS;
12
}
13
else
14
{
15
CyclestoLoops*=uS;
16
CyclestoLoops/=1000000;
17
}
18
19
if(CyclestoLoops<=320)
20
return;
21
22
CyclestoLoops-=100;// cycle count for entry/exit 100? should be measured
23
CyclestoLoops/=8;// cycle count per iteration
24
25
if(!CyclestoLoops)
26
return;
27
28
// das align 4 ist extrem wichtig, sonst
29
// sind Unterschiede im faktor 2 möglich jenachdem
30
// wie der Code erzeugt wird
31
__asmvolatile
32
(
33
// Load loop count to register
34
" mov r3, %[loops]\n"
35
" .align 4\n"
36
"loop: sub r3,#1 \n"// für M3/M4 hier subs verwenden
temp schrieb:> Das Register SystemCoreClock wird eigentlich immer vom CMSIS gesetzt.> Wenn nicht, dann muss da der Systemtakt rein.
Davon abgesehen, dass Atmel es leider nicht für nötig hält, das zu
implementieren, was mich daran stört ist, dass man dann die
Berechnung der Zyklenzahl zur Laufzeit macht. Gerade für sehr kurze
Delays entsteht daraus wieder zusätzlicher Overhead.
Obige Version erledigt (mit aktivierter Optimierung) alles zur
Compilezeit.
Dass so ein paar simple Delays ihre Grenzen haben (vor allem bei CPUs
mit Cache), ist logisch.
Mit den Berechnungen hast du natürlich recht. Allerdings hat deine
Variante den zusätzlichen Nachteil im Debugmodus die Berechnungen auch
noch in double auszuführen. Da ist es manchmal besser man bleibt bei
Macros. Hab deine Variante mal auf einem STM32F103 getestet:
Da bei sowas die Zeiten immer direkt im Code stehen, kann das der
Präprozessor rechnen. Das align habe ich wieder eingebaut und musste für
den STM32 die Zeiten durch 6 teilen. Die -4 am Ende beseitigt für meine
72MHz bei kleinen Zeiten etwas die fixen Zeiten.
Damit lagen die Unterschiede bei delay_us(10) bei <0.5us zwischen debug
und release. Deine inline-Variante ergibt im Debugmodus 38us.
Den Teil um an einem Ausgang zu Wackeln lasse ich hier mal komplett
aussen vor.
temp schrieb:> Allerdings hat deine> Variante den zusätzlichen Nachteil im Debugmodus die Berechnungen auch> noch in double auszuführen.
Ja.
-O0 kommt für mich einfach nicht in Frage, weil man dann sowieso
was komplett anderes debuggt als das, um was es eigentlich geht. ;-)
> Da bei sowas die Zeiten immer direkt im Code stehen, kann das der> Präprozessor rechnen.
Der Präprozessor rechnet nicht, das macht immer der Compiler. Der
Präprozessor ersetzt nur stur Texte. Der wesentliche Unterschied
mit den Makros dürfte es sein, dass ein -O0 das Inlining obiger
Funktionen verhindert. Wenn es auch -O0-fähig sein soll, müsste man
wohl auch delay_us und delay_ms als attribute "always_inline"
festzurren, das wird m. E. auch bei -O0 honoriert. Da die Ersetzung
der konstanten double-Ausdrücke ja offenbar mit deinen Makros klappt,
müsste sie dann auch funktionieren.
Ich habe das oben mal entsprechend modifiziert.
Jörg W. schrieb:> Der Präprozessor rechnet nicht, das macht immer der Compiler. Der> Präprozessor ersetzt nur stur Texte.
Danke Herr Lehrer, das spielt in dem Zusammenhang aber keine Rolle.
Jörg W. schrieb:> Der wesentliche Unterschied> mit den Makros dürfte es sein, dass ein -O0 das Inlining obiger> Funktionen verhindert. Wenn es auch -O0-fähig sein soll, müsste man> wohl auch delay_us und delay_ms als attribute "always_inline"> festzurren
das mit dem "müsste" ist immer so eine Sache. Manchmal ist es besser man
weiß was raus kommt auch wenn man beim Beschreiben Fehler macht, als
wenn man ratet.
das ist jedenfalls das Ergebnis mit "inline":
Das inlinen hat zwar funktioniert, aber die Berechnungen werden noch mit
double gemacht. Ich bleibe dabei: inline ist für diesen Zweck nur die 2.
Wahl. Es ist jedenfalls eine ziemliche Fehlerquelle, wenn etwas so
dramatisch von den Optimierungseinstellungen abhängig ist. Noch dazu
wenn der Leidensdruck das nicht erfordert. Trotzdem, es war
aufschlussreich.
temp schrieb:> das ist jedenfalls das Ergebnis mit "inline":
Danke fürs Testen!
Ein bisschen mysteriös ist es schon, dass der GCC den
Gleitkommaausdruck (wie er aus dem Präprozessor dann rausfällt) ohne
Optimierung trotzdem zur Compilezeit ausrechnet, die inline-Funktion
jedoch nicht. Naja, lässt sich dann wohl nicht ändern.
> Noch dazu wenn der Leidensdruck das nicht erfordert.
Inline-Funktionen sind ein wenig netter anzusehen, man hat lokale
Variablen und nicht diese grässliche Backslashitis. Aber es ist
natürlich richtig, wenn man damit Funktionalität einbüßt, die man
mit den Makros bekommen kann, dann sind sie die zweite Wahl. Ich
schau's mir heute abend nochmal an, und werde dann die
Makro-Implementierung nehmen.
Ich hatte übrigens gar nicht damit gerechnet, dass der Compiler
diese Ausdrücke ohne Optimierung jemals zur Compilezeit umsetzt,
daher hatte ich den Fall „Optimierung ist aus“ gedanklich für so
ein Vorhaben ausgeklammert. (Wer nicht optimiert, dem ist die
Ausführungszeit ja sowieso egal. ;-)
Was mich übrigens noch stutzig macht: warum musst du das eigentlich
durch 6 teilen? Ach, hmm, hast du wait states für den Zugriff auf
den Flash? Wäre noch die Frage, wie man diese in die Formel Einzug
halten lässt.
Jörg W. schrieb:> Was mich übrigens noch stutzig macht: warum musst du das eigentlich> durch 6 teilen? Ach, hmm, hast du wait states für den Zugriff auf> den Flash? Wäre noch die Frage, wie man diese in die Formel Einzug> halten lässt.
So ganz genau kann ich dir das auch nicht sagen, da kenne ich mich mit
dem arm-Kern und dem Assembler dazu doch zu wenig aus. Da ich bei meinen
Tests vor langer Zeit diese merkwürdige Abhängigkeit vom alignment
dieser paar Assembleranweisungen hatte, habe ich mir das mit dem
Flash-Zugriff erklärt. Man könnte jetzt noch untersuchen ob die selbe
Routine im RAM ausgeführt anders läuft, aber da steht für mich der
Aufwand in keinem vernünftigen Verhältnis mehr. Mir reicht es wenn es
reproduzierbar passt.
temp schrieb:> Man könnte jetzt noch untersuchen ob die selbe Routine im RAM ausgeführt> anders läuft
Vermutlich schon.
Die Ausführungszeiten der Befehle des Cortex-M0+ sind ja durch ARM
so vorgegeben, da können die einzelnen Siliziumhersteller nichts
ändern. Damit kann es aber nur an Flash-Waitstates liegen. Der
SAMD20, den ich hier gerade in den Fingern habe, kann bis 24 MHz
ohne Waitstates arbeiten (dann passt das auch mit dem Teilen durch
4), darüber müsste man da auch einen Waitstate einschieben.
Allerdings müssten es mit einem Waitstate eigentlich 7 CPU-Zyklen
pro Iteration werden (vier für die Befehlsausführung, drei
Waitstates für drei Befehle).
Bei Ausführung aus dem RAM geht's natürlich bis zu maximalem Takt
ohne Waitstates.
Ich habe eben mal etwas interessantes reproduzieren können auf einem
stm32f334:
Nehme ich das align 4 raus kriege ich bei folgendem Code:
while (1)
{
aLed.Set();
delay_us(10);
aLed.Clr();
delay_us(10);
}
10us Low und 15us Heigh!
1
ImDebuggerfindeich2malfolgendenCode:
2
3
6C7Bldrr3,[r7,#0x44]
4
3B01subsr3,#1
5
2B00cmpr3,#0
6
D1FCbne0x08000FC6
7
647Bstrr3,[r7,#0x44]
das subs liegt einmal auf Adresse 0x08000FC6 (10us) und einmal auf
0x08000FDA. Mit dem align 4 erzeugt er:
1
6C7Bldrr3,[r7,#0x44]
2
BF00nop
3
F3AF8000nop.w
4
F3AF8000nop.w
5
F3AF8000nop.w
6
08000FE0:3B01subsr3,#1
7
2B00cmpr3,#0
8
D1FCbne0x08000FE0
9
10
.....
11
12
6C3Bldrr3,[r7,#0x40]
13
F3AF8000nop.w
14
F3AF8000nop.w
15
F3AF8000nop.w
16
08001000:3B01subsr3,#1
17
2B00cmpr3,#0
18
D1FCbne0x08001000
damit komme ich auf 2x 10us und es passt. Ich wollte eigentlich nicht
soviel Zeit hier rein investieren, finde aber das schon interessant ist
und für Verwirrung sorgen kann. Die Cortexe sind eben keine AVRs wo
zählen allein immer stimmt.
Ja, klar, der Flash ist natürlich 32-bittig organisiert bei einem
ARM. Das erklärt dann auch den Teilerfaktor 6: du holst die ersten
beiden Befehle mit einem zusätzlichen Waitstate (+ 2 Takte für die
Ausführung), danach wird der Sprungbefehl ebenfalls mit einem Waitstate
geholt plus weitere zwei Takte für die Ausführung.
Hmm, ok, danke für den Tipp! Ich werde das Alignment da ebenfalls
noch reinsetzen.
Jörg W. schrieb:> Allerdings müssten es mit einem Waitstate eigentlich 7 CPU-Zyklen> pro Iteration werden (vier für die Befehlsausführung, drei> Waitstates für drei Befehle).
so einfach ist es glaube ich nicht: die meisten Cortexe haben ja noch
einen Cache, der den Effekt der Waitstates mindern soll. Ob und wie
effektiv der Cache ist, hängt dann aber vom Alignment und von Sprüngen
ab. Sieht man ja an den Ergebnissen von temp oben.
Und es hängt natürlich von der genauen Implementation des Caches ab. Und
soweit ich weiß, implementiert jeder Hersteller hier seinen eigenen
Cache, der ist nicht von ARM vorgegeben. Auch unterscheiden sich die
Caches zwischen den unterschiedlichen Linien eines Herstellers.
Ich hatte anno LPC2000 eine ähnliche Variante selbstkalibierend
implementiert. Bei Start des Systems wird über einen bekannten Timer,
Systick oder RTC, das Zeitverhalten der Delay-Routine gemessen und ein
Faktor abgeleitet. Der zur Kalibrierung verwendete Timer ist danach frei
für anderweitige Verwendung.
Damit sind dann auch dynamische Zeiten möglich, solange der Core einen
Multiplier hat und nicht zu langsam getaktet wird. Und der Code
funktioniert unabhängig von Waitstates, Caches und dergleichen. Dafür
handelt man sich durch den Referenz-Timer eine Abhängigkeit von der
Controller-Familie ein.
Jörg W. schrieb:> Magst du die hier vorstellen? Klingt auch interessant.
Da der LPC2129 sicherlich nicht mehr viele Freunde hat und der Code
ausserdem schlecht aus meiner Devicelib rausgezupft werden kann hier mal
die STM32 Variante. Viel verwendet worden ist die allerdings nicht, also
Vorsicht damit. Nicht adaptiv ist bisher die Angabe des Overheads, da
könnte man noch dran feilen.
Sind auch Definitionen drin, die ich mir zu den CMSIS Includes
hinzugefügt habe. Sowas wie SysTick_LOAD_RELOAD_Msk findet sich dort
nicht. Also direkt übersetzen lässt sich das für euch nicht. Müsst ihr
schon selbst was draus machen.
Bisschen Erklärung: Der Trick liegt darin, in der Schleife nicht
Durchläufe zu zählen, sondern Nanosekunden zu addieren bis die Zeit
erreicht ist. Wär mit 16 Bits etwas eng, aber der ARM hat genug davon.
Die Kalibrierung bestimmt, wieviele Nanosekunden ein Durchlauf benötigt.
Ein Faktor ist in dieser Variante nicht erforderlich und der Multiplier
wird nur für dynamische µs/ms gebraucht, oder beim unoptimierten
Debugging.
Es kann natürlich sein, dass der Durchlauf nicht genau in ns
spezifizierbar ist. Gibt dann eine systematische Abweichung. Aber bei
ohnehin ungenauen Spinloops sollte man damit leben können, solange die
verwendeten ARMe weit genug von den GHz weg sind.
Die Schleife ist mit Absicht nicht inlined, um in jedem Fall immer
denselben Code an derselben Stelle zu haben.