AVR GCC: ¿Cómo puedo mejorar la optimización del código?

2

Intenté compilar el siguiente código C:

period = TCNT0L;
period |= ((unsigned int)TCNT0H<<8);

El código del ensamblador que estoy obteniendo es el siguiente:

    period = TCNT0L;
  d2:   22 b7           in  r18, 0x32   ; 50
  d4:   30 e0           ldi r19, 0x00   ; 0
  d6:   30 93 87 00     sts 0x0087, r19
  da:   20 93 86 00     sts 0x0086, r18
    period |= ((unsigned int)TCNT0H<<8);
  de:   44 b3           in  r20, 0x14   ; 20
  e0:   94 2f           mov r25, r20
  e2:   80 e0           ldi r24, 0x00   ; 0
  e4:   82 2b           or  r24, r18
  e6:   93 2b           or  r25, r19
  e8:   90 93 87 00     sts 0x0087, r25
  ec:   80 93 86 00     sts 0x0086, r24

¡Así que en lugar de 4 instrucciones, obtiene 11!

Intenté elegir las opciones de optimización O1, O2, O3 y Os. El resultado es el mismo (excepto que la opción O3 optimizó para nada este código).

Podría escribir el código fuente de la siguiente manera:

period = TCNT0L | ((unsigned int)TCNT0H<<8);

Obtendré un código más pequeño, pero aún no óptimo:

  de:   22 b7           in  r18, 0x32   ; 50
  e0:   34 b3           in  r19, 0x14   ; 20
  e2:   93 2f           mov r25, r19
  e4:   80 e0           ldi r24, 0x00   ; 0
  e6:   82 2b           or  r24, r18
  e8:   90 93 87 00     sts 0x0087, r25
  ec:   80 93 86 00     sts 0x0086, r24

Sin embargo, no tendré una garantía de que primero se accederá al byte más bajo (este es un requisito esencial para mantener la lectura correcta de 16 bits). Y aún así, el código tiene muchas instrucciones adicionales innecesarias.

¿Puedo cambiar las opciones del compilador y / o cambiar el código fuente para mejorarlo? Evitaría ir al ensamblador.

UPDATE1:

Probé el código que sugirió @caveman:

((unsigned char*)(&period))[0] = TCNT0L;
((unsigned char*)(&period))[1] = TCNT0H;

Pero el resultado tampoco es muy bueno:

    ((unsigned char*)(&period))[0] = TCNT0L;
  dc:   82 b7           in  r24, 0x32   ; 50
  de:   e6 e8           ldi r30, 0x86   ; 134
  e0:   f0 e0           ldi r31, 0x00   ; 0
  e2:   80 83           st  Z, r24
    ((unsigned char*)(&period))[1] = TCNT0H;
  e4:   84 b3           in  r24, 0x14   ; 20
  e6:   81 83           std Z+1, r24    ; 0x01
    
pregunta Roman Matveev

4 respuestas

3

Un método es usar cargas directas a las mitades del período. Si bien esto parece complicado en C, generalmente generará un ensamblaje muy ajustado, es decir, 2 cargas y 2 tiendas.

((uint8_t*)(&period))[0] = TCNT0L;
((uint8_t*)(&period))[1] = TCNT0H;

En ocasiones, el uso de la matriz matemática puede causar problemas, por lo que puedes intentar esto:

*((uint8_t*)(&period)) = TCNT0L;
*((uint8_t*)(&period) + 1) = TCNT0H;

Esto realmente produce código óptimo. Mira cómo se usan 12 bytes.

  ((unsigned char*)(&period))[0] = TCNT0L;
  dc:   82 b7           in  r24, 0x32   ; 50
  de:   e6 e8           ldi r30, 0x86   ; 134
  e0:   f0 e0           ldi r31, 0x00   ; 0
  e2:   80 83           st  Z, r24
    ((unsigned char*)(&period))[1] = TCNT0H;
  e4:   84 b3           in  r24, 0x14   ; 20
  e6:   81 83           std Z+1, r24    ; 0x01

