Embedded C - Die eleganteste Art, eine Verzögerung einzufügen

8

Ich arbeite an einem Projekt mit einem cortex-m4 mcu (LPC4370). Und ich muss eine Verzögerung einfügen, während ich die Compiler-Optimierung einschalte. Bisher bestand meine Problemumgehung darin, einen digitalen Ausgang innerhalb einer for-Schleife auf und ab zu bewegen:

for (int i = 0; i < 50000; i++)
{
     LPC_GPIO_PORT->B[DEBUGPIN_PORT][DEBUG_PIN1] = TRUE;
     LPC_GPIO_PORT->B[DEBUGPIN_PORT][DEBUG_PIN1] = FALSE;
}

Aber ich frage mich, ob es einen besseren Weg gibt, GCC zu täuschen.

a_bet
quelle
8
Dies ist kein guter Weg, um Verzögerungen zu machen.
Marko Buršič
3
Wie lange soll die Verzögerung sein? Wie genau muss es sein?
Elliot Alderson
4
Sie können einen Timer so einstellen, dass der Unterlauf / Überlauf für die gewünschte Verzögerung unterbrochen wird, und einfach in den Ruhemodus wechseln. Der Prozessor wacht bei dem Interrupt auf, der einfach eine einzelne return-Anweisung haben könnte.
Peter Smith
5
Ich stimme dafür, diese Frage als nicht zum Thema gehörend zu schließen, da es sich um ein XY-Problem handelt: Die Verwendung einer Verzögerung im Kommunikationscode ist im Wesentlichen immer falsch. Sie müssen Ihr eigentliches Problem verstehen und lösen . In Fällen ohne Kommunikation, in denen eine Verzögerung angemessen ist, werden Sie feststellen, dass die meisten MCU-Software-Setups über einen Verzögerungsmechanismus für ausgelastete Wartezeiten verfügen.
Chris Stratton
3
@ChrisStratton Mir ist nicht bekannt, dass Beiträge vom Typ XY überall im Netzwerk ein gültiger enger Grund sind.
März 2377

Antworten:

22

Der Kontext dieser Inline-Verzögerung ohne Abhängigkeit fehlt hier. Ich gehe jedoch davon aus, dass Sie während der Initialisierung oder eines anderen Teils des Codes, in dem er blockieren darf, eine kurze Verzögerung benötigen.

Ihre Frage sollte nicht sein, wie man GCC zum Narren hält . Sie sollten GCC sagen, was Sie wollen.

#pragma GCC push_options
#pragma GCC optimize ("O0")   

for(uint i=0; i<T; i++){__NOP()}

#pragma GCC pop_options

Von der Oberseite meines Kopfes wird diese Schleife ungefähr 5 * T Uhren sein.

( Quelle )


Fairer Kommentar von Colin zu einer anderen Antwort . Es ist nicht garantiert, dass ein NOP Zyklen auf einem M4 benötigt. Wenn Sie die Geschwindigkeit verringern möchten, ist ISB (Flush Pipeline) möglicherweise die bessere Option. Siehe das allgemeine Benutzerhandbuch .

Jeroen3
quelle
Ok, es ist das erste Mal, dass ich ein #Pragma sehe. Wenn ich das richtig verstanden habe, gilt diese Art von Einstellungen nur für diesen kleinen Codeabschnitt. Würden Sie dies über eine Implementierung empfehlen, die einen Timer verwendet?
a_bet
2
Sie können nopstattdessen auch eine Nichtanweisung verwenden, die nicht aus der Pipeline entfernt wird.
Harry Beadle
4
@ Jeroen3: -O0macht ein bisschen schlechter als 5 * T , so etwas wie 8 Anweisungen mit ein paar Overheads. Es wäre besser, eine kurze optimierte Schleife zu erstellen (oder zumindest eine, die auf die gleiche Weise kompiliert wird, ohne Pragmas zu verwenden) und __asm__ __volatile__("");zu verhindern, dass GCC die Schleife entfernt, dh so etwas .
Groo
1
@Groo Ich kann nicht glauben, dass wir die Wirksamkeit von Verzögerungscode auf die schmutzigste Art und Weise diskutieren, die der Mensch kennt. Aber ja, eine flüchtige Inline-Montagelinie funktioniert genauso gut. Ich glaube, das Pragma drückt die Absicht jedem neuen Leser besser aus.
Jeroen3
2
asm volatile ist der richtige Weg, wenn Sie keine vom Hersteller bereitgestellte Verzögerungsfunktion / kein Makro haben. Deaktivieren Sie Optimierungen nicht, auch nicht für eine Zeile, da dies die for-Schleife beeinträchtigt.
Navin
12

Verwenden Sie einen Timer, falls Sie einen zur Verfügung haben. Der SysTick ist sehr einfach zu konfigurieren. Die Dokumentation finden Sie im Cortex M4-Benutzerhandbuch (oder M0, wenn Sie sich im M0-Teil befinden). Erhöhen Sie eine Zahl in ihrem Interrupt, und in Ihrer Verzögerungsfunktion können Sie blockieren, bis die Zahl eine bestimmte Anzahl von Schritten erhöht hat.

Ihr Teil enthält viele Timer, wenn der Systick bereits verwendet wird und das Prinzip dasselbe bleibt. Wenn Sie einen anderen Timer verwenden, können Sie ihn als Zähler konfigurieren und sich nur das Zählregister ansehen, um einen Interrupt zu vermeiden.

Wenn Sie es wirklich in Software tun möchten, können Sie es in asm("nop");Ihre Schleife einfügen. nopmuss keine Zeit in Anspruch nehmen, der Prozessor kann sie aus seiner Pipeline entfernen, ohne sie auszuführen, aber der Compiler sollte die Schleife trotzdem generieren.

