Ayuda para comprender el tiempo de ejecución de AVR

Estoy trabajando con un microcontrolador Atmel ATMEGA32U4 - hoja de datos aquí con un cristal de 16 MHz para el reloj del sistema.

Según tengo entendido, este chip tiene un fusible 'Dividir reloj por 8' programado de fábrica, lo que hace que el reloj de mi sistema sea de 2 MHz. (Página 348 de la hoja de datos. CKDIV8 (bit 7) el valor predeterminado es 0, programado).

Me gustaría confirmar la velocidad del reloj de mi sistema, así que escribí un código para generar un pin bajo, retrasar un ciclo de reloj y luego poner el pin alto nuevamente. Medir el tiempo que el pin está bajo debe ser igual a un ciclo de reloj.

Aquí está el código que usé para lograr esto:

//Set PORT E as output
DDRE = 0xFF;

asm volatile("nop\n\t"::); 
PORTE |= 1<<2;  

asm volatile("nop\n\t"::); 
PORTE &=~ (1<<2);   

asm volatile("nop\n\t"::); 
PORTE |= 1<<2;  

asm volatile("nop\n\t"::); 
PORTE &=~ (1<<2);

Un 'nop' es igual a un ciclo de reloj, según el manual del conjunto de instrucciones del AVR , página 108.

Con base en esta información, asumiría una instrucción 'nop' para tomar 500 nanosegundos. ¿Es correcta esta suposición? (16 MHz/8 = 2 MHz. 1 2 METRO H z = 500ns)

Aquí hay un diagrama de alcance de mis hallazgos:ingrese la descripción de la imagen aquí

Parecería que el tiempo de ejecución de un 'nop' es de solo 200 ns (5 MHz). Esperaría que hubiera una sobrecarga adicional usando C para establecer el puerto alto/bajo, por lo que 500 ns es en realidad el tiempo mínimo que esperaría que el pin estuviera bajo. . Pero como puede ver en mis cursores de medición 'a' y 'b', ni siquiera está cerca de 500 ns.

¿Alguien puede explicar qué está pasando?
¿Hay un error en mi método?
¿Me estoy perdiendo algo estúpido 'face palming-ly'? :p ¡
Gracias por cualquier ayuda!

¿Cómo estás compilando este código? Incluso con volatile, GCC aún moverá esas declaraciones de ensamblaje por gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html si tiene la optimización activada (cualquier otra cosa que no sea -O0).
Compilado con AVR GCC, optimizado para tamaño (-Os), que aparentemente es la configuración predeterminada. Veré qué hace desactivar las optimizaciones.
compilar y luego desensamblar su código en mi cabeza conduce a out DDRE, 0xFF; nop; sbi PORTB, 2; nop; cbi PORTB, 2; nop; sbi PORTB, 2; nop; cbi PORTB, 2;que, dado que todas estas son instrucciones de 1 ciclo, el tiempo entre los cambios de salida debe ser de 2 ciclos (uno para el nop, uno para sbi/ cbi). sin embargo, su compilador podría optimizar los nops o no ser lo suficientemente inteligente como para usarlo sbiycbi

Respuestas (3)

Este es un mal enfoque. Idealmente, mediría una gran cantidad de comandos NOP, digamos, un millón, en lugar de solo uno. Creo que es la primera tontería que puedo encontrar. No me sorprendería mucho si su número cambia justo después de implementar ese cambio.

Además, ¿puedes ver el desmontaje de tu código C? Ni siquiera intentaría juzgar el tiempo sin ver exactamente qué código ensamblador se generó a partir de mi código C. Intentaría trabajar hacia atrás desde el ensamblaje para calcular cuál debería ser el tiempo de ejecución (a través del manual del conjunto de instrucciones que encontró). De esa manera, puede ver si la medición del alcance se desvía un poco o mucho. Un poco podría significar que cometiste un error matemático. Muchas pueden significar que una de sus suposiciones es incorrecta.

¿Supongo que me gustaría un número como un millón para tener en cuenta la estabilidad del reloj (partes por millón)? Además, una buena sugerencia sobre mirar el desmontaje. No sé cómo hacerlo todavía, pero lo resolveré. Entonces puedo ver con certeza qué instrucciones se están ejecutando.
@dext0rb Sí, esa es una buena razón. Otra razón es que los comandos para alternar el DIO externo toman aproximadamente el mismo tiempo que un NOP, pero no tanto como un millón. Serán insignificantes.
Gracias a todos, desearía poder aceptar todas sus respuestas, ya que todas fueron muy buenas, pero esta me hizo pensar y profundizar en la asamblea. Seguí la sugerencia de @noah1989 de ir a 'bare metal' y me ayudó mucho. Hice 100 NOP seguidos y equivale a 50uS, lo que tiene sentido para mí.

Verifiqué varias configuraciones de reloj de un ATtiny45 hace un tiempo con el código C a continuación. Aunque no es perfecto con el bucle while, las rutinas de retardo vienen bastante bien optimizadas con las bibliotecas avr-gcc. Llamo al programa '1kHz.cpp'. Verifique con la hoja de datos qué pin exacto genera la onda de bloque.

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
  DDRB = 0x10;
  PORTB = 0x00;

  while (1)
  {
    PORTB = 0x00; _delay_us(500);  // pin low, then wait
    PORTB = 0x10; _delay_us(500);  // pin high, then wait
  }
}

El truco con el reloj es compilar el código con la configuración de reloj adecuada. En Linux se ve así, pero Windows debería ser razonablemente similar. Como puede ver, el ATtiny45 tiene un reloj predeterminado de fábrica de 1 MHz.

freq=8000000/8
baud=19200
src=1kHz.cpp
avr=attiny45
port=/dev/ttyUSB1

# Compile
avr-gcc -g -DF_CPU=$freq -Wall -Os -mmcu=$avr -c -o tmp.o $src
# Link
avr-gcc -g -DF_CPU=$freq -Wall -Os -mmcu=$avr -o tmp.elf tmp.o
# Convert to Intel .hex (required for my programmer)
avr-objcopy -j .text -j .data -O ihex tmp.elf tmp.hex
# Program the device
avrdude -p $avr -c stk500v1 -P $port -b $baud -v -U flash:w:tmp.hex

Por cierto, experimentar con la configuración del reloj puede bloquear tu dispositivo, así que ten cuidado con los fusibles que configuras o desactivas. El desbloqueo solo se puede hacer con un programador de alto voltaje (que se puede hacer con un Arduino de repuesto).

Si está haciendo un análisis de tiempo en el nivel de instrucciones individuales, ciertamente debe mirar el desensamblado de su programa, o escribirlo en ensamblador en primer lugar. Luego puede saber las duraciones exactas de la hoja de datos contando los ciclos para cada instrucción:

relojes_de_instrucciones

Sin embargo, si solo desea verificar que la frecuencia del reloj principal es correcta, hay una solución más fácil: simplemente genere una salida de baja frecuencia. (De 0,1 Hz a algunos kHz, dependiendo de lo que use para medirlo, un LED parpadeante y un cronómetro pueden ser suficientes). Puede usar uno de los temporizadores de 16 bits para eso, o simplemente usar un bucle con un tiempo ocupado largo. espera, por ej _delay_us(). El tiempo dedicado a los saltos del bucle será despreciable.