Avr-gcc no compila correctamente sin optimizaciones, pero funciona (mal) con -Os

Estoy desarrollando software para attiny88 con la cadena de herramientas avr-gcc. Este es un microcontrolador económico con memoria de programa de 8kB, SRAM de 512B y memoria de datos EEPROM no volátil de 64B. Todo esto es suficiente para la tarea que se supone que debe manejar. No tengo un depurador para este chip.

El dispositivo actúa como esclavo SPI y permite al maestro leer/programar la EEPROM y leer el estado del dispositivo (estado de algunas entradas analógicas y salidas digitales). El protocolo es simple: el primer byte de datos lleva la instrucción donde los dos bits más altos codifican la acción requerida (00-nada, 01-escribe eeprom, 10-lee eeprom, 11-lee estado) y el resto es dirección. El segundo byte siempre es cero, el tercer byte es el valor para leer o escribir, el cuarto byte siempre es cero.

El problema es que obtengo un comportamiento extraño del compilador. Es difícil poner un dedo en lo que está pasando, así que solo daré algunos ejemplos. Lo más llamativo es que sin la optimización -Os el dispositivo no responde sobre SPI. Esto no se debe a ninguna de las razones obvias: el programa se ajusta a la memoria y la pila no debe ejecutarse en secciones .bss/.data (el programa tiene ~700B, la SRAM está asignada entre 0x100 y 0x2ff, donde .bss_end está en 0x109; el montón está vacío; no hay llamadas de función anidadas, ni interrupciones anidadas).

Si enciendo la optimización -Os, entonces el programa responde según lo previsto. Aquí está el código de trabajo para manejar el ISR:

unsigned char state[8];
volatile unsigned char data;

ISR(SPI_STC_vect)
{
  switch(data>>6) {
  case 0: 
    data = SPDR;
    break;
  case 1:           /* write eeprom */
    while(EECR & (1<<EEPE));
    EECR = (0<<EEPM1)|(0<<EEPM0);
    EEARL = 0;
    EEDR = SPDR;
    EECR |= (1<<EEMPE);
    EECR |= (1<<EEPE);
    data = 0;
    break;
  case 2:               /* read eeprom */
    EEARL = data & 0x1f;    /* with 0x3f stops working (???) */
    EECR |= (1<<EERE);
    SPDR = EEDR;
    data = 0;
    break;
  case 3:           /* read state */
    SPDR = state[data&7];
    data = 0;
    break;
  }
}

Sin embargo, el programa se rompe cuando se escribe de forma semánticamente diferente:

  • Si cambio la línea "EEARL = data & 0x1f;" a "EEARL = data & 0x3f;", lo cual es deseable ya que permitiría abordar todo el espacio de direcciones de EEPROM, la escritura/lectura de ERPROM deja de funcionar (no necesito 64B completo, así que lo dejé como está)

  • La lectura de estado (caso 3) se interrumpe si reemplazo la línea "SPDR = state[data&7];" con una construcción switch-case que devuelve el valor del registro PORTD/PORTB cuando la dirección es 0 y 1 respectivamente (la solución actual es mantener el estado [0] y el estado [1] sincronizados con PORTB/PORTD en el bucle principal).

¿Me estoy perdiendo algo importante? Me parece que el compilador se estropea, pero no he encontrado ningún informe de error para avr-gcc o avr-libc (¿newlib?) que se ajuste a la factura.

La cadena de herramientas se instaló desde el paquete gcc-avr actual en los repositorios de aptitude. El archivo MAKE es bastante básico:

all: main.hex main.s

main.elf: main.o
    avr-gcc -g -mmcu=attiny88 -o main.elf main.o

main.s: main.elf
    avr-objdump -d main.elf > main.s

main.hex: main.elf
    avr-objcopy -O ihex  main.elf main.hex

main.o: main.c
    avr-gcc -mmcu=attiny88 -Os -c main.c -Wall

ACTUALIZACIÓN : sigue siendo el mismo resultado con los últimos binutils, gcc y avr-libc (recompilado de las fuentes)

