Problema de interrupción faltante con Attiny85

2

Estoy tratando de usar un Attiny85 (digispark) para despertar a otro controlador del modo de suspensión (un ESP8266).

El Attiny está conectado a un receptor IR que tiene una salida baja activa. Básicamente, he conectado una salida del pin Attiny al reinicio en el ESP8266, por lo que cuando se recibe una señal IR, se reinicia el ESP8266, pero luego se ignora el IR futuro hasta que reciba una señal del ESP de que se va a volver a dormir.

El Attiny también debe dormir, pero se activa con las interrupciones de los pines en el pin 0 (IR in) o en el pin 2 (resetEnable desde el ESP para indicar que el ESP se va a dormir y deberá ser despertado).

Tengo un código que funciona a veces, pero parece que la mayoría (pero no siempre) pierde el pulso de Activación de Restablecimiento que el ESP envía para notificarlo antes de que se ponga en suspensión. He verificado con el osciloscopio que el ESP está enviando un pulso alto de 3 ms cada vez que se pone en suspensión, pero al Attiny le falta la mayoría de esos pulsos (es decir, el indicador resetEnable permanece bajo).

La forma en que me imagino que debería funcionar es que la rutina de interrupción determina qué pin lo ha activado, luego, si el pin Restablecer Habilitar (pin 2) sube, establece un indicador, por lo que la próxima baja en el pin IR envíe un pulso de restablecimiento bajo activo al pin ESP RST conectado al pin 3 del Attiny).

El primer impulso IR recibido después del inicio del Attiny siempre restablece el ESP como debería, ya que el indicador resetEnable se establece como verdadero en la configuración (). Esto me hace pensar que la mayoría de mi código está funcionando, excepto la rutina de interrupción para establecer los indicadores resetEnable y resetEsp.

Aquí está mi código, que se compuso principalmente de fragmentos robados de la web:

#include <avr/sleep.h>

#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

int pinIR = 0;
int pinLed = 1;
int pinRSTen = 2;
int pinRST = 3;

volatile bool resetEsp = false;
volatile bool resetEnabled = true;

void setup(){
  pinMode(pinIR,INPUT);
  pinMode(pinLed,OUTPUT);
  pinMode(pinRSTen,INPUT);
  pinMode(pinRST,OUTPUT);
  digitalWrite(pinRST,HIGH); // let the Esp boot

  flash(4,500); // flash the led to show attiny has started

  sbi(GIMSK,PCIE); // Turn on Pin Change interrupt
  sbi(PCMSK,PCINT0); // Which pins are affected by the interrupt
  sbi(PCMSK,PCINT2);
  sei();
}

void loop(){
  system_sleep();
  if (resetEsp) {
    digitalWrite(pinRST, LOW); // reset the ESP
    delayMicroseconds(240);
    digitalWrite(pinRST, HIGH); // let the ESP Boot    
    flash(10, 50); // flash the led fast to show we're waking the ESP
    resetEsp = false; // clear the flags
    resetEnabled = false;
  } else if (resetEnabled) {
    flash(10, 500);  // mostly never get here
  } else {
    flash(2, 500);   // these are the flashes I see most of the time
  }


}

// From http://interface.khm.de/index.php/lab/experiments/sleep_watchdog_battery/
void system_sleep() {
  cbi(ADCSRA,ADEN); // Switch Analog to Digital converter OFF
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Set sleep mode
  sleep_mode(); // System sleeps here
  //sbi(ADCSRA,ADEN);  // Switch Analog to Digital converter ON
}

ISR(PCINT0_vect) {
   if (digitalRead(pinRSTen) == HIGH) {
     resetEnabled = true;  // set the flag so the next IR pulse will reset the ESP
   }
   if (resetEnabled && (digitalRead(pinIR) == LOW)) {
     resetEsp = true;  // reset the ESP
   }
}

void flash(int num, int wait) {
  for (int i=0;i<num;i++) {
    digitalWrite(pinLed, HIGH);
    delay(50);
    digitalWrite(pinLed, LOW);
    delay(wait);
  }
}

