Sondeo de varios botones desde una interrupción

Estoy tratando de sondear 3 botones diferentes desde una interrupción de tiempo de espera de vigilancia, en un ATtiny13.

Mi código funciona perfectamente para botones individuales, sin embargo, parece que no puedo sondear los 3 en un bucle. Todos los botones están asociados a un número arbitrario para su identificación en 0x01,0x02 y 0x04.

Por ejemplo, este código sondea el botón 0x02 y funciona bien:

ISR (WDT_vect){
if (debounce(0x02)==1){
    PORTB ^= _BV(PB1);//flip led 1
}}

Sin embargo, si trato de sondear los 3 en un bucle, parece que no se produce ninguna detección. En este ejemplo, simplemente alterno el mismo LED para los 3 botones:

ISR (WDT_vect){     
    for (int d=0x01;d<0x04;d<<1){
    if (debounce(d)==1){
        PORTB ^= _BV(PB1);//flip led 1
    }}}

Apilados if-else tampoco funcionan:

ISR (WDT_vect){

if (debounce(0x02)==1){
    PORTB ^= _BV(PB1);//flip led 1
} else  if (debounce(0x04)==1){
    PORTB ^= _BV(PB3);//flip led 2
}
}

El resto del código relevante abreviado para mayor claridad:

/***
 * curbtn: one of 0x01,0x02,0x04, matches mute, vol+,vol-
 * returns 1 if the button is considered pressed
 */
uint8_t debounce(uint8_t  curbtn){
static uint8_t button_history = 0;
uint8_t pressed = 0;
button_history = button_history << 1;
button_history |= read_btn(curbtn);
if ((button_history & 0b11000111) == 0b00000111) {
    pressed = 1;
    button_history = 0b11111111;
}
return pressed;
}
/**
 * sets up ports to read a specific button
 * returns 1 if the selected button is pressed
 */
uint8_t read_btn(uint8_t  curbtn){
uint8_t ret=0x00;
if (curbtn==0x01){
    DDRB &=~_BV(PB2);//PB2 en entree
    PORTB |=_BV(PB2);//pull-up actif
    ret= ( (PINB & _BV(PB2)) == 0 );
} else if (curbtn==0x02){
    DDRB |=_BV(PB2);//PB2 en sortie
    PORTB &=~_BV(PB2);//PB2 a 0
    DDRB &=~_BV(PB0);//PB0 en entree
    PORTB |=_BV(PB0);//pull up sur PB0
    ret= ( (PINB & _BV(PB0)) == 0 );
} else if (curbtn==0x04){
    DDRB |=_BV(PB0);//PB0 en sortie
    PORTB &=~_BV(PB0);//PB0 a 0
    DDRB &=~_BV(PB4);//PB4 en entree
    PORTB |=_BV(PB4);//pull up sur PB4
    ret= ((PINB & (1<<PB4)) == 0);//lecture de PB0
}
return ret;
}

Este es el diseño del circuito:ingrese la descripción de la imagen aquí

Me gustaría saber si voy en la dirección correcta y de qué manera se debe corregir mi código de sondeo.

¿Esquema de cómo se unen los botones?
¿Por qué prefiere sondear en lugar de usar una interrupción de cambio de pin?
Publicación de @bigjosh editada para incluir el esquema Mi circuito necesita cambiar activamente el estado de los pines alrededor de un interruptor. Por ejemplo, para habilitar una lectura en vol+ después de silenciar, PB2 debería convertirse en una salida baja. Tal como lo entiendo, no estoy seguro de que una interrupción de cambio de pin sea apropiada ya que ya estoy actuando en los estados de los pines. La parte del interruptor es un diseño cerrado que estoy reutilizando, de ahí el diseño exótico.
¿Hace debounce()un seguimiento del estado del botón de una invocación a la siguiente? Si es así, ¿puede rastrear múltiples botones diferentes o solo un botón?
@kkrambo Incluí la función completa, probablemente haya encontrado el problema. Sí, mantiene el valor de una invocación a otra, pero no es apropiado para más de un botón. Supongo que el siguiente paso sería implementar correctamente un button_history_xxpara cada botón.
Tiene una sola variable estática (button_history) en la función de rebote, intenta usar para todos los botones "juntos", ¿no es así? Entonces, al recorrer los botones, obtienes bits intercalados de los tres btns y nunca rebota, supongo.
Este es un código muy oscuro para algo tan simple. Su función "read_btn" parece principalmente enfocada en destruir el contenido de todos los registros de E/S, por razones desconocidas. El código simplemente no tiene ningún sentido. Además, los MCU de Atmel son bastante malos para conducir cosas directamente desde los pines de E / S ... ¿estás seguro de que puedes obtener unos 10-20 mA de esos pines?
@Lundin Tonterías. Con una capacidad de suministro (AMR) de 60 a 80 mA por pin, no debería quejarse de los controladores de salida. Conducir 20mA a través de un LED nunca ha sido un problema con los AVR.
@Martin Creo que ese fue el problema. He agregado variables de historial separadas para cada botón, pero aún no puedo apilar correctamente las pruebas de sondeo individuales para cada botón en el ISR. Esta vez solo parece evaluarse el último (en lugar de ninguno)
@JimmyB Esta es mi experiencia al usar Atmel, aunque no he usado AVR sino solo sus partes ARM. En cuanto a los detalles de esta parte particular ATtiny13, el manual está mal escrito. Aparentemente, encuentra la corriente nominal entre las líneas donde enumeran la salida de bajo / alto voltaje. Supuestamente, este pin puede manejar -20mA a +20mA, por lo que debería estar bien.
@Lundin Eso pensé. Pero los AVR siempre han tenido controladores de salida comparativamente fuertes. Consulte, por ejemplo, la hoja de datos de Tiny13, página 135, figura 19-25.

