Problema extraño con ATTiny10 + avr-gcc: ¿el contador utilizado en ISR está dañado por variables globales?

Tengo un problema muy extraño de una posible corrupción de memoria cuando uso variables globales y una interrupción de desbordamiento del temporizador en ATTiny10 (usando avr-gcc 4.9.2). No puedo entenderlo, pero logré reducirlo a reproducirlo con un programa muy simple:

#include <avr/io.h>

/* Timer overflow counter */
volatile unsigned int ovrf = 0;

/* Some global variables used in main() */
/* (MOVING THESE INTO main() FIXES THE ISSUE) */
unsigned long foo;
unsigned int bar;

int main(void) {
  /* Fast PWM 8 Bit Mode */
  TCCR0A |= _BV(WGM00);
  TCCR0B |= _BV(WGM02);
  /* Enable Timer Overflow Interrupt */
  TIMSK0 |= _BV(TOIE0);
  /* /8 prescaler */
  TCCR0B |= _BV(CS01); // 
  /* PB0 as output */
  DDRB |= _BV(PB0);
  /* Enable interrupts */
  sei();

  for (;;) {
    /* Some random code that uses the global vars */
    /* (REMOVING THIS FIXES THE ISSUE) */
    if (foo > bar) {
      foo = 0;
    }
  }
}

ISR(TIM0_OVF_vect) {
  ovrf++;

  /* Toggle LED (about once per second) */
  if ((ovrf / 500) % 2 == 0) {
    PORTB &= ~(_BV(PB0));      
  } else {
    PORTB |= _BV(PB0);
  }
}

Todo lo que hace es lo siguiente:

  • Configura el temporizador y un ISR de desbordamiento del temporizador que incrementa un contador (variable global ovrf) y enciende y apaga un LED según el valor de este contador.
  • El bucle principal solo accede a otras dos variables globales (ni siquiera escribe en ninguna parte).

Espero que el LED parpadee periódicamente, lo que demuestra que la interrupción funciona y el contador se incrementa correctamente. Pero no se enciende, o cuando se modifica ligeramente el programa, por ejemplo, al agregar más código o main()más variables, parpadea de forma errática oa un ritmo muy rápido. A partir de esto, después de muchas pruebas, tratando de excluir cualquier otra explicación, asumo que el contador ( ovrf) de alguna manera se está corrompiendo desde el ciclo principal.

Encontré varios cambios que pueden hacer que el problema desaparezca:

  • moviendo las dos variables globales ( fooy bar) amain()
  • eliminando el código que accede a ellos
  • cambiar el tipo de fooaint
  • desactivar todas las optimizaciones con -O0(el valor predeterminado era -Os), pero esto hace que el código sea ~1,6 veces más grande.

Pero todavía no puedo ver ninguna explicación sobre la causa real. ¿Me estoy perdiendo por completo algo obvio...? Me he quedado sin ideas y no puedo pensar en otra cosa que no sea un error del compilador, pero eso es muy poco probable, dado que este ejemplo es muy simple.

ACTUALIZAR

Basado en la sugerencia de @MarkU, traté de jugar con varias configuraciones de optimización, tratando de encontrar la opción exacta que podría causar el problema:

  • También lo intenté -O1, pero tampoco ayuda.

  • ¡ Descubrí que eso -Os -fno-toplevel-reorder también soluciona el problema! -- Sin embargo, sospecho que esto podría ser solo un efecto accidental:

  • En mi programa original (muy similar al ejemplo simplificado anterior), donde encontré el problema, nada de lo anterior ayuda (ni siquiera -O0). Allí tengo una variable global más (a bool), y lo único que parece ayudar es eliminar una asignación inicial (por ejemplo, "bool ledOn = true;" --> "bool ledOn;").

Así que definitivamente tiene algo que ver con la forma en que se asignan las variables, pero no simplemente con su tamaño general. (No hay otras dependencias, ni llamadas a funciones, etc.)

ACTUALIZAR 2

Siguiendo el consejo de @Curd, también intenté reemplazar ovrf / 500con ovrf >> 9(más o menos lo mismo, no me importa el tiempo exacto aquí de todos modos). Esto redujo el código en 74 bytes (!), ¡y esto también soluciona el problema!

Eché un vistazo al código desensamblado para el ISR: este cambio reduce la cantidad de bytes pushedal principio de 13 a 7, ¡lo que podría explicar por qué ayuda!

