AVR: Cómo optimizar el ISR de ciclo contado a código portátil, usando asm en línea

Estoy tratando de optimizar mis interrupciones RX y TX para cumplir con el tiempo máximo de ejecución de 25 ciclos mientras las interrupciones están deshabilitadas.

Hasta ahora, he descubierto que el código está lo suficientemente optimizado, pero empujar y abrir una serie de registros entre la carga y la descarga __SREG__excede el límite de tiempo.

 272:   80 91 24 01     lds r24, 0x0124
 276:   8f 5f           subi    r24, 0xFF   ; 255
 278:   8f 71           andi    r24, 0x1F   ; 31
 27a:   90 91 c6 00     lds r25, 0x00C6
 27e:   20 91 25 01     lds r18, 0x0125
 282:   28 17           cp  r18, r24
 284:   39 f0           breq    .+14        ; 0x294 <__vector_18+0x30>
 286:   e8 2f           mov r30, r24
 288:   f0 e0           ldi r31, 0x00   ; 0
 28a:   ea 5d           subi    r30, 0xDA   ; 218
 28c:   fe 4f           sbci    r31, 0xFE   ; 254
 28e:   90 83           st  Z, r25
 290:   80 93 24 01     sts 0x0124, r24

La única forma de poder colocarlo __SREG__en la ubicación más segura (incluya tantos empujones como sea posible en el área inconsciente) fue asm en línea.

Aquí está mi código actual:

ISR(RX0_INTERRUPT, ISR_NAKED)
{
    //push
    asm volatile("push r31" ::); // table pointer
    asm volatile("push r30" ::); // table pointer
    asm volatile("push r25" ::); // received character
    asm volatile("push r18" ::); // once compared to r24 -> rx0_first_byte

    asm volatile("push r24" ::); // most stuff is executed in r24
    asm volatile("in r24,__SREG__" ::); // - 
    asm volatile("push r24" ::); // but one byte more on stack

    register uint8_t tmp_rx_last_byte = (rx0_last_byte + 1) & RX0_BUFFER_MASK;
    register uint8_t tmp = UDR0_REGISTER;

    if(rx0_first_byte != tmp_rx_last_byte)
    {
        rx0_buffer[tmp_rx_last_byte] = tmp;
        rx0_last_byte = tmp_rx_last_byte;
    }

    //pop
    asm volatile("pop r24" ::);
    asm volatile("out __SREG__,r24" ::);
    asm volatile("pop r24" ::);

    asm volatile("pop r18" ::);
    asm volatile("pop r25" ::);
    asm volatile("pop r30" ::);
    asm volatile("pop r31" ::);

    reti();
}

Como puede ver, hay un registro codificado que empujó mi compilador, por cierto, está funcionando, pero no estoy seguro de qué tan portátil es.

El único registro que puedo obtener con el especificador "=r" es r24y está sucediendo incluso para mask y rx0_first_byte.

Entonces, ¿cómo puedo decirle al compilador que empuje/salte estos 5 registros, incluso si se colocarán en otro lugar?

¿ Cuál es la posibilidad de que el compilador use r19y r26en lugar de r18y r25?

No quiero reescribir todo el ISR en ensamblador.

