Cómo medir con precisión microsegundos con STM32

Cómo medir con precisión microsegundos con STM32

Recibí esta pregunta aparentemente simple de un lector de este blog: ¿cómo puedo retrasar un par de microsegundos en STM32? Es decir, ¿cómo medir microsegundos de manera precisa en STM32?

La respuesta es: hay varias formas de hacerlo, pero algunos métodos son más precisos y otros son más versátiles entre las diferentes MCU y la configuración del reloj.

Consideremos un miembro de la familia STM32F4: STM32F401RE, la MCU que equipa la placa STM32Nucleo-F401RE. Este micro es capaz de funcionar hasta 84Mhz usando un reloj RC interno. Esto significa que siempre que 1µs, el reloj cicla 84 veces. Por lo tanto, necesitamos una forma de contar 84 ciclos de reloj para afirmar que ha transcurrido 1µs (supongo que puede tolerar la precisión del 1% del reloj RC interno).

En algún momento es común encontrar código como este:

1234567891011void delay1US() {    #define CLOCK_CYCLES_PER_INSTRUCTION    X    #define CLOCK_FREQ                      Y  //IN MHZ (e.g. 16 for 16 MHZ)     volatile int cycleCount = CLOCK_FREQ / CLOCK_CYCLE_PER_INSTRUCTION;     while (cycleCount–);            // 1uS is elapsed 🙂    // Sure?    // :-/}

Pero, ¿cómo establecer cuántos ciclos de reloj se requieren para calcular un paso de la instrucción while (cycleCount–) ? Desafortunadamente, no es sencillo dar una respuesta. Supongamos que cycleCount es igual a 1. Haciendo algunas pruebas (explicaré más adelante cómo las he hecho),  con las optimizaciones del compilador deshabilitadas (opción -O0 a GCC), podemos ver que en este caso toda la instrucción C requiere 24 ciclos para ejecutar. ¿Cómo es posible que? Debe averiguar que nuestra instrucción C se desenrolla en varias instrucciones de ensamblaje, como podemos ver si desmontamos el archivo binario del firmware:

123456789… while(counter–); 800183e:       f89d 3003       ldrb.w  r3, [sp, #3] 8001842:       b2db            uxtb    r3, r3 8001844:       1e5a            subs    r2, r3, #1 8001846:       b2d2            uxtb    r2, r2 8001848:       f88d 2003       strb.w  r2, [sp, #3] 800184c:       2b00            cmp     r3, #0 800184e:       d1f6            bne.n   800183e <main+0x3e>
LEER MAS  Electronics Weekly - Analog Devices A / D Converter, módulos Samsung IoT seguros y más

Además, otra fuente de latencia está relacionada con la recuperación desde el flash MCU interno. Entonces esa instrucción tiene un “costo básico” de 24 ciclos. ¿Cuántos ciclos se requieren si cycleCountes igual a 2? En este caso, la MCU requiere 33 ciclos, es decir, 9 ciclos adicionales. Esto significa que si queremos girar durante 84 ciclos, CycleCount debe ser igual a (84-24) / 9, que es aproximadamente 7. Por lo tanto, podemos escribir nuestra función de retardo de una manera más general:

1234void delayUS(uint32_t us) { volatile uint32_t counter = 7*us; while(counter–);}

Probando esta función con este código:

123456while(1) { delayUS(1); GPIOA->ODR = 0x0; delayUS(1); GPIOA->ODR = 0x20;}

Podemos verificar usando un osciloscopio decente, conectado a un GPIO configurado como  GPIO_SPEED_HIGH ,  que esto es lo que esperamos:





¿Es esta manera de retrasar 1µs siempre consistente? 
La respuesta es no. 
En primer lugar, funciona bien solo cuando esta MCU específica (STM32F401RE) funciona a máxima velocidad (84Mhz). 
Si decide utilizar una velocidad de reloj diferente, debe reorganizarla haciendo pruebas. 
En segundo lugar, está sujeto a optimizaciones del compilador.
Vamos ahora a habilitar la optimización GCC para “tamaño” (-Os). 
¿Qué resultados obtenemos? 
En este caso, tenemos que la función 
delayUS () solo cuesta 72 ciclos de CPU, es decir, ~ 850ns. 
El alcance confirma esto:
 

¿Y qué pasa si habilitamos la optimización máxima para la velocidad (-O3)? 
En este caso, solo tenemos 64 ciclos de CPU, es decir, nuestro 
retrasoUS () dura solo ~ 750 ns, como lo confirma nuestro alcance:

Sin embargo, este problema se puede abordar utilizando una directiva pragma específica de GCC:

#pragma GCC push_options
#pragma GCC optimize (“O0”)
void delayUS(uint32_t us) {
volatile uint32_t counter = 7*us;
while(counter–);
}
#pragma GCC pop_options
Dicho esto, el hecho es que si queremos usar una frecuencia de CPU más baja o queremos trasladar nuestro código a una MCU diferente, debemos volver a realizar las pruebas.


Entonces, ¿cómo podemos obtener un retraso preciso de 1µs sin hacer pruebas si cambiamos la configuración del hardware? 
La respuesta es: necesitamos un temporizador de hardware. 
Y tenemos varias opciones.
El primero viene de las pruebas anteriores. 
¿Cómo he medido los ciclos de CPU? 
Los procesadores Cortex-M pueden tener una unidad de depuración opcional que proporciona puntos de vigilancia, rastreo de datos y perfiles del sistema para el procesador. 
Un registro de esta unidad es 
CYCCNT , que cuenta el número de ciclos realizados por la CPU. 
Por lo tanto, podemos usar esta unidad especial disponible en STM32 para contar el número de ciclos realizados por la MCU durante la ejecución de la instrucción.


¿Qué tan precisa es esta función? 
Si está interesado en la mejor resolución en 1µs, esta función no es la mejor, como lo muestra el alcance.



El mejor rendimiento se logra cuando se establece el nivel más alto de optimización del compilador. 
Como puede ver, para un retraso deseado de 1 µs, la función proporciona un retraso de aproximadamente 1.22 µs (22% más lento). 
Sin embargo, si queremos girar durante 10 µs, obtenemos un retraso real de 10.5µs (5% más lento), lo que está más cerca de lo que queremos.

A partir de un retardo de 100µs el error es completamente despreciable.
¿Por qué esta función no es tan precisa? 
Para entender por qué esta función es menos precisa que la otra, debe darse cuenta de que estamos utilizando una serie de instrucciones para verificar cuántos ciclos han expirado desde que se inició la función (la condición while). 
Estas instrucciones cuestan ciclos de CPU para actualizar los registros de CPU internos con el contenido de 
CYCCNT  y para realizar comparaciones y bifurcaciones. 
Sin embargo, la ventaja de esta función es que detecta automáticamente la velocidad de la CPU, y funciona de manera inmediata, especialmente si estamos trabajando en procesadores más rápidos.
Se podrían lograr otras soluciones utilizando temporizadores de hardware, como los temporizadores TIMx y SYSTICK. 
Sin embargo, he obtenido resultados similares a la función 
delayUS_DWT () .

Deja un comentario

Tu dirección de correo electrónico no será publicada.

CUANTAS HORAS FALTA PARA MAÑANA...