Temporizador de sistema de alta resolución en STM32

Tengo un software que se ejecuta en un STM32F103 (también me gustaría poder usar el F4) donde me gustaría poder 'marcar la hora' de eventos (como interrupciones en pines externos) al microsegundo más cercano (si no mejor).

Como el software se ejecutará durante algún tiempo, quiero usar un temporizador de 64 bits y, para mí, tenía sentido usar el temporizador integrado SysTick. Creé SysTickMajor (un contador de 64 bits) que aumenta cada vez que SysTick se desborda, y una función getSystemTime que combina este contador y el valor actual de SysTick para obtener un tiempo preciso de 64 bits:

#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;
}

El único problema es que esto no parece ser 100% preciso; por ejemplo, si hago lo siguiente:

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

No obtendré una buena onda cuadrada: ocasionalmente habrá fallas (a pesar de que la interrupción de SysTick finaliza muy rápido), porque en algunos puntos, el tiempo devuelto por getSystemTime es completamente incorrecto.


ACTUALIZACIÓN: cuando esto sucede (estoy registrando el valor de tiempo en una interrupción provocada por un evento externo), descargo los últimos 3 valores en serie. Repetidamente obtengo cosas como:

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

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

He incluido guiones para representar qué bits provienen de SysTick.

Ahora, solo para evitar dudas, he usado el código de starblue a continuación. También lo cambié para usar un valor de 32 bits para SysTickMajor, y saqué SysTick->VAL, por si acaso.

¡Parece que SysTick_Handler no se está adelantando a la otra interrupción!

Ahora revisé esto y tenía las prioridades de preferencia incorrectas anteriormente, pero ahora configuré y NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);luego configuré las interrupciones: SysTick prioridad de preferencia 0, y la otra interrupción es 7.

¿Hay algo más que deba hacer para que la preferencia funcione?


ACTUALIZACIÓN 2: creé un pequeño caso de prueba para este problema exacto (SysTick no se adelanta a otras interrupciones) y lo puse aquí:

Problemas de prioridad de interrupción (prioridad) de STM32


Entonces, ¿qué tiene de malo el código anterior? ¿Hay una mejor manera de obtener un buen temporizador de alta resolución? Parece algo obvio que la gente querría hacer, pero me cuesta encontrar ejemplos.

¿Quizás pruebes DMA? Aumente alguna variable en el desbordamiento del temporizador.
¿Tiene un ejemplo de incrementar una variable con DMA? Solo una nota: realmente debería haber una interrupción deshabilitada / habilitada alrededor de cada mención de SysTickMajor en getSystemTime; sin embargo, incluso esto no detiene la falla ocasional
Tu principal error es que corres while (getSystemTime() < t);: esta función tarda un tiempo, por lo que no podrás obtener el tiempo perfectamente. Trabaje en interrupciones o realice una operación atómica de cálculo de tiempo (la interrupción de SysTick modificaría alguna long longvariable mediante DMA u otro mecanismo, y la última línea puede ser comowhile(SystemTime - SystemTime0 < 1000000)
No es como si estuviera un poco apagado; ocasionalmente, el pulso podría estar entre 0 y la duración que se suponía que debía ser. Ahora he modificado la pregunta después de haber probado algunas otras cosas. Sin embargo, si tiene una solución que involucre DMA (¿no estoy seguro de cómo se puede hacer?), estaría muy interesado.

Respuestas (4)

Como ha descubierto, manejar el desbordamiento del contador con un controlador de interrupciones es complicado y propenso a la carrera. Incluso con su aumento de prioridad, obtendrá la respuesta incorrecta si las interrupciones están deshabilitadas mientras lee el contador. Hay una técnica mucho más simple que funciona si no está leyendo el reloj desde el contexto de interrupción (tenga en cuenta que aquí estoy usando el contador de ciclos DWT , que está integrado en el procesador, por lo que es rápido y simple):

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;
}

Si necesita llamarlo desde un contexto de interrupción (o con un sistema operativo preventivo):

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;
}

Es posible que primero deba habilitar el contador de ciclos:

DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;  // Set bit 0.
Genial, ¿entonces el mero acto de leer el contador lo borra? Supongo que potencialmente esto podría llamarse en el IRQ de desbordamiento del temporizador SysTick, lo que garantizaría que nunca llegara al punto de desbordamiento.
No, leer el contador no lo borra. Este código simplemente actualiza un contador más grande de 64 bits usando el valor actual del contador más pequeño de 32 bits. En principio, podría hacer lo mismo con el registro de SysTick, pero tendría que realizar su actualización con más frecuencia.
Este método general parece bueno si crea un temporizador personalizado para él. Sin embargo, el contador DWT->CYCCNT solo aumenta cuando se conecta la sonda de depuración. Si desea usarlo con la sonda de depuración desconectada, deberá habilitarlo por separado. Sin embargo, hacerlo puede permitir que un atacante acceda a todos los registros de depuración, por lo que parece desaconsejable confiar en DWT->CYCCNT.
Evan afirma: "el contador DWT->CYCCNT solo aumenta cuando se conecta la sonda de depuración". Al menos en mi plataforma (STM32F407), el contador CYCCNT parece incrementarse independientemente de si se adjunta una sonda de depuración. Estoy consultando el Manual de referencia de la arquitectura ARMv7-M (DDI0403E_d) y no veo ninguna mención sobre la necesidad de una sonda de depuración. Veo que el contador se detiene cuando se detiene el procesador, lo que suele ser deseable: "CYCCNT no aumenta cuando el procesador se detiene en estado de depuración".
Ah, debe habilitar CYCCNT configurando el bit bajo de DWT_CTRL. Se agregó una descripción de esto a la respuesta.

Su código tiene dos problemas:

  • No sabe si la interrupción que aumentó SysTickMajorocurrió antes o después de leer el valor del temporizador en time.

  • Las operaciones en valores de 64 bits no son atómicas en un sistema de 32 bits. Entonces, el valor de SysTickMajorpodría actualizarse por la interrupción entre la lectura de las dos palabras de 32 bits.

Yo lo haría de la siguiente manera:

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

minor2 = SysTick->VAL;
major2 = SysTickMajor;

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

Aquí se supone que si SysTickMajorse cambia durante el primer bloque de tareas, no volverá a cambiar en el segundo bloque. (Esta suposición podría ser incorrecta si este código se interrumpe durante esta rutina y no se puede ejecutar durante mucho tiempo).

Consulte también esta pregunta sobre Stackoverflow .

Gracias, ahora comencé a usar su código (vea la pregunta modificada), ¡pero todavía tengo el problema! Incluso cambié los valores de 64 bits a 32 bits para asegurarme de que eso no fuera un problema. Ahora parece que no se llama a la interrupción de SysTick cuando SysTick se desborda, sino que hay un problema de sincronización.
Prefiero leer bajo/alto/bajo y repetir si es necesario. Entre otras cosas, ese enfoque funcionará correctamente incluso si la "propagación de acarreo lógico" es lenta. Supongamos, por ejemplo, que cuando en el momento en que el procesador lee los datos SysTick->VALya ha comenzado a ejecutar la instrucción para leer SysTickMajoren major1. El minor1valor podría reflejar un temporizador que ha terminado, aunque major1no lo haría. Usando bajo/alto/bajo, estas cosas no sucederán a menos que la diferencia de tiempo relativa entre las acciones de palabras altas y bajas exceda el tiempo entre las dos lecturas de palabras bajas.

Acabo de encontrar la respuesta en un póster muy útil en el foro STM32:

Lo siguiente no es correcto. SysTick es un 'Controlador del sistema' y, como tal, la prioridad no se establece de esta manera en absoluto:

  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);

En realidad está configurado con:

  NVIC_SetPriority(SysTick_IRQn, 0);

¡Llamar a ese código resuelve el problema!

Entonces, mi versión de getSystemTime podría haber tenido un problema, y ​​el método de starblue es mejor que el mío; sin embargo, la causa real de los problemas fue la anterior.

Solo pensé en agregar otra posible mejora a getSystemTime que también probé:

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

Esto anulará efectivamente el problema en el que podría (de alguna manera) terminar con el SysTick implementado dos veces en una sucesión muy rápida.

Las respuestas anteriores son geniales. Esta es la que más me gusta:

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

Puede optimizarlo un poco más, ya que sabe que si tiene un rollover de SysTickMajor, no obtendrá otro rollover "en mucho tiempo". Deberías estar seguro haciendo esto:

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

Lo anterior asume, por supuesto, que SysTickMajor y VAL se declaran como volátiles. Hace varios años, en un trabajo anterior, descubrí que, lamentablemente, lo anterior aún no es perfecto en todos los procesadores. Estaba usando un STM32F407, que tiene un caché y está canalizado. A veces, la búsqueda del segundo SysTickMajor llegaba antes que el VAL aunque se emitieran en orden. Introduzca la barrera de la memoria (instrucción dmb):

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

En mi caso, estaba usando un TIM2 de 32 bits y un ISR de rollover de TIM2 que incrementa los 32 bits superiores de mi contador de ciclos de 64 bits, pero creo que debería actuar igual que lo que está haciendo con SysTick.