(Esto ovrf / 500solo pretendía ser una prueba rápida y simple para verificar que el ISR estaba funcionando, ¡pero no me di cuenta de que en la implementación real no es tan simple en absoluto! En mi programa original no hay división, mantengo un valor aproximado de milisegundos. cuente simplemente multiplicando ovrfpor 2.)

También comparé el código desensamblado para -Os("malo") y -Os -fno-toplevel-reorder("bueno"), pero aparte de que el código se reordenó al principio, tanto el contenido como main()el ISR parecen iguales (la misma cantidad de pop/pulsaciones, etc.)

--

Parece que puedo solucionar el problema en este ejemplo concreto con una de las soluciones anteriores, pero todavía me siento incómodo por no entender realmente la causa real y no saber cómo evitar esto en el caso general. Y no sé lo suficiente sobre ensamblaje para analizar el código generado.

Tal vez también debería hacer algunas de estas preguntas:

  • ¿Este tipo de proceso de prueba y error es "normal" cuando se usa C para ATTiny10? (Quiero decir: no hay suficientes recursos y / o soporte de compilador insuficiente para que esto sea confiable, así que no espere que funcione y simplemente vuelva al ensamblaje si no es así).

  • ¿Hay algo que deba evitarse en general (p. ej., no usar vars globales u optimización)?

ACTUALIZAR 3

Gracias por todos los comentarios y respuestas, hay muchas sugerencias útiles en ellos, ¡vale la pena revisarlas todas para cualquiera que tenga un problema similar!

Me quedaba un "misterio" más con mi programa original donde reemplazar a bool ledOn = true;con bool ledOn;era la única solución.

Ahora que entiendo más, eché otro vistazo al ensamblaje generado y al uso de la memoria: resulta que la inicialización hace que el compilador produzca un .datasegmento y se asigna un byte más en la memoria, que está justo por encima del límite para causar una colisión con el pila. Aunque (creo) usa un registro para esta variable al final, al igual que en el caso de "inicialización no explícita", por lo que la asignación adicional no debería ser necesaria. Supongo que el compilador simplemente no tiene optimización para este caso extremo con una cantidad tan pequeña de RAM.

¿Cómo se ve el código desensamblado?
"cambiar el tipo de foo a int" es un buen punto de partida. podría ser que su operador de comparación produzca un código de conversión de tipos que arruine los datos cercanos después de haber sido "optimizados".
Examine el código desensamblado, tanto para la configuración -O0como para -Osla configuración; mi suposición es que una de las banderas de optimización (como -fcaller-saves?) golpea el código que guarda/restaura el estado o los registros de índice dentro de la rutina del servicio de interrupción. Consulte la hoja de datos de Atmel ATtiny10, sección 5.8 Manejo de reinicio e interrupción. Consulte también las opciones de optimización de gcc-4.9.2
¿Cómo es tu mapa de memoria? ATtiny10 tiene solo 32 bytes de SRAM, con un sin firmar, un largo y un int (más cualquier sobrecarga que reclamen las bibliotecas c y el depurador), debe estar llenándose.
@IgnacioVazquez-Abrams: ¡Gracias! Me temo que no estoy seguro de qué buscar, no sé mucho sobre ensamblaje... Eché un vistazo a la salida de avr-objdump -Dy -Spara .hexlos .elfarchivos y puedo ver diferencias, algunas tienen sentido, pero realmente no puedo seguir lo que está pasando. (No quería publicar todo el resultado, pero estoy feliz de hacerlo, ¡o una parte si eso ayuda!)
@Maple Cuando cambio ambos a longs, el problema sigue ahí ... así que no es eso, sino probablemente algo sobre el acceso a estas variables en el ciclo principal. (Sin embargo, estoy desconcertado, ya que definitivamente no estoy escribiendo nada allí...)
@MarkU ¡Gracias por la sugerencia! Investigué un poco más, vea la actualización anterior. Mapa de memoria: lo siento, soy realmente un principiante en esta área, ¿dónde debo buscar el mapa de memoria...? No estoy seguro de que se trate simplemente de intentar asignar demasiada memoria, ya que no creo que se incluyan dependencias adicionales además del mínimo, y no uso un depurador.
Corrección de @Maple (no puedo editar mi comentario anterior): por "no escribir en nada", me refiero a no escribir en nada compartido con el ISR, al menos no intencionalmente ...
@pdenes: ¿sabes cuántos ciclos se necesitan para evaluar ovrf / 500? El ATtiny ni siquiera tiene una instrucción para la multiplicación; por no hablar de la división! Esta prueba se puede implementar mucho más con un microcontrolador. Tal vez se necesiten más ciclos para procesar el ISR de los que ha configurado en el intervalo del temporizador. Por cierto: ¡no veo el temporizador intevrall inicializado!
@Curd: ¡Muy buen punto! ¡Probé esto, vea la segunda actualización anterior! Reintervalo del temporizador: la frecuencia predeterminada de la CPU es 1 Mhz y con un preescalador /8 y el temporizador de 8 bits, la frecuencia del desbordamiento será 1000000/8/256 ~= 488 Hz, lo cual es consistente con lo que estoy viendo cuando funciona correctamente. (No creo que necesite inicializar nada más, ¡pero es posible que haya entendido mal algo!)

