Forum: Mikrocontroller und Digitale Elektronik delay: nop() als Schleife ausführen?


von Hartmut (Gast)


Lesenswert?

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?

von Falk B. (falk)


Lesenswert?

Was spricht dagegen, die _delay_us() Funktion bzw. _delay_ms() funktion 
nachzubauen? In der Doku der Lib-C steht viel dazu, in den Inludes der 
Rest.

von Hartmut (Gast)


Lesenswert?

Du meinst z.B. ?
1
_delay_loop_1(uint8_t __count)
2
 {
3
         __asm__ volatile (
4
                 "1: dec %0" "\n\t"
5
                 "brne 1b"
6
                 : "=r" (__count)
7
                 : "0" (__count)
8
         );
9
 }

Werde ich mir mal genauer anschauen.

von Dr. Sommer (Gast)


Lesenswert?

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.

von Peter D. (peda)


Lesenswert?

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.

von Jim M. (turboj)


Lesenswert?

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.

von Falk B. (falk)


Lesenswert?

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.

von Pete K. (pete77)


Angehängte Dateien:

Lesenswert?

Ich hab mir mal das hier zusammengesammelt für einen STM32. Damit 
funktioniert auch OneWire.

von Hartmut (Gast)


Lesenswert?

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.

von Hartmut (Gast)


Lesenswert?

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.

von Dr. Sommer (Gast)


Lesenswert?

Hartmut schrieb:
> Leider hat das enormen Overhead
Ein paar Zyklen muss man beim Timer-Abfragen halt abziehen um den 
Overhead auszugleichen...

von Peter D. (peda)


Lesenswert?

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"

von Hartmut (Gast)


Lesenswert?

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
inline void delay_us(uint32_t us)
2
  {
3
    LPC_TMR32B1 ->TC = 0;    //debug
4
    LPC_TMR32B1 ->TCR = 1;    //debug
5
    uint32_t tc = DELAY_TIMER ->TC;    // get current timer value
6
    uint32_t delayclocks = (us * 1000000) / delay_clktime_us;    // convert us in (nanoseconds * 1000) as "clockbase"
7
    uint32_t loopclocks;
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
        asm volatile ("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.

von Peter D. (peda)


Lesenswert?

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.

von Hartmut (Gast)


Lesenswert?

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ß

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.