Hochauflösender Systemtimer in STM32

Ich habe einige Software auf einem STM32F103 (ich möchte auch F4 verwenden können), bei der ich Ereignisse (z besser).

Da die Software einige Zeit laufen wird, möchte ich einen 64-Bit-Timer verwenden, und für mich war es sinnvoll, den eingebauten SysTick-Timer zu verwenden. Ich habe SysTickMajor (einen 64-Bit-Zähler) erstellt, der jedes Mal erhöht wird, wenn SysTick überläuft, und eine Funktion getSystemTime, die diesen Zähler und den aktuellen Wert von SysTick kombiniert, um eine genaue 64-Bit-Zeit zu erhalten:

#define SYSTICK_RANGE 0x1000000 // 24 bit
volatile long long SysTickMajor = SYSTICK_RANGE;

voif onInit(void) {
  SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
  SysTick_Config(SYSTICK_RANGE-1);
  /* Super priority for SysTick - is done over absolutely everything. */
  NVIC_InitStructure.NVIC_IRQChannel = SysTick_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}

void SysTick_Handler(void) {
  SysTickMajor+=SYSTICK_RANGE;
}

long long getSystemTime() {
  long long t1 = SysTickMajor;
  long long time = (long long)SysTick->VAL;
  long long t2 = SysTickMajor;
  // times are different and systick has rolled over while reading
  if (t1!=t2 && time > (SYSTICK_RANGE>>1)) 
    return t2 - time;
  return t1-time;
}

Das einzige Problem ist, dass dies nicht 100% genau zu sein scheint - zum Beispiel, wenn ich Folgendes mache:

bool toggle = false;
while (true) {
  toggle = !toggle;
  LED1.write(toggle);
  long long t = getSystemTime()() + 1000000;
  while (getSystemTime() < t);
}

Ich bekomme keine gute Rechteckwelle - gelegentlich kommt es zu Störungen (obwohl der SysTick-Interrupt sehr schnell beendet wird), weil die von getSystemTime zurückgegebene Zeit an einigen Stellen völlig falsch ist.


UPDATE: Wenn dies passiert (ich zeichne den Zeitwert bei einem Interrupt auf, der durch ein externes Ereignis ausgelöst wird), speichere ich die letzten 3 Werte auf seriell. Wiederholt erhalte ich Dinge wie:

>0x278-E2281A 
 0x278-E9999A 
 0x278-0001E5

>0x5BE-F11F51
 0x5BE-F89038
 0x5BE-0000FB

Ich habe Striche eingefügt, um darzustellen, welche Bits von SysTick stammen.

Um Zweifel zu vermeiden, habe ich den Code von starblue unten verwendet. Ich habe es auch geändert, um einen 32-Bit-Wert für SysTickMajor zu verwenden, und ich habe SysTick-> VAL mit AND verknüpft, nur für den Fall.

Es scheint, dass der SysTick_Handler den anderen Interrupt nicht vorwegnimmt!

Jetzt habe ich das überprüft und hatte vorher die Preemption-Prioritäten falsch, aber jetzt habe ich NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);die Interrupts eingerichtet und eingerichtet - SysTick-Preemption-Priorität 0 und der andere Interrupt ist 7.

Muss ich noch etwas tun, um die Präemption zu erhalten?


UPDATE2: Ich habe einen winzigen Testfall für genau dieses Problem erstellt (SysTick verhindert keine anderen Interrupts) und habe ihn hier abgelegt:

Probleme mit STM32-Interrupt-Priorität (Präemption).


Was ist also falsch an dem obigen Code? Gibt es einen besseren Weg, um einen guten hochauflösenden Timer zu bekommen? Es scheint etwas Offensichtliches zu sein, das die Leute tun möchten, aber ich habe Mühe, Beispiele zu finden.