Respuestas (2)

Con solo 32 bytes de memoria (como menciona MarkU en un comentario), la memoria del ATtiny10 es increíblemente limitada. El compilador AVR-GCC no proporciona ninguna herramienta para verificar la pila y felizmente generará un código que invadirá la pila. Por ejemplo, esto es lo que generó para el prólogo de su ISR:

000000ba <__vector_4>:
  ba:   1f 93           push    r17
  bc:   0f 93           push    r16
  be:   0f b7           in      r16, 0x3f       ; 63
  c0:   0f 93           push    r16
  c2:   10 e0           ldi     r17, 0x00       ; 0
  c4:   4f 93           push    r20
  c6:   5f 93           push    r21
  c8:   6f 93           push    r22
  ca:   7f 93           push    r23
  cc:   8f 93           push    r24
  ce:   9f 93           push    r25
  d0:   af 93           push    r26
  d2:   bf 93           push    r27
  d4:   ef 93           push    r30
  d6:   ff 93           push    r31

Cuento 13 pushes allí. Eso hará que la pila se expanda a casi la mitad de la memoria de su dispositivo. Combinado con otro rcallen el cuerpo del ISR, así como un par de pushes y rcalls en el prólogo de main, la pila del ISR terminará chocando con la memoria utilizada para almacenar sus variables globales, sobrescribiéndolas con datos inesperados.

El ATtiny10 no es un buen objetivo para un compilador de C. Si su aplicación admite un microcontrolador un poco más grande, es posible que se justifique la actualización a la familia tiny25/45/85. De lo contrario, recomendaría apuntar a este dispositivo con ensamblaje.

O uso juicioso y cuidadoso de ISR_NAKED.
@IgnacioVazquez-Abrams Posiblemente, aunque si no tiene cuidado, eso solo cambiará un conjunto de problemas (ISR causa colisión de pila) por otro (ISR corrompe los registros en uso por main()).
Simulé su código y descubrí que el puntero de la pila iba tan bajo como la dirección de RAM $46 al llamar al código para calcular ovrf/500 en el ISR, ¡que era la misma ubicación que ovrf! Con ovrf>>9, el puntero de la pila solo bajó a $4E.
@BruceAbbott Tiene sentido. La división se implementa mediante una llamada a una función udivdi; eliminar esa llamada convierte a la ISR en una función hoja, lo que podría mejorar la asignación de registros.
¡Gracias @duskwuff! Creo que esto también responde a mis preguntas de "seguimiento" sobre la validez del uso de C.
volatile unsigned int ovrf = 0;
unsigned long foo;
unsigned int bar;

+8 bytes de ram

int main(void) {

+2 bytes o ram, use el atributo ((OS_main))

ISR(TIM0_OVF_vect) {

Si no especifica nada, empuja un marco de pila, para una llamada de función. Eso será alrededor de 14 bytes, si no recuerdo mal de mis problemas con ATTiny10. La dirección de retorno (2 bytes) y algunos registros.

Con solo 32 bytes, puede realizar una llamada de función completa. Si quiere más, necesitará usar __attribute__((naked))y escribir ensamblador.

if ((ovrf / 500) % 2 == 0) {

Esto probablemente llama a funciones de biblioteca, de las cuales no puede almacenar el marco de pila.

Y también hay solo 1024 bytes de memoria de programa. Esto es muy pequeño para C, ~100 líneas pequeñas.