Mi código se dispara de manera confiable y emite dos destellos cada vez que le envío un IR. Solo el indicador resetEnable no parece configurarse de manera confiable cuando el ESP envía el pulso alto de 3 ms en el pin 2.

Después de que el código de interrupción funcione de manera confiable, también me gustaría que Attiny ignore los pulsos IR de menos de 320us, ya que el detector de IR parece fallar de vez en cuando por menos de 320us, y no lo hago. quiere que el ESP8266 se despierte en ese caso.

Editar:

<meta> ¡Nunca pensé que algo que parece tan simple podría convertirse en algo tan complicado! Muchísimas gracias a jms y JimmyB por su ayuda, sus comentarios fueron como oro para mí y he aprendido mucho. Es muy difícil depurar un código de interrupción en ejecución de Attiny que realmente no entendí (el primer código de interrupción que he usado), con un solo led con el que comunicarse, y además, mientras parpadea el solo led, en realidad faltan interrupciones! </meta>

Ahora que he dicho eso, aquí están las malas noticias ... Todavía no funciona. :-(

He usado el código que tanto jms como JimmyB han dado, eran casi idénticos.

Lamentablemente, el Attiny todavía no duerme correctamente.

Cuando el pulso resetEnable aparece en PB2, no establece la marca resetEnable, creo. Usando el código de abajo, solo destella el led una vez, igual que cuando IR entra en PB0. La única vez que el led está iluminado de forma fija (para mostrar que resetEnable es verdadero), es cuando el Attiny se inicia por primera vez. Se reiniciará el ESP como debería la primera vez que se introduzca un IR, pero creo que nunca más, ya que resetEnable no se vuelve a configurar.

Si comento la línea sleep_cpu(); , entonces todo funciona perfectamente (pero obviamente el Attiny no está durmiendo). No puedo entender por qué no funciona con el sueño allí.

Aquí está el código exacto que estoy usando ahora:

#include <avr/sleep.h>

#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

int pinIR = 0;
int pinLed = 1;
int pinRSTen = 2;
int pinRST = 3;

volatile bool resetEsp = false;
volatile bool resetEnabled = true;

void setup() {
  pinMode(pinIR, INPUT);
  pinMode(pinLed, OUTPUT);
  pinMode(pinRSTen, INPUT);
  pinMode(pinRST, INPUT); // set as input so ESP can auto-reset itself over USB serial
  //digitalWrite(pinRST, HIGH); // let the Esp boot

  flash(4, 500);
  sbi(GIMSK, PCIE); // Turn on Pin Change interrupt
  sbi(PCMSK, PCINT0); // Which pins are affected by the interrupt
  sbi(PCMSK, PCINT2);
  sei();
}

void loop() {
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Set sleep mode
  cli(); // Disable interrupts to avoid race condition

  if ( !resetEsp && !resetEnabled ) {
    // Only go to sleep if we have nothing to do right now.
    // Safe(*) code from the example in avr-libc:
    sleep_enable();
    sei();
    sleep_cpu(); // if this line is commented out, it all works perfectly.
    sleep_disable();
  } else {
    sei(); // Can go on processing IRQs.
  }

  if (resetEsp) {
    cli();
    resetEsp = false;
    resetEnabled = false;
    sei();
    pinMode(pinRST, OUTPUT); // needed to do this so auto-reset works when programming the ESP
                             // over serial
    digitalWrite(pinRST, LOW); // reset the ESP
    delayMicroseconds(240);
    digitalWrite(pinRST, HIGH); // let the ESP Boot
    pinMode(pinRST, INPUT);
    flash(10, 50); // show that we're resetting the ESP
  } else if (resetEnabled) {
    digitalWrite(pinLed, HIGH); // show that resets are enabled
    //flash(4, 100);
  } else {
    flash(1, 50);
  }
}

ISR(PCINT0_vect) {
  if (resetEnabled && !(PINB & (1 << PB0))) {
    resetEsp = true;
  }
  if ((PINB & (1 << PB2))) {
    resetEnabled = true;
  }
}

void flash(int num, int wait) {
  for (int i = 0; i < num; i++) {
    digitalWrite(pinLed, HIGH);
    delay(50);
    digitalWrite(pinLed, LOW);
    delay(wait);
  }
}

También, jms , ¿te importaría mostrarme exactamente cómo declarar y usar un solo byte de estado? Intenté buscarlo en Google pero no pude encontrar las palabras clave adecuadas para encontrar algo útil. No creo que sea un enum , tal vez un struct ? Como ha dicho, volatile uint8_t espIrRstState con tres valores posibles IR_RST_DISABLED , IR_RST_ENABLED , IR_RST_TRIGGERED .

¡¡Esta cosa está haciendo mi cabeza adentro !! ¡Me encantaría que funcione! Por favor ayúdame para que pueda seguir con mi vida.

    
pregunta localhost

2 respuestas

2

Vas a dormir incondicionalmente en cada iteración de loop() . Eso no es lo que quieres. La forma en que se encuentra su código ahora, se perderá cada evento de activación que ocurre mientras se ejecuta su código de programa normal.

Su código pasa la mayor parte del tiempo retrasando dentro de flash() . Y el controlador se pone en suspensión después de que se haya completado flash() . Siempre. Esto significa que siempre que ocurra un evento de activación mientras el LED está parpadeando, no provocará una activación. Esto se debe a que la interrupción ocurrió y fue reparada antes que se fue a dormir.

Eche un vistazo al ejemplo en avr-libc docs aquí .

Tu código debería tener este aspecto:

void loop(){

  cli(); // Disable interrupts to avoid race condition.

  if ( !resetEsp ) {
    // Only go to sleep if we have nothing to do right now.
    // Safe(*) code from the example in avr-libc:
    sleep_enable();
    sei();
    sleep_cpu();
    sleep_disable();    
  } else {
    sei(); // Can go on processing IRQs.
  }

  if (resetEsp) {

    cli();

    resetEsp = false; // clear the flags
    resetEnabled = false;

    sei();

    digitalWrite(pinRST, LOW); // reset the ESP
    delayMicroseconds(240);
    digitalWrite(pinRST, HIGH); // let the ESP Boot    
    flash(10, 50); // flash the led fast to show we're waking the ESP

  } else if (resetEnabled) {
    flash(10, 500);  // mostly never get here
  } else {
    flash(2, 500);   // these are the flashes I see most of the time
  }

}

El punto aquí es que deshabilitamos todas las interrupciones mientras verificamos si queremos ir a dormir. De esta manera, nos aseguramos de que no se produzcan interrupciones después de que verificamos, pero antes en realidad dormimos. Sin embargo, es crucial que activemos las interrupciones nuevamente antes de irnos a dormir, o nunca volveremos a despertarnos.

Además, @jms tiene razón al afirmar que tienes otra posible codificación de carrera al restablecer tus banderas, así que incluye algunos cli / sei allí también.

set_sleep_mode(SLEEP_MODE_PWR_DOWN); puede ir a setup() . Y si no estás controlando naves espaciales, lo mismo vale para sleep_enable(); , mientras que omites sleep_disable(); por completo.

(*) El código etiquetado como "seguro" arriba no es absolutamente a prueba de balas porque, en teoría, durante la optimización, el compilador podría decidir reordenar algunas instrucciones para que sei() y sleep_cpu() Podría acabar no estando en secuencia directa. Para estar absolutamente seguro, verifique el código del ensamblador generado ( gcc -save-temps ), o simplemente escriba las instrucciones requeridas como ensamblador en línea usted mismo. (Puede ser tan simple como asm volatile (" sei \r\n sleep \r\n " :::); .)

    
respondido por el JimmyB
2

Estoy bastante seguro de que este es un problema de concurrencia clásico que surge al borrar los dos indicadores después de restablecer el ESP.

ATtiny boots, initializes, resetEnabled set 
Main loop starts, ATtiny goes asleep.
IR pulse arrives
ATtiny wakes up, ISR executes, resetEsp set
Main loop code resets ESP                   ==>      ESP wakes up
Main loop code starts flashing LED                   ESP does stuff
ISR executes, doesn't do anything           <==      ESP enables IR reset
Main loop code finishes flashing LED                 ESP goes asleep
ResetEsp and resetEnabled cleared               
ATtiny goes asleep, resetEnabled is never set again  
IR pulse arrives, ISR executes, ESP reset skipped 

A veces, el ATtiny podría funcionar como se esperaba si el ESP toma el tiempo suficiente para hacer su trabajo antes de activar la interrupción.

La solución es simple, borre las marcas antes restableciendo el ESP.

void loop()
{
    system_sleep();
    if(resetEsp)
    {
        cli(); //prevent an interrupt from firing while 
               //..clearing the flags and messing them up
        resetEsp = false; // clear the flags
        resetEnabled = false;
        sei();
        digitalWrite(pinRST, LOW); // reset the ESP
        delayMicroseconds(240);
        digitalWrite(pinRST, HIGH); // let the ESP Boot    
        flash(10, 50); // flash the led fast to show we're waking the ESP
    }
    else
        flash(2, 500);   //reset by IR not enabled 
}

EDITAR:

Como descubrió @JimmyB, hay un segundo descuido tanto en el código de la pregunta original como en mi respuesta original. Si los reinicios de ESP IR están habilitados y llega un pulso de IR mientras el ATtiny está activo , el ATtiny se pondrá en suspensión sin reiniciar el AVR y tomará un segundo pulso de IR superfluo para activar un reinicio el ESP.

Para solucionar esto, el indicador resetEsp debe volver a comprobarse (mientras que las interrupciones están desactivadas) justo antes de quedarse dormido, y la última instrucción antes de que la instrucción de suspensión vuelva a habilitar las interrupciones. De esta manera, si llega un pulso de IR mientras se está comprobando nuevamente el indicador resetEsp, la transición del modo de suspensión se cancelará por la interrupción pendiente.

Este código también debería solucionar ese problema.

void loop()
{
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    cli();
    if(!resetEsp)
    {
        sleep_enable();         //Set the sleep enable bit, allowing the sleep instruction to work.
        sei();                  //Enable interrupts one cycle before falling asleep. If an 
        sleep_cpu();            //..interrupt is pending, the ISR will abort the sleeping process
        sleep_disable();        //Clear the sleep enable bit
    }
    else
        sei();

    if(resetEsp)
    {
        cli();                  //prevent an interrupt from messing with the flags while 
                                //..main loop code is in the process of clearing them
        resetEsp = false;       // clear the flags
        resetEnabled = false;
        sei();
        digitalWrite(pinRST, LOW); // reset the ESP
        delayMicroseconds(240);
        digitalWrite(pinRST, HIGH); // let the ESP Boot    
        flash(10, 50);          // signal that the ESP has been reset
    }
    else
    {
        if(resetEnabled)
            flash(10, 500);     //signal that the ESP has enabled IR resets
        else
            flash(2, 500);      //signal that the IR pulse was ignored
    }
}  

EDIT 2:

Aquí está el código completo de una implementación basada en una sola variable de estado. También modifiqué la forma en que se maneja la línea de reinicio de ESP: ahora, normalmente, se deja que la línea de reinicio flote sin ser impulsada, y se lleva a tierra brevemente cuando se ordena un reinicio.

#include <avr/sleep.h>
#include <cstdint.h>

//IO pin bit definitions
#define IR_IN               0
#define LED_OUT             1
#define RST_ENABLE_IN       2
#define RST_OUT             3

//State definitions
#define IR_RST_DISABLED     0
#define IR_RST_WAITING      1
#define IR_RST_TRIGGERED    2

//State variable
volatile uint8_t state;


void setup()
{
    DDRB = (0<<IR_IN) | 
           (1<<LED_OUT) | 
           (0<<RST_ENABLE_IN) | 
           (0<<RST_OUT); //set RST_OUT as input. This lets the reset line float undriven.

    PORTB |= (0<<RST_OUT);

    flashLed(4,500); // flash the led to show attiny has started

    state = IR_RST_WAITING; //start up with the IR reset enabled

    //enable IR_IN and RST_ENABLE_IN as pin change interrupt sources.
    PCMSK |= (1<<PCINT0) | (1<<PCINT2); 
    GIMSK |= (1<<PCIE);
    sei();
}

void loop()
{    
    switch(state)
    {
    case IR_RST_DISABLED:
    default:
        flashLed(2, 500);   //Signal that an IR pulse was received but ignored.
        break;

    case IR_RST_WAITING:
        flashLed(10, 500);  //Signal that IR Resets are now enabled
        break;

    case IR_RST_TRIGGERED:
        //Clear the state. No risk of race conditions, this takes just a single cycle. 
        state = IR_RST_DISABLED; 

        //Reboot the ESP
        DDRB |= (1<<RST_OUT);  //set RST_OUT as output. This drives the pin low, as the corresponding bit in PORTB is 0
        delayMicroseconds(240);
        DDRB &= ~(1<<RST_OUT); //set RST_OUT as input. This lets the reset line float undriven.

        flashLed(10, 50);   //Signal that the ESP was reset.
        break;
    }

    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    cli();               //disable interrupts so that the state won't change while we are falling asleep
    if(state != IR_RST_TRIGGERED)
    {
        sleep_enable();  //Set the sleep enable bit, allowing the sleep instruction to work.
        sei();           //Enable interrupts one cycle before falling asleep. If an interrupt source occurred after
        sleep_cpu();     //..disabling interrupts, the ISR will now trigger and abort the sleeping process
        sleep_disable(); //Clear the sleep enable bit, locking sleep functionality from accidential use
    }
    sei();               //enable interrupts back again and resume normal code execution.
}

ISR(PCINT0_vect)
{
    if(state == IR_RST_DISABLED)
    {
        if(PINB & (1 << RST_ENABLE_IN))
            state = IR_RST_WAITING; 
    }
    else
    {
        if(PINB & (1 << IR_IN))
            state = IR_RST_TRIGGERED; 
    }
}

void flashLed(int flashCount, int offTime)
{
    while (flashCount-- > 0) {
        PORTB |= (1<<LED_OUT);  //LED output high
        delay(50);
        PORTB &= ~(1<<LED_OUT); //LED output low
        delay(offTime);
    }
}

Si ya no desea que la función de parpadeo del LED, solo puede reemplazar toda la declaración del interruptor con un simple si:

if(state == IR_RST_TRIGGERED)
{
    //Clear the state. No risk of race conditions, this takes just a single cycle. 
    state = IR_RST_DISABLED; 

    //Reboot the ESP
    DDRB |= (1<<RST_OUT);  //set RST_OUT as output. This drives the pin low, as the corresponding bit in PORTB is 0
    delayMicroseconds(240);
    DDRB &= ~(1<<RST_OUT); //set RST_OUT as input. This lets the reset line float undriven.
}  

La eliminación de la funcionalidad de parpadeo también le permitiría implementar una depuración simple con el LED: alternar el estado del LED cuando se ejecuta una determinada pieza de código.
Por ejemplo, podría cambiar el LED cada vez que el pin RST_ENABLE_IN haya cambiado de estado, lo que le permitirá determinar si la línea de habilitación de reinicio alguna vez activa la interrupción en primer lugar

ISR(PCINT0_vect)
{
    if(PINB & (1 << RST_ENABLE_IN))
        PORTB ^= (1 << LED_OUT); //XOR the LED output with itself, toggling its state.
    ...
    
respondido por el jms

Lea otras preguntas en las etiquetas