EDITAR: gracias por todas las sugerencias, finalmente reescribí ISR en asm

    ISR(RX0_INTERRUPT, ISR_NAKED)
{
    asm volatile("\n\t"                      /* 5 ISR entry */
    "push  r31 \n\t"                         /* 2 */
    "push  r30 \n\t"                         /* 2 */
    "push  r25 \n\t"                         /* 2 */
    "push  r24 \n\t"                         /* 2 */
    "push  r18 \n\t"                         /* 2 */
    "in    r18, __SREG__ \n\t"               /* 1 */
    "push  r18 \n\t"                         /* 2 */

    /* read byte from UDR register */
    "lds   r25, %M[uart_data] \n\t"          /* 2 */

    /* load globals */
    "lds   r24, (rx0_last_byte) \n\t"        /* 2 */
    "lds   r18, (rx0_first_byte) \n\t"       /* 2 */

    /* add 1 & mask */
    "subi  r24, 0xFF \n\t" //???                  /* 1 */
    "andi  r24, %M[mask] \n\t"            /* 1 */

    /* if head == tail */
    "cp    r18, r24 \n\t"                    /* 1 */
    "breq  L_%= \n\t"                        /* 1/2 */

    "mov   r30, r24 \n\t"                    /* 1 */
    "ldi   r31, 0x00 \n\t"                   /* 1 */
    "subi  r30, lo8(-(rx0_buffer))\n\t"      /* 1 */
    "sbci  r31, hi8(-(rx0_buffer))\n\t"      /* 1 */
    "st    Z, r25 \n\t"                      /* 2 */
    "sts   (rx0_last_byte), r24 \n\t"        /* 2 */

"L_%=:\t"
    "pop   r18 \n\t"                         /* 2 */
    "out   __SREG__ , r18 \n\t"              /* 1 */
    "pop   r18 \n\t"                         /* 2 */
    "pop   r24 \n\t"                         /* 2 */
    "pop   r25 \n\t"                         /* 2 */
    "pop   r30 \n\t"                         /* 2 */
    "pop   r31 \n\t"                         /* 2 */
    "reti \n\t"                              /* 5 ISR return */

    : /* output operands */

    : /* input operands */
    [uart_data] "M"    (_SFR_MEM_ADDR(UDR0_REGISTER)),
    [mask]      "M"    (RX0_BUFFER_MASK)

    /* no clobbers */
    );

}

ACTUALIZAR:

Después de algunas pruebas, descubrí que las interrupciones se deshabilitan antes de ingresar al controlador ISR, no después de la descarga, __SREG__como se me sugirió anteriormente.

La única forma es globalizar los registros como sugiere ndim, o usar el siguiente código:

ISR(RX0_INTERRUPT, ISR_NAKED)
    {
        asm volatile("\n\t"                      /* 4 ISR entry */

        "push  r0 \n\t"                          /* 2 */
        "in    r0, __SREG__ \n\t"                /* 1 */

        "push  r31 \n\t"                         /* 2 */
        "push  r30 \n\t"                         /* 2 */
        "push  r25 \n\t"                         /* 2 */
        "push  r24 \n\t"                         /* 2 */
        "push  r18 \n\t"                         /* 2 */

        /* read byte from UDR register */
        "lds   r25, %M[uart_data] \n\t"          /* 2 */

#ifdef USART_UNSAFE_RX_INTERRUPT // enable interrupt after satisfying UDR register
        "sei \n\t"                               /* 1 */
#endif
        /* load globals */
        "lds   r24, (rx0_last_byte) \n\t"        /* 2 */
        "lds   r18, (rx0_first_byte) \n\t"       /* 2 */

        /* tmp_rx_last_byte = (rx0_last_byte + 1) & RX0_BUFFER_MASK */
        "subi  r24, 0xFF \n\t"                   /* 1 */
        "andi  r24, %M[mask] \n\t"               /* 1 */

        /* if(rx0_first_byte != tmp_rx_last_byte) */
        "cp    r18, r24 \n\t"                    /* 1 */
        "breq  .+14 \n\t"                        /* 1/2 */

        /* rx0_buffer[tmp_rx_last_byte] = tmp */
        "mov   r30, r24 \n\t"                    /* 1 */
        "ldi   r31, 0x00 \n\t"                   /* 1 */
        "subi  r30, lo8(-(rx0_buffer))\n\t"      /* 1 */
        "sbci  r31, hi8(-(rx0_buffer))\n\t"      /* 1 */
        "st    Z, r25 \n\t"                      /* 2 */

        /* rx0_last_byte = tmp_rx_last_byte */
        "sts   (rx0_last_byte), r24 \n\t"        /* 2 */

#ifdef USART_UNSAFE_RX_INTERRUPT
        "cli \n\t"                               /* 1 */
#endif

        "pop   r18 \n\t"                         /* 2 */
        "pop   r24 \n\t"                         /* 2 */
        "pop   r25 \n\t"                         /* 2 */
        "pop   r30 \n\t"                         /* 2 */
        "pop   r31 \n\t"                         /* 2 */

        "out   __SREG__ , r0 \n\t"               /* 1 */
        "pop   r0 \n\t"                          /* 2 */

        "reti \n\t"                              /* 4 ISR return */

        : /* output operands */

        : /* input operands */
        [uart_data] "M"    (_SFR_MEM_ADDR(UDR0_REGISTER)),
        [mask]      "M"    (RX0_BUFFER_MASK)

        /* no clobbers */
        );

    }