Vielleicht probierst du mal DMA aus? Erhöhen Sie eine Variable bei einem Timer-Überlauf.
Haben Sie ein Beispiel für das Inkrementieren einer Variablen mit DMA? Nur eine Anmerkung: Es sollte eigentlich bei jeder Erwähnung von SysTickMajor in getSystemTime eine Interrupt-Deaktivierung/-Aktivierung geben - aber selbst das verhindert nicht die gelegentlichen Störungen
Ihr Hauptfehler ist, dass Sie laufen while (getSystemTime() < t);: Diese Funktion sehnt sich nach einiger Zeit, deshalb werden Sie nicht in der Lage sein, die Zeit perfekt zu bekommen. Arbeiten Sie an Interrupts oder führen Sie eine atomare Operation der Zeitberechnung durch (SysTick-Interrupt würde eine long longVariable durch DMA oder einen anderen Mechanismus ändern, und zuletzt könnte Ihre Zeile so aussehenwhile(SystemTime - SystemTime0 < 1000000)
Es ist nicht so, dass es ein bisschen daneben ist - gelegentlich kann der Puls irgendwo zwischen 0 und der Länge liegen, die er haben sollte. Ich habe jetzt die Frage geändert, nachdem ich einige andere Dinge ausprobiert habe. Wenn Sie jedoch eine Lösung haben, die DMA beinhaltet (ich bin mir nicht sicher, wie das gemacht werden kann?), wäre ich sehr interessiert.

Antworten (4)

Wie Sie festgestellt haben, ist die Handhabung des Zählerüberlaufs mit einem Interrupt-Handler schwierig und rennanfällig. Selbst mit Ihrer Prioritätserhöhung erhalten Sie die falsche Antwort, wenn Interrupts deaktiviert sind, während Sie den Zähler ablesen. Es gibt eine viel einfachere Technik, die funktioniert, wenn Sie die Uhr nicht aus dem Interrupt-Kontext lesen (beachten Sie, dass ich hier den DWT-Zykluszähler verwende, der so schnell und einfach in den Prozessor integriert ist):

uint64_t last_cycle_count_64 = 0;

// Call at least every 2^32 cycles (every 59.6 seconds @ 72 MHz).
// Do not call from interrupt context!
uint64_t GetCycleCount64() {
  last_cycle_count_64 += DWT->CYCCNT - (uint32_t)(last_cycle_count_64);
  return last_cycle_count_64;
}

Wenn Sie es aus dem Interrupt-Kontext aufrufen müssen (oder mit einem präemptiven Betriebssystem):

volatile uint64_t last_cycle_count_64 = 0;

// Call at least every 2^32 cycles (every 59.6 seconds @ 72 MHz).
uint64_t GetCycleCount64() {
  uint32_t primask;
  asm volatile ("mrs %0, PRIMASK" : "=r"(primask));
  asm volatile ("cpsid i");  // Disable interrupts.
  int64_t r = last_cycle_count_64;
  r += DWT->CYCCNT - (uint32_t)(r);
  last_cycle_count_64 = r;
  asm volatile ("msr PRIMASK, %0" : : "r"(primask));  // Restore interrupts.
  return r;
}

Möglicherweise müssen Sie zuerst den Zykluszähler aktivieren:

DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;  // Set bit 0.
Schön - also löscht das bloße Lesen des Zählers ihn? Ich denke, dies könnte möglicherweise auf dem SysTick-Timer-Überlauf-IRQ aufgerufen werden, wodurch sichergestellt würde, dass es nie zum Überlauf kommt.
Nein, das Lesen des Zählers löscht ihn nicht. Dieser Code aktualisiert einfach einen größeren 64-Bit-Zähler mit dem aktuellen Wert des kleineren 32-Bit-Zählers. Prinzipiell könnten Sie dasselbe mit dem SysTick-Register machen, aber Sie müssten Ihr Update häufiger durchführen.
Diese allgemeine Methode scheint gut zu sein, wenn Sie einen benutzerdefinierten Timer dafür erstellen. Der DWT->CYCCNT-Zähler wird jedoch nur erhöht, wenn die Debug-Sonde angeschlossen ist. Wenn Sie es mit getrenntem Debug-Probe verwenden möchten, müssen Sie es separat aktivieren. Dies kann einem Angreifer jedoch den Zugriff auf alle Debug-Register ermöglichen, sodass es nicht ratsam erscheint, sich auf DWT->CYCCNT zu verlassen.
Evan sagt: "Der DWT->CYCCNT-Zähler wird nur erhöht, wenn die Debug-Sonde angeschlossen ist". Zumindest auf meiner Plattform (STM32F407) scheint der CYCCNT-Zähler zu inkrementieren, unabhängig davon, ob eine Debug-Sonde angeschlossen ist. Ich schaue mir das ARMv7-M Architecture Reference Manual (DDI0403E_d) an und sehe keine Erwähnung, dass ein Debug-Probe erforderlich ist. Ich sehe, dass der Zähler anhält, wenn der Prozessor angehalten wird, was normalerweise wünschenswert ist: "CYCCNT wird nicht erhöht, wenn der Prozessor im Debug-Zustand angehalten wird."
Ah, Sie müssen CYCCNT aktivieren, indem Sie das niedrige Bit von DWT_CTRL setzen. Der Antwort wurde eine Beschreibung hinzugefügt.

Ihr Code hat zwei Probleme:

  • Sie wissen nicht, ob der Interrupt, der sich erhöht hat, SysTickMajorvor oder nach dem Einlesen des Timer-Werts in aufgetreten ist time.

  • Operationen auf 64-Bit-Werten sind auf einem 32-Bit-System nicht atomar. Der Wert für SysTickMajorkönnte also durch den Interrupt zwischen dem Lesen der beiden 32-Bit-Wörter aktualisiert werden.

Ich würde es wie folgt machen:

major0 = SysTickMajor;
minor1 = SysTick->VAL;
major1 = SysTickMajor;

minor2 = SysTick->VAL;
major2 = SysTickMajor;

if ( major0 == major1 )
{
    time = major1 + minor1;
}
else
{
    time = major2 + minor2;
}

Hier gehen Sie davon aus, dass sich SysTickMajoreine Änderung im ersten Aufgabenblock im zweiten Block nicht mehr ändert. (Diese Annahme könnte falsch sein, wenn dieser Code während dieser Routine unterbrochen wird und längere Zeit nicht ausgeführt werden kann.)

Siehe auch diese Frage zu Stackoverflow .

Danke - ich habe jetzt begonnen, Ihren Code zu verwenden (siehe geänderte Frage), aber ich habe immer noch das Problem! Ich habe sogar die 64-Bit-Werte auf 32 Bit heruntergetauscht, um sicherzugehen, dass dies kein Problem war. Es sieht jetzt so aus, als ob der SysTick-Interrupt nicht aufgerufen wird, wenn SysTick überläuft, sondern dass ein Synchronisationsproblem vorliegt.
Ich ziehe es vor, niedrig/hoch/niedrig zu lesen und bei Bedarf zu wiederholen. Unter anderem funktioniert dieser Ansatz auch dann korrekt, wenn die "logische Übertragsausbreitung" langsam ist. Nehmen wir zum Beispiel an, dass der Prozessor, wenn er die Daten ausliest, bereits mit der Ausführung der Anweisung zum Einlesen SysTick->VALvon begonnen hat . Der Wert könnte einen umgebrochenen Timer widerspiegeln, obwohl dies nicht der Fall wäre. Bei der Verwendung von Low/High/Low werden solche Dinge nicht passieren, es sei denn, der relative Zeitunterschied zwischen High- und Low-Word-Aktionen überschreitet die Zeit zwischen den beiden Low-Word-Lesevorgängen. SysTickMajormajor1minor1major1

Ich habe gerade die Antwort von einem sehr hilfreichen Poster im STM32-Forum gefunden:

Folgendes ist nicht korrekt. SysTick ist ein 'System Handler', und als solcher wird die Priorität überhaupt nicht auf diese Weise festgelegt:

  NVIC_InitStructure.NVIC_IRQChannel = SysTick_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // Highest priority
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);