Intente leer/comparar el código de máquina generado.
¿ Dónde inicializas la datavariable? ¿Está confiando en la inicialización estática para darle "cero"? Eso siempre es una mala idea, ya que los microcontroladores a menudo se desvían del estándar C allí, para brindar una puesta en marcha más rápida.
Dicho esto, la razón más común por la que los programas dejan de funcionar entre diferentes niveles de optimización es que en algún lugar de su programa, tiene algún tipo de comportamiento indefinido. Es decir, es probable que tenga un error inactivo que apareció cuando cambió la compilación. No necesariamente en el código publicado.
@Lundin Inicialicé todo durante el inicio, incluso las preguntas frecuentes de avr-libc indican que inicializan variables ( nongnu.org/avr-libc/user-manual/FAQ.html#faq_varinit ), no confío en eso. La sección .data está vacía.
Hace años, ciertas rutinas en avr-libc eran problemáticas es -O0. Me pregunto si este es el caso (por ejemplo, consulte avrfreaks.net/forum/optimization-levels-avr-studio )
@PlasmaHH Gracias por la sugerencia, se me ocurrió que puedo diferenciar el desmontaje cuando solo cambio una constante de 0x1f a 0x3f como se explica en la pregunta. Las diferencias son enormes: prácticamente todo el programa es diferente. Sin embargo, parece incorrecto: por ejemplo, la instrucción cli (deshabilitar interrupciones) desapareció de la rutina del controlador de interrupciones
@Lundin Ves la mayor parte del programa, el resto es solo el inicio de todas las variables compartidas, el inicio de SPI y un ciclo infinito. El programa completo se ve diferente, pero lo estoy probando en esta versión mínima que reproduce los errores. No dejaré basura en la memoria y preinicializaré todo manualmente. Es un programa simple después de todo.
@Damien eso podría explicar por qué no funciona sin -Os (aunque me gustaría saber los detalles de por qué falla -O0), pero aún no explica los errores que describo. Acerca de la palabra clave volátil: los datos son volátiles cuando los escribo desde el ISR, el estado solo se lee desde el ISR, por lo que no es necesario que sea volátil.
Hacer tiempo tomando cosas (como escribir EEPROM) dentro de un controlador de interrupción (recordando cada evento de recepción SPI) definitivamente trae mala suerte.
@carloc Leer EEPROM detiene la CPU durante 4 ciclos, la escritura se realiza de forma asíncrona con autómatas autónomos en el chip; no bloquea la CPU. El problema no está ahí.
Observación aleatoria #1: también debe hacerlo while(EECR & (1<<EEPE));antes de leer la eeprom. Observación #2: No veo cómo funciona el protocolo de 4 bytes que describiste con tu ISR - Ejemplo: eeprom write; el primer byte se leerá como comando/dirección, el segundo byte (cero) activará la escritura real y escribirá 0 en eeprom, el tercer byte (datos) se interpretará nuevamente como un comando. Eso no coincide con la intención descrita...
@marcelm No necesito hacer el spinloop siempre que sepa que nadie programará la EEPROM justo antes de leerla. El spinloop cuando se escribe está allí solo como un mecanismo de seguridad contra la corrupción de la memoria y nunca debería girar. Acerca del programa: tenga en cuenta que la interrupción se activa cuando finaliza la transferencia de un byte, mientras que el siguiente byte ya puede estar en camino. Por eso existe el acolchado y por eso funciona. Es rápido, simple y requiere solo una variable global.
@marcelm Mis disculpas, escribí en la publicación que la escritura se realiza con el tercer byte, eso es incorrecto: el byte de datos para escribir en EEPROM es el segundo. Los datos de lectura todavía se desplazan con el tercer byte.

Respuestas (1)

Deberá inspeccionar la salida de avr-objdumppara ver qué instrucciones exactas se generaron para su código. Por cierto, sería útil incluir su código C en el desmontaje a través de avr-objdump -S main.elf > main.s. Dudo que todo el programa se vuelva diferente cuando reemplaza la 0x1Fconstante por 0x3F, aislar las diferencias en la lista y analizarlas cuidadosamente sería su próximo paso.

Tal análisis es lo más lejos que puede llegar sin las herramientas de desarrollo adecuadas. Obtener un depurador o un simulador le ahorraría mucho esfuerzo que se requiere cuando está limitado al análisis de listas estáticas.

PD: Supongo que aquí no recibe ninguna advertencia del compilador durante la compilación. Si tiene alguno, arreglarlos debería ser su primera prioridad. Los compiladores modernos hacen un trabajo bastante bueno al informarle sobre errores sutiles que pueden ser difíciles de encontrar de otra manera.

No hay advertencias con -Wall; Simulavr/debugger podría ser útil, pero dado que tengo una versión del software que funciona de manera repetible, no es un problema en el chip. Acerca de las diferencias debido al cambio de una constante de 0x1f a 0x3f: Diff marcará la mayor parte del código como una reescritura. Se reduce a una variable adicional en la sección .bss y cambios en las compensaciones en el programa que cambian los literales de direccionamiento. Es extraño, pero el programa es estructuralmente diferente: parece que el optimizador decide usar una variable adicional para acelerar las cosas con 0x3f constante.
@student intenta producir listados de ensamblador a partir del compilador, es decir avr-gcc -mmcu=attiny88 -Os -S main.c, . Eso debería dejar los literales de dirección en forma simbólica ( label_0001etc.), por lo que producir una diferencia debería ser más fácil.