¿Se adaptaría a su programa general simplemente establecer una bandera en el ISR y realizar el procesamiento fuera del ISR?
Esta interrupción solo lee el byte de uart RX y lo almacena en el búfer de anillo. Importante para mí es no desbordar el búfer.
¿Se da cuenta de que la próxima vez que compile, su compilador podría generar un código diferente, usando registros diferentes?
No estoy trabajando en AVR, pero como regla general: nunca confíe en ubicaciones aleatorias del compilador, un pequeño cambio de versión podría alterar el comportamiento, un pequeño cambio de código también podría hacerlo. También puede leer el manual del compilador con mucho cuidado, hay verdaderas gemas allí para cosas críticas (pragmas, intrínsecos)
Supongo que ggc pondría variables en diferentes registros, pero después de horas de buscar en Google todavía no sé cómo hacer push correctamente.
No creo que deba hacer las inserciones manualmente para una pieza de código generado por el compilador. Debe saber qué registros se utilizan, que en general no puede saber. A menos, por supuesto, que presione todo lo que la convención de llamadas requiere que guarde.
¿Sabe que el AVR UART tiene un búfer de dos bytes? Si maneja hasta dos bytes en su rutina de interrupción, ¿podría relajar su limitación de 25 ciclos?
Es probable que el búfer de dos bytes no deseche el byte entrante durante las transferencias atómicas USB, no para el código principal que puede atascarse en un bucle largo.
Si solo tiene 25 ciclos de duración, ¿qué hay de malo en escribir todo en código ensamblador? Entonces no necesita seguir las convenciones del compilador en absoluto, simplemente guarde los registros que usa dentro del ISR y restáurelos al final. Probablemente podrá lograr una optimización general aún mayor.
Deshágase de los push/pops y las registerpalabras clave y vea si logra producir un código más eficiente.
El ISR predeterminado (sin ISR_NAKED) no es adecuado debido al código basura generado que no se puede cambiar de ninguna manera. Mi código en sí tiene una duración de 18 ciclos, por lo que la desactivación y habilitación de interrupciones deben realizarse cerca del código, no después de la primera pulsación.
@ jnk0le La mejor manera es escribir un código asm completamente separado que maneje las interrupciones y mezclarlo con su código C. Esa es la única forma en que estará seguro de que el compilador no lo estropeará. Sugiero encarecidamente este documento de Atmel como lectura adicional: atmel.com/Images/doc42055.pdf

Respuestas (2)

Ahorrando algunos empujones/pops usando variables de registro global, poniendo todas las instrucciones en una asm()declaración, llegaría a algo como

#define RB_WIDTH 5
#define RB_SIZE (1<<(RB_WIDTH))
#define RB_MASK ((RB_SIZE)-1)

register uint8_t rb_head      asm("r13");
register uint8_t rb_tail      asm("r14");
register uint8_t rb_sreg_save asm("r15");

volatile uint8_t rb_buf[RB_SIZE];

ISR(USART0_RX_vect, ISR_NAKED)                 /* CLOCK CYCLES */
{
  asm("\n\t"                                   /* 5 ISR entry */
      "push  r24\n\t"                          /* 2 */
      "push  r25\n\t"                          /* 2 */
      "push  r30\n\t"                          /* 2 */
      "push  r31\n\t"                          /* 2 */
      "in    %r[sreg_save], __SREG__\n\t"      /* 1 */
      "\n\t"

      /* read byte from UART */
      "lds   r25, %M[uart_data]\n\t"           /* 2 */

      /* next_tail := (cur_tail + 1) & MASK; */
      "ldi   r24, 1\n\t"                       /* 1 */
      "add   r24, %r[tail]\n\t"                /* 1 */
      "andi  r24, %a[mask]\n\t"                /* 1 */

      /* if next_tail == cur_head */
      "cp    r24, %r[head]\n\t"                /* 1 */
      "breq  L_%=\n\t"                         /* 1/2 */

      /* rb_buf[next_tail] := byte */
      "mov   r30, r24\n\t"                     /* 1 */
      "ldi   r31, 0\n\t"                       /* 1 */
      "subi  r30, lo8(-(rb_buf))\n\t"          /* 1 */
      "sbci  r31, hi8(-(rb_buf))\n\t"          /* 1 */
      "st    Z, r25\n\t"                       /* 2 */

      /* rb_tail := next_tail */
      "mov   %r[tail], r24\n\t"                /* 1 */

      "\n"
"L_%=:\t"
      "out   __SREG__, %r[sreg_save]\n\t"      /* 1 */
      "pop   r31\n\t"                          /* 2 */
      "pop   r30\n\t"                          /* 2 */
      "pop   r25\n\t"                          /* 2 */
      "pop   r24\n\t"                          /* 2 */
      "reti\n\t"                               /* 5 ISR return */
      : /* output operands */
        [tail]      "+r"   (rb_tail)    /* both input+output */
      : /* input operands */
        [uart_data] "M"    (_SFR_MEM_ADDR(UDR0)),
        [mask]      "M"    (RB_MASK),
        [head]      "r"    (rb_head),
        [sreg_save] "r"    (rb_sreg_save)
        /* no clobbers */
      );
}