Es ist eigentlich eingestellt mit:

  NVIC_SetPriority(SysTick_IRQn, 0);

Das Aufrufen dieses Codes löst das Problem stattdessen!

Meine Version von getSystemTime könnte also ein Problem gehabt haben, und die Methode von starblue ist besser als meine - die eigentliche Ursache der Probleme war jedoch die oben genannte.

Ich dachte nur, ich würde getSystemTime eine weitere mögliche Verbesserung hinzufügen, die ich auch ausprobiert habe:

do {
 major0 = SysTickMajor;
 minor = SysTick->VAL;
 major1 = SysTickMajor;
while (major0 != major1);
return major0 - minor;

Dadurch wird das Problem, bei dem Sie (irgendwie) dazu führen könnten, dass der SysTick zweimal in sehr schneller Folge implementiert wird, effektiv negiert.

Die vorherigen Antworten sind großartig. Dieses hier gefällt mir am besten:

do {
 major0 = SysTickMajor;
 minor = SysTick->VAL;
 major1 = SysTickMajor;
while (major0 != major1);
return major0 - minor;

Sie können es noch ein bisschen optimieren, da Sie wissen, dass Sie bei einem Rollover von SysTickMajor „lange Zeit“ keinen weiteren Rollover mehr bekommen werden. Sie sollten sicher sein, dies zu tun:

major = SysTickMajor;
minor = SysTick->VAL;
if (major != SysTickMajor)
    return SysTickMajor - SysTick->Val;
return major - minor;

Das Obige setzt natürlich voraus, dass SysTickMajor und VAL als flüchtig deklariert sind. Vor einigen Jahren habe ich bei einem früheren Job festgestellt, dass das Obige leider immer noch nicht auf allen Prozessoren perfekt ist. Ich habe einen STM32F407 verwendet, der über einen Cache verfügt und über eine Pipeline verfügt. Manchmal kam der Abruf des zweiten SysTickMajor vor dem VAL an, obwohl sie in der richtigen Reihenfolge ausgegeben wurden. Geben Sie die Speicherbarriere ein (dmb-Anweisung):

major = SysTickMajor;
minor = SysTick->VAL;
__asm__ volatile("dmb");
if (major != SysTickMajor)
    return SysTickMajor - SysTick->Val;
return major - minor;

In meinem Fall habe ich ein 32-Bit-TIM2 und ein TIM2-Rollover-ISR verwendet, das die oberen 32 Bits meines 64-Bit-Zykluszählers erhöht, aber ich denke, es sollte sich genauso verhalten wie das, was Sie mit SysTick tun.