Si hicieras esto con el ensamblaje, probablemente parecería mejor hacerlo así. También es de 12 bytes, por lo que son equivalentes.

  dc:   82 b7           in  r24, 0x32   ; 50
  de:   80 93 86 00     sts 0x0086, r24
  e2:   84 b3           in  r24, 0x14   ; 20
  e4:   80 93 87 00     sts 0x0087, r24

Por supuesto, cuando digo "equivalente", me refiero al tamaño del código. Si el tiempo es más importante, entonces hay que mirar los ciclos. En este caso, parece que la versión de ensamblaje es de 6 ciclos y la versión del compilador es de 8 ciclos.

    
respondido por el caveman
0

En mi avr-gcc 5.4.0 simple period = TCNT1; para attiny841 parece emitir el código de esta manera:

    in  r24,0x2c
    in  r25,0x2d
    sts 0x0110,r25
    sts 0x010f,r24

Parece que el compilador ya conoce la forma en que se debe acceder a los registros de 16 bits y, por lo tanto, el código como el de arriba es seguro.

La rama Avr del gcc generalmente no es muy buena incluso en optimizaciones simples como los ejemplos de la pregunta, pero de todos modos, actualizar la versión de avr-gcc a menudo ayuda.

Otra preocupación es que los gccs posteriores y avr-libcs podrían admitir el acceso a TCNT0 como un registro único de 16 bits, lo que parece faltar en el gcc utilizado en la pregunta.

    
respondido por el lvd
0

Si está dispuesto a desperdiciar un pin, puede obtener una captura de 1 instrucción / 2 ciclos del TCNT cuando se llama al ISR utilizando la Unidad de captura de salida.

Configuración

  1. Establezca el bit en el DDR para el pin ICP para convertirlo en una salida.
  2. Configure ACIC para que use el pin de entrada para el disparo de ICU. Deje los otros bits de la UCI a los valores predeterminados (sin filtro de ruido, disparador en el flanco descendente)

Para cada captura

En primer plano

  1. Borre el bit ICF escribiendo un 1 en él.
  2. Establezca el bit PORT para el pin ICP para que salga en ALTO.
  3. Realice una encuesta en el bit ICF hasta que se convierta en 1 .
  4. Lea el valor TNCT capturado fuera del registro ICR .
  5. Enjuague. Repetir.

En ISR

  1. Establezca el bit PORT para el pin ICP usando la instrucción SBI.

    
respondido por el bigjosh
0

¡Si quieres ir duro con el ahorro de ciclo, podrías obtener este TCNT en un solo ciclo en el ISR!

Puede aprovechar el hecho de que el byte alto del registro TCNT se almacena en búfer cada vez que se lee el byte bajo.

Entonces,si preasignado un registro (por ejemplo, r16) para esta tarea ...

register unsigned char tcnt_low_byte asm("r16");

... luego llené este registro con el byte bajo del TCNT dentro del ISR como este ...

R16 = TCNTL;

... que debería compilar hasta el ciclo 1 ...

IN R16,TCNTL

... luego podrías leer el valor completo de TCNT int instantáneo en primer plano como este ...

period = (TCNTH << 8)| R16;

Solo asegúrese de leer el TCNTH antes de acceder a cualquier otro registro de temporizador de 16 bits, ya que todos ellos comparten ese registro de temperatura temporal.

El trabajo total realizado en el ISR es solo un in R16, TCNTL que es 1 ciclo.

El OP no indicó cómo indicaría al proceso de primer plano que ocurrió un ISR, pero si estaba cargando period con 0 y luego buscaba un cambio, entonces se necesita algo de trabajo extra ...

  1. precargue 0 en el registro TEMP de 16 bits (puede hacer esto escribiendo un 0 en cualquier registro de 16 bits).
  2. precargar 0 en R16 .

Luego puedes hacer una encuesta para ver si el ISR ocurrió con ...

x=TCNTH
if (x || R16) {
    period=(x<<8 | R16)
    // Process new period capture here...
}
    
respondido por el bigjosh

Lea otras preguntas en las etiquetas