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 | |
Und ein Sleep, der gleichzeitig noch auf Events (z.B. aus anderen Threads oder IRQs) wartet:
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
Gast
#4302034
Nette Sache, aber wer auf nem Coretex den Systick-Timer nicht standardmäßig mitlaufen lässt macht was falsch...
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.)
Gast
#4302295
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 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | |
21 | |
22 | |
23 | |
24 | |
25 | |
26 | |
27 | |
28 | |
29 | |
30 | |
31 | |
32 | |
33 | |
34 | |
35 | |
36 | |
37 | |
38 | |
39 | |
40 | |
41 | |
42 | |
43 | |
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.
Gast
#4302525
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:
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | |
21 | |
22 | |
23 | |
24 | |
25 | |
26 | |
27 | |
28 | |
29 | |
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.
Gast
#4302694
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":
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | |
21 | |
22 | |
23 | |
24 | |
25 | |
26 | |
27 | |
28 | |
29 | |
30 | |
31 | |
32 | |
33 | |
34 | |
35 | |
36 | |
37 | |
38 | |
39 | |
40 | |
41 | |
42 | |
43 | |
44 | |
45 | |
46 | |
47 | |
48 | |
49 | |
50 | |
51 | |
52 | |
53 | |
54 | |
55 | |
56 | |
57 | |
58 | |
59 | |
60 | |
61 | |
62 | |
63 | |
64 | |
65 | |
66 | |
67 | |
und das mit Macro":
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | |
21 | |
22 | |
23 | |
24 | |
25 | |
26 | |
27 | |
28 | |
29 | |
30 | |
31 | |
32 | |
33 | |
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.
Gast
#4303164
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.
Gast
#4303249
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 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
das subs liegt einmal auf Adresse 0x08000FC6 (10us) und einmal auf 0x08000FDA. Mit dem align 4 erzeugt er:
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
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.
A. K. schrieb: > Ich hatte anno LPC2000 eine ähnliche Variante selbstkalibierend > implementiert. Magst du die hier vorstellen? Klingt auch interessant.
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.
Gast
#4303352
Gut, da muss erstmal einer durchsteigen ;)
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.
Danke, klingt gut! Ja, dass die 32 Bits da manche Dinge einfacher machen, ist offensichtlich.
Antwort schreiben
Bitte melde dich an, um einen Beitrag zu schreiben.