Respuestas (2)

Progresó mucho, hubo 2 problemas.

Como sugirieron Martin y kkrambo, existía el problema de rastrear correctamente el estado de los 3 botones. El código que publiqué mantuvo una variable de historial estática. Ahora el código principal incluye 3 variables de historial diferentes.

uint8_t mute_history=0;
uint8_t volp_history=0;
uint8_t volm_history=0;

uint8_t read_btn(uint8_t  curbtn){
uint8_t ret=0x00;
if (curbtn==0x01){
    DDRB &=~_BV(PB2);//PB2 en entree
    PORTB |=_BV(PB2);//pull-up actif
    nop();nop();nop();nop();
    ret= ( (PINB & _BV(PB2)) == 0 );
} else if (curbtn==0x02){
    DDRB |=_BV(PB2);//PB2 en sortie
    PORTB &=~_BV(PB2);//PB2 a 0
    DDRB &=~_BV(PB0);//PB0 en entree
    PORTB |=_BV(PB0);//pull up sur PB0
    nop();nop();nop();nop();
    ret= ( (PINB & _BV(PB0)) == 0 );
} else if (curbtn==0x04){
    DDRB |=_BV(PB0);//PB0 en sortie
    PORTB &=~_BV(PB0);//PB0 a 0
    DDRB &=~_BV(PB4);//PB4 en entree
    PORTB |=_BV(PB4);//pull up sur PB4
    nop();nop();nop();nop();
    ret= ((PINB & (1<<PB4)) == 0);//lecture de PB0
}
return ret;
}

uint8_t debounce(uint8_t  *button_history,uint8_t  curbtn){
uint8_t pressed = 0;
*button_history = *button_history << 1;
*button_history |= read_btn(curbtn);
if ((*button_history & 0b11000111) == 0b00000111) {
    pressed=1;
    *button_history = 0b11111111;
}
return pressed;
}


ISR (WDT_vect){

if (debounce(&volp_history,0x02)==1){
    PORTB ^= _BV(PB3);//flip led 2
}

if (debounce(&mute_history,0x01)==1){
    PORTB &= ~_BV(PB1);//turn off
    PORTB &= ~_BV(PB3);//turn off
}

if (debounce(&volm_history,0x04)==1){
    PORTB ^= _BV(PB1);//flip led 2
}

}

El segundo problema fue el del tiempo, que me sugirió el usuario Tom Carpenter en este hilo . Sus comentarios fueron:

@Polyphil intente agregar algunas instrucciones de nop. Puede usar lo siguiente: #define nop() __asm__ __volatile__ ("nop \n\t"), y luego en su código haga nop();nop();nop(); justo antes de hacer la declaración de devolución.

@Polyphil, las entradas en ATTiny tienen una latencia de dos ciclos de reloj debido a una cadena sincronizadora, por lo que se necesitan al menos 2-3 ciclos de reloj después de cambiar el valor de pull-up antes de que se refleje en el registro PIN. Agregar un nop hace que el procesador espere un ciclo de reloj.

de ahí los nops().

El circuito se comporta como se esperaba en este momento.

¿Con qué frecuencia llama al WDT? ¿Por qué no elegir un precaller WDT para no tener que eliminar rebotes en absoluto? Si solo verifica los botones cada (digamos) 50-100 ms, entonces deberían rebotar y su código puede ser tan simple como simplemente verificar cada uno de los botones secuencialmente y actuar sobre el estado.

No he cambiado la configuración del prescaler. Suponiendo que el valor predeterminado es sin escalado previo, debería ser cada 16 ms de acuerdo con la hoja de datos. Me gusta cómo reacciona el sistema con esta función antirrebote en particular, pero no veo cómo su idea no funcionaría. La diferencia sería que mantener el botón presionado se leería como una nueva pulsación cada vez que se agote el tiempo de espera del WDT.
@bigjosh Tiene que muestrear al menos dos veces para eliminar el rebote, o su resultado puede ser inconsistente si muestrea mientras se produce el rebote.
@jimmyb si muestrea a una velocidad más lenta que el tiempo de rebote máximo y más rápido que el tiempo de subida/bajada más corto, entonces siempre verá todos los cambios de estado y no verá ningún rebote. En la práctica, si marca un botón una vez cada 50 ms, entonces .ISS los rebotes.
@polyphil Intente aumentar el prescaler un paso o dos y apuesto a que dejará de ver rebotes. En la práctica, creo que es poco probable que pueda sentir algún cambio en la capacidad de respuesta.