Esto todavía termina tomando 42 ciclos.

Reorganizar un poco el código del búfer de anillo podría reducir aún más la cantidad de código en el ISR que se escribe en el búfer de anillo (a costa de hacer que la función de lectura del búfer sea más compleja).

He puesto un ejemplo completo con un sistema de compilación y estructuras de soporte en https://github.com/ndim/avr-uart-example/

Parece que entendí totalmente mal todas las guías de asm en línea, especialmente los operandos. Intentaré escribirlo sin alinear las variables globales en los registros;)
Parece que gcc trata las variables no inicializadas como un archivo r24.
Entonces, como puedo ver, la mejor manera es simplemente escribir todo el código en ensamblador.

Algunas cosas útiles que encontré al hacer un AVR ISR corto y rápido para https://github.com/ndim/freemcan/tree/master/firmware fueron:

  • Haga que su sistema de compilación genere volcados en lenguaje ensamblador de su código generado durante cada reconstrucción y observe los cambios en el código generado cada vez que cambie la fuente. Realmente ayuda a ver lo que realmente sucede. (Yo uso avr-objdump -h -S firmware.elf > firmware.lss.)

  • Si necesita un ISR realmente rápido, puede guardar algunos ciclos para empujar/quitar registros indicando avr-gccque compile todo el código C sin usar algunos registros (p. ej -ffixed-r13.), y luego use esos registros como variables globales en el ISR sin empujar/quitar. Esto también le ahorra los ciclos adicionales para el acceso a la memoria. Los punteros de cabeza y cola para el búfer de anillo son candidatos en su caso.

  • No puedo recordar de antemano si el avr-gccISR generado siempre empuja/abre todos los registros, o solo los que realmente usa. Si empuja/hace estallar más de lo absolutamente necesario, es posible que tenga que escribir el ISR en ensamblaje después de todo.

  • Luego, aún puede tomar las instrucciones en lenguaje ensamblador generadas, colocarlas en un .Sarchivo fuente ensamblador y optimizarlo a mano aún más.

En mi caso de uso, sin embargo, resultó que el ISR no era tan crítico después de todo.

Por cierto, usaría los parámetros asm en línea para permitir que gcc seleccione los registros en lugar de codificarlos. Consulte http://www.nongnu.org/avr-libc/user-manual/inline_asm.html

GCC agrega código para presionar r0y r1(+ ponerlo a cero) y deshabilita las interrupciones después del primer impulso, mientras tanto, quiero hacer impulsos primero y luego deshabilitar las interrupciones para que quepan en 25 ciclos mientras las interrupciones están deshabilitadas. Pensé en volver a escribir ISR en ensamblador, pero necesito muchos conceptos básicos de avr-asm. El asm en línea es incluso peor que el normal: cómo llegar a las variables globales y cómo pasar los registros usados ​​por los parámetros del asm en línea (puedo obtenerlo r24y este manual no es lo suficientemente útil)
¿Los ejemplos en github.com/ndim/freemcan/blob/master/firmware/table-element.h ayudan a llegar a las variables desde asm en línea?
Bueno, si desea tener un preámbulo ISR y un código de limpieza diferentes del que genera avr-gcc, debe cambiar avr-gcc o (mucho más fácil), escribir ensamblador usted mismo. Utilice el código ensamblador generado por avr-gcc como base y continúe desde allí.