Hallo,
beim Umstieg von 8-bit AVRauf die Cortex M ist mir jetzt das Problem
untergekommen, daß es dort keine delay-Funktionen gibt. Der Grund soll
der sein, daß prinzipbedingt wegen Pipelines, Sprungvorhersage usw. kein
"sicheres" Vorhersagen der Laufzeit möglich sein soll.
Für längere delays im ms-Bereich ist das kein Problem; da ist die
"Unschärfe" akzeptabel. Aber im µs-Bereich wird es schon sehr schwierig.
Ich habe da schon einige Varianten ausprobiert, aber bei 12 MHz, z.B 5
µs für einen DS18B20, halbwegs hinzubekommen nicht. Nun besann ich mich
auf das gute alte asm volatile "nop", was auch an sich funktioniert. Nur
kann man ja kaum z.B. 100 nop's in den Quelltext schreiben, zumal das ja
auch nicht variabel ist. Und in einer Schleife durchzulaufen, macht
alles noch viel ungenauer.
Wie macht ihr das?
Falk Brunner schrieb:> Was spricht dagegen, die _delay_us() Funktion bzw. _delay_ms() funktion> nachzubauen?
Dass die Laufzeit stark variieren wird, wegen eben der Pipeline und
Out-of-Order-Execution. Deswegen macht man soetwas mit Timern; die kann
man problemlos zyklengenau programmieren, und in einer "busy-loop" fragt
man ab ob der Timer schon abgelaufen ist. Alternativ mit Interrupt&WFI.
Besser ist natürlich, das ganze komplett in Hardware zu machen -
mithilfe von UART/SPI -Modulen, Timer-PWM-Input oder Timer+DMA lassen
sich schon so einige serielle Protokolle verwerten.
Haben die Dinger nicht tonnenweise 32Bit-Timer?
Nimm einfach einen Timer.
Den Timer initialisierst Du einmalig, dann läuft er durch.
Zum Start des Delays lädst Du in ein Compareregister Timer + Delay,
löscht das Pending-Flag und loopst, bis es wieder gesetzt ist.
Dr. Sommer schrieb:> Dass die Laufzeit stark variieren wird, wegen eben der Pipeline und> Out-of-Order-Execution.
Cortex M3 kennt keine Out-of-Order-Execution, und die Pipline ist IIRC
3-stufig. Wenn man die eigentliche Warteschleife in Assembler packt,
z.B. wie [Beitrag "Re: lpcxpreeso SystemFrequency LPC1768"], wird
das normalerweise recht genau. Allerdings werden Interrupts die
Wartezeit verlängern.
Peter Dannegger schrieb:> Haben die Dinger nicht tonnenweise 32Bit-Timer?
Das ist sehr stark von der konkreten Variante abhängig, da diese
Peripherie vom Hersteller nach Gutdünken eingebaut wird. Der immer
vohandene Systick ist nur 24-bittig.
Auch beim AVR sind die _delay Funktionen nicht ultimativ genau,
Interrupts stören dort genau so. Kurze Verzögerungen im zweistelligen us
Bereich kann man so schon machen, denn die Schleife läuft auch mit Cache
mit konstanter Geschwindigkeit. Das man Verzögerungen im ms Bereich
besser mit Timern macht ist klar, aber für Testzwecke geht es auch mal
mit delay. Ist bei ARM nicht anders als bei AVR.
Peter Dannegger schrieb:> Den Timer initialisierst Du einmalig, dann läuft er durch.> Zum Start des Delays lädst Du in ein Compareregister Timer + Delay,> löscht das Pending-Flag und loopst, bis es wieder gesetzt ist.Dr. Sommer schrieb:> Deswegen macht man soetwas mit Timern; die kann> man problemlos zyklengenau programmieren, und in einer "busy-loop" fragt> man ab ob der Timer schon abgelaufen ist.
So habe ich es eigentlich auch schon probiert: Einen 32 bit-Timer mit
maximaler Geschwindigkeit (coreclock) frei laufen lassen. Zu Beginn der
Funktion den aktuellen Wert lesen, delay hinzuaddieren und dann den
Timer auf kleiner Zielwert pollen. So kommen sich z.B. verschachtelte
delays (z.B. Interrupt) nicht in die Quere, auch wenn das Timing des
ersten delays wohl dahin wäre. Leider hat das enormen Overhead (z.B. 206
statt 72 clocks bei 12 MHz; entspricht gut 17 statt 5 µs). Überlauf auch
berücksichtigt, wobei das kaum etwas ausmacht und bei 72 zu über 4
Milliarden auch nicht sehr wahrscheinlich ist.
Ich werde mal schauen, ob das in Assembler etwas bringt.
Pete K. schrieb:> Ich hab mir mal das hier zusammengesammelt für einen STM32. Damit> funktioniert auch OneWire.
Ohne es jetzt getestet zu haben: 25 MHz / 8 ist gut 3,5 mal langsamer
als 12 MHz. Ein clock wäre etwa 0,32 µs.
Den Timer auf Null setzen, starten und am Ende wieder stoppen kostet
auch zusätzlich Zeit.
Hartmut schrieb:> Leider hat das enormen Overhead (z.B. 206> statt 72 clocks bei 12 MHz; entspricht gut 17 statt 5 µs).
Dein Cortex M ist also erheblich langsamer als ein AVR?
Auf einem AVR mit 5MHz funktioniert das 1-Wire mit einer solchen
Delayfunktion nämlich:
Beitrag "DS1820, DS18B20 in C"
Nun gebe ich ja ehrlich zu, kein sonderlicher Profi zu sein. Mit
assembler schon gar nicht. Wie weit man dem Disassembler trauen kann
(compiliert mit -S), weiß ich nicht. gcc und ohne Optimierung. Ich nutze
die aktuelle LPCXpresso-Umgebung auf einem Cortex M0, 12 MHz.
Mein bisher bester Versuch
1
inlinevoiddelay_us(uint32_tus)
2
{
3
LPC_TMR32B1->TC=0;//debug
4
LPC_TMR32B1->TCR=1;//debug
5
uint32_ttc=DELAY_TIMER->TC;// get current timer value
6
uint32_tdelayclocks=(us*1000000)/delay_clktime_us;// convert us in (nanoseconds * 1000) as "clockbase"
7
uint32_tloopclocks;
8
if((tc+delayclocks)<0xFFFFFFFF)
9
{
10
loopclocks=tc+delayclocks;// minus a few clocks to compensate calculation time for value of delay_clocks doesn't work
11
}
12
else
13
{
14
loopclocks=delayclocks-(0xFFFFFFFF-tc);
15
}
16
while(DELAY_TIMER->TC<loopclocks)
17
{
18
asmvolatile("nop");//;
19
}
20
LPC_TMR32B1->TCR=0;//debug
21
}
Den Timer LPC_TMR32B1 nutze ich nur zum Messen, da der M0 leider nichts
anderes zu bieten hat. Er verursacht sicher auch einen Overhead, aber
der ist zum Vergleichen konstant.
delay_clktime_us hat in einer init-Funktion nur den aktuellen coreclock
in Nanosekunden * 1000 umgerechnet.
Beide Timer laufen mit coreclock.
Im Disassembler kommt da schon ordentlich lang was raus; hätte ich nicht
gedacht. Interessanter Weise bringt in der letzten while das nop 15
clocks weniger als ein leeres statement; wieder was gelernt.
Hartmut schrieb:> uint32_t delayclocks = (us * 1000000) / delay_clktime_us;
Ne, sowas läßt man natürlich nicht erst zur Laufzeit ausrechnen. Wenn
Dein MC keine HW-Division hat, wird das ein richtig fetter
Unterprogrammaufruf.
Man nimmt ein Macro mit Konstanten, das schon zur Compilezeit
ausgerechnet wird.
Schau Dir einfach mal mein Beispiel an (Macro DELAY_US(x)).
Zur Laufzeit ist dann nur noch eine Subtraktion mit Negativtest zu tun.
Vielen Dank, an 32 bit angepaßt klappt das mit einem kleinen konstanten
Offset schon sehr gut. Im Disassembler ist es auch demensprechend
deutlich kürzer. Habe da wieder gleich mehrere Dinge gelernt:
- Wenn der Compiler es rechnen soll, müssen schon alle Zahlen zur
Compilezeit bekannt sein.
- Und man muß es dann auch tatsächlich so schreiben, daß der Compiler es
auch erkennt. Nicht "nehme dir zur Laufzeit eine Variable und lese dort
...". Ich hatte ein wenig spekuliert, daß der Compiler das auch so
erkennt und entsprechend optimiert. Aber natürlich Quatsch und ohne
eingeschaltete Optimierung ja sowieso unmöglich.
- Zuerst dachte ich, daß der mögliche Überlauf des frei laufenden Timers
nicht berücksichtigt wäre. Ist aber ganz schön clever gemacht!
Einziger Nachteil durch die Konstanten ist, daß ein Wechsel des
coreclocks so nicht geht. Beim Cortex M0 in rechenintensiven Abschnitten
schon eher eine Option, aber wann kommt es mal vor und dann weiß man
auch, wo man aufpassen muß