Gestión de puertos de entrada salida digitales
Nos centraremos en los módulos IO de los procesadores AVR. Otros procesadores tienen ligeras diferencias, pero en general son muy similares.
Descripción general:
El módulo IO presenta la siguiente disposición
Un registro DDRx (Data Direction Register)
determina si el puerto es de entrada o de salida
Un registro PORTx (Pin Output Register) determina si el puerto presenta un valor alto o bajo cuando el puerto es de salida, y además controla si hay un pull-up cuando es de entrada Un registro PINx (Pin Input Register) nos permite leer el estado del puerto.
(x = A,B,C…)
Si queremos efectuar una salida por un pin determinado debemos poner primero ese pin como salida y escribir el valor deseado en él.
Si queremos que un pin actúe como entrada, primero lo configuramos como entrada (bien en alta impedancia o con pull-up interno) y después leemos el estado del pin.
Para efectuar estas acciones tenemos dos opciones:
Con funciones de biblioteca
Accediendo directamente a los registros en el espacio de direccionamiento
Si necesitamos ahorrar espacio, queremos hacer cambios a alta velocidad o cambiar varios pines simultáneamente usaremos esta alternativa.
El pin 12 se correponde con el bit 4 del puerto B
El DDRB es el registro en el que tenemos que que configurar el bit 4 como salida (a 1) → DDRB | 0b00010000
o como entrada (a 0) → DDRB & 0b111101111 DDRB bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
PORTB bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
PINB bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
Después si es salida tenemos que escribir en el bit 4 de PORTB el valor deseado (1 para alto, 0 para bajo)
En el caso de que fuese entrada tendríamos que decidir si queremos pull-up en ese pin (escribiendo un 1 o un 0 en PORTB) y leer el bit 4 de PINB
// escribiendo en un pin
void escribe_pin(int pin, int estado) {
pinMode(pin, OUTPUT);
digitalWrite(pin , estado);
}
// leyendo un pin int lee_pin(int pin) {
// pinMode(pin, INPUT_PULLUP);
pinMode(pin, INPUT);
return digitalRead(pin);
}
// escribiendo en un pin en el port b
void escribe_pin_portb(int bit, int estado) {
DDRB = DDRB | (1 < bit);
PORTB = (estado)? (PORTB | (1 < bit)) : (PORTB & ~(1 < bit));
}
// leyendo un pin del port b int lee_pin(int pin) {
DDRB = DDRB & ~(1 < bit);
// PORTB = PORTB | (1 < bit); // con pull-up PORTB = PORTB & ~(1 < bit); // sin pull-up return (PINB & (1 < bit));
}
Comparando los dos métodos:
Vamos a comparar dos programas en el arduino, ambos haciendo parpadear un pin a máxima velocidad. Uno será implementado con funciones de biblioteca y otro directamente.
Comprobaremos la máxima frecuencia alcanzada y el tamaño del programa
Frecuencia de blink: 66.666 Kh → período 15µs Tamaño: 722 bytes
Frecuencia de blink: 833.333 Kh → período 1.2µs Tamaño: 438 bytes
/*
blink un led a través de funciones de biblioteca
*/
void setup()
{ pinMode(12, OUTPUT);
}
void loop() {
while(1) {
digitalWrite(12 , HIGH);
digitalWrite(12 , LOW);
} }
/*
manejo de un port directamente
*/
void setup() {
// pinMode(12, OUTPUT);
DDRB =DDRB | 0x10; // pin12=PB4 0x10= 0b00010000 = 16 // mejor así
// DDRB |= 0x10 // o todavía mejor // DDRB |= (1 << 4) }
void loop() {
while(1) {
// digitalWrite(12 , HIGH);
PORTB |= 0x10;
// mejor asi
// PORTB |= (1 << 4) // digitalWrite(12 , LOW);
PORTB = PORTB & 0b11101111;
// o también PORTB &= (~(1 << 4)) }
}
Uso de interrupciones para gestionar puertos
Volvamos a examinar el código ya visto para manejar un led a través de un pulsador usando interrupciones (aquí hacemos uso de las interrupciones externas disponibles para los pines 2 y 3)
int ledPin = 9;
int buttonPin = 2;
void pulsador_cambio_estado() {
// si esta variable fuese global y accesible desde otro lado habría que declararla volatile int estado = !digitalRead(buttonPin);
digitalWrite(ledPin, estado);
}
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
// attachInterrupt(digitalPinToInterrupt(buttonPin), pulsador_cambio_estado, CHANGE);
attachInterrupt(0, pulsador_cambio_estado, CHANGE);
}
void loop() {
// hacer otras cosas...
delay(3000);
// ...
}
Interrupciones mediante acceso a registros
Veamos como implementar este mismo código pero accediendo directamente a los registros. El procedimiento era:
• Escribimos el código de la interrupción denominando adecuadamente la función según el tipo de interrupción (en este caso ISR(INT0_vect))
• En el setup configurar todos los periféricos como deseemos (entradas, salidas, …)
• Si es necesario dehabilitamos de forma general las interrupciones: cli()
• Habilitamos el bit de interrupción correspondiente a los eventos que deseamos interceptar
• Borramos cualquier aviso de interrupción pendiente. (puede no ser necesario, ya que se borra automáticamente cuando se procesa la interrupción)
• Habilitamos el bit general de interrupción Apliquemos la secuencia:
Para detectar interrupciones externas en el pin 2 o 3 tenemos que trabajar con los siguientes registros del procesador:
EICRA (External Interrupt Control Register A) En este registro seleccionamos el tipo de interrupción deseada (LOW, CHANGE, FALLING, RISING) según la siguiente tabla
7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
EICRA - - - - ISC11 ISC10 ISC01 ISC00
ISCx1 ISCx0 DESCRIPTION
0 0 Low
0 1 change
1 0 falling
1 1 rising
EIMSK (External Interrupt Mask Register) Para habilitar / deshabilitar la interrupción externa INT0 o INT1
7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
EIMSK - - - INT1 INT0
EIFR(External Interrupt Flag Register) queda registrada que interrupción se disparó (la 0 o la 1)
7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
EIFR - - - INTF1 INTF0
Primero deshabilitamos de forma general las interrupciones (En este caso no sería neceario y se hace sólo por motivos didácticos) con cli()
Debemos poner EICRA a “----01--” (- significa dejar el bit como está)
EICRA &= ~3; // clear existing flags
EICRA |= 1; // set wanted flags (change level interrupt)
Después habilitar la interrupción externa en pin 2 poniendo “---1” en EIMSKy dejando el resto como está
EIMSK |= 1; // enable it
Por si acaso borramos el flag de que se disparó la interrupción (por si estaba marcado de antes). Para ello ponemos a 1 el bit 0 de EIFR
EIFR |= 1
Ahora ya podemos habilitar de forma general las interrupciones con sei() El código nos quedaría de esta forma ISR(INT0_vect)
/*
Ejemplo de interrupciones externas en el pin 2 Usando la interrupción INT0
*/
int ledPin = 9;
int buttonPin = 2;
ISR(INT0_vect) {
// si esta variable fuese global y accesible desde otro lado habría que declararla volatile int estado = !digitalRead(buttonPin);
digitalWrite(ledPin, estado);
}
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
// des activar el flag general de interrupciones cli();
// queremos que se active interrupción cuando cambie el pin 2 EICRA &= (0b11111100);
EICRA |= 1;
// limpiamos el flag de aviso de cambio en en pin 2 // en este caso no sería imprescindible.
EIFR |= 0b00000001;
// activamos flag de cambio en el pin 2 EIMSK |= 0b00000001;
// activar el flag general de interrupciones sei();
}
void loop() {
// hacer otras cosas...
delay(3000);
// ...
}
Interrupciones generales del puerto por cambio de nivel
En el arduino tenemos disponibles además de las interrupciones externas en el pin 2 y 3,
interrupciones por cambio de nivel en cualquier puerto. Estas interrupciones sólo son accesibles a través de acceso directo a registros.
Se pueden configurar las interrupciones PCINT0 (puerto B), PCINT1 (puerto C), PCINT2(puerto D)
Vamos a implementarlos en este montaje:
Es muy parecido al anterior ejemplo, pero en este caso el pin que tendría que disparar la interrupción es el 7, que se corresponde con el puerto D bit 7 (D7)
Veamos los resgistros implicados:
PCICR Pin Change Interrupt Control Register
Es donde podemos habilitar las interrupciones por cambio de estado de un pin en un puerto. (PCIE0 para el puerto B, PCIE1 para el puerto C y PCIE2 para el puerto D)
Para nuestro ejemplo debemos poner a 1 el bit PCIE2 PCICR |= 0b00000100;
y la interrupción asociada debería llamarse:
ISR(PCINT2_vect)
Llegados aquí tenemos activadas las interrupciones por cambio en el puerto D. Ahora tenemos que decir que pines concretos del puerto D pueden disparar la interrupción. Esto lo hacemos en el registro PCMSK2 (para el puerto B tenemos el PCMSK0 y para el C el PCMSK1)
En nuestro caso nos interesa el D7 que se corresponde con el PCINT23 PCMSK2 |= 0b10000000;
Si hubiese mas pines que no interesasen los trataríamos de la misma manera.
Cuando se dispara la interrupción se marcará en PCIFR el bit que corresponde al puerto D
En nuestro caso comprobaríamos que está a 1 el bit PCIF2
El pin concreto que provocó la interrupción no tenemos forma de saberlo, solo podemos ir leyendo todos los pines y actuar en consecuencia.
int ledPin = 9;
int buttonPin = 7;
// pin7 es D7
// pin change interrupt for D0 to D7 ISR(PCINT2_vect)
{
// si esta variable fuese global y accesible desde otro lado habría que declararla volatile int estado = !digitalRead(buttonPin);
digitalWrite(ledPin, estado);
}
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
// des activar el flag general de interrupciones cli();
// activamos flag de cambio en el puerto D PCICR |= 0b00000100;
// queremos que se active interrupción cuando cambie el pin D7 PCMSK2 |= 0b10000000;
// limpiamos el flag de aviso de cambio en en port D PCIFR |= 0b00000100;
// activar el flag general de interrupciones sei();
}
void loop() {
// hacer otras cosas...
delay(3000);
// ...
}
El problema RMW (Read-Modify-Write)
En algunos microcontroladores existe una situación peligrosa, aunque raramente se produce, en la que el resultado de escribir en un puerto es distinto del esperado. En el siguiente ejemplo
supongamos un condensador de gran capacidad puesto en el pin 7 del port B y veamos lo que pasa al escribir un 1 en ese pin e inmediatamente hacer una lectura para una nueva escritura en otro bit PORTB |= (1<<7);
PORTB |= 1;
Este problema se presenta en algunos microcontroladores PIC y tiene dos soluciones:
• Un pequeño timeout entre las dos escrituras
• Uso de latches internos para leer el estado en vez de hacerlo directamente del port
Este problema no se da en los procesadores AVR ya que implementan la última solución apuntada (son registros distintos para leer y escribir: PINx y PORTx)
Hay otra circunstancia que genera un problema muy parecido: El cambio del valor del pin en una interrupción.
Imaginemos que queremos ejecutar la instrucción PORTB |= 1;
Esta instrucción normalmente no es atómica y para ejecutarse en ensamblador se divide en tres instrucciones:
1. leer el puerto a registro interno de trabajo 2. poner a 1 el bit 0 del registro interno 3. escribir el registro interno en el puerto.
Si tenemos una interrupción que también altere el mismo puerto (aunque sea otro bit) y la
interrupción se dispara después de la fase 1 y antes de la fase 3 el valor que tenemos en el registro de trabajo no se corresponde con el valor real del puerto del puerto.
Esto se pone de manifiesto en la siguiente tabla
Sentencia Acción Registro interno PortB
PORTB |= 1; xxxxxxxx 10000000
Leer portb a
RI 10000000 10000000
Set bit 0 en RI 10000001 10000000 Interrupción y
efectuamos PORTB |= 2;
10000010
Escribir RI 10000001 10000001
Siguiente sentencia …. …. ...
Realmente esta es una situación muy improbable y normalmente sólo la tendremos en cuenta en sistemas críticos. Una solución pasa por inhibir las interrupciones cuando manipulamos el puerto.