Colin
quelle
Systick ist sehr einfach zu konfigurieren, aber ich empfehle, so bald wie möglich einen anderen Timer zu verwenden, da Systick seine Einschränkungen hinsichtlich der Zählergröße und Interrupts hat, wenn es für Verzögerungen verwendet wird.
DKNguyen
Sie müssen nicht einmal Interrupts verwenden, sondern nur das Zählregister abfragen. Dies sollte als flüchtig definiert werden, damit der Compiler es nicht optimiert. IMO, SysTick ist eine gute Wahl, da es häufig so konfiguriert ist, dass es einen "O / S-Timer" gibt, z. B. einen Mikrosekunden-Timer. Sie werden dann einfache wait_microseconds(100);Dinge im Code haben.
Evil Dog Pie
@EvilDogPie Ist " nur das Zählregister abfragen " nicht fast so schlimm wie nur eine enge Schleife? (obwohl es wahrscheinlich einfacher ist, GCC davon abzuhalten, es zu optimieren).
TripeHound
@TripeHound Ja, es hat genau eine enge Schleife. Das ist es, was das O / P verlangt: eine enge Schleife für eine kurze Verzögerung, die von der Compiler-Optimierung nicht entfernt wird. Es gibt Orte, an denen eine enge Schleife kein schlechter Weg ist, um eine kurze Verzögerung durchzuführen, insbesondere in einem eingebetteten System, das kein Multitasking ist.
Evil Dog Pie
10

Nicht um andere Antworten hier abzulenken, aber genau welche Längenverzögerung benötigen Sie? Einige Datenblätter erwähnen Nanosekunden; andere Mikrosekunden; und noch andere Millisekunden.

  • Verzögerungen im Nanosekundenbereich werden normalerweise am besten durch Hinzufügen von Anweisungen zur Zeitverschwendung behoben. In der Tat bedeutet manchmal die Geschwindigkeit des Mikrocontrollers, dass die Verzögerung zwischen den angezeigten Anweisungen "Pin hoch setzen, dann Pin niedrig setzen" erfüllt wurde. Ansonsten ein oder mehr NOP, JMP-bis-next-Anweisung oder andere zeitraubende Anweisungen sind ausreichend.
  • Kurze Mikrosekundenverzögerungen können durch eine forSchleife (abhängig von der CPU-Rate) verursacht werden, längere können jedoch das Warten auf einen tatsächlichen Timer rechtfertigen.
  • Millisekundenverzögerungen werden normalerweise am besten behoben, indem Sie etwas anderes vollständig ausführen, während Sie auf den Abschluss des Prozesses warten und dann zurückgehen, um sicherzustellen, dass er tatsächlich abgeschlossen wurde, bevor Sie fortfahren.

Kurz gesagt, alles hängt vom Peripheriegerät ab.

John Burger
quelle
3

Der beste Weg ist die Verwendung von On-Chip-Timern. Systick-, RTC- oder Peripherie-Timer. Diese haben den Vorteil, dass das Timing präzise und deterministisch ist und leicht angepasst werden kann, wenn die CPU-Taktrate geändert wird. Optional können Sie die CPU sogar in den Ruhezustand versetzen und einen Weckinterrupt verwenden.

Schmutzige "Busy-Delay" -Schleifen hingegen sind selten genau und weisen verschiedene Probleme auf, z. B. eine "enge Kopplung" an einen bestimmten CPU-Befehlssatz und eine bestimmte Uhr.

Einige bemerkenswerte Dinge:

  • Das wiederholte Umschalten eines GPIO-Pins ist eine schlechte Idee, da dies unnötig Strom zieht und möglicherweise auch EMV-Probleme verursacht, wenn der Pin mit Leiterbahnen verbunden ist.
  • Die Verwendung von NOP-Anweisungen funktioniert möglicherweise nicht. Viele Architekturen (wie Cortex M, iirc) können NOP auf CPU-Ebene überspringen und tatsächlich nicht ausführen.

Wenn Sie darauf bestehen möchten, eine schmutzige Besetztschleife zu generieren, reicht es aus, nur volatileden Schleifeniterator zu qualifizieren. Zum Beispiel:

void dirty_delay (void)
{
  for(volatile uint32_t i=0; i<50000u; i++)
    ;
}

Dies erzeugt garantiert verschiedene Mistcodes. Zum Beispiel -O3 -ffreestandinggibt ARM gcc :

dirty_delay:
        mov     r3, #0
        sub     sp, sp, #8
        str     r3, [sp, #4]
        ldr     r3, [sp, #4]
        ldr     r2, .L7
        cmp     r3, r2
        bhi     .L1
.L3:
        ldr     r3, [sp, #4]
        add     r3, r3, #1
        str     r3, [sp, #4]
        ldr     r3, [sp, #4]
        cmp     r3, r2
        bls     .L3
.L1:
        add     sp, sp, #8
        bx      lr
.L7:
        .word   49999

Von da an können Sie theoretisch berechnen, wie viele Ticks jeder Befehl benötigt, und die magische Zahl 50000 entsprechend ändern. Pipelining, Verzweigungsvorhersage usw. bedeuten jedoch, dass der Code möglicherweise schneller als nur die Summe der Taktzyklen ausgeführt wird. Da der Compiler beschlossen hat, den Stack einzubeziehen, könnte auch das Zwischenspeichern von Daten eine Rolle spielen.

Mein ganzer Punkt hier ist, dass es schwierig ist, genau zu berechnen, wie viel Zeit dieser Code tatsächlich benötigt. Trial & Error-Benchmarking mit einem Scope ist wahrscheinlich eine sinnvollere Idee als theoretische Berechnungen.

Lundin
quelle